Roger Leung‘s Epcot

vuePress-theme-reco Roger Leung ( z3rog )    2018 - 2021
Roger Leung‘s Epcot

Choose mode

  • dark
  • auto
  • light
Blog
Note
Github (opens new window)
author-avatar

Roger Leung ( z3rog )

18

Article

20

Tag

Blog
Note
Github (opens new window)
  • 首页
  • 框架与工具链

    • Vue 3
    • Vue 2
    • Webpack 4
  • 前端性能优化

    • 性能优化的必要性
    • 性能指标
    • 基本手段
    • 离线缓存
  • 浏览器机制

    • 架构
    • 导航
    • 渲染机制
    • 缓存机制
  • 网络协议

    • TCP 协议
    • HTTP 协议
    • HTTPS 协议
    • HTTP 2 协议
    • HTTP 3 协议
  • 其他

    • V8 中的快慢属性与快慢数组
    • V8 解析执行 JavaScript 流程简述
    • V8 的垃圾回收机制
    • 100 行代码写一个破产版 Vite
    • 浅谈微前端

Vue 2

vuePress-theme-reco Roger Leung ( z3rog )    2018 - 2021

Vue 2

Roger Leung ( z3rog ) 2019-10-08 Vue

深入理解 Vue 2 ,附源码分析

vue-architecture

Vue 有三个与响应式相关的核心概念:

  • Observer:将数据处理为响应式数据,负责对数据进行监听,数据变化时触发通知
  • Dep:负责依赖管理与分发,每一个响应式数据需要依赖其他哪些数据,全部记录在 Dep 中
  • Watcher:Observer 与 Dep 的中间人,负责订阅与触发更新,连接数据与指令。在编辑阶段,由 Watcher 来告诉 Dep 数据有何依赖,Dep 进行收集;数据变化后 Dep 需要找到相应的 Watcher,由具体的 Watcher 触发模版的更新与渲染。

发布订阅模式 + 观察者模式

# Observer

Observer 的构造器是这样定义的:

src/core/observer/index.js


class Observer {
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 增加响应式数据标识
    def(value, '__ob__', this)
  
    if (Array.isArray(value)) {
      // 是否支持隐式原型 __proto__
      if (hasProto) {
        // 将数组的隐式原型改写为一个代理对象,见下文「处理响应式数组」分析
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 最后再监听该数组
      this.observeArray(value)
    } else {
      // value 是对象时,为每个元素 defineReactive
      this.walk(value)
    }
  }
}
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

可以看到,Vue 对数组、对象的响应式处理有所区别。

# 处理响应式对象

对于对象,遍历对象 obj 的每一个 key,分别调用 defineReactive()。

src/core/observer/index.js

class Observer {
  // 承接上面代码, 此处省略...
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}

function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 此处省略... 

  // 如果属性值是对象 / 数组会继续递归去监听,然后返回子 observer
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    get: reactiveGetter() {
      // TODO: dep.depend() 收集依赖
      // if (childOb) childOb.dep.depend() 
    },
    set: reactiveSetter() {
      // TODO: dep.notify(),分发,触发所有依赖对应的 Watcher
    }
  })
}
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

# 处理响应式数组

数组相对于对象有一些特殊,它的元素是有序的,以下标作为标识。如果我们像处理对象一样处理数组,并调用了一些可能引起元素顺序变化的 API(如 shift、unshift、splice),就有可能触发多个 setter 与 getter。例如使用 unshift 向数组的前面追加一个元素,导致原来所有元素逐一向后移位,这时候就触发所有原有元素的 getter 与 setter。可以尝试查看 codepen 这个例子 (opens new window)。

Vue 为了解决这个问题,对数组的原型方法进行了拦截与重写。

src/core/observer/array.js

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto) // 新建一继承 Array 原型的空对象

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

/**
 * 重写、拦截数组的方法
 */
methodsToPatch.forEach(function (method) {
  const original = arrayProto[method] // 数组原型上的方法

  // 将 arrayMethods 设置为一个可对操作数组方法进行拦截的响应式对象
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args) // 调用原生方法获取结果
    const ob = this.__ob__

    let inserted // 获取新插入的值
    switch (method) {
      // push、unshift,所有参数均是新插值
      case 'push':
      case 'unshift':
        inserted = args
        break
      // splice 的第三个参数是新值
      case 'splice':
        inserted = args.slice(2)
        break
    }

    if (inserted) ob.observeArray(inserted)

    ob.dep.notify() // 此时只需要触发一次更新通知
    return result
  })
})
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

对原型的拦截与重写后,调用数组方法会先经过代理对象 arrayMethods ,如果恰好是 methodsToPatch 中声明的方法,实际上调用代理对象的同名方法,也就是对数组操作一次只触发一次 dep.notify(),避免上面 codepen 例子 中 getter、setter 触发多次。

# Dep

Observer 的构造器中实例化了一个 dep(this.dep = new Dep()),也就是,每一个 Observer 都对应一个 dep。

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

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

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

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    const subs = this.subs.slice()

    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
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

如前面所说,Dep 是负责依赖管理与分发的,它记录了数据之间的依赖关系,以及每条数据发生变化后需要通知哪些 Watcher 。在 defineReactive 中我用注释省略了具体代码,实际上在 getter 与 setter 中分别有调用 Dep 实例的 depend 与 notify。。

  • depend:方法用于添加 Watcher 的依赖
  • addSub 、removeSub :对订阅者的增删管理
  • notify:发布函数,数据发生变化时通知对应的 Watcher 执行相应操作

小结

Observer 在 defineReactive 后实现了:

  • 获取数据时触发 getter, getter 会调用 Dep 作依赖收集,Dep 维护「数据间的依赖关系」和「数据与 Watcher 的依赖关系」
  • 设置数据时触发 setter,setter 会调用 Dep 作消息分发,通知依赖该数据的数据及 Watcher 进行更新

# Watcher

Watcher 是订阅者模式中的订阅者。Vue 中,Watcher 分为数据渲染 Watcher(Render Watcher) 与选项 Watcher。其中渲染 Watcher 是组件级别的,每一个组件都有一个渲染 Watcher,而数据 Watcher 是根据开发者编写的 data / watch / computed 选项、调用 Vue 原型上的 $watch 生成的 Watcher。

疑问 🤔️

Vue 收集依赖的入口是哪?何时触发?Watcher 什么时候实例化?

我们从 Vue 生命周期的顺序理一下。 Vue Compiler 会将 <template></template> 中的内容编译为 render 函数,比如:

<template>
  <div class="example">{{ msg }}</div>
</template>
1
2
3

编译为一个包含渲染函数的对象:

{
  render: function () {
    with (this) {
      return _c('div',{ staticClass: "example" },[ _v(_s(msg)) ])
    }
  },
  ...
}

1
2
3
4
5
6
7
8
9

你可以尝试使用 Runtime + Compiler 版本的 Vue 并调用 Vue.compile() 查看结果。这是 Vue 在线编译生成的 render 函数,本地开发中 .vue 单文件被打包后也类似,对 Compiler 暂不深究

当 Vue 需要渲染时,自然触发了 msg 的 getter,此时,实例化 watcher, 并将 Dep.target 指向当前实例化的 watcher,开始依赖收集,收集完毕后重置 Dep.target。也就是说所有 watcher 都是按序实例化、按序收集依赖,因为 JS 单线程执行,Dep.target 即使在全局中是唯一的,当前 watcher 也不会影响到其他 watcher 的依赖收集过程(Dep.target 相当于是一个瞬时 watcher 指针)。

// src/core/observer/watcher.js
class Watcher {
  constructor() {
    // 省略若干代码...

    this.value = this.lazy
      ? undefined
      : this.get()
  }

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      if (this.deep) {
        // deep watch
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
}
// src/core/observer/dep.js
export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}
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

# computed

一句话总结

computed 的本质是 lazy watcher

回过头来看 Watcher 的构造器中最后一句:

return this.value = this.lazy
  ? undefined
  : this.get()
1
2
3

这个 lazy 正是专门为 computed 设计的标识。在 src/core/instance/state.js 中这行代码说明一切:

const computedWatcherOptions = { lazy: true }
1

前面说过,依赖收集是在触发 getter 时进行的,而 computed 的数据,只在它依赖的数据发生变化后才需要初始化,因此对于 computed 来说,实例化 watcher 时并不需要马上调用 this.get()。

我们从生命周期的角度理一遍这里面的逻辑。

  1. Vue 初始化,调用 initState(vm)

src / core / instance / init.js

// ...
initInjections(vm)
initState(vm)
initProvide(vm)
// ...
1
2
3
4
5
  1. initState 时初始化 props、methods、data,监听 data。

src / core / instance / state.js

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

调用 observe 时数据时创建一个 Observer 实例,然后对 data 执行前面所述的依赖收集过程。在 initComputed 之前 已经将 data 处理为响应式数据。

  1. initComputed,为每一个 computed 属性调用 new Watcher 并存储到 _computedWatchers 对象中,此时因为 lazy 为 true,_computedWatchers 中所有 key 对应的值都是 undefined,但 defineComputed 为他们分别定义了专属于 computed 属性的 getter 和 setter。getter 在进行依赖收集前将 watcher 的 lazy 置为 false,watcher.value 就能正确返回了。之后 computed 属性依赖的数据发生了变化,会通知到 computedWatcher 修改它的 value。
function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    if (!isSSR) {
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}

// 读者自行查阅 defineComputed,如果前面都能理解,相信很容易看明白
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

TO BE CONTINUED