上次我们讲解了patch函数的工作原理,那么如果在一个进程中我们以同步的方式把数据改变多次,vue也会patch多次、渲染多次么?在前两篇《通过Vue2.0源码理解vue生命周期》讲完了渲染watcher的响应式原理以后,本次我们来讲解这个问题也可以将前两次所讲的内容连接起来。先来看一个简单的demo,页面是这样:
模板如下:
点击add按钮以后,会执行add函数:
在add函数中,会以同步方式为num自加两次。下面我们从源码来分析下为num自加两次时,vue是不是每次都渲染。
之前讲到在为响应式数据重新赋值时会调用src/core/observer/index.js文件中的defineProperty中定义的set函数,set函数最后会执行dep.notify()函数。
notify函数在src/core/observer/dep.js文件的Dep类中,最后dep实例会遍历在渲染的时候实例收集的渲染watcher,逐个进行update(如果忘记了其中的逻辑,可以看这篇):
update是在src/core/observer/watcher.js的watcher实例中,最后会执行queueWatcher(this),即把当前的watcher添加到渲染队列中。
queueWatcher是在src/core/observer/scheduler.js文件中的一个全局函数,在queueWatcher开始部分中可以看到has和queue都是在scheduler.js中的全局变量,waiting是用来判断是否已经把渲染函数加入了异步任务中了的标志位,flushing是判断异步渲染任务是否正在执行的标志位。index的作用是当渲染任务正在迭代watcher时,把watcher加入渲染队列中的最小索引。这三个标志位先有个印象,后续遇到会进一步说明。
在queueWatcher中首先通过watcher的id检查has对象中是否已经标记了这个watcher,如果已经标记那么直接返回,如果没有就进行标记。
再判断异步渲染flushing任务是否正在执行,如果没有执行,就直接加入到queue中,如果正在执行,因为此时渲染watcher的queue队列已经按照watcher的id从小到大排好序,所以需要通过while循环找出在渲染队列中还没有渲染完的watcher中,id比要加入的watcher的id小于等于的watcher,插入到前面。while循环中的index是一个全局变量,和异步渲染时遍历渲染watcher队列时共用一个index。
最后queueWatcher做的不光是把watcher添加到队列中,还会通过vue内部实现的nextTick函数,让浏览器在异步事件循环中处理flushSchedulerQueue这个函数。当然加入之前要通过waiting标志位检查一下已经加入的异步渲染任务是否已经执行完。
我们再来看一下这个异步的flushSchedulerQueue函数执行的时候都做了什么,首先遍历queue中的所有watcher执行watcher中的before()函数,在《通过Vue2.0源码理解vue生命周期》中讲过是在这里调用的beforeUpdate的钩子,然后执行watcher的run()函数来执行vue实例的重新渲染,后续通过resetSchedulerState()来初始化scheduler.js文件中全局变量的状态,最后调用原来的watcher队列中的vue实例的updated钩子:
在flushSchedulerQueue渲染任务执行完时resetSchedulerState函数会把index置为0和queue清空,waiting和flushing置为false。这样下次将watcher添加到渲染队列中就可以直接添加。下次调用queueWatcher的时候,又可以添加渲染异步任务了。
下面来重点讲下watcher.run()都做了什么,在run()中会执行this.get()函数,在前两篇《通过Vue2.0源码理解vue生命周期》一文中有讲过对于渲染watcher最终会执行updateComponent函数,即生成完render函数生成Vnode之后,再执行_update()进而执行__patch__()函数来派发更新,就开始执行上一篇中所介绍的patch和diff的流程:
下面我们再通过demo调试下,具体来走一下整个流程:
首先我们在setter中打断点,在点击add按钮的时候会走进来,可以看到newVal的值为1,也就是已经执行了第一个自加操作:
继续往下调试,进入notify函数之后,会迭代Dep中的subs数组,执行watcher的update函数,右侧可以看到此时subs数组中只有一个Watcher:
在update函数中会执行queueWatcher函数,把当前的watcher添加到渲染更新的队列当中:
在queueWatcher函数中,判断has中以这个watcher的id为键的元素是否为true,如果为false,就为has添加添加元素1:true。通过右侧可以看到闭包中的flushing变量为false,没有在执行异步flushing任务。所以可以把watcher直接push进渲染队列queue中,waiting等待标志位为false,说明异步任务中没有还没有执行的flushSchedulerQueue任务,就把waiting等待标志位置为true,在异步任务中添加flushSchedulerQueue任务。需要注意的是flushSchedulerQueue这个函数是异步执行的,所以在我们同步调试的时候,不会立即执行到这个函数中,需要在这个函数起始点添加断点,异步执行的时候才会执行进来。
继续往下就执行到了num自加:
下面继续执行set函数中的dep.notify():
notify()函数中,还是会迭代执行subs渲染watcher序列中watcher的update(),从右侧可以看出,响应式变量num的dep中实例中维护的watcher变量还是id为1的这个渲染watcher:
在渲染watcher中执行update函数,后来还是执行queueWatcher逻辑:
在queueWatcher函数中,通过右侧闭包的变量可以看到has中有1:true这个变量,在异步渲染的时候,一定会重新渲染这个vue实例的。所以queueWatcher的逻辑,都不会执行就直接跳过了。
当同步逻辑都执行完后,就可以执行异步的flushSchedulerQueue逻辑了,在flushSchedulerQueue函数中,对queue中的watch进行迭代,执行watcher的run函数:
在run函数中就执行到了watcher的gat()函数,在此重新渲染了当前的渲染watcher:
进入get函数,执行完this.getter.call之后,可以看到左侧的num显示为2。而getter函数在右侧可以看到正是在组建中派发更新的updateComponent函数:
最后执行resetSchedulerState函数,将scheduler闭包中的数据还原:
参考:《剖析 Vue.js 内部运行机制》