重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。
正文
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
如果传入的是一个组件,就设置 name
,然后把传入的 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
,这里传入的 Ctor
就是工厂函数了,接着看下 createComponent
里面:
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
)
}
}
}
由于传入的 Ctor
是一个函数,所以不会执行 baseCtor.extend(Ctor)
,也就是不会执行 Vue.extend(Ctor)
,所以也就没有 cid
,自然就进入了异步组件的创建逻辑。可以看到用 asyncFactory
保存了这个工厂函数,然后执行了一个 resolveAsyncComponent
方法,传入的参数分别是工厂函数、大Vue和当前实例,该方法定义在 src/core/vdom/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
就是Vue实例,后面使用了一个 once
函数来定义 resolve
和 reject
:
export function once (fn: Function): Function {
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}
一个简单的封装,确保传进来的函数只执行一次,之后的 called
就是true了,也就是说确保 resolve
和 reject
只执行一次,因为多次使用异步组件的话,也只能 resolve
一次,接着会执行 factory
(就是工厂函数),最后的返回值判断会返回一个 undefined
。
再回到 createComponent
方法里,如果 Ctor
是一个 undefined
,就执行一个 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
,它其实就是代码里 export
出来的那个组件对象,然后执行 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
,最终就会走到 mountComponent
方法里的下面这一段:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
也就是说通过 $forceUpdate
,来强制让它渲染一次,这样就会强制又走 render
,触发组件的重新渲染,然后就又会走一遍 createComponent
方法。
然后又会执行到 resolveAsynComponent
方法里,再来看下上面忽略的前几个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
}
它的 factory.resolved
判断就成了 true
(因为第一次把 ensureCtor
返回的构造器赋值给它),这样就会直接return出去,接着在 createComponent
里就不会执行 Ctor === undefined
这个逻辑了,接着就和普通的构造器Ctor走一样的逻辑了:安装组件钩子,创建组件vnode,patch……
捋一下:由于 Ctor
是一个工厂函数,所以没有 cid
,就会走 resolveAsyncComponent
方法,第一次 factory
参数在前面的if判断里都不满足,所以走到定义 resolve
和 reject
,这里调用了 once
方法保证了 resolve
和 reject
多次异步也只会走一次,接着走 factory
(也就是传入的工厂函数),然后return一个 undefined
。接着走 createComponent
的 createAsyncPlaceholder
方法,返回一个空vnode(注释节点),然后会走一个patch过程(因为在 createComponent
里)。接着后面回调 resolve
,传入代码里 export
出来的那个组件对象,会走 ensureCtor
函数并且赋值给 factory.resolved
,然后通过 forceRender
调用 $forceUpdate
来强制渲染一次,接着会又执行到 resolveAsyncComponent
里面去,此时有了 factory.resolved
就直接return,后面就安装组件钩子,创建组件vnode,patch……,等patch之后,那个空vnode(注释节点)就被替换掉了。
简化版:第一步创建了空注释节点,第二步渲染挂载DOM来替换空注释节点。
Promise异步组件
先来看下 Promise
的写法:
Vue.component(
'example',
// 该 `import` 函数返回一个 `Promise` 对象。
() => import('./Component')
)
在执行 resolveAsyncComponent
的时候,和工厂函数类似,不过在走到 res = factory(resovle, reject)
的时候,res其实是例子中 import
返回的 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,如果加载失败就执行 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
等,然后执行 factory
,它会执行前面定义 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
上,如果定义了 loading
,就创建一个 loading
的组件构造器扩展到 factor.loadingComp
上,
- 如果有loading,并且
delay
为0(也就是没有延迟时间),就直接把loading
设置为true,这个loading
为true的话,直接影响的就是返回值,返回值就会返回这个factory.loadingComp
,这样的话,返回值就不是undefined
了,那走到createComponent
的时候,就不会走createAsyncPlaceholder
了,会直接渲染这个loadingComp
。 - 如果
delay
不是0,就定义一个定时器,注意此时loading
还是没有的(定时器是异步的),这样返回值就还是undefined
,就会前面一样,渲染一个注释节点,如果后面组件没有加载成功,就会把loading
设置为true,并且会执行forceRender
方法,就又会重新进入resolveAsyncComponent
,此时如果有resolved
,就执行resolved
,如果laoding
是true,并且有loadingComp
,就渲染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()
}
})
除了警告信息外,如果定义了 errorComp
,就把 factory.error
设置为true,然后再调用 forceRender
,这样再回来的时候,第一个判断:
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
errorComp
就会执行(这个error判断放在方法的第一行,可见它的优先级是最高的),这样整体过程其实就是先一个空注释节点,再是替换成loading节点,最后替换成真正要展示的组件。
总结
-
异步组件实现的本质是2次渲染,先渲染成注释节点,当组件加载成功后,再通过
forceRender
重新渲染(通常是2次,上面的loading例子其实是3次)。 -
第二种
Promise
的设计是有webpack
的import
语法的支持实现的。 -
三种创建异步组件的方式里,高级异步组件是最灵活的,基本上每个状态都会有一个判定,然后通过配置实现了
loading
,resolve
,reject
,timeout
4种状态。
我的公众号:道道里的前端栈,每一天一篇前端文章,嚼碎的感觉真奇妙~
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!