Vue3框架原理学习实现(二)-vnode和mount

目的

这次主要目的是实现vue3react框架中的主要的vnode虚拟节点的初步简单实现和如何渲染挂在到真实的dom节点上面

相关博客
相关视频

虚拟dom

  1. 为什么vue和react框架中要用虚拟dom?

这些很多文章都讲过,减少没必要的dom渲染,提高性能,并且利用框架提供的数据双向绑定语法,开发者可以只关注代码实现逻辑之类的。这里贴篇博客,自己记录一下
vue核心之虚拟DOM(vdom)

  1. vnode

通过h函数或者createElement函数创建返回的对象。他并不是一个真实的dom对象,但是存储要渲染的真实dom节点的全部信息,可以告诉框架如何渲染真实dom节点及其子节点,我们把这样的节点描述为虚拟节点 (Virtual Node),也常简写它为VNode虚拟 DOM是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。

VNode

这里我们简单只实现下面四种类型

划分VNode种类

  1. Element

element对应普通元素,使用document.createElement创建。type指签名,props指元素属性,children指子元素,可以为字符串或者数组。为字符串时代表只有一个文本子节点。

//类型定义
{
    type: string,
    props: Object,
    children: string | Vnode[]
}
//举例
{
    type: 'div',
    props: {class: 'a'},
    children: 'hello'
}
复制代码
  1. Text

Text 对应的是文本节点,使用document.createTextNode创建。type所以应定义为一个Symbol,props为空,children为字符串,指具体的内容。

//类型定义
{
    type: Symbol,
    props: null,
    children: string
}
复制代码
  1. Fragment

Fragment是一个不会真实渲染的节点。相当于我们在vue中使用的template和React中的Fragmenttype 应定为一个Symbolprops 为空, children 子节点为数组。最后渲染时子节点会挂载到Fragment的父节点上面

//类型定义
{
    type: Symbol,
    props: null,
    children: Vnode[]
}
复制代码
  1. Component

Component是组件,组件有自己特殊的一套渲染方法,但是组件的最终参数,也是上面三种Vnode的集合。组件的type,就是定义组件的对象,props即是外部传入的props数据,children即是组件的slot(这里就不实现了)

{
    type: Object,
    props: Object,
    children: null
}
//示例
{
    type: {
        template: `{{msg}} {{name}}`,
        props: ['name'],
        setup(){
            return {
                msg: 'hello'
            } 
        } 
    },
    props: {name: 'world'}
}
复制代码

ShapeFlags

给每一个Vnode一个标记他的类型和children类型。利用位运算左移量不同来标识不同的Vnode节点,利用运算来标识所拥有的子节点类型,利用 &运算来Vnode 的类型和子节点类型,以下所示

//runtime/vnode.js
export const ShapeFlags = {
  ELEMENT: 1, //00000001
  TEXT: 1 << 1, //00000010
  FRAGMENT: 1 << 2, //00000100
  COMPONENT: 1 << 3, //00001000
  TEXT_CHILDREN: 1 << 4, //00010000
  ARRAY_CHILDREN: 1 << 5, //00100000
  CHILDREN: (1 << 4) | (1 << 5), //00110000
};
复制代码

另外定义两个 TextFragment 全局 Symbol标识,用来代表VNode TextFragment类型

//runtime/vnode.js
export const Text = Symbol('Text');
export const Fragment = Symbol('Fragment');
复制代码

h()实现

h函数,或者叫createVNodeElement 是用来创建Vnode的一个方法,接收type, props, children三个属性,返回Vnode对象

//runtime/vnode.js
/**
 * @param {string | Object | Text | Fragment} type ;
 * @param {Object | null} props
 * @param {string |number | Array | null} children
 * @returns VNode
 */
export function h(type, props, children) {
  //判断类型
  let shapeFlag = 0;
  //是否是字符串,是的话标识为Element类型
  if (isString(type)) {
    shapeFlag = ShapeFlags.ELEMENT;
  //是否是文本类型
  } else if (type === Text) {
    shapeFlag = ShapeFlags.TEXT;
  } else if (type === Fragment) {
  //是否是Fragment类型
    shapeFlag = ShapeFlags.FRAGMENT;
  } else {
  //剩下就是Component类型
    shapeFlag = ShapeFlags.COMPONENT;
  }
  //判断children 是否是文本类型还是数组类型
  if (isString(children) || isNumber(children)) {
    shapeFlag |= ShapeFlags.TEXT_CHILDREN;
    children = children.toString();
  } else if (isArray(children)) {
    shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
  }
  //返回VNode 对象
  return {
    type,
    props,
    children,
    shapeFlag,
  };
}

//utils/index.js

export function isString(target) {
  return typeof target === 'string';
}

export function isNumber(target) {
  return typeof target === 'number';
}

export function isBoolean(target) {
  return typeof target === 'boolean';
}

复制代码

render()实现

实现了构建VNode之后,我们要解析VNode渲染真实的dom节点,并且挂载到真实的dom节点上面

//runtime/render.js
/**
 * 将虚拟dom节点挂载到真实的dom 节点上
 * @param {Object} vnode
 * @param {HTMLElement} container 父容器dom节点
 */
export function render(vnode, container) {
  mount(vnode, container);
}
复制代码

实现mount()将虚拟节点VNodecontainer上面,
利用shapeFlag判断vnode类型,然后根据类型渲染为不同节点

//runtime/render.js
/**
 * 将虚拟dom节点挂载到真实的dom 节点上
 * @param {Object} vnode
 * @param {HTMLElement} container 父容器dom节点
 */
function mount(vnode, container) {
  //取出节点类型
  const { shapeFlag } = vnode;
  //是否是Element类型
  if (shapeFlag & ShapeFlags.ELEMENT) {
    mountElement(vnode, container);
  } else if (shapeFlag & ShapeFlags.TEXT) {
  //是否是文本类型
    mountTextNode(vnode, container);
    //是否是Fragment类型
  } else if (shapeFlag & ShapeFlags.FRAGMENT) {
    mountFragment(vnode, container);
  } else {
  //剩下是Component 类型
    mountComponent(vnode, container);
  }
}
复制代码

接下来就是实现各个类型具体的挂载方法 首先实现简单的文本节点挂载

//runtime/render.js

function mountTextNode(vnode, container) {
  //创建文本dom节点
  const textNode = document.createTextNode(vnode.children);
  //挂载到真实父容器上面
  container.appendChild(textNode);
}
复制代码

然后是 mountFragment() 方法实现

//runtime/render.js

function mountFragment(vnode, container) {
  //将子节点 挂载到fragment的父节点上面
  mountChildren(vnode, container);
}

function mountChildren(vnode, container) {
  const { shapeFlag, children } = vnode;
    //判断子节点是否是文本节点
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    mountTextNode(vnode, container);
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    //子节点是数组的话 利用mount将依次数组元素挂载到 fragment父节点下面
    children.forEach((child) => {
      mount(child, container);
    });
  }
}
复制代码

接下来实现mountElement()方法挂载

  1. 创建dom元素 挂载dom元素到父节点
  2. 将props属性挂载到dom节点上面去
  3. 挂载子节点
//runtime/render.js

function mountElement(vnode, container) {
  //取出VNode 类型 和 属性对象
  const { type, props } = vnode;
  //创建对应 dom 元素
  const el = document.createElement(type);
  //挂载props 属性 到dom 对象上面
  mountProps(props, el);
  //挂载子节点
  mountChildren(vnode, el);
  //将自身挂载到父节点上
  container.appendChild(el);
}
复制代码

首先我们发现这里多了一个mountProps()方法 该方法是将我们给的属性对象挂载到 dom 节点上面去,props 对象类似下面这种

//runtime/render.js
{
    class: 'a b',
    style: {
        color: 'red',
        fontSize: '14px',
    },
    onClick: () => console.log('click'),
    checked: '',
    custom: false
}
复制代码

将上面的classstyleclick事件 都挂载到对应的dom节点上面,这里我们class只取字符串类型style只能是对象类型,vue事件只能是on开头,事件名第一个字母大写。

//runtime/render.js
function mountProps(props, el) {
    for(const key in props) {
        const value = props[value];
        switch(key) {
            case 'class':
                el.className = value;
                break;
            case 'style':
                for(const styleName in value){
                    el.style[styleName] = value[styleName];
                }
                break;
            default:
                if(/^on[^a-z]/.test(key)) {
                    const eventName = key.slice(-2).toLowerCase();
                    el.addEventListener(eventName, value);
                }
                break;
        }
    }
}
复制代码

但是像checked 这种dom元素自带的Dom propertiescutsom 这种自己设置的attributes该怎么设置呢?
渲染器挂载之Attributes 和 DOM Properties处理
看完之后, 我们对这两个处理如下设置

//runtime/render.js

const domPropsRE = /[A-Z]|^(value | checked | selected | muted | disabled)$/;

function mountProps(props, el) {
    for(const key in props) {
        const value = props[value];
        switch(key) {
            case 'class':
                el.className = value;
                break;
            case 'style':
                for(const styleName in value){
                    el.style[styleName] = value[styleName];
                }
                break;
            default:
                if(/^on[^a-z]/.test(key)) {
                    const eventName = key.slice(-2).toLowerCase();
                    el.addEventListener(eventName, value);
                } else if(domPropsRE.test(key)) {
                    //判断是否是dom 的特殊 attributes 
                    el[key] = value;
                } else {
                    //不是的话 直接用setAttribute()设置
                    el.setAttribute(key, value);
                }
                break;
        }
    }
}
复制代码

但是如果 碰到如下节点

<input type="checkbox" checked />
复制代码

他的props 对象是:

{ "checked": "" }
复制代码

它的值是空字符串,按照上面方法处理会直接给checked设置为空字符串,空字符串它代表的值是false 但是checked 的值应该是true
所以还要加一个特殊判断

//满足正则的 做为 domProp 赋值
if(domPropsRE.test(key)) {
    //例如 {ckecked: ""} 还有判断它的原来值是否是布尔类型
    if(value === '' && typeof el[key] === 'boolean') {
        value = true; 
    }
    el[key] = value;
}
复制代码

还要考虑以下这种情况:
自定义的布尔类型custom属性, 如果我们传入下面这个props, 希望将custom 设置为false

{ "custom": false }
复制代码

这个时候我们利用setAttribute会让false变为"false", 其结果仍然为true,所以我们仍需做以下判断,并且使用removeAttribute移除custom

if (domPropsRE.test(key)) {
       ...
        } else {
          // 例如 自定义属性 {custom: ''} 应应用setAttribute设置为<input custom/>
          // 而 {custom: null} 应用removeAttribute 设置为 <input />
          if (value == null || value === false) {
            el.removeAttribute(key);
          } else {
            el.setAttribute(key, value);
          }
        }
复制代码

以上就简单实现了虚拟dom的创建和挂载。接下来我们写代码测试一下结果
首先导出相关代码

//runtime/index.js
export { h, Text, Fragment } from './vnode';
export { render } from './render';
复制代码
//index.js 

import { render, h, Text, Fragment } from './runtime';

const vnode = h(
  'div',
  {
    class: 'a b',
    style: {
      border: '1px solid',
      fontSize: '14px',
      backgroundColor: 'white',
    },
    onClick: () => console.log('click'),
    id: 'foo',
    checked: '',
    custom: false,
  },
  [
    h('ul', null, [
      h('li', { style: { color: 'red' } }, 1),
      h('li', null, 2),
      h('li', { style: { color: 'green' } }, 3),
      h(Fragment, null, [h('li', null, '4'), h('li')]),
      h('li', null, [h(Text, null, 'Hello Wrold')]),
    ]),
  ]
);
render(vnode, document.body);
复制代码

结果

image.png

image.png

OK !!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享