Auc的个人博客

vuePress-theme-reco Auc    2020 - 2021
Auc的个人博客 Auc的个人博客

选择一个主题吧~

  • 暗黑
  • 自动
  • 明亮
主页
分类
  • JavaScript
  • Vue
  • 数据结构与算法
  • 文档
  • 面试题
  • 笔记
标签
笔记
  • CSS
  • ES6
  • JavaScript
  • Vue
  • C语言
文档
  • Vueのapi
  • Vue Router
  • axios
  • Vue CLI
面试
  • JS
  • Vue
  • 基础
时间线
联系我
  • GitHub (opens new window)
author-avatar

Auc

62

文章

11

标签

主页
分类
  • JavaScript
  • Vue
  • 数据结构与算法
  • 文档
  • 面试题
  • 笔记
标签
笔记
  • CSS
  • ES6
  • JavaScript
  • Vue
  • C语言
文档
  • Vueのapi
  • Vue Router
  • axios
  • Vue CLI
面试
  • JS
  • Vue
  • 基础
时间线
联系我
  • GitHub (opens new window)
  • Vue

    • Vue 无法检测的数组改动问题
    • Vue 组件之间的通信方式
    • 造轮子 - 双向数据绑定

造轮子 - 双向数据绑定

vuePress-theme-reco Auc    2020 - 2021

造轮子 - 双向数据绑定

Auc 2020-07-10 Vue

# 造轮子 - 双向数据绑定

# 监听器 Observer 的实现

大名鼎鼎的 Object.defineProperty 相读者并不陌生,忘记的点击传送门 (opens new window)哦。

在 Observer 中其实就是利用了上述 API 来给 vm 中需要观测的属性添加了取值器 getter, 以及存值器 setter, 数据劫持就是那么回事儿。

# 简单看下 defineProperty()

下面先使用 Object.defineProperty 来简单观测一个对象。

let name = 'tom';
let person = {};
Object.defineProperty(person, name, {
  get() {
    console.log('name属性被读取了...');
    return val;
  },
  set(newVal) {
    console.log('name属性被修改了...');
    val = newVal;
  }
})
console.log(person);
1
2
3
4
5
6
7
8
9
10
11
12
13

这样,就给原来的空对象 person 添加了新属性 name, 并为其添加了存值器与取值器。读取 name 时触发取值器,修改 name 时触发存值器。

# 封装一个函数

上述手段并不高明(shēng dòng),当需要观测的属性一多,就需要一个个添加,不如封装个方法。

比如员工跟老板存一家银行,老板账户里有大把 money, 员工账户里 ...(暗示 zhǎng gōng zī)。一般银行都有短信服务,这个不多说了,直接看代码。

// 一家银行的几个账户
const myBank = {
  account1: '$1000000',
  account2: '$1',
  account3: '$1',
  account4: '$1',
  account5: '$1'
};
// 专为银行提供短信服务的供应商
function bankSMS(obj) {
  if (!(obj instanceof Object)) {
    return;
  };
  const keysArr = Object.keys(obj);
  keysArr.forEach((key) => {
    addSMS(obj, key, obj[key]);
  });
  return obj;
};
// 为该账户添加短信服务
function addSMS(obj, key, val) {
  Object.defineProperty(obj, key, {
    get () {
      console.log(`账户: ${key}; 余额: ${val} --- 主人~您还养的起我嘛?`);
      return val;
    },
    set (newVal) {
      console.log(`账户: ${key} --- 小金库被动了呢!`);
      return val = newVal;
    }
  });
};
// 使用
bankSMS(myBank);
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

结果分析:

读取对象 myBank 被观测属性时触发取值器;当其中一个员工实现一个“小目标”时, 触发存值器。这样对象里面的属性都是可观测的了。

# Vue 源码

什么?上面的还满足不了你?你这个年轻人不讲“武德”,那手写下 Vue 源码吧。

1. 设计思路

2. 代码

// 首先定义一个 Observer 类,传入原始数据对象,用来生成属性可被观测的实例
export class Observer {
  // 生成实例
  constructor (value) {
    // 1. 先将原数据对象的值拷贝到实例上
    this.value = value;
    // 给 value 添加一个 __ob__ 属性,值为 Observer 实例,可以认为指向实例
    // 作用是给 value 打一个标记,来表示此值是响应式的了,避免重复操作。
    // def(value, '__ob__', this);

    // 2. 然后在实例上,将每个属性都变成可观测的

    // 2.1 判断如果 value 为数组,我们之后再分析,先看 value 为对象的情况
    if (Array.isArray(value)) {
      // ... 下面我直接 retun 了
      return;
    } else {
      // 2.2 不是数组,执行实例原型上的 walk 方法
      this.walk(value);
    }
  }
  // 2.2 walk 方法,挂载到 Observer 的原型上
  // 作用: 遍历实例上的所有属性,并为其添加响应式功能
  walk (obj) {
    // 2.2.1 判断是否为对象,源码用的 ts 更简洁
    if (!(obj instanceof Object)) {
      return;
    }
    // 2.2.2 利用 Object.keys() 方法提取实例上所有 key 到一个数组
    const keys = Object.keys(obj);
    // 遍历数组,给每一个属性添加响应式功能
    for (let i = 0; i < keys.length; i++) {
      defineReactive (obj, keys[i]);
    };
  }
}
// definReactive 方法
// 功能:调用 Object.defineProperty(),给对象中的属性添加 getter setter,使其变得可观测。
function defineReactive (obj, key, val) {
  // 实参如果只传了两个,手动设置键值 val
  if (arguments.length === 2) {
    val = obj[key];
  }
  // 如果该属性为一个引用类型,递归调用 new Observer,使实例中属性值为对象的内部属性,也变得可被观测
  if (typeof val === 'Object') {
    new Observer(val);
  }
  // 调用 Object.defineProperty(), 给对象中的属性添加 getter setter,使其变得可观测
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get () {
      console.log(`账户: ${key}; 余额: ${val} --- 主人~您还养的起我嘛?`);
      return val;
    },
    set (newVal) {
      if (val === newVal) {
        return;
      }
      console.log(`账户: ${key} --- 小金库被动了呢!`);
      val = newVal;
    }
  });
}
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

# 订阅器 Dep 的实现

Dep 订阅器,用于收集 Watcher, 对监听器 Observer 和 订阅者 Watcher 进行统一管理。

即:

  • 每一个被 Observer 管理的可观测属性都拥有属于自己的 dep。
  • dep 中有一个属性为依赖于该可观测属性的 Watcher 构成的数组
  • 当取用和修改可观测属性时,会通过 dep 中不同的函数做出不同的处理,比如为可观测属性添加 watcher、通知 watcher 做出视图更新动作等。
// 为可观测的数据属性,构建了 dep 类,其示例为用于存储依赖于这个属性的 Watcher 构成的数组。 
export class Dep {
  constructor () {
    subs: []
  }
  // 添加 Watcher
  depend () {
    if (window.target) {
      this.addSub(window.target);
    }
  }
  addSub (sub) {
    this.subs.push(sub);
  }
  // 删除 Watcher
  removeSub (sub) {
    // 调用 remove()
    remove(this.subs, sub);
  }
  // 通知 Watcher 数组中的元素更新
  notify () {
    let subs = this.subs.slice();
    for (let i = 0; i < subs.length; i++) {
      subs[i].update();
    }
  }
}

// remove() 用于删除指定索引的数组元素。
function remove(arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item);
    if (index > -1) {
      arr.splice(index, 1);
    }
  }
}
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

构建完成之后,需要在 Observer 中使用

  • get () 中使用 dep.depend => 为其添加 watcher
  • set () 中使用 dep.notify => 通知 watcher 更新
function defineReactive (obj, key, val) {
  if (arguments.length === 2) {
    val = obj[key];
  }
  if (typeof val === 'Object') {
    new Observer(val);
  }
  // 实例化一个 Dep 订阅器
  const dep = new Dep();
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get () {
      console.log(`账户: ${key}; 余额: ${val} --- 主人~您还养的起我嘛?`);
      // 在 getter 中收集 watcher
      dep.depend();
      return val;
    },
    set (newVal) {
      if (val === newVal) {
        return;
      }
      console.log(`账户: ${key} --- 小金库被动了呢!`);
      // 在 setter 中通知 watcher 更新
      val = newVal;
      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

# 订阅者 watcher 的实现

【TODO】