浅曦Vue源码-44-patch 阶段-触发patch

782 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

一、前情回顾 & 背景

上一篇小作文文是 nextTick 的姊妹篇,同时又是深入理解 nextTick 精妙设计所在的过程。另外又解答了何为合并多次修改的性能优化,其核心实现如下:

  1. watcherpushqueue 之前有一步判断重复处理,即 has[id] == null
  2. watcher 被成功推入 queue 之后 has[id] = true,可保证下次 push 这个 watcher 时不会通过上一步的判重,因而不会重复加入 queue
  3. queue 中的 watcher 被重新求值前,会按 id 进行升序,用户 wacherid 小于 渲染 watcherid,所以用户 watcher 放心大胆的改响应式数据,同样是由于 has[渲染watcher id] = true 故而不会将渲染 watcher 多次加入 queue
  4. 最后,每个 watcherqueue 取出并重新求值后 has[被取出 watcher id] 被置为 null,这就保证下个 tick 中修改响应式数据时,这些 watcher 又可以被重新添加到 queue

二、示例代码

首先我们准备下面一个例子:

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Vue</title>
</head>
<body>
    <div id="app">
     <--! 点击这个按钮会修改 forProp 中 a 属性值,使之累加-->
     <button @click="goForPatch">使 forProp.a++ ===> {{forProp.a}}</button>
     <some-com :some-key="forProp"></some-com>
    </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: 'hhhhhhh' }
        }
      }
    },
    inject: ['foo']
  }

  new Vue({
    el: '#app',
    data: {
      forProp: {
        a: 100
      },
  
    },
    props: {
      someProp: {
        type: Object,
        default: function () { return { sp: 'NO 996' } }
      }
    },
    methods: {
      goForPatch () {
        this.forProp.a++;
      }
    },
    computed: {
      someComputed () {
        return this.msg + this.someProp.sp
      },
      forPatchComputed () {
        return this.forProp.a + ' reject 996'
      }
    },
   
    components: {
      someCom: sub
    }
  })
</script>
</body>
</html>

从上面的示例代码可以看出,当点击 button 按钮时,触发点击事件执行 goForPatch 方法,在该方法内修改 forProp.a 属性,使之累加。

forProp 在模板中两次被引用:

  1. <button> 元素中的文案:forProp.a
  2. 作为 prop 属性传递给子组件: <some-com :some-key="forProp" />

初次渲染时使用的是 forProp 的初始值,渲染完成后 forProp 被修改势必会触发重新渲染,这个过程就包含 DOM diff 过程;

三、触发渲染 Watcher 更新

触发 渲染 Watcher 更新的前提是 forProp.a 被更新,那么 forProp.a 被更新为什么会带来 渲染 Watcher 的更新?

带着这个问题我们回顾一下前面的全部过程:

3.1 触发点击事件

首先点击 button 触发点击事件处理函数 goForPatch,如下图所示:

image.png

goForPatch 方法中修改 forProp.a 属性,使之累加,forProp.a 的初始值为 100,累加后为 100

3.2 触发响应式数据 setter

在数据响应式初始化的时候通过 defineReactive 方法把 forProp.a 属性变成 gettersetter,当被修改时就会触发 settersetter 函数的参数接收 forProp.a 的新值 101

image.png

setter 的后面会调用 dep.notify 方法,depDep 类型的实例,每个响应式数据都与自己的 dep 实例,dep 的作用就是把它对应的响应式数据的 watcher 收集起来,当响应式数据被修改时也是由 dep.notify 通知 watcher 更新。

dep 收集的 watcher 中就包含了视图对应的 渲染 watcher

image.png

上面截图上的框中的 expression 并不是 Watcher 实例本身,而是 Watcher 实例的 get 函数,当 watcher 求值时会执行这个函数,这个函数中 vm._update/vm._render 都是大家熟悉的处理视图更新的方法;

当然,通过前面的小作文的讲解,notify 并不会直接调用这些 watcher 求值,而是通过 queueWatcher 方法合并当前事件循环中的更新,把要更新的 watcher 添加到一个队列中,在下个事件循环中再执行 watcher 的求值动作;

image.png

3.3 渲染 watcher 更新

经过 queueWatcher 合并后,在下个事件循环执行 flushSchedulerQueue 方法,队列中的 watcher 实例被逐个调用 watcher.run 方法进行重新求值:

image.png

下面是 Watcher.prototype.run 方法,在 run 方法内部会调用 Watcher.prototype.get 方法:

image.png

这个 get 方法其实就是调用创建 渲染 Watcher 实例的时候传入的 updateComponent 方法:

image.png

传入 updateComponent,创建渲染 WatchermountComponent 方法部分代码):

export function mountComponent () {
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  
  new Watcher(vm, updateComponent, noop, {
    before () {
    }
  }, true)
}

3.3 updateComponent 方法执行

从上面的代码块可以看出 updateComponent 就是调用 vm._render() 获取虚拟 DOMVNode 节点)然后把虚拟DOM 传递给 vm._update() 方法,而 vm._update 则根据有没有旧的节点(vm._vnode 属性)决定是进行初次渲染还是进行 patch,部分代码如下:

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this

  // 页面的挂载点,真实元素
  const prevEl = vm.$el

  // 旧的 VNode
  const prevVnode = vm._vnode
  
  const restoreActiveInstance = setActiveInstance(vm)

  // 新的 VNode
  // 如果执行过初次渲染后,vm._vnode 就不在是 undefined 了
  // 所以响应式数据更新时上面的 preVnode 就有值了
  vm._vnode = vnode
  
  if (!prevVnode) {
    // 老的 VNode 不存在,标识首次渲染,即初始化页面时走这里
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
  } else {
    // 响应式数据更新时,即更新页面时走这里
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
}

四、总结

好久不写这个系列的小作文了,好多内容都忘记了😂😂😂,前一阵子写 thread-loader 源码了,真的都想断更了。

但是想来想去善始善终吧,不然就彻底有遗憾了,重新捡起来和开始写第一篇一样困难。

本篇小作文相当于给自己复习了,如果是一路读过来的小伙伴到这里也就当复习了吧,本篇并没有开始写 DOM diff 的过程,而是 DOM diff 的前奏部分——触发渲染函数更新:

  1. 通过修改响应式数据(UI操作、代码修改均可)触发响应式数据的 setter 函数;

  2. setter 函数接收新的值,然后通过被修改的响应式数据的 dep 实例派发依赖该数据的 Watcher 更新;

  3. dep.notify 会把这些 watcher 添加到队列中,然后下个事件循环再集中更新这些 watcher

  4. 这些 watcher 中包含了 渲染 watcher,对 渲染 watcher 求值就会执行创建 渲染 watcher 时传入的 updateComponent 方法,这个方法就会执行 vm._update() 进而执行 vm.__patch__ 方法,进入 patch 阶段;