Vue2源码【3】patch的原理

从最高视角看: 已知patch阶段也是 vm.$mount 下的逻辑,但从patch这里开始,首次构建阶段和“数据变化驱动视图改变update阶段”的逻辑才开始明显出现区别。所以单独拿出一个章节来讲patch。

从源码层面看: 可搜索return function patch (oldVnode, vnode, hydrating, removeOnly),找到patch函数的源码。而 patch 函数,实际是由 createPatchFunction 这个工厂函数所返回的:createPatchFunction利用闭包,为 patch 缓存了一些对应运行环境的接口。因此 createPatchFunction 的具体实现先可以不用看,后面也会讲到。

首先,__patch__其实就是patch函数,记住结论,其证明见于后文。对于浏览器环境,__patch__的作用就是对比新旧vnode更新vm.$el,并完成真实dom更新。而其职责,选择性由 createElm(若组件则为createComponent) + patchVnode + “销毁函数” 来完成。

patch的宏观实现:是如何选择走 createElm(若组件则为createComponent) 还是 patchVnode 还是“销毁函数” 的?

已知条件是 oldVnode 和 vnode,所以有:

  • 【1】如果新节点不存在, 旧节点也不存在, 则patch无需任何操作;
  • 【2】如果新节点不存在,但旧节点存在, 说明需要删除旧节点, 调用一个销毁钩子
  • 【3】旧节点不存在,需要创建一个新节点,用到 createElm
  • 【4】新旧节点都存在
    • 【4-1】新旧节点是相似节点, 对新旧节点做详细的对比操作,比如检查children的不同,也就是 patchVnode
    • 【4-2】新旧节点不是相似节点。一增一删,合计为替换。其中用到 createElm

所以 patch 的功能,就是对比新旧vnode来计算。选择性由 createElm(若组件则为createComponent) + patchVnode + “销毁函数” 来完成。注意一下如果vnode是组件类型,craeteElm 会完全切换为 createComponent,后面有解释。

  • createElm 的逻辑:通过虚拟节点创建真实的 DOM (通过递归子节点)并插入到它的父节点中 @非组件类型
  • vnode.elm = /*判断得出*/nodeOps.createElement(tag, vnode) @非组件类型
    • 此时会创建“占位符”节点
    • 占位符:比如<HelloWorld>出现在父模板上就是一个HelloWorld的占位符,它里面的实现HelloWorld.vue里面的模板才是“渲染vnode”。
  • 总之无论是对应真实元素的vnode还是组件类型的vnode,他们通过插入顺序是一致的,所以整个 vnode 树节点的插入顺序是先子后父。非常巧妙。

以上其实就把patch的大致工作流程梳理清楚,不交代清楚没法往下讲啊。下面可以讲讲这其中一些厉害的编写代码的技法:

patch其中的一些编写代码的技巧:(其实都可以当做是附录内容了)

__patch__其实就是patch #可不看证明过程

// 调用时机和函数签名:
    if (!prevVnode) {
      // 首次render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // 生命周期的updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }

函数签名:vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)(即patch的)作用:根据vnode更新vm.$el,并完成真实dom更新

Vue.prototype.__patch__ = inBrowser ? patch : noop :因为在服务端渲染中,没有真实的浏览器 DOM 环境,所以不需要把 VNode 最终转换成 DOM,因此是一个空函数,而在浏览器端渲染中,它指向了 patch 方法,这个方法又是由 createPatchFunction 函数 创建的:

const patch = createPatchFunction({ nodeOps, modules })

createPatchFunction这个像是高阶函数,返回一个名为patch的函数,也就是利用闭包的特性,先把一部分参数或者配置,载入到patch中。

记录 patch 函数的工厂函数 createPatchFunction 的一些信息 #不太重要

直接看src/core/vdom/patch.js的 createPatchFunction 这个大函数。它这里就是反复import。

【1】const patch = createPatchFunction({ nodeOps, modules })返回一个名为patch的函数

  • nodeOps是一个对象,上面的那些方法,都是对web平台dom操作的简单封装和增强。 (src/core/vdom/patch)
  • modules:定义了一些模块的钩子函数的实现
    • attrs
    • klass
    • events
    • domProps
    • style
    • transition

【2】定义了一大堆辅助函数。

createPatchFunction 的柯里化实现“动静分离” #一些启发

【注入函数参数的方式】比如这个腾讯的用nuxt,前后端都能用axios;但在服务端渲染的时候,调用的是 Nuxt 的 asyncData 方法进行请求;而在客户端的时候,是直接请求后台 API

我们可以思考一下为何 Vue 源码绕了这么一大圈,把相关代码分散到各个目录:

前面也介绍过,patch 是平台相关的,在 Web 和 Weex 环境,它们把虚拟 DOM 映射到 “平台 DOM” 的方法是不同的,并且对 “DOM” 包括的属性模块创建和更新也不尽相同。**因此每个平台都有各自的 nodeOpsmodules,**它们的代码需要托管在 src/platforms 这个大目录下。

而不同平台的 patch 的主要逻辑部分是相同的,所以这部分公共的部分托管在 core 这个大目录下。

综上所述:差异化部分只需要通过参数来区别,这里利用闭包实现函数柯里化,通过 createPatchFunction 把差异化参数提前传入固化。好处是:

1、先在外部判断需要什么类型的patch,然后选择把一些依赖通过参数传递进去。而不是在patch函数内部实现一大堆判断逻辑

2、不用每次调用 patch 的时候都传入 nodeOpsmodules

文章目录
  1. patch的宏观实现:是如何选择走 createElm(若组件则为createComponent) 还是 patchVnode 还是“销毁函数” 的?
  2. patch其中的一些编写代码的技巧:(其实都可以当做是附录内容了)
    1. __patch__其实就是patch #可不看证明过程
    2. 记录 patch 函数的工厂函数 createPatchFunction 的一些信息 #不太重要
    3. createPatchFunction 的柯里化实现“动静分离” #一些启发