美文网首页
vue源码(3):vue异步更新

vue源码(3):vue异步更新

作者: 执念_01a7 | 来源:发表于2022-10-12 09:46 被阅读0次

    问题:

    1.能简单说一下vue 的异步更新机制吗?

    2.nextTick的原理是什么?

    • dep.notify

    • 源码地址:/src/core/observer/dep.js
    /*
       *通知dep中所有的watcher,执行watcher.updata()方法
      */ 
      notify () {
        const subs = this.subs.slice()
        if (process.env.NODE_ENV !== 'production' && !config.async) {
          subs.sort((a, b) => a.id - b.id)
        }
        //遍历dep中的存储就的watcher,执行watcher.updata()
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
    
    • watcher.update

    • 源码地址:/src/core/observer/watcher.js
      /**
       * 根据watcher配置项,决定接下来怎么走,一般是queryWatcher
       * 
       */
      update () {
        /* istanbul ignore else */
        if (this.lazy) {
          // 懒执行时走这里,比如computed
          // 将dirty职位true,可以让computedGetter执行重新计算computed回调函数的执行结果
          this.dirty = true
        } else if (this.sync) {
          // 同步执行,使用vm.watch选项是可以传递一个sync选项,
          // 当为true时,并且数据更新时watcher就不走异步队列,直接执行this.run
          this.run()
        } else {
          // 更新时一般都走这里,将watcher放入队列watcher中
          queueWatcher(this)
        }
      }
    
    • queueWatcher

    • 源码地址:/src/core/observer/scheduler.js
    /**
     * 将watcher放入,watcher队列中
     */
    export function queueWatcher (watcher: Watcher) {
      //给每个watcher添加id
      const id = watcher.id
      //判重watcher不会重复入队
      if (has[id] == null) {
        // 缓存一下watcher,用于判断watcher是否已入队
        has[id] = true
        if (!flushing) {
          // 如果flushing=false,表示当前watcher没有被刷新,watcher可以直接入队
          queue.push(watcher)
        } else {
          // watcher队列已经被刷新,这个时候watcher入队就需要特殊操作
          // 保证watcher入队以后,刷新的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为false时,表示当前浏览器的异步队列任务不支持flushSchedulerQueue函数
          waiting = true
    
          if (process.env.NODE_ENV !== 'production' && !config.async) {
            // 同步执行,直接刷新watcher队列
            // 性能会大打折扣
            flushSchedulerQueue()
            return
          }
          nextTick(flushSchedulerQueue)
        }
      }
    }
    
    • nextTick

    • 源码地址:/src/core/util/next-tick.js
    /**
     * 完成两件事:
     *   1、用 try catch 包装 flushSchedulerQueue 函数,然后将其放入 callbacks 数组
     *   2、如果 pending 为 false,表示现在浏览器的任务队列中没有 flushCallbacks 函数
     *     如果 pending 为 true,则表示浏览器的任务队列中已经被放入了 flushCallbacks 函数,
     *     待执行 flushCallbacks 函数时,pending 会被再次置为 false,表示下一个 flushCallbacks 函数可以进入
     *     浏览器的任务队列了
     * pending 的作用:保证在同一时刻,浏览器的任务队列中只有一个 flushCallbacks 函数
     * @param {*} cb 接收一个回调函数 => flushSchedulerQueue
     * @param {*} ctx 上下文
     * @returns 
     */
    export function nextTick (cb?: Function, ctx?: Object) {
      let _resolve
      // 用 callbacks 数组存储经过包装的 cb 函数
      callbacks.push(() => {
        if (cb) {
          // 用 try catch 包装回调函数,便于错误捕获
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          _resolve(ctx)
        }
      })
      if (!pending) {
        pending = true
        // 执行 timerFunc,在浏览器的任务队列中(首选微任务队列)放入 flushCallbacks 函数
        timerFunc()
      }
      // $flow-disable-line
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }
    
    • flushCallbacks

    • 源码地址:/src/core/util/next-tick.js
    /**
    * 做了三件事: 
    *      1.将pending设置为false
    *      2.清空callbacks数组
    *      3.执行 callbacks 数组中的每一个函数(比如 flushSchedulerQueue、用户调用 nextTick 传递的回调函数)
    */
    const callbacks = []
    let pending = false
    
    function flushCallbacks () {
      pending = false
      const copies = callbacks.slice(0)
      callbacks.length = 0
      // 遍历 callbacks 数组,执行其中存储的每个 flushSchedulerQueue 函数/
      for (let i = 0; i < copies.length; i++) {
        copies[i]()
      }
    }
    
    • flushSchedulerQueue

    • 源码地址:/src/core/observer/scheduler.js
    /**
     * Flush both queues and run the watchers.
     * 刷新队列,由 flushCallbacks 函数负责调用,主要做了如下两件事:
     *   1、更新 flushing 为 ture,表示正在刷新队列,在此期间往队列中 push 新的 watcher 时需要特殊处理(将其放在队列的合适位置)
     *   2、按照队列中的 watcher.id 从小到大排序,保证先创建的 watcher 先执行,也配合 第一步
     *   3、遍历 watcher 队列,依次执行 watcher.before、watcher.run,并清除缓存的 watcher
     */
    function flushSchedulerQueue () {
      currentFlushTimestamp = getNow()
      // 标志现在正在刷新队列
      flushing = true
      let watcher, id
    
      /**
       * 刷新队列之前先给队列排序(升序),可以保证:
       *   1、组件的更新顺序为从父级到子级,因为父组件总是在子组件之前被创建
       *   2、一个组件的用户 watcher 在其渲染 watcher 之前被执行,因为用户 watcher 先于 渲染 watcher 创建
       *   3、如果一个组件在其父组件的 watcher 执行期间被销毁,则它的 watcher 可以被跳过
       * 排序以后在刷新队列期间新进来的 watcher 也会按顺序放入队列的合适位置
       */
      queue.sort((a, b) => a.id - b.id)
    
      // 这里直接使用了 queue.length,动态计算队列的长度,没有缓存长度,是因为在执行现有 watcher 期间队列中可能会被 push 进新的 watcher
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        // 执行 before 钩子,在使用 vm.$watch 或者 watch 选项时可以通过配置项(options.before)传递
        if (watcher.before) {
          watcher.before()
        }
        // 将缓存的 watcher 清除
        id = watcher.id
        has[id] = null
    
        // 执行 watcher.run,最终触发更新函数,比如 updateComponent 或者 获取 this.xx(xx 为用户 watch 的第二个参数),当然第二个参数也有可能是一个函数,那就直接执行
        watcher.run()
      }
    
      // keep copies of post queues before resetting state
      const activatedQueue = activatedChildren.slice()
      const updatedQueue = queue.slice()
    
      /**
       * 重置调度状态:
       *   1、重置 has 缓存对象,has = {}
       *   2、waiting = flushing = false,表示刷新队列结束
       *     waiting = flushing = false,表示可以像 callbacks 数组中放入新的 flushSchedulerQueue 函数,并且可以向浏览器的任务队列放入下一个 flushCallbacks 函数了
       */
      resetSchedulerState()
    
      // call component updated and activated hooks
      callActivatedHooks(activatedQueue)
      callUpdatedHooks(updatedQueue)
    
      // devtool hook
      /* istanbul ignore if */
      if (devtools && config.devtools) {
        devtools.emit('flush')
      }
    }
    
    /**
     * Reset the scheduler's state.
     */
    function resetSchedulerState () {
      index = queue.length = activatedChildren.length = 0
      has = {}
      if (process.env.NODE_ENV !== 'production') {
        circular = {}
      }
      waiting = flushing = false
    }
    
    
    • watcher.run

    • 源码地址:/src/core/observer/watcher.js
    /**
     * 由 刷新队列函数 flushSchedulerQueue 调用,如果是同步 watch,则由 this.update 直接调用,完成如下几件事:
     *   1、执行实例化 watcher 传递的第二个参数,updateComponent 或者 获取 this.xx 的一个函数(parsePath 返回的函数)
     *   2、更新旧值为新值
     *   3、执行实例化 watcher 时传递的第三个参数,比如用户 watcher 的回调函数
     */
    run () {
      if (this.active) {
        // 调用 this.get 方法
        const value = this.get()
        if (
          value !== this.value ||
          // Deep watchers and watchers on Object/Arrays should fire even
          // when the value is the same, because the value may
          // have mutated.
          isObject(value) ||
          this.deep
        ) {
          // 更新旧值为新值
          const oldValue = this.value
          this.value = value
    
          if (this.user) {
            // 如果是用户 watcher,则执行用户传递的第三个参数 —— 回调函数,参数为 val 和 oldVal
            try {
              this.cb.call(this.vm, value, oldValue)
            } catch (e) {
              handleError(e, this.vm, `callback for watcher "${this.expression}"`)
            }
          } else {
            // 渲染 watcher,this.cb = noop,一个空函数
            this.cb.call(this.vm, value, oldValue)
          }
        }
      }
    }
    
    
    • watcher.get

    • 源码地址:/src/core/observer/watcher.js
      /**
       * 执行 this.getter,并重新收集依赖
       * this.getter 是实例化 watcher 时传递的第二个参数,一个函数或者字符串,比如:updateComponent 或者 parsePath 返回的函数
       * 为什么要重新收集依赖?
       *   因为触发更新说明有响应式数据被更新了,但是被更新的数据虽然已经经过 observe 观察了,但是却没有进行依赖收集,
       *   所以,在更新页面时,会重新执行一次 render 函数,执行期间会触发读取操作,这时候进行依赖收集
       */
      get () {
        // 打开 Dep.target,Dep.target = this
        pushTarget(this)
        // value 为回调函数执行的结果
        let value
        const vm = this.vm
        try {
          // 执行回调函数,比如 updateComponent,进入 patch 阶段
          value = this.getter.call(vm, vm)
        } catch (e) {
          if (this.user) {
            handleError(e, vm, `getter for watcher "${this.expression}"`)
          } else {
            throw e
          }
        } finally {
          // "touch" every property so they are all tracked as
          // dependencies for deep watching
          if (this.deep) {
            traverse(value)
          }
          // 关闭 Dep.target,Dep.target = null
          popTarget()
          this.cleanupDeps()
        }
        return value
      }
    

    总结:

    Vue的一部更新机制如何实现的?

    • vue的异步更新机制是利用浏览器的异步任务队列实现的(首选事微任务其次事宏任务)。
      当响应式数据更新时会调用dep.notify,通知dep中收集的watcher去执行update方法,watcher.update将watcher放入一个watcher队列中。

    Vue 的 nextTick API 是如何实现的?

    • vue中的nextTick方法其实做了两件事:
      1.递归回调函数用try catch 包裹然后放入到callbacks数组中。
      2.执行timerFun方法,在浏览器的异步执行队列中加入刷新的callbacks函数。

    相关文章

      网友评论

          本文标题:vue源码(3):vue异步更新

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