# Vue2.x 的生命周期

前言

vue 中不同的时机执行不同的生命周期:

  • 1.首次加载依次执行
  • 2.数据更新依次执行
  • 3.数据销毁依次执行
  • 4.组件报错时执行

先看一个问题引发的思考:

刷新
全屏/自适应
  • 结果

    组件类型 步骤 1 步骤 2 步骤 3 步骤 4
    父组件 beforeUpdate updated nextTick1 nextTick2

nextTick1 、 nextTick2 为微任务异步执行,最后打印的,msg 修改引起的更新,所以 beforeUpdate、updated 生命周期同步执行?

目录

# 1.首次加载

# 1.1 beforeCreate

  • Vue
function Vue(options) {
  this._init(options)
}
1
2
3
  • 初始化相关逻辑
Vue.prototype._init = function (options?: Record<string, any>) {
  ...
  callHook(vm, "beforeCreate", undefined, false /* setContext */)
  initInjections(vm)
  initState(vm)
  initProvide(vm)
}
1
2
3
4
5
6
7
  • initInjections 对 inject 进行初始化
  • initState 对 methods、data、computed、watch 进行初始化

结论

由此可知 beforeCreate不能访问 methods、data、computed、watch 数据

使用场景

一般插件会在此生命周期混入一些逻辑处理,如:vuex 、vue-router

# 1.2 created

Vue.prototype._init = function (options?: Record<string, any>) {
  ...
  initInjections(vm)
  initState(vm)
  initProvide(vm)
  callHook(vm, "created")
}
1
2
3
4
5
6
7

结论

由此可知 created能访问 methods、data、computed、watch 数据

使用场景

可以用来处理接口请求相关的逻辑

# 1.3 beforeMount

Vue.prototype._init = function (options?: Record<string, any>) {
  ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}
1
2
3
4
5
6
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
1
2
3
4
5
6
7
  • 重写$mount
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  return mount.call(this, el, hydrating)
}
1
2
3
4
5
6
7
8
export function mountComponent(
  vm: Component,
  el: Element | null | undefined,
  hydrating?: boolean
): Component {
  ...
  callHook(vm, "beforeMount")
}
1
2
3
4
5
6
7
8

结论

由此可知没有 el 属性,没有 vm.$mount,beforeMount、mounted 不会调用,手动挂载 vm.$mount("#app")后可以恢复正常

new Vue({
  beforeCreate: function () {
    console.log("调用了beforeCreate")
  },
  created: function () {
    console.log("调用了created")
  },
  beforeMount: function () {
    console.log("调用了beforeMount")
  },
  mounted: function () {
    console.log("调用了mounted")
  },
})
// 输出结果
// 调用了beforeCreate
// 调用了created
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

使用场景

可以处理 dom 挂载前业务逻辑

# 1.4 mounted

  • 渲染 watcher
export function mountComponent(
  vm: Component,
  el: Element | null | undefined,
  hydrating?: boolean
): Component {
  ...
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  const watcherOptions: WatcherOptions = {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, "beforeUpdate")
      }
    },
  }
  new Watcher(
    vm,
    updateComponent,
    noop,
    watcherOptions,
    true /* isRenderWatcher */
  )
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, "mounted")
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  • Watcher
export default class Watcher implements DepTarget {
  constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {
    this.getter = expOrFn
    this.value = this.lazy ? undefined : this.get()
  }
  get() {
    value = this.getter.call(vm, vm)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

根组件直接会走 mounted 生命周期

  • 组件生命周期 init
const componentVNodeHooks = {
  insert(vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, "mounted")
    }
  },
}
1
2
3
4
5
6
7
8
9

非根组件走组件自身的 hooks 触发 mounted:render-->组件 vnode--> patch-->创建组件-->实例化组件(内部调用 extend 相当于 new Vue)-- invokeInsertHook --> queue[i].data.hook.insert(queue[i])

结论

由此可知根组件和非根组件执行 mounted 的方式是不一样的,异步组件走非根组件逻辑,beforeCreate created beforeMount mounted 异步执行,其他情况下 beforeCreate created beforeMount mounted 同步执行

使用场景

可以处理 dom 相关的业务逻辑

# 1.5 父子组件单独加载案例

刷新
全屏/自适应
  • 初始化

    组件类型 步骤 1 步骤 2 步骤 3 步骤 4
    父组件 parent-beforeCreate parent-created parent-beforeMount parent-mounted
  • 创建子组件

    组件类型 步骤 1 步骤 2 步骤 3 步骤 4 步骤 5 步骤 6 步骤 7
    父组件 parent-beforeUpdate parent-updated
    子组件 child-beforeCreate child-created child-beforeMount child-mounted child-activated

# 1.6 父子组件一起加载案例

刷新
全屏/自适应
  • 初始化

    组件类型 步骤 1 步骤 2 步骤 3 步骤 4 步骤 5 步骤 6 步骤 7 步骤 8 步骤 9
    父组件 parent-beforeCreate parent-created parent-beforeMount parent-mounted
    子组件 child-beforeCreate child-created child-beforeMount child-mounted child-activated

# 2.数据更新

# 2.1 beforeUpdate

  • 渲染 watcher
export function mountComponent(
  vm: Component,
  el: Element | null | undefined,
  hydrating?: boolean
): Component {
  ...
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  const watcherOptions: WatcherOptions = {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, "beforeUpdate")
      }
    },
  }
  new Watcher(
    vm,
    updateComponent,
    noop,
    watcherOptions,
    true /* isRenderWatcher */
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  • watcher 类,响应式数据第一次访问时进行依赖收集,会将渲染 watcher 存储起来,数据有修改时派发更新
export default class Watcher implements DepTarget {
  constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {
    if ((this.vm = vm) && isRenderWatcher) {
      vm._watcher = this
    }
    if (options) {
      this.before = options.before
    }
  }
  addDep(dep: Dep) {
    ...
    dep.addSub(this)
  }
  update() {
    ...
    queueWatcher(this)
  }
  run() {
    ...
    this.cb.call(this.vm, value, oldValue) // cb 就是 updateComponent
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  • 响应式数据
Object.defineProperty(obj, key, {
  get: function reactiveGetter() {
    ...
    dep.depend() // 依赖收集
  },
  set: function reactiveSetter(newVal) {
    ...
    dep.notify() // 派发更新
  },
})
1
2
3
4
5
6
7
8
9
10
  • Dep
export default class Dep {
  addSub(sub: DepTarget) {
    this.subs.push(sub)
  }
  depend(info?: DebuggerEventExtraInfo) {
    ...
    Dep.target.addDep(this) //Dep.target 就是 Watcher
  }
  notify(info?: DebuggerEventExtraInfo) {
    ...
  for (let i = 0, l = subs.length; i < l; i++) {
    const sub = subs[i]
      sub.update()
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • queueWatcher
let waiting = false
export function queueWatcher(watcher: Watcher) {
  ...
  if (!waiting) {
    waiting = true
    nextTick(flushSchedulerQueue) // 异步任务 nextTick
  }
}
1
2
3
4
5
6
7
8
  • flushSchedulerQueue
function resetSchedulerState() {
  waiting = flushing = false
}
function flushSchedulerQueue() {
  flushing = true
  let watcher, id
  queue.sort(sortCompareFn)
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    watcher.before() // 生命周期函数 beforeUpdate
    watcher.run() // 更新模板
  }

  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
  cleanupDeps()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

结论

由此可知 beforeUpdate 通过 nextTick 异步执行

使用场景

可以处理 dom 更新前 相关的业务逻辑

# 2.2 updated

  • callUpdatedHooks
function callUpdatedHooks(queue: Watcher[]) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    callHook(vm, "updated") // 生命周期函数
  }
}
1
2
3
4
5
6
7
8

结论

由此可知 Vue 的 updated 钩子是异步的。Vue 在 DOM 更新完成后,会通过异步队列来执行 updated 钩子函数,但不保证它会在 DOM 更新完成后的下一个事件循环中同步触发。这意味着,如果你在 updated 钩子中需要访问更新后的 DOM 元素,你应该使用 this.$nextTick 来确保你的代码在 DOM 更新完成后执行

  • 一般不要操作数据 可能会导致死循环

使用场景

# 2.3 父子组件单独加载案例

刷新
全屏/自适应
  • 创建子组件

    组件类型 步骤 1 步骤 2 步骤 3 步骤 4 步骤 5 步骤 6 步骤 7 步骤 8
    父组件 parent-beforeUpdate parent-updated
    子组件 child-beforeCreate child-created child-beforeMount child-mounted child-activated

# 2.4 父子组件一起加载案例

刷新
全屏/自适应
  • 更新子组件数据

    组件类型 步骤 1 步骤 2 步骤 3 步骤 4
    父组件 parent-beforeUpdate parent-updated
    子组件 child-beforeUpdate child-updated

# 3.数据销毁

# 3.1 beforeDestroy

组件中使用 this.$destroy() 手动卸载组件

  Vue.prototype.$destroy = function () {
    ...
    callHook(vm, 'beforeDestroy')
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    vm.__patch__(vm._vnode, null)
  }
1
2
3
4
5
6
7
8
9

结论

由此可知 beforeDestroy 同步执行,在 beforeDestroy 生命钩子调用之前,所有实例都可以用

使用场景

# 3.2 destroyed

  Vue.prototype.$destroy = function () {
    ...
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    vm.__patch__(vm._vnode, null)
    callHook(vm, 'destroyed')
  }
1
2
3
4
5
6
7
8
9

结论

由此可知 destroyed 同步执行,在 destroyed 生命钩子调用之前,实列上的方法 监听都被移除掉,所有的子实例也会被销毁

使用场景

卸载定时器或者原生 js 相关方法

# 3.3 有 keep-alive 包裹案例

刷新
全屏/自适应
  • 销毁子组件

    组件类型 步骤 1 步骤 2 步骤 3
    父组件 parent-beforeUpdate parent-updated
    子组件 child-deactivated

# 3.4 无 keep-alive 包裹案例

这里使用 v-if 来销毁子组件

刷新
全屏/自适应
  • 销毁子组件

    组件类型 步骤 1 步骤 2 步骤 3 步骤 4
    父组件 parent-beforeUpdate parent-updated
    子组件 child-beforeDestroy child-destroyed

# 4.其他生命钩子

# 4.1 activated

const componentVNodeHooks = {
  insert(vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, "mounted")
    }
    if (vnode.data.keepAlive) {
      activateChildComponent(componentInstance, true /* direct */)
    }
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
export function activateChildComponent(vm: Component, direct?: boolean) {
  ...
  callHook(vm, "activated")
}
1
2
3
4

结论

由于在组件中使用 keep-alive,所以被包裹的组件不可能是根组件,组件如果执行 mounted,会在后面执行 activated

使用场景

缓存组件

# 4.2 deactivated

const componentVNodeHooks = {
  destroy(vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
export function deactivateChildComponent(vm: Component, direct?: boolean) {
  ...
  callHook(vm, "deactivated")
}
1
2
3
4

结论

由于在组件中使用 keep-alive,所以被包裹的组件不可能是根组件,组件 destroy 和 deactivated 是互斥的,既只能执行其中一个

使用场景

缓存组件

# 4.3 errorCaptured

export function handleError(err: Error, vm: any, info: string) {
  try {
    if (vm) {
      let cur = vm
      while ((cur = cur.$parent)) {
        const hooks = cur.$options.errorCaptured
        if (hooks) {
          for (let i = 0; i < hooks.length; i++) {
            try {
              const capture = hooks[i].call(cur, err, vm, info) === false
            } catch (e: any) {
            }
          }
        }
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

结论

当捕获一个来自子孙组件的错误时被调用

使用场景

捕获 vue 内部错误

# 5.案例分析

刷新
全屏/自适应
  • 结果

    组件类型 步骤 1 步骤 2 步骤 3 步骤 4
    父组件 beforeUpdate updated nextTick1 nextTick2

通过上述知识点的分析,mounted 由于是非根组件,不确定是否是同步执行,beforeUpdate、updated 异步执行

这里有两点要研究:1.此处的 mounted 是否是同步加载、2.beforeUpdate、updated 异步执行过程是否和 this.$nextTick 一致

# 5.1 mounted 同步异步

建立一个空项目,将上述代码放入App.vue中运行查看

  • 结果

    组件类型 步骤 1 步骤 2 步骤 3 步骤 4
    父组件 nextTick1 beforeUpdate updated nextTick2

这里可以确定组件是同步加载,符合预期

猜想案例中 mounted 是异步加载,所以组件是异步的,通过 () => import('./xxx.vue')的方式异步加载组件,发现和案例中的结果一致,所以可以得出结论

这里的组件异步加载的,mounted 是异步执行的

我们都知道实例化渲染watcher时/更新数据时,会触发传入的updateComponent函数,通过vm._render生成vnode,让后通过vm._updatevnode派发更新到dom

具体分析 renderupdate过程,可以了解其大概的流程

# 5.1.1 render 生成 vnode

  • createComponent











 


















export function createComponent(
  Ctor: typeof Component | Function | ComponentOptions | void,
  data: VNodeData | undefined,
  context: Component,
  children?: Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
    }
  }
  const name = getComponentName(Ctor.options) || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ""}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  return vnode
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  • resolveAsyncComponent














 









 





export function resolveAsyncComponent(
  factory: { (...args: any[]): any, [keye: string]: any },
  baseCtor: typeof Component
): typeof Component | void {
  const owner = currentRenderingInstance
  const owners = (factory.owners = [owner])
  const forceRender = (renderCompleted: boolean) => {
    for (let i = 0, l = owners.length; i < l; i++) {
      owners[i].$forceUpdate()
    }
  }
  const resolve = once((res: Object | Component) => {
    factory.resolved = ensureCtor(res, baseCtor)
    if (!sync) {
      forceRender(true)
    } else {
      owners.length = 0
    }
  })
  const res = factory(resolve, reject)

  if (isPromise(res)) {
    // () => Promise
    if (isUndef(factory.resolved)) {
      res.then(resolve, reject)
    }
  }
  return factory.loading ? factory.loadingComp : factory.resolved
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

执行res.then,通过forceRender强制更新组件

  • createAsyncPlaceholder
export function createAsyncPlaceholder(
  factory: Function,
  data: VNodeData | undefined,
  context: Component,
  children: Array<VNode> | undefined,
  tag?: string
): VNode {
  const node = createEmptyVNode()
  node.asyncFactory = factory
  node.asyncMeta = { data, context, children, tag }
  return node
}
1
2
3
4
5
6
7
8
9
10
11
12
  • $forceUpdate



 



Vue.prototype.$forceUpdate = function () {
  const vm: Component = this
  if (vm._watcher) {
    vm._watcher.update()
  }
}
1
2
3
4
5
6

通过渲染watcher上的update方法更新组件

  • watcher



 



export default class Watcher implements DepTarget {
  update() {
    ...
    queueWatcher(this)
  }
}
1
2
3
4
5
6
  • queueWatcher





 



let waiting = false
export function queueWatcher(watcher: Watcher) {
  ...
  if (!waiting) {
    waiting = true
    nextTick(flushSchedulerQueue) // 异步任务 nextTick
  }
}
1
2
3
4
5
6
7
8
  • flushSchedulerQueue
function resetSchedulerState() {
  waiting = flushing = false
}
function flushSchedulerQueue() {
  flushing = true
  let watcher, id
  queue.sort(sortCompareFn)
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    watcher.before() // 生命周期函数 beforeUpdate
    watcher.run() // 更新模板
  }

  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

可以看到异步任务通过nextTick异步队列就开始执行了

# 5.1.2 patch 到 dom

  • patch
return function patch(oldVnode, vnode, hydrating, removeOnly) {
  ...
  createElm(vnode, insertedVnodeQueue)
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
}
1
2
3
4
5

invokeInsertHook 调用 mounted

  • createElm
function createElm() {
  ...
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
}
1
2
3
4
5
6
  • createComponent
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef((i = i.hook)) && isDef((i = i.init))) {
      i(vnode, false /* hydrating */)
    }
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • componentVNodeHooks
const componentVNodeHooks = {
  init(vnode: VNodeWithData, hydrating: boolean): boolean | void {
    ...
      const child = (vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      ))
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  },
}
1
2
3
4
5
6
7
8
9
10

createComponentInstanceForVnode 相当于 new Vue

child.$mount 手动挂载由于不是根组件,所以不会走 mounted,只走 beforeMount

  • createComponentInstanceForVnode
export function createComponentInstanceForVnode(): Component {
  ...
  return new vnode.componentOptions.Ctor(options)
}
1
2
3
4
  • invokeInsertHook
function invokeInsertHook(vnode, queue, initial) {
  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])
    }
  }
}
1
2
3
4
5
6
7
8
9
  • insert
const componentVNodeHooks = {
  insert(vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, "mounted")
    }
  },
}
1
2
3
4
5
6
7
8
9

到此为止,父组件的挂载结束,同步代码执行完毕

上述render阶段产生的异步任务开始执行

# 5.1.3 $forceUpdate

从上述流程中可以看到,通过$forceUpdate可以更新组件,并且时放到$nextTick中进行异步更新的,此时触发更新的主体还是在父组件中,放入任务队列中的渲染watcher还是父组件

更新组件走更新逻辑,所以父组件上会触发beforeUpdateupdated两个生命周期(可以自己建一个空项目验证,父组件下有一个异步组件,父组件未更新数据却会触发更新的生命周期)

  • 父组件更新时:render 阶段

createComponent 之前初始化时由于是异步组件,走异步组件加载逻辑resolveAsyncComponent,现在可以拿到数据了走Ctor = baseCtor.extend(Ctor as typeof Component)逻辑





 




  Vue.extend = function (extendOptions: any): typeof Component {
    ...
    const Super = this
    const Sub = function VueComponent(this: any, options: any) {
      this._init(options)
    } as unknown as typeof Component
    return Sub
  }
1
2
3
4
5
6
7
8

可以看到子组件通过extend继承到了_init,后续在有需要的地方通过new就可以走和new Vue一样的初始化逻辑

通过createComponent生成组件vnode后就进入了 patch 阶段

  • patch

    vm._update拿到vnode后,会在createElm通过createComponent执行组件上的生命周期init

const child = (vnode.componentInstance = createComponentInstanceForVnode(
  vnode,
  activeInstance
))
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
1
2
3
4
5

从上面可以知道这两步两个目的:

  • 1.实例化子组件:createComponentInstanceForVnode执行后会执行this._init
  • 2.挂载子组件:会生成渲染watcher

到此处已经进入了子组件内部逻辑处理部分了,后面的流程可以看作,一个同步组件的初次加载流程了,由于不是根组件所以步骤2这里只执行beforeMount,不执行mounted

mounted的执行在组件的生命周期insert中执行

insert(vnode: MountedComponentVNode) {
  ...
  callHook(componentInstance, 'mounted')
}
1
2
3
4

此时可以知道异步子组件和同步子组件不同之处有:(父组件为根组件、或同步组件)

  • 1.父组件没有更新操作,使用同步子组件父组件不会触发beforeCreatecreated这两个生命周期,但是用异步子组件时,父组件会触发这两个生命周期
  • 2.同步子组件在父组件中初始化时是同步渲染的,派发过程也是同步的;异步子组件在父组件中初始化时是异步渲染的,派发过程也是异步的,生命周期beforeCreatecreatedbeforeMountmounted都是异步执行的,且异步都是在nextTick中执行的

都按微任务理解,更新也是微任务,那么打印顺序应该是:nextTick1 nextTick2 beforeUpdate updated

但是真实结果和这个不一样,那么具体来分析下nextTick

# 5.2 $nextTick

  • nextTick
const callbacks: Array<Function> = [] // 关键点
let pending = false
function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
let timerFunc
const p = Promise.resolve()
timerFunc = () => {
  p.then(flushCallbacks)
}

export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e: any) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

分析可以知道 nextTick 原理就是开启一个队列,存储$nextTick的回调函数,同时开启微任务,与一般的微任务的区别就是这里的任务是放入一个队列中,同一段代码同步执行都会放入队列中

  mounted() {
    this.$nextTick(() => {
      logsData.push("nextTick1")
    })
    this.msg = "111"
    this.$nextTick(() => {
      logsData.push("nextTick2")
    })
  },
  beforeUpdate() {
    logsData.push("beforeUpdate")
  },
  updated() {
    logsData.push("updated")
  },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

异步组件挂载过程中是在微任务中执行mounted,任务都放在updatedQueue中,这个任务还没执行完,此时this.$nextTick开启了另一个微任务,任务不会放到updatedQueue中;this.msg = "111"触发更新操作,更新任务是放到updatedQueue中的,所以在这个微任务中会继续执行beforeUpdateupdated,然后才执行 nextTick1nextTick2

总结

通过上述案例可以了解到:

  • 1.同一组件内生命周期首次加载同步执行、数据更新异步加载、数据销毁同步加载
  • 2.异步组件会影响父组件的生命周期,异步组件内部自身执行逻辑顺序和同步时也是不一样的,特别是使用自带的nextTick时容易出现 bug