最近面试总是会被问到这么一个问题:在应用vue的时候,将for
循环中申明的变量i
从1减少到100,而后将i
展现到页面上,页面上的i
是从1跳到100,还是会怎么?答案当然是只会显示100,并不会有跳转的过程。
怎么能够让页面上有从1到100显示的过程呢,就是用setTimeout
或者Promise.then
等办法去模仿。
讲道理,如果不在vue里,独自运行这段程序的话,输入肯定是从1到100,然而为什么在vue中就不一样了呢?
for(let i=1; i<=100; i++){ console.log(i);}
这就波及到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()
:
// src/core/observer/watcher.jsclass Dep { notify() { //subs是Watcher的实例数组 const subs = this.subs.slice() for(let i=0, l=subs.length; i<l; i++){ subs[i].update() } }}
遍历subs
里每一个Watcher
实例,而后调用实例的update
办法,上面咱们来看看update
是怎么去更新的:
class Watcher { update() { ... //各种状况判断之后 else{ queueWatcher(this) } }}
参考 前端vue面试题具体解答
update
执行后又走到了queueWatcher
,那就持续去看看queueWatcher
干啥了(心愿不要持续套娃了:
//queueWatcher 定义在 src/core/observer/scheduler.jsconst queue: Array<Watcher> = []let has: { [key: number]: ?true } = {}let waiting = falselet flushing = falselet index = 0export function queueWatcher(watcher: Watcher) { const id = watcher.id //依据id是否反复做优化 if(has[id] == null){ has[id] = true if(!flushing){ queue.push(watcher) }else{ let i=queue.length - 1 while(i > index && queue[i].id > watcher.id){ i-- } queue.splice(i + 1, 0, watcher) } if(!waiting){ waiting = true //flushSchedulerQueue函数: Flush both queues and run the watchers nextTick(flushSchedulerQueue) } }}
这里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行,是比拟好了解的。
const callbacks = []let pending = falseexport function nextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() }
首先将传入的回调函数cb
(上节的flushSchedulerQueue
)压入callbacks
数组,最初通过timerFunc
函数一次性解决。
let timerFuncif (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) if (isIOS) setTimeout(noop) } isUsingMicroTask = true} else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]')) { let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(flushCallbacks) }} else { timerFunc = () => { setTimeout(flushCallbacks, 0) }}
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 更新实现后被调用。
官网用例:
<div id="example">{{message}}</div>
var vm = new Vue({ el: '#example', data: { message: '123' }})vm.message = 'new message' // 更改数据vm.$el.textContent === 'new message' // falseVue.nextTick(function () { vm.$el.textContent === 'new message' // true})
并且因为$nextTick()
返回一个 Promise
对象,所以也能够应用async/await
语法去处理事件,十分不便。