浅曦Vue源码-45-patch 阶段-patch 方法概览

634 阅读5分钟

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

一、前情回顾 & 背景

上一篇小作文相当于复习,是 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 阶段;

本篇小作文将会进入 patch 方法的细节部分,详细讨论 DOM-diff 过程:

二、patch 方法结构

patch 方法是 createPatchFunction 工厂函数的返回值

方法位置:src/core/vdom/patch.js -> function createPatchFunction -> return function patch

方法参数:

  1. oldVnode,旧虚拟 DOM 节点

  2. vnode,新虚拟 DOM 节点

  3. hydraing,是否合成,忽略它

  4. removeonly,是否只移除

方法细节:

  1. 如果新节点不存在而旧节点存在,此时要销毁节点;

  2. 如果新节点存在旧节点不存在,说明此时是初次渲染,这个是前面我们研究的重点;

  3. 上面的新、旧节点都存在,此时都是要进入 patch 阶段了

export function createPatchFunction () {

  return function patch (oldVnode, vnode, hydrating, remoeonly) {
    if (isUndef(vnode)) {
       // 新的没有,销毁节点
    }
    
    ifisUndef(oldVnode)){
      // 旧节点不存在,说明是自定义组件的初次渲染,
      // 注意是自定义组件不是根实例,有别与
    } else {
      // 旧节点存在
      // 判断旧节点类型,如果是真实元素说明是根实例的初次渲染
      // 旧节点不是真实元素,即旧节点也是虚拟DOM时就是执行 patch
    }
  }
}

接下来的重点将是上面 else 中的代码块中关于旧节点不是真实元素的情形;

2.1 判断新节点是否存在

新节点是否存将作为当前节点在视图上是否要删除的判断依据。啥意思嘞?

就是说某个节点,在上一次视图对应的虚拟DOM树中时存在的,但是经历一番响应式数据变更重新执行 render 函数后得到的最新的虚拟 DOM 树中没有这个节点了,这就说明这个节点已经不需要了,可以销毁掉了。

return function patch (oldVnode, vnode, hydrating, removeOnly) {
  // 如果新节点不存在,老节点存在,调用 destroy 销毁老节点
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }

  // ....
}

2.1.1 invokeDestroyHook

  1. 执行组件的 destroy 钩子,即执行 $destroy 方法
  2. 执行组件各个模块(style, class, directive) 的 destroy 方法
  3. 如果 vnode 还存在子节点,则递归调用 invokeDestroyHook
function invokeDestroyHook (vnode) {
  let i, j
  const data = vnode.data
  if (isDef(data)) {
    // 执行 destroy 钩子
    if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
    for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
  }
  // 递归调用 invokeDestroyHook
  if (isDef(i = vnode.children)) {
    for (j = 0; j < vnode.children.length; ++j) {
      invokeDestroyHook(vnode.children[j])
    }
  }
}

2.1.1 vnode.data.hook.destroy

vnode.data.hook 对象时在组件创建时被合并到 vnode.data 对象上的,其中包含四个钩子:initinsertprepatchdestroy

销毁组件:

  1. 如果组件被 keep-alive 组件包裹,则组件失活,并不销毁组件实例,达到缓存组件状态的目的
  2. 如果组件未被 keep-alive 组件包裹,则调用实例的 $destroy 方法销毁组件
const componentVNodeHooks = {

  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
 
  },

  
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
 
  },

  
  insert (vnode: MountedComponentVNode) {
  
  },

  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        // 未被 keep-alive 包裹
        componentInstance.$destroy()
      } else {
        // 被 keep-alive 包裹
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}

2.2 判断旧节点是否存在

如果旧节点不存在说明这个是一个自定义组件的初次渲染,这里先不展开,稍后看到判断旧节点类型分辨是初次渲染还是 patch,这个问题也就清楚了;

2.3 else 判断旧节点类型

能够走道这里说明新旧节点都存在,因为这里是个 else 了,说明旧节点存在的,新节点已经在前面判断过了;

接着就是判断旧节点类型,如果旧节点类型是元素,即 oldVnode.nodeType 属性存在,nodeType 属性(戳这里看详情)是一个 DOM 属性,用以标识当前元素类型的,例如 1 表示元素,3 表示文本,8 表示注释等,如果这个值不为 undefined 则说明 oldVnode 是个真实的 HTML DOM 对象;

return function patch (oldVnode, vnode, hydrating, removeOnly) {
  // 如果新节点不存在,老节点存在,调用 destroy 销毁老节点
  if (isUndef(vnode)) {}

  let isInitialPatch = false
  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    // 新 VNode 存在,旧 VNode 不存在,
    // 说明这种情况下是一个组件初次渲染时出现,比如:
    // <div id='app'> <comp></comp> </div>
    // 这里的 comp 组件初次渲染时就会走这里
  } else {
    // 判断 oldVnode 是否是真实元素

    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // 不是真实元素,但是老节点和新节点是同一个节点,
      // 则是更新阶段,执行 patch 更新节点
   
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      // 是真实节点,则表示初次渲染
    }
}

2.3.1 旧节点元素类型初次渲染?

这里有个比较奇怪的问题,这个问题其实也困扰我很久?为啥旧节点元素类型就是初次渲染?

为了解开这个问题,我走了不少弯路,按照我的理解,如果有 nodeType 这个属性,难不成在 VNode 实例属性有这个属性?我把 VNode 的属性翻了个底朝天,然鹅也没发现。。。此时我发现路走偏了。

想知道为啥,只需要看看 patch 函数被调用时,传递的参数都是什么不就可以了?

patch 函数也就是 Vue.prototype.__patch__ 方法,它被 Vue.prototype._update 方法调用,如下:

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

  if (!prevVnode) {
    // 旧 VNode 不存在,标识首次渲染,即初始化页面时走这里
    // 首次渲染,即初始化页面时走这里
   
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false )
  } else {
    // 响应式数据更新时,即更新页面时走这里
    vm.$el = vm.__patch__(prevVnode, vnode)
  }

从上面的代码可以清晰看出,当初次渲染时 vm.__patch__ 函数收到的第一个参数是 vm.$el,也就传给 oldVnode 参数的实参。

在这个场景下冷不丁的一看 vm.el是个啥?你品,你细品,如果变成this.el 是个啥?你品,你细品,如果变成 `this.el你觉得熟悉吗?对的,就是表示真实挂载点的真实元素对象,他就是

` 这个元素啊~

它还有一个你常见的出场方式,这个你肯定认识:

new Vue({
  el: '#app' // 声明挂载点,这个 #app 就对应了上面的 vm.$el 属性
})

2.3.2 sameVnode 验证同一节点

为啥要验证同一节点?

先说节点是啥?可以粗暴的理解成页面上的 DOM 元素,这里的验证,是要求两段 VNode 描述的是同一段响应式数据,或者说对应的同一视图。

这是因为如果本次 diff 的不是同一节点,那就不需要再 diff 了直接走下面的 elsevnode 变成新的元素进行渲染就行了;

值得一提的是判定是否为同一节点的标准并不是 vnodeoldVnode 是同一个对象,这也没法做到,因为两次得到的是不同的 Vnode 实例,而真正的判断是一些描述 Vnode 对象的特征属性,如标签名、key 等;

function sameVnode (a, b) {
  return (
    // key 必须相同或
    a.key === b.key &&
    a.asyncFactory === b.asyncFactory && (
      (
        // 标签名相同
        a.tag === b.tag &&
        // 都是注释节点
        a.isComment === b.isComment &&
        //  都有 data 属性
        isDef(a.data) === isDef(b.data) &&
        //  input 标签的情况
        sameInputType(a, b)
      ) || (
        // 或者异步占位符节点
        isTrue(a.isAsyncPlaceholder) &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

2.4 执行 patchVnode 方法

有了前面一系列的判断,终于到了执行 patchVnode 这个方法了。我们接着捋一下执行到这里的条件:

  1. 新旧节点都存在;
  2. 旧节点类型不是元素;
  3. 新旧节点是同一节点;

以上三者同时满足才会执行重点方法 patchVnode,这里我们不再展开,我们下一篇专门讨论它;

三、总结

不得不吐槽下,用这个在线编辑器编辑好了,我看到的是已经存好的,结果等我再次从草稿箱进来时发现丢失了大半内容。。。。。

这给我提了一个醒我还是乖乖的做好备份吧,求诸人不如求己。。。。

本篇小作文讨论了一下 patch 方法的大致结构,真正 diff 两棵树的 patchVnode 方法还没开始,这里主要讨论的是如何进入到 patchVnode 的执行条件:

  1. 如果新节点不存在,就要销毁掉旧节点,因为视图不再需要它了;

  2. 判断旧节点是否存在,如果不存在说明是自定义组件的初次渲染,这个是坑,这里填一下,你会发现非自定义组件,甚至根实例的初次渲染是 oldVnode 传递真实 DOM 元素也不会使得 oldVnode 不存在。而自定义组件就不是了,它只有经历过初次渲染才会有 oldVnode,所以当 oldVnode 不存在时就是初次渲染了。

  3. 新旧节点都有,判断不是元素说明就是两颗虚拟 DOM 树了,此时调用 patchVnode 进行比对并 patch