前言

本文将介绍 Vue.js 在派发通知获取回调后渲染 DOM 的策略,其内部如何实现异步任务队列,以及Vue.set 如何使新增加的属性也能获得响应

Event Loop

事件循环机制和消息队列 的维护由事件触发线程控制的,事件触发线程同时维护一个 消息队列,JS 引擎线程遇到异步任务(DOM 事件监听、网络请求、setTimeout 计时器等…),会交给相应的线程单独去维护,等待某个时机(计时器结束、网络请求成功、用户点击 DOM),然后由事件触发线程将异步对应的 回调函数 加入到消息队列中,消息队列中的回调函数等待被执行。

同时,JS 引擎线程会维护一个执行栈,同步代码会依次加入执行栈然后执行,结束会退出执行栈。如果执行栈里的任务执行完成,即执行栈为空的时候(即 JS 引擎线程空闲),事件触发线程才会从消息队列取出一个任务(即异步的回调函数)放入执行栈中执行。执行完了后,执行栈再次为空,事件触发线程会重复上一步操作,再取出一个消息队列中的任务。

主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是任务(task),task 分为 Marcotasks 和 Microtasks,微任务为在当前正在执行的脚本之后立即发生的事情安排,微任务在每一个宏任务结束后清空

for (macroTask of macroTaskQueue) {
    // 1. Handle current MACRO-TASK
    handleMacroTask();

    // 2. Handle all MICRO-TASK
    for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
    }
}

异步更新队列

在实际开发中,你可能会遇到更新数据后的操作无法获取到新的数据,这是因为 Vue 在更新 DOM 时是异步执行的。在下一个 tick 异步更新队列的主要目的是在缓冲区对数据去重,避免同一个 Watcher 被多次触发造成浪费。Vue.js 在其他地方也有类似的考虑,比如在上一篇中响应式对象在收集依赖和重新渲染时对依赖去重,设置 deep Watcher 对对象做深度递归遍历时记录遍历过程中的子响应对象的 dep id 避免重复访问,生成子组件构造函数时对 Sub 构造函数做缓存,避免重复构造等等

nextTick

Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false

// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
    // in problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}

/**
 * Wrap a function so that if any code inside triggers state change,
 * the changes are queued using a (macro) task instead of a microtask.
 */
export function withMacroTask (fn: Function): Function {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    return res
  })
}

export 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
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

nextTick 内部声明 microTimerFuncmacroTimerFunc 变量分别实现 microTasksmacroTasks,且他们都会在下一个 tick 执行 flushCallbacksflushCallbacks 则遍历执行 callbacks 数组

如果不传 cb,nextTick() 返回一个 Promise

methods: {
  updateMessage: async function () {
    this.message = '已更新'
    console.log(this.$el.textContent) // => '未更新'
    await this.$nextTick()
    console.log(this.$el.textContent) // => '已更新'
  }
}

Vue.set

由于 Object.defineProperty 的限制,给对象添加新的属性不能触发它的 setter。使用全局的 API Vue.set,向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新

function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

set 函数在一系列判断条件后获取 target.__ob__ 即响应式对象的 Observer,通过 defineReactive(ob.value, key, val) 将新增属性变成响应式,最后手动ob.dep.notify() 触发更新,这里能够让新增的属性也能派发更新得益于在初始化过程中对 childOb 的深度绑定依赖

if (childOb) {
  childOb.dep.depend()
  if (Array.isArray(value)) {
    dependArray(value)
  }
}