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函數中去進行渲染。
虛擬節點其實就是JavaScript的一個對象,用來描述DOM。
這裡可以寫一個實際的簡單範例來輔助理解,以下是一段html的普通元素節點
<div class="title" >这是一个标题</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有元素,方法函數元件,有狀態的元件,子類別是文字等。
// 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可以抽象,把渲染的過程抽象化,讓元件的抽象能力也得到提升。然後因為vue需要可以跨平台,講節點抽象化後可以透過平台自己的實現,使之在各個平台上渲染更容易。不過同時要注意的一點,雖然使用的是vnode,但這並不代表vnode的效能更有優勢。例如很大的組件,是表格上千行的表格,在render過程中,創建vnode勢必得遍歷上千次vnode的創建,然後遍歷上千次的patch,在更新表格資料中,勢必會出現卡頓的情況。即使在patch中使用diff優化了對DOM操作次數,但始終需要操作。
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
以上,將虛擬節點其中一部分的屬性處理好之後,再傳入建立基礎虛擬節點函數中,做更進一步更詳細的屬性物件建立。
建立基礎虛擬節點(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物件的過程。
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()
函數的程式碼,著重了解當元件初次渲染的時候的流程。
// 注意:此闭包中的函数应使用 '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类型。
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也是一个vnode对象,然而这里的subTree和initialVNode是不同的。以下面举个例子:
<template> <div class="app"> <p>title</p> <helloWorld> </div> </template>
而helloWorld组件中是
标签
<template> <div class="hello"> <p>hello world</p> </div> </template>
在App组件中, 逻辑依旧,如果有n1老节点为null的时候,运行挂载元素的逻辑,否则运行更新元素节点的方法。 以下是 所以根据代码,首先是通过hostCreateElement方法创建了DOM元素节点。 是从options这个实参中解构并重命名为 从这流程图可以清楚的知道, 该文件位于:core/packages/runtime-dom/src/nodeOps.ts 从中可以看出,其实是调用了底层的DOM API document.createElement创建元素。 说回上面,创建完DOM节点元素之后,接下来是继续判断子节点的类型,如果子节点是文本类型的,则调用处理文本 与前面的createElement一样,setElementText方法是通过设置DOM元素的textContent属性设置文本。 而如果子节点的类型是数组类,则执行mountChildren方法,对子节点进行挂载: 子节点的挂载逻辑看起来会非常眼熟,在对children数组进行遍历之后获取到的每一个child,进行预处理后并对其执行挂载方法。 结合之前调用 明确的对应上了第二个参数是container,而调用 处理完节点的后,最后会调用 再次就用调用DOM方法将子类的内容挂载到parent,也就是把child挂载到parent下,完成节点的挂载。 注意点:node.insertBefore(newnode,existingnode)中_existingnode_虽然是可选的对象,但是实际上,在不同的浏览器会有不同的表现形式,所以如果没有existingnode值的情况下,填入null会将新的节点添加到node子节点的尾部。 以上是Vue3怎麼將虛擬節點渲染到網頁初次渲染的詳細內容。更多資訊請關注PHP中文網其他相關文章!renderComponentRoot
渲染生成的的subTree。 每个组件渲染的时候都会运行render函数,renderComponentRoot
就是去执行render函数创建整个组件内部的vnode,然后进行标准化就得到了该函数的返回结果:子树vnode。 生成子树后,接下来就是继续调用patch函数把子树vnode挂载到container上去。 回到patch后,就会继续对子树vnode进行判断,例如上面的App组件的根节点是当节点的类型是普通元素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
)
}
}
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相关处理。const {createElement:hostCreateElement } = options
hostCreateElement
方法的,那么这个实参是从哪里来 需要追溯一下,回到初次渲染开始的流程中去。options
中createElement
方法是从nodeOps.ts
文件中导出的并传入baseCreateRender()
方法内的。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
},
hostSetElementText()
方法。const {setElementText: hostSetElementText} = option
setElementText: (el, text) => {
el.textContent = text
},
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
)
}
}
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
)
mountChildren
方法时传入第二个参数的是在调用mountElement()
时创建的DOM节点,这样便建立起了父子关系。 而且,后续的继续递归patch()
,能深度遍历树的方式,可以完整的把DOM树遍历出来,完成渲染。 hostInsert(el, container, anchor)
const {insert: hostInsert} = option
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},