簡介
在大型應用里臣咖,有些組件可能一開始并不顯示,只有在特定條件下才會渲染自点,那么這種情況下該組件的資源其實不需要一開始就加載遂赠,完全可以在需要的時候再去請求久妆,這也可以減少頁面首次加載的資源體積,要在Vue
中使用異步組件也很簡單:
// AsyncComponent.vue
<template>
<div>我是異步組件的內容</div>
</template>
<script>
export default {
name: 'AsyncComponent'
}
</script>
// App.vue
<template>
<div id="app">
<AsyncComponent v-if="show"></AsyncComponent>
<button @click="load">加載</button>
</div>
</template>
<script>
export default {
name: 'App',
components: {
AsyncComponent: () => import('./AsyncComponent'),
},
data() {
return {
show: false,
}
},
methods: {
load() {
this.show = true
},
},
}
</script>
我們沒有直接引入AsyncComponent
組件進行注冊跷睦,而是使用import()
方法來動態(tài)的加載筷弦,import()
是ES2015 Loader 規(guī)范 定義的一個方法,webpack
內置支持抑诸,會把AsyncComponent
組件的內容單獨打成一個js
文件烂琴,頁面初始不會加載,點擊加載按鈕后才會去請求蜕乡,該方法會返回一個promise
奸绷,接下來,我們從源碼角度詳細看看這一過程层玲。
通過本文号醉,你可以了解Vue
對于異步組件的處理過程以及webpack
的資源加載過程。
編譯產物
首先我們打個包辛块,生成了三個js
文件:
第一個文件是我們應用的入口文件畔派,里面包含了main.js
、App.vue
的內容润绵,另外還包含了一些webpack
注入的方法线椰,第二個文件就是我們的異步組件AsyncComponent
的內容,第三個文件是其他一些公共庫的內容尘盼,比如Vue
憨愉。
然后我們看看App.vue
編譯后的內容:
上圖為App
組件的選項對象呜魄,可以看到異步組件的注冊方式,是一個函數莱衩。
上圖是App.vue
模板部分編譯后的渲染函數恋捆,當_vm.show
為true
的時候立镶,會執(zhí)行_c('AsyncComponent')
耳幢,否則執(zhí)行_vm._e()
牙寞,創(chuàng)建一個空的VNode
吃型,_c
即createElement
方法:
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
接下來看看當我們點擊按鈕后刑桑,這個方法的執(zhí)行過程棕硫。
createElement方法
function createElement (
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children;
children = data;
data = undefined;
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE;
}
return _createElement(context, tag, data, children, normalizationType)
}
context
為App
組件實例搓萧,tag
就是_c
的參數AsyncComponent
戚啥,其他幾個參數都為undefined
或false
奋单,所以這個方法的兩個if
分支都沒走,直接進入_createElement
方法:
function _createElement (
context,
tag,
data,
children,
normalizationType
) {
// 如果data是被觀察過的數據
if (isDef(data) && isDef((data).__ob__)) {
return createEmptyVNode()
}
// v-bind中的對象語法
if (isDef(data) && isDef(data.is)) {
tag = data.is;
}
// tag不存在猫十,可能是component組件的:is屬性未設置
if (!tag) {
return createEmptyVNode()
}
// 支持單個函數項作為默認作用域插槽
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {};
data.scopedSlots = { default: children[0] };
children.length = 0;
}
// 處理子節(jié)點
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children);
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children);
}
// ...
}
上述邏輯在我們的示例中都不會進入览濒,接著往下看:
function _createElement (
context,
tag,
data,
children,
normalizationType
) {
// ...
var vnode, ns;
// tag是字符串
if (typeof tag === 'string') {
var Ctor;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
if (config.isReservedTag(tag)) {
// 是否是保留元素,比如html元素或svg元素
if (false) {}
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
);
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 組件
vnode = createComponent(Ctor, data, context, children, tag);
} else {
// 其他未知標簽
vnode = new VNode(
tag, data, children,
undefined, undefined, context
);
}
} else {
// tag是組件選項或構造函數
vnode = createComponent(tag, data, context, children);
}
// ...
}
對于我們的異步組件拖云,tag
為AsyncComponent
贷笛,是個字符串,另外通過resolveAsset
方法能找到我們注冊的AsyncComponent
組件:
function resolveAsset (
options,// App組件實例的$options
type,// components
id,
warnMissing
) {
if (typeof id !== 'string') {
return
}
var assets = options[type];
// 首先檢查本地注冊
if (hasOwn(assets, id)) { return assets[id] }
var camelizedId = camelize(id);
if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
var PascalCaseId = capitalize(camelizedId);
if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }
// 本地沒有宙项,則在原型鏈上查找
var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
if (false) {}
return res
}
Vue
會把我們的每個組件都先創(chuàng)建成一個構造函數乏苦,然后再進行實例化,在創(chuàng)建過程中會進行選項合并尤筐,也就是把該組件的選項和父構造函數的選項進行合并:
上圖中汇荐,子選項是App
的組件選項,父選項是Vue
構造函數的選項對象盆繁,對于components
選項掀淘,會以父類的該選項值為原型創(chuàng)建一個對象,然后把子類本身的選項值作為屬性添加到該對象上改基,最后這個對象作為子類構造函數的options.components
的屬性值:
然后在組件實例化時繁疤,會以構造函數的options
對象作為原型創(chuàng)建一個對象,作為實例的$options
:
所以App
實例能通過$options
從它的構造函數的options.components
對象上找到AsyncComponent
組件:
可以發(fā)現就是我們前面看到過的編譯后的函數秕狰。
接下來會執(zhí)行createComponent
方法:
function createComponent (
Ctor,
data,
context,
children,
tag
) {
// ...
// 異步組件
var asyncFactory;
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor;
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
// ...
}
接著又執(zhí)行了resolveAsyncComponent
方法:
function resolveAsyncComponent (
factory,
baseCtor
) {
// ...
var owner = currentRenderingInstance;
if (owner && !isDef(factory.owners)) {
var owners = factory.owners = [owner];
var sync = true;
var timerLoading = null;
var timerTimeout = null
;(owner).$on('hook:destroyed', function () { return remove(owners, owner); });
var forceRender = function(){}
var resolve = once(function(){})
var reject = once(function(){})
// 執(zhí)行異步組件的函數
var res = factory(resolve, reject);
}
// ...
}
到這里終于執(zhí)行了異步組件的函數稠腊,也就是下面這個:
function AsyncComponent() {
return __webpack_require__.e( /*! import() */ "chunk-1f79b58b").then(__webpack_require__.bind(null, /*! ./AsyncComponent */ "c61d"));
}
欲知res
是什么,我們就得看看這幾個webpack
的函數是干什么的鸣哀。
加載組件資源
webpack_require.e方法
先看__webpack_require__.e
方法:
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// 已經加載的chunk
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) { // 0代表已經加載
// 值非0即代表組件正在加載中架忌,installedChunkData[2]為promise對象
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// 創(chuàng)建一個promise,并且把兩個回調參數緩存到installedChunks對象上
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
// 把promise對象本身也添加到緩存數組里
promises.push(installedChunkData[2] = promise);
// 開始發(fā)起chunk請求
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
// 拼接chunk的請求url
script.src = jsonpScriptSrc(chunkId);
var error = new Error();
// chunk加載完成/失敗的回到
onScriptComplete = function (event) {
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if (chunk !== 0) {
// 如果installedChunks對象上該chunkId的值還存在則代表加載出錯了
if (chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
// 設置超時時間
var timeout = setTimeout(function () {
onScriptComplete({
type: 'timeout',
target: script
});
}, 120000);
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
這個方法雖然有點長我衬,但是邏輯很簡單叹放,首先函數返回的是一個promise
饰恕,如果要加載的chunk
未加載過,那么就創(chuàng)建一個promise
井仰,然后緩存到installedChunks
對象上埋嵌,接下來創(chuàng)建script
標簽來加載chunk
,唯一不好理解的是onScriptComplete
函數俱恶,因為在這里面判斷該chunk
在installedChunks
上的緩存信息不為0
則當做失敗處理了雹嗦,問題是前面才把promise
信息緩存過去,也沒有看到哪里有進行修改合是,要理解這個就需要看看我們要加載的chunk
的內容了:
可以看到代碼直接執(zhí)行了了罪,并往webpackJsonp
數組里添加了一項:
window["webpackJsonp"] = window["webpackJsonp"] || []).push([["chunk-1f79b58b"],{..}])
看著似乎也沒啥問題,其實window["webpackJsonp"]
的push
方法被修改過了:
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
var parentJsonpFunction = oldJsonpFunction;
被修改成了webpackJsonpCallback
方法:
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
var moduleId, chunkId, i = 0,
resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
// 把該chunk的promise的resolve回調方法添加到resolves數組里
resolves.push(installedChunks[chunkId][0]);
}
// 標記該chunk已經加載完成
installedChunks[chunkId] = 0;
}
// 將該chunk的module數據添加到modules對象上
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
// 執(zhí)行原本的push方法
if (parentJsonpFunction) parentJsonpFunction(data);
// 執(zhí)行resolve函數
while (resolves.length) {
resolves.shift()();
}
}
這個函數會取出該chunk
加載的promise
的resolve
函數聪全,然后將它在installedChunks
上的信息標記為0
泊藕,代表加載成功,所以在后面執(zhí)行的onScriptComplete
函數就可以通過是否為0
來判斷是否加載失敗难礼。最后會執(zhí)行resolve
函數娃圆,這樣前面__webpack_require__.e
函數返回的promise
狀態(tài)就會變?yōu)槌晒Α?/p>
讓我們再回顧一下AsyncComponent
組件的函數:
function AsyncComponent() {
return __webpack_require__.e( /*! import() */ "chunk-1f79b58b").then(__webpack_require__.bind(null, /*! ./AsyncComponent */ "c61d"));
}
chunk
加載完成后會執(zhí)行__webpack_require__
方法。
__webpack_require__
方法
這個方法是webpack
最重要的方法鹤竭,用來加載模塊:
function __webpack_require__(moduleId) {
// 檢查模塊是否已經加載過了
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 創(chuàng)建一個新模塊踊餐,并緩存
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 執(zhí)行模塊函數
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 標記模塊加載狀態(tài)
module.l = true;
// 返回模塊的導出
return module.exports;
}
所以上面的__webpack_require__.bind(null, /*! ./AsyncComponent */ "c61d")
其實是去加載了c61d
模塊,這個模塊就在我們剛剛請求回來的chunk
里:
這個模塊內部又會去加載它依賴的模塊臀稚,最終返回的結果為:
其實就是AsyncComponent
的組件選項吝岭。
回到createElement方法
回到前面的resolveAsyncComponent
方法:
var res = factory(resolve, reject);
現在我們知道這個res
其實就是一個未完成的promise
,Vue
并沒有等待異步組件加載完成吧寺,而是繼續(xù)向后執(zhí)行:
if (isObject(res)) {
if (isPromise(res)) {
// () => Promise
if (isUndef(factory.resolved)) {
res.then(resolve, reject);
}
}
}
return factory.resolved
把定義的resolve
和reject
函數作為參數傳給promise
res
窜管,最后返回了factory.resolved
,這個屬性并沒有被設置任何值稚机,所以是undefined
幕帆。
接下來回到createComponent
方法:
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
if (Ctor === undefined) {
// 返回異步組件的占位符節(jié)點,該節(jié)點呈現為注釋節(jié)點赖条,但保留該節(jié)點的所有原始信息失乾。
// 這些信息將用于異步服務端渲染。
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
因為Ctor
是undefined
纬乍,所以會執(zhí)行createAsyncPlaceholder
方法返回一個占位符節(jié)點:
function createAsyncPlaceholder (
factory,
data,
context,
children,
tag
) {
// 創(chuàng)建一個空的VNode碱茁,其實就是注釋節(jié)點
var node = createEmptyVNode();
// 保留組件的相關信息
node.asyncFactory = factory;
node.asyncMeta = { data: data, context: context, children: children, tag: tag };
return node
}
最后讓我們再回到_createElement
方法:
// ...
vnode = createComponent(Ctor, data, context, children, tag);
// ...
return vnode
很簡單,對于異步節(jié)點仿贬,直接返回創(chuàng)建的注釋節(jié)點纽竣,最后把虛擬節(jié)點轉換成真實節(jié)點,會實際創(chuàng)建一個注釋節(jié)點:
現在讓我們來看看resolveAsyncComponent
函數里面定義的resolve
,也就是當chunk
加載完成后會執(zhí)行的:
var resolve = once(function (res) {d
// 緩存結果
factory.resolved = ensureCtor(res, baseCtor);
// 非同步解析時調用
// (SSR會把異步解析為同步)
if (!sync) {
forceRender(true);
} else {
owners.length = 0;
}
});
res
即AsyncComponent
的組件選項蜓氨,baseCtor
為Vue
構造函數聋袋,會把它們作為參數調用ensureCtor
方法:
function ensureCtor (comp, base) {
if (
comp.__esModule ||
(hasSymbol && comp[Symbol.toStringTag] === 'Module')
) {
comp = comp.default;
}
return isObject(comp)
? base.extend(comp)
: comp
}
可以看到實際上是調用了extend
方法:
前面也提到過,Vue
會把我們的組件都創(chuàng)建一個對應的構造函數穴吹,就是通過這個方法幽勒,這個方法會以baseCtor
為父類創(chuàng)建一個子類,這里就會創(chuàng)建AsyncComponent
子類:
子類創(chuàng)建成功后會執(zhí)行forceRender
方法:
var forceRender = function (renderCompleted) {
for (var i = 0, l = owners.length; i < l; i++) {
(owners[i]).$forceUpdate();
}
if (renderCompleted) {
owners.length = 0;
if (timerLoading !== null) {
clearTimeout(timerLoading);
timerLoading = null;
}
if (timerTimeout !== null) {
clearTimeout(timerTimeout);
timerTimeout = null;
}
}
};
owners
里包含著App
組件實例港令,所以會調用它的$forceUpdate
方法代嗤,這個方法會迫使 Vue
實例重新渲染,也就是重新執(zhí)行渲染函數缠借,進行虛擬DOM
的diff
和path
更新。
所以會重新執(zhí)行App
組件的渲染函數宜猜,那么又會執(zhí)行前面的createElement
方法泼返,又會走一遍我們前面提到的那些過程,只是此時AsyncComponent
組件已經加載成功并創(chuàng)建了對應的構造函數姨拥,所以對于createComponent
方法绅喉,這次執(zhí)行resolveAsyncComponent
方法的結果不再是undefined
,而是AsyncComponent
組件的構造函數:
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
function resolveAsyncComponent (
factory,
baseCtor
) {
if (isDef(factory.resolved)) {
return factory.resolved
}
}
接下來就會走正常的組件渲染邏輯:
var name = Ctor.options.name || tag;
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
可以看到對于組件其實也是創(chuàng)建了一個VNode
叫乌,具體怎么把該組件的VNode
渲染成真實DOM
不是本文的重點就不介紹了柴罐,大致就是在虛擬DOM
的diff
和patch
過程中如果遇到的VNode
是組件類型,那么會new
一個該組件的實例關聯到VNode
上憨奸,組件實例化和我們new Vue()
沒有什么區(qū)別革屠,都會先進行選項合并、初始化生命周期排宰、初始化事件似芝、數據觀察等操作,然后執(zhí)行該組件的渲染函數板甘,生成該組件的VNode
党瓮,最后進行patch
操作,生成實際的DOM
節(jié)點盐类,子組件的這些操作全部完成后才會再回到父組件的diff
和patch
過程寞奸,因為子組件的DOM
已經創(chuàng)建好了,所以插入即可在跳,更詳細的過程有興趣可自行了解枪萄。
以上就是本文全部內容。