异步组件

概述

        在我们平时的开发工作中,为了减少首屏代码体积,往往会把一些非首屏的组件设计成异步组件,按需加载。Vue 也原生支持了异步组件的能力,如下:

异步组件

        示例中可以看到,Vue 注册的组件不再是一个对象,而是一个工厂函数,函数有两个参数 resolve 和 reject,函数内部用 setTimout 模拟了异步,实际使用可能是通过动态请求异步组件的 JS 地址,最终通过执行 resolve 方法,它的参数就是我们的异步组件对象。

在了解了异步组件如何注册后,我们从源码的角度来分析一下它的实现。

        上一节我们分析了组件的注册逻辑,由于组件的定义并不是一个普通对象,所以不会执行 Vue.extend 的逻辑把它变成一个组件的构造函数,但是它仍然可以执行到 createComponent 函数,我们再来对这个函数做回顾,它的定义在 src/core/vdom/create-component/js 中:

create-component中异步组件

        我们省略了不必要的逻辑,只保留关键逻辑,由于我们这个时候传入的 Ctor 是一个函数,那么它也并不会执行 Vue.extend 逻辑,因此它的 cid 是 undefiend,进入了异步组件创建的逻辑。这里首先执行了 Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context) 方法,它的定义在 src/core/vdom/helpers/resolve-async-component.js 中:

        resolveAsyncComponent 函数的逻辑略复杂,因为它实际上处理了 3 种异步组件的创建方式,除了刚才示例的组件注册方式,还支持 2 种,一种是支持 Promise 创建组件的方式,如下:

Promise 创建组件的方式

另一种是高级异步组件,如下:

高级异步组件

那么接下来,我们就根据这 3 种异步组件的情况,来分别去分析 resolveAsyncComponent 的逻辑。

普通函数异步组件

resolveAsyncComponent中普通函数相关

        针对普通函数的情况,前面几个 if 判断可以忽略,它们是为高级组件所用,对于 factory.contexts 的判断,是考虑到多个地方同时初始化一个异步组件,那么它的实际加载应该只有一次。接着进入实际加载逻辑,定义了 forceRenderresolve 和 reject 函数,注意 resolve 和 reject 函数用 once 函数做了一层包装,它的定义在 src/shared/util.js 中:

闭包使函数只执行一次

        once 逻辑非常简单,传入一个函数,并返回一个新函数,它非常巧妙地利用闭包和一个标志位保证了它包装的函数只会执行一次,也就是确保 resolve 和 reject 函数只执行一次。

        接下来执行 const res = factory(resolve, reject) 逻辑,这块儿就是执行我们组件的工厂函数,同时把 resolve 和 reject 函数作为参数传入,组件的工厂函数通常会先发送请求去加载我们的异步组件的 JS 文件,拿到组件定义的对象 res 后,执行 resolve(res) 逻辑,它会先执行 factory.resolved = ensureCtor(res, baseCtor)

ensureCtor

        这个函数目的是为了保证能找到异步组件 JS 定义的组件对象(在ES6和CommonJS规范下都能取到export出来的对象),并且如果它是一个普通对象,则调用 Vue.extend 把它转换成一个组件的构造函数。

        resolve 逻辑最后判断了 sync,显然我们这个场景下 sync 为 false,那么就会执行 forceRender 函数,它会遍历 factory.contexts,拿到每一个调用异步组件的实例 vm, 执行 vm.$forceUpdate() 方法,它的定义在 src/core/instance/lifecycle.js 中:

forceRender中的forceRender

        $forceUpdate 的逻辑非常简单,就是调用渲染 watcher 的 update 方法,让渲染 watcher 对应的回调函数执行,也就是触发了组件的重新渲染。之所以这么做是因为 Vue 通常是数据驱动视图重新渲染,但是在整个异步组件加载过程中是没有数据发生变化的,所以通过执行 $forceUpdate 可以强制组件重新渲染一次

Promise 异步组件

Promise 异步组件

        webpack 2+ 支持了异步加载的语法糖:() => import('./my-async-component'),当执行完 res = factory(resolve, reject),返回的值就是 import('./my-async-component') 的返回值,它是一个 Promise 对象。接着进入 if 条件,又判断了 typeof res.then === 'function'),条件满足,执行:

如果按普通函数没取到,用promise取法去取

        当组件异步加载成功后,执行 resolve,加载失败则执行 reject,这样就非常巧妙地实现了配合 webpack 2+ 的异步加载组件的方式(Promise)加载异步组件。

高级异步组件

        由于异步加载组件需要动态加载 JS有一定网络延时,而且有加载失败的情况,所以通常我们在开发异步组件相关逻辑的时候需要设计 loading 组件和 error 组件,并在适当的时机渲染它们。Vue.js 2.3+ 支持了一种高级异步组件的方式,它通过一个简单的对象配置,帮你搞定 loading 组件和 error 组件的渲染时机,你完全不用关心细节,非常方便。接下来我们就从源码的角度来分析高级异步组件是怎么实现的

高级异步组件怎么使用

        高级异步组件的初始化逻辑和普通异步组件一样,也是执行 resolveAsyncComponent,当执行完 res = factory(resolve, reject),返回值就是定义的组件对象,显然满足 else if (isDef(res.component) && typeof res.component.then === 'function') 的逻辑,接着执行 res.component.then(resolve, reject),当异步组件加载成功后,执行 resolve,失败执行 reject。

因为异步组件加载是一个异步过程,它接着又同步执行了如下逻辑:

高级异步组件另外执行

        先判断 res.error 是否定义了 error 组件,如果有的话则赋值给 factory.errorComp。

        接着判断 res.loading 是否定义了 loading 组件,如果有的话则赋值给 factory.loadingComp,如果设置了 res.delay 且为 0,则设置 factory.loading = true,否则延时 delay 的时间执行:

delay

        最后判断 res.timeout,如果配置了该项,则在 res.timout 时间后,如果组件没有成功加载,执行 reject。

        在 resolveAsyncComponent 的最后有一段逻辑:

最后

        如果 delay 配置为 0,则这次直接渲染 loading 组件,否则则延时 delay 执行 forceRender,那么又会再一次执行到 resolveAsyncComponent。

        那么这时候我们有几种情况,按逻辑的执行顺序,对不同的情况做判断。

异步组件加载失败

当异步组件加载失败,会执行 reject 函数:

reject

        这个时候会把 factory.error 设置为 true,同时执行 forceRender() 再次执行到 resolveAsyncComponent:

那么这个时候就返回 factory.errorComp,直接渲染 error 组件。

异步组件加载成功

当异步组件加载成功,会执行 resolve 函数:

首先把加载结果缓存到 factory.resolved 中,这个时候因为 sync 已经为 false

则执行 forceRender() 再次执行到 resolveAsyncComponent:

那么这个时候直接返回 factory.resolved,渲染成功加载的组件。

异步组件加载中

如果异步组件加载中并未返回,这时候会走到这个逻辑:

那么则会返回 factory.loadingComp,渲染 loading 组件。

异步组件加载超时

如果超时,则走到了 reject 逻辑,之后逻辑和加载失败一样,渲染 error 组件。

异步组件 patch

回到 createComponent 的逻辑:

如果是第一次执行 resolveAsyncComponent,除非使用高级异步组件 0 delay 去创建了一个 loading 组件,否则返回是 undefiend

        接着通过 createAsyncPlaceholder 创建一个注释节点作为占位符。它的定义在 src/core/vdom/helpers/resolve-async-components.js 中:

实际上就是就是创建了一个占位的注释 VNode,同时把 asyncFactory 和 asyncMeta 赋值给当前 vnode。

        当执行 forceRender 的时候,会触发组件的重新渲染,那么会再一次执行 resolveAsyncComponent,这时候就会根据不同的情况,可能返回 loading、error 或成功加载的异步组件,返回值不为 undefined,因此就走正常的组件 render、patch 过程,与组件第一次渲染流程不一样,这个时候是存在新旧 vnode 的,下一章我会分析组件更新的 patch 过程。

总结

        通过以上代码分析,我们对 Vue 的异步组件的实现有了深入的了解,知道了 3 种异步组件的实现方式,并且看到高级异步组件的实现是非常巧妙的,它实现了 loading、resolve、reject、timeout 4 种状态。异步组件实现的本质是 2 次渲染,除了 0 delay 的高级异步组件第一次直接渲染成 loading 组件外,其它都是第一次渲染生成一个注释节点,当异步获取组件成功后,再通过 forceRender 强制重新渲染,这样就能正确渲染出我们异步加载的组件了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352

推荐阅读更多精彩内容

  • 1.什么是异步组件? 异步组件就是定义的时候什么都不做,只在组件需要渲染(组件第一次显示)的时候进行加载渲染并...
    钱罗罗_阅读 26,737评论 4 11
  • 利用webpack对代码进行分割是懒加载的前提,懒加载就是异步调用组件,需要时候才下载(按需加载)。 为什么需要懒...
    前端girl吖阅读 6,339评论 0 5
  • 异步组件,顾名思义,按需加载组件。 在vue的文档中,除了异步组件的高级使用方法外,介绍了三个异步组件的解决方案 ...
    Kaiser丶阅读 1,846评论 0 0
  • 前言: 项目中融入了React Native,很多页面都跟安卓那边通用,很多程度节约了开发时间,在开发过程中,遇到...
    捷风阅读 6,847评论 14 5
  • 作为一只导盲犬 我是失格的 我不能在需要我的人身边 我在水泥森林的顶层 红色的信号灯代替了闪烁的星光 广告的灯光是...
    铸言阅读 185评论 0 0