簡介
在大型應用里,有些組件可能一開始并不顯示,只有在特定條件下才會渲染,那么這種情況下該組件的資源其實不需要一開始就加載,完全可以在需要的時候再去請求,這也可以減少頁面首次加載的資源體積,要在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()方法來動態的加載,import()是ES2015 Loader 規范 定義的一個方法,webpack內置支持,會把AsyncComponent組件的內容單獨打成一個js檔案,頁面初始不會加載,點擊加載按鈕后才會去請求,該方法會回傳一個promise,接下來,我們從原始碼角度詳細看看這一程序,
通過本文,你可以了解Vue對于異步組件的處理程序以及webpack的資源加載程序,
編譯產物
首先我們打個包,生成了三個js檔案:

第一個檔案是我們應用的入口檔案,里面包含了main.js、App.vue的內容,另外還包含了一些webpack注入的方法,第二個檔案就是我們的異步組件AsyncComponent的內容,第三個檔案是其他一些公共庫的內容,比如Vue,
然后我們看看App.vue編譯后的內容:

上圖為App組件的選項物件,可以看到異步組件的注冊方式,是一個函式,

上圖是App.vue模板部分編譯后的渲染函式,當_vm.show為true的時候,會執行_c('AsyncComponent'),否則執行_vm._e(),創建一個空的VNode,_c即createElement方法:
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
接下來看看當我們點擊按鈕后,這個方法的執行程序,
createElement方法
function createElement (
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children;
children = data;
data = https://www.cnblogs.com/wanglinmantan/archive/2021/12/29/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 = https://www.cnblogs.com/wanglinmantan/archive/2021/12/29/data || {};
data.scopedSlots = { default: children[0] };
children.length = 0;
}
// 處理子節點
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會把我們的每個組件都先創建成一個建構式,然后再進行實體化,在創建程序中會進行選項合并,也就是把該組件的選項和父建構式的選項進行合并:

上圖中,子選項是App的組件選項,父選項是Vue建構式的選項物件,對于components選項,會以父類的該選項值為原型創建一個物件,然后把子類本身的選項值作為屬性添加到該物件上,最后這個物件作為子類建構式的options.components的屬性值:



然后在組件實體化時,會以建構式的options物件作為原型創建一個物件,作為實體的$options:

所以App實體能通過$options從它的建構式的options.components物件上找到AsyncComponent組件:

可以發現就是我們前面看到過的編譯后的函式,
接下來會執行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
)
}
}
// ...
}
接著又執行了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(){})
// 執行異步組件的函式
var res = factory(resolve, reject);
}
// ...
}
到這里終于執行了異步組件的函式,也就是下面這個:
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 = https://www.cnblogs.com/wanglinmantan/archive/2021/12/29/installedChunks[chunkId];
if (installedChunkData !== 0) { // 0代表已經加載
// 值非0即代表組件正在加載中,installedChunkData[2]為promise物件
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// 創建一個promise,并且把兩個回呼引數快取到installedChunks物件上
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
// 把promise物件本身也添加到快取陣列里
promises.push(installedChunkData[2] = promise);
// 開始發起chunk請求
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
// 拼接chunk的請求url
script.src = https://www.cnblogs.com/wanglinmantan/archive/2021/12/29/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 = https://www.cnblogs.com/wanglinmantan/archive/2021/12/29/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未加載過,那么就創建一個promise,然后快取到installedChunks物件上,接下來創建script標簽來加載chunk,唯一不好理解的是onScriptComplete函式,因為在這里面判斷該chunk在installedChunks上的快取資訊不為0則當做失敗處理了,問題是前面才把promise資訊快取過去,也沒有看到哪里有進行修改,要理解這個就需要看看我們要加載的chunk的內容了:

可以看到代碼直接執行了,并往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];
}
}
// 執行原本的push方法
if (parentJsonpFunction) parentJsonpFunction(data);
// 執行resolve函式
while (resolves.length) {
resolves.shift()();
}
}
這個函式會取出該chunk加載的promise的resolve函式,然后將它在installedChunks上的資訊標記為0,代表加載成功,所以在后面執行的onScriptComplete函式就可以通過是否為0來判斷是否加載失敗,最后會執行resolve函式,這樣前面__webpack_require__.e函式回傳的promise狀態就會變為成功,
讓我們再回顧一下AsyncComponent組件的函式:
function AsyncComponent() {
return __webpack_require__.e( /*! import() */ "chunk-1f79b58b").then(__webpack_require__.bind(null, /*! ./AsyncComponent */ "c61d"));
}
chunk加載完成后會執行__webpack_require__方法,
__webpack_require__方法
這個方法是webpack最重要的方法,用來加載模塊:
function __webpack_require__(moduleId) {
// 檢查模塊是否已經加載過了
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 創建一個新模塊,并快取
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 執行模塊函式
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 標記模塊加載狀態
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并沒有等待異步組件加載完成,而是繼續向后執行:
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) {
// 回傳異步組件的占位符節點,該節點呈現為注釋節點,但保留該節點的所有原始資訊,
// 這些資訊將用于異步服務端渲染,
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
因為Ctor是undefined,所以會執行createAsyncPlaceholder方法回傳一個占位符節點:
function createAsyncPlaceholder (
factory,
data,
context,
children,
tag
) {
// 創建一個空的VNode,其實就是注釋節點
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
很簡單,對于異步節點,直接回傳創建的注釋節點,最后把虛擬節點轉換成真實節點,會實際創建一個注釋節點:

現在讓我們來看看resolveAsyncComponent函式里面定義的resolve,也就是當chunk加載完成后會執行的:
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會把我們的組件都創建一個對應的建構式,就是通過這個方法,這個方法會以baseCtor為父類創建一個子類,這里就會創建AsyncComponent子類:

子類創建成功后會執行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 實體重新渲染,也就是重新執行渲染函式,進行虛擬DOM的diff和path更新,
所以會重新執行App組件的渲染函式,那么又會執行前面的createElement方法,又會走一遍我們前面提到的那些程序,只是此時AsyncComponent組件已經加載成功并創建了對應的建構式,所以對于createComponent方法,這次執行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
可以看到對于組件其實也是創建了一個VNode,具體怎么把該組件的VNode渲染成真實DOM不是本文的重點就不介紹了,大致就是在虛擬DOM的diff和patch程序中如果遇到的VNode是組件型別,那么會new一個該組件的實體關聯到VNode上,組件實體化和我們new Vue()沒有什么區別,都會先進行選項合并、初始化生命周期、初始化事件、資料觀察等操作,然后執行該組件的渲染函式,生成該組件的VNode,最后進行patch操作,生成實際的DOM節點,子組件的這些操作全部完成后才會再回到父組件的diff和patch程序,因為子組件的DOM已經創建好了,所以插入即可,更詳細的程序有興趣可自行了解,
以上就是本文全部內容,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/396395.html
標籤:其他
上一篇:揭開Vue異步組件的神秘面紗
