创建一个自定义渲染器。通过提供平台特定的节点创建以及更改 API,你可以在非 DOM 环境中也享受到 Vue 核心运行时的特性。
用于编程式地创建组件虚拟 DOM 树的函数。
创建虚拟 DOM 节点 (vnode)。h用法大全
类型比对
packages/shared/src/shapeFlags.ts
// 标识
export const enum ShapeFlags {
ELEMENT = 1,
FUNCTIONAL_COMPONENT = 1 << 1,
STATEFUL_COMPONENT = 1 << 2,
TEXT_CHILDREN = 1 << 3,
ARRAY_CHILDREN = 1 << 4,
SLOTS_CHILDREN = 1 << 5,
TELEPORT = 1 << 6,
SUSPENSE = 1 << 7,
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
COMPONENT_KEPT_ALIVE = 1 << 9,
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
后续可以使用 | 运算符进行多种类型存储,使用 & 运算符进行判断是否包含某一种类型( a & b > 0 )
packages/runtime-core/src/vnode.ts
import { isArray, isString, ShapeFlags } from '@vue/shared';
export const Text = Symbol('Text');
export function isVNode(value) {
return !!(value && value.__v_isVnode);
}
/**
* 创建虚拟节点
* @param type 虚拟节点类型
* @param props 属性
* @param children 子节点
*/
export function createVNode(type, props, children = null) {
let shapeFlag = isString(type) ? ShapeFlags.ELEMENT : 0;
// 虚拟dom,可以跨平台,性能好
const vnode = {
__v_isVnode: true, // 是否是虚拟节点
shapeFlag, // 类型标识
type, // 节点类型
props, // 属性
children, // 子节点
key: props?.key, // key
/**
* 对应的真实节点,后续diff算法比对两个vnode时会替换新的属性值,并更新el
*/
el: null,
};
if (children) {
let type = 0;
if (isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN;
} else {
children = String(children);
type = ShapeFlags.TEXT_CHILDREN;
}
// 通过位运算将当前vnode类型及子节点类型存储起来
vnode.shapeFlag |= type;
}
return vnode;
}
虚拟节点不用考虑平台兼容,并且可以将虚拟节点利用js存储并进行比对后再渲染真实dom,不用频繁操作dom元素,性能更好
packages/runtime-core/src/h.ts
/*
// type only
h('div')
// type + props
h('div', {})
// type + omit props + children
// Omit props does NOT support named slots
h('div', []) // array
h('div', 'foo') // text
h('div', h('br')) // vnode
h(Component, () => {}) // default slot
// type + props + children
h('div', {}, []) // array
h('div', {}, 'foo') // text
h('div', {}, h('br')) // vnode
h(Component, {}, () => {}) // default slot
h(Component, {}, {}) // named slots
// named slots without props requires explicit `null` to avoid ambiguity
h(Component, null, {})
**/
import { isArray, isObject } from '@vue/shared';
import { createVNode, isVNode } from './vnode';
export function h(type, propsOrChildren?, children?) {
const l = arguments.length;
if (l === 2) {
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
if (isVNode(propsOrChildren)) {
return createVNode(type, null, [propsOrChildren]);
}
return createVNode(type, propsOrChildren);
} else {
return createVNode(type, null, propsOrChildren);
}
} else {
if (l > 3) {
children = Array.prototype.slice.call(arguments, 2);
} else if (l === 3 && isVNode(children)) {
children = [children];
}
return createVNode(type, propsOrChildren, children);
}
}
h方法对创建虚拟节点操作进行了二次封装,使用法变得多种多样
packages/runtime-core/src/renderer.ts
import { isString, ShapeFlags } from '@vue/shared';
import { createVNode, isSameVNode, Text } from './vnode';
export function createRenderer(renderOptions) {
let {
insert: hostInsert,
createElement: hostCreateElement,
createText: hostCreateText,
remove: hostRemove,
setElementText: hostSetElementText,
setText: hostSetText,
querySelector: hostQuerySelector,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
patchProp: hostPatchProp,
} = renderOptions;
const normalize = (child, i) => {
if (isString(child[i])) {
let vnode = createVNode(Text, null, child[i]);
child[i] = vnode;
return child[i];
}
return child[i];
};
// 递归挂载子节点
const mountChildren = (children, container) => {
for (let i = 0; i < children.length; i++) {
let child = normalize(children, i);
patch(null, child, container);
}
};
const mountElement = (vnode, container) => {
let { type, props, children, shapeFlag } = vnode;
// 挂载真实dom到vnode上
let el = (vnode.el = hostCreateElement(type));
// 属性
if (props) {
for (const key in props) {
hostPatchProp(el, key, null, props[key]);
}
}
// 子节点处理,& 预算判断是否为某一个类型
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 文本
hostSetElementText(el, children);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(children, el);
}
// 插入真实dom到容器中
hostInsert(el, container);
};
const processText = (n1, n2, container) => {
if (n1 === null) {
hostInsert((n2.el = hostCreateText(n2.children)), container);
} else {
// 文本内容变化,节点复用
const el = (n2.el = n1.el);
if (n1.children !== n2.children) {
// 更新文本
hostSetText(el, n2.children);
}
}
};
const patchProps = (oldProps, newProps, el) => {
for (let key in newProps) {
hostPatchProp(el, key, oldProps[key], newProps[key]);
}
for (let key in oldProps) {
if (!newProps[key]) {
hostPatchProp(el, key, oldProps[key], undefined);
}
}
};
const unmountChildren = (children) => {
for (let i = 0; i < children.length; i++) {
unmount(children[i]);
}
};
// 比较两个节点的差异
const patchKeyChildren = (c1, c2, el) => {
};
// 比较两个节点的子节点,el为当前父节点
const patchChildren = (n1, n2, el) => {
const c1 = n1.children;
const c2 = n2.children;
const prevShapeFlag = n1.shapeFlag;
const shapeFlag = n2.shapeFlag;
// 新值为文本
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 旧值为数组
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 文本 数组
unmountChildren(c1);
}
if (c1 !== c2) {
// 文本 文本
hostSetElementText(el, c2);
}
} else {
// 旧值为数组
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 新值为数组
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 数组 数组 diff
patchKeyChildren(c1, c2, el); // 全量更新,同级比较
} else {
// 空 数组
unmountChildren(c1);
}
} else {
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 空 文本
// 数组 文本
hostSetElementText(el, '');
}
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 数组 空
// 数组 文本
mountChildren(c2, el);
}
}
}
};
// 先复用节点,然后比较属性,再比较子节点
const patchElement = (n1, n2) => {
// 复用节点
let el = (n2.el = n1.el);
let oldProps = n1.props || {};
let newProps = n2.props || {};
patchProps(oldProps, newProps, el);
patchChildren(n1, n2, el);
};
const processElement = (n1, n2, container) => {
if (n1 === null) {
mountElement(n2, container);
} else {
// 对比元素
patchElement(n1, n2);
}
};
const patch = (n1, n2, container) => {
if (n1 === n2) {
return;
}
// 如果新值与老值完全没有可比性,删除老值,创建新值
if (n1 && !isSameVNode(n1, n2)) {
unmount(n1);
n1 = null;
}
const { type, shapeFlag } = n2;
switch (type) {
case Text: // 文本
processText(n1, n2, container);
break;
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// 元素
processElement(n1, n2, container);
}
break;
}
};
const unmount = (vnode) => {
hostRemove(vnode.el);
};
const render = (vnode, container) => {
if (vnode === null) {
// 卸载dom
if (container._vnode) {
unmount(container._vnode);
}
} else {
// 初始化及更新
patch(container._vnode || null, vnode, container);
}
// 缓存下次直接更新
container._vnode = vnode;
};
return { render };
}