深度解讀 Vue3 源碼 | 組件建立過程

前言

image

在「Vue3」中,建立一個組件實例由 createApp 「API」完成。建立完一個組件實例,咱們須要調用 mount() 方法將組件實例掛載到頁面中:javascript

createApp({
    ...
}).mount("#app");

在源碼中整個組件的建立過程:
java

mountComponent() 實現的核心是 setupComponent(),它能夠分爲兩個過程node

  • 開始安裝,它會初始化 propsslots、調用 setup()、驗證組件和指令的合理性。
  • 結束安裝,它會初始化 computeddatawatchmixin 和生命週期等等。

那麼,接下來咱們仍然從源碼的角度,詳細地分析一下這兩個過程。react

1 開始安裝

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 行會先初始化組件的 propsslots。而後,在 A 行判斷 shapeFlagtrue 時,調用 setupStatefulComponent()app

這裏又用到了 shapeFlag,因此須要強調的是 shapeFlagpatchFlag 具備同樣的地位(重要性)。

setupStatefulComponent() 則會處理組合 Composition API,即調用 setup()函數

1.1 setupStatefulComponent

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 的值爲 truefalse 來控制此時是否進行依賴收集。之因此,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)

1.2 handleSetupResult

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() 返回的值是 nullundefined或者其餘非對象類型。

1.3 小結

到此,組件的開始安裝過程就結束了。咱們再來回顧一下這個過程會作的幾件事,初始化 propsslot以及處理 setup() 返回的結果,期間還涉及到一個暫停依賴收集的微妙處理。

須要注意的是,此時組件並沒有開始建立,所以咱們稱之爲這個過程爲安裝。而且,這也是爲何官方文檔會這麼介紹 setup()

一個組件選項, 在建立組件以前執行,一旦 props 被解析,並做爲組合 API 的入口點

2 結束安裝

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),即對應的 computedwatchlifecycle 等等。

2.1 applyOptions

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 重名時選擇本地仍是全局的處理,有興趣的同窗能夠去官方文檔瞭解。

咱們再從代碼層面看整個流程,這裏分析幾點常關注的屬性是怎麼初始化的:

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 對應的 getset(行 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,它是優於 mixindatawatchcomputed 先處理:

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 源碼 | 從編譯過程,理解靜態節點提高

❤️愛心三連擊

寫做不易,若是你以爲有收穫的話,能夠愛心三連擊!!!

相關文章
相關標籤/搜索