美文网首页
Vue 异步更新源码解析

Vue 异步更新源码解析

作者: 梦晓半夏_d68a | 来源:发表于2021-06-01 23:53 被阅读0次

    先补充一下 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
    

    相关文章

      网友评论

          本文标题:Vue 异步更新源码解析

          本文链接:https://www.haomeiwen.com/subject/zdagsltx.html