组件渲染
vnode 本质是用来描述 DOM 的 JavaScript 对象,它在 Vue 中可以描述不同类型的节点,比如:普通元素节点、组件节点等。
vnode 的优点:
抽象:引入 vnode,可以把渲染过程抽象化,从而使得组件的抽象能力也得到提升
跨平台:因为 patch vnode 的过程不同平台可以有自己的实现,基于 vnode 再做服务端渲染、weex 平台、小程序平台的渲染
组件的渲染流程:
-
创建 vnode
createVNode 主要做了四件事:
- 处理 props,标准化 class 和 style
- 对 vnode 类型信息编码
- 创建 vnode 对象
- 标准化子节点
/** * 创建 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
- 如果之前有,现在没有,则销毁
- 如果现在有,则创建或更新
- 缓存 vnode,用于判断是否已经渲染
/** * 渲染 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
扩展:嵌套组件
组件 vnode 主要维护着组件的定义对象,组件上的各种 props,而组件本身是一个抽象节点,它自身的渲染其实是通过执行组件定义的 render 渲染函数生成的子树 vnode 来完成,然后再通过 patch 这种递归的方式,无论组件的嵌套层级多深,都可以完成整个组件树的渲染。