• Vue2源码学习笔记 - 16.响应式原理—更新调度


    前面几节文章我们已经学习了响应式的整个过程,其中在派发通知环节,不管是什么类型的 Watcher,都会执行到 Watcher.update 方法,然后由它了决策接下来的处理,我们继续来学习它之后的流程。

    加入调度队列

    update 方法中,计算属性设置 watcher.dirty 为真即结束流程,而渲染函数和常规的侦听属性则执行调用 queueWatcher(watcher) 的流程。

    file: /src/core/observer/watcher.js

    // Watcher class
    update () {
      if (this.lazy) {
        // 计算属性执行此流程
        this.dirty = true
      } else if (this.sync) {
        // 当 sync 为真是执行此流程,同步方式的侦听属性
        this.run()
      } else {
        // 渲染Watcher 和常规侦听属性执行此流程
        queueWatcher(this)
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    我们来看看 queueWatcher 的实现,它的实现单独划分了一个文件模块,上代码:

    file: /src/core/observer/scheduler.js

    ...
    export const MAX_UPDATE_COUNT = 100
    const queue: Array<Watcher> = []
    const activatedChildren: Array<Component> = []
    let has: { [key: number]: ?true } = {}
    let circular: { [key: number]: number } = {}
    let waiting = false
    let flushing = false
    let index = 0
    
    // Reset the scheduler's state.
    function resetSchedulerState () {
      index = queue.length = activatedChildren.length = 0
      has = {}
      waiting = flushing = false
    }
    
    export let currentFlushTimestamp = 0
    
    let getNow: () => number = Date.now
    
    if (inBrowser && !isIE) {
      const performance = window.performance
      if (
        performance &&
        typeof performance.now === 'function' &&
        getNow() > document.createEvent('Event').timeStamp
      ) {
        getNow = () => performance.now()
      }
    }
    
    // Flush both queues and run the watchers.
    function flushSchedulerQueue () {
      currentFlushTimestamp = getNow()
      flushing = true
      let watcher, id
    
      queue.sort((a, b) => a.id - b.id)
    
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        if (watcher.before) {
          watcher.before()
        }
        id = watcher.id
        has[id] = null
        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) {
            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)
    }
    
    function callUpdatedHooks (queue) {
      let i = queue.length
      while (i--) {
        const watcher = queue[i]
        const vm = watcher.vm
        if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'updated')
        }
      }
    }
    
    export function queueActivatedComponent (vm: Component) {
      vm._inactive = false
      activatedChildren.push(vm)
    }
    
    function callActivatedHooks (queue) {
      for (let i = 0; i < queue.length; i++) {
        queue[i]._inactive = true
        activateChildComponent(queue[i], true /* true */)
      }
    }
    
    export function queueWatcher (watcher: Watcher) {
      const id = watcher.id
      if (has[id] == null) {
        has[id] = true
        if (!flushing) {
          queue.push(watcher)
        } else {
          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
          ...
          nextTick(flushSchedulerQueue)
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112

    queueWatcher 函数主要把 Watcher 对象放入 queue 数组中,并把 Watcher.id 存入 has,然后把函数 flushSchedulerQueue 作为参数调用一次 nextTick,通过 waiting 来限制在一个任务调度中只被调用一次。那么 flushSchedulerQueue 呢?它会在下一个任务调度中被执行,这个上一节学习 nextTick 时学习过。

    Watcher 对象在放入 queue 时先判断其 id 不在 has 中,然后判断 flushing 其值为假则尚未调用 flushSchedulerQueue 刷新队列,这时直接 push 进 queue。如果 flushing 为真,则 flushSchedulerQueue 此刻正在被调用,那么把它插入到队列中剩余尚未刷新的,并且位于 id 比它大的对象前面(如果存在的话),id 比它小的对象后面(如果存在的话)。

    vue 渲染flushSchedulerQueue 刷新队列

    刷新调度队列

    flushSchedulerQueue 根据执行环境设置为优先在微任务中执行,如果不支持则在宏任务中执行。

    // Flush both queues and run the watchers.
    function flushSchedulerQueue () {
      currentFlushTimestamp = getNow()
      flushing = true // 正在刷新
      let watcher, id
    
      // 按 Watcher.id 从小到大 排序
      queue.sort((a, b) => a.id - b.id)
    
      // 遍历 queue 数组
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        if (watcher.before) {
          // 执行 watcher.before 方法
          watcher.before()
        }
        id = watcher.id
        has[id] = null
        // 执行 watcher.run 方法
        watcher.run()
        ...
      }
    
      // keep copies of post queues before resetting state
      // 激活的组件数组队列
      const activatedQueue = activatedChildren.slice()
      // 处理后的queue 复制到 updatedQueue数组队列
      const updatedQueue = queue.slice()
    
      // 重置调度状态
      resetSchedulerState()
    
      // 调用相关钩子函数
      // call component updated and activated hooks
      callActivatedHooks(activatedQueue)
      callUpdatedHooks(updatedQueue)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    在这个 flushSchedulerQueue 函数中,先对 queue 队列按 Watcher.id 从小到大排序,这是为了确保三件事:
    1、组件从父到子被更新,因为父组件先于子组件被创建。
    2、用户级的 Watcher.run(计算属性和侦听属性)先于 renderWatcher.run 被执行,因为它们都先于 renderWatcher 被创建。
    3、当在父组件 Watcher.run 执行期间子组件被销毁,则该子组件的 watcher 皆被忽略。

    在排序好之后就开始遍历执行 Watcher.run 方法,如果有 Watcher.before 则在此之前先执行它。在这个 Watcher.run 方法中,不同类型的 Watcher 执行不同操作:renderWatcher 在 run 中调用 updateComponent 重新渲染页面;watchWatcher 在 run 方法中执行用户定义的回调函数。

    遍历执行完之后,调用 resetSchedulerState 恢复一些关键变量为初始状态,迎接下一次调度任务。然后调用相关的组件的 updated 和 activated 钩子函数。

    总结:

    在任务调度中,本质还是调用 nextTick 设置下一个调度任务,并在此回调函数中对由 Watcher 对象组成的队列排序并调用其接口方法 run 执行必要的操作,比如重新渲染页面和调用侦听属性的回调函数等,总体来说流程比较简单。

  • 相关阅读:
    9、组合模式(结构性模式)
    力扣(LeetCode)88. 合并两个有序数组(C++)
    用户登录错误次数太多锁定账号
    C++学习笔记(三十五)
    智慧灯杆网关智慧交通应用
    pycharm 断点调试python Flask
    前端学习 linux —— 软件安装(Ubuntu)
    Ps:画布大小
    SpringMVC完整版详解
    linux内核网络收包过程(二)
  • 原文地址:https://blog.csdn.net/dclnet/article/details/126098273