Vue源码解析系列——数据驱动篇:patch的执行过程

本文详细解读了Vue 2.6.12中__patch__函数的工作原理,涉及函数柯里化和平台模块选择,展示了如何通过闭包实现DOM操作的复用。从实例开始,逐步剖析createPatchFunction和patch方法,深入理解虚拟DOM更新过程。
摘要由CSDN通过智能技术生成

准备

vue版本号2.6.12,为方便分析,选择了runtime+compiler版本。

回顾

如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:《Vue源码分析系列:目录》

__patch__的定义

pathch定义在platforms/web/runtime/index中。由于Vue既可以运行在浏览器也可以运行在服务端,所以这里对patch做了一个兼容性处理:

import { patch } from './patch'

Vue.prototype.__patch__ = inBrowser ? patch : noop

进入./patch:

patch

这里引入了一些不同平台的dom操作和一些dom属性,并传递给createPatchFuntion函数,之后将createPatchFuntion函数的返回值赋值给patch


import * as nodeOps from "web/runtime/node-ops"; // 不同平台的一些dom操作
import { createPatchFunction } from "core/vdom/patch";
import baseModules from "core/vdom/modules/index";
import platformModules from "web/runtime/modules/index"; //不同平台的一些dom模块

const modules = platformModules.concat(baseModules);

export const patch: Function = createPatchFunction({ nodeOps, modules });

进入createPatchFuntion函数:

createPatchFuntion

这个函数的函数体非常长,定义了非常多的辅助函数,这里就不贴了,之后要用到这些辅助函数的时候在进行讲解。
我们直接看createPatchFuntion函数的返回值吧:

  return function patch(oldVnode, vnode, hydrating, removeOnly) {
    //somecode...
  };

返回值patch()方法

哎,这个函数的返回值也是一个函数。说明了这里使用了闭包,也就是说,其实__patch__真正执行的就是这个createPatchFuntion函数的返回值函数。
那为什么Vue要绕这么一大圈来定义这个patch呢?为什么不能直接在__patch__中就定义好这个返回值patch()中所有的逻辑?
其实这里使用了一种编程思想,函数柯里化
这样做的好处是,在执行这个返回值patch()方法时,就不用一直去判断是属于哪个平台的domModules和DOM操作,因为作用域链的关系,在第一次执行patch时就已经将对应平台的domModules和DOM操作全部牵引在自己的作用域链上了。这样就可以做到变量的复用。

我们接下来详细阅读下返回值patch()的函数体:

 /**
   * 使用函数柯里化牵引着一些平台化的dom操作
   */
  return function patch(oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode);
      return;
    }

    let isInitialPatch = false;
    const insertedVnodeQueue = [];

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true;
      createElm(vnode, insertedVnodeQueue);
    } else {
      //判断是不是真实的dom
      const isRealElement = isDef(oldVnode.nodeType);
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        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
          //将真实的dom转换为一个VNode
          oldVnode = emptyNodeAt(oldVnode);
        }

        // replacing existing element
        //真实的dom => #app
        const oldElm = oldVnode.elm;
        //父节点 => body
        const parentElm = nodeOps.parentNode(oldElm);

        // create new node
        //深度优先创建真实DOM
        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
        //在dom上删除老的节点
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0);
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode);
        }
      }
    }

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

代码很多,我们一点一点分析。
先来看函数前两个参数oldVnodevnode,一个是旧的节点,一个是新节点。
我们在core/instance/lifecycle中可以了解到一个逻辑:

const prevVnode = vm._vnode;
if (!prevVnode) {
// initial render
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
  // updates
  vm.$el = vm.__patch__(prevVnode, vnode);
}
  • 如果前一次没有渲染过DOM,就说明是第一次渲染,所以__patch__第一个参数是$el,也就是#app的dom
  • 如果之前有渲染过DOM,第一个参数就传递上一次渲染的VNode。

也就是说,这个函数中的oldVnode既有可能是真实的DOM,又有可能是一个VNode类型。知道了这个之后我们继续向下看。

//判断是不是真实的dom
const isRealElement = isDef(oldVnode.nodeType);

if (isRealElement) {
  //将真实的dom转换为一个VNode
  oldVnode = emptyNodeAt(oldVnode);
}

isDef(oldVnode.nodeType);判断是不是一个真实的DOM节点,,如果是就执行emptyNodeAt(),我们来看一下emptyNodeAt()

function emptyNodeAt(elm) {
  return new VNode(
    nodeOps.tagName(elm).toLowerCase(),
    {},
    [],
    undefined,
    elm
  );
}

可以看到,patch$el转换成了一个VNode,所以最后oldVnode都会变成一个VNode。
再往下看:

//真实的dom => #app
const oldElm = oldVnode.elm;
//父节点 => body
const parentElm = nodeOps.parentNode(oldElm);

//深度优先创建真实DOM
createElm(
  vnode,
  insertedVnodeQueue,
  oldElm._leaveCb ? null : parentElm,
  nodeOps.nextSibling(oldElm)
);

将之前真实的DOM赋值给oldElm,父DOM节点赋值给parentElm,之后调用createElm(),传入parentElmoldElm的兄弟节点。进入createElm():

辅助函数createElm()

/**
   * 一个深度优先创建包含子节点的真实dom的方法
   * @param {*} vnode
   * @param {*} insertedVnodeQueue
   * @param {*} parentElm
   * @param {*} refElm
   * @param {*} nested
   * @param {*} ownerArray
   * @param {*} index
   */
  function createElm(
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(vnode);
    }

    vnode.isRootInsert = !nested; // for transition enter check
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return;
    }

    const data = vnode.data;
    const children = vnode.children;
    const tag = vnode.tag;
    //如果tag是个有效值
    if (isDef(tag)) {
      if (process.env.NODE_ENV !== "production") {
        if (data && data.pre) {
          creatingElmInVPre++;
        }
        if (isUnknownElement(vnode, creatingElmInVPre)) {
          warn(
            "Unknown custom element: <" +
              tag +
              "> - did you " +
              "register the component correctly? For recursive components, " +
              'make sure to provide the "name" option.',
            vnode.context
          );
        }
      }
      //创建一个真实的dom
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode);
      //css scoped相关
      setScope(vnode);

      /* istanbul ignore if */
      if (__WEEX__) {
        // in Weex, the default insertion order is parent-first.
        // List items can be optimized to use children-first insertion
        // with append="tree".
        const appendAsTree = isDef(data) && isTrue(data.appendAsTree);
        if (!appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue);
          }
          insert(parentElm, vnode.elm, refElm);
        }
        createChildren(vnode, children, insertedVnodeQueue);
        if (appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue);
          }
          insert(parentElm, vnode.elm, refElm);
        }
      } else {
        //如果有子节点,就递归创建子节点的真实dom
        //深度优先的插入节点方法
        createChildren(vnode, children, insertedVnodeQueue);
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue);
        }
        //将子节点插入本节点
        insert(parentElm, vnode.elm, refElm);
      }

      if (process.env.NODE_ENV !== "production" && data && data.pre) {
        creatingElmInVPre--;
      }
    } else if (isTrue(vnode.isComment)) {
      //如果是个注释节点,就创建注释
      vnode.elm = nodeOps.createComment(vnode.text);
      insert(parentElm, vnode.elm, refElm);
    } else {
      //如果是个文本节点,就创建文本
      vnode.elm = nodeOps.createTextNode(vnode.text);
      insert(parentElm, vnode.elm, refElm);
    }
  }

代码还是很长,一段一段看:

if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  return;
}
const data = vnode.data;
const children = vnode.children;
const tag = vnode.tag;

if (isDef(tag)) {
      if (process.env.NODE_ENV !== "production") {
        if (data && data.pre) {
          creatingElmInVPre++;
        }
        if (isUnknownElement(vnode, creatingElmInVPre)) {
          warn(
            "Unknown custom element: <" +
              tag +
              "> - did you " +
              "register the component correctly? For recursive components, " +
              'make sure to provide the "name" option.',
            vnode.context
          );
        }
      }
      //创建一个真实的dom
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode);
      //css scoped相关
      setScope(vnode);

先尝试着调用createComponent检查是不是一个组件,如果是组件就执行createComponent里面的逻辑,如果不是就继续向下。
之后检查tag是不是一个有效值,如果是有效值再判断是不是一个未知组件,如果是,就抛出警告。如果不是就先给这个VNode创建一个真实的DOM节点。继续:

//如果有子节点,就递归创建子节点的真实dom
//深度优先的插入节点方法
createChildren(vnode, children, insertedVnodeQueue);
//将子节点插入本节点
insert(parentElm, vnode.elm, refElm);

调用createChildren()看名称像是创建子DOM节点用的方法。然后再调用insert看名字像是把子节点插入刚刚创建的DOM节点。
不说了,先进入createChildren()的身体:

辅助函数createChildren()

这个方法的逻辑倒也不复杂,说白了就是判断children如果是个数组的话就递归调用createElm()

function createChildren(vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    if (process.env.NODE_ENV !== "production") {
      checkDuplicateKeys(children);
    }
    for (let i = 0; i < children.length; ++i) {
      createElm(
        children[i],
        insertedVnodeQueue,
        vnode.elm,
        null,
        true,
        children,
        i
      );
    }
  } else if (isPrimitive(vnode.text)) {
    nodeOps.appendChild(
      vnode.elm,
      nodeOps.createTextNode(String(vnode.text))
    );
  }
}

辅助函数insert()

这个方法就是调用原生DOM在父节点中插入子节点,没啥好说的。

 /**
   * 在父节点中插入子节点
   * @param {*} parent
   * @param {*} elm
   * @param {*} ref
   */
function insert(parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (nodeOps.parentNode(ref) === parent) {
        nodeOps.insertBefore(parent, elm, ref);
      }
    } else {
      nodeOps.appendChild(parent, elm);
    }
  }
}

结合上面两个createChildren()insert()先后调用来看,createElm()其实是一个深度优先地创建真实DOM节点的方法。
好了,我们回到createElm继续向下看:

else if (isTrue(vnode.isComment)) {
	//如果是个注释节点,就创建注释
	vnode.elm = nodeOps.createComment(vnode.text);
	insert(parentElm, vnode.elm, refElm);
} else {
	//如果是个文本节点,就创建文本
	vnode.elm = nodeOps.createTextNode(vnode.text);
	insert(parentElm, vnode.elm, refElm);
}
  • 如果发现vnodeisComment标记,也就是说是个注释节点,就创建真实DOM的注释节点。
  • 如果其他条件都不满足,就说明是个文本节点,于是就创建真实DOM的文本节点。

到这里,整一个createEle就大概分析完毕了,当然,这里面还有一些关于Vue组件的判断我没提到,这部分之后我们专门开一个篇章来阅读Vue组件的源码是怎么写的。
好了,我们在回到返回值patch函数,接下去看:

 //在dom上删除老的节点
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0);
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode);
        }
      }
    }

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

这里使用了原生DOM的删除节点的方法,将老的节点删除了,如果你在这之前打一个debugger的话,你会发现老节点和新节点同时存在于DOM树上,执行这边的方法之后,老节点就被删除了。
最后返回了一个渲染后的真实的DOM元素。
至此,__patch__阅读完毕

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱学习的前端小黄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值