vue源码阅读—06—响应式原理(三)之 props
props主要三大块,
- 规范化的流程,
- 初始化的流程,
- 子组件两次不同更新的流程
一、props的规范化
1.1概念:规范化只是格式上的规范化,确保我们的props是一个对象,对象的key的value又是一个对象; 后续会在props的初始化里进行props语法正确与否的校验;
1.2流程:
规范化主要发生在这几个流程中:
<script> const A = { name:'A', props:['name','nick-name'], template:` <div id="dependencyDepend"> <p> name: {{name}}</p> <p> nickName: {{nickName}}</p> </div> `, } const B = { name:'B', template:` <div id="dependencyDepend"> <p> age: {{age}}</p> <p> sex: {{sex}}</p> </div> `, props:{ age:Number, sex:{ type:String, default:'female', validator(value){ return value === 'male' || value === 'female' } } } } new Vue({ el: "#app", components:{ A,B, }, template:` <div id="dependencyDepend"> <A name='kobe' nickName='james'> </A> <B :age='18' sex='mail'> </B> </div> ` }); </script>
在根vm实例init时,会进行Vue.opstions和自己写的options合并,所以会调用mergeOptions函数;
然后这个函数又会调用normalizeProps;并且把我们定义的props配置项和根vm实例传递进去;
然后就是把我们自己写的props配置项规范化成对象的形式;方便后续mergeOptions;
因为vue提供了非常灵活书写props的方式,但是最终vue处理的时候,都要把props转化成对象的形式;
二、props的初始化
initProps主要做三件事,
第一件事校验;
第二件事响应式;给vm._props对象的每个key都添加响应式的getter和setter;
第三件事代理; 把vm._props代理到vm实例上;
2.1props初始化之校验
校验又分为三个步骤,
1.Boolean类型的特殊处理
2.默认值的处理
3.断言: 检查我们最终得到的props对象的每个key对象的value属性是否和type属性匹配,如果不匹配,说明这个props无效;
为什么后面又需要校验 ,前面不是已经规范化了吗?
前面的规范化只是格式上的规范化,确保我们的props是一个对象,对象的key的value又是一个对象;
但是格式对了,语法并不一定对。
比如:
props:{ age:{ type:'djldsadaf', default:'18', } }
这里面的type属性的值就是错的,所以需要验证;而且还有就是如果父组件给子组件传递的propsData里没有数据是undefiend,那么初始化会帮助我们去已经规范化过的props对象里找default默认值,然后作为props的value;
所以,props的初始化主要是
1.取值:Booklean类型的特殊处理和默认值的处理,做得都是一件事,那就是确定props的value到底是什么,到底是父组件给子组件的propsData上取值,还是根据我们自定义的props配置项上的default上取值?
2.断言: 检查我们最终得到的props对象的每个key对象的value属性是否和type属性匹配,如果不匹配,说明这个props无效;
2.2props初始化之响应式
2.3props初始化之代理
三、props的更新
为什么prepatch函数内,vnode.componentOptions可以获取到本组件传递给子组件的propsData?
方式:
我们的模板编译成render函数,然后调用render函数时,会生成占位符vnode,父组件传递给子组件的数据,比如<child value='hello' foo='bar'>, 都会保存在占位符vnode.data中;
然后通过extractPropsFromVNodeData()获取vnode的propsData;
然后在实例化组件vnode时,会把propsDarta传递给vnode的第七个参数对象里,
而第七个参数对象的名字就是ComponenteOPions;
3.1props变化后,子组件如何更新?
首先,prop
数据的值变化在父组件,我们知道在父组件的 render
过程中会访问到这个 prop
数据变量,然后把自己的渲染watcher加入到这个变量的getter中了;
所以当 prop
数据变化一定会触发父组件的重新渲染,那么重新渲染是如何更新子组件对应的 prop
的值呢?
在父组件重新渲染的最后,会执行 patch
过程,进而执行 patchVnode
函数,patchVnode
通常是一个递归过程,所有如果有子组件的化,又会执行子组件的patchVnode过程;当它遇到组件 vnode
的时候,会执行组件更新过程的 prepatch
钩子函数
然后执行uopdateChildComponent();
重点来看更新 props
的相关逻辑,这里的 propsData
是父组件传递的 props
数据,vm
是子组件的实例。vm._props
指向的就是子组件的 props
值,propKeys
就是在之前 initProps
过程中,缓存的子组件中定义的所有 prop
的 key
。
主要逻辑就是遍历 propKeys
,然后执行 props[key] = validateProp(key, propOptions, propsData, vm)
重新验证和计算新的 prop
数据,更新 vm._props的值
,也就是子组件的 props
,
因为子组件的props也被添加到了响应式,所以在修改子组件的值的时候,会触发vm._props的setter,随意子组件会重新渲染;
这个就是子组件 props
的更新过程。
3.2props变化后,子组件如何重新渲染?
<script> const B = { name:'B', template:` <div id="dependencyDepend"> <p> age: {{age}}</p> <p> sex: {{sex}}</p>
<p> hobby:{{hobby}}</p> <p> hobby: {{hobby.game}}</p> </div> `, props:{ age:Number, sex:{ type:String, }, hobby:{ type:Object, } } } new Vue({ el: "#app", components:{ A,B, }, data(){ return{ age:18, sex:'mail', hobby:{ game:'lol' } } }, methods:{ modifyAge(){ this.age++ }, modifyHobby(){ this.hobby.game = 'Dota' } }, template:` <div id="dependencyDepend"> <B :age='age' :sex='sex' :hobby='hobby'> </B> <button @click='modifyAge'>click me</button> <button @click='modifyHobby'>click me</button> </div> ` }); </script>
重新渲染分为两种情况,
第一种是props直接修改了值,比如例子中的modyfyAge,那个直接触发了子组件props数据age的setter,所以子组件重新渲染;
第二种,props传递的数据是一个对象hobby,修改了对象的某个属性的值game,这个时候子组件也会重新渲染,但是它不是因为子组件props数据hobby的setter被触发,而是因为子组件的渲染watcher被依赖收集到了父组件的hobby的getter中了。hobby发生变化,触发setter,让子组件也重新渲染了。
父组件是data是响应式的,
子组件是props也是响应式的,所以子组件虽然也依赖了hobby属性,但是不会添加到父组件的hobby的getter里;因为子组件调用的是自己props的hobby,会被添加到自己props的hobby的getter里,并不会触发父组件的data的hobby的getter;
但是,为什么,子组件的渲染watcher可以添加到父组件的hobby的game的getter呢?
因为hobby的game成为响应式调用的是!shallow&&observe(val),所以game属性其实是一个单独的响应式,所以game的依赖收集条件是只要调用了hobby对象的game属性,就会收集,而hobby对象是引用数据类型,父和子都指向同一个hobby对象;
所以子组件调用hobby对象的game属性实际上就是调用父组件的hobby对象的game属性,所以子组件的watcher会被添加到父组件的hooby对象的game属性的dep实例的subs数组里;
这也就说明了,为什么,子组件在初始化时,给vm._props和vm._data做响应式是不一样的,vm._props直接用defineReactive方法,而vm._data用的是observe方法;
就是因为,vm._props不需要深度遍历它的数据,它的数据是由父组件传递过来的,如果父祖家传递的地址变了,我跟着变化即可。
如果父组件传递过来的地址没有变化,只是引用数据类型里的某个属性变化了,那么只要子组件模板引用了这个属性比如说hobby的game属性,那么子组件渲染watcher直接被添加到game的dep中了。
因为hobby对象在父组件中是父vm._data里,是通过obseve深度响应式的;
我们先看第一种情况:
当我们点击按钮时,age会+1,那么会触发age的setter,然后会导致父组件的重新渲染;
即调用patch方法。
然后会进入patchVnode方法
然后先执行prepatch方法;
然后执行updateComponent方法;去更新子组件的attrs、listeners、props;
然后通过validatePorp去校验并获取父组件传递过来的propData数据,然后赋值给子组件自己的prop,这就触发了子组件prop的setter
然后就会触发子组件的重新渲染;
到这里,第一种情况就结束了。
第二种情况,修改hobby的game属性的值;
这个其实很简单,当我们修改hobby的game的时候,会触发game的setter,然后dep.notify()里保存的watcher就是子组件的渲染watcher;
为什么?
因为prop传递基本数据类型时可能传递一个新的值,但是传递引用数据类型时,其实传递的是一个地址,所以子组件对prop中hobby对象的引用其实就是在引用父组件的hobby对象,那么求hobby的值时触发的是父组件hobby对象的getter,那么就会被依赖收集到父组件的hobby对象中;
因为这里引用的是父组件hobby对象的game属性,所以子组件渲染watcher被依赖收集到game的getter中,当game发生改变时,子组件渲染watcher也会重新渲染;
四、toggleObserving的作用是什么?
toggleOBserving函数主要是控制一个shouldObserve开关,通过这个开关,决定是否要递归的响应嵌套对象的内层对象;
所以,在给子组件的hobby对象添加到响应式的过程中,所以使用toggleOBserving函数,所以,可以让hobby的内层对象不在响应式中;因为内层对象已经在父组件的data中,使用!shallow&&observe(val)的方式添加过了。
所以只需要把最外层的对象响应式到prop就行了。
等到添加完了,再把toggleObserving(false);