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类继续看
那么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触发做准备;
都执行完后即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
执行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属性,
- 那么它这个渲染watcher就被msg属性的dep收集了,
- 也被msg属性对应的对象.__ob__.dep收集了。
所以,
- 当我们修改msg指向的引用地址时,会触发setter,
- 当我们使用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();