组件渲染
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 和 styleif (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}// 挂载新 vnodeconst { type, shapeFlag } = n2switch (type) {case Text:// 处理文本节点breakcase Comment:// 处理注释节点breakcase Static:// 处理静态节点breakcase Fragment:// 处理 Fragment 元素breakdefault:if (shapeFlag & 1/* ELEMENT */) {// 处理普通 DOM 元素processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)} else if (shapeFlag & 6/* COMPONENT */) {// 处理 COMPONENTprocessComponent(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、渲染组件生成子树 vnodeconst subTree = (instance.subTree = renderComponentRoor(instance))// 2、把子树 vnode 挂载到 container 中patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)// 保留渲染生成的子树根 DOM 节点initialVNode.el = subTree.elinstance.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 elconst { 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 方法挂载每一个 childfor (let i = start; i < children.length; i++) {// 预处理 childconst child = (children[i] = optimized ? cloneIfMounted(children[i]) : normalizeVNode(children[i]))// 执行 patch 挂载 child// 执行 patch 而非 mountElement 的原因:因为子节点可能有其他类型的 vnode,比如 组件 vnodepatch(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 这种递归的方式,无论组件的嵌套层级多深,都可以完成整个组件树的渲染。