Vue为何采用异步渲染
Vue
在更新DOM
时是异步执行的,只要侦听到数据变化,Vue
将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更,如果同一个watcher
被多次触发,只会被推入到队列中一次,这种在缓冲时去除重复数据对于避免不必要的计算和DOM
操作是非常重要的,然后,在下一个的事件循环tick
中,Vue
刷新队列并执行实际(已去重的)工作,Vue
在内部对异步队列尝试使用原生的Promise.then
、MutationObserver
和setImmediate
,如果执行环境不支持,则会采用setTimeout(fn, 0)
代替。
# 异步渲染的原因
对于Vue
为何采用异步渲染,简单来说就是为了提升性能,因为不采用异步更新,在每次更新数据都会对当前组件进行重新渲染,为了性能考虑,Vue
会在本轮数据更新后,再去异步更新视图,举个例子,让我们在一个方法内重复更新一个值。
this.msg = 1;
this.msg = 2;
this.msg = 3;
2
3
事实上,我们真正想要的其实只是最后一次更新而已,也就是说前三次DOM
更新都是可以省略的,我们只需要等所有状态都修改好了之后再进行渲染就可以减少一些性能损耗。
对于渲染方面的问题是很明确的,最终只渲染一次肯定比修改之后即渲染所耗费的性能少,在这里我们还需要考虑一下异步更新队列的相关问题,假设我们现在是进行了相关处理使得每次更新数据只进行一次真实DOM
渲染,来让我们考虑异步更新队列的性能优化。
假设这里是同步更新队列,this.msg=1
,大致会发生这些事: msg
值更新 ->
触发setter
->
触发Watcher
的update
->
重新调用 render
->
生成新的vdom \-> dom-diff \-> dom
更新,这里的dom
更新并不是渲染(即布局、绘制、合成等一系列步骤),而是更新内存中的DOM
树结构,之后再运行this.msg=2
,再重复上述步骤,之后的第3
次更新同样会触发相同的流程,等开始渲染的时候,最新的DOM
树中确实只会存在更新完成3
,从这里来看,前2
次对msg
的操作以及Vue
内部对它的处理都是无用的操作,可以进行优化处理。
如果是异步更新队列,会是下面的情况,运行this.msg=1
,并不是立即进行上面的流程,而是将对msg
有依赖的Watcher
都保存在队列中,该队列可能这样[Watcher1, Watcher2...]
,当运行this.msg=2
后,同样是将对msg
有依赖的Watcher
保存到队列中,Vue
内部会做去重判断,这次操作后,可以认为队列数据没有发生变化,第3
次更新也是上面的过程,当然,你不可能只对msg
有操作,你可能对该组件中的另一个属性也有操作,比如this.otherMsg=othermessage
,同样会把对otherMsg
有依赖的Watcher
添加到异步更新队列中,因为有重复判断操作,这个Watcher
也只会在队列中存在一次,本次异步任务执行结束后,会进入下一个任务执行流程,其实就是遍历异步更新队列中的每一个Watcher
,触发其update
,然后进行重新调用render
->
new vdom
->
dom-diff
->
dom
更新等流程,但是这种方式和同步更新队列相比,不管操作多少次msg
, Vue
在内部只会进行一次重新调用真实更新流程,所以,对于异步更新队列不是节省了渲染成本,而是节省了Vue
内部计算及DOM
树操作的成本,不管采用哪种方式,渲染确实只有一次。
此外,组件内部实际使用VirtualDOM
进行渲染,也就是说,组件内部其实是不关心哪个状态发生了变化,它只需要计算一次就可以得知哪些节点需要更新,也就是说,如果更改了N
个状态,其实只需要发送一个信号就可以将DOM
更新到最新,如果我们更新多个值。
this.msg = 1;
this.age = 2;
this.name = 3;
2
3
此处我们分三次修改了三种状态,但其实Vue
只会渲染一次,因为VIrtualDOM
只需要一次就可以将整个组件的DOM
更新到最新,它根本不会关心这个更新的信号到底是从哪个具体的状态发出来的。
而为了达到这个目的,我们需要将渲染操作推迟到所有的状态都修改完成,为了做到这一点只需要将渲染操作推迟到本轮事件循环的最后或者下一轮事件循环,也就是说,只需要在本轮事件循环的最后,等前面更新状态的语句都执行完之后,执行一次渲染操作,它就可以无视前面各种更新状态的语法,无论前面写了多少条更新状态的语句,只在最后渲染一次就可以了。
将渲染推迟到本轮事件循环的最后执行渲染的时机会比推迟到下一轮快很多,所以Vue
优先将渲染操作推迟到本轮事件循环的最后,如果执行环境不支持会降级到下一轮,Vue
的变化侦测机制(setter
)决定了它必然会在每次状态发生变化时都会发出渲染的信号,但Vue
会在收到信号之后检查队列中是否已经存在这个任务,保证队列中不会有重复,如果队列中不存在则将渲染操作添加到队列中,之后通过异步的方式延迟执行队列中的所有渲染的操作并清空队列,当同一轮事件循环中反复修改状态时,并不会反复向队列中添加相同的渲染操作,所以我们在使用Vue
时,修改状态后更新DOM
都是异步的。
当数据变化后会调用notify
方法,将watcher
遍历,调用update
方法通知watcher
进行更新,这时候watcher
并不会立即去执行,在update
中会调用queueWatcher
方法将watcher
放到了一个队列里,在queueWatcher
会根据watcher
的进行去重,若多个属性依赖一个watcher
,则如果队列中没有该watcher
就会将该watcher
添加到队列中,然后便会在$nextTick
方法的执行队列中加入一个flushSchedulerQueue
方法(这个方法将会触发在缓冲队列的所有回调的执行),然后将$nextTick
方法的回调加入$nextTick
方法中维护的执行队列,flushSchedulerQueue
中开始会触发一个before
的方法,其实就是beforeUpdate
,然后watcher.run
()才开始真正执行watcher
,执行完页面就渲染完成,更新完成后会调用updated
钩子。