每日一题之 Vue 的异步更新实现原理是怎样的?
最近面试总是会被问到这么一个问题:在使用 vue 的时候,将for
循环中声明的变量i
从 1 增加到 100,然后将i
展示到页面上,页面上的i
是从 1 跳到 100,还是会怎样?答案当然是只会显示 100,并不会有跳转的过程。
怎么可以让页面上有从 1 到 100 显示的过程呢,就是用setTimeout
或者Promise.then
等方法去模拟。
讲道理,如果不在 vue 里,单独运行这段程序的话,输出一定是从 1 到 100,但是为什么在 vue 中就不一样了呢?
这就涉及到 Vue 底层的异步更新原理,也要说一说nextTick
的实现。不过在说nextTick
之前,有必要先介绍一下 JS 的事件运行机制。
JS 运行机制
众所周知,JS 是基于事件循环的单线程的语言。执行的步骤大致是:
当代码执行时,所有同步的任务都在主线程上执行,形成一个执行栈;
在主线程之外还有一个任务队列(task queue),只要异步任务有了运行结果就在任务队列中放置一个事件;
一旦执行栈中所有同步任务执行完毕(主线程代码执行完毕),此时主线程不会空闲而是去读取任务队列。此时,异步的任务就结束等待的状态被执行。
主线程不断重复以上的步骤。
我们把主线程执行一次的过程叫一个
tick
,所以nextTick
就是下一个tick
的意思,也就是说用nextTick
的场景就是我们想在下一个tick
做一些事的时候。
所有的异步任务结果都是通过任务队列来调度的。而任务分为两类:宏任务(macro task)和微任务(micro task)。它们之间的执行规则就是每个宏任务结束后都要将所有微任务清空。常见的宏任务有setTimeout/MessageChannel/postMessage/setImmediate
,微任务有MutationObsever/Promise.then
。
nextTick 原理
派发更新
大家都知道 vue 的响应式的靠依赖收集和派发更新来实现的。在修改数据之后的派发更新过程,会触发setter
的逻辑,执行dep.notify()
:
遍历subs
里每一个Watcher
实例,然后调用实例的update
方法,下面我们来看看update
是怎么去更新的:
update
执行后又走到了queueWatcher
,那就继续去看看queueWatcher
干啥了(希望不要继续套娃了:
这里queue
在 pushwatcher
时是根据id
和flushing
做了一些优化的,并不会每次数据改变都触发watcher
的回调,而是把这些watcher
先添加到⼀个队列⾥,然后在nextTick
后执⾏flushSchedulerQueue
。
flushSchedulerQueue
函数是保存更新事件的queue
的一些加工,让更新可以满足 Vue 更新的生命周期。
这里也解释了为什么 for 循环不能导致页面更新,因为for
是主线程的代码,在一开始执行数据改变就会将它 push 到queue
里,等到for
里的代码执行完毕后i
的值已经变化为 100 时,这时 vue 才走到nextTick(flushSchedulerQueue)
这一步。
参考 前端进阶面试题详细解答
nextTick 源码
接着打开 vue2.x 的源码,目录core/util/next-tick.js
,代码量很小,加上注释才 110 行,是比较好理解的。
首先将传入的回调函数cb
(上节的flushSchedulerQueue
)压入callbacks
数组,最后通过timerFunc
函数一次性解决。
timerFunc
下面一大片if else
是在判断不同的设备和不同情况下选用哪种特性去实现异步任务:优先检测是否原生⽀持Promise
,不⽀持的话再去检测是否⽀持MutationObserver
,如果都不行就只能尝试宏任务实现,首先是setImmediate
,这是⼀个⾼版本 IE 和 Edge 才⽀持的特性,如果都不⽀持的话最后就会降级为 setTimeout 0。
这⾥使⽤callbacks
⽽不是直接在nextTick
中执⾏回调函数的原因是保证在同⼀个 tick 内多次执⾏nextTick
,不会开启多个异步任务,⽽把这些异步任务都压成⼀个同步任务,在下⼀个 tick 执⾏完毕。
nextTick 使用
nextTick
不仅是 vue 的源码文件,更是 vue 的一个全局 API。下面来看看怎么使用吧。
当设置 vm.someData = 'new value'
,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环 tick 中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用数据驱动的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用Vue.nextTick(callback)
。这样回调函数将在 DOM 更新完成后被调用。
官网用例:
并且因为$nextTick()
返回一个 Promise
对象,所以也可以使用async/await
语法去处理事件,非常方便。
评论