Skip to content

虚拟DOM如何创建

vue中,使用hcreateVNode两个函数创建虚拟节点。

真正创建虚拟节点的是createVNode这个函数,而常用的h函数,只是对createVNode的参数进行了一个封装,更加方便使用而已,因为在使用h函数的时候,会存在多个场景:

  1. h('div', 'hello world') 第二个参数为子节点
  2. h('div', [h('span', 'hello'), h('span', ' world')]) 第二参数为子节点
  3. h('div', h('span', 'hello')) 第二个参数为子节点
  4. h('div', { class: 'container' }) 第二参数是props
  5. h('div', { class: 'container' }, 'hello world') 第二个参数是props,第三个是子节点
  6. h('div', { class: 'container' }, h('span', 'hello world')) 第二个是props,第三个是子节点
  7. h('div', { class: 'container' }, h('span', 'hello'), h('span', 'world')) 第二个是props, 后面所有都是子节点
  8. h('div', { class: 'container' }, [h('span', 'hello'), h('span', 'world')]) 和7一个意思
  • 可以看到h函数可以传递两个参数,也可以传递三个参数。第二个参数可以子节点,也可以是props
  • h函数只是对createVNode的参数进行了一个标准化处理而已,因为createVNode的第二参数必须是props,第三个参数必须是children
ts
// runtime-core/src/h.ts
import { isArray, isObject } from '@vue/shared'
/**
 * h函数的使用方法
 * 1. h('div', 'hello world')
 * 2. h('div', [h('span', 'hello'), h('span', 'world')]) 第二参数为子节点
 * 3. h('div', h('span', 'hello')) 第二个参数为子节点
 * 4. h('div', {class: 'container'}) 第二个参数是props
 * ----------
 * 5. h('div', { class: 'container' }, 'hello world')
 * 6. h('div',{ class: 'container' },h('span', 'hello world'),)
 * 7. h('div',{ class: 'container' },h('span', 'hello world'),h('span', 'hello'),)
 * 8. h('div',{ class: 'container' },[h('span', 'hello world'),h('span', 'hello')],) 和7一个意思
 */
export function h(type, propsOrChildren?, children?) {
    /**
     * h函数主要的作用的,是对createVNode做一个参数标准化
     */
    let l = arguments.length

    if(l == 2) {
        if(isArray(propsOrChildren)) {
            // 2. h('div', [h('span', 'hello'), h('span', 'world')]) 第二参数为子节点
            return createVNode(type, null, propsOrChildren)
        }
        if(isObject(propsOrChildren)) {
            if(isVNode(propsOrChildren)) {
                // 3. h('div', h('span', 'hello')) 第二个参数为子节点
                return createVNode(type, null, [propsOrChildren])
            }
            // 4. h('div', {class: 'container'}) 第二个参数是props
            return createVNode(type, propsOrChildren, children)
        }
        // 1. h('div', 'hello world')
        return createVNode(type, null, propsOrChildren)
    } else {
        if(l > 3) {
            // 7. h('div',{ class: 'container' },h('span', 'hello world'),h('span', 'hello'),)
            // 转换成 h('div',{ class: 'container' },[h('span', 'hello world'),h('span', 'hello')],)
            children = [...arguments].slice(2)
        } else if(isVNode(children)) {
            // 6. h('div',{ class: 'container' },h('span', 'hello world'),)
            children = [children]
        }
        return createVNode(type, propsOrChildren, children)
    }
}

/**
 * 判断是不是一个虚拟节点,根据 __v_isVNode 属性
 * @param value
 */
function isVNode(value) {
    return value?.__v_isVNode
}

/**
 * 创建虚拟节点的底层方法
 * @param type 节点类型
 * @param props 节点的属性
 * @param children 子节点
 */
function createVNode(type, props?, children?) {
  const vnode = {
    // 证明我是一个虚拟节点
    __v_isVNode: true,
    type,
    props,
    children,
    // 做 diff 用的
    key: props?.key,
    // 虚拟节点要挂载的元素
    el: null,
    shapeFlag: 9
  }

  return vnode
}

虚拟节点中特殊的属性-shapeFlag

在虚拟节点(vnode)中有一个特殊的属性shapeFlag,什么作用呢?

shapeFlag中,如果某一位的值是1,就表示它是一个DOM,或者某一个位是1,就表示它的子节点是一个文本,这样就可以在一个属性中通过这种组合的方式,表示更多信息。

官方的ShapeFlags

ts
// packages/shared/src/shapeFlags.ts
export enum ShapeFlags {
    // 表示 DOM 元素
    ELEMENT = 1,
    // 表示函数组件
    FUNCTIONAL_COMPONENT = 1 << 1, //二进制 1往左移动一位:10》2
    // 表示有状态组件(带有状态、生命周期等)
    STATEFUL_COMPONENT = 1 << 2, // 移动两位100 》 4
    // 表示该节点的子节点是纯文本
    TEXT_CHILDREN = 1 << 3, // 移动三位 1000 》 8
    // 表示该节点的子节点是数组形式(多个子节点)
    ARRAY_CHILDREN = 1 << 4, // 10000 > 16
    // 表示该节点的子节点是通过插槽(slots)传入的
    SLOTS_CHILDREN = 1 << 5,
    // 表示 Teleport 组件,用于将子节点传送到其他位置
    TELEPORT = 1 << 6,
    // 表示 Suspense 组件,用于处理异步加载组件时显示备用内容
    SUSPENSE = 1 << 7,
    // 表示该组件应当被 keep-alive(缓存)
    COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
    // 表示该组件已经被 keep-alive(已缓存)
    COMPONENT_KEPT_ALIVE = 1 << 9,
    // 表示组件类型,有状态组件与无状态函数组件的组合
    COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT,
}

看一下如何使用ShapeFlag进行判断某一个虚拟节点的类型是什么,children是什么:

ts
let shapeFlag = 0

const vnode = {
    __v_isNode: true,
    type: 'div',
    children: 'hello world'
    shapeFlag
}

if(typeof vnode.type === 'string') {
    shapeFlag = ShapeFlags.ELEMENT // 1
}

if(typeof vnode.children === 'string') {
    /**
     * 或运算:只要有一个1,结果就为1,两个位都为0时,结果才为0,
     * 0001 | 1000 = 1001 = 9
     */
    shapeFlag = shapeFlag | ShapeFlags.TEXT_CHILDREN  // 1001
}

vnode.shapeFlag = shapeFlag

if(vnode.shapeFlag & ShapeFlags.ELEMENT) {
    /**
     *  *
     * 与运算:两个位都为1时,结果才为1,只要有一个0,结果就为0
     * 1001 & 0001 = 0001
     */
    console.log('是一个dom元素')
}

if(vnode.shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    /**
   * 与运算 两个相同的位置,都是1,就是1
   * 1001
   * 1000
   * 1000
   */
    console.log('子元素是一个纯文本节点')
}
if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    /**
     * 与运算
     * 01001
     * 10000
     * 00000
     */
    console.log('子元素是一个数组')
}

createVNode函数:

ts
/**
 * 创建虚拟节点的方法
 * @param type 节点类型
 * @param props 节点属性
 * @param children 子节点 数组或者字符串
 */
export function createVNode(type, props?, children?) {
    /**
     * | & 运算
     *
     * 或运算:只要有一个1,结果就为1,两个位都为0时,结果才为0,
     * 0001 | 1000 = 1001 = 9
     *
     * 与运算:两个位都为1时,结果才为1,只要有一个0,结果就为0
     */
    let shapeFlag
    if (isString(type)) {
        shapeFlag = ShapeFlags.ELEMENT // 1
    }
    if (isString(children)) {
        // 0001 | 1000
        // shapeFlag = shapeFlag | ShapeFlags.TEXT_CHILDREN // 1000
        shapeFlag |= ShapeFlags.TEXT_CHILDREN
    } else if (isArray(children)) {
        shapeFlag |= ShapeFlags.ARRAY_CHILDREN
    }
    const vnode = {
        // 证明是一个虚拟节点
        __v_isVNode: true,
        type,
        props,
        children,
        // 做diff用的
        key: props?.key,
        // 虚拟节点要挂载的元素
        el: null,
        // 9 表示type是一个dom元素类型,children是一个字符串
        shapeFlag,
    }

    return vnode
}

Released under the MIT License.