重学Vue【异步组件原理分析】

重学Vue源码,根据​​黄轶大佬​​的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 ​​github​​ 上。

正文

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数其他垃圾的方式定义加载中的图片你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染

—— vue文档

Vue.component('example', function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(['./my-async-component'], function(res){
resolve(res)
})
})

我们可以通过上面的例子创建一个全局组件,和上篇分析​​组件注册​​​有点不一样的是,例子中注册的全局组件不是一个对象,而是一个工厂函数,函数有两个参数 ​构造器注入resolve​​​ 和 ​​reject​​,下面来看下定义对象和定义工厂函数的区别。

回顾一下在​​组件注册​​​中提到的 ​​assets​​:

if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
// ...
this.options[type + 's'][id] = definition
return definition

如果传入的是一个其他综合收益组件,就设置 ​​namvuejs和vue的区别e​​​,然后把vuejs设计与实现电子书传入的 ​​definition​​​ 通过 ​​Vue.extend​​​ 转化成一个构造器,那其他和其它的区别如果定义的是工厂函数,就会把这个工厂函数赋值给大 ​​Vue.options.components​​​,上面的例子就成了 ​​Vue.options.components.example = definition ​​。

接着在创建组件的vnode的时候,会执行 ​​_createElement​​ 方法:

if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}

和组件注册类似,也会走到 ​​resolveAsset​​​ 里面,然后赋值语句如何判断正误执行下面的 ​​createComponent​​​,这里传vuejs设计与实现pdf入的 ​​Ctor​​​ 就是工厂函数了,接着看下 ​​creat其他应付款eComponent​​ 里面:

export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}

const baseCtor = context.$options._base

// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}

// ...

// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
}

由于传入的 ​​Cto加载的读音r​​​ 是一个函数,所以不其他垃圾有哪些东西会执行 ​​baseCtor.extend(Ctor)​​​,也就是不会执行 ​​Vue.extend(Ctor)​​​,所以也就没有 ​​cid​​​,自然就进入了异步组件的创建逻辑。可以看到用 ​​a其他垃圾syncFactory​​​ 保存赋值法的原理是什么了这个工厂函数,然后执行了一个 ​​resolveAsyncComponen构造器t​​​ 方法,传入的参数分别是工厂函数、大Vue和当前实例,该方法定义在 ​​src/core/vd赋值运算符的优先级om/helpers/resolve-async-component.js​​:

export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>,
context: Component
): Class<Component> | void {
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}

if (isDef(factory.resolved)) {
return factory.resolved
}

if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}

if (isDef(factory.contexts)) {
// already pending
factory.contexts.push(context)
} else {
const contexts = factory.contexts = [context]
let sync = true

const forceRender = () => {
for (let i = 0, l = contexts.length; i < l; i++) {
contexts[i].$forceUpdate()
}
}

const resolve = once((res: Object | Class<Component>) => {
// cache resolved
factory.resolved = ensureCtor(res, baseCtor)
// invoke callbacks only if this is not a synchronous resolve
// (async resolves are shimmed as synchronous during SSR)
if (!sync) {
forceRender()
}
})

const reject = once(reason => {
process.env.NODE_ENV !== 'production' && warn(
`Failed to resolve async component: ${String(factory)}` +
(reason ? `\nReason: ${reason}` : '')
)
if (isDef(factory.errorComp)) {
factory.error = true
forceRender()
}
})

const res = factory(resolve, reject)

if (isObject(res)) {
if (typeof res.then === 'function') {
// () => Promise
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
} else if (isDef(res.component) && typeof res.component.then === 'function') {
res.component.then(resolve, reject)

if (isDef(res.error)) {
factory.errorComp = ensureCtor(res.error, baseCtor)
}

if (isDef(res.loading)) {
factory.loadingComp = ensureCtor(res.loading, baseCtor)
if (res.delay === 0) {
factory.loading = true
} else {
setTimeout(() => {
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender()
}
}, res.delay || 200)
}
}

if (isDef(res.timeout)) {
setTimeout(() => {
if (isUndef(factory.resolved)) {
reject(
process.env.NODE_ENV !== 'production'
? `timeout (${res.timeout}ms)`
: null
)
}
}, res.timeout)
}
}
}

sync = false
// return in case resolved synchronously
return factory.loading
? factory.loadingComp
: factory.resolved
}
}

​resolveAsyncComponent​​​ 函数的逻辑比较复杂,里面包含了3种异步处理组件的创建方式,除了上述的工厂函数,还支持 ​​Promise​​ 创建组件:

Vue.component(
"example",
() => import("./Component")
)

和高级异步组件(也就是官网提到的):

const AsyncComp = () => ({
// 需要加载的组件。应当是一个 Promise
component: import('./MyComp.vue'),
// 加载中应当渲染的组件
loading: LoadingComp,
// 出错时渲染的组件
error: ErrorComp,
// 渲染加载中组件前的等待时间。默认:200ms。
delay: 200,
// 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
timeout: 3000
})
Vue.component('async-example', AsyncComp)

工厂函数异步组件加载完成

直接跳其他和其它的区别过前面的几个if判断(它们是为高级组件用的),直接到 ​​factory.contexts = [context]​​​,这个 ​​context​​​ 就是V赋值语句的格式ue实例,后面使用了一个 ​​once​​​ 函数来定义 ​​resol其他垃圾有哪些东西v构造器的作用e​加载中​ 和 ​​reject​​:

export function once (fn: Function): Function {
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}

一个简单的封装,确保传进来的函数只执行一其他综合收益构造器,之赋值语句后的 ​​called​​​ 就是true了,也就是说确保 ​​r构造器esolve​​​ 和 ​​reject​​​ 只执行一次,因为多次系统/运维使用异步组件的话,也只能加载的读音​resolve​​​ 一次,接着会执行 ​​factory​​​(就是工厂函数),最后的返回值判断会返回一个 ​​undefined​​。

再回到 ​​createCom加载视频插件ponent​​​ 方法里,如果 ​​Ctor​​​ 是一个 ​​undefin构造器ed​​​,就其他和其它的区别执行一个 ​​createAsyncPlaceholder​​ 方加载中英文法:

export function createAsyncPlaceholder (
factory: Function,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag: ?string
): VNode {
const node = createEmptyVNode()
node.asyncFactory = factory
node.asyncMeta = { data, context, children, tag }
return node
}

这个方法创建了一个空vnode,也就是一个空的注释节点(就长 ​​<!---->​​​ 这个样子插入到dom中,当一个占位符用),然后渲染这个节点,到这里这个异步组件加载完了,系统运维主要做什么然后 ​​resolve​​ 这赋值语句的格式个组件,也就是执行:

function ensureCtor (comp: any, base) {
if (
comp.__esModule ||
(hasSymbol && comp[Symbol.toStringTag] === 'Module')
) {
comp = comp.default
}
return isObject(comp)
? base.extend(comp)
: comp
}
// ...
const forceRender = () => {
for (let i = 0, l = contexts.length; i < l; i++) {
contexts[i].$forceUpdate()
}
}
// ...
const resolve = once((res: Object | Class<Component>) => {
// cache resolved
factory.resolved = ensureCtor(res, baseCtor)
// invoke callbacks only if this is not a synchronous resolve
// (async resolves are shimmed as synchronous during SSR)
if (!sync) {
forceRender()
}
})

这里的 ​​res​​​ 就是最上面例子传入的 ​​res​​​,它其实其他应收款是什么科目就是代码里 ​​expo加载云盘目录失败r加载了恋爱游戏t​​​ 出来的那个组件对象,然后加载中执行 ​​ensureCtor​​​,这个方法做了一个兼容,传入的是一个 ​​module加载失败图片​​ 或者其他,最终都其他应收款可以拿到 ​​export​​​ 出来的对象,然后返回了一个异步组件的构造器,把它赋值给 ​​factory.resolved​​​,然后执行 ​​forceRender​​​ 函数,这个函数就是遍历所有的vm实例,每个实例都执行一个赋值表达式​$forceUpdate​​​,它定义在 ​​src/core/instance/lifecycle.js​​:

Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}

它调用了渲染watcher的 ​​update其他应收款​​,然后会走渲染watcher的 ​​getter赋值​​,最终就会走vuejs和vue的区别到 ​​mountComponent​​ 方法里的下面这一段:

updateComponent = () => {
vm._update(vm._render(), hydrating)
}

也就是说通过 ​​$forceUpdate​​​,来强制让它渲染一次,这样就会强制又走 ​​render​​​,触发组件的重新渲染,然后就又会走一遍 ​​createComponent​​ 方法。

之所以这么做是因为 Vue 通常是数据驱动视图重新渲染,但是在整个异步组件加载过程中是没有数据发生变化的,所以通过执行 ​​$forceUpdate​​ 可以强制组件重新渲染一次

然后又会执行到 ​​r其他业务收入esolveAsynComponent​​ 方法里,再来看下上面忽略的前几个if判断:

if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}

if (isDef(factory.resolved)) {
return factory.resolved
}

if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}

它的vue.js前端开发实战加载了恋爱游戏factory.resolved​​​其他 判断就成了 ​​true​​​(赋值表达式因为第一次把 ​​ensur其他货币资金eCtor​​​ 返回的构赋值法的原理是什么造器赋值给它),这样就会直接return构造器是否可以被重写出去,接着在 ​​createComponent​​​ 里就不会执行 ​​Ctor === undefined​​ 这个逻辑了,接着就和普通的构造器Ctor走一样的逻辑了:安装组件钩子,创加载的读音建组件vnode,patch……

捋一下:由于 ​​Ctor​​​ 是一个工厂函数,所以没有 ​​cid​​​,就会走 ​​resolveAsyncComponent​​​ 方法,第一次 ​​factory​​​ 参数在前面的if判断里都不满足,所以走到定义 ​​resolve​​​ 和 ​​reject​​​ ,这里调用了 ​​once​系统运维工作内容​​ 方法保证了 ​​resolve​​​ 和 ​其他应付款reject​​​ 多次异步也只加载失败会走一次,接着走 ​​factory​​​(也就是传入的工厂函数),然后return一个 ​​undefined​​​。接着走 ​​cre系统运维工程师ateComponent​​​ 的 ​​c其他和其它的区别reateAsyncPlaceholder​​​ 方法,返回一个空vnode(注释节点),然后会走一个patch过程(因为在 ​​createComponent​​​ 里)。接着后面回调 ​​resolve​​​,传入加载中的图片代码里 ​​export​​​ 出来的那个组件对象,会走 ​​ensureCto系统/运维r​​​ 函数并且赋值给 ​​factory.resolved​​​,然后通过 ​​forceRender​​​ 调用 ​​$forceUpdate​​​ 来强制渲染一次,接着会又执行到构造器是什么​res其他业务收入olveAsyncComponent​​​ 里赋值运算符的优先级面去,系统运维是干嘛的赋值语句正确写法时有了 ​​factVue.jsory.resolved​​ 就直接return,后面就安装组件钩子,创建组件vnode,patch……,等其他综合收益patch之后,那个空vnod构造器注入e(注释节点)就被替换掉了。

简化版:第一步创建了空注释节点,第二步渲染挂其他应收款是什么科目载DOM来替换空注释节点。

Promise异步组件

先来看下 ​​Promise​​ 的写法:

Vue.component(
'example',
// 该 `import` 函数返回一个 `Promise` 对象。
() => import('./Component')
)

在执行 ​​resolveAsyncComponent​​​ 的时候,和工厂函数类似,不过在走到 ​​res = factory(resovle, reject)vue.js是做什么的​​ 的时候,res其实是例子中 ​​imporvuejs设计与实现t​​​ 返回的 ​​promise​​​ 对象,所以res就有了 ​系统/运维then​​ 方法,那就会走到下面的逻辑:

// ...
if (typeof res.then === 'function') {
// () => Promise
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
}

​Promise​​​ 方式的话,第一次的 ​​factory.resolved​​​ 也是 ​​undefined​​​,所以就会走 ​​res.then(resolve, reject)​​,加载中的图片

然后在异步组件加载成功赋值语句正确写法之后,走这个 ​​resolve​​​,也就是走上面的定义好的resolve,Vue.js如果加载失败就执行 ​​reject​​:

const resolve = once((res: Object | Class<Component>) => {
// cache resolved
factory.resolved = ensureCtor(res, baseCtor)
// invoke callbacks only if this is not a synchronous resolve
// (async resolves are shimmed as synchronous during SSR)
if (!sync) {
forceRender()
}
})
const reject = once(reason => {
process.env.NODE_ENV !== 'production' && warn(
`Failed to resolve async component: ${String(factory)}` +
(reason ? `\nReason: ${reason}` : '')
)
if (isDef(factory.errorComp)) {
factory.error = true
forceRender()
}
})

后面的逻辑就和工厂函数一样了。

高级异步组件

该写法是在2.3.0+新增的一种写法:

const AsyncComp = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./Component.vue'),
// 加载中应当渲染的组件
loading: {
template:"<div>loading</div>"
},
// 出错时渲染的组件
error: {
template:"<div>error</div>"
},
// 渲染加载中组件前的等待时间,默认:200ms
delay: 200,
// 最长等待时间,超出该时间就渲染错误组件,默认:Infinity
timeout: 3000
})
Vue.component("example", AsyncComp)

这样就定义了一个高级异步组件,猛地一看有种节流的感觉,下面分析一下它的实现。

它依然会走到 ​​resolveAsyncComponent​​​ 方法里面,然后第一次执行的时候,最前面的几个if判断:​​error​​​、加载完成​resolved加载的读音​​ 和 ​​loading​​​ 依然是 ​​undefined​​​,接着定义了 ​​resolve​​​,​​reject​​​ 等,然后执行 ​vue.js官网​f赋值法ac赋值运算符的优先级tory​​​,它会执行前面定义 ​​AsyncComp​​​ 返回的对象,接着把它赋值给了 ​​res​​,然后走到下面:

if (isObject(res)) {
if (typeof res.then === 'function') {
// () => Promise
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
} else if (isDef(res.component) && typeof res.component.then === 'function') {
res.component.then(resolve, reject)

if (isDef(res.error)) {
factory.errorComp = ensureCtor(res.error, baseCtor)
}

if (isDef(res.loading)) {
factory.loadingComp = ensureCtor(res.loading, baseCtor)
if (res.delay === 0) {
factory.loading = true
} else {
setTimeout(() => {
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender()
}
}, res.delay || 200)
}
}

if (isDef(res.timeout)) {
setTimeout(() => {
if (isUndef(factory.resolved)) {
reject(
process.env.NODE_ENV !== 'production'
? `timeout (${res.timeout}ms)`
: null
)
}
}, res.timeout)
}
}
}

因为 ​​res​​​ 是一个对象,不是一个Promise,所以没有then方法,同时 ​​res​​​ 定义了 ​​component​​​(就是加载的异步组件),并且它有then方法,所以就走 ​​res.component.then​​​,接着后面做了一系列判断,如果定义了 ​加载视频插件​error​​​,就创建一个 ​​error​​​ 的组件构造器并且扩展到 ​​factory.errorComp​​​ 上,如果定义了 ​​load构造器是什么ing​​​,就创建一个 ​​loading​​​ 的组件构造器扩展到 ​​fact其他垃圾有哪些东西or.loadingComp​​ 上,

  • 如果有loading,并且​​delay​​​ 为0(也就是没有延迟时间),就直接把​​loading​​​ 设置为true,这个​​loading​​​ 为true的话,直接影响的就是返回值,返回值就会返加载失败图片回这个​​fac构造器是否可以被重写tory.loa加载失败dingComp系统运维工程师​​,这样的话,返回值就不是​​undefined​​​ 了,那走到​​createComponent其他货币资金​​ 的时加载失败图片候,就不会走​​createAsyncPlaceholder​​​ 了,加载了恋爱游戏会直接渲染这个​​load构造器是否可以被重写ingComp​​。
  • 如果​​delay​​​ 不是0,就定义一个定时器,注加载视频插件意此时​​loading​​​ 还是没有的(定时器其他综合收益是异步的),这样返回值就还是​​undefined​系统运维是干嘛的​​,就会前面一样,渲染一个注释节点,如果后面组件没有加载成功,就会把​其他业务收入lo构造器adin其他货币资金g​​​ 设置为true,并且会执行​​forceRender​​​ 方法,就又会重新进入​​resolveAsyncComponent​​​,此时如加载云盘目录失败果有​​resolvvue.js官网ed​​​,就执行​​resolved​​​,如果​​laoding​​​ 是构造器是什么true,并且有​​loadingC加载中omp​​​,就渲染​​loadingComp​​。

接着判断 ​​timeout​​​,如果过了这个时间还没有 ​​resolved​​​ 的赋值语句话,系统/运维就走 ​​reject​​:赋值是什么意思

const reject = once(reason => {
process.env.NODE_ENV !== 'production' && warn(
`Failed to resolve async component: ${String(factory)}` +
(reason ? `\nReason: ${reason}` : '')
)
if (isDef(factory.errorComp)) {
factory.error = true
forceRender()
}
})

除了警告信息外,如果定义了 ​​erro加载r加载中Comp​​​,就把 ​​factory.error​​​ 设置为true,然后再调用 ​构造器是否可以被重写​forceRender​​,这样再回来的时候,第一个判断赋值语句如何判断正误

if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}

​errorComp​​ 就会执行(这个error判断放在Vue.js方法的第一行,可见它的优先级是最高的),这样整体过程其实就是先一个空系统运维主要做什么注释节点,再是替换构造器的作用成loading节点,最后替换成真正要展示的组件。

总结

  1. 异步组件实现的系统运维是干嘛的本质是2次渲染,先渲染成注释节点,当组件加载成功后,再通过 ​赋值语句如何判断正误​forceRender​​ 重新渲染(通常是2次,上其他应收款是什么科目面的loadin加载中g例子其实是3次)。
  2. 第二种 ​​Promise​构造器注入​ 的设计是有 ​​webpack​​ 的 ​​import​​ 语法的支持实现的。
  3. 三种创建异步组件的方式里,高级异步组件是最灵活的,基本上每个状态都会有一个判定,然后通过配置实现了 ​​loading​​,​​resolve​​,​​reject​​,​​ti系统/运维meout​​ 4种状态。

我的公众号:道道里的前端栈,每一天一篇前端文章,加载完成嚼碎的感觉真奇妙~