Vue 异步更新源码解析

先补充一下 Vue 实现双向数据绑定的原理:
  通过Object.defineproperty 拦截对数据的访问 get 和设置 set,当拦截到数据的访问时进行依赖收集,拦截到数据的设置时则执行dep.notify 通知 watcher 进行更新。

notify

/src/core/observer/dep.js

/**
   * 通知该依赖收集的所有 watcher 执行 update 方法,进行异步更新
   */
  notify () {
    // subs 保存的是该依赖收集的所有 watcher
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }


update

  调用 queueWatcher 方法,将 watcher 加入队列

/src/core/observer/watcher.js

/**
   * 根据 watcher 配置,决定接下来怎么走,一般是 queueWatcher
   */
  update () {
    /* istanbul ignore else */
    // 如果是懒执行,设置 dirty 为 true
    // computed 就是懒执行,可以让 computedGetter 执行时重新计算 computed 回调函数的执行结果
    // if (watcher.dirty) {
    //   watcher.evaluate()
    // }
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      // 同步执行,在使用 vm.$watch 或者 watch 选项时可以传一个 sync 选项,
      // 当为 true 时在数据更新时该 watcher 就不走异步更新队列,直接执行 this.run 
      this.run()
    } else {
      // 将 watcher 放入 watcher 队列,一般走这里
      queueWatcher(this)
    }
  }


queueWatcher

  1. 每个 watcher 都有自己的 id,如果 has 记录到对应的 watcher,则跳过,不会重复入队,这在 官网 有提到:如果同一个 watcher 被多次触发,只会被推入到队列中一次
  2. watcher 加入到队列中,等待执行。
  3. 当队列没有在刷新的时候,执行nextTick(flushSchedulerQueue)。这里的 waiting 的作用就是一个锁,防止 nextTick 重复执行。

ps:flushSchedulerQueue 作为回调传入 nextTick 异步执行。

/src/core/observer/scheduler.js

/**
 * 将 watcher 放入 watcher 队列
 * 具有重复 id 的 watcher 会跳过
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 如果 watcher 已经存在,则跳过,不会重复入队
  if (has[id] == null) {
    has[id] = true
    // 不管是否刷新, watcher 都能立即入队 queue
    if (!flushing) {
      // 如果当前没有处于刷新队列状态,watcher 直接入队
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      // 如果已经在刷新队列,把 watcher 放入正确的位置
      // 从队列末尾开始倒序遍历,根据当前 watcher.id 找到它大于的 watcher.id 的位置,然后将自己插入到该位置之后的下一个位置
      // 即将当前 watcher 放入已排序的队列中,且队列仍是有序的
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        // 直接刷新调度队列, 一般不会走这儿,Vue 默认是异步执行,如果改为同步执行,性能会大打折扣
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}


nextTick

  1. cb 即传入的回调,它被 push 进一个 callbacks 数组,等待调用。
  2. pending 的作用和 queueWatcher 方法中的 waiting一样,也是一个锁。防止后续的 nextTick 重复执行 timerFunctimerFunc 内部创建会一个微任务或宏任务,等待所有的 nextTick 同步执行完成后,再去执行 callbacks 内的回调。
  3. 如果没有传入回调,用户可能使用的是 Promise 形式,返回一个 Promise_resolve 被调用时进入到 then

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

/**
 * 1、用 try catch 获取 cb ,确保拿到的是函数,然后将其放入 callbacks 数组
 * 2、pending = false:表示现在浏览器的任务队列没有 flushCallbacks 函数
 *  pending = true:表示浏览器的任务队列中有 flushCallbacks 函数
 *   待执行 flushCallbacks 函数时,pending 会被再次置为 false,表示下一个 flushCallbacks 函数可以进入
 *   浏览器的任务队列
 * @param {*} cb 接收一个回调函数 => flushSchedulerQueue
 * @param {*} ctx 上下文
 * @returns 
 */
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 因为用户可以采用 Vue.nextTick 和 this.$nextTick 调用 nextTick ,
  // 因此在对 callbacks 数组存储 cb 回调之前,做了捕获,确保拿到 cb
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    // 执行 timerFunc,在浏览器的任务队列中(首选微任务队列)放入 flushCallbacks 函数
    pending = true
    timerFunc()
  }
  // 处理采用 promise 进行调用的情况: Vue.nextTick().then(function () { })
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

timerFunc

flushCallbacks函数放入浏览器的异步任务队列中
通过兼容来创建合适的 timerFunc,最优先肯定是微任务,其次再到宏任务。优先级为 promise.then(微任务) > MutationObserver(微任务) > setImmediate(宏任务) > setTimeout(宏任务)

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

let timerFunc
// 首选 Promise.resolve().then() 微任务
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    // 在 微任务队列 中放入 flushCallbacks 函数
    p.then(flushCallbacks)
    /**
     * 在有问题的UIWebViews中,Promise.then不会完全中断,但是它可能会陷入怪异的状态,
     * 在这种状态下,回调被推入微任务队列,但队列没有被刷新,直到浏览器需要执行其他工作,例如处理一个计时器。
     * 因此,我们可以通过添加空计时器来“强制”刷新微任务队列。
     */
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  // 第二选择 MutationObserver 微任务
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  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)) {
  // 第三选择 MutationObserver 宏任务,但是依然比 setTimeout 好
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 前三种都不支持的情况见,选择 setTimeout 宏任务
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}


flushCallbacks

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

/**
 * 清空 callbacks 数组、pending 设为 false、
 * 执行 callbacks 数组中的每一个函数(比如 flushSchedulerQueue、用户调用 nextTick 传递的回调函数)
 */
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}


nextTick 回调flushSchedulerQueue

将刚刚加入 queuewatcher 逐个 run 更新。resetSchedulerState 重置状态,等待下一轮的异步更新。
要特别注意: 在执行nextTick(flushSchedulerQueue)的时候flushSchedulerQueue 并不是马上执行,它只是作为回调传入而已。因为用户可能还会调用 nextTick 方法。这种情况下,callbacks 里的内容为 ["flushSchedulerQueue", "用户传入的nextTick回调"],当所有 nextTick同步任务执行完才开始执行 callbacks 里面的回调。

/src/core/observer/scheduler.js

/**
 * 刷新 watcher, 运行 watcher.run 方法,触发更新函数
 */
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // 刷新队列之前先给队列排序(升序),可以保证:
  // 1. 父组件比子组件先更新. (因为父组件比子组件先创建)
  // 2. 一个组件的用户user watcher 比渲染 watcher 先执行 (用户user watcher 比渲染 watcher 先创建)
  // 3. 如果一个组件在其父组件的 watcher 执行期间被销毁,则它的 watcher 可以被跳过
  queue.sort((a, b) => a.id - b.id)

  // 直接使用了 queue.length,动态计算队列的长度。不要缓存队列的长度,因为在我们运行存在的 watcher 的时候,别的 watcher 可能会 push 进来
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    // 执行 before 钩子,在使用 vm.$watch 或者 watch 选项时可以通过配置项(options.before)传递
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // 执行 watcher.run,最终触发更新函数
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && 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
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  // 重置调度状态
  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}


resetSchedulerState

重置状态,意味着这一'tick' 刷新队列结束,刷新状态改为 false ,等待下一轮的异步更新

/src/core/observer/scheduler.js

/**
 * 重置调度状态
 * 1、has = {}
 * 2、waiting = flushing = false,表示刷新队列结束
 * 可以给 callbacks 数组中放入新的 flushSchedulerQueue 函数
 * 并且可以向浏览器的任务队列放入下一个 flushCallbacks 函数
 */
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容