3-4、Vue2-核心源码讲解

Vue2 源码仓库:https://github.com/vuejs/vue

为什么还要看 vue2 的源码:
因为 vue3 结构比较最新的,并且细节很多,不利于了解核心的东西

源码入口

查找顺序:

  1. https://github.com/vuejs/vue/blob/main/package.json
    1. 可以看到dev:*的命令都是Rollup打包逻辑,我们重点跟踪dev命令
  2. https://github.com/vuejs/vue/blob/main/scripts/config.js
    1. 该文件主要是处理、生成rollup打包的配置项,dev命令对应的配置为full-dev
1
2
3
4
5
6
7
8
9
'full-dev': {
// 入口配置
entry: resolve('web/entry-runtime-with-compiler.ts'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
  1. https://github.com/vuejs/vue/blob/main/src/platforms/web/entry-runtime-with-compiler.ts
    1. 该文件完整代码如下:
1
2
3
4
5
6
7
8
9
10
import Vue from './runtime-with-compiler' // 关键代码
import * as vca from 'v3'
import { extend } from 'shared/util'

extend(Vue, vca)

import { effect } from 'v3/reactivity/effect'
Vue.effect = effect

export default Vue
  1. https://github.com/vuejs/vue/blob/main/src/platforms/web/runtime-with-compiler.ts
    1. 该文件里面主要定义$mount,关键代码如下:
1
2
3
4
5
6
7
8
9
10
import Vue from './runtime/index'

// .....

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function () { /* ... */}

// .....

export default Vue as GlobalAPI
  1. https://github.com/vuejs/vue/blob/main/src/platforms/web/runtime/index.ts
    1. 该文件也是一些Vue的配置,关键代码如下:
1
2
3
4
5
import Vue from 'core/index'

// ...

export default Vue
  1. https://github.com/vuejs/vue/blob/main/src/core/index.ts
    1. 该文件是Vue.prototype的配置,关键代码如下:
1
2
3
4
5
import Vue from './instance/index'

// ...

export default Vue
  1. https://github.com/vuejs/vue/blob/main/src/core/instance/index.ts
    1. 该文件为new Vue时调用的
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
// 完整代码
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'
import type { GlobalAPI } from 'types/global-api'

// 构造函数 Vue,这也是为什么我们在使用时用的为:new Vue(...)
// 因为 Vue 本质就是个构造函数
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options) // ⭐️ 初始化的核心代码,执行 _init 方法,传参为:配置项
}

//@ts-expect-error Vue has function type
initMixin(Vue) // ⭐️ 调用 initMixin 函数,将 _init 方法挂载到 Vue.prototype 上
//@ts-expect-error Vue has function type
stateMixin(Vue)
//@ts-expect-error Vue has function type
eventsMixin(Vue)
//@ts-expect-error Vue has function type
lifecycleMixin(Vue)
//@ts-expect-error Vue has function type
renderMixin(Vue)

export default Vue as unknown as GlobalAPI
  1. https://github.com/vuejs/vue/blob/main/src/core/instance/init.ts
    1. 该文件为new Vue时执行的_init方法的定义(真正的入口):
1
2
3
4
5
6
7
// ...

Vue.prototype._init = function (options?: Record<string, any>) {
// ...
}

// ...

初始化

我们在使用 Vue 时,是这样初始化的

1
2
3
4
5
6
7
8
9
10
11
new Vue({
el: '#app',
data: {
count: 1
},
methods: {
addCount() {
this.count++
}
}
})

现在我们开始学习源码后,那要关注下new Vue到底做了什么

1
2
3
4
5
6
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options) // 执行 _init 方法
}

_init 代码解析

完整源码:https://github.com/vuejs/vue/blob/main/src/core/instance/init.ts
核心源码:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// ...

// 在 Vue 原型上定义一个 _init 方法,用于初始化实例。
// 这个方法接收一个可选参数 options,类型为记录(Record)类型,键为字符串,值为任意类型
// 通常用来传入组件的选项对象。
Vue.prototype._init = function (options?: Record<string, any>) {
// 定义一个常量 vm,类型为 Component,指向当前正在创建的 Vue 实例。
const vm: Component = this
// 给每个 Vue 实例设置一个唯一的 _uid 标识符,用以区分不同的实例。
vm._uid = uid++

let startTag, endTag
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
// 如果处于开发环境 (__DEV__) 并且支持性能标记(config.performance && mark)
// 则进行 Vue 初始化性能检测的相关操作。
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}

// 设置 _isVue 属性为 true,表明这是一个 Vue 实例。
vm._isVue = true
// 避免 Vue 监听器观察到此实例。
vm.__v_skip = true
// 创建一个新的副作用作用域(EffectScope),用于追踪和执行副作用函数,如计算属性、watcher 等。
// 这里指定为独立作用域,即不受父作用域的影响。
vm._scope = new EffectScope(true /* detached */)
// 设置 Vue 实例的副作用作用域的父级为 undefined
vm._scope.parent = undefined
// 以及标识其与 Vue 实例关联。
vm._scope._vm = true

if (options && options._isComponent) {
// 如果是内部组件,则调用 initInternalComponent 进行优化
initInternalComponent(vm, options as any)
} else {
// ⭐️ 否则通过 mergeOptions 合并构造函数的默认选项和用户传入的选项
// 并将结果赋值给 $options。
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
/* istanbul ignore else */
if (__DEV__) {
// 如果在开发环境下,调用 initProxy 函数来设置代理访问实例数据的逻辑
initProxy(vm)
} else {
// 非开发环境下,直接设置 _renderProxy 指向自身。
vm._renderProxy = vm
}
// 将实例自身暴露给实例自身的 _self 属性。
vm._self = vm
// 调用内部方法初始化 生命周期
initLifecycle(vm)
// 调用内部方法初始化 事件系统
initEvents(vm)
// 调用内部方法初始化 渲染相关
initRender(vm)
// ⭐️ 调用钩子函数 beforeCreate
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
// 在解析[数据/props]之前解析依赖项(injections)。
initInjections(vm)
// ⭐️ 初始化实例的状态:包括 data、props、methods、computed、watch
initState(vm)
// 在解析[数据/props]之后解析提供项(provide)。
initProvide(vm)
// ⭐️ 调用钩子函数 created
callHook(vm, 'created')

/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
// 开发环境下完成性能标记,记录 Vue 初始化过程的时间消耗,并给实例设置名称。
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}

if (vm.$options.el) {
// ⭐️ 如果实例的选项中包含 el(元素挂载点),则调用 $mount 方法挂载 Vue 实例到指定 DOM 元素。
vm.$mount(vm.$options.el)
}
}

// ...

mergeOptions 方法

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
export function mergeOptions( // 合并[构造函数上的 options]与[实例的 options]
parent: Record<string, any>, // Vue实例构造函数上的 options
child: Record<string, any>, // new Vue(...) 传入的 options
vm?: Component | null // 实例本身
): ComponentOptions {
// parent = {
// components:{},
// directives: {},
// filters: {},
// _base: Vue
// }
// child = { el: '#app', data: { count: 1 } }
if (__DEV__) {
checkComponents(child) // 检测组件名称是否合法
}

if (isFunction(child)) {
// @ts-expect-error
child = child.options // 如果 child 是函数,则取 child.options 作为 child
}

// 把 props 属性转为对象形式(标准结构)
// props: ["count", "a-name"] => props: { count: { type: null }, aName: { type: null } }
// props: { count: "number" } => props: { count: { type: "number" } }
normalizeProps(child, vm)

// 把 inject 属性转为对象形式(标准结构)
normalizeInject(child, vm)

// 把 directives 属性转为对象形式(标准结构)
normalizeDirectives(child)

// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
if (child.extends) {
// 当存在 child.extends 属性时,则调用 mergeOptions 实现合并
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
// 当存在 child.mixins 属性时,则循环调用 mergeOptions 实现合并
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}

const options: ComponentOptions = {} as any
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField(key: any) {
// const defaultStrat = function (parentVal: any, childVal: any): any {
// return childVal === undefined ? parentVal : childVal
// }
// defaultStrat 逻辑为:优先取 child 的值,没有再取 parent 的值

// const strats = config.optionMergeStrategies
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}

_init 的核心逻辑

  1. 传入的配置项与默认配置项合并
  2. 初始化该实例的生命周期、事件系统、渲染逻辑
  3. 调用该实例的beforeCreated钩子
  4. 初始化该实例的状态:data、props 等
  5. 调用该实例的created钩子
  6. el存在则执行该实例的挂载$mount逻辑

数据观测

vue 是数据驱动的,数据改变则视图也跟着变化。核心方式是Object.defineProperty()实现数据的劫持
vue 的数据观测核心机制是观察者模式
数据是被观察的一方,当数据发生变化时通知所有观察者,这样观察者就能做出响应(比如:重新渲染视图)
我们将观察者称为watcher,关系为data -> watcher
一个数据可以有多个观察者,通过中间对象dep来记录这种依赖关系data -> dep -> watcher
dep 的数据结构为{ id: uuid, subs: []},其中的subs用于存放所有观察者
源码分析:
代码为 init.ts 里面的initState方法里面的initData方法
核心代码的展示与组合:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
// initData 方法:
// vm:当前实例
function initData(vm: Component) {
// 获取实例上的 data 值
let data: any = vm.$options.data
// 当 data 为函数时,则执行它并获取它的返回值,否则直接用 data
data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
if (!isPlainObject(data)) {
// 当 data 不是对象时,报警告
data = {}
__DEV__ &&
warn(
'data functions should return an object:\n' +
'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data) // 获取 data 对象的 key 数组
const props = vm.$options.props // 获取实例的 props 值
const methods = vm.$options.methods // 获取实例的 methods 值
let i = keys.length // data 的 key 数组长度
while (i--) { // 递减循环,即从数组的后往前循环
const key = keys[i] // 获取具体的 key
if (__DEV__) {
if (methods && hasOwn(methods, key)) {
// data 的 key 与 methods 的 key重复时,报警告
warn(`Method "${key}" has already been defined as a data property.`, vm)
}
}
if (props && hasOwn(props, key)) {
// data 的 key 与 props 的 key重复时,报警告
__DEV__ &&
warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
/**
// * Check if a string starts with $ or _
// */
// export function isReserved(str: string): boolean {
// const c = (str + '').charCodeAt(0)
// return c === 0x24 || c === 0x5f
// }
// ⭐️ 调用 proxy 函数,传参为:
// vm-实例
// '_data'-固定属性键,等同于 data = vm._data
// key-当前 data 的属性键
// 将 this._data.xx 代理为 this.xx
proxy(vm, `_data`, key)
}
}
// ⭐️ 进行 data 数据的观察逻辑,响应式的核心代码
const ob = observe(data)
ob && ob.vmCount++
}

// target:vm 实例
// sourceKey:固定值 _data
// key:当前 data 的属性键
export function proxy(target: Object, sourceKey: string, key: string) {
function noop(a?: any, b?: any, c?: any) {}

const sharedPropertyDefinition = {
enumerable: true, // 是否可枚举
configurable: true, // 是否可更改与删除
get: noop,
set: noop
}

// 定义具体的 get 函数
sharedPropertyDefinition.get = function proxyGetter() {
// this 等价于 target
return this[sourceKey][key] // 等价于 target._data[key]
}
// 定义具体的 set 函数
sharedPropertyDefinition.set = function proxySetter(val) {
// this 等价于 target
this[sourceKey][key] = val // 等价于 target._data[key] = val
}
// 属性劫持
Object.defineProperty(target, key, sharedPropertyDefinition)
}

// ⭐️ observe 函数:响应式的核心代码
// value:为实例的 data
// shallow:undefined
// ssrMockReactivity:undefined
// 返回值为:new Observe(...) 后的实例
export function observe(
value: any,
shallow?: boolean,
ssrMockReactivity?: boolean
): Observer | void {
// 当已经被观察后,则直接返回
if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
return value.__ob__
}

// 一系列的判断,可以忽略,直接看里面执行的逻辑
if (
shouldObserve &&
(ssrMockReactivity || !isServerRendering()) &&
(isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value.__v_skip /* ReactiveFlags.SKIP */ &&
!isRef(value) &&
!(value instanceof VNode)
) {
// ⭐️ new 调用 Observer 构造函数,参数为:
// value:为实例的 data
// shallow:undefined
// ssrMockReactivity:undefined
return new Observer(value, shallow, ssrMockReactivity)
}
}

export class Observer {
dep: Dep
vmCount: number // number of vms that have this object as root $data

// value:为实例的 data
// shallow:false
// mock:false
constructor(public value: any, public shallow = false, public mock = false) {
// dep = { id: uuid, subs: [] }
this.dep = mock ? mockDep : new Dep()
this.vmCount = 0

/**
* Define a property.
*/
function def(obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}

// 在实例的 data 里面新增 __ob__ 属性,其值为 Observer 实例
// 暂存一份数据
def(value, '__ob__', this)
if (isArray(value)) {
// 当实例的 data 为数组时
if (!mock) {
if (hasProto) {
/* eslint-disable no-proto */
;(value as any).__proto__ = arrayMethods
/* eslint-enable no-proto */
} else {
for (let i = 0, l = arrayKeys.length; i < l; i++) {
const key = arrayKeys[i]
def(value, key, arrayMethods[key])
}
}
}
if (!shallow) {
this.observeArray(value)
}
} else {
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// ⭐️ 调用 defineReactive,传参为:
// value:实例的 data
// key:实例的 data 的每个 key
// NO_INITIAL_VALUE = {}
// undefined
// shallow:false
// mock:false
defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
}
}
}

/**
* Observe a list of Array items.
*/
observeArray(value: any[]) {
for (let i = 0, l = value.length; i < l; i++) {
observe(value[i], false, this.mock)
}
}
}

/**
* Define a reactive property on an Object.
*/
// ⭐️ 调用 defineReactive,接受到的参数为:
// obj:实例的 data
// key:实例的 data 的每个 key
// val:{}
// customSetter: undefined
// shallow:false
// mock:false
// observeEvenIfShallow:undefined
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean,
observeEvenIfShallow = false
) {
// dep = { id: uuid, subs: [] }
const dep = new Dep()

const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}

// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if (
(!getter || setter) &&
(val === NO_INITIAL_VALUE || arguments.length === 2)
) {
val = obj[key] // 取值,等价于 data[key]
}

// 将取得的值,再次递归调用 observe
let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)

// ⭐️ 使用 Object.defineProperty 实现属性拦截
// obj:实例的 data
// key:实例的 data 的每个 key
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val

// ⭐️ dep.depend() 实现依赖的采集
if (Dep.target) {
if (__DEV__) {
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key
})
} else {
dep.depend()
}
if (childOb) {
childOb.dep.depend() // 孩子的依赖采集
if (isArray(value)) {
dependArray(value)
}
}
}
return isRef(value) && !shallow ? value.value : value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (!hasChanged(value, newVal)) {
return
}
if (__DEV__ && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else if (getter) {
// #7981: for accessor properties without setter
return
} else if (!shallow && isRef(value) && !isRef(newVal)) {
value.value = newVal
return
} else {
val = newVal
}
childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock)

// ⭐️ dep.notify() 实现观察者的通知
if (__DEV__) {
dep.notify({
type: TriggerOpTypes.SET,
target: obj,
key,
newValue: newVal,
oldValue: value
})
} else {
dep.notify()
}
}
})

return dep
}

export default class Dep {
static target?: DepTarget | null
id: number
subs: Array<DepTarget | null>
// pending subs cleanup
_pending = false

constructor() {
this.id = uid++
this.subs = []
}

addSub(sub: DepTarget) {
this.subs.push(sub)
}

removeSub(sub: DepTarget) {
// #12696 deps with massive amount of subscribers are extremely slow to
// clean up in Chromium
// to workaround this, we unset the sub for now, and clear them on
// next scheduler flush.
this.subs[this.subs.indexOf(sub)] = null
if (!this._pending) {
this._pending = true
pendingCleanupDeps.push(this)
}
}

depend(info?: DebuggerEventExtraInfo) {
if (Dep.target) {
Dep.target.addDep(this) // ⭐️ 新增依赖的观察者
if (__DEV__ && info && Dep.target.onTrack) {
Dep.target.onTrack({
effect: Dep.target,
...info
})
}
}
}

notify(info?: DebuggerEventExtraInfo) {
// stabilize the subscriber list first
const subs = this.subs.filter(s => s) as DepTarget[]
if (__DEV__ && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
const sub = subs[i]
if (__DEV__ && info) {
sub.onTrigger &&
sub.onTrigger({
effect: subs[i],
...info
})
}
sub.update() // ⭐️ 通知观察者有更新
}
}
}

官方文档:https://v2.cn.vuejs.org/v2/guide/reactivity.html

watch 的一些属性

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
export default {
data() {
return {
user: {
name: 'xxx',
age: 30
}
}
},
created() {
// 当 immediate 为 false 时,无法触发对应的 watch
// 当 immediate 为 true 时,可以触发对应的 watch
this.changeUserName()
},
methods: {
changeUserName() {
// 当 deep 为 false 时,无法触发对应的 watch
// 当 deep 为 true 时,可以触发对应的 watch
this.user.name = 'yyy'
}
},
watch: {
user: {
handler(newVal, oldVal) {
console.log(`watched ${oldVal} -> ${newVal}`)
},
deep: true,
immediate: true
}
}
}

通过 watch 的源码,可以看到deep、immediate的具体实现
immediate核心源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Vue.prototype.$watch = function (
expOrFn: string | (() => any),
cb: any,
options?: Record<string, any>
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) { // ⭐️ immediate 为 true 时
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
// ⭐️ 执行对应的回调函数并且捕获错误
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn() {
watcher.teardown()
}
}

deep核心源代码:

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
function _traverse(val: any, seen: SimpleSet) {
let i, keys
const isA = isArray(val)
if (
(!isA && !isObject(val)) ||
val.__v_skip /* ReactiveFlags.SKIP */ ||
Object.isFrozen(val) ||
val instanceof VNode
) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else if (isRef(val)) {
_traverse(val.value, seen)
} else {
// ⭐️ 针对对象,获取所有的 keys,然后循环递归处理依赖的监听
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}

Diff 算法

虚拟 DOM:用来描述真实 DOM 的一个 JS 对象
diff 算法:对比新、旧虚拟 DOM 差异的算法
patch(打补丁):把新旧节点的差异应用到真实 DOM 上的操作
当数据更新后,会重新生成 VDOM,通过 DIFF 算法找到新旧 VDOM 的差异,最后通过 patch 进行页面更新
虚拟 DOM 结构(伪代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// html 代码
<div id="app">
<h1>Hello, Virtual DOM!</h1>
<p class="paragraph">This is a paragraph.</p>
</div>

// 对应的虚拟 DOM 结构(简化的伪代码)
const vnode = {
tag: 'div', // 节点的标签名
props: { id: 'app' }, // 节点的属性
children: [ // 子节点数组
{
tag: 'h1',
children: ['Hello, Virtual DOM!']
},
{
tag: 'p',
props: { className: 'paragraph' },
children: ['This is a paragraph.']
}
]
};

虚拟 DOM 源码:https://github.com/vuejs/vue/blob/main/src/core/vdom/vnode.ts

Diff 算法核心

总结:递归、同级、双端
先比较新旧 VDOM 的根节点,然后一层层往下进行同级比较,同级比较时会采用首尾双端来加快对比
diff 流程的源码解析:https://github.com/vuejs/vue/blob/main/src/core/vdom/patch.ts
patch函数的核心流程为:patchVnode -> updateChildren
updateChildren里面进行 diff 并且进行 dom 的更新

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
// ...

export function createPatchFunction(backend) {
// ...

function updateChildren(
parentElm,
oldCh,
newCh,
insertedVnodeQueue,
removeOnly
) {
let oldStartIdx = 0 // 旧-首 下标
let newStartIdx = 0 // 新-首 下标
let oldEndIdx = oldCh.length - 1 // 旧-尾 下标
let oldStartVnode = oldCh[0] // 旧-首 节点
let oldEndVnode = oldCh[oldEndIdx] // 旧-尾 节点
let newEndIdx = newCh.length - 1 // 新-尾 下标
let newStartVnode = newCh[0] // 新-首 节点
let newEndVnode = newCh[newEndIdx] // 新-尾 节点
let oldKeyToIdx, idxInOld, vnodeToMove, refElm

// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly

if (__DEV__) {
checkDuplicateKeys(newCh)
}

// ⭐️ 核心 diff 算法:双指针对比
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 当旧-首下标小于尾下标 && 新-首下标小于尾下标,则表明还未对比完,将继续
if (isUndef(oldStartVnode)) {
// 旧-首 节点不存在时,则往右移一个
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
// 旧-尾 节点不存在时,则往左移一个
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 旧-首 节点 与 新-首 节点 相同时:表明首 节点位置未变
// 调用 patchVnode 函数,进行它俩的子节点对比
patchVnode(
oldStartVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
// 旧-首 节点则往右移一个
oldStartVnode = oldCh[++oldStartIdx]
// 新-首 节点则往右移一个
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 旧-尾 节点 与 新-尾 节点 相同时:表明尾 节点位置未变
// 调用 patchVnode 函数,进行它俩的子节点对比
patchVnode(
oldEndVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
)
// 旧-尾 节点则往左移一个
oldEndVnode = oldCh[--oldEndIdx]
// 新-尾 节点则往左移一个
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 旧-首 节点 与 新-尾 节点 相同时:表明该节点位置往左移了
// 调用 patchVnode 函数,进行它俩的子节点对比
patchVnode(
oldStartVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
)
canMove &&
nodeOps.insertBefore(
parentElm,
oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm)
)
// 旧-首 节点则往右移一个
oldStartVnode = oldCh[++oldStartIdx]
// 新-尾 节点则往左移一个
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
// 旧-尾 节点 与 新-首 节点 相同时:表明该节点位置往右移了
// 调用 patchVnode 函数,进行它俩的子节点对比
patchVnode(
oldEndVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
canMove &&
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// 旧-尾 节点则往左移一个
oldEndVnode = oldCh[--oldEndIdx]
// 新-首 节点则往右移一个
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx))
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
// 当 新-首 节点的 key 不在 旧节点的 keys 内,则表明要走新增逻辑
// New element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
)
} else {
// 当 新-首 节点的 key 在 旧节点的 keys 内,则表明是位置移动了
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// 如果相同 key 对应的新旧节点[相同]时

// 调用 patchVnode 函数,进行它俩的子节点对比
patchVnode(
vnodeToMove,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
// 旧节点该 key 的值充值为 undefined
oldCh[idxInOld] = undefined
canMove &&
nodeOps.insertBefore(
parentElm,
vnodeToMove.elm,
oldStartVnode.elm
)
} else {
// 如果相同 key 对应的新旧节点[不相同]时,则表明要走新增逻辑
// same key but different element. treat as new element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
)
}
}
// 新-首 节点则往右移一个
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
// 当[旧-首 下标] > [旧-尾 下标],则要补充节点
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(
parentElm,
refElm,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
)
} else if (newStartIdx > newEndIdx) {
// 当[新-首 下标] > [新-尾 下标],则要删除节点
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}

function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly?: any
) {
// ....

updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)

// ....
}
function isUndef(v: any): v is undefined | null {
return v === undefined || v === null
}

function isDef<T>(v: T): v is NonNullable<T> {
return v !== undefined && v !== null
}

return function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) { // 新的 Vnode 没有时
// 旧的 Vnode 有时,则需要进行销毁操作
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}

let isInitialPatch = false
const insertedVnodeQueue: any[] = []

if (isUndef(oldVnode)) {
// 如果新的 Vnode 存在,旧的 Vnode 不存在,则需要进行创建操作
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else { // 新旧 Vnode 都存在时则需要执行的核心逻辑 ⭐️
// isRealElement:是否为真正存在的元素
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// ⭐️ 不是真正存在的元素 && 新旧 Vnode 相同时
// ⭐️ patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 是真正存在的元素 || 新旧 Vnode 不相同时
if (isRealElement) {
// 是真正存在的元素时
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
// 服务端渲染
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
// 混合渲染
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (__DEV__) {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// 既不是服务端渲染,又不是混合渲染
// 则基于 oldVnode 创建一个空的 Vnode,并替换它
oldVnode = emptyNodeAt(oldVnode)
}

// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)

// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)

// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
// clone insert hooks to avoid being mutated during iteration.
// e.g. for customed directives under transition group.
const cloned = insert.fns.slice(1)
for (let i = 0; i < cloned.length; i++) {
cloned[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}

// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}

invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}

渲染流程

写在<template> / template选项里面的代码渲染到页面中,有两个阶段:编译、渲染

编译

触发编译的逻辑为:调用 实例.$mount()函数
入口为compileToFunctions函数

  1. 调用compile函数最终生成ast、render、staticRenderFns
    1. <template> / template选项的代码,通过parse函数解析生成为ast
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
new Vue({
template: `
<div>
<h1>你好呀</h1>
<p>{{ msg }}</p>
<p if="array.length">{{ array.length }}</p>
</div>
`,
data() {
return { msg: 'hello, my children', array: [1, 2] }
}
})

// ast 结构:
{
type: 1,
tag: 'div',
attrsList: [],
attrsMap: {},
rawAttrsMap: {},
parent: undefined,
children: [
{
type: 1,
tag: 'h1',
attrsList: [],
attrsMap: {},
rawAttrsMap: {},
parent: [Circular *1],
children: [Array],
start: 20,
end: 32,
plain: true
},
{ type: 3, text: ' ', start: 32, end: 47 },
{
type: 1,
tag: 'p',
attrsList: [],
attrsMap: {},
rawAttrsMap: {},
parent: [Circular *1],
children: [Array],
start: 47,
end: 63,
plain: true
},
{ type: 3, text: ' ', start: 63, end: 78 },
{
type: 1,
tag: 'p',
attrsList: [Array],
attrsMap: [Object],
rawAttrsMap: [Object],
parent: [Circular *1],
children: [Array],
start: 78,
end: 121,
plain: false,
attrs: [Array]
}
],
start: 0,
end: 140,
plain: true
}
  1. 根据生成的ast,调用optimize进行优化:静态节点标记(static:true)
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
{
type: 1,
tag: 'div',
attrsList: [],
attrsMap: {},
rawAttrsMap: {},
parent: undefined,
children: [
{
type: 1,
tag: 'h1',
attrsList: [],
attrsMap: {},
rawAttrsMap: {},
parent: [Circular *1],
children: [Array],
start: 20,
end: 32,
plain: true,
static: true, // ⭐️ 静态节点的标记
staticInFor: false, // ⭐️ 静态节点的标记
staticRoot: false // ⭐️ 静态节点的标记
},
{ type: 3, text: ' ', start: 32, end: 47, static: true },
{
type: 1,
tag: 'p',
attrsList: [],
attrsMap: {},
rawAttrsMap: {},
parent: [Circular *1],
children: [Array],
start: 47,
end: 63,
plain: true,
static: false,
staticRoot: false
},
{ type: 3, text: ' ', start: 63, end: 78, static: true },
{
type: 1,
tag: 'p',
attrsList: [Array],
attrsMap: [Object],
rawAttrsMap: [Object],
parent: [Circular *1],
children: [Array],
start: 78,
end: 121,
plain: false,
attrs: [Array],
static: false,
staticRoot: false
}
],
start: 0,
end: 140,
plain: true,
static: false,
staticRoot: false
}
  1. 根据ast,调用generate函数生成render字符串和staticRenderFns数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// generate 函数的返回值
{
render: `with (this) {
return _c('div', [
_c('h1', [_v('你好呀')]),
_v(' '),
_c('p', [_v(_s(msg))]),
_v(' '),
_c(
'p',
{
attrs: {
if: 'array.length'
}
},
[_v(_s(array.length))]
)
])
}`,
staticRenderFns: []
}
  1. 调用createFunction函数,将其生成render函数
1
2
3
4
5
6
7
8
9
function createFunction(code, errors) {
try {
return new Function(code)
} catch (err: any) {
errors.push({ err, code })
return noop
}
}
$options.render = createFunction(render)
  1. render函数最终被挂载到实例的$options.render

运行

  1. 调用实例挂载的render函数生成vnode
  2. 若在浏览器端则再调用patch函数,进行 diff 与 dom 的更新
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
// ⭐️ 入口代码
vm._update(vm._render())

// 原型对象上的 _render 函数主要功能:调用实例的 render 函数,并将其返回值作为 Vnode 返回
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render } = vm.$options
// ...

let vnode = render.call(vm._renderProxy, vm.$createElement)

// ...

return vnode
}

// 空函数
function noop(a?: any, b?: any, c?: any) {}

// 原型对象上的 __patch__ 属性:根据平台进行初始化
// patch 就是我们《Diff 算法核心》那块
// 源码:https://github.com/vuejs/vue/blob/main/src/core/vdom/patch.ts
Vue.prototype.__patch__ = inBrowser ? patch : noop

// 原型对象上的 _update 函数主要功能:调用实例的 __patch__ 函数进行 diff 与 dom 的更新
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this

// ...

vm.$el = vm.__patch__(prevVnode, vnode)

// ...
}

总结

template编译为render函数,在运行时执行render函数生成vnode,进行diff后再渲染到页面

Vue 中的数据结构

v-dom:

补充知识

Rollup

打包工具,将小的、松散耦合的模块(主要为 js 代码)组合成更大型的应用程序或库,支持 ES6 模块,适用于前端库和组件
与之相应的webpack则适用于应用程序,单应用、多应用等等,支持各种模块加载(css/js/图片等)、按需引入等等功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default {
input: 'src/index.js', // 指定入口文件
output: {
file: 'dist/my-lib.js', // 输出的 bundle 文件路径
format: 'umd', // 输出格式:UMD,可以在浏览器全局作用域下运行,也支持 CommonJS 和 AMD 模块系统
name: 'MyLib', // 全局变量名称,用于在非模块化环境中引用该库
},
plugins: [
resolve(), // 解析第三方模块的绝对路径
commonjs(), // 将 CommonJS 模块转换为 ES6 模块供 Rollup 处理
],
};

Vue2 源码调试

  • 下载源码
  • 安装依赖:pnpm i
  • 在需要的地方加上打印
  • 运行:pnpm test:unit将跑所有的单测,这时候控制台就不停的打印,等运行完毕后,再看结果
  • 如果觉得打印的实在太多了,则可以新增一个命令:
    • "test:unit:newvue": "vitest run test/unit/modules/vdom/create-component.spec.ts"
    • 然后命令行运行:pnpm test:unit:newvue
    • 这也会执行new Vue(...)的操作,并且打印的数据更少

Vue2 中直接通过下标修改数组元素不会触发视图原因是?

Vue2 使用 Object.defineProperty 来实现对对象属性的响应式追踪
Object.defineProperty 只能应用于对象属性,而无法直接应用于数组的索引(因为数组索引不是标准意义上的对象属性)
Vue2 对于数组的响应式处理是通过重写数组的几个可变方法(如 push()、pop() 、shift()等)来间接实现的
以下为数组部分方法重写的源代码:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/

import { TriggerOpTypes } from '../../v3'
import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]

/**
* Define a property.
*/
export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}

/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original 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)
// notify change
if (__DEV__) {
ob.dep.notify({
type: TriggerOpTypes.ARRAY_MUTATION,
target: this,
key: method
})
} else {
ob.dep.notify()
}
return result
})
})

课后参考

人人都能懂的 Vue 源码系列(一)—Vue 源码目录结构_慕课手记


3-4、Vue2-核心源码讲解
https://mrhzq.github.io/职业上一二事/前端面试/前端八股文/3-4、Vue2 核心源码讲解/
作者
黄智强
发布于
2024年1月13日
许可协议