依赖更新

什么时候更新?

通过 Object.defainePropetry 设置 set 函数,当 属性 被重新赋值 或 值变化 时,则会触发 set 函数,此时就可以 set 函数里做更新操作了,通过调用 【dep.netify】通知收集到的所有依赖(watcher) 源码如下:

  1. function defineReactive (obj, key, val) {
  2. // 在闭包中实例化 Dep 对象,用于收集 watcher 对象
  3. const dep = new Dep()
  4. // ...
  5. Object.defineProperty(obj, key, {
  6. get: function reactiveGetter () {
  7. // ...
  8. if (Dep.target) {
  9. // 进行依赖收集,将观察者实例(watcher)放入 dep.sub 数组中
  10. dep.depend()
  11. }
  12. return value
  13. },
  14. set: function reactiveSetter (newVal) {
  15. // ...
  16. // dep.notify 将通知所有保存在 dep.subs 的 watcher 实例
  17. dep.notify()
  18. }
  19. })
  20. }

需要注意的是:动态添加的 属性(property )或移除 不会触发更新,所有 响应式 的属性、对象 都必须提前声明,哪怕是一个空值,对于对象对于数组


如何更新?

  1. 【dep.netify】方法被触发后将遍历逐个 调用 watcher.update 方法。源码如下:

    1. class Dep {
    2. constructor () {
    3. // 存放 watcher 依赖的数组
    4. this.subs = []
    5. }
    6. notify () {
    7. const subs = this.subs.slice()
    8. for (let i = 0, l = subs.length; i < l; i++) {
    9. subs[i].update() // 触发 watcher update 方法
    10. }
    11. }
    12. }
  2. update 方法 调用后就直接更新页面吗?当然不是。会先开启一个 队列Queue 本质是一个数组),将发在 同一事件循环 内所有更新的 watcher 推入队列内,如果同一个 wathcer 被多次触发,根据 watcher.id 判断已经存在 队列 里则不会 推入。然后使用 异步方法(nextTick) 下一次 事件循环“Tick”中,执行 队列 中的所有 wathcer.run() 方法。源码如下:

update 方法

  1. class Watcher {
  2. update () {
  3. // ...
  4. queueWatcher(this)
  5. }
  6. }

queueWatcher 方法将 wathcer 推入队列

  1. let has = {} // 用于 watcher 去处重处理
  2. let queue = [] // 存放 watcher 队列
  3. let waiting = false
  4. let flushing = false
  5. function queueWatcher (watcher) {
  6. if (has[id] == null) { // 判断 watcher 是否已经添加过
  7. has[id] = true
  8. if (!flushing) {
  9. queue.push(watcher) // watcher 推入队列
  10. } else {
  11. // 按照 已排好序的 队列,找到位置插入新的 watcher
  12. let i = queue.length - 1
  13. while (i > index && queue[i].id > watcher.id) {
  14. i--
  15. }
  16. queue.splice(i + 1, 0, watcher)
  17. }
  18. }
  19. if(!waiting) {
  20. waiting = true
  21. // nextTick 方法,将注册 flushSchedulerQueue 回调函数 在下一次 Tick 进行触发
  22. nextTick(flushSchedulerQueue)
  23. }
  24. }
  • waiting 标记, true 时表示,已经把 flushSchedulerQueue 注册到 微任务上,开始执行逐个执行 队列的 watcher 直到全部将 队列 全部清空后,才重置为 false。


  • flushing 标记,为 true 时表示,队列正在执行更新,执行前 会先给所有 watcher 按照 watcher.id 进行 升序排序。此时再有 wathcer 进来,根据自身 watcher.id 从 队列 再 按照排好的顺序插入。直到全部将 队列 全部清空后,才重置为 false


  • nextTick 方法,将 flushSchedulerQueue 注册到 微任务 执行

flushSchedulerQueue 方法 逐个执行 队列 watcher

  1. function flushSchedulerQueue () {
  2. // 给 watcher 按 id 升序排序
  3. queue.sort((a, b) => a.id - b.id)
  4. let watcher
  5. for (index = 0; index < queue.length; index++) {
  6. watcher = queue[index]
  7. has[ watcher.id ] = null
  8. watcher.run() // 执行更新
  9. }
  10. }
  • 为什么要给 watcher 按照 id(id是自增的) 升序排序?


  1. 这样做可以保证,组件是 从父组件到子组件 的顺序更新,因为父组件总是比子组件先创建

  2. 一个组件的 user watchers(用户创建的)会比 render watcher(页面更新) 先运行,因为 user watchers 往往比 render watcher 更早创建

  3. 如果一个子组件 在父组件 watcher 运行期间被销毁,它的 watcher 执行将被跳过

  • 为何 从父组件到子组件 的顺序更新呢?

因为 父组件跟子组件是有联系的 如:props ,所以,父组件必须先更新,把最新数据传给 子组件,子组件再更新,此时才能获取最新的数据。

  • 使用 队列、微任务异步更新 目的是什么?


  1. 减少重复更新。去除重复的 watcher ,避免不必要计算和 DOM 操作
  2. 加快异步代码的执行。Micro Task微任务) 执行,会比 Macro task(宏任务) 更早执行,所以优先使用
  3. 避免频繁地更新。缓存在同一事件循环中发生的所有数据变更

根据 HTML Standard,在每个 task 运行完以后,UI 都会重渲染,那么在 Micro Task微任务中就完成数据更新, 当前 task 结束就可以得到最新的 UI 了。反之如果新建一个 task 来做数据更新,那么渲染就会进行两次。 参考:https://www.zhihu.com/question/55364497/answer/144215284


  1. run 方法 执行更新,主要是执行 watcher 创建之前保存的 更新函数(页面渲染、computed、watch …),执行时页面渲染时会将会 重新收集新的依赖,并且清除 原来存在,本次渲染不存在的依赖,完成依赖收集之后得到 新的 VNode 将会跟 旧的 VNode 进行 Diff 比较,找出最小的更新点,进行 原生的DOM 操作更新,至此完成 数据 到 页面的更新过程。源码如下:
    1. class Watcher{
    2. get () {
    3. pushTarget(this) // 将自身设置给 Dep.target,用以依赖收集
    4. // ...
    5. value = this.getter.call(vm, vm) // 执行更新函数,重新收集依赖
    6. //...
    7. popTarget() // 取出并设置给 Dep.target
    8. this.cleanupDeps() // 清除上一次存在,但本次渲染不存在的依赖
    9. return value
    10. }
    11. // ...
    12. run () {
    13. const value = this.get() // 执行更新函数,重新收集依赖
    14. // ..
    15. const oldValue = this.value // 保存旧的 value 值
    16. // ..
    17. this.value = value // 设置新的 value 值
    18. // 触发更新后回调
    19. this.cb.call(this.vm, value, oldValue)
    20. }
    21. // ...
    22. }

nextTick 方法实现原理

前面提到 Vue 使用 nextTick 方法 ,在内部维护了一个 任务队列 去配合 宏微任务 实现异步更新,其目的是:

  1. 减少 宏微任务的注册。尽量把所有的异步执行代码 放在一个 宏微任务中,减少消耗,如果一个异步代码就注册一个 宏微任务的话,那么完全执行完代码肯定慢得多

  2. 加快异步代码的执行。优先使用 微任务 使其异步代码能尽快执行,内部对异步队列尝试使用原生的Promise.then、MutationObserver和setImmediate,如果执行环境不支持,则会采用setTimeout(fn, 0)代替。

  3. 避免频繁地更新。Vue 中就算修改多次数据,页面还是只会更新一次。避免多次修改数据导致 多次频繁更新页面,实现让多次修改只用更新最后一次

下面是具体源码:

  1. let isUsingMicroTask = false // 是否是 微任务
  2. const callbacks = [] // 存放 异步所需执行的函数
  3. let pending = false // 判断 宏微任务 是否在运行中,为 true 宏微正在运行中,还未执行,当执行完时 重置为 false
  4. let timerFunc
  5. function flushCallbacks () {
  6. pending = false
  7. const copies = callbacks.slice(0)
  8. callbacks.length = 0
  9. for (let i = 0; i < copies.length; i++) {
  10. copies[i]()
  11. }
  12. }
  13. /*
  14. * 尝试使用原生的 Promise.then、MutationObserver 和 setImmediate
  15. * 如果执行环境不支持,则会采用 setTimeout(fn, 0)
  16. */
  17. if (typeof Promise !== 'undefined'){
  18. const p = Promise.resolve()
  19. timerFunc = () => {
  20. p.then(flushCallbacks)
  21. }
  22. } else if (!isIE && typeof MutationObserver !== 'undefined') {
  23. let counter = 1
  24. const observer = new MutationObserver(flushCallbacks)
  25. const textNode = document.createTextNode(String(counter))
  26. observer.observe(textNode, {
  27. characterData: true
  28. })
  29. timerFunc = () => {
  30. counter = (counter + 1) % 2
  31. textNode.data = String(counter)
  32. }
  33. } else if (typeof setImmediate !== 'undefined') {
  34. timerFunc = () => {
  35. setImmediate(flushCallbacks)
  36. }
  37. } else {
  38. timerFunc = () => {
  39. setTimeout(flushCallbacks, 0)
  40. }
  41. }
  42. nextTick (cb, ctx) {
  43. let _resolve
  44. callbacks.push(() => {
  45. cb.call(ctx)
  46. })
  47. if (!pending) {
  48. pending = true
  49. timerFunc()
  50. }
  51. }
  1. 通过判断 pending 来确定是否需要注册宏微任务

当第一次注册的时候,把 pending 设置为 true,表示任务队列已经在开始了,同一时期内无需注册了

然后在 任务队列 执行完毕之后,再把 pending 设置为 false(在 flushCallbacks 中)

  1. callbacks 是一个数组,用于存放各种 需要异步执行的函数,例如:

image.png

就会把你设置的这个回调,放到 callbacks 数组中
image.png

  1. flushCallbacks 方法

1、复制一遍 callbacks

2、把 原来 callbacks 清空

3、遍历 逐个执行 复制出来 callbacks

这个方法是直接用于 宏微任务 执行的实际函数
image.png

Diff 比较 更新过程中的比较算法

  • Diff 作用?
  • 如何比较?
  • 为什么这么比较?

Vue Diff 比较

更新流程

点击查看【processon】