乐趣区

关于前端:vue-nextTick原理

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 更新后在执行回调。

退出移动版