揭开Vue异步组件的神秘面纱

简介: 揭开Vue异步组件的神秘面纱

简介


在大型应用里,有些组件可能一开始并不显示,只有在特定条件下才会渲染,那么这种情况下该组件的资源其实不需要一开始就加载,完全可以在需要的时候再去请求,这也可以减少页面首次加载的资源体积,要在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文件:


image.png

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


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


image.png


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


image.png


上图是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会把我们的每个组件都先创建成一个构造函数,然后再进行实例化,在创建过程中会进行选项合并,也就是把该组件的选项和父构造函数的选项进行合并:


image.png


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


image.png


image.png



image.png


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


image.png

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


image.png


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


接下来会执行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的内容了:


image.png


可以看到代码直接执行了,并往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里:


image.png


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


image.png



其实就是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函数作为参数传给promiseres,最后返回了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


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


image.png


现在让我们来看看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方法:


image.png


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


image.png


子类创建成功后会执行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,而是AsyncComp

onent组件的构造函数:


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
0
0
0
15
分享
相关文章
|
3月前
|
vue使用iconfont图标
vue使用iconfont图标
162 1
Vue Router 核心原理
Vue Router 是 Vue.js 的官方路由管理器,用于实现单页面应用(SPA)的路由功能。其核心原理包括路由配置、监听浏览器事件和组件渲染等。通过定义路径与组件的映射关系,Vue Router 将用户访问的路径与对应的组件关联,支持哈希和历史模式监听 URL 变化,确保页面导航时正确渲染组件。
ry-vue-flowable-xg:震撼来袭!这款基于 Vue 和 Flowable 的企业级工程项目管理项目,你绝不能错过
基于 Vue 和 Flowable 的企业级工程项目管理平台,免费开源且高度定制化。它覆盖投标管理、进度控制、财务核算等全流程需求,提供流程设计、部署、监控和任务管理等功能,适用于企业办公、生产制造、金融服务等多个场景,助力企业提升效率与竞争力。
83 12
Vue中的class和style绑定
在 Vue 中,class 和 style 绑定是基于数据驱动视图的强大功能。通过 class 绑定,可以动态更新元素的 class 属性,支持对象和数组语法,适用于普通元素和组件。style 绑定则允许以对象或数组形式动态设置内联样式,Vue 会根据数据变化自动更新 DOM。
Vue Router 简介
Vue Router 是 Vue.js 官方的路由管理库,用于构建单页面应用(SPA)。它将不同页面映射到对应组件,支持嵌套路由、路由参数和导航守卫等功能,简化复杂前端应用的开发。主要特性包括路由映射、嵌套路由、路由参数、导航守卫和路由懒加载,提升性能和开发效率。安装命令:`npm install vue-router`。
iframe嵌入页面实现免登录思路(以vue为例)
通过上述步骤,可以在Vue.js项目中通过 `iframe`实现不同应用间的免登录功能。利用Token传递和消息传递机制,可以确保安全、高效地在主应用和子应用间共享登录状态。这种方法在实际项目中具有广泛的应用前景,能够显著提升用户体验。
137 8
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
192 64
Vue 组件化开发:构建高质量应用的核心
本文深入探讨了 Vue.js 组件化开发的核心概念与最佳实践。
87 1
基于VUE的校园二手交易平台系统设计与实现毕业设计论文模板
基于Vue的校园二手交易平台是一款专为校园用户设计的在线交易系统,提供简洁高效、安全可靠的二手商品买卖环境。平台利用Vue框架的响应式数据绑定和组件化特性,实现用户友好的界面,方便商品浏览、发布与管理。该系统采用Node.js、MySQL及B/S架构,确保稳定性和多功能模块设计,涵盖管理员和用户功能模块,促进物品循环使用,降低开销,提升环保意识,助力绿色校园文化建设。
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
82 8

热门文章

最新文章