vue3-runtime(一) render

108 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第15天,点击查看活动详情

这一期先讲如何讲虚拟dom结构,虚拟dom怎么转化成真实元素(h和render函数)

cn.vuejs.org/guide/extra…

划分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 为一个不会真实渲染的节点。相当于 template 或 react 的 Fragment。type 因定为一个 Symbol,props 为空,children 为数组,表示子节点。最后渲染时子节点会挂载到 Fragment 的父节点上
react里的<>  </>
// 类型定义
{
  type: Symbol,
  props: null,
  children: []
}

  1. Component Component 是组件,组件有自己特殊的一套渲染方法,但组件最终的产物,也是上面三种 VNode 的集合。组件的 type,就是定义组件的对象,props 即是外部传入组件的 props 数据,children 即是组件的 slot(但我们不准备实现 slot,跳过)。
// 类型定义
{
  type: Object,
  props: Object,
  children: null
}

// 举例
{
  type: {
    template:`{{ msg }} {{ name }}`,
    props: ['name'],
    setup(){
      return {
        msg: 'hello'
      }
    }
  },
  props: { name: 'world' },
}

还有注释节点、tele、suspense等不讲

ShapeFlags

ShapeFlags 是一组标记,用于快速辨识 VNode 的类型和它的 children 的类型。

复习一下位运算

// 按位与运算
0 0 1 0 0 0 1 1
0 0 1 0 1 1 1 1
&
0 0 1 0 0 0 1 1

// 按位或运算
0 0 1 0 0 0 1 1
0 0 1 0 1 1 1 1
|
0 0 1 0 1 1 1 1
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
};

采用二进制位运算<<|生成,使用时用&运算判断,例如:

if (flag & ShapeFlags.ELEMENT) //说明是元素节点

再例如,一个值为 33 的 flag,它的二进制值为 00100001,那么它:

说明是元素节点,且有数组子节点

let flag = 33;
flag & ShapeFlags.ELEMENT; // true
flag & ShapeFlags.ARRAY_CHILDREN; // true
flag & ShapeFlags.CHILDREN; // true
  • 渲染Render需要的数据
{
  type,
  props,
  children,
  shapeFlag,
}

渲染例子代码

h函数生成Vnoderender函数渲染挂载Vnode

image-20220820230658699.png

image-20220820231403077.png

源码

h

export const Text = Symbol('Text');
export const Fragment = Symbol('Fragment');

export const ShapeFlags = {
  ELEMENT: 1,
  TEXT: 1 << 1,
  FRAGMENT: 1 << 2,
  COMPONENT: 1 << 3,
  TEXT_CHILDREN: 1 << 4,
  ARRAY_CHILDREN: 1 << 5,
  CHILDREN: (1 << 4) | (1 << 5),
};

/**
 * vnode有四种类型:dom元素,纯文本,Fragment,组件
 * @param {string | Text | Fragment | Object } type
 * @param {Object | null} props
 * @param {string | array | null} children
 * @returns VNode
 */
export function h(type, props = null, children = null) {
    // h函数作用 其实就是判断当前vnode种类。
  let shapeFlag = 0;
  // 如果type是字符,说明是元素
  if (isString(type)) {
    shapeFlag = ShapeFlags.ELEMENT;
  } else if (type === Text) {
    // 文本节点
    shapeFlag = ShapeFlags.TEXT;
  } else if (type === Fragment) {
    shapeFlag = ShapeFlags.FRAGMENT;
  } else {
    shapeFlag = ShapeFlags.COMPONENT;
  }
  // 文本子节点和数组子节点,这里要进行或运算
  if (typeof children === 'string' || typeof children === 'number') {
    shapeFlag |= ShapeFlags.TEXT_CHILDREN;
    children = children.toString();
  } else if (Array.isArray(children)) {
    shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
  }


  return {
    type,
    props,
    children,
    shapeFlag,
  };
}

关于元素的attribute和Properties

id这些标准属性,可以通过document.body.id这样的方式获取到值,而非标准的属性则只能拿到undefined

虽然setAttribute可以为所有属性赋值,但是有弊端,就是不能赋值布尔类型,赋值false会改成'false',这样拿到还是true

不过这种是有限的,所以只要把 checked 这类特别注意下就行

render

渲染元素属性举例:

{
  class: 'a b',
  style: {
    color: 'red',
    fontSize: '14px',
  },
  onClick: () => console.log('click'),
  checked: '',
  custom: false
}
export function render(vnode, container) {
  const {shapeFlag} = vnode
  if(shapeFlag & ShapeFlags.ELEMENT){
      //代表是一个元素节点
      mountElement(vnode, container)
  }
  //fragment则是取出子节点渲染 mountChildren(vnode.children, container)
    ...其他
}
//渲染元素节点,挂载属性,渲染子节点,拼接到父元素
function mountElement(vnode, container) {
  const { type, props, children } = vnode;
  const el = document.createElement(type);
    mountProps(props,el)
    mountChildren(vnode, el);
  container.appenChild(el);
}
function mountChildren(vnode, container) {
  const { shapeFlag,  children } = vnode;
    if(shapeFlag & ShapeFlags.TEXT_CHILDREN){
        mountTextNode(vnode, container)
    }else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN){
        children.forEach(child=>render(child,container))
    }
}
function mountTextNode(vnode, container, anchor) {
  const textNode = document.createTextNode(vnode.children);
  container.appenChild(textNode);
}
// innerHtml等属性
const domPropsRE = /[A-Z]|^(value|checked|selected|muted|disabled)$/;
function mountProps(props,el){
    for(const key in props){
        const value = props[key]
        switch(key){
            case 'class':
                el.className = value
                break;
                // 虽然 style:{a:b;c:d}这样子的,但是我们直接赋值就行了,不用自己拼,元素内部自动处理
            case 'style':
                for(const name in value){
                    el[style][name] = value[name]
                }
                break;
            default:
                if (domPropsRE.test(key)) {
                  // 满足上面正则的,作为domProp赋值
                  if (value === '' && typeof el[key] === 'boolean') {
                    // 例如{checked: ''}
                    value = true;
                  }
                  el[key] = value;
                }else {
                  // 例如自定义属性{custom: ''},应该用setAttribute设置为<input custom />
                  // 而{custom: null},应用removeAttribute设置为<input />
                  if (value == null || value === false) {
                    el.removeAttribute(key);
                  } else {
                    el.setAttribute(key, value);
                  }
                }
        }
    }
}