经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » JavaScript » 查看文章
揭开Vue异步组件的神秘面纱
来源:cnblogs  作者:街角小林  时间:2021/12/31 8:58:03  对本文有异议

简介

在大型应用里,有些组件可能一开始并不显示,只有在特定条件下才会渲染,那么这种情况下该组件的资源其实不需要一开始就加载,完全可以在需要的时候再去请求,这也可以减少页面首次加载的资源体积,要在Vue中使用异步组件也很简单:

  1. // AsyncComponent.vue
  2. <template>
  3. <div>我是异步组件的内容</div>
  4. </template>
  5. <script>
  6. export default {
  7. name: 'AsyncComponent'
  8. }
  9. </script>
  1. // App.vue
  2. <template>
  3. <div id="app">
  4. <AsyncComponent v-if="show"></AsyncComponent>
  5. <button @click="load">加载</button>
  6. </div>
  7. </template>
  8. <script>
  9. export default {
  10. name: 'App',
  11. components: {
  12. AsyncComponent: () => import('./AsyncComponent'),
  13. },
  14. data() {
  15. return {
  16. show: false,
  17. }
  18. },
  19. methods: {
  20. load() {
  21. this.show = true
  22. },
  23. },
  24. }
  25. </script>

我们没有直接引入AsyncComponent组件进行注册,而是使用import()方法来动态的加载,import()ES2015 Loader 规范 定义的一个方法,webpack内置支持,会把AsyncComponent组件的内容单独打成一个js文件,页面初始不会加载,点击加载按钮后才会去请求,该方法会返回一个promise,接下来,我们从源码角度详细看看这一过程。

通过本文,你可以了解Vue对于异步组件的处理过程以及webpack的资源加载过程。

编译产物

首先我们打个包,生成了三个js文件:

image-20211214194854431.png

第一个文件是我们应用的入口文件,里面包含了main.jsApp.vue的内容,另外还包含了一些webpack注入的方法,第二个文件就是我们的异步组件AsyncComponent的内容,第三个文件是其他一些公共库的内容,比如Vue

然后我们看看App.vue编译后的内容:

image-20211224161447196.png

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

image-20211224161252075.png

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

  1. vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };

接下来看看当我们点击按钮后,这个方法的执行过程。

createElement方法

  1. function createElement (
  2. context,
  3. tag,
  4. data,
  5. children,
  6. normalizationType,
  7. alwaysNormalize
  8. ) {
  9. if (Array.isArray(data) || isPrimitive(data)) {
  10. normalizationType = children;
  11. children = data;
  12. data = undefined;
  13. }
  14. if (isTrue(alwaysNormalize)) {
  15. normalizationType = ALWAYS_NORMALIZE;
  16. }
  17. return _createElement(context, tag, data, children, normalizationType)
  18. }

contextApp组件实例,tag就是_c的参数AsyncComponent,其他几个参数都为undefinedfalse,所以这个方法的两个if分支都没走,直接进入_createElement方法:

  1. function _createElement (
  2. context,
  3. tag,
  4. data,
  5. children,
  6. normalizationType
  7. ) {
  8. // 如果data是被观察过的数据
  9. if (isDef(data) && isDef((data).__ob__)) {
  10. return createEmptyVNode()
  11. }
  12. // v-bind中的对象语法
  13. if (isDef(data) && isDef(data.is)) {
  14. tag = data.is;
  15. }
  16. // tag不存在,可能是component组件的:is属性未设置
  17. if (!tag) {
  18. return createEmptyVNode()
  19. }
  20. // 支持单个函数项作为默认作用域插槽
  21. if (Array.isArray(children) &&
  22. typeof children[0] === 'function'
  23. ) {
  24. data = data || {};
  25. data.scopedSlots = { default: children[0] };
  26. children.length = 0;
  27. }
  28. // 处理子节点
  29. if (normalizationType === ALWAYS_NORMALIZE) {
  30. children = normalizeChildren(children);
  31. } else if (normalizationType === SIMPLE_NORMALIZE) {
  32. children = simpleNormalizeChildren(children);
  33. }
  34. // ...
  35. }

上述逻辑在我们的示例中都不会进入,接着往下看:

  1. function _createElement (
  2. context,
  3. tag,
  4. data,
  5. children,
  6. normalizationType
  7. ) {
  8. // ...
  9. var vnode, ns;
  10. // tag是字符串
  11. if (typeof tag === 'string') {
  12. var Ctor;
  13. ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
  14. if (config.isReservedTag(tag)) {
  15. // 是否是保留元素,比如html元素或svg元素
  16. if (false) {}
  17. vnode = new VNode(
  18. config.parsePlatformTagName(tag), data, children,
  19. undefined, undefined, context
  20. );
  21. } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
  22. // 组件
  23. vnode = createComponent(Ctor, data, context, children, tag);
  24. } else {
  25. // 其他未知标签
  26. vnode = new VNode(
  27. tag, data, children,
  28. undefined, undefined, context
  29. );
  30. }
  31. } else {
  32. // tag是组件选项或构造函数
  33. vnode = createComponent(tag, data, context, children);
  34. }
  35. // ...
  36. }

对于我们的异步组件,tagAsyncComponent,是个字符串,另外通过resolveAsset方法能找到我们注册的AsyncComponent组件:

  1. function resolveAsset (
  2. options,// App组件实例的$options
  3. type,// components
  4. id,
  5. warnMissing
  6. ) {
  7. if (typeof id !== 'string') {
  8. return
  9. }
  10. var assets = options[type];
  11. // 首先检查本地注册
  12. if (hasOwn(assets, id)) { return assets[id] }
  13. var camelizedId = camelize(id);
  14. if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
  15. var PascalCaseId = capitalize(camelizedId);
  16. if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }
  17. // 本地没有,则在原型链上查找
  18. var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
  19. if (false) {}
  20. return res
  21. }

Vue会把我们的每个组件都先创建成一个构造函数,然后再进行实例化,在创建过程中会进行选项合并,也就是把该组件的选项和父构造函数的选项进行合并:

image-20211227112643613.png

上图中,子选项是App的组件选项,父选项是Vue构造函数的选项对象,对于components选项,会以父类的该选项值为原型创建一个对象,然后把子类本身的选项值作为属性添加到该对象上,最后这个对象作为子类构造函数的options.components的属性值:

image-20211227113823227.png

image-20211227113909991.png

image-20211227113657329.png

然后在组件实例化时,会以构造函数的options对象作为原型创建一个对象,作为实例的$options

image-20211227135444816.png

所以App实例能通过$options从它的构造函数的options.components对象上找到AsyncComponent组件:

image-20211227140124998.png

可以发现就是我们前面看到过的编译后的函数。

接下来会执行createComponent方法:

  1. function createComponent (
  2. Ctor,
  3. data,
  4. context,
  5. children,
  6. tag
  7. ) {
  8. // ...
  9. // 异步组件
  10. var asyncFactory;
  11. if (isUndef(Ctor.cid)) {
  12. asyncFactory = Ctor;
  13. Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
  14. if (Ctor === undefined) {
  15. return createAsyncPlaceholder(
  16. asyncFactory,
  17. data,
  18. context,
  19. children,
  20. tag
  21. )
  22. }
  23. }
  24. // ...
  25. }

接着又执行了resolveAsyncComponent方法:

  1. function resolveAsyncComponent (
  2. factory,
  3. baseCtor
  4. ) {
  5. // ...
  6. var owner = currentRenderingInstance;
  7. if (owner && !isDef(factory.owners)) {
  8. var owners = factory.owners = [owner];
  9. var sync = true;
  10. var timerLoading = null;
  11. var timerTimeout = null
  12. ;(owner).$on('hook:destroyed', function () { return remove(owners, owner); });
  13. var forceRender = function(){}
  14. var resolve = once(function(){})
  15. var reject = once(function(){})
  16. // 执行异步组件的函数
  17. var res = factory(resolve, reject);
  18. }
  19. // ...
  20. }

到这里终于执行了异步组件的函数,也就是下面这个:

  1. function AsyncComponent() {
  2. return __webpack_require__.e( /*! import() */ "chunk-1f79b58b").then(__webpack_require__.bind(null, /*! ./AsyncComponent */ "c61d"));
  3. }

欲知res是什么,我们就得看看这几个webpack的函数是干什么的。

加载组件资源

webpack_require.e方法

先看__webpack_require__.e方法:

  1. __webpack_require__.e = function requireEnsure(chunkId) {
  2. var promises = [];
  3. // 已经加载的chunk
  4. var installedChunkData = installedChunks[chunkId];
  5. if (installedChunkData !== 0) { // 0代表已经加载
  6. // 值非0即代表组件正在加载中,installedChunkData[2]为promise对象
  7. if (installedChunkData) {
  8. promises.push(installedChunkData[2]);
  9. } else {
  10. // 创建一个promise,并且把两个回调参数缓存到installedChunks对象上
  11. var promise = new Promise(function (resolve, reject) {
  12. installedChunkData = installedChunks[chunkId] = [resolve, reject];
  13. });
  14. // 把promise对象本身也添加到缓存数组里
  15. promises.push(installedChunkData[2] = promise);
  16. // 开始发起chunk请求
  17. var script = document.createElement('script');
  18. var onScriptComplete;
  19. script.charset = 'utf-8';
  20. script.timeout = 120;
  21. // 拼接chunk的请求url
  22. script.src = jsonpScriptSrc(chunkId);
  23. var error = new Error();
  24. // chunk加载完成/失败的回到
  25. onScriptComplete = function (event) {
  26. script.onerror = script.onload = null;
  27. clearTimeout(timeout);
  28. var chunk = installedChunks[chunkId];
  29. if (chunk !== 0) {
  30. // 如果installedChunks对象上该chunkId的值还存在则代表加载出错了
  31. if (chunk) {
  32. var errorType = event && (event.type === 'load' ? 'missing' : event.type);
  33. var realSrc = event && event.target && event.target.src;
  34. error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
  35. error.name = 'ChunkLoadError';
  36. error.type = errorType;
  37. error.request = realSrc;
  38. chunk[1](error);
  39. }
  40. installedChunks[chunkId] = undefined;
  41. }
  42. };
  43. // 设置超时时间
  44. var timeout = setTimeout(function () {
  45. onScriptComplete({
  46. type: 'timeout',
  47. target: script
  48. });
  49. }, 120000);
  50. script.onerror = script.onload = onScriptComplete;
  51. document.head.appendChild(script);
  52. }
  53. }
  54. return Promise.all(promises);
  55. };

这个方法虽然有点长,但是逻辑很简单,首先函数返回的是一个promise,如果要加载的chunk未加载过,那么就创建一个promise,然后缓存到installedChunks对象上,接下来创建script标签来加载chunk,唯一不好理解的是onScriptComplete函数,因为在这里面判断该chunkinstalledChunks上的缓存信息不为0则当做失败处理了,问题是前面才把promise信息缓存过去,也没有看到哪里有进行修改,要理解这个就需要看看我们要加载的chunk的内容了:

image-20211227153327294.png

可以看到代码直接执行了,并往webpackJsonp数组里添加了一项:

  1. window["webpackJsonp"] = window["webpackJsonp"] || []).push([["chunk-1f79b58b"],{..}])

看着似乎也没啥问题,其实window["webpackJsonp"]push方法被修改过了:

  1. var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
  2. var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
  3. jsonpArray.push = webpackJsonpCallback;
  4. var parentJsonpFunction = oldJsonpFunction;

被修改成了webpackJsonpCallback方法:

  1. function webpackJsonpCallback(data) {
  2. var chunkIds = data[0];
  3. var moreModules = data[1];
  4. var moduleId, chunkId, i = 0,
  5. resolves = [];
  6. for (; i < chunkIds.length; i++) {
  7. chunkId = chunkIds[i];
  8. if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
  9. // 把该chunk的promise的resolve回调方法添加到resolves数组里
  10. resolves.push(installedChunks[chunkId][0]);
  11. }
  12. // 标记该chunk已经加载完成
  13. installedChunks[chunkId] = 0;
  14. }
  15. // 将该chunk的module数据添加到modules对象上
  16. for (moduleId in moreModules) {
  17. if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
  18. modules[moduleId] = moreModules[moduleId];
  19. }
  20. }
  21. // 执行原本的push方法
  22. if (parentJsonpFunction) parentJsonpFunction(data);
  23. // 执行resolve函数
  24. while (resolves.length) {
  25. resolves.shift()();
  26. }
  27. }

这个函数会取出该chunk加载的promiseresolve函数,然后将它在installedChunks上的信息标记为0,代表加载成功,所以在后面执行的onScriptComplete函数就可以通过是否为0来判断是否加载失败。最后会执行resolve函数,这样前面__webpack_require__.e函数返回的promise状态就会变为成功。

让我们再回顾一下AsyncComponent组件的函数:

  1. function AsyncComponent() {
  2. return __webpack_require__.e( /*! import() */ "chunk-1f79b58b").then(__webpack_require__.bind(null, /*! ./AsyncComponent */ "c61d"));
  3. }

chunk加载完成后会执行__webpack_require__方法。

__webpack_require__方法

这个方法是webpack最重要的方法,用来加载模块:

  1. function __webpack_require__(moduleId) {
  2. // 检查模块是否已经加载过了
  3. if (installedModules[moduleId]) {
  4. return installedModules[moduleId].exports;
  5. }
  6. // 创建一个新模块,并缓存
  7. var module = installedModules[moduleId] = {
  8. i: moduleId,
  9. l: false,
  10. exports: {}
  11. };
  12. // 执行模块函数
  13. modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  14. // 标记模块加载状态
  15. module.l = true;
  16. // 返回模块的导出
  17. return module.exports;
  18. }

所以上面的__webpack_require__.bind(null, /*! ./AsyncComponent */ "c61d")其实是去加载了c61d模块,这个模块就在我们刚刚请求回来的chunk里:

image-20211227161841023.png

这个模块内部又会去加载它依赖的模块,最终返回的结果为:

image-20211227162447114.png

其实就是AsyncComponent的组件选项。

回到createElement方法

回到前面的resolveAsyncComponent方法:

  1. var res = factory(resolve, reject);

现在我们知道这个res其实就是一个未完成的promiseVue并没有等待异步组件加载完成,而是继续向后执行:

  1. if (isObject(res)) {
  2. if (isPromise(res)) {
  3. // () => Promise
  4. if (isUndef(factory.resolved)) {
  5. res.then(resolve, reject);
  6. }
  7. }
  8. }
  9. return factory.resolved

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

接下来回到createComponent方法:

  1. Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
  2. if (Ctor === undefined) {
  3. // 返回异步组件的占位符节点,该节点呈现为注释节点,但保留该节点的所有原始信息。
  4. // 这些信息将用于异步服务端渲染。
  5. return createAsyncPlaceholder(
  6. asyncFactory,
  7. data,
  8. context,
  9. children,
  10. tag
  11. )
  12. }

因为Ctorundefined,所以会执行createAsyncPlaceholder方法返回一个占位符节点:

  1. function createAsyncPlaceholder (
  2. factory,
  3. data,
  4. context,
  5. children,
  6. tag
  7. ) {
  8. // 创建一个空的VNode,其实就是注释节点
  9. var node = createEmptyVNode();
  10. // 保留组件的相关信息
  11. node.asyncFactory = factory;
  12. node.asyncMeta = { data: data, context: context, children: children, tag: tag };
  13. return node
  14. }

最后让我们再回到_createElement方法:

  1. // ...
  2. vnode = createComponent(Ctor, data, context, children, tag);
  3. // ...
  4. return vnode

很简单,对于异步节点,直接返回创建的注释节点,最后把虚拟节点转换成真实节点,会实际创建一个注释节点:

image-20211227181319356.png

现在让我们来看看resolveAsyncComponent函数里面定义的resolve,也就是当chunk加载完成后会执行的:

  1. var resolve = once(function (res) {d
  2. // 缓存结果
  3. factory.resolved = ensureCtor(res, baseCtor);
  4. // 非同步解析时调用
  5. // (SSR会把异步解析为同步)
  6. if (!sync) {
  7. forceRender(true);
  8. } else {
  9. owners.length = 0;
  10. }
  11. });

resAsyncComponent的组件选项,baseCtorVue构造函数,会把它们作为参数调用ensureCtor方法:

  1. function ensureCtor (comp, base) {
  2. if (
  3. comp.__esModule ||
  4. (hasSymbol && comp[Symbol.toStringTag] === 'Module')
  5. ) {
  6. comp = comp.default;
  7. }
  8. return isObject(comp)
  9. ? base.extend(comp)
  10. : comp
  11. }

可以看到实际上是调用了extend方法:

image-20211227182323558.png

前面也提到过,Vue会把我们的组件都创建一个对应的构造函数,就是通过这个方法,这个方法会以baseCtor为父类创建一个子类,这里就会创建AsyncComponent子类:

image-20211227182849384.png

子类创建成功后会执行forceRender方法:

  1. var forceRender = function (renderCompleted) {
  2. for (var i = 0, l = owners.length; i < l; i++) {
  3. (owners[i]).$forceUpdate();
  4. }
  5. if (renderCompleted) {
  6. owners.length = 0;
  7. if (timerLoading !== null) {
  8. clearTimeout(timerLoading);
  9. timerLoading = null;
  10. }
  11. if (timerTimeout !== null) {
  12. clearTimeout(timerTimeout);
  13. timerTimeout = null;
  14. }
  15. }
  16. };

owners里包含着App组件实例,所以会调用它的$forceUpdate方法,这个方法会迫使 Vue 实例重新渲染,也就是重新执行渲染函数,进行虚拟DOMdiffpath更新。

所以会重新执行App组件的渲染函数,那么又会执行前面的createElement方法,又会走一遍我们前面提到的那些过程,只是此时AsyncComponent组件已经加载成功并创建了对应的构造函数,所以对于createComponent方法,这次执行resolveAsyncComponent方法的结果不再是undefined,而是AsyncComponent组件的构造函数:

  1. Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
  2. function resolveAsyncComponent (
  3. factory,
  4. baseCtor
  5. ) {
  6. if (isDef(factory.resolved)) {
  7. return factory.resolved
  8. }
  9. }

接下来就会走正常的组件渲染逻辑:

  1. var name = Ctor.options.name || tag;
  2. var vnode = new VNode(
  3. ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
  4. data, undefined, undefined, undefined, context,
  5. { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
  6. asyncFactory
  7. );
  8. return vnode

可以看到对于组件其实也是创建了一个VNode,具体怎么把该组件的VNode渲染成真实DOM不是本文的重点就不介绍了,大致就是在虚拟DOMdiffpatch过程中如果遇到的VNode是组件类型,那么会new一个该组件的实例关联到VNode上,组件实例化和我们new Vue()没有什么区别,都会先进行选项合并、初始化生命周期、初始化事件、数据观察等操作,然后执行该组件的渲染函数,生成该组件的VNode,最后进行patch操作,生成实际的DOM节点,子组件的这些操作全部完成后才会再回到父组件的diffpatch过程,因为子组件的DOM已经创建好了,所以插入即可,更详细的过程有兴趣可自行了解。

以上就是本文全部内容。

原文链接:http://www.cnblogs.com/wanglinmantan/p/15743476.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号