目录
- 正文
- 何时会进行虚拟函数的创建和渲染?
- 什么是VNode?
- 前置须知
- ShapeFlags
- 为什么要使用Vnode?
- Vnode是如何创建的?
- 先是判断
- createBaseVNode 虚拟节点初始化创建
- render 渲染 VNode
- patch VNode
- processComponent 节点类型是组件下的处理
- subTree是什么?
- 当节点的类型是普通元素DOM时候,patch判断运行processElement
正文
在上一篇中,只讲了大致的执行流程,其中有关渲染部分的内容并没有深入,而这部分关系到创建Vnode和渲染Vnode的过程,就是将代码通过渲染变成大家可见的网页画面的这一部分内容。 createApp函数内部的app.mount方法是一个标准的可跨平台的组件渲染流程:先创建VNode,再渲染VNode。
何时会进行虚拟函数的创建和渲染?
vue3初始化过程中,createApp()
指向的源码 core/packages/runtime-core/src/apiCreateApp.ts中
export function createAppAPI<HostElement>( | |
render: RootRenderFunction<HostElement>,//由之前的baseCreateRenderer中的render传入 | |
hydrate?: RootHydrateFunction | |
): CreateAppFunction<HostElement> { | |
return function createApp(rootComponent, rootProps = null) {//rootComponent根组件 | |
let isMounted = false | |
//生成一个具体的对象,提供对应的API和相关属性 | |
const app: App = (context.app = {//将以下参数传入到context中的app里 | |
//...省略其他逻辑处理 | |
//挂载 | |
mount( | |
rootContainer: HostElement, | |
isHydrate?: boolean,//是用来判断是否用于服务器渲染,这里不讲所以省略 | |
isSVG?: boolean | |
): any { | |
//如果处于未挂载完毕状态下运行 | |
if (!isMounted) { | |
//创建一个新的虚拟节点传入根组件和根属性 | |
const vnode = createVNode( | |
rootComponent as ConcreteComponent, | |
rootProps | |
) | |
// 存储app上下文到根虚拟节点,这将在初始挂载时设置在根实例上。 | |
vnode.appContext = context | |
} | |
//渲染虚拟节点,根容器 | |
render(vnode, rootContainer, isSVG) | |
isMounted = true //将状态改变成为已挂载 | |
app._container = rootContainer | |
// for devtools and telemetry | |
;(rootContainer as any).__vue_app__ = app | |
return getExposeProxy(vnode.component!) || vnode.component!.proxy | |
}}, | |
}) | |
return app | |
} | |
} |
在mount的过程中,当运行处于未挂载时, const vnode = createVNode(rootComponent as ConcreteComponent,rootProps)创建虚拟节点并且将 vnode(虚拟节点)、rootContainer(根容器),isSVG作为参数传入render函数中去进行渲染。
什么是VNode?
虚拟节点其实就是JavaScript的一个对象,用来描述DOM。
这里可以编写一个实际的简单例子来辅助理解,下面是一段html的普通元素节点
<div class="title" style="font-size:16px;width=100px">这是一个标题</div>
如何用虚拟节点来表示?
const VNode ={ | |
type:'div', | |
props:{ | |
class:'title', | |
style:{ | |
fontSize:'16px', | |
width:'100px' | |
} | |
}, | |
children:'这是一个标题', | |
key:null | |
} |
这里官方文档给出了建议:完整的 VNode
接口包含其他内部属性,但是强烈建议避免使用这些没有在这里列举出的属性。这样能够避免因内部属性变更而导致的不兼容性问题。
vue3对vnode的type做了更详细的分类。在创建vnode之前先了解一下shapeFlags
,这个类对type的类型信息做了对应的编码。以便之后在patch阶段,可以通过不同的类型执行对应的逻辑处理。同时也能看到type有元素,方法函数组件,带状态的组件,子类是文本等。
前置须知
ShapeFlags
// package/shared/src/shapeFlags.ts | |
//这是一个ts的枚举类,从中也能了解到虚拟节点的类型 | |
export const enum ShapeFlags { | |
//DOM元素 HTML | |
ELEMENT = 1, | |
//函数式组件 | |
FUNCTIONAL_COMPONENT = 1 << 1, //2 | |
//带状态的组件 | |
STATEFUL_COMPONENT = 1 << 2,//4 | |
//子节点是文本 | |
TEXT_CHILDREN = 1 << 3,//8 | |
//子节点是数组 | |
ARRAY_CHILDREN = 1 << 4,//16 | |
//子节点带有插槽 | |
SLOTS_CHILDREN = 1 << 5,//32 | |
//传送,将一个组件内部的模板‘传送'到该组件DOM结构外层中去,例如遮罩层的使用 | |
TELEPORT = 1 << 6,//64 | |
//悬念,用于等待异步组件时渲染一些额外的内容,比如骨架屏,不过目前是实验性功能 | |
SUSPENSE = 1 << 7,//128 | |
//要缓存的组件 | |
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,//256 | |
//已缓存的组件 | |
COMPONENT_KEPT_ALIVE = 1 << 9,//512 | |
//组件 | |
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT | |
}//4 | 2 |
它用来表示当前虚拟节点的类型。我们可以通过对shapeFlag
做二进制运算来描述当前节点的本身是什么类型、子节点是什么类型。
为什么要使用Vnode?
因为vnode可以抽象,把渲染的过程抽象化,使组件的抽象能力也得到提升。 然后因为vue需要可以跨平台,讲节点抽象化后可以通过平台自己的实现,使之在各个平台上渲染更容易。 不过同时需要注意的一点,虽然使用的是vnode,但是这并不意味着vnode的性能更具有优势。比如很大的组件,是表格上千行的表格,在render过程中,创建vnode势必得遍历上千次vnode的创建,然后遍历上千次的patch,在更新表格数据中,势必会出现卡顿的情况。即便是在patch中使用diff优化了对DOM操作次数,但是始终需要操作。
Vnode是如何创建的?
vue3 提供了一个 h()
函数用于创建 vnodes:
import {h} from 'vue' | |
h('div', { id: 'foo' }) |
其本质也是调用 createVNode()
函数。
const vnode = createVNode(rootComponent as ConcreteComponent,rootProps)
createVNode()
位于 core/packages/runtime-core/src/vnode.ts
//创建虚拟节点 | |
export const createVNode = ( _createVNode) as typeof _createVNode | |
function _createVNode( | |
//标签类型 | |
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, | |
//数据和vnode的属性 | |
props: (Data & VNodeProps) | null = null, | |
//子节点 | |
children: unknown = null, | |
//patch标记 | |
patchFlag: number = 0, | |
//动态参数 | |
dynamicProps: string[] | null = null, | |
//是否是block节点 | |
isBlockNode = false | |
): VNode { | |
//内部逻辑处理 | |
//使用更基层的createBaseVNode对各项参数进行处理 | |
return createBaseVNode( | |
type, | |
props, | |
children, | |
patchFlag, | |
dynamicProps, | |
shapeFlag, | |
isBlockNode, | |
true | |
) | |
} |
刚才省略的内部逻辑处理,这里去除了只有在开发环境下才运行的代码:
先是判断
if (isVNode(type)) { | |
//创建虚拟节点接收到已存在的节点,这种情况发生在诸如 <component :is="vnode"/> | |
// #2078 确保在克隆过程中合并refs,而不是覆盖它。 | |
const cloned = cloneVNode(type, props, true /* mergeRef: true */) | |
//如果拥有子节点,将子节点规范化处理 | |
if (children) {normalizeChildren(cloned, children)}: | |
//将拷贝的对象存入currentBlock中 | |
if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) { | |
if (cloned.shapeFlag & ShapeFlags.COMPONENT) { | |
currentBlock[currentBlock.indexOf(type)] = cloned | |
} else { | |
currentBlock.push(cloned) | |
} | |
} | |
cloned.patchFlag |= PatchFlags.BAIL | |
//返回克隆 | |
return cloned | |
} | |
// 类组件规范化 | |
if (isClassComponent(type)) { | |
type = type.__vccOpts | |
} | |
// 类(class)和风格(style) 规范化. | |
if (props) { | |
//对于响应式或者代理的对象,我们需要克隆来处理,以防止触发响应式和代理的变动 | |
props = guardReactiveProps(props)! | |
let { class: klass, style } = props | |
if (klass && !isString(klass)) { | |
props.class = normalizeClass(klass) | |
} | |
if (isObject(style)) { | |
// 响应式对象需要克隆后再处理,以免触发响应式。 | |
if (isProxy(style) && !isArray(style)) { | |
style = extend({}, style) | |
} | |
props.style = normalizeStyle(style) | |
} | |
} |
与之前的shapeFlags枚举类结合,将定好的编码赋值给shapeFlag
// 将虚拟节点的类型信息编码成一个位图(bitmap) | |
// 根据type类型来确定shapeFlag的属性值 | |
const shapeFlag = isString(type)//是否是字符串 | |
? ShapeFlags.ELEMENT//传值1 | |
: __FEATURE_SUSPENSE__ && isSuspense(type)//是否是悬念类型 | |
? ShapeFlags.SUSPENSE//传值128 | |
: isTeleport(type)//是否是传送类型 | |
? ShapeFlags.TELEPORT//传值64 | |
: isObject(type)//是否是对象类型 | |
? ShapeFlags.STATEFUL_COMPONENT//传值4 | |
: isFunction(type)//是否是方法类型 | |
? ShapeFlags.FUNCTIONAL_COMPONENT//传值2 | |
: 0//都不是以上类型 传值0 |
以上,将虚拟节点其中一部分的属性处理好之后,再传入创建基础虚拟节点函数中,做更进一步和更详细的属性对象创建。
createBaseVNode 虚拟节点初始化创建
创建基础虚拟节点(JavaScript对象),初始化封装一系列相关的属性。
function createBaseVNode( | |
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,//虚拟节点类型 | |
props: (Data & VNodeProps) | null = null,//内部的属性 | |
children: unknown = null,//子节点内容 | |
patchFlag = 0,//patch标记 | |
dynamicProps: string[] | null = null,//动态参数内容 | |
shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,//节点类型的信息编码 | |
isBlockNode = false,//是否块节点 | |
needFullChildrenNormalization = false | |
) { | |
//声明一个vnode对象,并且将各种属性赋值,从而完成虚拟节点的初始化创建 | |
const vnode = { | |
__v_isVNode: true,//内部属性表示为Vnode | |
__v_skip: true,//表示跳过响应式转换 | |
type, //虚拟节点类型 | |
props,//虚拟节点内的属性和props | |
key: props && normalizeKey(props),//虚拟阶段的key用于diff | |
ref: props && normalizeRef(props),//引用 | |
scopeId: currentScopeId,//作用域id | |
slotScopeIds: null,//插槽id | |
children,//子节点内容,树形结构 | |
component: null,//组件 | |
suspense: null,//传送组件 | |
ssContent: null, | |
ssFallback: null, | |
dirs: null,//目录 | |
transition: null,//内置组件相关字段 | |
el: null,//vnode实际被转换为dom元素的时候产生的元素,宿主 | |
anchor: null,//锚点 | |
target: null,//目标 | |
targetAnchor: null,//目标锚点 | |
staticCount: 0,//静态节点数 | |
shapeFlag,//shape标记 | |
patchFlag,//patch标记 | |
dynamicProps,//动态参数 | |
dynamicChildren: null,//动态子节点 | |
appContext: null,//app上下文 | |
ctx: currentRenderingInstance | |
} as VNode | |
//关于子节点和block节点的标准化和信息编码处理 | |
return vnode | |
} |
由此可见,创建vnode就是一个对props中的内容进行标准化处理,然后对节点类型进行信息编码,对子节点的标准化处理和类型信息编码,最后创建vnode对象的过程。
render 渲染 VNode
baseCreateRenderer()
返回对象中,有render()
函数,hydrate用于服务器渲染和createApp函数的。 在baseCreateRenderer()
函数中,定义了render()
函数,render的内容不复杂。
组件在首次挂载,以及后续的更新等,都会触发mount()
,而这些,其实都会调用render()
渲染函数。render()
会先判断vnode虚拟节点是否存在,如果不存在进行unmount()
卸载操作。 如果存在则会调用patch()
函数。因此可以推测,patch()
的过程中,有关组件相关处理。
const render: RootRenderFunction = (vnode, container, isSVG) => { | |
if (vnode == null) {//判断是否传入虚拟节点,如果节点不存在则运行 | |
if (container._vnode) {//判断容器中是否已有节点 | |
unmount(container._vnode, null, null, true)//如果已有节点则卸载当前节点 | |
} | |
} else { | |
//如果节点存在,则调用patch函数,从参数看,会传入新旧节点和容器 | |
patch(container._vnode || null, vnode, container, null, null, null, isSVG) | |
} | |
flushPreFlushCbs() //组件更新前的回调 | |
flushPostFlushCbs()//组件更新后的回调 | |
container._vnode = vnode//将虚拟节点赋值到容器上 | |
} |
patch VNode
这里来看一下有关patch()
函数的代码,侧重了解当组件初次渲染的时候的流程。
// 注意:此闭包中的函数应使用 'const xxx = () => {}'样式,以防止被小写器内联。 | |
// patch:进行diff算法,crateApp->vnode->element | |
const patch: PatchFn = ( | |
n1,//老节点 | |
n2,//新节点 | |
container,//宿主元素 container | |
anchor = null,//锚点,用来标识当我们对新旧节点做增删或移动等操作时,以哪个节点为参照物 | |
parentComponent = null,//父组件 | |
parentSuspense = null,//父悬念 | |
isSVG = false, | |
slotScopeIds = null,//插槽 | |
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren | |
) => { | |
if (n1 === n2) {// 如果新老节点相同则停止 | |
return | |
} | |
// 打补丁且不是相同类型,则卸载旧节点,锚点后移 | |
if (n1 && !isSameVNodeType(n1, n2)) { | |
anchor = getNextHostNode(n1) | |
unmount(n1, parentComponent, parentSuspense, true) | |
n1 = null //n1复位 | |
} | |
//是否动态节点优化 | |
if (n2.patchFlag === PatchFlags.BAIL) { | |
optimized = false | |
n2.dynamicChildren = null | |
} | |
//结构n2新节点,获取新节点的类型 | |
const { type, ref, shapeFlag } = n2 | |
switch (type) { | |
case Text: //文本类 | |
processText(n1, n2, container, anchor)//文本节点处理 | |
break | |
case Comment://注释类 | |
processCommentNode(n1, n2, container, anchor)//处理注释节点 | |
break | |
case Static://静态类 | |
if (n1 == null) {//如果老节点不存在 | |
mountStaticNode(n2, container, anchor, isSVG)//挂载静态节点 | |
} | |
break | |
case Fragment://片段类 | |
processFragment( | |
//进行片段处理 | |
) | |
break | |
default: | |
if (shapeFlag & ShapeFlags.ELEMENT) {//如果类型编码是元素 | |
processElement( | |
n1, | |
n2, | |
container, | |
anchor, | |
parentComponent, | |
parentSuspense, | |
isSVG, | |
slotScopeIds, | |
optimized | |
) | |
} else if (shapeFlag & ShapeFlags.COMPONENT) {//如果类型编码是组件 | |
processComponent( | |
n1, | |
n2, | |
container, | |
anchor, | |
parentComponent, | |
parentSuspense, | |
isSVG, | |
slotScopeIds, | |
optimized | |
) | |
} else if (shapeFlag & ShapeFlags.TELEPORT) { | |
;(type as typeof TeleportImpl).process( | |
// 如果类型是传送,进行处理 | |
) | |
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { | |
;(type as typeof SuspenseImpl).process( | |
//悬念处理 | |
) | |
} | |
} | |
// 设置 参考 ref | |
if (ref != null && parentComponent) { | |
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2) | |
} | |
} |
patch函数可见,主要做的就是 新旧虚拟节点之间的对比,这也是常说的diff算法,结合render(vnode, rootContainer, isSVG)
可以看出vnode对应的是n1也就是新节点,而rootContainer对应n2,也就是老节点。其做的逻辑判断是。
- 新旧节点相同则直接返回
- 旧节点存在,且新节点和旧节点的类型不同,旧节点将被卸载
unmount
且复位清空null
。锚点移向下个节点。 - 新节点是否是动态值优化标记
- 对新节点的类型判断
- 文本类:
processText
- 注释类:
processComment
- 静态类:
mountStaticNode
- 片段类:
processFragment
- 默认
而这个默认才是主要的部分也是最常用到的部分。里面包含了对类型是元素element
、组件component
、传送teleport
、悬念suspense
的处理。这次主要讲的是虚拟节点到组件和普通元素渲染的过程,其他类型的暂时不提,内容展开过于杂乱。
实际上第一次初始运行的时候,patch判断vnode类型根节点,因为vue3书写的时候,都是以组件的形式体现,所以第一次的类型势必是component类型。
processComponent 节点类型是组件下的处理
const processComponent = ( | |
n1: VNode | null,//老节点 | |
n2: VNode,//新节点 | |
container: RendererElement,//宿主 | |
anchor: RendererNode | null,//锚点 | |
parentComponent: ComponentInternalInstance | null,//父组件 | |
parentSuspense: SuspenseBoundary | null,//父悬念 | |
isSVG: boolean, | |
slotScopeIds: string[] | null,//插槽 | |
optimized: boolean | |
) => { | |
n2.slotScopeIds = slotScopeIds | |
if (n1 == null) {//如果老节点不存在,初次渲染的时候 | |
//省略一部分n2其他情况下的处理 | |
//挂载组件 | |
mountComponent( | |
n2, | |
container, | |
anchor, | |
parentComponent, | |
parentSuspense, | |
isSVG, | |
optimized | |
) | |
} else { | |
//更新组件 | |
updateComponent(n1, n2, optimized) | |
} | |
} |
老节点n1不存在null
的时候,将挂载n2节点。如果老节点存在的时候,则更新组件。因此mountComponent()
最常见的就是在首次渲染的时候,那时旧节点都是空的。
接下来就是看如何挂载组件mountComponent()
const mountComponent: MountComponentFn = ( | |
initialVNode,//对应n2 新的节点 | |
container,//对应宿主 | |
anchor,//锚点 | |
parentComponent,//父组件 | |
parentSuspense,//父传送 | |
isSVG,//是否SVG | |
optimized//是否优化 | |
) => { | |
// 2.x编译器可以在实际安装前预先创建组件实例。 | |
const compatMountInstance = | |
//判断是不是根组件且是组件 | |
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component | |
const instance: ComponentInternalInstance = | |
compatMountInstance || | |
//创建组件实例 | |
(initialVNode.component = createComponentInstance( | |
initialVNode, | |
parentComponent, | |
parentSuspense | |
)) | |
// 如果新节点是缓存组件的话那么将internals赋值给期渲染函数 | |
if (isKeepAlive(initialVNode)) { | |
;(instance.ctx as KeepAliveContext).renderer = internals | |
} | |
// 为了设置上下文处理props和slot插槽 | |
if (!(__COMPAT__ && compatMountInstance)) { | |
//设置组件实例 | |
setupComponent(instance) | |
} | |
//setup()是异步的。这个组件在进行之前依赖于异步逻辑的解决 | |
if (__FEATURE_SUSPENSE__ && instance.asyncDep) { | |
parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect) | |
if (!initialVNode.el) {//如果n2没有宿主 | |
const placeholder = (instance.subTree = createVNode(Comment)) | |
processCommentNode(null, placeholder, container!, anchor) | |
} | |
return | |
} | |
//设置运行渲染副作用函数 | |
setupRenderEffect( | |
instance,//存储了新节点的组件上下文,props插槽等其他实例属性 | |
initialVNode,//新节点n2 | |
container,//容器 | |
anchor,//锚点 | |
parentSuspense,//父悬念 | |
isSVG,//是否SVG | |
optimized//是否优化 | |
) | |
} |
挂载组件中,除开缓存和悬挂上的函数处理,其逻辑上基本为:创建组件的实例createComponentInstance()
,设置组件实例 setupComponent(instance)
和设置运行渲染副作用函数setupRenderEffect()
。
创建组件实例,基本跟创建虚拟节点一样的,内部以对象的方式创建渲染组件实例。 设置组件实例,是将组件中许多数据,赋值给了instance,维护组件上下文,同时对props和插槽等属性初始化处理。
然后是setupRenderEffect
设置渲染副作用函数;
const setupRenderEffect: SetupRenderEffectFn = ( | |
instance,//实例 | |
initialVNode,//初始化节点 | |
container,//容器 | |
anchor,//锚点 | |
parentSuspense,//父悬念 | |
isSVG,//是否是SVG | |
optimized//优化标记 | |
) => { | |
//组件更新方法 | |
const componentUpdateFn = () => { | |
//如果组件处于未挂载的状态下 | |
if (!instance.isMounted) { | |
let vnodeHook: VNodeHook | null | undefined | |
//解构 | |
const { el, props } = initialVNode | |
const { bm, m, parent } = instance | |
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode) | |
toggleRecurse(instance, false) | |
// 挂载前的钩子 | |
// 挂载前的节点 | |
toggleRecurse(instance, true) | |
//这部分是跟服务器渲染相关的逻辑处理 | |
//创建子树,同时 | |
const subTree = (instance.subTree = renderComponentRoot(instance)) | |
//递归 | |
patch( | |
null,//因为是挂载,所以n1这个老节点是空的。 | |
subTree,//子树赋值到n2这个新节点 | |
container,//挂载到container上 | |
anchor, | |
instance, | |
parentSuspense, | |
isSVG | |
) | |
//保留渲染生成的子树DOM节点 | |
initialVNode.el = subTree.el | |
// 已挂载钩子 | |
// 挂在后的节点 | |
//激活为了缓存根的钩子 | |
// #1742 激活的钩子必须在第一次渲染后被访问 因为该钩子可能会被子类的keep-alive注入。 | |
instance.isMounted = true | |
// #2458: deference mount-only object parameters to prevent memleaks | |
// #2458: 遵从只挂载对象的参数以防止内存泄漏 | |
initialVNode = container = anchor = null as any | |
} else { | |
// 更新组件 | |
// 这是由组件自身状态的突变触发的(next: null)。或者父级调用processComponent(下一个:VNode)。 | |
} | |
} | |
// 创建用于渲染的响应式副作用 | |
const effect = (instance.effect = new ReactiveEffect( | |
componentUpdateFn, | |
() => queueJob(update), | |
instance.scope // 在组件的效果范围内跟踪它 | |
)) | |
//更新方法 | |
const update: SchedulerJob = (instance.update = () => effect.run()) | |
//实例的uid赋值给更新的id | |
update.id = instance.uid | |
// 允许递归 | |
// #1801, #2043 组件渲染效果应允许递归更新 | |
toggleRecurse(instance, true) | |
update() | |
} |
setupRenderEffect()
最后执行的了 update()
方法,其实是运行了effect.run()
,并且将其赋值给了instance.updata中。而 effect 涉及到了 vue3 的响应式模块,该模块的主要功能就是,让对象属性具有响应式功能,当其中的属性发生了变动,那effect副作用所包含的函数也会重新执行一遍,从而让界面重新渲染。这一块内容先不管。从effect函数看,明白了调用了componentUpdateFn
, 即组件更新方法,这个方法涉及了2个条件,一个是初次运行的挂载,而另一个是节点变动后的更新组件。 componentUpdateFn
中进行的初次渲染,主要是生成了subTree
然后把subTree
传递到patch进行了递归挂载到container上。
subTree是什么?
subTree也是一个vnode对象,然而这里的subTree和initialVNode是不同的。以下面举个例子:
<template> | |
<div class="app"> | |
<p>title</p> | |
<helloWorld> | |
</div> | |
</template> |
而helloWorld组件中是<div>标签包含一个<p>标签
<template> | |
<div class="hello"> | |
<p>hello world</p> | |
</div> | |
</template> |
在App组件中,<helloWorld> 节点渲染渲染生成的vnode就是 helloWorld组件的initialVNode,而这个组件内部所有的DOM节点就是vnode通过执行renderComponentRoot
渲染生成的的subTree。 每个组件渲染的时候都会运行render函数,renderComponentRoot
就是去执行render函数创建整个组件内部的vnode,然后进行标准化就得到了该函数的返回结果:子树vnode。 生成子树后,接下来就是继续调用patch函数把子树vnode挂载到container上去。 回到patch后,就会继续对子树vnode进行判断,例如上面的App组件的根节点是<div>标签,而对应的subTree就是普通元素vnode,接下来就是堆普通Element处理的流程。
当节点的类型是普通元素DOM时候,patch判断运行processElement
const processElement = ( | |
n1: VNode | null, //老节点 | |
n2: VNode,//新节点 | |
container: RendererElement,//容器 | |
anchor: RendererNode | null,//锚点 | |
parentComponent: ComponentInternalInstance | null, | |
parentSuspense: SuspenseBoundary | null, | |
isSVG: boolean, | |
slotScopeIds: string[] | null, | |
optimized: boolean | |
) => { | |
isSVG = isSVG || (n2.type as string) === 'svg' | |
if (n1 == null) {//如果没有老节点,其实就是初次渲染,则运行mountElement | |
mountElement( | |
n2, | |
container, | |
anchor, | |
parentComponent, | |
parentSuspense, | |
isSVG, | |
slotScopeIds, | |
optimized | |
) | |
} else { | |
//如果是更新节点则运行patchElement | |
patchElement( | |
n1, | |
n2, | |
parentComponent, | |
parentSuspense, | |
isSVG, | |
slotScopeIds, | |
optimized | |
) | |
} | |
} |
逻辑依旧,如果有n1老节点为null的时候,运行挂载元素的逻辑,否则运行更新元素节点的方法。
以下是mountElement()
的代码:
const mountElement = ( | |
vnode: VNode, | |
container: RendererElement, | |
anchor: RendererNode | null, | |
parentComponent: ComponentInternalInstance | null, | |
parentSuspense: SuspenseBoundary | null, | |
isSVG: boolean, | |
slotScopeIds: string[] | null, | |
optimized: boolean | |
) => { | |
let el: RendererElement | |
let vnodeHook: VNodeHook | undefined | null | |
const { type, props, shapeFlag, transition, dirs } = vnode | |
//创建元素节点 | |
el = vnode.el = hostCreateElement( | |
vnode.type as string, | |
isSVG, | |
props && props.is, | |
props | |
) | |
// 首先挂载子类,因为某些props依赖于子类内容 | |
// 已经渲染, 例如 `<select value>` | |
// 如果标记判断子节点类型是文本类型 | |
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { | |
// 处理子节点是纯文本的情况 | |
hostSetElementText(el, vnode.children as string) | |
//如果标记类型是数组子类 | |
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { | |
//挂载子类,进行patch后进行挂载 | |
mountChildren( | |
vnode.children as VNodeArrayChildren, | |
el, | |
null, | |
parentComponent, | |
parentSuspense, | |
isSVG && type !== 'foreignObject', | |
slotScopeIds, | |
optimized | |
) | |
} | |
if (dirs) { | |
invokeDirectiveHook(vnode, null, parentComponent, 'created') | |
} | |
// 设置范围id | |
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent) | |
// props相关的处理,比如 class,style,event,key等属性 | |
if (props) { | |
for (const key in props) { | |
if (key !== 'value' && !isReservedProp(key)) {//key值不等于value字符且不是 | |
hostPatchProp( | |
el, | |
key, | |
null, | |
props[key], | |
isSVG, | |
vnode.children as VNode[], | |
parentComponent, | |
parentSuspense, | |
unmountChildren | |
) | |
} | |
} | |
if ('value' in props) { | |
hostPatchProp(el, 'value', null, props.value) | |
} | |
if ((vnodeHook = props.onVnodeBeforeMount)) { | |
invokeVNodeHook(vnodeHook, parentComponent, vnode) | |
} | |
} | |
Object.defineProperty(el, '__vueParentComponent', { | |
value: parentComponent, | |
enumerable: false | |
} | |
} | |
if (dirs) { | |
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount') | |
} | |
// #1583 对于内部悬念+悬念未解决的情况,进入钩子应该在悬念解决时调用。 | |
// #1689 对于内部悬念+悬念解决的情况,只需调用它 | |
const needCallTransitionHooks = | |
(!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) && | |
transition && !transition.persisted | |
if (needCallTransitionHooks) { | |
transition!.beforeEnter(el) | |
} | |
//把创建的元素el挂载到container容器上。 | |
hostInsert(el, container, anchor) | |
if ( | |
(vnodeHook = props && props.onVnodeMounted) || | |
needCallTransitionHooks || | |
dirs | |
) { | |
queuePostRenderEffect(() => { | |
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode) | |
needCallTransitionHooks && transition!.enter(el) | |
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted') | |
}, parentSuspense) | |
} | |
} |
mountElement
挂载元素主要做了,创建DOM元素节点,处理节点子节点,挂载子节点,同时对props相关处理。
所以根据代码,首先是通过hostCreateElement方法创建了DOM元素节点。
const {createElement:hostCreateElement } = options
是从options这个实参中解构并重命名为hostCreateElement
方法的,那么这个实参是从哪里来 需要追溯一下,回到初次渲染开始的流程中去。
从这流程图可以清楚的知道,options
中createElement
方法是从nodeOps.ts
文件中导出的并传入baseCreateRender()
方法内的。
该文件位于:core/packages/runtime-dom/src/nodeOps.ts
createElement: (tag, isSVG, is, props): Element => { | |
const el = isSVG | |
? doc.createElementNS(svgNS, tag) | |
: doc.createElement(tag, is ? { is } : undefined) | |
if (tag === 'select' && props && props.multiple != null) { | |
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple) | |
} | |
return el | |
}, |
从中可以看出,其实是调用了底层的DOM API document.createElement创建元素。
说回上面,创建完DOM节点元素之后,接下来是继续判断子节点的类型,如果子节点是文本类型的,则调用处理文本hostSetElementText()
方法。
const {setElementText: hostSetElementText} = option | |
setElementText: (el, text) => { | |
el.textContent = text | |
}, |
与前面的createElement一样,setElementText方法是通过设置DOM元素的textContent属性设置文本。
而如果子节点的类型是数组类,则执行mountChildren方法,对子节点进行挂载:
const mountChildren: MountChildrenFn = ( | |
children,//子节点数组里的内容 | |
container,//容器 | |
anchor, | |
parentComponent, | |
parentSuspense, | |
isSVG, | |
slotScopeIds, | |
optimized,//优化标记 | |
start = 0 | |
) => { | |
//遍历子节点中的内容 | |
for (let i = start; i < children.length; i++) { | |
//根据优化标记进行判断进行克隆或者节点初始化处理。 | |
const child = (children[i] = optimized | |
? cloneIfMounted(children[i] as VNode) | |
: normalizeVNode(children[i])) | |
//执行patch方法,递归挂载child | |
patch( | |
null,//因为是初次挂载所以没有老的节点 | |
child,//虚拟子节点 | |
container,//容器 | |
anchor, | |
parentComponent, | |
parentSuspense, | |
isSVG, | |
slotScopeIds, | |
optimized | |
) | |
} | |
} |
子节点的挂载逻辑看起来会非常眼熟,在对children数组进行遍历之后获取到的每一个child,进行预处理后并对其执行挂载方法。 结合之前调用mountChildren()
方法传入的实参和其形参之间的对比。
mountChildren( | |
vnode.children as VNodeArrayChildren, //节点中子节点的内容 | |
el,//DOM元素 | |
null, | |
parentComponent, | |
parentSuspense, | |
isSVG && type !== 'foreignObject', | |
slotScopeIds, | |
optimized | |
) | |
const mountChildren: MountChildrenFn = ( | |
children,//子节点数组里的内容 | |
container,//容器 | |
anchor, | |
parentComponent, | |
parentSuspense, | |
isSVG, | |
slotScopeIds, | |
optimized,//优化标记 | |
start = 0 | |
) |
明确的对应上了第二个参数是container,而调用mountChildren
方法时传入第二个参数的是在调用mountElement()
时创建的DOM节点,这样便建立起了父子关系。 而且,后续的继续递归patch()
,能深度遍历树的方式,可以完整的把DOM树遍历出来,完成渲染。
处理完节点的后,最后会调用 hostInsert(el, container, anchor)
const {insert: hostInsert} = option | |
insert: (child, parent, anchor) => { | |
parent.insertBefore(child, anchor || null) | |
}, |
再次就用调用DOM方法将子类的内容挂载到parent,也就是把child挂载到parent下,完成节点的挂载。
注意点:node.insertBefore(newnode,existingnode)中_existingnode_虽然是可选的对象,但是实际上,在不同的浏览器会有不同的表现形式,所以如果没有existingnode值的情况下,填入null会将新的节点添加到node子节点的尾部。