浅曦Vue源码-41-patch 阶段-触发响应式更新

一、前情回顾 & 背景

上一篇的小作文详细讨论了 createElm 这个方法的逻辑,这个方法根据 VNode 创建真实的元素,其中包含两种场景:

  1. 如果 VNode 是自定义组件,则调用 createComponent 方法处理;
  2. 如果是普通元素,则通过 nodeOps.createElement 创建原生 HTML 元素;

在上一篇的最后梳理了整个从 new Vueinsert 的全过程执行栈流程,这一个过程就是整个 Vuenew Vue 到渲染到页面上的全过程。

但是 Vue 是个响应式的系统,前面的过程只完成了一半,而剩下的一半就是当响应式数据发生变化时,Vue 的渲染 watcher 收到通知触发重新渲染,也就是大家常说的 patch 阶段了。

对于 Vue 自身内部设计来说,只要是 VNode 渲染到页面的过程都叫做 patch,并没有拆成两个大的流程,虽然内部是区分还是第一次渲染你还是响应式数据发生变更而渲染。但是这个对于初学 Vue 源码的人来说不友好,所以我自作聪明的把它分成两个过程还起了名字;

  1. 第一次渲染我们叫他初次渲染,也就是前面的挂载阶段,第一次把模板变成 DOM 渲染到页面;
  2. 后面的这一部分叫做 patch 阶段,后面的这个阶段就是响应式数据更新触发重新渲染,你最爱的 dom diff 就是这个阶段的小可爱了(她虐你千百遍,你待她如初恋);

二、过程分析 + 示例代码

为了适应 patch 阶段需要响应式数据的更新,我们在模板中加入了一个按钮 button#btn,这个按钮点击事件handler 会修改 data.forProp.a 属性:

  • forPatchComputed 这个计算属性依赖 data.forProp
  • <some-com /> 组件接收的 someKey prop 绑定的数据也是 data.forProp

test.html 代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Vue</title>
</head>
<body>
<div id="app">
 <button id="btn" @click="goForPatch">使 forProp.a++</button>
 <some-com :some-key="forProp"><div slot="namedSlot">SLOT-CONTENT</div></some-com>
 <div>forPatchComputed = {{forPatchComputed}}</div>
 <div class="static-div">静态节点</div>
</div>
<script src="./dist1/vue.js"></script>
<script>
  const sub = {
    template: `
      <div style="color: red;background: #5cb85c;display: inline-block">
    <slot name="namedSlot">slot-fallback-content</slot>
    {{ someKey.a + ' foo' }}
   </div>`,
    props: {
      someKey: {
        type: Object,
        default () {
          return { a: '996-rejected!!' }
       }
     }
   };
  debugger
  new Vue({
    el: '#app',
    data: {
      forProp: {
        a: 100
      }
    },
    methods: {
      goForPatch () {
         this.forProp.a++
      }
    },
    computed: {
       // 计算属性
       forPatchComputed () {
         return this.forProp.a + ' reject 996'
       }
    },
    components: {
      someCom: sub
    }
  })
</script>
</body>
</html>
复制代码

所以当 data.forProp 被修改时,现在有三个 watcher 要触发更新了:

  1. forPatchComputed 计算属性对应的 watcher,注意计算属性的 watcherlazy 的;
  2. some-com 对应的渲染 watcher
  3. div#app 这个根实例的模板对应的渲染 watcher

三、触发响应式更新

在响应式系统中是一个明显的观察者模式,这就要求我们搞清楚谁是观察者,谁是被观察者,谁负责这二者间的通信

  • 观察者就是 Watcher 实例,watcher 从字面量已经看出是观察者(watch 英文翻译不是注视、看吗~);
  • 被观察者自然就是数据本身了,比如 data.forProp 这个对象;
  • 负责二者的通信就是 Dep 实例了,Dep 是依赖,注意是名词不是动词(想表达的是被 watcher 依赖,如果变成动词就是依赖别人了);每个响应式数据都有 depdep 负责收集用到这个数据的 watcher,然后数据改变了则派发通知,让 watcher 行动起来;

3.1 复习响应式的实现

数据的响应式一共有三个组成部分:

  1. 在初始化响应式数据的时候将 data 通过 defineReactive 方法(核心是 Object.defineProperty)将 data.forProp 变成 gettersetter,当被访问时通过 getter 被触发收集依赖,当被需改时触发 setter 通知依赖这个数据的 watcher 们更新;
  2. 初始化响应式数据时处理 computed 的逻辑:
    • 2.1 给每个计算属性创建一个 watcher,而创建 Watcher 类的实例时传入的 expOrFn 即要求值的函数,就是定义计算属性时声明的方法例如上面的例子:forPatchComputed () { return this.forProp.a + 'reject 996!!!' }
    • 2.2 计算属性是 lazy watcher,即这个计算属性被访问的时候才会求值;
    • 2.3 什么时候求值呢?被访问到时候,forPatchComputed 是根实例的模板对应的 render 函数执行的时候就会拿 forPatchComputed 对应的值,这时求值;
  3. 修改 data.forProp.a 触发 setter,而前面 getter 已经知道有三个 watcher 依赖了这个 forProp,此时通知他们三个更新;

谁来负责收集依赖 watcher 们和通知 watcher 们更新呢?Dep 类,在数据响应式初始化的时候给每个数据都创建一个 Dep 的实例,dep.denpend 收集依赖 watcherdep.notify 通知 watcher 更新。watcher 更新则是通过 watcher.update() 方法实现的;

function defineRective () {
  const dep = new Dep();
  
  Object.defineProperty(target, key {
    get () {
      if (Dep.target) dep.depend()
    }
    set () {
      dep.notify()
    }
  })
}
复制代码

下图是点击 button#btn 后因修改 this.forProp.a 而触发 setter 进入到 setter

image.png

3.2 Dep.prototype.notify

export default class Dep {

  // 把 watcher 放大响应式数据的依赖列表中
  depend () {
    if (Dep.target) {
      // Dep.target 是响应式数据 getter 时设置的,值是 watcher 实例
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()

    // 遍历 dep 中存储的 watcher,执行 watcher.update()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
复制代码

下图是点击按钮后断点调试进入到 dep.notify,你会发现 this.subs 中有三个 watcher

image.png

  1. 第一个是 forPatchComputed 这个计算属性 watcher,如图

image.png 2. 第二个是 div#app 这个根实例模板对应的渲染 watcher

image.png

  1. 第三个则是 <some-com /> 自定义组件的渲染 watcher

image.png

3.3 Wather.prototype.update

export default class Watcher {
  constructor (...) {}
 
  update () {

    if (this.lazy) {
      // 懒执行的 watcher 走这里
      // 将 dirty 置为 true,
      // 就可以让 计算属性对应的 getter 被访问到的时候
      // 再触发重新计算 computed 回调函数的执行结果
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 更新时一般都在这里,
      // 将 watcher 放入到 watcher 队列,
      // 然后异步更新队列
      queueWatcher(this)
    }
  }
}
复制代码

3.3.1 计算属性的 update

image.png

3.3.2 渲染 watcher 的 update

image.png

queueWatcher 是一个重点,我们为它单独开一篇;

四、总结

本篇小作文作为 patch 阶段的第一篇主要做了以下工作:

  1. 重新修改 test.html 加入了可以修改响应式数据的 button#btn 元素,以及绑定点击事件修改 data.forProp.a
  2. 重新梳理了完整的响应式流程,包含依赖收集、修改数据、派发更新的过程;并且明确了 WatcherDep以及响应式数据间的依赖和被依赖关系以及三者协作过程;
  3. 通过修改 this.forProp.a 进入到了 dep.notify(),接着看到了作为计算属性lazy watcher普通 watcherwatcher.update() 方法中的不同处理方式:
    • 3.1 lazy watcher 把 this.dirty 置为 true;这就可以使得计算属性的缓存失效,当计算属性再次被访问到的时候,就会重新求值,这个过程我们在说 Watcher 的时候详细介绍过 computed 的缓存原理;

    • 3.2 普通 watcher 包括渲染 watcher用户 watcher 都会执行 queueWatcher 方法进行异步的队列更新;

猜你喜欢

转载自juejin.im/post/7078897921801846798