响应式原理(对象)

2021-08-11 23:04:30 Vue 大约 3 分钟

说起vue的响应式原理,大家也许只知道它的底层是对js的原生方法defineProperty的封装,而具体的实现却是一头雾水。下面我们就一探究竟。

# 1、封装defineProperty

主要目的:就是存取拦截;在get中收集依赖,在set中触发依赖;

依赖是什么?依赖就是用到数据的地方,那个地方用到了这个属性,那么我们就通知谁,也就是触发依赖;

const Dep = require('./Dep.js')
const Observer = require('./Observer.js')
/**
 * 存取描述符
 */
function defineReactive(data, key, val) {
  // 新增 递归子属性;
  if (typeof val === 'object') {
    new Observer(val)
  }
  // 每一个属性都会有一个依赖表,用来收集自己的依赖;
  let dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      // 收集依赖
      dep.depend()
      return val
    },
    set: function (newval) {
      if (val === newval) {
        return
      }
      val = newval
      // 通知触发收集的依赖项
      dep.notify()
    }
  })
}
exports.defineReactive =  defineReactive
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

# 2、封装 Dep的类;

主要目的:用于处理依赖。

/**
 * 收集依赖
 */
let uid = 0

/**
 * 此id 会在每一次new Dep的时候执行,也就是只会在 首次转换为 get 和set 的时候执行;
 */
 class Dep {
  constructor () {
    // 收集依赖的集合
    this.id = uid++
    this.subs = []
  }

  // 添加依赖;
  addSub (sub) {
    this.subs.push(sub)
  }

  // 删除依赖;
  removeSub (sub) {
    remove(this.subs, sub)
  }

  // 收集依赖
  depend () {
    if (global.target) {
      global.target.addDep(this)
    }
  }

  // 通知
  notify () {
    const subs = this.subs.slice()
    for (let i = 0; i < subs.length; i++) {
      subs[i].update()
    }
  }
}

function remove (arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item)
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}

exports.Dep = Dep

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

# 3、Observer;

作用:将一个数据内的所有的属性都转换成getter/setter的形式。然后去追踪他们的变化;

const defineReactive = require('./defineReactive.js')

class Observer {
  constructor (value) {
    this.value = value
    if (!Array.isArray(value)) {
      this.walk(value)
    }
  }

  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
     defineReactive.defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
}
exports.Observer = Observer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 4、Watcher;

一个 Watcher 中介的角色,收集的依赖就是它。

因为它知道你要访问的属性;

class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    this.deps = []
    this.depIds = new Set()
    // 执行this.getter(),就可以读取data.a.b.c的内容
    this.getter = parsePath(expOrFn)
    this.cb = cb
    this.value = this.get()
  }

  get() {
    // 把依赖存在 global.target 上,
    global.target = this
    // 这一步会触发 getter 函数从而把自己添加到依赖表中;value 是读取到的值。
    let value = this.getter.call(this.vm, this.vm)
    // 避免重复添加依赖,但是在通知更新的时候依然添加了重复的依赖;
    global.target = undefined
    return value
  }

  update() {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }

  addDep(dep) {
    const id = dep.id
    /**
     * 主要目的:避免通知更新的时候重复收集依赖;
     */
    if (!this.depIds.has(id)) {
      this.depIds.add(id)
      this.deps.push(dep)
      dep.addSub(this)
    }
  }

  // 取消观察;
  teardown() {
    let i = this.deps.length
    while (i--) {
      this.deps[i].removeSub(this)
    }
  }
}

// 用于根据路径来读取属性的值;
const bailRE = /[^\w.$]/

function parsePath(path) {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      // obj[segments[i]] 在读取值时会触发 get 拦截函数;
      obj = obj[segments[i]]
    }
    return obj
  }
}

exports.Watcher = 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
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

# 5、使用;

实现:vm.$watch()

const Observer = require('./Observer.js')
const Watcher = require('./Watcher.js')

var data = {
  name: '小红',
  age: 18,
  skill: {
    dance: '芭蕾舞',
    exercise: '跑步'
  }
}
// 观测所有的属性的变化
new Observer.Observer(data)

function Vm() {}

Vm.prototype.$watch = function (obj, path, cb) {
  const watcher = new Watcher.Watcher(obj, path, cb)

  // 取消观察
  return function unwatchFn() {
    watcher.teardown()
  }
}
var vm = new Vm()

vm.$watch(data, 'skill.dance', (newVal, oldVal) => {
  console.log('我执行了', newVal, oldVal)
})
data.skill.dance = '哈哈=='
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
object
    ├── defineReactive.js
    ├── Dep.js
    ├── index.js
    ├── Observer.js
    ├── Watcher.js
1
2
3
4
5
6

# 总结;

vue通过对js的原生方法的封装;来达到对数据的访问和赋值的拦截,然后做执行自己的逻辑代码;

要想达到对数据的响应,就要对数据进行处理,就是把用到这个数据的地方收集起来,然后当这个值发生变化时,通过便会通过触发get函数,来通知用到这个数据的地方,则其修改;

1589695503414

上次编辑于: 2023年7月4日 09:36