1. 程式人生 > >vue的原始碼學習之六——6.非同步元件

vue的原始碼學習之六——6.非同步元件

1 介紹

       版本:2.5.17。 

       我們使用vue-vli建立基於Runtime+Compiler的vue腳手架。

       學習文件:https://ustbhuangyi.github.io/vue-analysis/components/async-component.html

2 非同步元件

2.1 定義 

 非同步元件就是定義的時候什麼都不做,只在元件需要渲染(元件第一次顯示)的時候進行載入渲染並快取,快取是以備下次訪問。Vue 也原生支援了非同步元件的能力,如下:

Vue.component('async-example', function (resolve, reject) {
   // 這個特殊的 require 語法告訴 webpack
   // 自動將編譯後的程式碼分割成不同的塊,
   // 這些塊將通過 Ajax 請求自動下載。
   require(['./my-async-component'], resolve)
})

 示例中可以看到,Vue 註冊的元件不再是一個物件,而是一個工廠函式,函式有兩個引數 resolve 和 reject,函式內部用 setTimout 模擬了非同步,實際使用可能是通過動態請求非同步元件的 JS 地址,最終通過執行 resolve

 方法,它的引數就是我們的非同步元件物件。

2.2  原始碼分析

上一節我們分析了元件的註冊邏輯,由於元件的定義並不是一個普通物件,所以不會執行 Vue.extend 的邏輯把它變成一個元件的建構函式,但是它仍然可以執行到 createComponent 函式,我們再來對這個函式做回顧,它的定義

2.2.1 createComponent

   createComponent在 src/core/vdom/create-component/js 中:

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  
  // ...

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }
}

我們省略了不必要的邏輯,只保留關鍵邏輯,由於我們這個時候傳入的 Ctor 是一個函式,那麼它也並不會執行 Vue.extend 邏輯,因此它的 cid 是 undefiend,進入了非同步元件建立的邏輯。這裡首先執行了 Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context) 方法。

2.2.2 resolveAsyncComponent

resolveAsyncComponent定義在 src/core/vdom/helpers/resolve-async-component.js 中:

export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>,
  context: Component
): Class<Component> | void {
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }

  if (isDef(factory.resolved)) {
    return factory.resolved
  }

  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }

  if (isDef(factory.contexts)) {
    // already pending
    factory.contexts.push(context)
  } else {
    const contexts = factory.contexts = [context]
    let sync = true

    const forceRender = () => {
      for (let i = 0, l = contexts.length; i < l; i++) {
        contexts[i].$forceUpdate()
      }
    }

    const resolve = once((res: Object | Class<Component>) => {
      // cache resolved
      factory.resolved = ensureCtor(res, baseCtor)
      // invoke callbacks only if this is not a synchronous resolve
      // (async resolves are shimmed as synchronous during SSR)
      if (!sync) {
        forceRender()
      }
    })

    const reject = once(reason => {
      process.env.NODE_ENV !== 'production' && warn(
        `Failed to resolve async component: ${String(factory)}` +
        (reason ? `\nReason: ${reason}` : '')
      )
      if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender()
      }
    })

    const res = factory(resolve, reject)

    if (isObject(res)) {
      if (typeof res.then === 'function') {
        // () => Promise
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject)
        }
      } else if (isDef(res.component) && typeof res.component.then === 'function') {
        res.component.then(resolve, reject)

        if (isDef(res.error)) {
          factory.errorComp = ensureCtor(res.error, baseCtor)
        }

        if (isDef(res.loading)) {
          factory.loadingComp = ensureCtor(res.loading, baseCtor)
          if (res.delay === 0) {
            factory.loading = true
          } else {
            setTimeout(() => {
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                factory.loading = true
                forceRender()
              }
            }, res.delay || 200)
          }
        }

        if (isDef(res.timeout)) {
          setTimeout(() => {
            if (isUndef(factory.resolved)) {
              reject(
                process.env.NODE_ENV !== 'production'
                  ? `timeout (${res.timeout}ms)`
                  : null
              )
            }
          }, res.timeout)
        }
      }
    }

    sync = false
    // return in case resolved synchronously
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}

 resolveAsyncComponent 函式的邏輯略複雜,因為它實際上處理了 3 種非同步元件的建立方式

  • 普通函式非同步元件
Vue.component('async-example', function (resolve, reject) {
   require(['./my-async-component'], resolve)
})
  • Promise 非同步元件
Vue.component(
  'async-webpack-example',
  // 該 `import` 函式返回一個 `Promise` 物件。
  () => import('./my-async-component')
)
  • 高階非同步元件
const AsyncComp = () => ({
  // 需要載入的元件。應當是一個 Promise
  component: import('./MyComp.vue'),
  // 載入中應當渲染的元件
  loading: LoadingComp,
  // 出錯時渲染的元件
  error: ErrorComp,
  // 渲染載入中元件前的等待時間。預設:200ms。
  delay: 200,
  // 最長等待時間。超出此時間則渲染錯誤元件。預設:Infinity
  timeout: 3000
})
Vue.component('async-example', AsyncComp)

2.3 resolveAsyncComponent 的邏輯

我們就根據普通函式非同步元件、Promise 非同步元件、高階非同步元件的情況,來分別去分析 resolveAsyncComponent 的邏輯 

2.3.1 普通函式非同步元件

針對普通函式的情況,前面幾個 if 判斷可以忽略,它們是為高階元件所用。 

...
  if (isDef(factory.contexts)) {
    // already pending
    factory.contexts.push(context)
  } else {
    const contexts = factory.contexts = [context]
    let sync = true

    const forceRender = () => {
      for (let i = 0, l = contexts.length; i < l; i++) {
        contexts[i].$forceUpdate()
      }
    }

    const resolve = once((res: Object | Class<Component>) => {
      // cache resolved
      factory.resolved = ensureCtor(res, baseCtor)
      // invoke callbacks only if this is not a synchronous resolve
      // (async resolves are shimmed as synchronous during SSR)
      if (!sync) {
        forceRender()
      }
    })
    const reject = once(reason => {
      process.env.NODE_ENV !== 'production' && warn(
        `Failed to resolve async component: ${String(factory)}` +
        (reason ? `\nReason: ${reason}` : '')
      )
      if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender()
      }
    })
...

對於 factory.contexts 的判斷,是考慮到多個地方同時初始化一個非同步元件,那麼它的實際載入應該只有一次。接著進入實際載入邏輯,定義了 forceRenderresolve 和 reject 函式,注意 resolve 和 reject 函式用 once 函式做了一層包裝,它的定義在 src/shared/util.js 中:

/**
 * Ensure a function is called only once.
 */
export function once (fn: Function): Function {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}

once 邏輯非常簡單,傳入一個函式,並返回一個新函式,它非常巧妙地利用閉包和一個標誌位保證了它包裝的函式只會執行一次,也就是確保 resolve 和 reject 函式只執行一次。

    const res = factory(resolve, reject)

接下來執行 const res = factory(resolve, reject) 邏輯,這塊兒就是執行我們元件的工廠函式,同時把 resolve 和 reject 函式作為引數傳入,元件的工廠函式通常會先發送請求去載入我們的非同步元件的 JS 檔案,拿到元件定義的物件 res 後,執行 resolve(res) 邏輯,它會先執行 factory.resolved = ensureCtor(res, baseCtor)

function ensureCtor (comp: any, base) {
  if (
    comp.__esModule ||
    (hasSymbol && comp[Symbol.toStringTag] === 'Module')
  ) {
    comp = comp.default
  }
  return isObject(comp)
    ? base.extend(comp)
    : comp
}

這個函式目的是為了保證能找到非同步元件 JS 定義的元件物件,並且如果它是一個普通物件,則呼叫 Vue.extend 把它轉換成一個元件的建構函式。

resolve 邏輯最後判斷了 sync,顯然我們這個場景下 sync 為 false,那麼就會執行 forceRender 函式,它會遍歷 factory.contexts,拿到每一個呼叫非同步元件的例項 vm, 執行 vm.$forceUpdate() 方法,它的定義在 src/core/instance/lifecycle.js 中:

Vue.prototype.$forceUpdate = function () {
  const vm: Component = this
  if (vm._watcher) {
    vm._watcher.update()
  }
}

$forceUpdate 的邏輯非常簡單,就是呼叫渲染 watcher 的 update 方法,讓渲染 watcher 對應的回撥函式執行,也就是觸發了元件的重新渲染。之所以這麼做是因為 Vue 通常是資料驅動檢視重新渲染,但是在整個非同步元件載入過程中是沒有資料發生變化的,所以通過執行 $forceUpdate 可以強制元件重新渲染一次。

2.3.2 Promise 非同步元件

Vue.component(
  'async-webpack-example',
  // 該 `import` 函式返回一個 `Promise` 物件。
  () => import('./my-async-component')
)

webpack 2+ 支援了非同步載入的語法糖:() => import('./my-async-component'),當執行完 res = factory(resolve, reject),返回的值就是 import('./my-async-component') 的返回值,它是一個 Promise 物件。接著進入 if 條件,又判斷了 typeof res.then === 'function'),條件滿足,執行:

if (isUndef(factory.resolved)) {
  res.then(resolve, reject)
}

 當元件非同步載入成功後,執行 resolve,載入失敗則執行 reject,這樣就非常巧妙地實現了配合 webpack 2+ 的非同步載入元件的方式(Promise)載入非同步元件。

2.3.3 高階非同步元件

由於非同步載入元件需要動態載入 JS,有一定網路延時,而且有載入失敗的情況,所以通常我們在開發非同步元件相關邏輯的時候需要設計 loading 元件和 error 元件,並在適當的時機渲染它們。Vue.js 2.3+ 支援了一種高階非同步元件的方式,它通過一個簡單的物件配置,幫你搞定 loading 元件和 error 元件的渲染時機。

const AsyncComp = () => ({
  // 需要載入的元件。應當是一個 Promise
  component: import('./MyComp.vue'),
  // 載入中應當渲染的元件
  loading: LoadingComp,
  // 出錯時渲染的元件
  error: ErrorComp,
  // 渲染載入中元件前的等待時間。預設:200ms。
  delay: 200,
  // 最長等待時間。超出此時間則渲染錯誤元件。預設:Infinity
  timeout: 3000
})
Vue.component('async-example', AsyncComp)

高階非同步元件的初始化邏輯和普通非同步元件一樣,也是執行 resolveAsyncComponent,當執行完 res = factory(resolve, reject),返回值就是定義的元件物件,顯然滿足 else if (isDef(res.component) && typeof res.component.then === 'function') 的邏輯,接著執行 res.component.then(resolve, reject),當非同步元件載入成功後,執行 resolve,失敗執行 reject

由於非同步元件載入是一個非同步過程,它接著又同步執行了如下邏輯:

if (isDef(res.error)) {
  factory.errorComp = ensureCtor(res.error, baseCtor)
}

if (isDef(res.loading)) {
  factory.loadingComp = ensureCtor(res.loading, baseCtor)
  if (res.delay === 0) {
    factory.loading = true
  } else {
    setTimeout(() => {
      if (isUndef(factory.resolved) && isUndef(factory.error)) {
        factory.loading = true
        forceRender()
      }
    }, res.delay || 200)
  }
}

if (isDef(res.timeout)) {
  setTimeout(() => {
    if (isUndef(factory.resolved)) {
      reject(
        process.env.NODE_ENV !== 'production'
          ? `timeout (${res.timeout}ms)`
          : null
      )
    }
  }, res.timeout)
}

sync = false
    // return in case resolved synchronously
    return factory.loading
      ? factory.loadingComp
      : factory.resolved

按邏輯的執行順序,對不同的情況做判斷:

1、判斷 res.error 是否定義了 error 元件,如果有的話則賦值給 factory.errorComp

2、判斷 res.loading 是否定義了 loading 元件,如果有的話則賦值給 factory.loadingComp,如果設定了 res.delay 且為 0,則設定 factory.loading = true,否則延時 delay 的時間執行:

if (isUndef(factory.resolved) && isUndef(factory.error)) {
    factory.loading = true
    forceRender()
}

3、判斷 res.timeout,如果配置了該項,則在 res.timout 時間後,如果元件沒有成功載入,執行 reject

4、如果 delay 配置為 0,則這次直接渲染 loading 元件,否則則延時 delay 執行 forceRender,那麼又會再一次執行到 resolveAsyncComponent

2.4 非同步元件載入失敗

當非同步元件載入失敗,會執行 reject 函式: 

const reject = once(reason => {
  process.env.NODE_ENV !== 'production' && warn(
    `Failed to resolve async component: ${String(factory)}` +
    (reason ? `\nReason: ${reason}` : '')
  )
  if (isDef(factory.errorComp)) {
    factory.error = true
    forceRender()
  }
})

這個時候會把 factory.error 設定為 true,同時執行 forceRender() 再次執行到 resolveAsyncComponent

if (isTrue(factory.error) && isDef(factory.errorComp)) {
  return factory.errorComp
}

這個時候就返回 factory.errorComp,直接渲染 error 元件。

2.5 非同步元件載入成功

當非同步元件載入成功,會執行 resolve 函式:

const resolve = once((res: Object | Class<Component>) => {
  factory.resolved = ensureCtor(res, baseCtor)
  if (!sync) {
    forceRender()
  }
})

首先把載入結果快取到 factory.resolved 中,這個時候因為 sync 已經為 false,則執行 forceRender() 再次執行到 resolveAsyncComponent

if (isDef(factory.resolved)) {
  return factory.resolved
}

這個時候直接返回 factory.resolved,渲染成功載入的元件。

2.6 非同步元件載入中

如果非同步元件載入中並未返回,這時候會走到這個邏輯:

if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
  return factory.loadingComp
}

 則會返回 factory.loadingComp,渲染 loading 元件。

2.7 非同步元件載入超時

如果超時,則走到了 reject 邏輯,之後邏輯和載入失敗一樣,渲染 error 元件。 

2.8 非同步元件 patch

回到 createComponent 的邏輯:

Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor === undefined) {
  return createAsyncPlaceholder(
    asyncFactory,
    data,
    context,
    children,
    tag
  )
}

如果是第一次執行 resolveAsyncComponent,除非使用高階非同步元件 0 delay 去建立了一個 loading 元件,否則返回是 undefiend,接著通過 createAsyncPlaceholder 建立一個註釋節點作為佔位符。它的定義在 src/core/vdom/helpers/resolve-async-components.js 中:

export function createAsyncPlaceholder (
  factory: Function,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag: ?string
): VNode {
  const node = createEmptyVNode()
  node.asyncFactory = factory
  node.asyncMeta = { data, context, children, tag }
  return node
}

實際上就是就是建立了一個佔位的註釋 VNode,同時把 asyncFactory 和 asyncMeta 賦值給當前 vnode

當執行 forceRender 的時候,會觸發元件的重新渲染,那麼會再一次執行 resolveAsyncComponent,這時候就會根據不同的情況,可能返回 loading、error 或成功載入的非同步元件,返回值不為 undefined,因此就走正常的元件 renderpatch 過程,與元件第一次渲染流程不一樣,這個時候是存在新舊 vnode 的,下一章我會分析元件更新的 patch 過程。

3 總結

我們對 Vue 的非同步元件的實現有了深入的瞭解,知道了 3 種非同步元件的實現方式,並且看到高階非同步元件的實現是非常巧妙的,它實現了 loading、resolve、reject、timeout的 4 種狀態。非同步元件實現的本質是 2 次渲染,除了 0 delay 的高階非同步元件第一次直接渲染成 loading 元件外,其它都是第一次渲染生成一個註釋節點,當非同步獲取元件成功後,再通過 forceRender 強制重新渲染,這樣就能正確渲染出我們非同步載入的元件了。