在「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 源碼 | 從編譯過程,理解靜態節點提高
寫做不易,若是你以爲有收穫的話,能夠愛心三連擊!!!