电梯
Vue 源码
持续更新中...
Vue Router 4 源码
源码拉取步骤可以看这篇文章有讲解,下面直接进入正题:
前言
我们在 源码篇 使用及分析 Vue 全局 API 这篇文章中介绍了 Vue 的全局 API,除此之外 Vue 还有选项式 API 和组合式 API,其中包含了响应式 API、生命周期钩子等等......
那么在本篇我们将围绕 Vue2 和 Vue3 的生命周期源码展开。正文开始前,我们先来看官网给出的生命周期图示:

我们知道 Vue2 的生命周期流程是
beforeCreate ↓ created ↓ beforeMount ↓ mounted ↓ beforeUpdate ↓ updated ↓ beforeDestroy ↓ destroyedVue2 主要使用 Options API(选项式 API),Vue3 保留了 Options API,所以我们在 Vue2 项目中这样写:
javascriptexport default { mounted() {} }在 Vue3 中也这样写,仍然也是生效的:
javascriptexport default { mounted() {} }不同的是,在销毁阶段,生命周期钩子的名称变了:
Vue2 Vue3 beforeDestroy beforeUnmount destroyed unmounted 并且新增了两个钩子,用于调试响应式依赖:
javascriptrenderTracked() renderTriggered()除此之外,Vue3 运用了 Composition API(组合式 API),不再把生命周期写成对象方法,在 setup() 中注册:
javascriptimport { onMounted } from 'vue' export default { setup() { onMounted(() => {}) } }或者另一种写法:
javascript<script setup> import { onMounted } from 'vue' onMounted(() => {}) </script>还有一个最大的改动点就是去掉了 beforeCreate 和 created,因为 setup() 的执行时机就在 beforeCreate 和 created 之间,也就是这样:
javascriptbeforeCreate ↓ setup() ↓ createdVue 官方直接把大量逻辑搬到了 setup(),这里也有一个需要注意的点,如果我们在 setup() 中去获取某个元素是拿不到的,因为此时元素还没挂载:
javascriptsetup() { console.log(document.querySelector('.box')) }此时拿到的就是 null
但如果我们在 onMounted 中访问:
javascriptonMounted(() => { console.log(document.querySelector('.box')) })此时拿到的就是
html<div class="box"></div>那么我们来对比一下三者的生命钩子命名情况:
Vue2 Options API Vue3 Options API Vue3 Composition API beforeCreate beforeCreate setup() created created setup() beforeMount beforeMount onBeforeMount mounted mounted onMounted beforeUpdate beforeUpdate onBeforeUpdate updated updated onUpdated beforeDestroy beforeUnmount onBeforeUnmount destroyed unmounted onUnmounted activated activated onActivated deactivated deactivated onDeactivated errorCaptured errorCaptured onErrorCaptured 我们将 Vue 的生命周期总结为4个核心阶段:
- 初始化阶段:创建组件实例,并初始化组件的配置项,建立响应式系统
- → 模板编译阶段:编译模板生成渲染函数
- → 挂载阶段:执行渲染函数生成虚拟 DOM,并转换为真实 DOM 挂载到页面
- → 销毁阶段:卸载组件实例,解除数据监听和事件绑定
正文
接下来我们进入正文部分,在 Vue 的初始化阶段具体干了哪些事情呢?
初始化阶段
new Vue()
源码位置:src/core/instance/index.js
javascript
function Vue(options) {
this._init(options)
}
这里只做了一件事:
javascript
new Vue(options)
这个时候会执行:
javascript
this._init(options)
但是此时:
javascript
Vue.prototype._init
这个原型是在哪里定义的呢?
紧接着源码:
javascript
initMixin(Vue)
这里就是在给 Vue 原型挂方法
源码位置:src/core/instance/init.js
将 initMixin 函数简化一下:
javascript
export function initMixin (Vue) {
Vue.prototype._init = function (options) {
const vm = this // 保存当前实例
vm.$options = mergeOptions( // 合并配置
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
vm._self = vm // 保存自身引用
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件系统
initRender(vm) // 初始化渲染上下文
callHook(vm, 'beforeCreate') // 调用生命周期钩子 beforeCreate
initInjections(vm) // 初始化依赖注入
initState(vm) // 初始化状态(props,methods,data,computed,watch等)
initProvide(vm) // 初始化 provide
callHook(vm, 'created') // 调用生命周期钩子 created
if (vm.$options.el) { // 如果用户传入了el: '#app',则执行 $mount 进入模板编译和挂载阶段
vm.$mount(vm.$options.el)
}
}
}
mergeOptions
它有三个入参,简化为(parent, child, vm),其中的 parent 有这样的一段处理逻辑:
- resolveConstructorOptions 用于找到当前构造函数上"真正有效的选项",我们举个例子:
假如我现在是 Vue 本身
javascriptnew Vue({ data: { msg: 'hello' } })那么此时构造函数就是 Vue,那么 Vue.super 不存在(没有父类),直接返回 Vue.options(通过 Vue.mixin() 等全局 API 挂上去的东西)
但假如我现在是 Vue.extend 生成的子类
javascriptconst Sub = Vue.extend({ created() { console.log('子类') } }) new Sub({ data: { msg: 'hello' } })那么此时构造函数是 Sub,那么 Sub.super === Vue(有父类),这个时候就要检查父类的选项有没有变过,可为什么父类的选项会变?因为:
javascriptconst Sub = Vue.extend({ ... }) // 这里改了 Vue 的全局选项 Vue.mixin({ created() { console.log('后来加的') } }) // 现在 new Sub,Sub 需要感知到父类变了 new Sub()检查的逻辑:
递归拿到父类的最新选项:resolveConstructorOptions(Vue) → 得到 Vue.options
跟当初快照对比:Vue.options !== Sub.superOptions?
- 如果父类变了就重新合并:把父类的新选项和子类自己的选项(Sub.extendOptions)重新合并一次,更新 Sub.options
- 如果父类没变,直接返回 Sub.options
接下来就是合并父选项与子选项:
源码位置:src/core/util/options.js
- 这里就需要预处理 child 选项:
javascript
// 1. 如果 child 是函数,取其 options 属性(Vue.extend 场景)
if (isFunction(child)) {
child = child.options
}
// 2. 规范化 props:数组形式 → 对象形式
// ['name', 'age'] → { name: { type: null }, age: { type: null } }
normalizeProps(child, vm)
// 3. 规范化 inject:数组形式 → 对象形式
// ['foo'] → { foo: { from: 'foo' } }
normalizeInject(child, vm)
// 4. 规范化 directives:函数形式 → 对象形式
// { vFocus(el) {} } → { vFocus: { bind: fn, update: fn } }
normalizeDirectives(child)
- 再处理 extends 和 mixins(递归合并)
javascript
if (!child._base) { // _base 标记说明已经合并过,避免重复处理
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm) // 递归
}
if (child.mixins) {
for (const mixin of child.mixins) {
parent = mergeOptions(parent, mixin, vm) // 递归
}
}
这里有一个精妙的设计:extends 和 mixins 不是与 child 合并,而是先与 parent 合并,把 parent 逐步丰富,最后统一与 child 合并。确保了优先级:child > mixins > extends > parent
- 接着逐字段合并
javascript
const options = {}
// 遍历 parent 和 child 的所有 key
for (key in parent) mergeField(key)
for (key in child) {
if (!hasOwn(parent, key)) mergeField(key)
}
function mergeField(key) {
const strat = strats[key] || defaultStrat // 查找对应策略
options[key] = strat(parent[key], child[key], vm, key) // 执行合并
}
此处的 mergeField 按照 key 执行不同的合并策略(设计模式中的策略模式)
策略1:生命周期钩子 --- 合并为数组
这里我们着重分析,请看源码:
javascriptexport const LIFECYCLE_HOOKS = [ 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', 'activated', 'deactivated', 'errorCaptured', 'serverPrefetch', 'renderTracked', 'renderTriggered' ] LIFECYCLE_HOOKS.forEach(hook => { strats[hook] = mergeLifecycleHook }) function mergeLifecycleHook(parentVal, childVal) { const res = childVal ? parentVal ? parentVal.concat(childVal) // parent和child定义了相同的钩子函数:拼接数组 : isArray(childVal) ? childVal // 仅 child,且已是数组 : [childVal] // 仅 child,转为数组 : parentVal // 仅 parent return res ? dedupeHooks(res) : res // 去重 }为什么要把相同的钩子函数合并成数组呢?
因为 Vue.mixin(前面的篇章讲到它可以向 Vue 实例混入自定义行为) 和 用户在实例化 Vue 的时候,如果设置了同一个钩子函数,那么触发这个钩子函数的时候就都会执行
javascript// Vue.options (parent): { created: [fn_from_global_mixin] } // 组件选项 (child): { created: function() { ... } } // 合并结果: { created: [fn_from_global_mixin, fn_self] } // callHook 遍历数组依次执行
------ 以下是扩展内容,可以酌情看 ------
策略2:data --- 深度合并
javascriptstrats.data = function(parentVal, childVal, vm) { if (!vm) { // Vue.extend 合并阶段:两者都应该是函数 // 返回一个新函数,调用时合并两者的结果 return mergeDataOrFn(parentVal, childVal) } // 实例化合并阶段(有 vm) return mergeDataOrFn(parentVal, childVal, vm) }mergeDataOrFn 返回一个合并函数,在实例化时才执行:
javascriptfunction mergedInstanceDataFn() { const instanceData = childVal.call(vm, vm) // 调用子 data() const defaultData = parentVal.call(vm, vm) // 调用父 data() return mergeData(instanceData, defaultData) // 深度合并对象 }mergeData 递归合并两个对象,子值优先:
javascriptfunction mergeData(to, from) { for (const key of keys) { if (key === '__ob__') continue // 跳过观察者 const toVal = to[key] const fromVal = from[key] if (!hasOwn(to, key)) { set(to, key, fromVal) // to 没有 → 直接用 from 的 } else if (两者都是纯对象) { mergeData(toVal, fromVal) // 都是对象 → 递归合并 } // to 有且不是对象 → 保留 to 的(子优先) }策略 3:watch --- 合并为数组
javascriptstrats.watch = function(parentVal, childVal) { if (!childVal) return Object.create(parentVal || null) if (!parentVal) return childVal const ret = {} extend(ret, parentVal) // 先拷贝父 for (const key in childVal) { let parent = ret[key] const child = childVal[key] if (parent && !isArray(parent)) parent = [parent] ret[key] = parent ? parent.concat(child) : isArray(child) ? child : [child] } return ret }同一个 key 的 watcher 不会覆盖,而是合并为数组,全部生效
策略 4:props/methods/inject/computed --- 子覆盖父
javascriptstrats.props = strats.methods = strats.inject = strats.computed = function(parentVal, childVal) { if (!parentVal) return childVal const ret = Object.create(null) // 以 parent 为原型 extend(ret, parentVal) // 先拷贝父 if (childVal) extend(ret, childVal) // 子覆盖同名属性 return ret }methods 和 computed 同名时子覆盖父,这就是为什么子组件的 method 会覆盖 mixin 的同名 method
策略 5:provide --- 类似 data 的合并
javascriptstrats.provide = function(parentVal, childVal) { if (!parentVal) return childVal return function() { const ret = Object.create(null) mergeData(ret, isFunction(parentVal) ? parentVal.call(this) : parentVal) if (childVal) { mergeData(ret, isFunction(childVal) ? childVal.call(this) : childVal, false) } return ret }策略 6:assets (components/directives/filters) --- 原型链继承
javascriptfunction mergeAssets(parentVal, childVal) { const res = Object.create(parentVal || null) // 父作为原型 if (childVal) return extend(res, childVal) return res }用原型链实现查找链:子组件找不到的 asset 会沿原型链到父级查找,这就是为什么全局注册的组件在所有地方都能用
策略 7:默认策略 --- 子覆盖父
javascriptconst defaultStrat = function(parentVal, childVal) { return childVal === undefined ? parentVal : childVal }适用于 el、template、render 等普通选项,子有就用子的
callHook
源码位置:src/core/instance/lifecycle.js
callHook() 可以理解为负责执行指定生命周期钩子的统一入口函数,比如说:
javascript
callHook(vm, 'beforeCreate')
最终会执行我们编写的逻辑:
javascript
beforeCreate() {
console.log('beforeCreate')
}
源码中的这个部分:
javascript
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, args || null, vm, info)
}
}
invokeWithErrorHandling(...) 等价于 handlersi.call(vm),只是 Vue 多包了一层异常处理
因此源码可以简化为:
javascript
export function callHook (vm: Component, hook: string) {
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`)
}
}
}
}
这里有一个容易疑惑的地方,为什么 handlers 是数组?
beforeCreate() {} 明明是一个函数,但在 mergeOptions() 合并配置的时候会变成数组
源码位置:
src/core/instance/lifecycle.js
src/core/instance/events.js
src/core/instance/render.js
src/core/instance/inject.js
src/core/instance/state.js
initLifecycle
javascript
export function initLifecycle(vm) {
const options = vm.$options
// ① 找到第一个"非抽象"父组件,把自己挂上去
let parent = options.parent
if (parent && !options.abstract) {
// 如果当前组件不是抽象组件并存在父级,通过while循环继续向上循环;如果当前组件的父级是抽象组件并存在父级,继续向上查找直到找到第一个不是抽象类型的父级时,将其赋值
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent // 跳过 keep-alive 等抽象组件
}
parent.$children.push(vm) // 自己加入父的 $children
}
// ② 建立引用关系
vm.$parent = parent
vm.$root = parent ? parent.$root : vm // 没有父级自己就是根,否则就找到祖先为根
// ③ 初始化各种状态标记
vm.$children = []
vm.$refs = {}
vm._provided = parent ? parent._provided : Object.create(null)
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false // 还没挂载
vm._isDestroyed = false // 还没销毁
vm._isBeingDestroyed = false // 没在销毁中
}
为什么跳过抽象组件? keep-alive、transition 这类组件是"抽象"的,它们不出现在组件树里。所以子组件的 $parent 应该跳过它们,直接指向业务父组件:
html<keep-alive> ← 抽象组件,跳过 <MyComponent/> ← $parent 指向 keep-alive 的父级,而不是 keep-alive </keep-alive>
initEvents
javascript
export function initEvents(vm) {
vm._events = Object.create(null) // 清空事件池
vm._hasHookEvent = false // 优化标记:有没有人监听 hook:xxx
// 处理父组件通过 @xxx 传进来的事件监听
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
核心就一个问题:_parentListeners 从哪来?
当父组件使用子组件时:
html<Child @click="handleClick" @update="handleUpdate" />编译后会变成:
javascript{ on: { click: handleClick, update: handleUpdate } }这些在创建子组件 VNode 时被存为 vnodeComponentOptions.listeners,然后通过 initInternalComponent 赋给 vm.$options._parentListeners;
updateComponentListeners 内部用 updateListeners 对比新旧监听器,增删事件,最终调的是 on / off:
javascriptfunction add(event, fn) { target.$on(event, fn) } function remove(event, fn) { target.$off(event, fn) }_hasHookEvent 这个标记很巧妙------只有当有人用 @hook:mounted 这种方式监听生命周期时才设为 true,这样 callHook 可以跳过 $emit('hook:xxx') 的调用,省性能
initRender
javascript
export function initRender(vm) {
vm._vnode = null // 还没有子树 VNode
vm._staticTrees = null // v-once 缓存
const parentVnode = vm.$vnode = vm.$options._parentVnode // 父组件中的占位 VNode
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext) // 解析插槽
vm.$scopedSlots = parentVnode
? normalizeScopedSlots(vm.$parent, parentVnode.data.scopedSlots, vm.$slots)
: emptyObject
// 两个 createElement 的区别:最后一个参数不同
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) // 模板编译用
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) // 手写 render 用
// $attrs 和 $listeners 做成响应式
// 这样 HOC(高阶组件)用了它们,父组件变化时能自动更新
defineReactive(vm, '$attrs', (parentData && parentData.attrs) || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
三个关键点:
1.vm._c vs vm.createElement:模板编译出的 render 函数用 _c(不需要手动 normalize children),用户手写的 render 函数用 createElement(总是 normalize)。这就是为什么手写 render 时写 h('div', child) 而模板不用操心格式
2.attrs / listeners 为什么要响应式? 因为高阶组件经常这样用:
javascript// 高阶组件透传 render(h) { return h(Child, { attrs: this.$attrs, // 父组件的 attrs 变了,我要重新渲染 on: this.$listeners // 父组件的 listeners 变了,我要重新渲染 }) }如果不响应式,父组件传新属性进来,高阶组件不会更新
3.$vnode vs _vnode:
- $vnode:父组件中自己的占位节点(即 <MyComp/> 编译出的 VNode)
- _vnode:自己渲染出的根节点(即 render() 返回的 VNode)
initInjections
provide/inject 用法回顾
在分析下面这部分源码之前,先插播一段 provide/inject 的用法分析,以便更好地理解。
1.为什么会有 provide/inject?
假设组件结构:
App └─ Layout └─ Sidebar └─ Menu └─ MenuItem如果 MenuItem 需要访问 App 中的数据:
html<App> ↓ props <Layout> ↓ props <Sidebar> ↓ props <Menu> ↓ props <MenuItem>中间组件可能根本不需要这些数据,这就叫 属性钻孔(Props Drilling)
Vue 提供 provide() 和 inject() 来实现跨层访问:
htmlApp └─ provide MenuItem └─ inject2.Vue 2 的写法
javascript// Provider export default { provide() { return { theme: 'dark' } } }
javascript// Consumer export default { inject: ['theme'] } // 使用 console.log(this.theme) // 结果输出:dark3.Vue3的写法
javascript// Provider <script setup> import { provide } from 'vue' provide('theme', 'dark') </script>子组件去使用:
javascript// Consumer <script setup> import { inject } from 'vue' const theme = inject('theme') </script>4.源码分析
provide --- 向上提供数据
javascriptexport function provide(key, value) { if (!currentInstance) { // 不在 setup() 中调用 } else { resolveProvided(currentInstance)[key] = value } }
- 核心就一行:拿到当前实例的 _provided 对象,把 key-value 存进去
resolveProvided --- 惰性创建 provides 对象
- 惰性创建 = 用到时才创建,不用就不创建(在 resolveProvided 里体现在:组件实例化时并不马上创建自己的 _provided 对象,而是直接引用父级的)
javascriptexport function resolveProvided(vm) { const existing = vm._provided const parentProvides = vm.$parent && vm.$parent._provided if (parentProvides === existing) { return (vm._provided = Object.create(parentProvides)) } else { return existing } }这里用原型链实现了 provide 的继承与覆盖:
在 initLifecycle 方法中有如图这样一句代码,可以用来解释下面的第一点:
- 初始状态:实例创建时 vm._provided = vm.$parent._provided(直接引用父级的 provides)
- 首次 provide 时:existing === parentProvides 成立 → 用 Object.create(parentProvides) 创建一个以父级 provides 为原型的新对象,赋值给 vm._provided
- 后续 provide:existing !== parentProvides → 直接返回已有对象
效果:
- 子组件可以读取所有祖先提供的值(原型链向上查找)
- 子组件的 provide 不会污染父级(创建了新对象)
- 同名 key 子组件的值会覆盖父级的值(原型链遮蔽)
inject --- 向下注入数据
javascriptexport function inject(key, defaultValue, treatDefaultAsFactory = false) { const instance = currentInstance if (instance) { const provides = instance.$parent && instance.$parent._provided if (provides && key in provides) { return provides[key] } else if (arguments.length > 1) { return treatDefaultAsFactory && typeof defaultValue === 'function' ? defaultValue.call(instance) : defaultValue } } }查找流程:
- 取 instance.$parent._provided --- 拿到直接父组件的 provides 对象
- 用 in 操作符查找 key --- in 会沿原型链向上查找,所以能找到所有祖先 provide 的值
- 找不到则返回默认值:
- treatDefaultAsFactory 为 true 且默认值是函数 → 当工厂函数调用 defaultValue.call(instance)
- 否则直接返回 defaultValue
我们可以得到这样一个数据流:
javascriptRoot ─ provide('theme', 'dark') → _provided = { theme: 'dark' } │ ├── Child ─ provide('theme', 'light') → _provided = Object.create(parent) │ { theme: 'light' } (遮蔽) │ │ │ └── GrandChild ─ inject('theme') → 查 $parent._provided │ → 'light' (原型链找到 Child 的) │ └── Child2 ─ inject('theme') → 查 $parent._provided → 'dark' (原型链找到 Root 的)值得注意的点
inject 只看 $parent._provided:不是看自己的,而是看父组件的。保证 provide/inject 是跨层级的,组件不能 inject 自己 provide 的值
好了,接下来我们回归 initInjections 的源码
javascript
export function initInjections(vm) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false) // 关闭响应式追踪
Object.keys(result).forEach(key => {
defineReactive(vm, key, result[key]) // 定义到 vm 上
})
toggleObserving(true)
}
resolveInject 的查找逻辑:
javascript
function resolveInject(inject, vm) {
const result = Object.create(null)
for (const key in inject) {
const provideKey = inject[key].from // 从哪个 key 取值
// 沿着 _provided 向上找
if (provideKey in vm._provided) {
result[key] = vm._provided[provideKey]
} else if ('default' in inject[key]) {
// 找不到就用默认值
result[key] = isFunction(provideDefault)
? provideDefault.call(vm)
: provideDefault
}
}
return result
}
为什么 toggleObserving(false)?
因为 inject 的值来自 provide,它本身就是响应式的(provide 那边已经 observe 过了)。如果这里再深度 observe 一次,不仅浪费,还可能产生重复的 Observer。所以只做浅层 defineReactive,不深度遍历
为什么 inject 在 data 之前?
因为 data 里可能用到 inject 的值:
javascriptexport default { inject: ['apiUrl'], data() { return { url: this.apiUrl // 依赖 inject 的值 } }
initState
javascript
export function initState(vm) { // 初始化全部响应式状态
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) // ① props
initSetup(vm) // ② setup (Composition API)
if (opts.methods) initMethods(vm, opts.methods) // ③ methods
if (opts.data) initData(vm) // ④ data
if (opts.computed) initComputed(vm, opts.computed) // ⑤ computed
if (opts.watch) initWatch(vm, opts.watch) // ⑥ watch
}
- 注意:这个顺序不是随意的,有明确的依赖关系
1.initProps --- 校验 + 响应式 + 代理
javascriptfunction initProps(vm, propsOptions) { const propsData = vm.$options.propsData || {} const props = vm._props = shallowReactive({}) // 用浅响应式对象存 const keys = vm.$options._propKeys = [] const isRoot = !vm.$parent if (!isRoot) { toggleObserving(false) } for (const key in propsOptions) { keys.push(key) const value = validateProp(key, propsOptions, propsData, vm) // 校验类型+默认值 defineReactive(props, key, value, undefined, true /* shallow */) if (!(key in vm)) { proxy(vm, `_props`, key) // vm.key → vm._props.key } } toggleObserving(true) }proxy 的作用:
javascriptfunction proxy(target, sourceKey, key) { Object.defineProperty(target, key, { get() { return this[sourceKey][key] }, set(val) { this[sourceKey][key] = val } }) }
- 这样我们写 this.msg 实际访问的是 this._props.msg,数据统一挂在 _props 上,但用起来像直接挂在 this 上
2.initMethods --- 绑定 this
javascriptfunction initMethods(vm, methods) { for (const key in methods) { vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm) } }
- 就做一件事:把 methods 的 this 硬绑到 vm。所以我们在 method 里写 this 永远指向组件实例,不会丢失
3.initData --- 调用 data() + 代理 + 深度 observe
javascriptfunction initData(vm) { let data = vm.$options.data data = vm._data = isFunction(data) ? getData(data, vm) : data || {} // getData 内部:pushTarget() → data.call(vm, vm) → popTarget() // pushTarget 防止 data() 中读取响应式数据时产生无用依赖 const keys = Object.keys(data) let i = keys.length while (i--) { const key = keys[i] if (props && hasOwn(props, key)) { // data 和 props 同名 → 警告,props 优先 } else if (!isReserved(key)) { proxy(vm, `_data`, key) // vm.key → vm._data.key } // isReserved: 以 _ 或 $ 开头的 key 不代理,避免和 Vue 内部属性冲突 } const ob = observe(data) // 深度递归,给每个对象加 __ob__ ob && ob.vmCount++ // 标记这是根数据对象 }为什么不代理 _ 和 $ 开头的 key?
javascriptfunction isReserved(key) { const c = (key + '').charCodeAt(0) return c === 0x24 || c === 0x5F // $ 或 _ }因为 Vue 实例上 _xxx 和 xxx 都是内部属性(_data、el、watch...),如果 data 里有个 name,代理后会覆盖实例方法
4.initComputed --- 创建惰性 Watcher
javascriptfunction initComputed(vm, computed) { const watchers = vm._computedWatchers = Object.create(null) for (const key in computed) { const userDef = computed[key] const getter = isFunction(userDef) ? userDef : userDef.get // 创建"惰性 Watcher",不会立即执行 watchers[key] = new Watcher(vm, getter, noop, { lazy: true }) if (!(key in vm)) { defineComputed(vm, key, userDef) } } }defineComputed 在 vm 上定义 getter:
javascriptfunction createComputedGetter(key) { return function computedGetter() { const watcher = this._computedWatchers[key] if (watcher.dirty) { // 脏了才重新计算 watcher.evaluate() // 执行 getter,清脏标记 } if (Dep.target) { watcher.depend() // 让外层 Watcher 也收集这个 computed 的依赖 } return watcher.value } }惰性求值:lazy: true 使得 Watcher 创建时不执行 get(),而是标记 dirty = true。首次访问时才计算,之后只有依赖变了才重新计算
5.initWatch --- 创建用户 Watcher
javascriptfunction initWatch(vm, watch) { for (const key in watch) { const handler = watch[key] if (isArray(handler)) { // 数组形式:watch: { foo: [fn1, fn2] } for (let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]) } } else { createWatcher(vm, key, handler) } } } function createWatcher(vm, expOrFn, handler, options) { // 支持对象形式:watch: { foo: { handler: fn, deep: true } } if (isPlainObject(handler)) { options = handler handler = handler.handler } // 支持字符串形式:watch: { foo: 'handleFoo' } if (typeof handler === 'string') { handler = vm[handler] } return vm.$watch(expOrFn, handler, options) }$watch 最终创建一个 user: true 的 Watcher,和渲染 Watcher 区分开
initProvide
javascript
export function initProvide(vm) {
const provideOption = vm.$options.provide
if (provideOption) {
const provided = isFunction(provideOption)
? provideOption.call(vm) // 支持函数形式
: provideOption // 也支持对象形式
const source = resolveProvided(vm) // 就是 vm._provided
const keys = hasSymbol ? Reflect.ownKeys(provided) : Object.keys(provided)
for (const key in keys) {
Object.defineProperty(source, key,
Object.getOwnPropertyDescriptor(provided, key)!)
}
}
为什么 provide 在 data 之后?
因为 provide 可能依赖 data:
javascriptexport default { data() { return { theme: 'dark' } }, provide() { return { theme: this.theme } // 依赖 data } }
这里我们总结一下以上方法的协作关系:
javascript
initLifecycle(vm)
│ 建好 $parent / $root / $children 骨架
│ 初始化状态标记 (_isMounted 等)
│
initEvents(vm)
│ 初始化 _events 事件池
│ 注册父组件传入的事件监听
│
initRender(vm)
│ 解析 $slots / $scopedSlots
│ 创建 _c / $createElement
│ $attrs / $listeners 响应式化
│
────── callHook('beforeCreate') ────── ← 以上都可用,但 data/props/methods 还没有
│
initInjections(vm)
│ 沿 _provided 向上查找注入值
│ defineReactive 到 vm 上(不深度 observe)
│
initState(vm)
├─ initProps → vm._props + proxy + shallowReactive
├─ initSetup → Composition API setup()
├─ initMethods → bind(this) 挂到 vm
├─ initData → vm._data + proxy + 深度 observe
├─ initComputed → 惰性 Watcher + getter 代理
└─ initWatch → 用户 Watcher
│
initProvide(vm)
│ 把 provide 的值挂到 vm._provided
│(后代组件的 initInjections 会从这里取)
│
────── callHook('created') ────── ← 全部响应式状态就绪,但还没 DOM
- 整个顺序的核心逻辑:先有骨架,再注入依赖,再初始化自己的状态,最后才能 provide 给别人
模板编译阶段

在初始化阶段结束之后,准备调用 vm.$mount 时,根据流程图,我们来到了模板编译阶段,这一阶段要做的主要工作是将用户传入的模板内容编译成 Render 函数:

我们先看一个例子:
javascript
new Vue({
el: '#app',
template: '<div>{{ message }}</div>',
data: { message: 'hello' }
})
Vue 要把它变成 DOM,必须经历两步:
javascript
template 字符串 ─编译器──→ render 函数 ─ 运行时──→ 真实 DOM
"你的模板" 👆 "你的逻辑" 👆
编译器干的事 运行时干的事
- 编译器:把 template 字符串编译成 render 函数
- 运行时:执行 render 函数生成 VNode,再 patch 到 DOM 上
两个步骤的区别就是 ------ 编译器在不在,是否存在模板编译阶段
也就是说,Vue基于源码构建的有两个版本,一个是 runtime only (一个只包含运行时的版本),另一个是 runtime + compiler (一个同时包含编译器和运行时的完整版本)
通俗点来说,当我们写了一段 template 的模板字符串的时候才会用到编译器,帮我们编译成 render 函数,这个时候才有模板编译阶段;如果我们直接手搓 render函数,比如这样:
javascript
new Vue({
el: '#app',
data: { message: 'hello' },
render(h) {
return h('div', this.message)
}
})
这个时候我们用不到编译器了,就不会存在模板编译阶段,当时没有了编译过程也会大大降低性能损耗
基于是否存在模板编译阶段的区别,调用 vm.$mount 方法的实现上也会有差异,我们来看看源码层面的证据:
源码位置:src/platforms/web/runtime/index.js
Runtime Only 的 $mount
javascript
Vue.prototype.$mount = function (el, hydrating) {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
直接调 mountComponent,没有任何编译逻辑。如果你传的是 template 而不是 render,到 mountComponent 里就会发现 vm.$options.render 不存在:
javascript
// lifecycle.js
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode // 降级为空节点
// 并警告:template or render function not defined
}
Runtime + Compiler 的 $mount
javascript
const mount = Vue.prototype.$mount // 先存下 runtime 版的 $mount
Vue.prototype.$mount = function (el, hydrating) {
const options = this.$options
// 关键:如果没有 render,才尝试编译 template
if (!options.render) {
let template = options.template
// 1. template 是 '#xxx' → 取 DOM 元素的 innerHTML
if (template && typeof template === 'string' && template.charAt(0) === '#') {
template = idToTemplate(template)
}
// 2. template 是 DOM 节点 → 取 innerHTML
else if (template.nodeType) {
template = template.innerHTML
}
// 3. 没有 template 但有 el → 取 el 的 outerHTML
else if (el) {
template = getOuterHTML(el)
}
// 4. 编译!template → render 函数
if (template) {
const { render, staticRenderFns } = compileToFunctions(template, { ... }, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
// 最后调用 runtime 版的 $mount
return mount.call(this, el, hydrating)
}
- 这里的核心就是:在调用原来的 $mount 之前,先把 template 编译成 render
将整个逻辑画成一个流:
javascript
Runtime + Compiler 的 $mount
│
├── 有 render? → 跳过编译,直接调 runtime 的 $mount
│
└── 没有 render?
├── 有 template?
│ ├── 字符串 '#app' → 取 DOM innerHTML
│ ├── DOM 节点 → 取 innerHTML
│ └── 其它字符串 → 直接用
├── 没有 template 但有 el?
│ └── 取 el 的 outerHTML 作为 template
│
└── compileToFunctions(template)
→ 生成 render + staticRenderFns
→ 挂到 vm.$options 上
│
└── 调 runtime 的 $mount → mountComponent
挂载阶段

模板编译阶段之后,我们来到了挂载阶段,在这个阶段:
- 编译模板(完整版)
- 创建渲染 Watcher 首次渲染(render 生成 VNode,patch 生成 DOM)
- 建立数据与视图的响应式联系(数据监控 + 依赖更新)
- 数据变了,Watcher 自动重新执行这个流程,只更新变化的部分

首先我们找到 $mount 入口:
javascript
// init.js 中 _init 的最后
if (vm.$options.el) {
vm.$mount(vm.$options.el) // 传入 '#app' 对应的 DOM 元素
}
上一部分我们已经看了 Runtime + Compiler 的 mount 源码,compileToFunctions 会把模板编译成 render 函数,然后调 Runtime Only 版的 mount → mountComponent
javascript
function mountComponent(vm, el, hydrating) {
vm.$el = el // 记住挂载目标
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode // 兜底:没有 render 就渲染空节点
}
// 1.触发 beforeMount
callHook(vm, 'beforeMount')
// 2.定义更新函数
let updateComponent = () => {
vm._update(vm._render(), hydrating)
// ↑ 渲染VNode ↑ 补丁到DOM
}
// 3.创建渲染 Watcher
new Watcher(vm, updateComponent, noop, {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate') // 数据变了但DOM还没更新时触发
}
}
}, true /* isRenderWatcher */)
// 4.根实例直接触发 mounted
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
在创建 Watcher 实例的时候,传入的第二个参数是 updateComponent,我们在前面的篇章源码篇 剖析 Vue2 双向绑定原理有提到过 Watcher 类 构造函数的源码,看下图:

this.getter 就是 updateComponent,即 vm._update(vm._render()),所以执行链是:
javascript
Watcher 构造函数
→ this.get()
→ updateComponent()
→ vm._render() // 生成 VNode
→ vm._update() // 把 VNode 渲染到 DOM
之后我们绑定的元素的数据变化了,就会通知这个 Watcher 重新执行 updateComponent,并更新视图,如此反复,直到实例被销毁
销毁阶段

终于来到了最后一个阶段,在这个阶段将调用 vm.$destroy 将 Vue 实例从父级实例中销毁,并移除当前实例上的依赖追踪和事件监听器

什么时候会触发销毁?
javascript
// 场景1:手动调用
this.$destroy()
// 场景2:v-if 变为 false,组件被移除
// 最终也是调 $destroy,只是由 patch 过程自动触发的
<Child v-if="show" /> // show 从 true 变为 false
接下来分析一下 $destroy 的完整执行过程
javascript
Vue.prototype.$destroy = function () {
const vm = this
// ① 防重复销毁
if (vm._isBeingDestroyed) {
return
}
// ② beforeDestroy
callHook(vm, 'beforeDestroy')
// ③ 标记"正在销毁"
vm._isBeingDestroyed = true
// ④ 从父组件的 $children 中移除自己
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// ⑤ 停止作用域(清除所有 watcher)
vm._scope.stop()
// ⑥ 减少数据对象的引用计数
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// ⑦ 标记"已销毁"
vm._isDestroyed = true
// ⑧ 从 DOM 中移除(patch 旧 VNode 为 null)
vm.__patch__(vm._vnode, null)
// ⑨ destroyed
callHook(vm, 'destroyed')
// ⑩ 移除所有事件监听
vm.$off()
// ⑪ 清理 DOM 引用
if (vm.$el) {
vm.$el.__vue__ = null
}
// ⑫ 清理父 VNode 引用
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
1.为什么需要防重复销毁?
因为可能同时存在多个触发销毁的路径。比如父组件销毁时会递归销毁子组件,而子组件自己也可能调了 $destroy()。加个标记防止走两遍
2.beforeDestroy 钩子
javascriptcallHook(vm, 'beforeDestroy')此时组件还是完整的:this.$el、this.data、this.methods、子组件、watcher 全都在。这是做清理工作的最后机会:
javascriptbeforeDestroy() { clearInterval(this.timer) // 清除定时器 window.removeEventListener('resize', this.onResize) // 移除全局事件 this.websocket.close() // 关闭连接 }3.步骤4中三个条件的含义
javascriptvm._isBeingDestroyed = true const parent = vm.$parent if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { remove(parent.$children, vm) }
- !parent._isBeingDestroyed:如果父组件也在销毁中,说明是父组件带动的批量销毁,父组件的 $children 后面会整体清空,不需要单独移除
- !vm.options.abstract:抽象组件(keep-alive)不出现在 children 中,所以不需要移除
- remove(parent.children, vm):从父组件的 children 数组里把自己删掉
4.停止作用域 --- 清除所有 Watcher
javascriptvm._scope.stop()_scope 是在 _init 时创建的 EffectScope,所有 Watcher(渲染 Watcher、计算属性 Watcher、用户 Watcher)都注册在这个作用域里;
scope.stop() 会遍历 scope.effects,逐个调 watcher.teardown():
javascript// watcher.js teardown() { if (this.vm && !this.vm._isBeingDestroyed) { remove(this.vm._scope.effects, this) } if (this.active) { let i = this.deps.length while (i--) { this.deps[i].removeSub(this) // 从所有 dep 的订阅列表中移除自己 } this.active = false } }这一步之后,数据变了,Watcher 不再收到通知,不会触发重新渲染
5.减少数据对象的引用计数
javascriptif (vm._data.__ob__) { vm._data.__ob__.vmCount-- }vmCount 记录有多少个组件实例引用了这个数据对象。当 vmCount 降为 0 时,这个数据对象的 Observer 就可以被回收了。这是为了避免内存泄漏------如果数据被多个组件共享,只有最后一个使用者销毁时才应该释放 Observer
6.patch 旧 VNode 为 null --- 从 DOM 移除 + 递归销毁子组件
javascriptvm.__patch__(vm._vnode, null)这里做了两件事:
- 移除 DOM
javascript// patch.js // patch(oldVNode, null) 进入 patch 函数 // vnode 为 null,走销毁逻辑 if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return }
- 递归销毁子组件
javascriptfunction invokeDestroyHook(vnode) { const data = vnode.data if (isDef(data)) { // 触发组件级别的 destroy 钩子 if (isDef(data.hook) && isDef(data.hook.destroy)) { data.hook.destroy(vnode) } // 触发模块的 destroy 钩子(directives、ref 等) for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode) } } // 递归处理子节点 if (isDef(vnode.children)) { for (j = 0; j < vnode.children.length; ++j) { invokeDestroyHook(vnode.children[j]) } } }data.hook.destroy 就是 componentVNodeHooks.destroy:
javascriptdestroy(vnode) { const { componentInstance } = vnode if (!componentInstance._isDestroyed) { if (!vnode.data.keepAlive) { componentInstance.$destroy() // 递归调用子组件的 $destroy } else { deactivateChildComponent(componentInstance, true) // keep-alive 只停用不销毁 } } }如此就可以形成递归链:
javascript父组件 $destroy() → __patch__(vm._vnode, null) → invokeDestroyHook(父VNode) → 遍历子 VNode → invokeDestroyHook(子VNode) → data.hook.destroy(子VNode) → 子组件.$destroy() ← 递归 → __patch__(子vm._vnode, null) → invokeDestroyHook(孙子VNode) → 孙组件.$destroy() ← 继续递归 → ...所以销毁是自上而下递归的,父组件先触发 beforeDestroy,然后依次销毁子组件、孙组件...最后才到自己的 destroyed
7.移除事件监听
javascriptcallHook(vm, 'destroyed') vm.$off()$off() 无参数调用时,清空所有事件:
javascriptVue.prototype.$off = function (event, fn) { if (!arguments.length) { vm._events = Object.create(null) // 清空整个事件池 return vm } // ... }8.vue 是什么?
vue 是 Vue 在 DOM 元素上留下的引用(方便 DevTools 调试),销毁时要断开,否则 DOM 节点被回收了但还引用着 Vue 实例,造成内存泄漏
这里我们要注意的一点:
销毁钩子执行顺序:
javascript
child beforeDestroy → child destroyed → parent beforeDestroy → parent destroyed
挂载钩子执行顺序:
javascript
parent beforeMount → child beforeMount → child mounted → parent mounted
Keep-Alive 的特殊处理
抽象组件(keep-alive)我们一直没有销毁,一起来看看:
javascript
destroy(vnode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy() // 普通组件:真销毁
} else {
deactivateChildComponent(componentInstance, true) // keep-alive:只停用
}
}
}
被 keep-alive 包裹的组件不走 $destroy,而是走 deactivateChildComponent:
javascript
function deactivateChildComponent(vm, direct) {
if (!vm._inactive) {
vm._inactive = true
for (let i = 0; i < vm.$children.length; i++) {
deactivateChildComponent(vm.$children[i]) // 递归停用子组件
}
callHook(vm, 'deactivated') // 触发 deactivated 而不是 destroyed
}
}
抽象组件只是被"冻结"(_inactive = true),DOM 从页面移除但实例保留在内存中。下次激活时走 activateChildComponent,触发 activated 钩子,DOM 重新插入,不用重新创建实例。
总结
本篇我们介绍了生命周期的完整流程,被分为了四个阶段:初始化阶段------模板编译阶段------挂载阶段------销毁阶段。在这个过程中,我们也区分了选项式 API 和组件式 API 在生命周期钩子的不同定义方式。
