vue2源码学习 (14) 虚拟DOM-4.patch

39 阅读4分钟

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

14. 虚拟DOM-4.patch

start

  • 上一节提到了 __patch__,这一节继续梳理相关的逻辑

__patch__

// `src/platforms/web/runtime/index.js`import { patch } from './patch'Vue.prototype.__patch__ = inBrowser ? patch : noop
​
// noop 其实就是一个空函数
export function noop(a?: any, b?: any, c?: any) {}

__patch__ 赋值的时候做了区分,如果是浏览器端的,才需要修改真实 DOM,所以赋值为 patch函数。其他情况是不需要修改 DOM 的,直接赋值一个 noop ;

noop 看到源码,其实也就是一个空函数

patch

// src\platforms\web\runtime\patch.jsimport * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
​
// 是一个函数
export const patch: Function = createPatchFunction({ nodeOps, modules })

patch是什么?看到最后一行的代码,patch其实就是 createPatchFunction函数执行后的返回值(是一个函数类型的)。

解释一下传入的参数

nodeOps 封装了一系列 DOM 操作的方法;

modules 定义了一些模块的钩子函数的实现;

createPatchFunction

// src\core\vdom\patch.js// 创建 patch 的函数
export function createPatchFunction(backend) {

  /* 省略了函数的声明 */


  // 主干逻辑 patch ,滑动到底部,createPatchFunction 函数归根到底返回的是 patch这个函数
  // 四个参数, 旧节点;_render返回的新节点;是否是服务端渲染;removeOnly 是给 transition-group 用的,之后会介绍。
 return function patch(oldVnode, vnode, hydrating, removeOnly) {
    // 1. 新节点 是否是undeifned 或者 null
    if (isUndef(vnode)) {
      // 旧节点存在 调用销毁钩子
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode);
      return;
    }

    let isInitialPatch = false;
    const insertedVnodeQueue = [];

    // 2.如果旧节点为空,新节点存在
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      // 空挂载(可能作为组件),创建新的根元素
      isInitialPatch = true;

      // 依据 新节点vnode 直接创建 真实dom
      createElm(vnode, insertedVnodeQueue);
    } else {
      // oldVnode 是不是真实的 DOM 元素
      const isRealElement = isDef(oldVnode.nodeType);

      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        // 给现有根节点打补丁

        // 3. 是相似的 vnode,开始深入对比
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.

          /* 服务端渲染的逻辑 */
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR);
            hydrating = true;
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true);
              return oldVnode;
            } else if (process.env.NODE_ENV !== "production") {
              warn(
                "The client-side rendered virtual DOM tree is not matching " +
                  "server-rendered content. This is likely caused by incorrect " +
                  "HTML markup, for example nesting block-level elements inside " +
                  "<p>, or missing <tbody>. Bailing hydration and performing " +
                  "full client-side render."
              );
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it

          /* 如果不是服务端,返回一个空的 vnode */
          oldVnode = emptyNodeAt(oldVnode);
        }

        // replacing existing element
        // 4. 替换现有的元素
        const oldElm = oldVnode.elm;
        const parentElm = nodeOps.parentNode(oldElm);

        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        );

        // update parent placeholder node element, recursively
        // 递归地更新父占位符节点元素
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent;
          const patchable = isPatchable(vnode);
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor);
            }
            ancestor.elm = vnode.elm;
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor);
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert;
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]();
                }
              }
            } else {
              registerRef(ancestor);
            }
            ancestor = ancestor.parent;
          }
        }

        // destroy old node
        // 销毁旧节点
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0);
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode);
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
    return vnode.elm;
  };
}

createPatchFunction这个方法代码行数比较多,接近 1000 行左右的 js。

直接到最后一行,可以看到它本质上是返回的一个名为 patch 的函数。

createPatchFunction函数开头的部分代码我做了省略,着重研究一下这个返回的patch 函数

patch

1.patch的四个参数

  1. 旧节点;
  2. _render 返回的新节点;
  3. 是否是服务端渲染;
  4. removeOnly 是给 transition-group 用的,之后会介绍。

2.认识一下patch中有关的函数

2.1 isUndef
export function isUndef(v: any): boolean %checks {
  return v === undefined || v === null
}
// 判断 传入的参数 是否 为 undefined 或者为 null
2.2 isDef
export function isDef(v: any): boolean %checks {
  return v !== undefined && v !== null
}
​
//判断 传入的参数 是否 不是 undefined 也不是 null
2.3 sameVnode
// 是不是相似的 虚拟节点
function sameVnode(a, b) {
  return (
    a.key === b.key &&
    a.asyncFactory === b.asyncFactory &&
    ((a.tag === b.tag &&
      a.isComment === b.isComment &&
      isDef(a.data) === isDef(b.data) &&
      sameInputType(a, b)) ||
      (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
  )
}
​
// 参数:`ab`分别为旧节点,新节点。
// 作用:判断新旧 vnode 是否有所不同。
// 核心逻辑: 可以看到,判断是否是相似节点,判断了 key,tag,asyncFactory,isComment

这里就可以看到, key 对于 vnode对比 的重要性了。

2.4 isRealElement
const isRealElement = isDef(oldVnode.nodeType);

// 什么情况 isRealElement 会为 true。
// 组件初始化的时候,会传入一个真实DOM,oldVnode.nodeType就会存在。

3. patch主干逻辑

3.1 vnode 不存在,oldVnode 存在,调用 invokeDestroyHook 方法销毁 oldVnode;

旧的存在,新的不存在,说明旧的那些不需要了,所以直接销毁即可;

3.2 vnode 存在,oldVnode 不存在,说明是最初挂载,将 isInitialPatch 标记置为 true,调用了 createElm(vnode, insertedVnodeQueue) 方法创建一个新的根元素;

旧的不存在,新的存在,直接创建一个新的根元素

createElm 创建真实的dom

oldVnode 、vnode 都存在:

3.3 是相同节点,这里有个判断是同一个节点的方法 sameVnode ,调用 patchVnode 方法对节点进行比对;

相同节点,就需要 patchVnode 去 diff

3.4 如果 vnode 和 oldVnode 不是同一个节点,那么根据 vnode 创建新的元素并挂载至 oldVnode 父元素下。

直接拿到 之前节点的父元素,然后将最新的元素更新到 oldVnode 父元素下,删除旧的元素即可。

4. 小节

  • 整体看下来,从 __patch__createPatchFunction函数执行后的返回的patch函数。
  • createPatchFunction函数执行后的返回的patch函数 到 创建真实的dom。

end

到目前为止可以梳理到这么一个流程:

new-vue.png