Appearance
虚拟DOM如何创建
在vue中,使用h和createVNode两个函数创建虚拟节点。
真正创建虚拟节点的是createVNode这个函数,而常用的h函数,只是对createVNode的参数进行了一个封装,更加方便使用而已,因为在使用h函数的时候,会存在多个场景:
h('div', 'hello world')第二个参数为子节点h('div', [h('span', 'hello'), h('span', ' world')])第二参数为子节点h('div', h('span', 'hello'))第二个参数为子节点h('div', { class: 'container' })第二参数是propsh('div', { class: 'container' }, 'hello world')第二个参数是props,第三个是子节点h('div', { class: 'container' }, h('span', 'hello world'))第二个是props,第三个是子节点h('div', { class: 'container' }, h('span', 'hello'), h('span', 'world'))第二个是props, 后面所有都是子节点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
}