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初始化之响应式

// 响应式注意一点,在开发环境中:
// 第四个参数即回调函数,是我们自定义的setter,在开发模式,如果props被触发setter即被修改,则会报警告;因为单向数据流的原因,子组件不能修改props的数据;
如果是基本数据类型不能修改值,如果是引用数据类型不能修改父组件传递过来的地址。当然,如果传递过来一个对象,你自然可以修改对象里属性的值,因为这不涉及地址的修改,并且vue也不会想上述一样报错报错,但是按照单向数据流的原则,强烈不建议。

2.3props初始化之代理

//注意一点:
// 在子组件对象变成子组件构造函数的Vue.extend()期间,组件的props已经被代理到组件的原型上了。
 
// 我们在这里只需要代理vm根实例的props。

 

 

 

 

 

 

 

 


 

三、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);

 

 

 

posted @ 2022-08-01 02:30  Eric-Shen  阅读(1189)  评论(1编辑  收藏  举报