The principle of asynchronous update of vue source code reading

As we all know, Vue is based on data-driven views, and the data changes will trigger datathe setterfunction of the notification watcherto update. The update process also needs to go through a lot of operations, such as template compilation, dom diff, rendering, etc. If frequent updates are performed, the performance will be poor. So how is it done in Vue? Before understanding, let's first understand the operation mechanism of js.

js running mechanism

As we all know, JS is a single-threaded language based on event loop. The general steps of the event loop are as follows.

✅(1) All synchronization tasks are executed on the main thread, forming an execution context stack.

✅(2) In addition to the main thread, there is also a "task queue". As soon as the asynchronous task has a running result, an event (callback) is placed in the "task queue".

✅(3) Once all synchronization tasks in the "execution stack" are executed, the system will read the "task queue" to see what events are in it. Those corresponding asynchronous tasks then end the waiting state, enter the execution stack, and start execution.

✅(4) The main thread keeps repeating the third step above.

Tasks in the task queue are divided into two categories, namely macro tasks and micro tasks.

Macro task: In the process of a new event loop, when a macro task is encountered, the macro task will be added to the task queue, but it will not be executed until the next event loop. Common macro tasks aresetTimeout、setInterval、setImmediate、requestAnimationFrame

Microtask: After the synchronization task of the current event loop is executed, the tasks in the task queue will be executed in sequence. During execution, if a microtask is encountered, the microtask is added to the microtask queue of the current event loop for execution until it is emptied. Simply put, as long as there are microtasks in the current event loop, they will be executed until they are emptied, instead of being executed in the next event loop. Common microtasks areMutationObserver、Promise.then、process.nextTick。

Vue data update mechanism

只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环 tick 中,Vue 刷新队列并执行实际 (已去重的) 工作。

例如,当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环 tick 中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用 “数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。例如:

// html
<div id="example">{{message}}</div>

// javascript
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改数据
// dom 没有立即更新
vm.$el.textContent === 'new message' // false
// 在nextTick中 获取到更新后的dom
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})
复制代码

为什么nextTick可以访问更新后的dom?

// 当一个 Data 更新时,会执行以下代码
// 1. 触发 setter
// 2. setter 中调用 dep.notify
// 3. Dep notify 会遍历所有相关的 Watcher 执行 update 方法
class Watcher {
  // 4. 执行更新操作
  update() {
      queueWatcher(this)
  }
}

const queue = [];

function queueWatcher(watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) { // 避免添加同一 watcher
    has[id] = true
    if (!flushing) { // 处理 Watcher 渲染时,可能产生的新 Watcher  eg:v-if 触发新watcher
      // 5. 将当前 Watcher 添加到异步队列
      queue.push(watcher)
    } else {
      // 新watcher 添加到指定位置
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // 保证此次watcher都在同一tick 并且不会重复执行
    if (!waiting) {
      waiting = true
      // 6. 执行异步队列,并传入更新视图回调
      nextTick(flushSchedulerQueue)
    }
  }
}

// 更新视图的具体方法
function flushSchedulerQueue() {
  let watcher, id;
  // 排序,先渲染父节点,再渲染子节点
  // 这样可以避免不必要的子节点渲染,如:父节点中 v-if 为 false 的子节点,就不用渲染了
  queue.sort((a, b) => a.id - b.id);
  // 遍历所有 Watcher 进行批量更新。
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    // 更新 DOM
    watcher.run();
  }
}
复制代码

nextTick

可以看到watcher的update并没有直接更新视图,而是将更新视图的方法传给了nextTick方法,接下来看下nextTick源码

// src\core\util\next-tick.js

const callbacks = [];
let pending = false;
let timerFunc;

export function nextTick (cb?: Function, ctx?: Object) {
  // 1.将更新视图方法添加 callbacks中
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } 
  })
  // 2.pending用来保障同一时间 只会执行一个timerFunc()
  if (!pending) {
    pending = true
    timerFunc()
  }
}
复制代码
let timerFunc;

// 判断是否支持 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
   
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} 
// 判断是否支持 MutationObserver
else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  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
} 
// 判断是否支持 setImmediate
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} 
// 最不济 setTimeout
else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
复制代码

可以看出timerFunc 其实是根据浏览器兼容创建的异步方法,优雅降级,最终调用flushCallbacks执行flushSchedulerQueue方法更新dom。

vm.nextTick可以访问到更新后的dom,是因为异步队列也遵循先来后到,修改 Data 触发的更新异步队列会先执行,执行完成 dom 更新,此时调用 nextTick 的回调可以访问到更新后的dom。

this.$nextTick

this.$nextTick原理就是nextTick方法

// src\core\instance\render.js
Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
}
复制代码

总结

vue异步更新原理就是触发data的setter最终调用watcher的update方法,update调用queueWatcher将本身推入一个全局的queue队列,并将更新视图的方法传给nextTick,最终通过nextTick方法异步更新视图。

Guess you like

Origin juejin.im/post/7086354741935996958