初始阶段
new Vue() 到created之间 的阶段叫做初始化阶段。
这个阶段的目的是在vue.js实例上初始化一些属性、事件以及响应式数据。如: props,methods,data,computed,watch,provide和injdect等。模板编译阶段
created钩子函数 与 beforeMount 钩子函数 之间的阶段 是模板编译阶段。
这个阶段的目的是 将模板编译为渲染函数,只存在于完整版中。
如果在职包含运行时的构建版本中 执行 new Vue(),则不会存在这个阶段。
当使用 vue-loader 或 vueify 时, *.vue 单文件 内部的 模板 template 会在构建时 预编译 成 javascript, 所有最终打好 的包里 是不需要编译器的(已经编译过了), 用运行时版本即可。挂载阶段
beforeMount钩子函数到 mounted钩子函数之间是挂载阶段。
这个阶段,Vue.js会 将实例 挂载到DOM元素上, 并在挂载过程中,开启 watcher 来 持续追踪依赖变化。
在已挂载的状态下,当数据(状态)变化时, watcher 会通知 虚拟DOM重新渲染视图, 并且会在渲染视图前 触发 beforeUpdate钩子函数,渲染完毕后触发 updated 钩子函数。
- 卸载阶段
应用调用vm.$destroy方法后, Vue.js的生命周期会进入卸载阶段。
这个阶段,Vue.js会将自身 从父组件 中删除, 取消实例上 所有 依赖的追踪 并且 移除所有的 事件监听器。
源码角度了解生命周期
生命周期整体上分为两个部分:
- 第一个部分: 初始化、模板编译、挂载阶段
- 第二个部分:卸载阶段
事实上 卸载阶段 其实就是 vm.$destroy方法 内部原理(https://www.jianshu.com/p/b4737801a416),这里就不重复说了。
new Vue() 被调用时发生了什么
new Vue()被调用时, 会进行一些初始化操作,然后进入模板编译阶段,最后进入挂载阶段。
源码-vue2/src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
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)
}
initMixin(Vue) // Vue构造函数的prototype属性会被添加_init方法
stateMixin(Vue) // vue构造函数 的prototype属性挂载与数据相关的实例方法。 vm.$watch, vm.$set, vm.$delete. 并且会挂载上 $data 和 $props
eventsMixin(Vue) // eventsMixin中 会实现 vm.$on vm.$off vm.$once vm.$emit这个四个方法。
lifecycleMixin(Vue) // vm.$forceUpdate,vm.$destroy vm._update
renderMixin(Vue) // vm.$nextTick vm._render 的实现
export default Vue
构造函数的逻辑很简单。 首先进行安全检查,然后调用 this._init(options) 来执行 生命周期的 初始化流程。
_init方法是 initMixin方法 中被挂载到Vue.js原型上的。
initMixin 方法 的实现 是在 Vue.prototype属性上添加了_init方法。
export function initMixin (Vue: Class) {
Vue.prototype._init = function (options?: Object) {
...
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
...
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')
...
// 如果用户 传入el 选项, 则自动开启模板编译阶段和挂载阶段
// 否则 不进入下一个生命周期 需要用户 执行 vm.$mount方法,手动开启编译和挂载阶段
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
vue.js会在初始化流程的不同时期通过callHook函数触发生命周期钩子。
在执行初始化流程之前, 实例上挂载了 $options
属性。 目的是 将用户的options 选项与 钩子函数的options属性 及其父级实例 钩子函数 的options合并赋值给 $options属性。
可以通过代码看到, 初始过程中, 首先 初始化事件与属性, 然后 触发生命周期钩子 beforeCreate. 随后 初始化 provide/inject 和状态(props、methods、 data、computed 、 以及 watch)。随后 触发 生命周期钩子 created. 最后 判断是否有el选项, 如果是 直接调用 vm.$mount
方法 ,进入后面 的生命周期阶段,否则等待 用户 执行 vm.$mount
方法之后 进入模板编译与挂载阶段。
callHook函数的内部原理
callHook的作用 是触发 用户设置的生命周期钩子,生命周期 钩子 会在 执行 new Vue()时 通过参数传递给 Vue.js.
用户传入的options 参数 最后会与 构造函数的 options合并 赋值给 vm.$options
属性, 我们就可以 得到用户设置 的生命周期函数了。例如: vm.$opionts.created 得到用户 设置 的created钩子函数。
vue.js在合并options 的过程中 会找出 options中所有key 是 钩子函数的名称 ,并将它转换为数组。
也就是说 vm.$options.cretead 是一个 数组。数组中 包含了钩子函数。
这里设置为数组的原因是:
Vue.mixin和 用户 实例化Vue.js时,如果设置了同一个 生命周期钩子,那么保存为数组后, 就可以在同一个 生命周期钩子中保存过得生命周期钩子了。
比如: Vue.mixin设置了生命周期钩子 mounted, new Vue时,参数中也设置了 mounted 生命钩子。 这时 ,vm.$options.mounted这个数组中,就会包括这 两个生命周期钩子。
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
// 获取全局当前 唯一的watcher
pushTarget()
// 尝试获取 对应 hook的函数 列表
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
// 遍历执行
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
// 删除targetStack从删除最后一个 targetStack.pop()
// Dep.target = targetStack[targetStack.length - 1]
popTarget()
}
参数:vm 是 vue.js实例 , hook 是生命周期钩子的名称
我们获取到对应生命周期钩子 的列表后 遍历执行每一个 钩子函数。
初始化实例属性
Vue的生命周期中,初始实例属性是第一步(initLifecycle向实例中挂载属性)。
实例化的属性既有 Vue.js内部使用 属性(vm._watcher),也有外部使用的属性(vm.$parent)
以$ 开头的属性 提供给用户使用的 外部属性, 以_开头的属性是 内部使用的内部属性。
vue通过initLifecycle函数向实例中挂载属性, 该函数接受vue.js实例作为参数。
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
// 找出第一个非抽象父类
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
逻辑就是在Vue.js实例上设置一下属性 并提供默认值。
注意 : vm.$parent 属性 需要找到 第一个 非抽象类型的父类。 (抽象组件与普通组件一样,只是它不会在界面上显示任何 DOM 元素。它们只是为现有组件添加额外的行为。 就像很多你已经熟悉的 Vue.js 的内置组件,比如:
)
另一个注意的是 vm.$children
属性,它会包含当前实例的直接子组件。该属性的值是从子组件中 主动添加 到父组件中的。 上面代码中的 parent.$children.push(vm)
, 就是将当前实例添加到父组件实例的$children
属性中。
最后一个 注意的是 vm.$root
。 当前组件的根Vue.js实例。 如果当前组件没有父组件,那么它就是根组件。 它的$root
就是自己。 然后它 的子组件的vm.$root
沿用父级的$root
, 孙组件的$root
沿用子组件的root 传递给每一个子组件。
初始化事件
实例初始化阶段,初始化的事件指的是 父组件 在模板 中 使用 v-on监听的子组件内触发的事件。
比如: v-on 写在 组件标签上 ,那么这事件会被注册到子组件vue.js事件系统中。 如果写在 平台标签如div上,那么事件会被注册到浏览器事件中。
initEvents函数 执行 初始化事件相关的逻辑
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// 初始化 父组件 附加的事件
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
vm上新增 _events属性初始化为 空对象,用来储存事件。 所有使用 vm.$on
注册的事件监听器 都会保存到 vm._events
属性中。
在模板编译阶段,当模板解析到组件标签时,会实例化子组件,同时将标签上注册的事件解析成 object 并 通过参数 传递 给 子组件。 所有当 子组件实例化时, 可以在参数中获取 父组件向 自己注册的事件, 这些 事件最终 会 保存在 vm.$options._parentListeners
updateComponentListeners方法,将父组件向子组件注册的事件注册到子组件的实例中。
let target: any
function add (event, fn) {
target.$on(event, fn)
}
function remove (event, fn) {
target.$off(event, fn)
}
function createOnceHandler (event, fn) {
const _target = target
return function onceHandler () {
const res = fn.apply(null, arguments)
if (res !== null) {
_target.$off(event, onceHandler)
}
}
}
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
add 新增事件 remove删除事件 createOnceHandler新增一次性事件
updateListeners函数的实现
export function updateListeners (
on: Object, // 需要执行的事件列表
oldOn: Object, // 旧的事件列表
add: Function, // 新增 函数
remove: Function, // 移除 函数
createOnceHandler: Function, // 一次性 执行事件
vm: Component // 实例
) {
let name, def, cur, old, event
// 循环 需要执行的事件列表
for (name in on) {
def = cur = on[name]
old = oldOn[name]
event = normalizeEvent(name)
/* istanbul ignore if */
if (__WEEX__ && isPlainObject(def)) {
cur = def.handler
event.params = def.params
}
// 判断事件名对应的值是否是 undefined和null。如果是调用控制台警告
if (isUndef(cur)) {
process.env.NODE_ENV !== 'production' && warn(
`Invalid handler for event "${event.name}": got ` + String(cur),
vm
)
}
// 判断事件名 是否 在oldOn中 ,如果不存在,则调用 add 注册事件
else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm)
}
if (isTrue(event.once)) {
// 一次性 事件
cur = on[name] = createOnceHandler(event.name, cur, event.capture)
}
add(event.name, cur, event.capture, event.passive, event.params)
}
// 如果 on和oldOn都存在,但是它们不相同,则将事件回调替换成 on 中的回调,并且把on 中的回调引用指向真实的事件系统中注册的事件,也就是 oldOn 中对应的事件
else if (cur !== old) {
old.fns = cur
on[name] = old
}
}
// 循环 旧 的事件列表
// 删除 不存在 的事件
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
}
该函数接收5个参数,分别是on,onldOn, add,remove,vm.
主要逻辑对比on和oldon来分辨哪些事件需要执行add注册事件,哪些事件需要执行remove删除事件。
上面代码一部分循环on,第二部分循环oldOn。 第一个部分主要作用是判断哪些事件在oldOn中不存在,调用add注册这些事件。 第二部分的作用是循环oldOn, 判断哪些事件在on中不存在,调用remove移除这些事件。
其中代码中 normalizeEvent 函数 的作用 是将事件修饰符解析出来。
Vue的模板中支持事件修饰符,例如capture,once,passive等,如果我们在模板中注册事件时使用了事件修饰符,那么在编译阶段解析标签上的属性时,会将这些修饰符改成对应的符号加上事件名前面。
例如: vm.$options._parentListeners
是这个样子 {~increment: function(){}}
const normalizeEvent = cached((name: string): {
name: string,
once: boolean,
capture: boolean,
passive: boolean,
handler?: Function,
params?: Array
} => {
const passive = name.charAt(0) === '&'
name = passive ? name.slice(1) : name
const once = name.charAt(0) === '~' // Prefixed last, checked first
name = once ? name.slice(1) : name
const capture = name.charAt(0) === '!'
name = capture ? name.slice(1) : name
return {
name,
once,
capture,
passive
}
})
代码中 如果修饰符 ,则会将它截取出来。最终 输出的对象中保存了事件名已经一些事件修饰符,这些修饰符为true说明事件使用了此事件修饰符。