Vue
实例在被创建之前都要经过一系列的初始化过程。例如需要设置数据监听、编译模板、挂载实例到 DOM
、在数据变化时更新 DOM
等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,给予用户机会在一些特定的场景下添加他们自己的代码。在我们实际项目开发过程中,会非常频繁地和 Vue
组件的生命周期打交道,接下来我们就从源码的角度来看一下这些生命周期的钩子函数是如何被执行的。源码中最终执行生命周期的函数都是调用 callHook
方法,它的定义在 src/core/instance/lifecycle
中:export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
callHook
函数的逻辑很简单,根据传入的字符串hook
,去拿到vm.$options[hook]
对应的回调函数数组,然后遍历执行,执行的时候把vm
作为函数执行的上下文。
在之前详细地介绍了 Vue.js
合并 options
的过程,各个阶段的生命周期的函数也被合并到 vm.$options
里,并且是一个数组。因此 callhook
函数的功能就是调用某个生命周期钩子注册的所有回调函数。了解了生命周期的执行方式后,接下来会具体介绍每一个生命周期函数它的调用时机。
beforeCreate & created
,如下所示:
beforeCreate
和 created
函数都是在实例化 Vue
的阶段,在 _init
方法中执行的,它的定义在 src/core/instance/init.js
中:Vue.prototype._init = function (options?: Object) {
// ...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// ...
}
可以看到 beforeCreate
和 created
的钩子调用是在 initState
的前后,initState
的作用是初始化 props
、data
、methods
、watch
、computed
等属性,。那么显然 beforeCreate
的钩子函数中就不能获取到 props
、data
中定义的值,也不能调用 methods
中定义的函数。
在这俩个钩子函数执行的时候,并没有渲染 DOM
,所以我们也不能够访问 DOM
,一般来说,如果组件在加载的时候需要和后端有交互,放在这俩个钩子函数执行都可以,如果是需要访问 props
、data
等数据的话,就需要使用 created
钩子函数,之后发现它们都混合了 beforeCreate
钩子函数。
beforeMount & mounted
,如下所示:beforeMount
钩子函数发生在 mount
,也就是 DOM
挂载之前,它的调用时机是在 mountComponent
函数中,定义在 src/core/instance/lifecycle.js
中:export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// ...
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
vm._render()
函数渲染 VNode
之前,执行了 beforeMount
钩子函数,在执行完 vm._update()
把 VNode patch
到真实 DOM
后,执行 mounted
钩子。注意,这里对 mounted
钩子函数执行有一个判断逻辑,vm.$vnode
如果为 null
,则表明这不是一次组件的初始化过程,而是我们通过外部 new Vue
初始化过程。VNode patch
到 DOM
后,会执行 invokeInsertHook
函数,把 insertedVnodeQueue
里保存的钩子函数依次执行一遍,它的定义在 src/core/vdom/patch.js
中:function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
insert
这个钩子函数,对于组件而言,insert
钩子函数的定义在 src/core/vdom/create-component.js
中的 componentVNodeHooks
中:const componentVNodeHooks = {
// ...
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
// ...
},
}
mounted
钩子函数,并且我们之前分析过,insertedVnodeQueue
的添加顺序是先子后父,所以对于同步渲染的子组件而言,mounted
钩子函数的执行顺序也是先子后父。beforeUpdate & updated
,如下所示:beforeUpdate
和 updated
的钩子函数执行时机都应该是在数据更新的时候,beforeUpdate
的执行时机是在渲染 Watcher
的 before
函数中,我们刚才提到过:export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// ...
}
注意这里有个判断,也就是在组件已经 mounted
之后,才会去调用这个钩子函数。
update
的执行时机是在flushSchedulerQueue
函数调用的时候,它的定义在 src/core/observer/scheduler.js
中:
function flushSchedulerQueue () {
// ...
// 获取到 updatedQueue
callUpdatedHooks(updatedQueue)
}
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}
flushSchedulerQueue
函数我们之后会详细介绍,updatedQueue
是更新了的 wathcer
数组,那么在 callUpdatedHooks
函数中,它对这些数组做遍历,只有满足当前 watcher
为 vm._watcher
以及组件已经 mounted
这两个条件,才会执行 updated
钩子函数。
我们之前提过,在组件 mount
的过程中,会实例化一个渲染的 Watcher
去监听 vm
上的数据变化重新渲染,这段逻辑发生在 mountComponent
函数执行的时候:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
// 这里是简写
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// ...
}
Watcher
的过程中,在它的构造函数里会判断 isRenderWatcher
,接着把当前 watcher
的实例赋值给 vm._watcher
,定义在 src/core/observer/watcher.js
中:export default class Watcher {
// ...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// ...
}
}
wathcer
实例 push
到 vm._watchers
中,vm._watcher
是专门用来监听 vm
上数据变化然后重新渲染的,所以它是一个渲染相关的 watcher
,因此在 callUpdatedHooks
函数中,只有 vm._watcher
的回调执行完毕后,才会执行 updated
钩子函数。beforeDestroy & destroyed
,如下所示:beforeDestroy
和 destroyed
钩子函数的执行时机在组件销毁的阶段,最终会调用 $destroy
方法,它的定义在 src/core/instance/lifecycle.js
中:Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
beforeDestroy
钩子函数的执行时机是在 $destroy
函数执行最开始的地方,接着执行了一系列的销毁动作,包括从 parent
的 $children
中删掉自身,删除 watcher
,当前渲染的 VNode
执行销毁钩子函数等,执行完毕后再调用 destroy
钩子函数。
在 $destroy
的执行过程中,它又会执行 vm.__patch__(vm._vnode, null)
触发它子组件的销毁钩子函数,这样一层层的递归调用,所以 destroy
钩子函数执行顺序是先子后父,和 mounted
过程一样。
activated & deactivated
,activated
和 deactivated
钩子函数是专门为 keep-alive
组件定制的钩子,在介绍 keep-alive
组件的时候会详细介绍。
总结:Vue
生命周期中各个钩子函数的执行时机以及顺序,通过分析,我们知道了如在 created
钩子函数中可以访问到数据,在 mounted
钩子函数中可以访问到 DOM
,在 destroy
钩子函数中可以做一些定时器销毁工作,了解它们有利于我们在合适的生命周期去做不同的事情。
Vue.js
中,除了它内置的组件如 keep-alive
、component
、transition
、transition-group
等,其它用户自定义组件在使用前必须注册,在开发过程中可能会遇到如下报错信息:'Unknown custom element: - did you register the component correctly?
For recursive components, make sure to provide the "name" option.'
一般报这个错的原因都是我们使用了未注册的组件。Vue.js 提供了两种组件的注册方式,全局注册和局部注册。接下来我们从源码分析的角度来分析这两种注册方式。
Vue.component(tagName, options)
。例如:Vue.component('my-component', {
// 选项
})
Vue.component
函数的定义过程发生是在最开始初始化 Vue
的全局函数的时候,代码在 src/core/global-api/assets.js
中,如下所示:import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}
ASSET_TYPES
,得到 type
后挂载到 Vue
上,ASSET_TYPES
的定义在 src/shared/constants.js
中:export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
所以实际上 Vue
是初始化了三个全局函数,并且如果 type
是 component
且 definition
是一个对象的话,通过 this.opitons._base.extend
, 相当于 Vue.extend
把这个对象转换成一个继承于 Vue
的构造函数,最后通过 this.options[type + 's'][id] = definition
把它挂载到 Vue.options.components
上。
由于我们每个组件的创建都是通过 Vue.extend
继承而来,我们之前分析过在继承的过程中有这么一段逻辑:
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Vue.options
合并到 Sub.options
,也就是组件的 options
上, 然后在组件的实例化阶段,会执行 merge options
逻辑,把 Sub.options.components
合并到 vm.$options.components
上。然后在创建 vnode
的过程中,会执行 _createElement
方法,我们再来回顾一下这部分的逻辑,它的定义在 src/core/vdom/create-element.js
中:export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// ...
let vnode, ns
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)
}
// ...
}
这里有一个判断逻辑
isDef(Ctor = resolveAsset(context.$options, 'components', tag))
,先来看一下resolveAsset
的定义,在src/core/utils/options.js
中:
/**
* Resolve an asset.
* This function is used because child instances need access
* to assets defined in its ancestor chain.
*/
export function resolveAsset (
options: Object,
type: string,
id: string,
warnMissing?: boolean
): any {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
const assets = options[type]
// check local registration variations first
if (hasOwn(assets, id)) return assets[id]
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// fallback to prototype chain
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
)
}
return res
}
这段逻辑很简单,先通过 const assets = options[type]
拿到 assets
,然后再尝试拿 assets[id]
,这里有个顺序,先直接使用 id
拿,如果不存在,则把 id
变成驼峰的形式再拿,如果仍然不存在则在驼峰的基础上把首字母再变成大写的形式再拿,如果仍然拿不到则报错。这样说明了我们在使用 Vue.component(id, definition)
全局注册组件的时候,id
可以是连字符、驼峰或首字母大写的形式。
那么回到我们的调用 resolveAsset(context.$options, 'components', tag)
,即拿 vm.$options.components[tag]
,这样我们就可以在 resolveAsset
的时候拿到这个组件的构造函数,并作为 createComponent
的钩子的参数。
局部注册,Vue.js
也同样支持局部注册,我们可以在一个组件内部使用 components
选项做组件的局部注册,例如:
import HelloWorld from './components/HelloWorld'
export default {
components: {
HelloWorld
}
}
其实理解了全局注册的过程,局部注册是非常简单的。在组件的 Vue
的实例化阶段有一个合并 option
的逻辑,之前我们也分析过,所以就把 components
合并到 vm.$options.components
上,这样我们就可以在 resolveAsset
的时候拿到这个组件的构造函数,并作为 createComponent
的钩子的参数。
注意,局部注册和全局注册不同的是,只有该类型的组件才可以访问局部注册的子组件,而全局注册是扩展到 Vue.options
下,所以在所有组件创建的过程中,都会从全局的 Vue.options.components
扩展到当前组件的 vm.$options.components
下,这就是全局注册的组件能被任意使用的原因。
总结:我们对组件的注册过程有了认识,并理解了全局注册和局部注册的差异。其实在平时的工作中,当我们使用到组件库的时候,往往更通用基础组件都是全局注册的,而编写的特例场景的业务组件都是局部注册的。了解了它们的原理,对我们在工作中到底使用全局注册组件还是局部注册组件是有这非常好的指导意义的。
Vue
也原生支持了异步组件的能力,如下所示:Vue.component('async-example', function (resolve, reject) {
// 这个特殊的 require 语法告诉 webpack
// 自动将编译后的代码分割成不同的块,
// 这些块将通过 Ajax 请求自动下载。
require(['./my-async-component'], resolve)
})
在示例中可以看到,Vue
注册的组件不再是一个对象,而是一个工厂函数,函数有两个参数 resolve
和 reject
,函数内部用 setTimout
模拟了异步,实际使用可能是通过动态请求异步组件的 JS
地址,最终通过执行 resolve
方法,它的参数就是我们的异步组件对象。
在了解了异步组件如何注册后,我们从源码的角度来分析一下它的实现。之前我们分析了组件的注册逻辑,由于组件的定义并不是一个普通对象,所以不会执行 Vue.extend
的逻辑把它变成一个组件的构造函数,但是它仍然可以执行到 createComponent
函数,我们再来对这个函数做回顾,它的定义在 src/core/vdom/create-component/js
中:
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
是一个函数,那么它也并不会执行 Vue.extend
逻辑,因此它的 cid
是 undefiend
,进入了异步组件创建的逻辑。这里首先执行了 Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
方法,它的定义在 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
函数的逻辑略复杂,因为它实际上处理了三种异步组件的创建方式,除了刚才示例的组件注册方式,还支持两种,一种是支持 Promise
创建组件的方式,如下所示:Vue.component(
'async-webpack-example',
// 该 `import` 函数返回一个 `Promise` 对象。
() => import('./my-async-component')
)
另一种是高级异步组件,如下所示:
const AsyncComp = () => ({
// 需要加载的组件。应当是一个 Promise
component: import('./MyComp.vue'),
// 加载中应当渲染的组件
loading: LoadingComp,
// 出错时渲染的组件
error: ErrorComp,
// 渲染加载中组件前的等待时间。默认:200ms。
delay: 200,
// 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
timeout: 3000
})
Vue.component('async-example', AsyncComp)
那么我们就根据这三种异步组件的情况,来分别去分析
resolveAsyncComponent
的逻辑。
factory.contexts
的判断,是考虑到多个地方同时初始化一个异步组件,那么它的实际加载应该只有一次。接着进入实际加载逻辑,定义了 forceRender
、resolve
和 reject
函数,注意 resolve
和 reject
函数用 once
函数做了一层包装,它的定义在 src/shared/util.js
中:/**
* Ensure a function is called only once.
*/
export function once (fn: Function): Function {
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}
once
逻辑非常简单,传入一个函数,并返回一个新函数,它非常巧妙地利用闭包和一个标志位保证了它包装的函数只会执行一次,也就是确保resolve
和reject
函数只执行一次。
const res = factory(resolve, reject)
逻辑,这块儿就是执行我们组件的工厂函数,同时把 resolve
和 reject
函数作为参数传入,组件的工厂函数通常会先发送请求去加载我们的异步组件的 JS
文件,拿到组件定义的对象 res
后,执行 resolve(res)
逻辑,它会先执行 factory.resolved = ensureCtor(res, baseCtor)
:function ensureCtor (comp: any, base) {
if (
comp.__esModule ||
(hasSymbol && comp[Symbol.toStringTag] === 'Module')
) {
comp = comp.default
}
return isObject(comp)
? base.extend(comp)
: comp
}
这个函数目的是为了保证能找到异步组件 JS 定义的组件对象,并且如果它是一个普通对象,则调用
Vue.extend
把它转换成一个组件的构造函数。
resolve
逻辑最后判断了 sync
,显然我们这个场景下 sync
为 false
,那么就会执行 forceRender
函数,它会遍历 factory.contexts
,拿到每一个调用异步组件的实例 vm
, 执行 vm.$forceUpdate()
方法,它的定义在 src/core/instance/lifecycle.js
中:Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
$forceUpdate
的逻辑非常简单,就是调用渲染 watcher
的 update
方法,让渲染 watcher
对应的回调函数执行,也就是触发了组件的重新渲染。之所以这么做是因为 Vue
通常是数据驱动视图重新渲染,但是在整个异步组件加载过程中是没有数据发生变化的,所以通过执行 $forceUpdate
可以强制组件重新渲染一次。
Promise
异步组件,如下所示:
Vue.component(
'async-webpack-example',
// 该 `import` 函数返回一个 `Promise` 对象。
() => import('./my-async-component')
)
webpack 2+
支持了异步加载的语法糖:() => import('./my-async-component')
,当执行完 res = factory(resolve, reject)
,返回的值就是 import('./my-async-component')
的返回值,它是一个 Promise
对象。接着进入 if
条件,又判断了 typeof res.then === 'function')
,条件满足,执行:if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
当组件异步加载成功后,执行
resolve
,加载失败则执行reject
,这样就非常巧妙地实现了配合 webpack 2+ 的异步加载组件的方式(Promise
)加载异步组件。
JS
,有一定网络延时,而且有加载失败的情况,所以通常我们在开发异步组件相关逻辑的时候需要设计 loading
组件和 error
组件,并在适当的时机渲染它们。Vue.js 2.3+
支持了一种高级异步组件的方式,它通过一个简单的对象配置,帮你搞定 loading
组件和 error
组件的渲染时机,你完全不用关心细节,非常方便。接下来我们就从源码的角度来分析高级异步组件是怎么实现的,如下所示:const AsyncComp = () => ({
// 需要加载的组件。应当是一个 Promise
component: import('./MyComp.vue'),
// 加载中应当渲染的组件
loading: LoadingComp,
// 出错时渲染的组件
error: ErrorComp,
// 渲染加载中组件前的等待时间。默认:200ms。
delay: 200,
// 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
timeout: 3000
})
Vue.component('async-example', AsyncComp)
高级异步组件的初始化逻辑和普通异步组件一样,也是执行 resolveAsyncComponent
,当执行完 res = factory(resolve, reject)
,返回值就是定义的组件对象,显然满足 else if (isDef(res.component) && typeof res.component.then === 'function')
的逻辑,接着执行 res.component.then(resolve, reject)
,当异步组件加载成功后,执行 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.error
是否定义了 error 组件,如果有的话则赋值给factory.errorComp
。
接着判断res.loading
是否定义了 loading 组件,如果有的话则赋值给factory.loadingComp
,如果设置了res.delay
且为 0,则设置factory.loading = true
,否则延时delay
的时间执行,如下所示:
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender()
}
最后判断
res.timeout
,如果配置了该项,则在res.timout
时间后,如果组件没有成功加载,执行reject
。
resolveAsyncComponent
的最后有一段逻辑,如下所示:sync = false
return factory.loading
? factory.loadingComp
: factory.resolved
如果 delay
配置为 0
,则这次直接渲染 loading
组件,否则则延时 delay
执行 forceRender
,那么又会再一次执行到 resolveAsyncComponent
。那么这时候我们有几种情况,按逻辑的执行顺序,对不同的情况做判断。
异步组件加载失败,当异步组件加载失败,会执行 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()
}
})
这个时候会把
factory.error
设置为true
,同时执行forceRender()
再次执行到resolveAsyncComponent
:
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
那么这个时候就返回
factory.errorComp
,直接渲染 error 组件。
resolve
函数:const resolve = once((res: Object | Class<Component>) => {
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender()
}
})
首先把加载结果缓存到
factory.resolved
中,这个时候因为sync
已经为 false,则执行forceRender()
再次执行到resolveAsyncComponent
:
if (isDef(factory.resolved)) {
return factory.resolved
}
那么这个时候直接返回
factory.resolved
,渲染成功加载的组件。
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
那么则会返回
factory.loadingComp
,渲染 loading 组件。
异步组件加载超时,如果超时,则走到了 reject
逻辑,之后逻辑和加载失败一样,渲染 error
组件。
异步组件 patch
,回到 createComponent
的逻辑:
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
resolveAsyncComponent
,除非使用高级异步组件 0 delay
去创建了一个 loading
组件,否则返回是 undefiend
,接着通过 createAsyncPlaceholder
创建一个注释节点作为占位符。它的定义在 src/core/vdom/helpers/resolve-async-components.js
中: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,同时把
asyncFactory
和asyncMeta
赋值给当前vnode
。
当执行 forceRender
的时候,会触发组件的重新渲染,那么会再一次执行 resolveAsyncComponent
,这时候就会根据不同的情况,可能返回 loading、error
或成功加载的异步组件,返回值不为 undefined
,因此就走正常的组件 render
、patch
过程,与组件第一次渲染流程不一样,这个时候是存在新旧 vnode
的,后面会分析组件更新的 patch
过程。
总结:我们对 Vue
的异步组件的实现有了深入的了解,知道了三种异步组件的实现方式,并且看到高级异步组件的实现是非常巧妙的,它实现了 loading、resolve、reject、timeout
四种状态。异步组件实现的本质是两次渲染,除了 0 delay
的高级异步组件第一次直接渲染成 loading
组件外,其它都是第一次渲染生成一个注释节点,当异步获取组件成功后,再通过 forceRender
强制重新渲染,这样就能正确渲染出我们异步加载的组件了。