美文网首页
简单的看一下vnode的设计

简单的看一下vnode的设计

作者: HelenYin | 来源:发表于2021-04-19 22:54 被阅读0次

    首先,我们需要用vnode来描述一个真实DOM标签
    一个真实DOM标签有:名称,属性,事件,样式,子节点等

    const elementVnode = {
      tag: 'div',
      data: {
        style: {
          width: '100px',
          height: '100px',
          backgroundColor: 'red'
        }
      }
    }
    

    使用tag属性来存储标签名称,data属性用来存出标签的附加信息,这里我们把VNode对象的data属性,称为VNodeData。
    为了描述子节点,我们需要给VNode对象添加children属性

    const elementVNode = {
      tag: 'div',
      data: null,
      children: {
        tag: 'span',
        data: null,
      }
    }
    

    除了标签之外,DOM中还有文本节点

    const textVNode = {
      tag: null,
      data: null,
      children: '文本内容'
    }
    

    尽可能的在保证语义能够说得通的情况下复用属性,会使 VNode 对象更加轻量

    用VNode来描述抽象内容

    所谓抽象内容就是组件,例如:

    <div>
      <MyComponent />
    </div>
    

    我们的意图并不是要在页面中渲染一个名为<MyComponent />的标签元素,而是要渲染<MyComponent />组件所产出的内容。
    我们仍然要用VNode来描述`<MyComponent />,并且给此类组件的VNode添加一个标识,一遍在挂在的时候有办法区分一个VNode是普通标签还是组件

    const elementVnode = {
      tag: 'div',
      data: null,
      children: {
        tag: MyComponent,
        data: null,
      }
    }
    

    这样我们可以通过tag属性是否是字符串来确定一个VNode是否是普通标签。

    除此之外,还有两个抽象内容:FragmentPortal

    Fragment

    Fragment是指渲染一个片段
    比如有如下模板:

    <template>
      <table>
        <tr>
          <Colums />
        </tr>
      </table>
    </template>
    

    组件Colums会返回多个<td>

    <template>
      <td></td>
      <td></td>
      <td></td>
    </template>
    

    假设模板中只有一个td标签,只有一个根元素,这很容易表示

    const elementVNode = {
      tag: 'td',
      data: null
    }
    

    如果有多个根元素,我们就需要引入一个抽象元素Fragment

    const Fragment = Symbol()
    const fragmentVNode = {
      // tag属性值是一个唯一标识
      tag: Fragment,
      data: null,
      children: [
        { tag: 'td', data: null },
        { tag: 'td', data: null },
        { tag: 'td', data: null },
      ]
    }
    

    这样我们把所有td标签作为fragmantVnode的子节点,根元素并不是一个实实在在的真实DOM,而是一个抽象的标识,即Fragment
    当渲染器在渲染VNode时候,如果发现该Vnode的类型是Fragment,就只需要把该Vnode的子节点渲染到页面上

    Portal

    它允许你把内容渲染到任何地方。
    使用场景:你需要渲染一个蒙层<Overlay />,要求该组件的z-index的层级最高,无论在哪里使用都希望遮住全部内容,用户可能会将其用在任何你需要蒙层的地方。

    <template>
      <div id="box" style="z-index: -1">
        <Overlay />
      </div>
    </template>
    

    如果没有Portal的情况下,上面的<Overlay>组件内容只能渲染到id=boxdiv标签下,这就会导致蒙层的层级失效甚至布局都可能受到影响。
    使用Portal就可以这样来编写

    <template>
      <Portal target="#app-root">
        <div class="overlay"></div>
      </Portal>
    </template>
    

    其最终的效果是,无论你在何处使用<Overlay />组件,他都会把内容渲染到id=app-root的元素下。由此可知,所谓Portal就是把子节点渲染到给定的目标,我们可以使用如下的VNode对象来描述

    const Portal = Symbol()
    const portalVNode = {
      tag: Portal,
      data: {
        target: '#app-root',
      },
      children: {
        tag: 'div',
        data: {
          class: 'overlay'
        }
      }
    }
    

    VNode的种类

    当VNode描述不同的事物时,其属性的值也各不相同,比如一个VNode对象时html标签的描述,那么其tag属性就是一个字符串,即标签名;如果是组件的描述,那么tag属性值则引用组件类本身;如果是文本节点的描述,那么tag属性值为nul
    最终我们发现,不同类型的VNode拥有不同的设计,这些差异积少成多,所以我们完全可以将他们分门别类。

    我们可以把VNode分为五类:html/svg元素,组件,纯文本,Fragment,Portal

    使用flags作为VNode的标识

    既然VNode 有类表之分,我们就需要一个为一个的标识,来表明某一个VNode属于哪一类,给VNode添加flags,这是vnode算法的优化手段之一

    if (flags & VNodeFlag.ELEMENT) {
      mountElement(/*...*/)
    } else if (flags & VNodeFlags.COMPONENT) {
      mountComponent/*...*/)
    } else if (flags & VNodeFlags.TEXT) {
      mountText(/*...*/)
    }
    

    使用位运算,性能提升

    // VNode对象
    {
      flags: ...
    }
    

    枚举值VNodeFlags

    每一个VNode种类我们都为其分配一个flags值,我们把它设计成一个枚举值,名称为VNodeFlags,在js中用一个对象来表示即可

    const VNodeFlags = {
      // html标签
      ELEMENT_HTML: 1,  // 0000 0000 0000 0001
      // SVG
      ELEMENT_SVG: 1 << 1,  // 0000 0000 0000 0010
      // 普通有状态的组件
      COMPONENT_STATEFUL_NORMAL: 1 << 2,  // 0000 0000 0000 0100
      // 需要被keepAlive的有状态的组件
      COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE: 1 << 3,  // 0000 0000 0000 1000
      // 已经被keepAlive的有状态组件
      COMPONENT_STATEFUL_KEPT_ALIVE: 1 << 4,  // 0000 0000 0001 0000
      // 函数式组件
      COMPONENT_FUNCTIONAL: 1 << 5,  // 0000 0000 0010 0000
      // 纯文本
      TEXT: 1 << 6,  // 0000 0000 0100 0000
      // Fragment
      FRAGMENT: 1 << 7,  // 0000 0000 1000 0000
      // Portal
      PORTAL: 1 << 8,  // 0000 0001 0000 0000
    }
    

    可以派生出来额外的三个标识:
    html和svg

    // 0000 0000 0000 0011
    VNodeFlag.ELEMENT = VNodeFlag. ELEMENT_HTML | VNodeFlag. ELEMENT_SVG 
    

    普通有状态组件,需要被keepAlive的有状态组件、已经被keepAlice的有状态组件 都是“有状态组件”,统一用 COMPONENT_STATEFUL 表示

    // 0000 0000 0001 1100
    VNodeFlags.COMPONENT_STATEFUL =
      VNodeFlags.COMPONENT_STATEFUL_NORMAL |
      VNodeFlags.COMPONENT_STATEFUL_SHOULD_KEEP_ALIVE |
      VNodeFlags.COMPONENT_STATEFUL_KEPT_ALIVE
    

    有状态的组件和函数式组件都是组件,用COMPONENT表示

    // 0000 0000 0011 1100
    VNodeFlags.COMPONENT = VNodeFlags.COMPONENT_STATEFUL | VNodeFlags.COMPONENT_FUNCTIONAL
    

    这样我们在创建vnode的时候就可以预先为其加上flags,表明VNode的类型

    // html 元素节点
    const htmlVnode = {
      flags: VNodeFlags.Element_HTML,
      tag: 'div',
      data: null,
    }
    // svg 元素节点
    const svgVnode = {
      flags: VnodeFlags.ELEMENT_SVG,
      tag: 'svg',
      data: null
    }
    // 函数式组件
    const functionalComponentVnode = {
      flags: VNodeFlags. COMPONENT_FUNCTIONAL
    }
    // 普通有状态组件
    const normalComponentVnode = {
      flags: VNodeFlags.COMPONENT_STATEFUL_NORMAL,
      tag: MyStatefulComponent
    }
    // Fragment
    const fragmentVnode = {
      flags: VNodeFlags.FRAGMENT,
      // 由于有flags的存在,我们已经不需要使用tag属性来存储唯一标识
      tag: null
    }
    // Portal
    const portalVnode = {
      flags: VNodeFlags. PORTAL,
      tag: target
    }
    

    如下是利用VNodeFlags判断vnode类型的例子,

    functionalComponentVnode.flags & VNodeFlags.COMPONENT  // 真 (非0)
    normalComponentVnode.flags & VNodeFlags.COMPONENT // 真(非0)
    htmlVnode.flags & VNodeFlags.COMPONENT // 假(为0)
    

    children 和 childrenFlag

    一个标签的子节点有哪些种类?

    • 没有子节点
    • 只有一个子节点
    • 有多个子节点(有key,无key)
    • 不知道子节点的情况

    我们可以用一个叫做ChildrenFlag来枚举以上这些情况,作为一个VNode的子节点的类型:

    const ChildrenFlag = {
      UNKNOW_CHILDREN: 0,
      NO_CHILDREN: 1,
      SINGLE_VNODE: 1 << 1,
      KEYED_VNODE: 1 << 2,
      NONE_KEYED_VNODE: 1 << 3
    }
    

    由于KEYED_VNODENONE_KEYED_VNODE都属于多个children,所以我们可以派生出来一个多children的类型,以方便程序判断

    ChildrenFlags. MULTIPLE_VNODES = ChildrenFlag. KEYED_VNODE | ChildrenFlag. NONE_KEYED_VNODE;
    
    

    这样我们判断一个VNode的子节点是否是多个子节点

    someVnode.childrenFlag & ChildrenFlag.MULTIPLE_VNODES;
    
    

    在一个VNode对象中,我们使用flags来存储vnode的类型,用childrenFlags来存储子节点的类型。

    // 没有子节点的div标签
    const elementVNode = {
      flags: VNodeFlags.ELEMENT_HTML,
      tag: 'div',
      data: null,
      children: null,
      childFlags: ChildrenFlags.NO_CHILDREN
    }
    // 文本节点的 childrenFlags 始终都是 NO_CHILDREN
    const textVNode = {
      tag: null,
      data: null,
      children: '我是文本',
      childFlags: ChildrenFlags.NO_CHILDREN
    }
    // 拥有多个使用了key的 li 标签作为子节点的 ul 标签
    const elementVNode = {
      flags: VNodeFlags.ELEMENT_HTML,
      tag: 'ul',
      data: null,
      childFlags: ChildrenFlags.KEYED_VNODES,
      children: [
        {
          tag: 'li',
          data: null,
          key: 0
        },
        {
          tag: 'li',
          data: null,
          key: 1
        }
      ]
    }
    // 只有一个子节点的 Fragment
    const elementVNode = {
      flags: VNodeFlags.FRAGMENT,
      tag: null,
      data: null,
      childFlags: ChildrenFlags.SINGLE_VNODE,
      children: {
        tag: 'p',
        data: null
      }
    }
    

    VNodeData

    VNodeData是对VNode进行描述的数据,任何对VNode进行描述的数据都应该放在data里。

    {
      flags: VNodeFlags.ELEMENT_HTML,
      tag: 'div',
      data: {
        class: ['class-a', 'active'],
        style: {
          background: 'red',
          color: 'green'
        },
        // 其他数据...
      }
    }
    

    当vnode是组件时

    <MyComponent @some-event="handler" prop-a="1" />
    
    
    {
      flags: VNodeFlags.COMPONENT_STATEFUL,
      tag: 'div',
      data: {
        on: {
          'some-event': handler
        },
        propA: '1'
        // 其他数据...
      }
    }
    

    到目前为止,还差一个el属性,el属性值在vnode被渲染为真实DOM之前一直都是null。当vnode被渲染为真实dom之后,el的值指向该真实DOM。

    相关文章

      网友评论

          本文标题:简单的看一下vnode的设计

          本文链接:https://www.haomeiwen.com/subject/riyilltx.html