Skip to content

浅析 vue 异步更新策略和 nextTick 源码

准备工作

去 github 上把 vue2 的仓库 clone 下来。下载依赖,给 dev 脚本加上--sourcemap 参数。

image.png

然后执行dev脚本。

再然后我们可以打开一个测试案例,然后进行调试。比如 todoMVC。

js
vue\examples\classic\todomvc\index.html

注意要引入 dist 目录下的 vue.js 文件,因为这个文件是加上--sourcemap 参数才有的,可以进行源码映射方便调试。

image.png

简单的代码 这是我经过简化 todoMVC 后的代码。。。

html
<!doctype html>
    <section class="todoapp">
      {{count}}
    </section>
    <script src="../../../dist/vue.js"></script>
    <script src="app.js"></script>
  </body>
</html>

同目录下的 app.js

// app Vue instance
var app = new Vue({
  data: {
    count: 0
  },
  mounted() {
    this.count = 1 // 等下在浏览器调试时,这行代码打上断点。
  },

})

// mount
app.$mount('.todoapp')

开始调试

当在 mounted 钩子里给 count 赋值为 1 的时候,会触发 set 函数

js
export function proxy(target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter(val) {
    // ===================
    this[sourceKey][key] = val 
    // ===================
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

进入 set 函数,看看里面是怎么做的。

我的理解是 dep 相当于一个响应式数据,watcher 相当于响应式数据在模板上的监视者。一个 dep 对应一个或多个 watcher。当然一个 watcher 也只对应视图模板上的某一个地方。

js
export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean
) {
  const dep = new Dep()

  // ...
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        if (__DEV__) {
          dep.depend({
            target: obj,
            type: TrackOpTypes.GET,
            key
          })
        } else {
          dep.depend()
        }
        if (childOb) {
          childOb.dep.depend()
          if (isArray(value)) {
            dependArray(value)
          }
        }
      }
      return isRef(value) && !shallow ? value.value : value
    },
    // 执行这里的 set 函数
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (!hasChanged(value, newVal)) {
        return
      }
      if (__DEV__ && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else if (getter) {
        return
      } else if (!shallow && isRef(value) && !isRef(newVal)) {
        value.value = newVal
        return
      } else {
        // 将新值赋值给 dep 的 val 属性上
        val = newVal
      }
      childOb = !shallow && observe(newVal, false, mock)
      if (__DEV__) {
        // 响应式数据去通知它的 watcher 们更新视图了。在这里打断点进入代码看看。
        dep.notify({
          type: TriggerOpTypes.SET,
          target: obj,
          key,
          newValue: newVal,
          oldValue: value
        })
      } else {
        dep.notify()
      }
    }
  })

  return dep
}

dep.notify 函数内部:

js
export default class Dep {
  static target?: DepTarget | null
  id: number
  subs: Array<DepTarget>

  constructor() {
    this.id = uid++
    this.subs = []
  }

  addSub(sub: DepTarget) {
    this.subs.push(sub)
  }

  removeSub(sub: DepTarget) {
    remove(this.subs, sub)
  }

  depend(info?: DebuggerEventExtraInfo) {
    if (Dep.target) {
      Dep.target.addDep(this)
      if (__DEV__ && info && Dep.target.onTrack) {
        Dep.target.onTrack({
          effect: Dep.target,
          ...info
        })
      }
    }
  }

  // 响应式数据去通知它的 watcher 们更新视图了,但不是立即同步执行更新 (run 方法),而是执行 update 方法
  notify(info?: DebuggerEventExtraInfo) {
    // subs 是一个 watcher 数组,这里复制一个副本
    const subs = this.subs.slice()
    if (__DEV__ && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    // for 循环,执行 watcher 的 update 函数,可以简单的理解为更新视图的函数。
    for (let i = 0, l = subs.length; i < l; i++) {
      if (__DEV__ && info) {
        const sub = subs[i]
        // 触发将要更新的钩子函数
        sub.onTrigger &&
          sub.onTrigger({
            effect: subs[i],
            ...info
          })
      }
      // 依次执行数组中 watcher 的 update 方法。
      // 重点就在这里了,update 方法内部并不是同步执行的,而是异步执行,那它到底是怎么实现的呢?
      subs[i].update()
    }
  }
}

subs[i].update() 执行方法内部:

即会去执行 watcher 实例的 update 方法。

js
export default class Watcher implements DepTarget {
  // ...
  constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {
    // 
  }
  get() {
    // ...
  }
  // ...
  update() {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 会去执行这个方法,把 watcher 放入队列中。传入参数为 watcher 实例 this。
      queueWatcher(this)
    }
  }

  run() {
    if (this.active) {
      // 真正的更新视图
      // ...
    }
  }

  // ...
}

queueWatcher 方法内部:

可以看到,

接收获取到 watcher 实例,

js
const queue: Array<Watcher> = []
let waiting = false

// ...

// 这个方法的作用就是将要更新的 watcher 放入定义的 queue 数组中,
// 并且执行一次 nextTick 方法。这应该是异步更新的关键。
export function queueWatcher(watcher: Watcher) {
  const id = watcher.id
  if (has[id] != null) {
    return
  }

  if (watcher === Dep.target && watcher.noRecurse) {
    return
  }

  has[id] = true
  if (!flushing) { // 没有执行 flushSchedulerQueue 方法时
    // 往 queue 数组中 push 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)
  }

  // wait 默认为 false
  if (!waiting) {
    // 然后就置为 true 了。
    waiting = true

    if (__DEV__ && !config.async) {
      flushSchedulerQueue()
      return
    }
    // 在这里会执行 nextTick 方法。没错,源码内部也使用了,也给我们暴露可以使用。
    // 传入参数是一个名为 flushSchedulerQueue 的函数。
    nextTick(flushSchedulerQueue)
  }
}

// ==========================
// flushSchedulerQueue 方法定义
// ==========================
function flushSchedulerQueue() {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  queue.sort(sortCompareFn)

  // ===========================触发 watcher 的 run 方法========================
  // 同步 for 顺序循环 queue 数组,执行 watcher 的 run 方法,触发视图更新。
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    if (__DEV__ && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' +
            (watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`),
          watcher.vm
        )
        break
      }
    }
  }
   // ===========================触发 watcher 的 run 方法 end========================

  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}


function resetSchedulerState() {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (__DEV__) {
    circular = {}
  }
  waiting = flushing = false
}

在 nextTick 所在的文件,会有一个数组来存放传入 nextTick 的函数入参。

nextTick 的作用就是把参数 push 进名为 callbacks 的数组中,并且执行一次 timeFunc 方法。

而 timeFunc 的作用就是使用 Promise.then 来异步执行 flushCallbacks 方法,把存入 callbacks 中的数组统统 flush 掉 (执行一遍)

看到这里,是不是感觉和很多个 watcher 放入 queue 数组有点相像?

js
// 这个文件里会有一个数组,专门保存传入 nextTick 的回调参数
const callbacks: Array<Function> = []
// ...
let pending = false


export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
  let _resolve
  // 把回调参数 push 进 callbacks 数组中。
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e: any) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    // 然后执行在同一文件里的 timerFunc 方法。
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

// 因为有些浏览器不执行 Promise,所以需要做处理。。ie,你知道的。。这里删减一些代码,太长了
let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  // 原来 timerFunc 方法就是使用 Promise 的微任务,异步执行 flushCallbacks 方法。
  timerFunc = () => {
    p.then(flushCallbacks)
  }
} else if (
  // ...
) {
  // ...
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // ...
} else {
  // ...
}

// 执行这里
// callbacks 数组中存的是传入 nextTick 参数的函数。
// for 循环执行。
function flushCallbacks() {
  pending = false
  // 保存一个副本
  const copies = callbacks.slice(0)
  // 将原本的函数数组清空、初始化。
  callbacks.length = 0
  // 循环执行副本的每一个函数。
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

总结

当 vue 的响应式数据更新时,会去触发对应的 set 方法,进而执行 dep.notify 方法,通知所有的 watcher 去更新视图。

每个 watcher 会执行 update 方法,将自己的实例 this 作为参数调用 queueWatcher(this) 方法,然后把自己添加到队列数组queue中。

queueWatcher 方法第一次还会调用 nextTick(flushSchedulerQueue) 方法。

flushSchedulerQueue 会for 循环依次执行 queue 中保存的 watcher 的 run 方发。

而 nextTick 方法也是将传入的函数参数添加到一个数组callbacks中。

并且第一次还会执行 timeFunc 方法,在兼容的情况下也就是执行 Promise.then(flushCallbacks) 方法。

flushCallbacks() 会for 循环依次执行 callbacks 中保存的函数。

Released under the MIT License.