我們知道,元件是
Vue
體系的核心,熟練使用元件是掌握Vue
進行開發的基礎。回顧深入剖析Vue原始碼 - 元件基礎小節,我們深入瞭解了Vue
元件註冊到使用渲染的完整流程。這一節我們會在上一節的基礎上介紹元件的兩個高階用法:非同步元件和函式式元件。
6.1 非同步元件
6.1.1 使用場景
Vue
作為單頁面應用遇到最棘手的問題便是首屏載入時間的問題,最終打包的指令碼檔案會包含所有業務和非業務的程式碼,檔案過大便是造成載入速度過慢的原因。因此作為首屏效能優化的課題,最常用的方法是對檔案的拆分和程式碼的分離。按需載入的概念也是在這個前提下引入的。我們往往會把一些非首屏的元件設計成非同步元件,按需載入。通俗點理解,非同步元件是在需要使用時才去請求載入元件程式碼,我們藉助webpack
會更加直觀。
6.1.2 工廠函式
Vue
中允許使用者通過工廠函式的形式定義元件,這個工廠函式會非同步解析元件定義,元件需要渲染的時候才會觸發該工廠函式,載入結果會進行快取,以供下一次呼叫元件時使用。
具體使用:
// 全域性註冊:
Vue.component('asyncComponent', function(resolve, reject) {
require(['./test.vue'], resolve)
})
// 區域性註冊:
var vm = new Vue({
el: '#app',
template: '<div id="app"><asyncComponent></asyncComponent></div>',
components: {
asyncComponent: (resolve, reject) => require(['./test.vue'], resolve)
}
})
複製程式碼
6.1.2.1 require.ensure
在結合webpack進行非同步元件程式碼分離時,經常需要關注分離檔案的chunkname
,這時可以使用webpack
提供的require.ensure
進行程式碼分離。webpack 在編譯時,會靜態地解析程式碼中的 require.ensure()
,同時將模組新增到一個分開的 chunk
中,其中函式的第三個引數為分離程式碼塊的名字。修改上述的程式碼寫法:
Vue.component('asyncComponent', function (resolve, reject) {
require.ensure([], function () {
resolve(require('./test.vue'));
}, 'asyncComponent'); // asyncComponent為chunkname
})
複製程式碼
6.1.2.2 流程分析
有了上一節元件註冊的基礎,我們來分析非同步元件的實現邏輯。簡單回憶一下,子元件的建立分為vnode節點的建立和vnode到真實節點patch的過程,而子vnode建立的過程發生在根節點掛載時遞迴建立子vnode中,遇到子佔位符時,會呼叫createComponent方法。而非同步元件也放在這一階段處理。
// 建立子元件過程
function createComponent (
Ctor, // 子類構造器
data,
context, // vm例項
children, // 子節點
tag // 子元件佔位符
) {
···
// 針對區域性註冊元件建立子類構造器
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
// 非同步元件分支
var asyncFactory;
if (isUndef(Ctor.cid)) {
// 非同步工廠函式
asyncFactory = Ctor;
// 建立非同步元件函式
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
···
// 建立子元件vnode
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
);
return vnode
}
複製程式碼
工廠函式的用法使得Vue.component(name, options)
的第二個引數不是一個物件,因此不論是全域性註冊還是區域性註冊,都不會執行Vue.extend
生成一個子元件的構造器,所以Ctor.cid
不會存在,程式碼會進入非同步元件的分支。
非同步元件分支的核心是resolveAsyncComponent
,接下來只關心工廠函式處理部分。
function resolveAsyncComponent (
factory,
baseCtor
) {
if (!isDef(factory.owners)) {
// 非同步請求成功處理
var resolve = function() {}
// 非同步請求失敗處理
var reject = function() {}
// 建立子元件時會先執行工廠函式,並將resolve和reject傳入
var res = factory(resolve, reject);
// resolved 同步返回
return factory.loading
? factory.loadingComp
: factory.resolved
}
}
複製程式碼
工廠函式的處理,總結來說就三點:
-
- 定義非同步請求成功的函式處理,定義非同步請求失敗的函式處理;
-
- 執行元件定義的工廠函式;
-
- 同步返回請求成功的函式處理。
先關注一下高階函式once
, 為了防止多個地方同時呼叫非同步元件時,resolve,reject
呼叫多次,once
函式保證了函式在程式碼只執行一次。
// once函式保證了這個呼叫函式只在系統中呼叫一次
function once (fn) {
// 利用閉包特性將called作為標誌位
var called = false;
return function () {
// 呼叫過則不再呼叫
if (!called) {
called = true;
fn.apply(this, arguments);
}
}
}
複製程式碼
成功和失敗的處理邏輯如下:
// 成功處理
var resolve = once(function (res) {
// 轉成元件構造器,並將其快取到resolved屬性中。
factory.resolved = ensureCtor(res, baseCtor);
if (!sync) {
//強制更新渲染檢視
forceRender(true);
} else {
owners.length = 0;
}
});
// 失敗處理
var reject = once(function (reason) {
warn(
"Failed to resolve async component: " + (String(factory)) +
(reason ? ("\nReason: " + reason) : '')
);
if (isDef(factory.errorComp)) {
factory.error = true;
forceRender(true);
}
});
複製程式碼
非同步元件載入完畢,會呼叫resolve
定義的方法,方法會通過ensureCtor
將載入完成的元件轉換為建構函式,並儲存在resolved
屬性中,其中 ensureCtor
的定義為:
function ensureCtor (comp, base) {
if (comp.__esModule ||(hasSymbol && comp[Symbol.toStringTag] === 'Module')) {
comp = comp.default;
}
// comp結果為物件時,呼叫extend方法建立一個子類構造器
return isObject(comp)
? base.extend(comp)
: comp
}
複製程式碼
元件構造器建立完畢,會進行一次檢視的重新渲染,由於Vue是資料驅動檢視渲染的,而元件在載入到完畢的過程中,並沒有資料發生變化,因此需要手動強制更新檢視。forceRender
函式的內部會拿到每個呼叫非同步元件的例項,執行原型上的$forceUpdate
方法,這部分的知識等到響應式系統時介紹。
非同步元件載入失敗後,會呼叫reject
定義的方法,方法會提示並標記錯誤,最後同樣會強制更新檢視。
回到非同步元件建立的流程,執行非同步過程會同步為載入中的非同步元件建立一個註釋節點Vnode
function createComponent (){
···
// 建立非同步元件函式
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
if (Ctor === undefined) {
// 建立註釋節點
return createAsyncPlaceholder(asyncFactory,data,context,children,tag)
}
}
複製程式碼
createAsyncPlaceholder
的定義也很簡單,其中createEmptyVNode
之前有介紹過,是建立一個註釋節點vnode
,而asyncFactory,asyncMeta
都是用來標註該節點為非同步元件的臨時節點和相關屬性。
// 建立註釋Vnode
function createAsyncPlaceholder (factory,data,context,children,tag) {
var node = createEmptyVNode();
node.asyncFactory = factory;
node.asyncMeta = { data: data, context: context, children: children, tag: tag };
return node
}
複製程式碼
執行forceRender
觸發元件的重新渲染過程時,又會再次呼叫resolveAsyncComponent
,這時返回值Ctor
不再為 undefined
了,因此會正常走元件的render,patch
過程。這時,舊的註釋節點也會被取代。
6.1.3 Promise非同步元件
非同步元件的第二種寫法是在工廠函式中返回一個promise
物件,我們知道import
是es6引入模組載入的用法,但是import
是一個靜態載入的方法,它會優先模組內的其他語句執行。因此引入了import()
,import()
是一個執行時載入模組的方法,可以用來類比require()
方法,區別在於前者是一個非同步方法,後者是同步的,且import()
會返回一個promise
物件。
具體用法:
Vue.component('asyncComponent', () => import('./test.vue'))
複製程式碼
原始碼依然走著非同步元件處理分支,並且大部分的處理過程還是工廠函式的邏輯處理,區別在於執行非同步函式後會返回一個promise
物件,成功載入則執行resolve
,失敗載入則執行reject
.
var res = factory(resolve, reject);
// res是返回的promise
if (isObject(res)) {
if (isPromise(res)) {
if (isUndef(factory.resolved)) {
// 核心處理
res.then(resolve, reject);
}
}
}
複製程式碼
其中promise
物件的判斷最簡單的是判斷是否有then
和catch
方法:
// 判斷promise物件的方法
function isPromise (val) {
return (isDef(val) && typeof val.then === 'function' && typeof val.catch === 'function')
}
複製程式碼
6.1.4 高階非同步元件
為了在操作上更加靈活,比如使用loading
元件處理載入元件載入時間過長的等待問題,使用error
元件處理載入元件失敗的錯誤提示等,Vue
在2.3.0+版本新增了返回物件形式的非同步元件格式,物件中可以定義需要載入的元件component
,載入中顯示的元件loading
,載入失敗的元件error
,以及各種延時超時設定,原始碼同樣進入非同步元件分支。
Vue.component('asyncComponent', () => ({
// 需要載入的元件 (應該是一個 `Promise` 物件)
component: import('./MyComponent.vue'),
// 非同步元件載入時使用的元件
loading: LoadingComponent,
// 載入失敗時使用的元件
error: ErrorComponent,
// 展示載入時元件的延時時間。預設值是 200 (毫秒)
delay: 200,
// 如果提供了超時時間且元件載入也超時了,
// 則使用載入失敗時使用的元件。預設值是:`Infinity`
timeout: 3000
}))
複製程式碼
非同步元件函式執行後返回一個物件,並且物件的component
執行會返回一個promise
物件,因此進入高階非同步元件處理分支。
if (isObject(res)) {
if (isPromise(res)) {}
// 返回物件,且res.component返回一個promise物件,進入分支
// 高階非同步元件處理分支
else if (isPromise(res.component)) {
// 和promise非同步元件處理方式相同
res.component.then(resolve, reject);
···
}
}
複製程式碼
非同步元件會等待響應成功失敗的結果,與此同時,程式碼繼續同步執行。高階選項設定中如果設定了error
和loading
元件,會同時建立兩個子類的構造器,
if (isDef(res.error)) {
// 非同步錯誤時元件的處理,建立錯誤元件的子類構造器,並賦值給errorComp
factory.errorComp = ensureCtor(res.error, baseCtor);
}
if (isDef(res.loading)) {
// 非同步載入時元件的處理,建立錯誤元件的子類構造器,並賦值給errorComp
factory.loadingComp = ensureCtor(res.loading, baseCtor);
}
複製程式碼
如果存在delay
屬性,則通過settimeout
設定loading
元件顯示的延遲時間。factory.loading
屬性用來標註是否是顯示loading
元件。
if (res.delay === 0) {
factory.loading = true;
} else {
// 超過時間會成功載入,則執行失敗結果
setTimeout(function () {
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true;
forceRender(false);
}
}, res.delay || 200);
}
複製程式碼
如果在timeout
時間內,非同步元件還未執行resolve
的成功結果,即resolve
沒有賦值,則進行reject
失敗處理。
接下來依然是渲染註釋節點或者渲染loading
元件,等待非同步處理結果,根據處理結果重新渲染檢視節點。相似過程不再闡述。
6.2 函式式元件
Vue提供了一種就可以讓元件變為無狀態、無例項的函式化元件,既然這個元件是函式,那麼它的渲染開銷會低很多。一搬情況下,當我們需要在多個元件中選擇一個來代為渲染,或者在將children,props,data
等資料傳遞給子元件前進行資料處理,我們都可以用函式式元件來完成,其本質上也是對元件的一個外部包裝。
6.2.1 使用場景
-
- 定義兩個元件物件,
test1,test2
- 定義兩個元件物件,
var test1 = {
props: ['msg'],
render: function (createElement, context) {
return createElement('h1', this.msg)
}
}
var test2 = {
props: ['msg'],
render: function (createElement, context) {
return createElement('h2', this.msg)
}
}
複製程式碼
- 2.定義函式式元件
Vue.component('test3', {
// 函式式元件的標誌 functional設定為true
functional: true,
props: ['msg'],
render: function (createElement, context) {
var get = function() {
return test1
}
return createElement(get(), context)
}
})
複製程式碼
- 3.使用函式式元件
<test3 :msg="msg" id="test">
</test3>
new Vue({
el: '#app',
data: {
msg: 'test'
}
})
複製程式碼
最終渲染的結果為:
<h2>test</h2>
複製程式碼
6.2.2 原始碼分析
函式式元件會在元件的物件定義中,將functional
屬性設定為true
,在根例項掛載的過程中,遇到函式式元件的佔位符依然會進入建立子元件createComponent
的流程。由於functional
屬性的存在,程式碼會進入函式式元件的分支中,並返回createFunctionalComponent
呼叫的結果。
function createComponent(){
···
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
}
複製程式碼
createFunctionalComponent
方法會對傳入的資料進行檢測和合並,例項化FunctionalRenderContext
,最終呼叫函式式元件自定義的render
方法執行渲染過程。
function createFunctionalComponent(
Ctor, // 函式式元件構造器
propsData, // 傳入元件的props
data, // 佔位符元件傳入的attr屬性
context, // vue例項
children// 子節點
){
// 資料檢測合併
...
// 例項化
var renderContext = new FunctionalRenderContext(data,props,children,contextVm,Ctor);
// 執行render函式
var vnode = options.render.call(null, renderContext._c, renderContext)
}
複製程式碼
而Function
這個類最終的目的是定義一個和真實元件渲染不同的render
方法。
function FunctionalRenderContext() {
// 省略其他邏輯
this._c = function (a, b, c, d) { return createElement(contextVm, a, b, c, d, needNormalization); };
}
複製程式碼
執行render
函式的過程,又會遞迴呼叫createElement
的方法,這時的元件已經是真實的元件,開始執行正常的元件掛載流程。
從原始碼中可以看出,函式式元件並不會像普通元件那樣有掛載元件週期鉤子,監聽狀態和管理資料的過程,它只會原封不動的接收傳遞給元件的資料做處理。因此作為純粹的函式它只做資料的處理以及渲染元件的選擇,這也大大降低了函式式元件的開銷。
6.3 小結
這一小節介紹了元件兩個進階的用法,非同步元件和函式式元件。它們都是為了解決某些型別的場景引入的高階元件用法。其中非同步元件是首屏效能優化的一個解決方案,並且Vue提供了多達三種的使用方法,高階配置的用法讓非同步元件的使用更加靈活。函式式元件在多元件中選擇渲染元件的過程中效果同樣顯著。