手把手带你阅读Vue2源码
前言
Vue.js 作为目前最流行的前端框架之一,其源码设计精妙,架构清晰。通过阅读Vue2的源码,我们不仅能深入理解其工作原理,还能学习到很多优秀的设计模式和编程思想。本文将带你从零开始,深入Vue2的核心源码,理解其实现原理。
Vue2源码结构
首先,让我们了解一下Vue2的源码目录结构:
csharp
src/
├── core/ # 核心代码
│ ├── instance/ # Vue实例相关
│ │ ├── index.ts # Vue构造函数
│ │ ├── init.ts # 实例初始化
│ │ ├── state.ts # 状态管理(data、props、computed等)
│ │ ├── render.ts # 渲染相关
│ │ ├── lifecycle.ts # 生命周期
│ │ └── ...
│ ├── observer/ # 响应式系统
│ │ ├── index.ts # Observer类
│ │ ├── dep.ts # 依赖收集
│ │ ├── watcher.ts # 观察者
│ │ ├── scheduler.ts # 调度器
│ │ └── array.ts # 数组响应式处理
│ ├── vdom/ # 虚拟DOM
│ └── util/ # 工具函数
├── compiler/ # 模板编译
└── platforms/ # 平台相关代码
一、Vue构造函数与实例初始化
1.1 Vue构造函数
Vue的入口在 src/core/instance/index.ts:
typescript
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
// 通过Mixin模式扩展Vue原型
initMixin(Vue) // 初始化相关
stateMixin(Vue) // 状态相关
eventsMixin(Vue) // 事件相关
lifecycleMixin(Vue) // 生命周期相关
renderMixin(Vue) // 渲染相关
Vue采用了Mixin模式来组织代码,将不同功能的代码分别通过不同的Mixin注入到Vue原型上,这样既保持了代码的模块化,又避免了单一文件过大。
1.2 实例初始化流程
当执行 new Vue(options) 时,会调用 _init 方法,定义在 src/core/instance/init.ts:
typescript
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
// 1. 唯一标识
vm._uid = uid++
// 2. 标记为Vue实例
vm._isVue = true
// 3. 合并选项
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
// 4. 初始化代理(开发环境)
if (__DEV__) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// 5. 初始化生命周期
initLifecycle(vm)
// 6. 初始化事件
initEvents(vm)
// 7. 初始化渲染
initRender(vm)
// 8. 调用beforeCreate钩子
callHook(vm, 'beforeCreate')
// 9. 初始化injections
initInjections(vm)
// 10. 初始化状态(data、props、methods、computed、watch)
initState(vm)
// 11. 初始化provide
initProvide(vm)
// 12. 调用created钩子
callHook(vm, 'created')
// 13. 如果有el选项,自动挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
关键点:
- 初始化顺序很重要:
beforeCreate→injections→state→provide→created - 这样设计是为了确保在
created钩子中能够访问到data、props等状态 injections在state之前初始化,provide在state之后初始化,符合依赖注入的设计
二、响应式系统核心原理
Vue2的响应式系统是其核心特性,基于 Object.defineProperty 实现。主要包含三个核心类:Observer、Dep、Watcher。
2.1 Observer(观察者)
Observer 类负责将普通对象转换为响应式对象,定义在 src/core/observer/index.ts:
typescript
export class Observer {
dep: Dep
vmCount: number
constructor(public value: any, public shallow = false, public mock = false) {
this.dep = mock ? mockDep : new Dep()
this.vmCount = 0
// 在对象上添加 __ob__ 属性,指向Observer实例
def(value, '__ob__', this)
if (isArray(value)) {
// 数组:重写数组方法
if (hasProto) {
value.__proto__ = arrayMethods
} else {
for (let i = 0, l = arrayKeys.length; i < l; i++) {
def(value, arrayKeys[i], arrayMethods[arrayKeys[i]])
}
}
this.observeArray(value)
} else {
// 对象:遍历属性,转换为getter/setter
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
defineReactive(value, keys[i], NO_INITIAL_VALUE, undefined, shallow, mock)
}
}
}
}
核心机制:
- 为对象添加
__ob__属性,存储Observer实例 - 数组通过重写原型方法实现响应式
- 对象通过
defineReactive将属性转换为getter/setter
2.2 defineReactive(定义响应式属性)
defineReactive 是响应式系统的核心函数:
typescript
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean
) {
const dep = new Dep() // 每个属性都有独立的Dep实例
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
const getter = property && property.get
const setter = property && property.set
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
// 依赖收集:如果当前有Watcher,将其添加到Dep中
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
// 值没有变化,直接返回
if (!hasChanged(value, newVal)) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 新值也需要变成响应式
childOb = observe(newVal, false, mock)
// 通知所有依赖更新
dep.notify()
}
})
}
工作流程:
- getter :当访问属性时,如果当前有
Dep.target(Watcher),调用dep.depend()收集依赖 - setter :当修改属性时,调用
dep.notify()通知所有依赖更新
2.3 Dep(依赖收集器)
Dep 类负责管理依赖,定义在 src/core/observer/dep.ts:
typescript
export default class Dep {
static target?: DepTarget | null
id: number
subs: Array<DepTarget | null>
constructor() {
this.id = uid++
this.subs = []
}
// 添加订阅者
addSub(sub: DepTarget) {
this.subs.push(sub)
}
// 移除订阅者
removeSub(sub: DepTarget) {
this.subs[this.subs.indexOf(sub)] = null
}
// 依赖收集:将当前Watcher添加到subs中
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知所有订阅者更新
notify() {
const subs = this.subs.filter(s => s) as DepTarget[]
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// 全局唯一的当前Watcher
Dep.target = null
const targetStack: Array<DepTarget | null | undefined> = []
export function pushTarget(target?: DepTarget | null) {
targetStack.push(target)
Dep.target = target
}
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
设计亮点:
Dep.target是全局唯一的,同一时间只能有一个Watcher在收集依赖- 使用栈结构
targetStack处理嵌套Watcher的情况
2.4 Watcher(观察者)
Watcher 是连接响应式数据和视图更新的桥梁,定义在 src/core/observer/watcher.ts:
typescript
export default class Watcher implements DepTarget {
vm?: Component | null
expression: string
cb: Function
id: number
deps: Array<Dep>
newDeps: Array<Dep>
getter: Function
value: any
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
// 将表达式转换为getter函数
if (isFunction(expOrFn)) {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.lazy ? undefined : this.get()
}
// 获取值,触发依赖收集
get() {
pushTarget(this) // 设置当前Watcher为Dep.target
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // 执行getter,触发属性的get
} catch (e: any) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} finally {
if (this.deep) {
traverse(value) // 深度遍历,收集所有嵌套属性的依赖
}
popTarget() // 恢复之前的Watcher
this.cleanupDeps() // 清理不再需要的依赖
}
return value
}
// 依赖更新时调用
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run() // 同步执行
} else {
queueWatcher(this) // 异步执行,加入队列
}
}
// 执行回调
run() {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
if (this.user) {
this.cb.call(this.vm, value, oldValue)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
}
Watcher类型:
- Render Watcher:每个组件实例都有一个,用于更新视图
- User Watcher :用户通过
$watch或watch选项创建的 - Computed Watcher :计算属性的Watcher,具有
lazy特性
2.5 响应式系统流程图
scss
访问属性 (getter)
↓
Dep.target 存在?
↓ 是
dep.depend() → watcher.addDep(dep) → dep.addSub(watcher)
↓
依赖收集完成
修改属性 (setter)
↓
dep.notify()
↓
遍历 subs,调用 watcher.update()
↓
queueWatcher(watcher)
↓
nextTick(flushSchedulerQueue)
↓
watcher.run() → watcher.get() → 重新渲染/执行回调
三、渲染系统
3.1 render函数
每个Vue组件都有一个render函数,定义在 src/core/instance/render.ts:
typescript
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// 设置父vnode
vm.$vnode = _parentVnode
// 执行render函数,生成vnode
let vnode
try {
setCurrentInstance(vm)
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e: any) {
handleError(e, vm, `render`)
vnode = vm._vnode
} finally {
currentRenderingInstance = prevRenderInst
setCurrentInstance(prevInst)
}
// 确保返回单个根节点
if (isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
if (!(vnode instanceof VNode)) {
vnode = createEmptyVNode()
}
vnode.parent = _parentVnode
return vnode
}
关键点:
render函数接收createElement作为参数vm._renderProxy在开发环境下是Proxy,用于警告未定义的属性- render函数执行时会访问响应式数据,触发依赖收集
3.2 渲染Watcher
在组件挂载时,会创建一个Render Watcher:
typescript
export function mountComponent(
vm: Component,
el: Element | null | undefined,
hydrating?: boolean
): Component {
// ...
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
},
true /* isRenderWatcher */
)
// ...
}
当响应式数据变化时:
- 触发属性的setter
- 调用
dep.notify() - Render Watcher的
update()被调用 - 加入更新队列,在
nextTick中执行 - 执行
updateComponent,重新渲染
四、调度器(Scheduler)
Vue使用调度器来批量处理更新,避免频繁的DOM操作,定义在 src/core/observer/scheduler.ts:
typescript
const queue: Array<Watcher> = []
let has: { [key: number]: true | undefined | null } = {}
let waiting = false
let flushing = false
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
// 去重:同一个Watcher只添加一次
if (has[id] != null) {
return
}
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// 如果正在刷新,按id插入到正确位置
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// 确保只调用一次nextTick
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
function flushSchedulerQueue() {
currentFlushTimestamp = getNow()
flushing = true
// 排序:确保父组件先于子组件更新
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before() // 调用beforeUpdate钩子
}
has[id] = null
watcher.run() // 执行更新
}
resetSchedulerState()
callUpdatedHooks(updatedQueue)
}
设计优势:
- 批量更新:同一事件循环中的多次数据变化,只会触发一次更新
- 去重:同一个Watcher不会重复添加
- 排序:确保父组件先于子组件更新,避免不必要的子组件更新
五、数组的响应式处理
由于 Object.defineProperty 无法监听数组索引的变化,Vue2通过重写数组方法来实现响应式:
typescript
// src/core/observer/array.ts
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
ob.dep.notify() // 通知更新
return result
})
})
实现原理:
- 创建新对象继承
Array.prototype - 重写会改变数组的方法
- 在方法中调用
ob.dep.notify()通知更新 - 对新添加的元素进行响应式处理
六、总结
通过阅读Vue2源码,我们可以学到:
- Mixin模式:通过Mixin组织代码,保持模块化
- 观察者模式:Observer、Dep、Watcher的经典实现
- 依赖收集:通过getter/setter实现自动依赖收集
- 批量更新:使用调度器优化性能
- 数组处理:通过重写原型方法实现数组响应式
Vue2的响应式系统虽然基于 Object.defineProperty,存在一些局限性(如无法监听新增属性、数组索引等),但其设计思想仍然值得学习。Vue3改用Proxy实现,解决了这些问题,但核心的依赖收集和更新机制仍然类似。
希望这篇文章能帮助你更好地理解Vue2的工作原理。如果你对某个部分有疑问,建议直接阅读源码,结合调试工具,会有更深入的理解。
参考资源: