Vue
coderljw 2024-10-13 大约 8 分钟
# 1. 数据响应式
Vue2 的数据响应式依赖 Object.defineProperty(Vue3 采用 Proxy)和发布订阅模式。
- 数据初始化时定义响应式对象:initState -- > observe -- > defineReactive -> Object.defineProperty
export function defineReactive( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() // ... let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { // ... // 收集依赖:添加订阅者(watcher) if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter(newVal) { // ... // 派发更新:通知订阅者(watcher) dep.notify() }, }) }
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- 挂载组件时收集依赖:mountComponent -> vm._update(vm._render(), hydrating),在解析模板生成 ast 时会标记 vm 上的数据,执行 render 函数时会访问改数据(getter)。
export function mountComponent( vm: Component, el: ?Element, hydrating?: boolean ): Component { // ... let updateComponent if () { // ... } else { // 更新组件(patch) updateComponent = () => { vm._update(vm._render(), hydrating) } } // 每个组件都有一个对应的 RenderWatcher new Watcher( vm, updateComponent, noop, { before() { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } }, }, true /* isRenderWatcher */ ) // ... }
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- 改变响应式对象的属性时派发更新:setter(Object.defineProperty) -> dep.notify -> watcher.update -> queueWatcher -> nextTick -> flushSchedulerQueue -> watcher.run -> watcher.get -> watcher.getter -> updateComponent -> vm._update(vm._render(), hydrating),最终 patch 后更新视图。
function flushSchedulerQueue() { // ... // 队列排序 queue.sort((a, b) => a.id - b.id) for (index = 0; index < queue.length; index++) { // ... // 执行 run watcher.run() // ... } // ... }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16Vue2 中 Array 为非响应式对象,Vue3 使用 Proxy 不会有此限制。
- Array.length 的 configurable 默认为 false(不可重新配置),也就不能使用 Object.defineProperty 重新定义 length 为响应式。vm.items[overflowLength] = value 及原生 push、pop 等可改变原数组长度的方法也就不能触发视图更新,而 Vue 重写了这些原生方法,在其中手动通知视图更新(src/core/observer/array (opens new window))。
- vm.items[indexOfItem] = newValue 不使用 Object.defineProperty 定义为响应式对象,是由于性能代价和获得的用户体验收益不成正比(为什么 vue 没有提供对数组属性的监听 (opens new window))。Vue 提供 Vue.set(vm.$set)、Vue.delete(vm.$delete)方法,在其中手动通知视图更新(src/core/observer/index (opens new window))。
-
function isObject(obj) { return ( typeof obj === 'object' && !Array.isArray(obj) && obj !== null && obj !== undefined ) } function observe(obj) { if (!isObject(obj)) { throw new TypeError() } Object.keys(obj).forEach(key => { let internalValue = obj[key] let dep = new Dep() Object.defineProperty(obj, key, { get() { dep.depend() return internalValue }, set(newValue) { const isChanged = internalValue !== newValue if (isChanged) { internalValue = newValue dep.notify() } }, }) }) } window.Dep = class Dep { constructor() { this.subscribers = new Set() } depend() { if (activeUpdate) { this.subscribers.add(activeUpdate) } } notify() { this.subscribers.forEach(subscriber => subscriber()) } } let activeUpdate // 作用:确保 update() 执行时,depend 可以访问到 activeUpdate function autorun(update) { // 作用:保证依赖项一直是最新的 function wrappedUpdate() { activeUpdate = wrappedUpdate update() activeUpdate = null } wrappedUpdate() } const state = { count: 0, } observe(state) autorun(() => { console.log(state.count) }) state.count++
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
# 2. Watcher
- computed:computed watcher 是一个
lazy: true
的 watcher,通过 dirty 控制是否重新计算 value。
实例化 Watcher 时
this.dirty = this.lazy
为 true;解析模板取值时dirty: true
计算 value(evaluate),计算完后dirty: false
;之后取值直接返回 watcher.value,仅当依赖的 dep 变化触发 dep.notify 执行 watcher.update 后dirty: true
,取值时重新计算后dirty: false
。// computed:getter function createComputedGetter(key) { return function computedGetter() { // ... if (watcher) { // dirty 为 true 重新计算 if (watcher.dirty) { watcher.evaluate() } // 收集依赖 if (Dep.target) { watcher.depend() } return watcher.value } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17export default class Watcher { constructor() { // ... // 实例化赋值 dirty this.dirty = this.lazy // ... // 计算属性实例化 Watcher 不会进行取值操作 this.value = this.lazy ? undefined : this.get() } // dep setter 触发 notify 执行 update update() { if (this.lazy) { // 取值后将 dirty 置为 true this.dirty = true } // ... } // 计算值 evaluate() { this.value = this.get() // 计算完将 dirty 置为 false this.dirty = false } }
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- computed:computed watcher 是一个
- watch & $watch:user watcher 是用户自定义 watcher,会处理回调错误,可选择配置 deep 和 immediate。
- immediate:立即触发回调。
- deep:深度监听对象(递归遍历方式收集依赖)。
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): 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) { const info = `callback for immediate watcher "${watcher.expression}"` pushTarget() // 立即触发回调 invokeWithErrorHandling(cb, vm, [watcher.value], vm, info) popTarget() } // 返回取消观察函数 return function unwatchFn() { watcher.teardown() } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24export default class Watcher { get() { // ... try { // ... } catch (e) { // ... } finally { // 深度收集依赖 if (this.deep) { traverse(value) } } return value }, // 取消观察 teardown () { if (this.active) { if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } } }
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// 递归遍历收集依赖(seenObjects 处理循环依赖) export function traverse(val: any) { _traverse(val, seenObjects) seenObjects.clear() } function _traverse(val: any, seen: SimpleSet) { let i, keys const isA = Array.isArray(val) if ( (!isA && !isObject(val)) || 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 { keys = Object.keys(val) i = keys.length while (i--) _traverse(val[keys[i]], seen) } }
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- Vuex strict mode:sync watcher 是 Vuex 严格模式下的 watcher。不经过 nextTick 异步执行,可以保证所有的状态变更都能被调试工具跟踪到(严格模式 - Vuex (opens new window))。
export default class Watcher { update() { if (this.lazy) { // ... } else if (this.sync) { // 同步执行 run this.run() } // ... } }
1
2
3
4
5
6
7
8
9
10
# 3. extend
- Vue.extend 使用原型继承的方式构造一个 Vue 的子类 Sub,并对构造的 Sub 进行缓存处理。
Vue.extend = function (extendOptions: Object): Function {
const Super = this
const SuperId = Super.cid
// ...
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
// ...
const Sub = function VueComponent(options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
// ...
// cache constructor
cachedCtors[SuperId] = Sub
return Sub
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 4. proxy
- proxy 的作用是把 props、data 等代理到实例上,使得可以通过 this 访问。
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop,
}
export function proxy(target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 重置 data
Object.assign(this.$data, this.$options.data.call(this))
// 重置 data 上的 form 属性
this.form = this.$options.data.call(this).form
1
2
3
4
5
2
3
4
5
# 5. nextTick
nextTick 在异步任务中遍历 callbacks 执行回调函数,采用的异步任务优先级:Promise -> MutationObserver -> setImmediate(IE 与 Node) -> setTimeout。
使用 callbacks 而不是在 nextTick 直接执行回调函数,可以保证在同一个 tick 内多次执行 nextTick 不会开启多个异步任务。
若 nextTick 没有传入回调函数,那么会返回一个 Promise。
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
<div id="div" @click="onClick">{{ papa }}</div>
</div>
</template>
<script>
export default {
data() {
return {
papa: 'jack',
}
},
methods: {
onClick() {
// 异步:在组件 RenderWatcher 之后执行,视图已更新
this.$nextTick().then(() => {
console.log(div.innerHTML, 'promise') // pony
})
// 在组件 RenderWatcher 之前执行,视图尚未更新
this.$nextTick(() => {
console.log(div.innerHTML, 'papa before') // jack
})
// queue 添加 RenderWatcher
this.papa = 'pony'
// 在组件 RenderWatcher 之后执行,视图已更新
this.$nextTick(() => {
console.log(div.innerHTML, 'papa after') // pony
})
},
},
}
</script>
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
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
<template>
<div>
<div id="div" @click="onClick">{{ papa }}</div>
<div>{{ assets }}</div>
</div>
</template>
<script>
export default {
data() {
return {
papa: 'jack',
assets: 777,
}
},
methods: {
onClick() {
// 异步:在组件 RenderWatcher 之后执行,视图已更新
this.$nextTick().then(() => {
console.log(div.innerHTML, 'promise') // pony
})
// queue 添加 RenderWatcher
this.assets = 888
// 在组件 RenderWatcher 之后执行,视图已更新
this.$nextTick(() => {
console.log(div.innerHTML, 'papa before') // pony
})
// RenderWatcher 已在 queue 队列中,这里不会再次添加 RenderWatcher
// https://github.com/vuejs/vue/blob/v2.6.14/src/core/observer/scheduler.js#L165-L167
this.papa = 'pony'
// 在组件 RenderWatcher 之后执行,视图已更新
this.$nextTick(() => {
console.log(div.innerHTML, 'papa after') // pony
})
},
},
}
</script>
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
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
# 6. compiler
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions
): CompiledResult {
// 解析 template 生成 ast
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 优化 ast
optimize(ast, options)
}
// 生成可执行代码
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns,
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 7. v-for 与 v-if
由于在 Vue2 中 v-for 的优先级高于 v-if,当 v-for 与 v-if 连用时会存在性能浪费(src/compiler/codegen/index (opens new window))。
Vue3 中 v-if 的优先级高于 v-for,无此问题。(packages/compiler-core/src/compile (opens new window))。
<template>
<div>
<!-- bad -->
<ul>
<li v-for="(item, index) in list" v-if="index % 2">
{{ item }}
</li>
</ul>
<!-- good -->
<ul>
<li v-for="(item, index) in filterList">
{{ item }}
</li>
</ul>
<!-- bad -->
<ul>
<li v-for="(item, index) in list" v-if="visible">
{{ item }}
</li>
</ul>
<!-- good -->
<ul>
<template v-if="visible">
<li v-for="(item, index) in list">
{{ item }}
</li>
</template>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
list: Array(100)
.fill(0)
.map((_, index) => index),
visible: false,
}
},
computed: {
filterList({ list }) {
return list.filter((_, index) => index % 2)
},
},
}
</script>
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
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