Vue源码解读:05生命周期篇

1,255 阅读6分钟

Vue源码解读:05生命周期篇

目录

image.png

第一节·先看目录结构

├─instance                 # 实现vue实例的代码
│  ├─render-helpers   # render辅助代码
│  ├─events.js            # 事件初始化处理相关代码
│  ├─index.js              #
│  ├─init.js                  # 初始化代码
│  ├─inject.js              #
│  ├─lifecycle.js          # 生命周期相关代码
│  ├─proxy.js              # 代理相关代码
│  ├─render.js            # render渲染函数相关代码
│  └─state.js              #

第二节·Vue生命周期简述

我不太敢写什么是生能周期这个定义,总感觉压力很大,虽然只是笔记,哈哈哈。在学校上C语言的必修课的时候,似乎没听说过生命周期这个说话,但从java开始,很多技术栈,也不知道是个啥东东,就老爱有生命周期这个东东。反正不管啥,生命周期应该也许大概可能就是从开始到结束的过程,前提是支持周期的。了解生命周期的意义应在于可以帮助我们写出更优雅的代码,以及快速定位bug,而不是纠结于定义本身。知道vue的生命周期中都干了啥,啥时候干的,是我们了解vue生周期核心所在。

第一段就当做是日记,下面开始正经的笔记。vue实例开始创建直至被销毁的过程,称为vue的生命周期。vue的生命周期依次,主要经历初始化,模板编译,挂载,销毁四个阶段(官网vue实例生命期图示底部提示了一句话,模板编译在使用构造生成文件中,将提前执行。)。与vue生命周期密切相关的钩子函数有beforeCreate,created,beforeMount,mounted,beforeUpdate,updated,beforeDestroy,destroyed。vue的api文档上,一共有11个和生命周期有关的钩子函数,另外三个分别是activated,deactivated,最后一个是vue2.5.0新增的,叫errorCaptured。

那么来一张官方的生命周期图。

image.png

第三节·生命周期的几个阶段

1.生命周期中的几个阶段。

从官网的生命周期图和源码看,可以大致将vue的生命周期分为4个阶段,分别是初始化阶段,模板编译阶段,挂载阶段,销毁阶段。

初始化阶段:为vue实例初始化属性、事件、数据观测等。

模板编译阶段:将模板字符串编译成渲染函数。(该阶段在runtime版本中不存在,因为已经编译好了。)

挂载阶段:将vue实例挂载到指定dom上,即将模板渲染到真实dom中。

销毁阶段:解绑指令,移除事件监听器,销毁子实例。

2.初始化阶段

初始化阶段主要干了两件事,一是创建Vue实例,二是为实例初始化属性、事件、响应式数据。

从instance的入口文件index.js可以看到,Vue函数代码很少,核心代码就一行即调用实例的_init方法,如下:

//源码位置 src/instance/index.js

function Vue (options) {
    if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)
    ) {
        warn('Vue is a constructor and should be called with the `new` keyword')
    }
    this._init(options)
}


//源码位置 src/instance/init.js
而_init方法是在initMixin函数中被挂载到Vue原型上的。initMixin代码如下:

export function initMixin (Vue: Class<Component>) {
    Vue.prototype._init = function (options?: Object) {
        const vm: Component = this
        // a uid
        vm._uid = uid++

        let startTag, endTag
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            startTag = `vue-perf-start:${vm._uid}`
            endTag = `vue-perf-end:${vm._uid}`
            mark(startTag)
        }

        // a flag to avoid this being observed
        vm._isVue = true
        // merge options
        if (options && options._isComponent) {
            // optimize internal component instantiation
            // since dynamic options merging is pretty slow, and none of the
            // internal component options needs special treatment.
            initInternalComponent(vm, options)
        } else {
            vm.$options = mergeOptions(
                resolveConstructorOptions(vm.constructor),
                options || {},
                vm
            )
        }
        /* istanbul ignore else */
        if (process.env.NODE_ENV !== 'production') {
            initProxy(vm)
        } else {
            vm._renderProxy = vm
        }
        // expose real self
        vm._self = vm
        initLifecycle(vm) //初始化生命周期
        initEvents(vm) //初始化事件
        initRender(vm) //初始化渲染
        callHook(vm, 'beforeCreate') //触发beforeCreate钩子
        initInjections(vm) // resolve injections before data/props
        initState(vm) //初始化状态,例如props,methods,data,computed,watch
        initProvide(vm) // resolve provide after data/props
        callHook(vm, 'created') //触发created钩子

        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            vm._name = formatComponentName(vm, false)
            mark(endTag)
            measure(`vue ${vm._name} init`, startTag, endTag)
        }

        if (vm.$options.el) {
            vm.$mount(vm.$options.el) //调用$mount,进入下一阶段:挂载阶段或模板编译阶段
        }
    }
}

从源码中可以看到,在init初始化中,初始化了生命周期,事件,render渲染,触发beforeCreate钩子,实例状态(props,methods,data,computed,watch),触发created钩子等。从这里我们可以得到一个信息,created钩子的时候,实例已经被初始化,且props,methods,data,computed,watch可用;因为触发beforeCreate钩子在初始化实例状态之前,所以按理beforeCreate钩子中是无法使用实例的props,methods,data,computed,watch属性的。

//源码位置 src/instance/state.js

export function initState (vm: Component) {
    vm._watchers = []
    const opts = vm.$options
    if (opts.props) initProps(vm, opts.props)//初始化Props属性
    if (opts.methods) initMethods(vm, opts.methods)//初始化Methods属性
    if (opts.data) {
        initData(vm) //初始化data属性
    } else {
        observe(vm._data = {}, true /* asRootData */)
    }
    if (opts.computed) initComputed(vm, opts.computed) //初始化Computed计算属性
    if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)//初始化Watch监听属性
    }
}

关注一下initState(vm) 初始化状态,这个函数,里面核心代码如下,从中我们可以得到一个信息,vue实例的属性在初始化的时候的执行顺序依次是props,methods,data,computed,watch。

初始化阶段通过callHook触发的生命周期钩子是beforeCreate,created。

3.模板编译阶段

在初始化函数_init中,在调用一系列初始化函数后,即初始化工作完成后之后,在改该函数的末尾,调用了$mount,使vue生命周期进入下一个阶段。这里的下一个阶段是模板编译阶段,当然这个阶段不一定存在,有可能初始化阶段完成之后就直接跳到挂载阶段了。

模板编译阶段的主要内容是将模板字符串转换成render函数,在vue的某些版本比如vue.runtime.js,就不会有模板编译阶段。模板编译阶段的之所以有可能不存在,是因为某些情况下,模板字符串可能已经提前编译了,比如vue-loader就有预编译的功能。事实上,我们的日常开发中,使用.vue的文件内部的模板字符串,都是在构建的时候就已经预编译了。

4.挂载阶段

image.png

瞅一眼官方的生命周期图示,大概就可以知道挂载阶段主要工作一是创建创建Vue实例并用其替换el选项对应的DOM元素,二是开启数据状态的监控(如何监控的详看变化侦测篇)。挂载阶段有关的钩子函数有beforeMount,beforeUpdate,mounted,updated。

关于挂载阶段beforeMount,beforeUpdate,mounted的触发时机,可以看下lifecycle.js文件中的mountComponent函数,该函数是在进入挂载阶段时调用的。

// 源码位置 src/instance/lifecycle.js

export function mountComponent (
    vm: Component,
    el: ?Element,
    hydrating?: boolean
): Component {
    vm.$el = el
    if (!vm.$options.render) {
        vm.$options.render = createEmptyVNode
        if (process.env.NODE_ENV !== 'production') {
            /* istanbul ignore if */
            if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
                vm.$options.el || el) {
                warn(
                    'You are using the runtime-only build of Vue where the template ' +
                    'compiler is not available. Either pre-compile the templates into ' +
                    'render functions, or use the compiler-included build.',
                    vm
                )
            } else {
                warn(
                    'Failed to mount component: template or render function not defined.',
                    vm
                )
            }
        }
    }
    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 && !vm._isDestroyed) {
                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
}

5.销毁阶段

image.png

vue实例生命周期的最后一个阶段,销毁阶段,该阶段的主要工作是销毁,比如解绑指令,销毁子组件,取消依赖最终,销毁事件监听器。从上图可以看出,当调用vm.destroy()时进入销毁阶段。destroy()时进入销毁阶段。destroy函数的源码如下。

//源码位置 src/instance/lifecycle.js

Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
        return
    }
    callHook(vm, 'beforeDestroy') //触发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') //触发destroyed钩子
    // turn off all instance listeners.
    vm.$off() //解绑所有的实例监听器
    // remove __vue__ reference 
    //移除ref引用
    if (vm.$el) {
        vm.$el.__vue__ = null
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
        vm.$vnode.parent = null
        //将vue实例从父级中解绑
    }
}

由源码看到,在销毁阶段触发钩子函数有beforeDestroy,destroyed。beforeDestroy是在比较靠前的位置,这时vue实例还没被销毁,即是说vue实例还是可以正常使用的。我们想要在实例销毁前做的事情,可以再beforeDestroy钩子中写,比如销毁定时器等。

第四节·有关执行顺序

1.几个生命周期钩子的运行顺序,依次先后是beforeCreate,created,beforeMount,mounted,beforeDestroy,destroyed。beforeUpdate,updated的话,dom只要有更新,他们就会运行,可以确定的只能是beforeUpdate必定在updated之前运行。

2.几个内置属性的执行顺序依次是props,methods,data ,computed,watch。

3.父子组件中几个钩子的执行顺序。父组件是子组件的容器,从这角度理解,那么应当是父组件先创建,然后才能容纳子组件创建。而挂载渲染,子组件作为父组件一部分,父组件的挂载渲染完成,应当以子组件为前提。(特例除外)

所以 子mounted 先于父mounted执行,父created 先于 子created执行。

加载渲染:
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate 
-> 子created -> 子beforeMount -> 子mounted -> 父mounted
 
子组件更新:
父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
 
父组件更新:
父beforeUpdate -> 父updated
 
销毁:
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed

第五节·几个生命周期钩子

// 源码位置 src/shared/constants.js
// 2.6.11版本的vue

export const LIFECYCLE_HOOKS = [
    'beforeCreate',
    'created',
    'beforeMount',
    'mounted',
    'beforeUpdate',
    'updated',
    'beforeDestroy',
    'destroyed',
    'activated',
    'deactivated',
    'errorCaptured',
    'serverPrefetch'
]

研究vue版本是2.6.11,如上代码,我们可以看到一共注册了12钩子。

serverPrefetch:最后一个serverPrefetch不了解,我猜是和vue支持ssr服务端渲染有关。

errorCaptured:倒数第二个errorCaptured是在2.5.0+ 新增,官网关于它的介绍是:

当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:

错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。

此钩子可以返回 false 以阻止该错误继续向上传播。

activated:被 keep-alive 缓存的组件激活时调用。(在服务器端渲染期间不被调用)

deactivated:被 keep-alive 缓存的组件停用时调用。(在服务器端渲染期间不被调用)

beforeCreate:初始化阶段,vue实例的挂载元素$el和数据对象data都为undefined,还不可用。

created:初始化阶段,data,methods已经初始化,可用。$el不可用。

beforeMount:挂载阶段,实例挂载前,此时按理el还不可用,但el还不可用,但el已经初始化,dom节点还是虚拟dom节点。

mounted:挂载阶段,此时实例已经完成挂载,$el可用。

beforeUpdate:有更新才会触发,data被修改时,dom更新前触发。

updated:有更新才会触发,dom更新时触发。

beforeDestroy:销毁阶段,实例销毁前执行,此时vue实例还可正常使用。

destroyed:销毁阶段,据官网实例销毁后调用。此时this指向的实例还存在,但是子实例,监听器等已经被销毁。

我做了一个简单的打印验证,打印结果和代码如下:

<template>
    <div>
        <div>{{name}}</div>
        <div @click="to">跳转到别的页面</div>
        <tab-bar-bottom ref="child"></tab-bar-bottom>
    </div>
</template>

<script>
    export default {
    name: "withdrawal",
    methods:{
    to(){
    this.$router.push({path:'/'}).then()
}
},
    data() {
    return {
    name: 'hello world'
}
},
    beforeCreate() {
    console.log('==============' + 'beforeCreated' + '===================')
    console.log(this.$el) //undefined
    console.log(this.$data) //undefined
    console.log(this.name) //undefined
    console.log(this.$refs.child) //undefined
},
    created() {
    console.log('==============' + 'created' + '===================')
    console.log(this.$el) //undefined
    console.log(this.$data) //有值
    console.log(this.name) //有值
    console.log(this.$refs.child) //undefined

},
    beforeMount() {
    console.log('==============' + 'beforeMount' + '===================')
    console.log(this.$el) //undefined
    console.log(this.$data) //有值
    console.log(this.name) //有值
    console.log(this.$refs.child) //undefined
},
    mounted() {
    console.log('==============' + 'mounted' + '===================')
    console.log(this.$el) //有值
    console.log(this.$data) //有值
    console.log(this.name) //有值
    console.log(this.$refs.child) //有值
},
    beforeUpdate() {
    console.log('==============' + 'beforeUpdate' + '===================')
    console.log(this.$el) //点击更新name后进入,且有值
    console.log(this.$data) //点击更新name后进入,且有值
    console.log(this.name) //点击更新name后进入,且有值
    console.log(this.$refs.child) //点击更新name后进入,且有值
},
    updated() {
    console.log('==============' + 'updated' + '===================')
    console.log(this.$el) //点击更新name后进入,且有值
    console.log(this.$data) //点击更新name后进入,且有值
    console.log(this.name) //点击更新name后进入,且有值
    console.log(this.$refs.child) //点击更新name后进入,且有值
},
    beforeDestroy() {
    console.log('==============' + 'beforeDestroy' + '===================')
    console.log(this.$el) //有值
    console.log(this.$data) //有值
    console.log(this.name) //有值
    console.log(this.$refs.child) //有值
    console.log(this) //有值
},
    destroyed() {
    console.log('==============' + 'destroyed' + '===================')
    console.log(this.$el) //有值
    console.log(this.$data) //有值
    console.log(this.name) //有值
    console.log(this.$refs.child) //undefined
    console.log(this) //有值
}
}
</script>

image.png

第六节·篇章小结

本篇章小结如下:

①1个定义:vue实例开始创建直至被销毁的过程,称为vue的生命周期。

②4个阶段:介绍了初始化、模板编译、挂载、销毁四个阶段。

③12个钩子:简单介绍了12个钩子函数:beforeCreate,created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy,destroyed, activated,deactivated, errorCaptured, serverPrefetch。

④2个排序:一是几个钩子执行顺序依次为beforeCreate,created,beforeMount,mounted,beforeDestroy,destroyed。beforeUpdate,updated则是dom有要更新才会出发。二是vue实例几个属性的执行顺序依次是props,methods,data ,computed,watch。