在「Vue3」中,建立一个组件实例由 createApp
「API」完成。建立完一个组件实例,咱们须要调用 mount()
方法将组件实例挂载到页面中:javascript
createApp({ ... }).mount("#app");
在源码中整个组件的建立过程:java
mountComponent()
实现的核心是 setupComponent()
,它能够分为两个过程:node
props
、slots
、调用 setup()
、验证组件和指令的合理性。computed
、data
、watch
、mixin
和生命周期等等。那么,接下来咱们仍然从源码的角度,详细地分析一下这两个过程。react
setupComponent()
的定义:segmentfault
// packages/runtime-core/src/component.ts function 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()
。app
这里又用到了shapeFlag
,因此须要强调的是shapeFlag
和patchFlag
具备同样的地位(重要性)。
而 setupStatefulComponent()
则会处理组合 Composition API
,即调用 setup()
。函数
setupStatefulComponent()
定义(伪代码):优化
// packages/runtime-core/src/component.ts setupStatefulComponent( 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
以前,咱们会作两个操做暂停依赖收集、恢复依赖收集:this
暂停依赖收集 pauseTracking()
:spa
// packages/reactivity/src/effect.ts function pauseTracking() { trackStack.push(shouldTrack) shouldTrack = false }
恢复依赖收集 resetTracking()
:
// packages/reactivity/src/effect.ts resetTracking() { const last = trackStack.pop() shouldTrack = last === undefined ? true : last }
本质上这两个步骤是经过改变 shouldTrack
的值为 true
或 false
来控制此时是否进行依赖收集。之因此,shouldTrack
能够控制是否进行依赖收集,是由于在 track
的执行开始有这么一段代码:
// packages/reactivity/src/effect.ts function 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)
handleSetupResult()
定义:
// packages/runtime-core/src/component.ts function 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
或者其余非对象类型。到此,组件的开始安装过程就结束了。咱们再来回顾一下这个过程会作的几件事,初始化 props
、slot
以及处理 setup()
返回的结果,期间还涉及到一个暂停依赖收集的微妙处理。
须要注意的是,此时组件并没有开始建立,所以咱们称之为这个过程为安装。而且,这也是为何官方文档会这么介绍 setup()
:
一个组件选项, 在建立组件以前执行,一旦 props 被解析,并做为组合 API 的入口点
finishComponentSetup()
定义(伪代码):
// packages/runtime-core/src/component.ts function 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
等等。applyOptions()
定义:
// packages/runtime-core/src/componentOptions.ts function applyOptions( instance: ComponentInternalInstance, options: ComponentOptions, deferredData: DataFn[] = [], deferredWatch: ComponentWatchOptions[] = [], asMixin: boolean = false ) { ... }
因为, applyOptions()
涉及的代码较多,咱们先不看代码,看一下总体的流程:
applyOptions()
的流程并不复杂,可是从流程中咱们总结出两点日常开发中忌讳的点:
beforeCreate
中访问 mixin
相关变量。mixin
后于全局 mixin
执行,因此在一些变量命名重复的场景,咱们须要确认要使用的是全局 mixin
的这个变量仍是本地的 mixin
。
对于
mixin
重名时选择本地仍是全局的处理,有兴趣的同窗能够去官方文档了解。
咱们再从代码层面看整个流程,这里分析几点常关注的属性是怎么初始化的:
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)。而且,在开发环境下会对当前上下文属性的惟一性进行判断。
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
能够获取到该计算属性。
生命周期的处理比较特殊的是 beforeCreate
,它是优于 mixin
、data
、watch
、computed
先处理:
if (!asMixin) { callSyncHook('beforeCreate', options, publicThis, globalMixins) applyMixins(instance, globalMixins, deferredData, deferredWatch) }
至于其他的生命周期是在最后处理,即它们能够正常地访问实例上的属性(伪代码):
if (lifecycle) { onBeforeMount(lifecycle.bind(publicThis)) }
结束安装过程,主要是初始化咱们常见的组件上的选项,只不过咱们能够不用 options
式的写法,可是实际上源码中仍然是转化成 options
处理,主要也是为了兼容 options
写法。而且,结束安装的过程比较重要的一点就是调用各个生命周期,而熟悉每一个生命周期的执行时机,也能够便于咱们日常的开发不犯错。
这是「深度解读 Vue3 源码」系列的第四篇文章,理论上也是第七篇。每写完一篇,我都在思考如何表达才能使得文章的阅读性变得更好,而这篇文章表达方式也是在翻译了两篇 Dr. Axel Rauschmayer
大佬文章后,我思考的几点文章中须要作的改变。最后,文章中若是存在不当的地方,欢迎各位同窗提 Issue。
为何是第七篇,由于我将会把这个系列的文章汇总成一个 Git Page,因此,有一些文章并无同步这里,目前正在整理中。
深度解读 Vue3 源码 | 内置组件 teleport 是什么“来头”?
深度解读 Vue 3 源码 | compile 和 runtime 结合的 patch 过程
深度解读 Vue 3 源码 | 从编译过程,理解静态节点提高
写做不易,若是你以为有收获的话,能够爱心三连击!!!