前言
在「Vue3」中,创立一个组件实例由 createApp
「API」实现。创立完一个组件实例,咱们须要调用 mount()
办法将组件实例挂载到页面中:
createApp({ ...}).mount("#app");
在源码中整个组件的创立过程:
mountComponent()
实现的外围是 setupComponent()
,它能够分为两个过程:
- 开始装置,它会初始化
props
、slots
、调用setup()
、验证组件和指令的合理性。 - 完结装置,它会初始化
computed
、data
、watch
、mixin
和生命周期等等。
那么,接下来咱们依然从源码的角度,具体地剖析一下这两个过程。
1 开始装置
setupComponent()
的定义:
// packages/runtime-core/src/component.tsfunction setupComponent( instance: ComponentInternalInstance, isSSR = false) { isInSSRComponentSetup = isSSR const { props, children, shapeFlag } = instance.vnode const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT // {A} initProps(instance, props, isStateful, isSSR) // {B} initSlots(instance, children) // {C} const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : undefined // {D} isInSSRComponentSetup = false return setupResult}
抛开 SSR
的逻辑,B 行和 C 行会先初始化组件的 props
和 slots
。而后,在 A 行判断 shapeFlag
为 true
时,调用 setupStatefulComponent()
。
这里又用到了shapeFlag
,所以须要强调的是shapeFlag
和patchFlag
具备一样的位置(重要性)。
而 setupStatefulComponent()
则会解决组合 Composition API
,即调用 setup()
。
1.1 setupStatefulComponent
setupStatefulComponent()
定义(伪代码):
// packages/runtime-core/src/component.tssetupStatefulComponent( instance: ComponentInternalInstance, isSSR: boolean) { const Component = instance.type as ComponentOptions // {A} 验证逻辑 ... instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers) ... const { setup } = Component if (setup) { const setupContext = (instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null) currentInstance = instance // {B} pauseTracking() // {C} const setupResult = callWithErrorHandling( setup, instance, ErrorCodes.SETUP_FUNCTION, [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext] ) // {D} resetTracking() // {E} currentInstance = null if (isPromise(setupResult)) { ... } else { handleSetupResult(instance, setupResult, isSSR) // {F} } } else { finishComponentSetup(instance, isSSR) }}
首先,在 B 行会给以后实例 currentInstance
赋值为此时的组件实例 instance
,在回收 currentInstance
之前,咱们会做两个操作暂停依赖收集、复原依赖收集:
暂停依赖收集 pauseTracking()
:
// packages/reactivity/src/effect.tsfunction pauseTracking() { trackStack.push(shouldTrack) shouldTrack = false}
复原依赖收集 resetTracking()
:
// packages/reactivity/src/effect.tsresetTracking() { const last = trackStack.pop() shouldTrack = last === undefined ? true : last}
实质上这两个步骤是通过扭转 shouldTrack
的值为 true
或 false
来管制此时是否进行依赖收集。之所以,shouldTrack
能够管制是否进行依赖收集,是因为在 track
的执行开始有这么一段代码:
// packages/reactivity/src/effect.tsfunction track(target: object, type: TrackOpTypes, key: unknown) { if (!shouldTrack || activeEffect === undefined) { return } ...}
那么,咱们就会提出疑难为什么这个时候须要暂停依赖收?这里,咱们回到 D 行:
const setupResult = callWithErrorHandling( setup, instance, ErrorCodes.SETUP_FUNCTION, [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext] ) // {D}
在 DEV
环境下,咱们须要通过 shallowReadonly(instance.props)
创立一个基于组件 props
的拷贝对象 Proxy
,而 props
实质上是响应式地,这个时候会触发它的 track
逻辑,即依赖收集,显著这并不是开发中理论须要的订阅对象,所以,此时要暂停 props
的依赖收集,过滤不必要的订阅。
相比拟,「Vue2.x」泛滥的订阅关系而言,这里不得不给「Vue3」对订阅关系解决的谨严思维点赞!
通常,咱们 setup()
返回的是一个 Object
,所以会命中 F 行的逻辑:
handleSetupResult(instance, setupResult, isSSR)
1.2 handleSetupResult
handleSetupResult()
定义:
// packages/runtime-core/src/component.tsfunction handleSetupResult( instance: ComponentInternalInstance, setupResult: unknown, isSSR: boolean) { if (isFunction(setupResult)) { instance.render = setupResult as InternalRenderFunction } else if (isObject(setupResult)) { if (__DEV__ && isVNode(setupResult)) { warn( `setup() should not return VNodes directly - ` + `return a render function instead.` ) } instance.setupState = proxyRefs(setupResult) if (__DEV__) { exposeSetupStateOnRenderContext(instance) } } else if (__DEV__ && setupResult !== undefined) { warn( `setup() should return an object. Received: ${ setupResult === null ? 'null' : typeof setupResult }` ) } finishComponentSetup(instance, isSSR)}
handleSetupResult()
的分支逻辑较为简单,次要是验证 setup()
返回的后果,以下两种状况都是不非法的:
setup()
返回的值是render()
的执行后果,即VNode
。setup()
返回的值是null
、undefined
或者其余非对象类型。
1.3 小结
到此,组件的开始装置过程就完结了。咱们再来回顾一下这个过程会做的几件事,初始化 props
、slot
以及解决 setup()
返回的后果,期间还波及到一个暂停依赖收集的奥妙解决。
须要留神的是,此时组件并没有开始创立,因而咱们称之为这个过程为装置。并且,这也是为什么官网文档会这么介绍 setup()
:
一个组件选项,在创立组件之前执行,一旦 props 被解析,并作为组合 API 的入口点
2 完结装置
finishComponentSetup()
定义(伪代码):
// packages/runtime-core/src/component.tsfunction finishComponentSetup( instance: ComponentInternalInstance, isSSR: boolean) { const Component = instance.type as ComponentOptions ... if (!instance.render) { // {A} if (compile && Component.template && !Component.render) { ... Component.render = compile(Component.template, { isCustomElement: instance.appContext.config.isCustomElement || NO, delimiters: Component.delimiters }) ... } instance.render = (Component.render || NOOP) as InternalRenderFunction // {B} if (instance.render._rc) { instance.withProxy = new Proxy( instance.ctx, RuntimeCompiledPublicInstanceProxyHandlers ) } } if (__FEATURE_OPTIONS_API__) { // {C} currentInstance = instance applyOptions(instance, Component) currentInstance = null } ...}
整体上 finishComponentSetup()
能够分为三个外围逻辑:
- 绑定
render
函数到以后实例instance
上(行 A),这会两种状况,一是手写render
函数,二是模板template
写法,它会调用compile
编译模板生成render
函数。 - 为模板
template
生成的render
函数(行 B),独自应用一个不同的has
陷阱。因为,编译生成的render
函数是会存在withBlock
之类的优化,以及它会有一个全局的白名单来实现防止进入has
陷阱。 - 利用
options
(行 C),即对应的computed
、watch
、lifecycle
等等。
2.1 applyOptions
applyOptions()
定义:
// packages/runtime-core/src/componentOptions.tsfunction applyOptions( instance: ComponentInternalInstance, options: ComponentOptions, deferredData: DataFn[] = [], deferredWatch: ComponentWatchOptions[] = [], asMixin: boolean = false) { ...}
因为, applyOptions()
波及的代码较多,咱们先不看代码,看一下整体的流程:
applyOptions()
的流程并不简单,然而从流程中咱们总结出两点平时开发中禁忌的点:
- 不要在
beforeCreate
中拜访mixin
相干变量。 - 因为本地
mixin
后于全局mixin
执行,所以在一些变量命名反复的场景,咱们须要确认要应用的是全局mixin
的这个变量还是本地的mixin
。
对于 mixin
重名时抉择本地还是全局的解决,有趣味的同学能够去官网文档理解。
咱们再从代码层面看整个流程,这里剖析几点常关注的属性是怎么初始化的:
2.1.1 注册事件(methods)
if (methods) { for (const key in methods) { const methodHandler = (methods as MethodOptions)[key] if (isFunction(methodHandler)) { ctx[key] = methodHandler.bind(publicThis) // {A} if (__DEV__) { checkDuplicateProperties!(OptionTypes.METHODS, key) } } else if (__DEV__) { warn( `Method "${key}" has type "${typeof methodHandler}" in the component definition. ` + `Did you reference the function correctly?` ) } }}
事件的注册,次要就是遍历曾经解决好的 methods
属性,而后在以后上下文 ctx
中绑定对应事件名的属性 key
的事件 methodHandler
(行 A)。并且,在开发环境下会对以后上下文属性的唯一性进行判断。
2.1.2 绑定计算属性(computed)
if (computedOptions) { for (const key in computedOptions) { const opt = (computedOptions as ComputedOptions)[key] const get = isFunction(opt) ? opt.bind(publicThis, publicThis) : isFunction(opt.get) ? opt.get.bind(publicThis, publicThis) : NOOP // {A} if (__DEV__ && get === NOOP) { warn(`Computed property "${key}" has no getter.`) } const set = !isFunction(opt) && isFunction(opt.set) ? opt.set.bind(publicThis) : __DEV__ ? () => { warn( `Write operation failed: computed property "${key}" is readonly.` ) } : NOOP // {B} const c = computed({ get, set }) // {C} Object.defineProperty(ctx, key, { enumerable: true, configurable: true, get: () => c.value, set: v => (c.value = v) }) {D} if (__DEV__) { checkDuplicateProperties!(OptionTypes.COMPUTED, key) } } }
绑定计算属性次要是遍历构建好的 computedOptions
,而后提取每一个计算属性 key
对应的 get
和 set
(行 A),也是咱们相熟的对于 get
是强校验,即计算属性必须要有 get
,能够没有 set
,如果没有 set
(行 B),此时它的 set
为:
() => { warn( `Write operation failed: computed property "${key}" is readonly.` )}
所以,这也是为什么咱们批改一个没有定义 set
的计算属性时会提醒这样的谬误。
而后,在 C 行会调用 computed
注册该计算属性,即 effect
的注册。最初,将该计算属性通过 Object.defineProperty
代理到以后上下文 ctx
中(行 D),保障通过 this.computedAttrName
能够获取到该计算属性。
2.1.3 生命周期解决
生命周期的解决比拟非凡的是 beforeCreate
,它是优于 mixin
、data
、watch
、computed
先解决:
if (!asMixin) { callSyncHook('beforeCreate', options, publicThis, globalMixins) applyMixins(instance, globalMixins, deferredData, deferredWatch)}
至于其余的生命周期是在最初解决,即它们能够失常地拜访实例上的属性(伪代码):
if (lifecycle) { onBeforeMount(lifecycle.bind(publicThis))}
2.2 小结
完结装置过程,次要是初始化咱们常见的组件上的选项,只不过咱们能够不必 options
式的写法,然而实际上源码中依然是转化成 options
解决,次要也是为了兼容 options
写法。并且,完结装置的过程比拟重要的一点就是调用各个生命周期,而相熟每个生命周期的执行机会,也能够便于咱们平时的开发不犯错。
写在最初
这是「深度解读 Vue3 源码」系列的第四篇文章,实践上也是第七篇。每写完一篇,我都在思考如何表白能力使得文章的浏览性变得更好,而这篇文章表达方式也是在翻译了两篇 Dr. Axel Rauschmayer
大佬文章后,我思考的几点文章中须要做的扭转。最初,文章中如果存在不当的中央,欢送各位同学提 Issue。
为什么是第七篇,因为我将会把这个系列的文章汇总成一个 Git Page,所以,有一些文章并没有同步这里,目前正在整顿中。
往期文章回顾
深度解读 Vue3 源码 | 内置组件 teleport 是什么“来头”?
深度解读 Vue 3 源码 | compile 和 runtime 联合的 patch 过程
深度解读 Vue 3 源码 | 从编译过程,了解动态节点晋升
❤️爱心三连击
写作不易,如果你感觉有播种的话,能够爱心三连击!!!