vue源码阅读—04—响应式原理(一)obseve、dep和watcher;nexttick;

一、响应式对象

1、响应式的作用

 响应式主要帮助我们做手动操作 DOM 重新渲染这一步;

 

2、把props、data添加到响应式的流程

我们知道,vue里props和data的数据都是响应式的。   里面的数据发生了变化,会立马通过依赖它的模板或者计算属性或watch侦听器;(计算属性和侦听器时响应式的吗???)

那么props和data里的数据是如何响应式的?

2.1props

2.2data

我们在执行vue构造函数里代码的this_init()时,会执行到initState()这一步,它就是用来初始化我们的props、methods、data、computed、watch侦听器;

通过分析initData()的代码,主要是三步:

1.获取我们自已定义的data配置并赋值给vm._data;为后续代理到vm做准备

2.判断data对象的属性名不能和props的名字或methods的重复;

3.调用observe去响应式data

我们进入observe函数的代码继续看:

1.首先判断,observe观测的必须是一个对象,并且不能是vnode;

2.将要观测的对象作为参数去实例化一个OBserver;

 

 我们进入到OBserver类继续看

 1. Observer类的作用,就是通过给对象添__ob__属性,把自己附加到每个被观察对象上。一旦附加,观察者就会将目标对象的属性键转换为getter/setter来收集依赖关系和分派更新。
2.如果value是一个对象,那么走walk方法,即对被观察对象的每个属性key走defineReactive方法;

 

 

那么defineReactive方法是干什么的?

(1)首先给每个对象比如vm._props  vm._data添加一个属性key,方便后续做代理;

(2)其次对被观察对象的key做

  • 1.为每个key新建一个dep
  • 2.遍历这个key,看它是不是一个对象,如果时一个对象,继续调用observe观察它;
  • 3.使用OBject.defineProperty为这个对象添加getter和setter;

 

 
 
 
 
 响应式:

给这个对象,添加__ob__属性,指向observer实例;,

给这个对象的每个key,添加响应式和dep实例;

 

 

二、依赖收集

 

2.1依赖收集的流程

实例化watcher时,会先把watcehr实例赋值给Dep.target。然后会调用getter即调用updateComponent()即vm._update(vm._render),

那么会render.call(vm,$createElement),这个时候就会触发被观测对象比如props对象data对象里的属性的getter,那么就会执行这段代码:

 然后调用watcher的addDep方法;

 

 这段代码主要做两件事:

1.将dep实例的id添加到watcher的newDepIds和newDep属性里;为后续调用cleanupDeps方法清楚老的id做准备;

2.调用addDep方法,就是将watcher实例添加到dep实例的subs数组里,而每个被观测对象的属性都会被创建一个dep实例,所以subs数组里的元素(即watcher实例)可以说就都是依赖这个属性的。为后续的setter触发做准备;

 通过这两个方式,完成了dep实例和watcher实例的相互引用,也即是订阅者和观察者相互绑定了。

 

都执行完后即this.getter执行完后,便开始执行这三段代码,第一深度遍历暂时不说,第二是popTArget也就是获取现在的watcher实例状态;第三是清除本次渲染中中所有没用到的但是却存在了的老dep,免得因为他们重新渲染;

 

 

 

2.2详细说说cleanupDeps

2.2.1observe方法:

给每个vm._props对象或vm._data对象先拓展手写prop或data里的每个key属性。
然后拓展一个__ob__属性,表示一种意义———这个对象的每个key都已经添加响应式了。

2.2.2dep实例和watcher实例

每个key在被添加响应式时都会有一个Dep实例,这个实例是干啥的?它和watcher实例是什么关系?

  • warcher实例的newDepIds属性保存着新的dep实例编号;   warcher实例的newDeps属性保存着新的dep实例
  • dep实例subs属性保存着引用对象key的watcher

总结:相互引用的关系;

2.2.3cleanup方法的作用:

很好玩的一点,如果watchers的newDepIds没有这个自己保存的deps数组的某个dep的id,那么自己不会把这个dep删除掉,而是让这个dep的subs数组里把自己删除掉。这样以后这个dep修改触发setter触发重新渲染都和自己这个watcher无关了。

然后,后续会把newDepIds都赋值给depIds,然后把newDepIds都赋值为空,准备下一次getter;

所以呢,dep实例和watcher实例相互引用,在cleanup方法里,如果没有引用了又会相互删除;

2.2.4cleanup方法配合例子详解

 这个东西其实是vue实现的很巧妙的一个东西,我们举例:

  <script>
      new Vue({
        el: "#app",
        data() {
          return {
            flag:true,
            msg:'hello world',
            msg1:'hello vue',
          };
        },
        template:`
              <div id="dependencyDepend">
                <div v-if="flag">{{msg}}</div>
                <div v-else>{{msg1}}</div>
                <button @click="change">change</button>
                <button @click="toggle">toggle</button>
              </div>
                 `,
        methods:{
          change(){
            this.msg = Math.random();
          },
          toggle(){
            this.flag = !this.flag;
          }
        }
      });

初始状态下第一次渲染,会有渲染watcher依赖msg属性;由于flag是true,没有watcher依赖msg1;

然后,我们先点击change按钮,那么在第二次渲染中,这个watcher任然依赖msg,所以说dep里仍然有这个watcher实例,我们假设它叫aaa;

然后,我们点击toggle按钮,那么第三次渲染中,有新的watcher实例bbb依赖msg1,而且由于flag为false,所以没有watcher实例依赖msg了;

然后,我们点击toggle按钮,那么第三次渲染中,因为watcher实例这个时候只触发msg1的getter,所以watcher会被添加到msg1的dep实例里,当然了,watcher实例的newDepIds数组也会添加msg1的dep实例;

最后,当执行了完了watcher实例的this.getter,开始执行this.cleanupDeps了,这个时候,就是两个功能:

1.把watcher的newDepIds里v,没有包含的dep找出来,找出来后,把这个dep.subs数组里删除本watcher,免得以后这个dep要触发响应式的setter时还会让本watcher重新渲染;

2.把newDepIds都赋值给depIds,然后把newDepIds都赋值为空,准备下一次getter;这个时候,watcher实例的newDepIds实际上也完成了对原有msg的dep实例的删除;

 

 

想想看,如果没有cleanupDeps,msg的dep实例里还包含了这个wathcher,当我们点击change按钮时,由于会改变msg的值,会触发msg的setter,那么还是会做一遍dep的派发更新操作,所以还是会让这个watcher重新渲染,但是我们的watcher实际页面没有任何变化,因为现在的页面模板已经依赖的数据是msg1提供的数据了,整个页面没有msg提供的数据了。

这就会让人很疑惑,数据没有变化,页面却刷星了,就是因为msg数据的dep实例还存在watcher实例,没有被删除;

2.3pushTarget和proTarget的作用

因为组件套组件,每个组件在创造的时候都会实例化并mount,所以都会在mount过程中new一个watcher,所以通过一个栈结构,先进后出,维护当前的watcher实例状态;

要创造一个新组件了,就把老的watcher即Dep.target压栈targetStack中;然后把当前的watchher实例即_target赋值给Dep.target,这样Dep.target一直都是新的watcehr实例状态;

 等到本组件mount结束了,就会回到上一个父组件。然后执行finally里的popTarget操作,把父组件的watcehr实例出栈,赋值给Dep.target;

 

 

 

 

 

 

 

三、派发更新

3.1派发更新流程

 

 

 数据修改,触发setter,然后主要是这两步:

1.如果赋值是一个对象,那么把这个对象也加入到响应式系统中;

2.派发更新,notify

 

 我们进入notify,继续看,notify就是便遍历自己的subs数组,调用所有的元素即watcehr的update方法;

 

 那么update方法是啥?

前两个暂时不看,它们可能是计算属性,同步有关的东西,暂时不考虑。

主要是调用queueWAtcher;

 

queuewatcher有两步,

1. 主要是将watcher放入到queue这个数组中。这个queue数组里,保存了所有的后续会被重新渲染的watcher。

这个has[id],也就是has对象是很巧妙地。

因为一个watcher可能即依赖了flag属性,有依赖了msg属性,那么会触发两次setter,也就是会触发两次watcher的update操作,也就是会触发两次queuewatcher操作,但是通过has【id】,巧妙只添加一次watcher到queue数组中。这样可以减少watcher的重新渲染;

2.在nexttick里执行flushScherdulerqueue;

 

 flushScherdulerquue主要是两步操作

1.执行watcher.run()操作,即wathcer的重新渲染;它会重新调用watcher的getter即updateComponent即vm._update(vm._render()); 完成vnode到真实dom的一次重新渲染;

2.执行reset SCheduler Sate(),将所有的状态清空,恢复到原始值;

 

 

3.2循环更新问题是如何产生的

 已知:模板是渲染watcher,watch侦听器是用户watcher;

    <script>
      new Vue({
        el: "#app",
        data() {
          return {
            flag:true,
            msg:'hello world',
            msg1:'hello vue',
          };
        },
        template:`
              <div id="dependencyDepend">
                <div v-if="flag">{{msg}}</div>
                <div v-else>{{msg1}}</div>
                <button @click="change">change</button>
                <button @click="toggle">toggle</button>
              </div>
                 `,
        methods:{
          change(){
            this.msg = Math.random();
          },
          toggle(){
            this.flag = !this.flag;
          }
        },
        watch:{
          msg(){
            this.msg = Math.random();
          }
        }
      });
    </script>

当我们点击change按钮时,就会报错;

为什么?

因为我们点击change按钮,修改了msg的值,那么会触发setter,那么会触发   渲染watcher 和  user watcher   的重新渲染;

user watcher的操作有一项是 this.cb.call(this.vm, value, oldValue),这里面的ch在user watcher中是侦听器的代码,那就是执行回调函数即执行侦听器的代码

结果呢?侦听器的代码又是修改了msg,那么自己触发自己,又要触发渲染watcher和user watcher;就这杨一直死循环下去;

 

 我们现在从源码的角度看一下:

 

 然后因为user watcher 是比渲染watcher先创建的,所以会先执行user watcher;

 

然后把watcher  push到queue队列中,并在nexttick中执行flushSchelerQueue方法;

 

 

然后继续执行渲染watcher的update过程;也是吧渲染watcher添加到queue队列中;

 

 

等到nexttick中,开始调用watcher.run();因为queue队列中的第一个watcher时user  watcher,所以调用的时侦听器;

 

 

因为时user watcher,所以会调用cb方法,这个cb回调函数就是我们自定义的侦听器函数;

 

 

 

 

 

结果呢?侦听器的代码又是修改了msg,那么自己触发自己,又要触发setter,  所以又会触发渲染watcher和user watcher的update;

那么我们又进入到了user watcher的update()过程;

这个时候有会执行queueWatcher方法;但是这个时候出现了一个问题。

我们知道,目前其实还处在flushScheluerQueue方法中调用user  watcher的run()方法调用栈中;

所iflushing已经被赋值为true;

 

 

 

 

那我们进入到else逻辑,这个逻辑是干啥的?

看注释就知道,如果正在刷新,则根据其id拼接一个新的watcher;

所以我们会根据目前的user watcher的位置索引,在它的后面,再添加一个一样的user  watcher;

 

然后同样的道理,目前其实还处在flushScheluerQueue方法中调用user  watcher的run()方法调用栈中,所以wating为true,所以不会执行下面的nexttick方法;

最后,第一个watcher.run()执行完毕;

然后回到flushScheluerQueue方法,开始执行第二个queue;

因为我们刚刚在前面,给queue添加了第二个元素,也还是user watcher;

所以就会走一遍老路,又会触发user watcher  的cb回调函数,然后修改msg的值,然后触发setter,然后再flushing时添加第三个user watcher到queue中,然后。。。

最后会添加非常多的user watcehr,直至报警告;

 

 

 

 

 

 

 

 

 

 

 


 

一、nexttick

本质上就是把所有要在nexttick执行的函数,都收集到callbacks数组中,然后通过使用macroTimerFunc或者microTimerFunc的方式,把这些函数都放在宏任务队列里或者微任务队列列在下一个tick中执行;

 执行macroTimerFunc或者microTimerFunc函数就是使用settimeout或者promise等宏任务队列、微任务队列执行flushcallback函数;

  • macroTimerFunc等定义:
  • microTimerFunc的定义: 

flushcallbacks这个函数就是把callbacks里的元素都取出来执行;

 

 总结:

所以说,nexttick这个函数,就是把所有的cb都放到全局变量callbacks数组里;然后通过microTimerFunc的方式,在执行下一个微任务队列时,再去执行callbacks里的元素;

 1.2测试

    <script>
      new Vue({
        el: "#app",
        data() {
          return {
            msg:'hello world',
          };
        },
        template:`
              <div id="dependencyDepend">
                <div v-if="flag" ref='msg'>{{msg}}</div>
                <button @click="change">change</button>
              </div>
                 `,
        methods:{
          change(){
this.$nexttick(()=> console.log('nexttick', this.$refs.msg.innerText) ) //helloword 虽然也是在nexttick中执行,但是这个对调函数才callbacks数组的位置比渲染watcher前面,所以仍然是helloworld this.msg = 'test nextTick'; //虽然msg的值可以立马改变,但是获取模板的值却并没有立马改变,因为模板即渲染watcher的重新渲染走的时nextTick(flushSchedulerQueue),所以 //说模板实在nextTick中才会重新渲染,所以这个时候同步代码msg还是helloWorld;但是同样放在nextTick中的代码,由于按照顺序也被加入到callbacks数组中, //所以在会在watcher重新渲染好之后,才执行,这个时候获取模板的值就是改变后的值了test nextTick console.log('sync',this.$refs.msg.innerText); //helloWorld this.$nextTick( ()=>{ console.log('nextTick',this.$refs.msg.innerText); //test nextTick }); this.$nextTick().then(()=>{ console.log('nextTick with promise',this.$refs.msg.innerText); //test nextTick }) }, }, }); </script>

 

 

 

 

二、检测变化的注意事项

2.1说明

1.即给对象新添加属性,这个新的属性不是响应式的即模板并不会自己显现出来;

2.通过下标的方式修改数组元素的值,或者给数组里添加一个元素,也不是响应式的即模板并不会自己显现出来;

3.不能修改rootData,即根数据;

 

为啥不是响应式的?

因为没有触发setter,没有触发重新渲染;所以模板不会自己显现出来了

为什么没有触发setter?

  • 对于对象来说,我们是给每个key添加响应式的。然后我们每次添加一个新的key,这个新的key是没有响应式的,所以肯定不会有getter和setter,那何来的触发setter这一说?
  • 对于数组里说,通过下表修改值,没有改变老key的地址引用,老key还是指向那个数组,所以不会触发setter。
  • 总结:因为我们的响应式是通过OBject.definedProperty定义的,所以哪种方法可以触发Object.definedProperty的set就说明是可以触发setter的;

 

那为什么使用Vue.set就可以响应式了?

因为Vue.set会手动帮助我们触发setter,然后去重新渲染;

2.2测试

    <script>
      new Vue({
        el: "#app",
        data() {
          return {
            msg:{
              a:'hello'
            },
            arr:[1,2,3,4]
          };
        },
        template:`
              <div id="dependencyDepend">
                <div  ref='msg'>{{msg}}</div>
                <div  ref='msg'>{{arr}}</div>

                <button @click="arrChange">arrChange</button>
                <button @click="arrAdd"> arrAdd</button>
                <button @click="msgChange">msgChange</button>
              </div>
                 `,
        methods:{
          arrChange(){ //修改数组的某个值
            //this.arr[1] = 2222222;
            Vue.set(this.arr,1,3333333333333333)
          },
          arrAdd(){  //往数组的最后一位添加值
            //this.arr[4] = 44444444
            this.arr.push(444444444444444)
          },
          msgChange(){
            //this.msg.b = 'vue';
            Vue.set(this.msg, 'b', 'vue')
          },
        },

      });
    </script>

2.3修改对象 

当我们点击msgChange,使用Vue.set给msg对象添加一个新属性时,

1. 首先会进入到Vue.set方法;target,也就是msg对象,是由__ob__属性的,为什么?

 

因为再组件初始化时,会给vm._data定义响应式 ,然后因为data里的msg属性又是一个对象,所以会走let childOb = !shallow && observe(val)这一步,把data对象的子属性msg对象继续响应式化;

所以msg对象会有一个__ob__属性,指向它自己的Observer实例;

然后又通过childOb.dep.depend()方法,把渲染watcher也添加到msg对象的oberver实例的deps数组里。   就是为了,以后如果要给msg对象添加一个新的属性,可以通过childOb.dep.notify()进行手动触发setter;

 2.第二步,使用defineReactive方法,把key添加到msg对象里面,因为使用的defineReactive方法,所以会被b属性创建一个实例,并且给b属性的dep数组里添加渲染watcher;

 

 

 3.调用ob.dep.notify,手动触发setter;

 

 总结:

 props和data的响应式实现不一样。

 vm._props的实现:

遍历我们手写props的所有key,然后给 vm._props使用OBject.definedProperty(vm._props,手写props的每个key,{getter和setter});

vm._data的实现:

把手写的data对象赋值给vm._data对象,然后对整个vm._data对象调用observe(vm._data),完成响应式;这个步骤会添加__ob__属性;

为什么实现的方式不一样?

我猜测,因为props是父组件传来的,所以不需要观察它,只要做一个数据劫持就好,如果父组件改变了这个值,那么我们就跟着变化即可;

但是data是自己定义的,要有一个__ob__属性,要个它的子属性对象继续响应式,而props不需要,因为props的数据子组件是不能改的,单向数据流,子组件只能接受;

 

总结:

 我有点理解了,观察者订阅者模式:

一个对象vm._data,我们把它当作要观察的东西,给它定义一个__ob__属性,指向自己的OBserver实例,observer实例就是观察者,observer实例也有dep实例,dep实例就是我们的依赖收集器;渲染watcher就是订阅者;

然后给data对象的每个属性都使用Object.definedProperty进行响应式;

如果某个属性,比如说msg,又是一个对象,那么执行childObj = !shallow $$ observe(msg),给msg对象也添加__ob__属性,因为msg对象也是我们要观察的东西;然后把这个watcher添加到msg.__ob__.dep里,进行依赖收集;也就是说,模板的这个渲染watcher,

只要用到了vm._data对象的msg属性,

  1. 那么它这个渲染watcher就被msg属性的dep收集了,
  2. 也被msg属性对应的对象.__ob__.dep收集了。

所以,

  1. 当我们修改msg指向的引用地址时,会触发setter,
  2. 当我们使用Vue.set修改msg对象里的某个属性时,也可以触发setter;

 

 2.4修改数组

如果是我们要被观察的数组,比如例子中的data对象里的arr属性,这个肯定是在实例初始化阶段调用initData时就把data对象都添加到响应式了。

所以,arr属性肯定是被观察的属性。

那么,就会被修改原型;

value是arr属性所引用的数组;

arrMethods就是要修改到的原型;  arr.__proto__ = arrMethods

 

 arrayMethods的原型还是Array.prototype,所以当我们arrayMethod没有的方法还是回去Array.prototype里找,但是有的方法就会被arrayMethods重写;

 这个方法会手动帮助我们调用ob.dep.notify();

 

 2.4.1

1.根据例子,当我们调用arrChange方法时,去修改数组,我们会进入这里,调用被重写的splice方法来修改数组,

 

所以会来到这里;

 

 其实我们仔细想想,既然被观测到的数据arr的某些方法都已经被重写了,而且我们调用Vue.set实际上调用的就是被重写后的方法,

那么我们直接使用this.arr.splice方法去修改数组的某个元素也可以有响应式;经过测试效果一样;

 

 

 

 

2.4.2

2.根据例子,当我们调用arrAdd时, 使用的是push方法,由于push方法被重写了,所以可以会直接跳转到

 

这里,这里也会手动调用ob.dep.notofy()

 

 

 总结:

对于对象, Vue.set就是帮助我们重新调用defineReavtive和重新调用ob.dep.notify()

对于数组,Vue.set就是帮助我们重新调用被重写的splice方法;splice方法里如果有新元素添加就observerArray,把新元素添加到getter,然后帮组我们重新调用ob.dep.notify();

总之,Vue.set主要就是帮助我们把新数据添加到getter,然后重新调用ob.dep.notify();

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2022-06-13 17:18  Eric-Shen  阅读(271)  评论(0编辑  收藏  举报