vue nextTick原理

后面谈到了vue2.x的响应式原理,vue.js在视图更新采纳的是异步更新策略,咱们来看看它是怎么做到的。

/** ? */for(let i = 0; i < 100; i++) {    this.count++;}/** ? */在dom更新后执行一些操作this.$nextTick(fn)

先抛出两个问题:

  1. for循环更新count数值,dom会被更新100次吗?
  2. nextTick是如何做到监听dom更新结束的?

异步更新波及到js的运行机制,具体的可看这里
【event loop机制】
这篇文章呢咱们次要从源码角度来剖析nextTick的原理实现。

这是咱们响应式外面的watcher类

<!--观察者Watcher类-->class Watcher {    constructor  () {        Dep.target = this  // new Watcher的时候把观察者寄存到Dep.target外面    }    update () {        queueWatcher(this) // 异步更新策略    }    run () {        //  dom在这里执行真正的更新    }}

watcher对象在进行更新执行update,外部次要执行了一个queueWatcher函数,将watcher对象作为this进行传递,所以咱们便从queueWatcher这个口子开始。

queueWatcher

queueWatcher函数在scheduler文件外面

/** queueWatcher函数*/let has = {};let queue = [];let waiting = false;function queueWatcher (watcher: Watcher) {  const id = watcher.id  // 避免queue队列wachter对象反复  if (has[id] == null) {    has[id] = true    queue.push(watcher)        // 传递本次的更新工作    if (!waiting) {      waiting = true      nextTick(flushSchedulerQueue)    }  }}/** flushSchedulerQueue函数 */function flushSchedulerQueue () {    let watcher, id;    for (index = 0; index < queue.length; index++) {        watcher = queue[index];        id = watcher.id;        has[id] = null;        // 执行更新        watcher.run();    }    // 更新结束复原标记位    waiting = false;}
  1. queue外面寄存着咱们本次要更新的watcher对象,queueWatcher函数做了一个判重操作,雷同的watcher对象只会被退出到queue队列一次。
  2. flushSchedulerQueue函数顺次调用了wacther对象的run办法执行更新。并作为回调传递给了nextTick函数。
  3. waiting这个标记位代表咱们是否曾经向nextTick函数传递了更新工作,nextTick会在以后task完结后再去解决传入的回掉,只须要传递一次,更新结束再重置这个标记位。

next-tick

let callbacks = [];let pending = false;let timerFunc;/**----- nextTick -----*/function nextTick (cb) {    // 把传进来的回调函数放到callbacks队列里    callbacks.push(cb);    // pending代表一个期待状态 等这个tick执行    if (!pending) {        pending = true        timerFunc()    }        // 如果没传递回调 提供一个Promise化的调用    if (!cb && typeof Promise !== 'undefined') {        return new Promise(resolve => {          _resolve = resolve        })    }}/**----- timerFunc ----*/// 1、优先思考Promise实现if (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) ||  MutationObserver.toString() === '[object MutationObserverConstructor]')) {// 2、降级到MutationObserver实现  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)) {// 3、降级到setImmediate实现  timerFunc = () => {    setImmediate(flushCallbacks)  }} else {// 4、如果以上都不反对就用setTimeout来兜底了  timerFunc = () => {    setTimeout(flushCallbacks, 0)  }}function flushCallbacks () {  // 将callbacks中的cb顺次执行  pending = false  const copies = callbacks.slice(0)  callbacks.length = 0  for (let i = 0; i < copies.length; i++) {    copies[i]()  }}
  1. 传进来的回调函数会被保留到callbacks队列外面,这里应用callbacks 而没有在nextTick中间接执行回调函数,是因为这样能够保障在同一个tick 内屡次执行nextTick,在一个tick外面实现渲染,不会开启多个异步工作。

    // 举个栗子????// 如果咱们间接在nexttick外面间接执行回调function nextTick (cb) {    setTimeout(cb)}nextTick(cb1)nextTick(cb2)这种状况下就会开启两个异步工作,也就是两次事件循环,造成了页面不必要的渲染
  2. timerFunc是实现的外围,它会优先应用Promise等microtask,保障在同一个事件循环外面执行,这样页面只须要渲染一次。切实不行的话用setTimeout来兜底,尽管会造成二次渲染,但这也是最差的状况。vue在这里用了降级解决的策略。

$nextTick

最初再把nexttick函数挂到Vue原型上就OK了

Vue.prototype.$nextTick = function (fn) {    return nextTick(fn, this)}

小结

vue异步更新,实质上是js事件机制的一种使用,优先思考了具备高优先级的microtask,为了兼容,又做了降级策略。

当初再回头看结尾的那两个问题

  1. for循环更新count数值,dom会被更新100次吗?

    不会,因为queueWatcher函数做了过滤,雷同的watcher对象不会被反复增加。

  2. nextTick是如何做到监听dom更新结束的?

    vue用异步队列的形式来管制DOM更新和nextTick回调先后执行,保障了能在dom更新后在执行回调。