前言:
篇幅过长,基本都是文字,配图(可以看看官网相应式原理的配图)啥的,代码啥的都很少,大家可以耐着性子,边思考边看,慢慢来。也可以结合源码阅读。
正文:
我们都知道,vue是一个很明显的使用数据驱动视图的框架。vue2内部的核心原理是使用的es5中的Object.defineProperty
API来实现数据相应式的,这个api提供了getter
和setter
方法来实现数据劫持,使得vue可以来监听访问对象的属性或者对对象的属性进行赋值。
在new Vue之后,vue会将我们在data中定义好的数据使用 Observer 变成相应式数据,并给每一个数据都分配一个 Dep 实例,该实例就是用来在运行getter函数时进行依赖收集的(dep.depend()
),当我们的数据发生变化时,便会运行我们的setter函数,在该函数里面便会进行派发更新(dep.notify()
),至于到底收集的是什么,我们之后再说
此外,我们都知道,vue会将我们写的模板编译成为render函数,或者我们直接自己书写render函数也是可以的,之后会运行render函数,在运行render函数的过程中,会用到我们在data中定义好的数据。这个时候自然会调用我们定义好的getter函数,然后,边进行依赖收集,也就是说,告诉data里面被使用到的数据,是render函数依赖了你,你把这个函数给收集进去,到时候,你变了,就可以通知这个render函数,你变了,需要再次运行这个render函数了,因为,运行完render函数后,会返回一个vnode,vue拿到vnode之后,会进行新旧vnode的对比,然后找到真正需要改变的地方,对真实的dom进行相关操作,试图也就自然更新了。话回到之前,我们收集的依赖就是render函数吗?那么有一个疑问,就是说,我运行了render函数,然后里面使用到了data中的相应式数据,然后运行了数据的getter方法,在这个方法里面要进行依赖收集,把render函数收集到dep中去,那么在这个方法里面,我们怎么就知道要收集的是这个render函数呢,换句话说,我怎么知道是哪个函数在用我这个数据呢,我们仔细想想,会发现,这个好像是有点难,不知道从何下手。那么,接下来,我们看看vue中是如何解决这个有趣的难题的。
其实,vue中,还使用了一个 Watcher 类,我们的render函数,其实是new Watcher的时候当成参数传进去的,然后便会运行我们的render函数,也就是说,render函数并不是说直接调用,而是在watcher里面调用的,我们收集的依赖,也并不是render函数,而是watcher实例。可是这样有什么用呢,解决了上面的那个问题了吗?别急,我们在看看vue接下来是怎么做的。在watcher里面,运行render函数之前,vue做了一个非常简单的操作,就是给Dep类上设置了一个静态属性target,然后并将当前的watcher实例赋值给了这个target,然后才运行的render函数,这个时候,当使用到了data中的数据,在运行getter方法的时候,dep.depend(),会先看看Dep上的静态属性target是否有值,有的话,就会把这个值,也就是watcher实例添加到dep.subs里面去,这是一个数组,因为可能不止一个函数依赖这个数据,还可能有我们的计算属性,watch监听都会依赖这个data数据。当我们的render函数运行完之后,依赖也就收集完了,之后,vue会将Dep上的target置空。之后,当我们数据发生变化的时候,运行了setter方法,进行依赖派发更新,dep.notify(),其实也就是把subs里面的watcher实例遍历一次,然后一次运行watcher实例上的update函数,这个函数里面,会再次运行我们的render函数或者其他的依赖函数(watch监听写的函数等)。然后,如果是render函数的话,我们的视图自然就会更新啦!
接下来,我们考虑一件事,这样的设计有没有什么问题。当我们某个函数在运行的时候,连续改变了好几次,好多个data中的数据,会发生什么?一改变data,便会执行setter,便会派发更新,便会循环收集的依赖,便会执行watcher中的依赖函数,如果这个依赖是render函数,便会执行render函数,然后便会拿到vnode,再进行patch,diff更改视图。想想会进行多少次diff,多少次操作真实dom,仔细想想,真的有必要执行这么多次吗,这会有多浪费效率,我们只需要在这些值都改变完了之后,在一次性去改变视图不好吗?那怎么办呢,别急,vue已经考虑到了这一点,来看看vue是怎么来解决这个问题的。
其实watcher的update方法,里面并不是直接执行render函数的,而是将我们的watcher交给了
schedule 调度器,然后调度器会把watcher放进一个数组里面去,然后,调度器会将一个函数交给 nextTick ,nextTick会把这个函数放进微任务队列等待同步任务执行完,在执行这个函数,而这个函数执行过程中,会把刚刚说的那个数组遍历一次,拿到watcher之后,调用watcher的run方法,而run方法会真正的去执行render函数。在将watcher放进数组的时候,会先检查这个数组中是不是已经放进过当前这个watcher,没有的话就放进去,已经有了的话,就不放了。后面的watcher放进去这个数组之后,调度器就不会在放一个函数到微任务队列了,有一个就足够了。之后运行这个函数,就会把数组中的所有watcher的run运行一遍了。render函数也知识运行一次,因为刚刚讲了,去重了。
我们再举一个例子,来理解上面的话,如下:
{
data(){
return {
a:1,
b:2,
c:3
}
},
watch:{
a(newVal){
// 做某些事
}
},
methods:{
click(){
this.a = 2;
this.b = 3;
this.c = 4
}
}
}
当执行this.a = 2的时候,会执行a的setter函数,然后进行dep.notify(),然后循环subs进行watcher.update(),a的依赖一共有两个,一个是render函数的watcher,一个是watch的watcher,因为我们的a变了,我们需要执行那个监听函数。update中,把render函数的watcher交给schedule调度器,调度器查看数组里面有没有push进去过render函数的watcher,一看没有,把他加进去,然后将一个函数交给nextTick,nextTick把这个函数放进微任务队列,然后会来看a的watch的watcher,交给调度器之后,再看数组里面有没有这个watcher,一看,没有,那么继续放进去,这个时候就不会再交给nextTIck啥东西了。然后,代码回到了this.b = 3;又把刚刚的顺序来一遍,只不过到调度器在看要不要把当前这个render函数的watcher加到数组中去的时候,因为之前render函数的watcher已经加进去了,所以不会再把相同的watcher加进去了,然后,this.c = 4;同理。这个时候同步代码就执行完了,然后开始执行异步任务,这个时候,我们之前放进微任务的那个函数执行了,它把数组里面的watcher一个个拿出来,其实也就两个wathcer,一个是render函数的watcher,一个是a的监听函数watch的watcher。把他们依次拿出来执行watcher.run()就完事了。
其实,nextTick里面还不完全是我上面讲的这样,还有一些地方为了方便理解简化了一些细节,具体可以去看看源码了。经过我上面的一番话,如果大家懂了个大概的话,可以去看看源码的具体实现了。跟着我说的思路,大家多去看看源码的具体实现,一定会有所收获!
留一个悬念,大家想想计算属性的watcher是怎么一个流程,大家也可以去看看源码是如何实现的,之后有时间我会跟大家一起讲讲vue中关于computed的源码,也是蛮有意思的。
最后,再告诉大家一个细节,其实,不仅仅是dep中会收集依赖,我们的watcher实例中,也会收集是哪些dep收集到了自己!这个大家也可以看看源码是如何写的。