简介

在大型利用里,有些组件可能一开始并不显示,只有在特定条件下才会渲染,那么这种状况下该组件的资源其实不须要一开始就加载,齐全能够在须要的时候再去申请,这也能够缩小页面首次加载的资源体积,要在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.jsApp.vue的内容,另外还蕴含了一些webpack注入的办法,第二个文件就是咱们的异步组件AsyncComponent的内容,第三个文件是其余一些公共库的内容,比方Vue

而后咱们看看App.vue编译后的内容:

上图为App组件的选项对象,能够看到异步组件的注册形式,是一个函数。

上图是App.vue模板局部编译后的渲染函数,当_vm.showtrue的时候,会执行_c('AsyncComponent'),否则执行_vm._e(),创立一个空的VNode_ccreateElement办法:

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 = undefined;  }  if (isTrue(alwaysNormalize)) {    normalizationType = ALWAYS_NORMALIZE;  }  return _createElement(context, tag, data, children, normalizationType)}

contextApp组件实例,tag就是_c的参数AsyncComponent,其余几个参数都为undefinedfalse,所以这个办法的两个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;    }    // 解决子节点    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);    }    // ...}

对于咱们的异步组件,tagAsyncComponent,是个字符串,另外通过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 = 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 = 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未加载过,那么就创立一个promise,而后缓存到installedChunks对象上,接下来创立script标签来加载chunk,惟一不好了解的是onScriptComplete函数,因为在这外面判断该chunkinstalledChunks上的缓存信息不为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加载的promiseresolve函数,而后将它在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其实就是一个未实现的promiseVue并没有期待异步组件加载实现,而是持续向后执行:

if (isObject(res)) {    if (isPromise(res)) {        // () => Promise        if (isUndef(factory.resolved)) {            res.then(resolve, reject);        }    }}return factory.resolved

把定义的resolvereject函数作为参数传给promise res,最初返回了factory.resolved,这个属性并没有被设置任何值,所以是undefined

接下来回到createComponent办法:

Ctor = resolveAsyncComponent(asyncFactory, baseCtor);if (Ctor === undefined) {    // 返回异步组件的占位符节点,该节点出现为正文节点,但保留该节点的所有原始信息。    // 这些信息将用于异步服务端渲染。    return createAsyncPlaceholder(        asyncFactory,        data,        context,        children,        tag    )}

因为Ctorundefined,所以会执行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;    }});

resAsyncComponent的组件选项,baseCtorVue构造函数,会把它们作为参数调用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 实例从新渲染,也就是从新执行渲染函数,进行虚构DOMdiffpath更新。

所以会从新执行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不是本文的重点就不介绍了,大抵就是在虚构DOMdiffpatch过程中如果遇到的VNode是组件类型,那么会new一个该组件的实例关联到VNode上,组件实例化和咱们new Vue()没有什么区别,都会先进行选项合并、初始化生命周期、初始化事件、数据察看等操作,而后执行该组件的渲染函数,生成该组件的VNode,最初进行patch操作,生成理论的DOM节点,子组件的这些操作全副实现后才会再回到父组件的diffpatch过程,因为子组件的DOM曾经创立好了,所以插入即可,更具体的过程有趣味可自行理解。

以上就是本文全部内容。