vueソースコード読み取りの非同期更新の原則

ご存知のとおり、Vueはデータ駆動型のビューに基づいており、データの変更dataによりsetter通知の機能watcherが更新されます。更新プロセスでは、テンプレートのコンパイル、dom diff、レンダリングなどの多くの操作も実行する必要があります。頻繁に更新を実行すると、パフォーマンスが低下します。では、Vueではどのように行われるのでしょうか?理解する前に、まずjsの操作メカニズムを理解しましょう。

js実行メカニズム

ご存知のとおり、JSはイベントループに基づくシングルスレッド言語です。イベントループの一般的な手順は次のとおりです。

✅(1)すべての同期タスクはメインスレッドで実行され、実行コンテキストスタックを形成します。

✅(2)メインスレッドに加えて、「タスクキュー」もあります。非同期タスクの実行結果が得られるとすぐに、イベント(コールバック)が「タスクキュー」に配置されます。

✅(3)「実行スタック」内のすべての同期タスクが実行されると、システムは「タスクキュー」を読み取り、その中にあるイベントを確認します。次に、これらの対応する非同期タスクは待機状態を終了し、実行スタックに入り、実行を開始します。

✅(4)メインスレッドは上記の3番目のステップを繰り返し続けます。

タスクキュー内のタスクは、マクロタスクとマイクロタスクの2つのカテゴリに分けられます。

マクロタスク:新しいイベントループのプロセスで、マクロタスクが検出されると、マクロタスクはタスクキューに追加されますが、次のイベントループまで実行されません。一般的なマクロタスクはsetTimeout、setInterval、setImmediate、requestAnimationFrame

マイクロタスク:現在のイベントループの同期タスクが実行された後、タスクキュー内のタスクが順番に実行されます。実行中にマイクロタスクが検出されると、マイクロタスクは現在のイベントループのマイクロタスクキューに追加され、空になるまで実行されます。簡単に言えば、現在のイベントループにマイクロタスクがある限り、それらは次のイベントループで実行されるのではなく、空になるまで実行されます。一般的なマイクロタスクはMutationObserver、Promise.then、process.nextTick。

Vueデータ更新メカニズム

只要侦听到数据变化,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方法异步更新视图。

おすすめ

転載: juejin.im/post/7086354741935996958