关于javascript:揭开Vue异步组件的神秘面纱

4次阅读

共计 11241 个字符,预计需要花费 29 分钟才能阅读完成。

简介

在大型利用里,有些组件可能一开始并不显示,只有在特定条件下才会渲染,那么这种状况下该组件的资源其实不须要一开始就加载,齐全能够在须要的时候再去申请,这也能够缩小页面首次加载的资源体积,要在 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 曾经创立好了,所以插入即可,更具体的过程有趣味可自行理解。

以上就是本文全部内容。

正文完
 0