render, h, mount, patch
render, h, mount, patch
Section titled “render, h, mount, patch”为了更好的揭示其原理,后文的示例和实现都是简化版,这样避免我们因为各种和主要原理无关的细节而耗费精力。
接下来本文主要会讲述四点:
- 弄清楚Vue中的VNode是什么,VDOM又是什么;
- 组件选项的render函数作用是什么,h又是什么;
- mount(app, '#app')是如何将- VNode渲染到界面容器中;
- patch的目的究竟是什么,它大概是怎么个流程;
VNode 是什么
Section titled “VNode 是什么”在Vue2 ,VNode存在一个对应的类,通过它可以实例化很多不同类型的vnode实例,这些不同类型的vnode实例各自可以代表不同类型的DOM元素:
export default class VNode {  constructor(    tag,    data,    children,    text,    elm,    context,    componentOptions,    asyncFactory,  ) {    this.tag = tag;    this.data = data;    this.children = children;    this.text = text;    this.elm = elm;    this.ns = undefined;    this.context = context; // rendered in this component's scope    this.functionalContext = undefined; // real context vm for functional nodes    this.functionalOptions = undefined; // for SSR caching    this.functionalScopeId = undefined; // functioanl scope id support    this.key = data && data.key;    this.componentOptions = componentOptions;    this.componentInstance = undefined; // component instance    this.parent = undefined; // component placeholder node
    // strictly internal    this.raw = false; // contains raw HTML? (server only)    this.isStatic = false; // hoisted static node    this.isRootInsert = true; // necessary for enter transition check    this.isComment = false; // empty comment placeholder?    this.isCloned = false; // is a cloned node?    this.isOnce = false; // is a v-once node?    this.asyncFactory = asyncFactory; // async component factory function    this.asyncMeta = undefined;    this.isAsyncPlaceholder = false;  }
  // DEPRECATED: alias for componentInstance for backwards compat.  get child() {    return this.componentInstance;  }}在Vue3中,VNode是一个interface(ts中的类型的一种表示方式,可以描述一个对象的结构),它同样可以描述不同类型的vnode对象,这些vnode对象同样可以代表不同类型的DOM元素:
export interface VNode<  HostNode = RendererNode,  HostElement = RendererElement,  ExtraProps = { [key: string]: any },> {  __v_isVNode: true;  [ReactiveFlags.SKIP]: true;  type: VNodeTypes; // 节点类型  props: (VNodeProps & ExtraProps) | null; // 节点相关属性  key: string | number | symbol | null;  ref: VNodeNormalizedRef | null;
  slotScopeIds: string[] | null;  children: VNodeNormalizedChildren; // 子节点  component: ComponentInternalInstance | null; // 组件实例(如果是组件节点的话)  dirs: DirectiveBinding[] | null; // 指令  transition: TransitionHooks<HostElement> | null;
  // DOM  el: HostNode | null; // 真实的 DOM 元素  anchor: HostNode | null; // fragment anchor  target: HostElement | null; // teleport target  targetAnchor: HostNode | null; // teleport target anchor
  //...}VNode本质上就是一个JS对象,它能够描述不同的DOM元素,原生DOM对象的所具备的各种属性,VNode它基本都有;
也正是因为此,VNode可以理解为一个描述页面真实DOM元素的对象,它描述了Vue该如何通过它去创建一个真实的DOM元素。
举一个最简单的例子:
const vnode = {  // 说明该节点是元素节点(它具有标签) --> createElement(创建方式)  tag: 'div'  // 元素节点一般有子节点, 它的子节点同样是 vnode  children: [  {    // 如果只有 text, 那么说明这个节点是文本节点, --> createTextNode(创建方式)    text: 'xxx'  },  childVnode2  //...  ]}VDom 是什么
Section titled “VDom 是什么”由VNode组成的Tree就可以看作VDom,这里其实可以类比为Dom Tree,VDom 和 Dom是存在对应关系的。

VNode 是的常见作用是什么
Section titled “VNode 是的常见作用是什么”Vue中在首次渲染视图时,会调用render生成第一个VNode(此时页面中没有任何节点),是直接将VNode对应的Dom元素插入到界面上。
当在下一次渲染时,会重新调用render得到生成一个新的VNode,也就是newVNode,那么上一次调用render函数得到的VNode就是oldVNode(oldVNode对应当前页面上的DOM元素),然后调用patch(oldVNode, newVNode)比较两者的不同,得到最小操作代价来更新界面,使得界面上的元素能够和newVNode的对应的Dom结构相同。
我们知道,Vue是通过将VNode渲染成真实的DOM元素,然后插入到容器中。
这里正是因为VNode这一个抽象层的存在,将Vue和真实的DOM元素实现了解耦,也就是说,实现了和平台相关的DOM API实现了解耦。
在Vue中它是通过nodeOps来描述的渲染API,也就是说,其他平台只要结合自身平台实现nodeOps即可,这样就实现了跨平台的效果。
render,h,mount 的概念和简单实现
Section titled “render,h,mount 的概念和简单实现”一个Vue组件如何变为页面上的DOM元素,大概流程:

其中VNode是通过render中的h得到的,而VNode到视图又是通过mount实现的。
render,h 代码实现
Section titled “render,h 代码实现”首先我们知道,render函数会返回调用h --> hyperScript得到的VNode,因此代码如下:
function render() {  return h();}
// 得到 VNode, 这里我们简单模拟实现一个, VNode 的核心就是描述 DOM 元素function h(tag, props, children) {  return {    // Dom 标签(div, span, h1...)    tag,    // Dom 属性(class: xxx, style: xxx, onClick: xxx...)    props,    // 子节点(Vnode)    children,  };}mount 代码实现
Section titled “mount 代码实现”mount的作用是将VNode解析为真实的Dom结构然后插入到指定容器中,代码如下:
// 将 VNode 转换成 Dom, 然后插入到 container 下function mount(vnode, container) {  const tag = vnode.tag;  const el = document.createElement(tag);  // 将真实 dom 元素放在 vnode 上  vnode.el = el;  const events = (vnode.events = new Map());
  // 设置属性, 监听器, ...  const props = vnode.props;  if (props) {    Object.keys(props).forEach((key) => {      const val = props[key];      // 针对事件的处理      if (key.startsWith("on")) {        const eventName = key.slice(2).toLowerCase();
        // 把事件缓存到 vnode 上, 方便后期我们 patch 时进行比较(因为对于函数来说, 如果不能缓存其引用, 不方便进行比较判断)        const eventCache = events.get(eventName) ?? [];        eventCache.push(val);        events.set(eventName, eventCache);
        el.addEventListener(eventName, val);      } else {        // 针对普通属性的处理        el.setAttribute(key, val);      }    });  }
  const children = vnode.children;  // 如果 children 不为 null 且不为 undefined  if (!isEmpty(children)) {    if (["number", "string"].includes(typeof children)) {      el.textContent = children.toString();    } else if (Array.isArray(children)) {      children.forEach((child) => {        // 递归插入        mount(child, el);      });    }  }  container.appendChild(el);}
<html lang="en">  <head>    <title>Document</title>    <style>      .red {        color: red;      }    </style>  </head>  <body>    <div id="app"></div>
    <script>      // ...      function render() {        return h("div", { class: "red" }, [          h("h1", null, "hello"),          h("h1", null, "world"),        ]);      }
      const vnode = render();      mount(vnode, document.getElementById("app"));    </script>  </body></html>patch 的概念和简单实现
Section titled “patch 的概念和简单实现”patch本身就有补丁,修补的意思。
patch(oldVNode, newVNode, container)的作用是比较两者的不同,以最小操作代价(DOM操作)在现有Dom(oldVNode.el代表当前的界面上的元素)上进行修改,最终使得界面上的Dom元素和newVNode描述的Dom相同即可。
- 
如果 oldVNode和newVNode完全相同,那么直接返回;
- 
如果 oldVNode不存在,直接挂载newVNode即可; 
- 
如果 oldVNode存在, 但是newVNode不存在,那么直接将oldVNode对应的界面元素卸载掉即可;
- 
判断 oldVNode和newVNode的类型是否相同:- 
如果不同,直接替换 oldVNode为newVNode即可; 
- 
如果相同,则进行深层次的比较: - 
比较两者 props是否相同:- newProps上有,- oldProps上没有;
- newProps上有,- oldProps上有, 但是两者不同;
- oldProps上有,- newProps上没有;
 
- 
比较两者子元素是否相同: - 
如果 newVNode.children的类型为字符串或者数字;- 
如果 oldVNode.children的类型为字符串或者数字且不和newVNode.children相同,那么直接覆盖,el.textContent = newVNode.children; 
- 
如果 oldVNode.children的类型不为字符串或者数字,那么先清空oldVNode.children,然后插入newVNode.children,el.innerHtml = ''; el.textContent = newVNode.children;
 
- 
- 
如果 newVNode.children的类型为数组;- 如果oldVNode.children的类型为数字或者字符串,清空oldVNode.children文本内容,然后遍历newVNode.children执行mount操作;
- 如果oldVNode.children的类型也为数组,这里我们采用简化版diff算法:- 取newChildren和oldChildren的公共长度的元素直接遍历进行patch;
- 对于newChildren比oldChildren多的元素进行删除;
- 对于newChildren比oldChildren少的元素进行新增;
 
- 取
 
- 如果
 
- 
 
- 
 
- 
patch 代码实现
Section titled “patch 代码实现”// n1: oldVNode// n2: newVNodefunction patch(n1, n2, container) {  // 如果 oldVNode 和 newVNode 完全相同, 那么直接返回  if (n1 === n2) return;
  // 如果 oldVNode 不存在, 那么直接将 n2 插入到 dom 中  if (!n1) {    mount(n2, container);    return;  }
  // 如果 oldVNode 存在, 但是 newVNode 不存在, 直接移除 oldVNode  if (!n2) {    container.removeChild(n1.el);    return;  }
  // 当两个节点的类型相同时(这里先我们简单的用 tag 来表示类型)  if (n1.tag === n2.tag) {    // 当需要进行更新操作时
    // 这里我们将 oldVNode 的上挂载的真实的 DOM 元素赋值给 newVNode, 这样我们后续直接在 el 上进行操作即可    const el = (n2.el = n1.el);    const events = (n2.events = n1.events);
    // 比较 props
    // 首先先比较 props: { class: xxx, style: xxx }    const oldProps = n1.props ?? {};    const newProps = n2.props ?? {};
    for (let key of Object.keys(newProps)) {      const oldVal = oldProps[key];      const newVal = newProps[key];      // 1. newProps 上有, oldProps 上没有      // 2. newProps 上有, oldProps 上有, 但是两者不同
      // 针对事件直接覆盖(临时做法) , 因为事件绑定的是函数, 每次都是一个新的引用      if (key.startsWith("on")) {        const eventName = key.slice(2).toLowerCase();        const eventCache = events.get(eventName);        // 如果 newVal 和 oldVal 不同        if (!eventCache.includes(newVal)) {          // 删除掉多余的事件(oldVNode 和 newVNode 不同的)          el.removeEventListener(eventName, oldVal);          eventCache.splice(eventCache.indexOf(oldVal), 1);        }        // 增加 newVNode 上的事件        el.addEventListener(eventName, newVal);        eventCache.push(newVal);      } else if (newVal !== oldVal) {        el.setAttribute(key, newVal);      }    }
    for (let key of Object.keys(oldProps)) {      // 1. oldProps 上有, newProps 上没有      // 那么需要去除多余的 prop      if (!Reflect.has(newProps, key)) {        if (key.startsWith("on")) {          // oldVNode: { onEnter: xxx }  newVNode { onClick: xxx }          // 清除 onEnter 相关绑定事件          const eventName = key.slice(2).toLowerCase();          const eventCache = events.get(eventName);
          // 清空          eventCache.forEach((cb) => el.removeEventListener(eventName, cb));          events.set(eventName, []);        } else {          el.removeAttribute(key);        }      }    }
    // 比较子元素
    // 如果 newVNode 内容是字符串或者数字(两者渲染到界面上都是字符串)    if (["number", "string"].includes(typeof n2.children)) {      const n2Text = n2.children.toString();      // 如果 oldVNode 的内容也是字符串, 那么直接覆盖即可      if (["number", "string"].includes(typeof n1.children)) {        const n1Text = n1.children.toString();        // 当两者内容不同时        if (n2Text !== n1Text) {          el.textContent = n2Text;        }      } else {        // 如果是其他类型, 那就需要先清空        el.innerHtml = "";        el.textContent = n2Text;      }    } else if (Array.isArray(n2.children)) {      // 真实的 patch 做法不是这样的, 这里是简化版的 diff 过程      // 会造成很多冗余的 dom 操作(比如多余的新建元素操作等...)      // 这里你不妨想想为什么会造成这些冗余操作, 是否有更好的 diff 的做法对这些 oldVNode 做到最大程度的复用以减少操作代价(Dom 操作)
      // 如果 newVNode 的子元素是数组      // 那就需要比较两个数组, 来寻找不同, 然后对 dom(el) 进行操作      if (Array.isArray(n1.children)) {        const oldChildren = n1.children;        const newChildren = n2.children;
        const oldChildrenLen = oldChildren.length;        const newChildrenLen = newChildren.length;
        const commonLength = Math.min(oldChildrenLen, newChildrenLen);
        // 比较他们的公共长度的元素, 直接进行 patch        for (let i = 0; i < commonLength; i++) {          const oldChild = oldChildren[i];          const newChild = newChildren[i];          patch(oldChild, newChild, el);        }
        // oldChildren 比 newChildren 要多, 那么就要删除        if (oldChildrenLen > newChildrenLen) {          const toBeRemovedChilds = oldChildren.slice(commonLength);          toBeRemovedChilds.forEach((child) => {            el.removeChild(child.el);          });        } else if (oldChildrenLen < newChildrenLen) {          // newChildren 比 oldChildren 要多, 那么就是需要新增的元素          const toBeAppendChilds = newChildren.slice(commonLength);          toBeAppendChilds.forEach((child) => {            el.appendChild(child);          });        }      } else if (typeof n1.children === "string") {        // 清空 oldVNode 的内容        el.textContent = "";        // 然后将 newVNode 的子元素挂载到 el 上        n2.children.forEach((child) => {          mount(child, el);        });      }    }  } else {    // 如果类型都不同, 那么就可以直接抛弃 oldVNode, 直接替换为 newVNode 即可    container.removeChild(n1.el);    mount(n2, container);  }}
<html lang="en">  <head>    <title>Document</title>    <style>      .red {        color: red;      }    </style>  </head>  <body>    <div id="app"></div>
    <script>      // ...      const container = document.getElementById("app");      const vnode1 = h("div", { class: "red" }, [h("h1", null, "hello")]);      mount(vnode1, container);
      const vnode2 = h("div", { class: "green" }, [        h("span", null, "changed!"),      ]);      patch(vnode1, vnode2, container);    </script>  </body></html>结合响应式最终实现示例效果
Section titled “结合响应式最终实现示例效果”接下来的实现就以这个示例为目标。
运行之后:

<body>  <div id="app"></div>  <script src="https://unpkg.com/vue@next"></script>  <script>    const { createApp, h } = Vue    // 创建 app(应用)实例    const app = createApp({      data() {        return {          count: 0        }      },      render() {        return h('div', null, [          h(            'button',            {              onClick: () => {                this.count++              }            },            '增加'          ),          h('h1', null, this.count)        ])      }    })    // 将 vnode 挂载到 dom 元素上    app.mount('#app')  </script></body>大致分解一下示例的步骤:
- 
Vue组件选项中的render函数会返回调用h --> hyperScript得到的VNode;
- 
当在调用 app.mount时, 会触发render得到VNode,然后会直接将整个VNode变成DOM元素插入到页面容器中;
- 
data在内部会被转化为响应式数据,同时render也有依赖响应式数据的地方,因此会触发响应式数据的get收集依赖。当响应式数据发生改变时,会触发这些收集的依赖(也就是副作用),然后会重新触发 render,然后调用patch(oldVNode, newVNode)比较两者的不同。最终得到最小的操作代价来更新界面,使得界面上的元素能够和 newVNode的对应的Dom结构相同;
模拟实现 createApp
Section titled “模拟实现 createApp”这里我们实现的createApp 主要就是逐步完成上文中拆分的三个步骤。
响应式相关实现可以参考: 响应式实现。
createApp方法的参数是一个组件选项,它的返回值是一个对象,且具有mount方法:
function createApp(component) {  // 对 mount 进行重写  const _mount = (selector) => {    if (!selector.startsWith("#")) {      throw `${selector} is not start with '#'`;    }    const container = document.querySelector(selector);    if (!container) {      throw `can't find dom by ${selector}`;    }
    let isMounted = false;    let currentVNode, oldVNode, newVNode;    // 这里结合之前讲过的响应式    // 在 render 中是有响应式数据的, 因此这个更新函数能被收集为依赖    // 当响应式数据发生变化时, 重新触发    watchEffect(() => {      // 还没挂载时, 直接插入      if (!isMounted) {        currentVNode = component.render();        mount(currentVNode, container);        isMounted = true;      } else {        // 对于已经挂载过的节点,  当前节点变为 oldVNode,        oldVNode = currentVNode;        // 获取最新的 render(), 将其赋值为 newVNode        newVNode = component.render();        patch(oldVNode, newVNode, container);        // 当前最新的渲染结果就为 currentVNode        currentVNode = newVNode;      }    });  };
  return {    mount: _mount,  };}可以看运行效果,最终和示例相同。

<body>  <div id="app"></div>  <script>    const app = createApp({      // data 需要时响应式数据      data: reactive({        count: 0,      }),      render() {        return h("div", null, [          h(            "button",            {              onClick: () => {                this.data.count++;              },            },            "增加",          ),          h("h1", null, this.data.count),        ]);      },    });    app.mount("#app");  </script></body>