vue3源码浅析:createApp
vue3源码浅析:mount
一个简单的例子
mount
方法,将应用实例的根组件挂载在提供的 DOM 元素上。数据响应式;vnode获取,vnode diff,最后渲染成DOM,这些方法都是在mount执行中完成的。
还是从一个简单的例子中看看其执行过程
<div class="" id="app">
<button @click='add'>
<h3>{{num}}</h3>
</button>
</div>
<script>
const {
createApp,ref
} = Vue
const app = createApp({
setup(){
let num = ref(0);
const add = ()=>{
num.value++;
}
return{
num,add
}
}
}).mount('#app')
</script>
mount的执行过程
以下为本例中app.mount()的执行
下面针对每一个函数的执行过程做一个初步解析
app.mount
app.mount
方法是在createApp中对mount方法的扩展,会将根元素传递给内部的mount
方法
执行过程如下:
-
normalizeContainer
通过document.querySelector获得DOM元素在本例中参数
containerOrSelector
为#app,得到的container为div#app及其子元素 -
得到
createApp
的配置对象,保存在component
中在本例中app._component为setup函数
-
判断配置对象
component
,得到template元素,并保存到component
内在本例中component.template为div#app的子元素
-
将根元素传递给内部的
mount
方法执行
//** \packages\runtime-dom\src\index.ts */
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// clear content before mounting
container.innerHTML = ''
const proxy = mount(container)
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
return proxy
}
mount
mount
方法是app实例的应用方法,就执行过程如下:(SSR暂不做讨论)
-
调用
createVNode
获得vnode,参数rootComponent
为调用createApp(config)
的时候传递进来的config对象和template本例中createVNode参数具体值如下
rootComponent rootProps null -
context则是通过
createAppContext()
获得的,将context保存在根节点上context -
执行
render
将vnode转变成DOM元素并挂载到根元素上本例中render中参数如下
vnode rootContainer div#app
//** \packages\runtime-core\src\apiCreateApp.ts */
const context = createAppContext()
mount(rootContainer: HostElement, isHydrate?: boolean): any {
if (!isMounted) {
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context
// HMR root reload
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer)
}
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
return vnode.component!.proxy
} else if (__DEV__) {
warn(...)
}
},
render
render
方法执行步骤如下:
-
判断vnode是否存在,不存在且container._vnode有值,也就是有之前的dom渲染,则进行unmount操作
-
如果vnode不为空,则进行patch操作,dom diff和渲染
本例中vnode存在执行
patch
方法 -
执行
flushPostFlushCbs
函数,回调调度器,使用Promise实现 -
把container的_vnode存储为当前vnode,方便后面进行dom diff操作
//** \packages\runtime-core\src\renderer.ts */
const render: RootRenderFunction = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
container._vnode = vnode
}
patch
在下面思维导图中,n1代表前一次的vnode,n2代表本次的vnode
path方法执行过程如下:
-
判断n1是否不为null,n1和n2的type是否相同,如果符合条件执行unmount
本例中n1为null,不执行unmount
-
根据n2的vnode.type的类型选择执行方式
本例中vnode.type为component,执行的是processComponent方法,最后调用
mountComponent
方法
//*packages\runtime-core\src\renderer.ts */
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
optimized = false
) => {
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
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)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
processFragment(...)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(...)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(...)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(...)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process(...)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2)
}
}
mountComponent
执行过程如下:
-
调用
createComponentInstance
生成当前组件实例instance本例中instance实例如下
-
调用
setupComponent(instance)
进行组件初始化,初始化props和slots等,并完成数据响应式 -
调用
setupRenderEffect()
安装渲染函数
//** \packages\runtime-core\src\renderer.ts */
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
...
setupComponent(instance)
...
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
}
setupComponent
执行步骤如下:
- 执行initProps,初始化props
- 执行initSlots,初始化slots
//*packages\runtime-core\src\component.ts*//
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR
const { props, children, shapeFlag } = instance.vnode
const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
initProps(instance, props, isStateful, isSSR)
initSlots(instance, children)
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
return setupResult
}
setupRenderEffect
setupRenderEffect
只有一步:
- 为当前实例挂载上update方法,update方法是通过effect生成的
effect在Vue3中的作用就相当于Vue2中的observe,update生成后,挂载之前会先运行一下生成的effect方法,最后返回当前effect方法给update;运行effect函数就相当于Vue2中watcher调用get的过程.
//** \packages\runtime-core\src\renderer.ts */
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// create reactive effect for rendering
instance.update = effect(function componentEffect() {
if (!instance.isMounted){
...
}else {
...
}
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
}
componentEffect
componentEffect函数有两个逻辑,判断是否已经渲染:instance.isMounted;如果已经渲染,则走更新逻辑;未渲染,则走未渲染的逻辑
该函数的执行位置在effect --> createReactiveEffect中,未渲染的执行过程如下:
-
beforeMount生命周期如果存在,执行该生命函数
-
父类的BeforeMount生命周期如果存在,执行该生命函数
-
调用
renderComponentRoot
,进行渲染组件的根元素,得到subTreeinstance实例 subTree -
执行
patch
方法本例中第二次执行patch,vnode.type为element,执行的是
processElement
方法,最后调用mountElement
方法 -
调用当前实例的mounted钩子函数;调用n2的父类的mounted钩子函数;调用当前实例的activated钩子函数;不是直接调用,而是通过queuePostRenderEffect放到队列中去调用
-
最终把实例的isMounted置为true
//** \packages\runtime-core\src\renderer.ts */
function componentEffect() {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance
// beforeMount hook
if (bm) {
invokeArrayFns(bm)
}
// onVnodeBeforeMount
if ((vnodeHook = props && props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parent, initialVNode)
}
// render
...
const subTree = (instance.subTree = renderComponentRoot(instance))
...
if (el && hydrateNode) {
...
} else {
...
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
...
initialVNode.el = subTree.el
}
// mounted hook
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
// onVnodeMounted
if ((vnodeHook = props && props.onVnodeMounted)) {
const scopedInitialVNode = initialVNode
queuePostRenderEffect(() => {
invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode)
}, parentSuspense)
}
// activated hook for keep-alive roots.
// #1742 activated hook must be accessed after first render
// since the hook may be injected by a child keep-alive
const { a } = instance
if (
a &&
initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
) {
queuePostRenderEffect(a, parentSuspense)
}
instance.isMounted = true
// #2458: deference mount-only object parameters to prevent memleaks
initialVNode = container = anchor = null as any
} else {
...
}
patch
这次第二次执行patch,第一次patch的对象是单个组件,而这次是div#app内的元素,执行过程和第一次的相同
mountElement
mountElement
作为元素渲染方法,其将vnode通过insert方法渲染为真实DOM,如果碰到含有子元素则调用mountChildren
//** \packages\runtime-core\src\renderer.ts */
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const {
type,
props,
shapeFlag,
transition,
scopeId,
patchFlag,
dirs
} = vnode
if (
!__DEV__ &&
vnode.el &&
hostCloneNode !== undefined &&
patchFlag === PatchFlags.HOISTED
) {
// If a vnode has non-null el, it means it's being reused.
// Only static vnodes can be reused, so its mounted DOM nodes should be
// exactly the same, and we can simply do a clone here.
// only do this in production since cloned trees cannot be HMR updated.
el = vnode.el = hostCloneNode(vnode.el)
} else {
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is
)
// mount children first, since some props may rely on child content
// being already rendered, e.g. `<select value>`
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(...)
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// props
if (props) {
for (const key in props) {
if (!isReservedProp(key)) {
hostPatchProp(...)
}
}
if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}
// scopeId
setScopeId(el, scopeId, vnode, parentComponent)
}
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
Object.defineProperty(el, '__vnode', {
value: vnode,
enumerable: false
})
Object.defineProperty(el, '__vueParentComponent', {
value: parentComponent,
enumerable: false
})
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
}
// #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved
// #1689 For inside suspense + suspense resolved case, just call it
const needCallTransitionHooks =
(!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
transition &&
!transition.persisted
if (needCallTransitionHooks) {
transition!.beforeEnter(el)
}
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)
}
}
effect
effect
作为 reactive
的核心,主要负责收集依赖,更新依赖
effect
保存了组件更新函数componentEffect
,如果数据发生变化,重新执行组件更新函数
effect函数有两个参数
fn | prodEffectOptions | fn = ƒ componentEffect() |
---|
//* vue-next\packages\reactivity\src\effect.ts */
export interface ReactiveEffectOptions {
lazy?: boolean // 是否延迟触发 effect
computed?: boolean // 是否为计算属性
scheduler?: (job: ReactiveEffect) => void // 调度函数
onTrack?: (event: DebuggerEvent) => void // 追踪时触发
onTrigger?: (event: DebuggerEvent) => void // 触发回调时触发
onStop?: () => void // 停止监听时触发
}
//* packages\reactivity\src\effect.ts */
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
// 如果已经是 `effect` 先重置为原始对象
if (isEffect(fn)) {
fn = fn.raw
}
// 创建`effect`
const effect = createReactiveEffect(fn, options)
// 如果没有传入 lazy 则直接执行一次 `effect`
if (!options.lazy) {
effect()
}
return effect
}
createReactiveEffect
createReactiveEffect
为effect
创建函数,依赖收集过程如图所示
三个函数作用如下:
effect:将回调函数保存起来备用,立即执行一次回调函数触发它里面一些响应数据的getter
track:getter中调用track,把前面存储的回调函数和当前target,key之间建立映射关系
trigger:setter中调用trigger,把target,key对应的响应函数都执行一遍
//* packages\reactivity\src\effect.ts /
function createReactiveEffect<T = any>(
fn: (...args: any[]) => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
// 没有激活,说明我们调用了effect stop 函数,
if (!effect.active) {
// 如果没有调度者,直接返回,否则直接执行fn
return options.scheduler ? undefined : fn(...args)
}
// 判断effectStack中有没有effect, 如果在则不处理
if (!effectStack.includes(effect)) {
// 清除effect依赖,定义在下方
cleanup(effect)
try {
// 开始重新收集依赖
enableTracking()
// 压入Stack
effectStack.push(effect)
// 将activeEffect当前effect
activeEffect = effect
return fn(...args)
} finally {
// 完成后将effect弹出
effectStack.pop()
// 重置依赖
resetTracking()
// 重置activeEffect
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++ // 自增id, effect唯一标识
effect._isEffect = true // 是否是effect
effect.active = true // 是否激活
effect.raw = fn // 挂载原始对象
effect.deps = [] // 当前 effect 的dep 数组
effect.options = options // 传入的options,在effect有解释的那个字段
return effect
}
const effectStack: ReactiveEffect[] = []
// 每次 effect 运行都会重新收集依赖, deps 是 effect 的依赖数组, 需要全部清空
function cleanup(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
参考
effect源码分析
mount源码分析
vue3初探
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!