vnode 本质是用来描述 DOM 的 JavaScript 对象,它在 Vue 中可以描述不同类型的节点,比如:普通元素节点、组件节点等。
vnode 的优点:
抽象:引入 vnode,可以把渲染过程抽象化,从而使得组件的抽象能力也得到提升
跨平台:因为 patch vnode 的过程不同平台可以有自己的实现,基于 vnode 再做服务端渲染、weex 平台、小程序平台的渲染
组件的渲染流程:
创建 vnode
createVNode 主要做了四件事:
/**
* 创建 vnode
*/
function createVNode(type, props = null, children = null) {
// 1、处理 props,标准化 class 和 style
if (props) {
// ...
}
// 2、对 vnode 类型信息编码
const shapeFlag = isString(type)
? 1 /* ELEMENT */
: isSuspense(type)
? 128 /* SUSPENSE */
: isTeleport(type)
? 64 /* TELEPORT */
: isObject(type)
? 4 /* STATEFUL_COMPONENT */
: isFunction(type)
? 2 /* FUNCTIONAL_COMPONENT */
: 0
// 3、创建 vnode 对象
const vnode = {
type,
props,
shapeFlag,
// 一些其他属性
}
// 4、标准化子节点,把不同数据类型的 children 转成数组或者文本类型
normalizeChildren(vnode, children)
return vnode
}
渲染 vnode
render 主要做了几件事:
/**
* 渲染 vnode
*/
const render = (vnode, container) => {
// vnode 为 null,则销毁组件
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
}
// 否则创建或者更新组件
else {
patch(container._vnode || null, vnode, container)
}
// 缓存 vnode 节点,表示已经渲染
container._vnode = vnode
}
patch 主要做了两件事:
/**
* 更新 DOM
* @param {vnode} n1 - 旧的 vnode(为 null 时表示第一次挂载)
* @param {vnode} n2 - 新的 vnode
* @param {DOM} container - DOM 容器,vnode 渲染生成 DOM 后,会挂载到 container 下面
*/
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
// 如果存在新旧节点,且新旧节点类型不同,则销毁旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// 挂载新 vnode
const { type, shapeFlag } = n2
switch (type) {
case Text:
// 处理文本节点
break
case Comment:
// 处理注释节点
break
case Static:
// 处理静态节点
break
case Fragment:
// 处理 Fragment 元素
break
default:
if (shapeFlag & 1/* ELEMENT */) {
// 处理普通 DOM 元素
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
} else if (shapeFlag & 6/* COMPONENT */) {
// 处理 COMPONENT
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
} else if (shapeFlag & 64/* TELEPORT */) {
// 处理 TELEPORT
} else if (shapeFlag & 128/* SUSPENSE */) {
// 处理 SUSPENSE
}
}
}
处理组件
/**
* 处理 COMPONENT
*/
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 旧节点为 null,表示不存在旧节点,则直接挂载组件
if (n1 == null) {
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
// 旧节点存在,则更新组件
else {
updateComponent(n1, n2, parentComponent, optimized)
}
}
/**
* 挂载组件
* mountComponent 做了三件事:
* 1、创建组件实例
* 2、设置组件实例
* 3、设置并运行带副作用的渲染函数
*/
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 1、创建组件实例,内部也通过对象的方式去创建了当前渲染的组件实例
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
// 2、设置组件实例,instance 保留了很多组件相关的数据,维护了组件的上下文包括对 props、插槽,以及其他实例的属性的初始化处理
setupComponent(instance)
// 3、设置并运行带副作用的渲染函数
setupRenderEffet(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}
/**
* 初始化渲染副作用函数
* 副作用:当组件数据发生变化时,effect 函数包裹的内部渲染函数 componentEffect 会重新执行一遍,从而达到重新渲染组件的目的
*/
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// 创建响应式的副作用渲染函数
instance.update = effect(function componentEffect() {
// 如果组件实例 instance 上的 isMounted 属性为 false,说明是初次渲染
/**
* 初始化渲染主要做两件事情:
* 1、渲染组件生成子树 subTree
* 2、把 subTree 挂载到 container 中
*/
if (!instance.isMounted) {
// 1、渲染组件生成子树 vnode
const subTree = (instance.subTree = renderComponentRoor(instance))
// 2、把子树 vnode 挂载到 container 中
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
// 保留渲染生成的子树根 DOM 节点
initialVNode.el = subTree.el
instance.isMounted = true
}
// 更新组件
else {
// ...
}
}, prodEffectOptions)
}
处理普通元素
/**
* 处理 ELEMENT
*/
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
isSVG = isSVG || n2.type === 'svg'
// 旧节点为 null,说明没有旧节点,为第一次渲染,则挂载元素节点
if (n1 == null) {
mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
// 否则更新元素节点
else {
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}
/**
* 挂载元素
* mountElement 主要做了四件事:
* 1、创建 DOM 元素节点
* 2、处理 props
* 3、处理子节点
* 4、把创建的 DOM 元素节点挂载到 container 上
*/
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
let el
const { type, props, shapeFlag } = vnode
// 1、创建 DOM 元素节点
el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is)
// 2、处理 props,比如 class、style、event 等属性
if (props) {
// 遍历 props,给这个 DOM 节点添加相关的 class、style、event 等属性,并作相关的处理
for (const key in props) {
if (!isReservedProp(key)) {
hostPatchProp(el, key, null, props[key], isSVG)
}
}
}
// 3、处理子节点
// 子节点是纯文本的情况
if (shapeFlag & 8/* TEXT_CHILDREN */) {
hostSetElementText(el, vnode.children)
}
// 子节点是数组的情况
else if (shapeFlag & 16/* ARRAY_CHILDREN */) {
mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren)
}
// 4、把创建的 DOM 元素节点挂载到 container 上
hostInsert(el, container, anchor)
}
/**
* 创建元素
*/
function createElement(tag, isSVG, is) {
// 在 Web 环境下的方式
isSVG ? document.createElementNS(svgNS, tag) : document.createElement(tag, is ? { is } : undefined)
// 如果是其他平台就不是操作 DOM 了,而是平台相关的 API,这些相关的方法是在创建渲染器阶段作为参数传入的
}
/**
* 处理子节点是纯文本的情况
*/
function setElementText(el, text) {
// 在 Web 环境下通过设置 DOM 元素的 textContent 属性设置文本
el.textContent = text
}
/**
* 处理子节点是数组的情况
*/
function mountChildren(children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0) {
// 遍历 chidren,获取每一个 child,递归执行 patch 方法挂载每一个 child
for (let i = start; i < children.length; i++) {
// 预处理 child
const child = (children[i] = optimized ? cloneIfMounted(children[i]) : normalizeVNode(children[i]))
// 执行 patch 挂载 child
// 执行 patch 而非 mountElement 的原因:因为子节点可能有其他类型的 vnode,比如 组件 vnode
patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
}
/**
* 把创建的 DOM 元素节点挂载到 container 下
* 因为 insert 的执行是在处理子节点后,所以挂载的顺序是先子节点,后父节点,最终挂载到最外层的容器上
*/
function insert(child, parent, anchor) {
// 如果有参考元素 anchor,则把 child 插入到 anchor 前
if (anchor) {
parent.insertBefore(child, anchor)
}
// 否则直接通过 appendChild 插入到父节点的末尾
else {
parent.appendChild(child)
}
}
扩展:嵌套组件
组件 vnode 主要维护着组件的定义对象,组件上的各种 props,而组件本身是一个抽象节点,它自身的渲染其实是通过执行组件定义的 render 渲染函数生成的子树 vnode 来完成,然后再通过 patch 这种递归的方式,无论组件的嵌套层级多深,都可以完成整个组件树的渲染。