likes
comments
collection
share

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

作者站长头像
站长
· 阅读数 22

render函数的构建

渲染器render,接收三个参数虚拟DOM,容器,isSVG.可以将虚拟DOM渲染到指定容器中

讲在前面

在开始阅读源码和实现之前,先了解一些前备知识,不至于在阅读时一头雾水:

渲染器模块结构

vue在实现渲染器时把码解耦为runtime-coreruntime-dom

  • runtime-dom中封装了浏览器中直接操作DOMAPI
  • runtime-core中封装了vue处理虚拟DOM的核心逻辑

这样做的好处: 专门定义一套DOM操作的接口,在core模块通过这些接口完成DOM操作,使核心逻辑的实现不依赖于平台,当需要适配其他平台如uniapp,weex,ssr时,只需实现这些接口,即可实现各个平台的渲染器

HTML Attribute和DOM Properties

通过js操作的DOM的时候会涉及两种属性:

  • HTML Arrtibute:指直接放在HTML标签中的属性HTML属性参考,是HTML标签的一部分,可以用来提供初始值,通过setAttribute,getAttribute,removeAttribute进行操作

  • DOM properties:DOM对象上的属性 Element属性,直接通过DOM对象的属性进行操作

后续在源码中就可以看到: vue在处理props时,需要区分HTML AttributeDOM Properties

阅读和实现的顺序

vue渲染器的代码非常庞大而且逻辑复杂,因此,本次debugger与代码实现只做核心中的核心部分.这还不够,为了更好理解,和以往查看与实现不同,将分这几个部分分别进行查看和实现:

  • 第一部分: 查看Element类型的虚拟DOM处理过程
    • 首先关注整个渲染器的架构,然后实现架构
    • 查看挂载流程,然后实现挂载流程
      • 处理class
      • 处理style
      • 处理事件
      • 处理HTML AttributeDOM Properties
    • 查看更新流程,然后实现更新流程
    • 查看卸载流程,然后实现卸载流程
  • 第三部分: 查看并实现其他类型虚拟DOM的挂载和更新(组件类型除外,将在下一篇中详聊)

渲染ELEMENT类型vnode

debugger

使用如下测试用例:

<!DOCTYPE html>
    ...
    <script src="../../dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      const { h, render } = Vue
      // 渲染一个Element | Text_children类型的虚拟node
      const vnode = h('div', { class: 'test',style:{color:'red'} }, 'hello')
      debugger
      render(vnode, document.querySelector('#app'))
    </script>
  </body>
</html>

老样子,open with live server打开浏览器,进入debugger,这次我们主要查看render函数的整体逻辑,不去关注具体怎么渲染

  1. 这里的代码是用来调用真正的render函数

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

  1. 猛点下一步,直到进入真正的render中,在这里,

    • 如果vnode不在,则说明是卸载组件,走卸载逻辑

    • 如果vnode存在,则说明是渲染组件,走渲染逻辑,进入patch

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

  1. patch中有很多参数,我们只用关注前四个

    • n1: 旧虚拟DOM,n2:新虚拟DOM,container: 容器,anchor:锚点,用于标记元素插入位置

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

  1. 进入patch函数,先做了两个判断,这里与当前测试用例无关,不关注,

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

  1. 之后,根据新虚拟DOMtypeshapFlag,来对不同类型的虚拟DOM进行不同的处理,这里我们的虚拟DOM类型是ELEMENT,所以会进入processElement,这里为了更清晰,放的是编辑器中折叠后的源码:

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

  1. 进入processElement,这是一个专门处理Element类型的虚拟DOM的,在这个函数种,根据旧虚拟DOM存在与否,决定是挂载渲染还是更新渲染,我们是第一次渲染,所以进入mountElement

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

  1. mountElement中主要做了这些事情:

    1. hostCreateElement创建DOM对象

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

  1. children类型进行不同处理:hostSetElement处理TEXT_CHILDREN类型的children`

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

  1. patchProps处理props

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

  1. hostInsertDOM对象添加到页面的容器中

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

至此一个ELEMENT | TEXT_CHILDREN类型的vnode就渲染完成了,这一遍,我们更多关注整个渲染器的架构,处理props等细节,在完成渲染器架构之后再展开

实现渲染器基本结构

总结一下,整个结构如下:

render:区分是unmount还是patch

patch:根据虚拟DOM类型不同进行不同的处理

processElement:区分本次patch是挂载mountElemet还是更新patchElement

mountElement:创建DOM对象,根据子元素类型处理children,处理props,将DOM对象添加到容器

有了这些结构,就可以把渲染器的架子搭起来了:

packages/runtime-core/src/renderer.ts,

  1. createBaseRenderer,在该函数中实现各个方法,并返回一个包含render的对象,按照上述分析,分别实现render,patch,processElement,mountElement方法

    function createBaseRenderer(): {
      render: Function;
    } {
    
      /**
       * @message: 渲染函数
       * @param vnode 新的虚拟DOM
       * @param {*} container 容器
       */
      const render = (
        vnode: Vnode | null,
        container: Element & { _vnode: Vnode | null }
      ) => {
        const needUnMount = vnode === null;
        if (needUnMount) {
          if (container._vnode) {
           // TODO: 需要卸载
          }
        } else {
          // TODO 需要挂载或更新
          patch(container._vnode, vnode, container);
        }
        container._vnode = vnode;
      };
    
      /**
       * @message: 对vnode打补丁(挂载和更新)
       * @param {Vnode} oldVnode
       * @param {Vnode} newVnode
       */
      const patch = (
        oldVnode: Vnode | null = null,
        newVnode: Vnode,
        container: Element,
        anchor = null
      ) => {
        if (oldVnode === newVnode) {
          return;
        }
        // TODO 如果新旧节点类型不同,删除旧节点
    
        const { type, shapeFlag } = newVnode;
        switch (type) {
          case Text:
            // TODO 处理TEXT类型vnode
            break;
          case Fragment:
            // TODO 处理Fragment类型vnode
            break;
          case Comment:
            // TODO 处理Comment类型vnode
            break;
          default: {
            if (shapeFlag & ShapeFlags.ELEMENT) {
              // 处理ELEMENT类型vnode
              processElement(oldVnode, newVnode, container, anchor);
            } else if (shapeFlag & ShapeFlags.COMPONENT) {
              // TODO 处理COMPONENT类型vnode
            }
          }
        }
      };
    
      /**
       * @message: 处理Element类型vnode的挂载和更新
       */
      const processElement = (
        oldVnode: Vnode | null = null,
        newVnode: Vnode,
        container: Element,
        anchor = null
      ) => {
        if (oldVnode === null) {
          // 挂载
          mountElement(newVnode, container, anchor);
        } else {
          // TODO 更新
          
        }
      };
    
      /**
       * @message: DOM挂载
       */
      const mountElement = (vnode: Vnode, contanier: Element, anchor = null) => {
        // 创建DOM对象
        // 处理props
        // 处理children
        // 将DOM对象添加到容器
      };
    
    
      return {
        render,
      };
    }
    

debugger-挂载

实现了基本架构,接下来先查看ELEMENT类型渲染逻辑,然后实现其中的各个方法,重新进入debugger,来到mountElement函数

  1. 在这里首先创建了DOM对象,进入hostCreateElement

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

  1. hostCreateElement方法,通过document.createElement创建DOM对象,并返回

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

  1. 执行完毕,回到mountElement,接下来处理children,此时的childrenTEXT类型所以进入hostSetElementText,设置文本值

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

  1. 设置完毕,下面开始处理props,进入hostPatchProps

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

  1. hostPatchProps中对class,style,事件,HTML Attribute,DOM Properties进行分别处理,这里先不处理事件,事件比较复杂,之后单独处理

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

5.1 处理class,将各个类名,连接起来

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

5.2 处理style,两次循环,一次设置新值,一次清空旧值

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

5.3 处理DOM Properties,通过DOM对象直接赋值

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

5.4 处理HTML Attribute,通过setAttribute设置新的值

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

  1. 处理完所有props,最后一步调用hostInsert方法把处理完的DOM对象添加到容器中

vue3源码阅读与实现: runtime运行时-render模块,实现原生标签的挂载render函数的构建 渲染器ren

到此一个Element | TEXT_CHILDREN类型的vnode就成功渲染到页面上了.然后来分别实现一下上述过程中提到的方法,并把每个方法塞到render架构中

封装DOM操作

这种host开头的方法,都是runtime-dom中定义的方法,在core中使用时添加了一个host前缀,所以,实现这些方法时,不需要加host前缀.

同时为了能在core模块的createBaseRenderer中使用,我们需要对架构增加一些处理,把各种API以参数的形式传过来:

packages\runtime-core\src\renderer.ts

// runtime-dom中封装的各种兼容API
interface RenderOptions {
  // 向父元素中锚点位置插入一个元素
  insert: (el: Element, parent: Element, anchor: Element | null) => void;
  // 根据标签名创建HTML元素
  createElement: (tag: string) => Element;
  // 设置元素的text
  setElementText: (el: Element, text: string) => void;
  // 为元素的某个属性打补丁
  patchProps: (el: Element, key: string, preValue: any, nextValue: any) => void;
  // 删除元素
  remove: (el: Element) => void;
  createText: (text: string) => any;
  createComment: (text: string) => any;
  setText: (el: Element, text: string, anchor: null) => void;
}

export function createRenderer(options: RenderOptions) {
  return createBaseRenderer(options);
}

// 将各个模块封装的DOM操作相关的api合并
const renderOptions = extend({ patchProps }, nodeOps);
let renderer;
export const render = (...args) => {
  return ensureRenderer().render(...args);
};

function ensureRenderer(): { render: Function } {
  return renderer || (renderer = createBaseRenderer(renderOptions));
}

function createBaseRenderer(renderOptions: RenderOptions): {
  render: Function;
} {
  // 获取runtime-dom中的api
  const {
    insert: hostInsert,
    createElement: hostCreateElement,
    setElementText: hostSetElementText,
    patchProps: hostPatchProps,
    remove: hostRemove,
    createText: hostCreateText,
    setText: hostSetText,
    createComment: hostCreateComment,
  } = renderOptions;

.....
}

packages\runtime-dom\src\nodeOps.ts中封装大部分的DOM操作:

// 封装浏览器相关的DOM操作
export const nodeOps = {
  insert,
  createElement,
  setElementText,
  remove,
  createText,
  setText,
  createComment,
};
/**
 * @message: 插入元素
 */
function insert(el: Element, parent: Element, anchor: Node | null = null) {
  parent.insertBefore(el, anchor);
}
/**
 * @message: 创建元素
 */
function createElement(tag: string) {
  const el = document.createElement(tag);
  return el;
}

/**
 * @message: 设置元素文本内容
 */
function setElementText(el: Element, text: string) {
  el.textContent = text;
}

/**
 * @message: 删除DOM元素
 */
function remove(el: Element) {
  el?.remove();
}

/**
 * @message: 创建纯文本节点
 */
function createText(text: string) {
  return document.createTextNode(text);
}

function setText(el: Element, text: string) {
  el.nodeValue = text;
}

实现patchProps

packages\runtime-core\src\renderer.tsmountElement

  const mountElement = (vnode: Vnode, contanier: Element, anchor = null) => {
    const tag = vnode.type;
    const el = hostCreateElement(tag);
    vnode.el = el;
    // 初始化属性
    const props = vnode.props;
    for (let key in props) {
      if (props.hasOwnProperty(key)) {
        hostPatchProps(el, key, null, props[key]);
      }
    }
    // 为真实DOM添加子元素
    const shapeFlag = vnode.shapeFlag;
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      // 处理text类型的子元素
      hostSetElementText(el, vnode.children);
    } else if (shapeFlag & ShapeFlags.COMPONENT) {
      // TODO 处理组件类型的子元素
    }
    // 将真实DOM添加到容器
    hostInsert(el, contanier, anchor);
  };

packages\runtime-dom\src\patchProps.ts中:

import { isOn } from "@vue/shared";
import { patchClass } from "./module/class";
import { patchDOMProps } from "./module/props";
import { patchAttrs } from "./module/attr";
import { patchStyles } from "./module/styles";
import { patchEvent } from "./module/event";

// 封装处理DOM属性的操作
export const patchProps = (
  el: Element,
  key: string,
  preValue: any,
  nextValue: any
) => {
  if (key === "class") {
    patchClass(el, nextValue);
  } else if (key === "style") {
    // 处理style属性
    patchStyles(el, preValue, nextValue);
  } else if (isOn(key)) {
    // 处理事件
    patchEvent(el, key, preValue, nextValue);
  } else if (shouldSetAsProp(el, key, nextValue)) {
    // 处理 DOM Props
    patchDOMProps(el, key, nextValue);
  } else {
    // 处理HTML Attribute
    patchAttrs(el, key, nextValue);
  }
};

/**
 * @message: 判断key是否需要通过DOM对象直接设置
 */
function shouldSetAsProp(el, key, value) {
  // form标签的属性是只读的,必须通过setAttribute设置
  if (key === "form") {
    return false;
  }

  //
  if (key === "list" && el.tagName === "INPUT") {
    return false;
  }

  if (key === "type" && el.tagName === "TEXTAREA") {
    return false;
  }

  return key in el;
}

实现patchClass

packages\runtime-dom\src\module\class.ts中:

/**
 * @message: 为class属性打补丁
 */
export const patchClass = (el: Element, value: string) => {
  if (value) {
    el.className = value;
  } else {
    el.removeAttribute("class");
  }
};

实现patchStyle

packages\runtime-dom\src\module\styles.ts中:

import { EMPTY_OBJ, isArray, isObject, isString } from "@vue/shared";

/**
 * @message: 处理样式
 */
export const patchStyles = (el: Element, preValue: any, nextValue: any) => {
  debugger;
  const style = (el as HTMLElement).style;
  if (nextValue && !isString(nextValue)) {
    // 删除旧的
    if (preValue && !isString(preValue)) {
      for (let key in preValue) {
        setStyle(style, key, "");
      }
    }
    // 添加新的
    for (let key in nextValue) {
      setStyle(style, key, nextValue[key]);
    }
  }
};

const setStyle = (style: CSSStyleDeclaration, key: string, value: any) => {
  style[key] = value;
};

总结

至此一个基本的渲染器架构和原生标签的渲染已经实现,之后的更新过程将涉及diff操作,下一篇文章再详细介绍

转载自:https://juejin.cn/post/7405510539210539034
评论
请登录