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
  