# 7.响应式原理

# 要点:

  1. 依赖:我们把用了某个数据 a 的组件,称作 a 的依赖。即 watcher,谁用到了数据,谁就是依赖
  2. 每个数据,都有一个Dep依赖管理器,来管理哪些组件用了它。(即收集依赖 wathcer)
  3. getter 中收集组件(依赖),setter 中更新组件(依赖)

# 原理

  1. 视图(view)变化更新数据(data)。这个很简单,通过事件监听就可以实现
  2. 数据(data)变化更新视图(view)。通过数据劫持(即Object.defineProperty( )方法)结合发布者-订阅者模式

总流程,三步走:

  1. Observer
    1. Observe 一个对象 obj
    2. 判断是否ob已经被响应化了,判断是否为数组,对数组七种方法进行特殊处理
    3. 给对象 obj 新增一个 dep,依赖数组
    4. 调用 defineReactive 递归监听所有属性 Object.defineProperty()
    5. 在子数据的 getter 里面将 Dep.target(即 watcher,订阅者,可能为一个 Vue 组件),dep.addSub 添加到 dep 里面
    6. 在子数据的 setter 里面,添加 dep.notify 方法,通知 watcher(即 Vue 组件)要更新了
    7. dep.notify:遍历 dep 实例中的 subs(即订阅者列表,watcher),subs[i].update()
  2. Watcher
    1. this.callback.call(this.vm, value, oldVal); 触发 watcher 实例化时传入的回调(一般是更新 DOM 的函数)
    2. 如:
new Watcher(this.vm, exp, function(value) {
    self.updateText(node, value);
});
1
2
3
  1. Complie
    1. 解析 Dom 树,找到 v-modal, {{}}, ::data = ,这种属性并且初始化数据。
    2. 使用 DocumentFragement,对 DOM 节点进行文档拆分
    3. 同时在里面 new Watcher 进行订阅数据变化

在这里插入图片描述

# 实现过程

  1. 实现一个监听器 Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。

  2. 实现一个订阅者 Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。

  3. 实现一个解析器 Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。

Ps:因为订阅者是有很多个,所以我们需要有一个消息订阅器 Dep 来专门收集这些订阅者

在这里插入图片描述

# 1. 实现一个 Observer(Object.defineProperty( ))

Observer = (Object.defineProperty() 监听 + 递归 ) + 订阅器

Observer 是一个数据监听器,其实现核心方法就是前文所说的 Object.defineProperty( )。 如果要对所有属性都进行监听的话,那么可以通过递归方法遍历所有属性值,并对其进行 Object.defineProperty( )处理。

  • 思路分析中,需要创建一个可以容纳订阅者的消息订阅器 Dep,订阅器 Dep 主要负责收集订阅者,然后再属性变化的时候执行对应订阅者的更新函数。
  • 所以显然订阅器需要有一个容器,这个容器就是list,将上面的 Observer 稍微改造下,植入消息订阅器: Observer 一旦有了 set 触发,就会通知到 Dep
/**
 * Observer类会通过递归的方式把一个对象的所有属性都转化成可观测对象
 */
export class Observer {
  constructor(value) {
    this.value = value;
    // 给value新增一个__ob__属性,值为该value的Observer实例
    // 相当于为value打上标记,表示它已经被转化成响应式了
    // 作用:
    // 1. 避免重复操作
    // 2. 重写数组push、unshift、splice方法的时候,可以通过this.__ob__获取到当前的实例,给新push的数据设置响应式
    def(value, "__ob__", this);
    // value.__ob__ = this
    if (Array.isArray(value)) {
      // 当value为数组时的逻辑
      // 重写数组的七种方法
    } else {
      this.walk(value);
    }
  }

  walk(obj: Object) {
    var self = this;
    Object.keys(obj).forEach(function (key) {
      self.defineReactive(obj, key, obj[key]);
    });
  }
}

function defineReactive(data, key, val) {
  // 这里用到闭包,将val闭包到函数里面,与newVal进行比较
  // 递归遍历所有子属性
  if (typeof val === "object") {
    new Observer(val);
  }
  var dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      //我们将订阅器Dep添加一个订阅者设计在getter里面,
      //这是为了让Watcher初始化进行触发,
      //因此需要判断是否要添加订阅者。
      // Dep.target 就是 一个watcher,也是一个回调函数
      if (Dep.target) {
        // 判断是否需要添加订阅者
        dep.addSub(Dep.target); // 在这里添加一个订阅者
      }
      return val;
    },
    set: function (newVal) {
      if (val === newVal) {
        return;
      }
      val = newVal;
      console.log(
        "属性" + key + "已经被监听了,现在值为:“" + newVal.toString() + "”"
      );
      dep.notify(); // 如果数据变化,通知所有订阅者
    },
  });
}
Dep.target = null;

var library = {
  book1: {
    name: "",
  },
  book2: "",
};

function obeserve(value) {
  if (typeof value != "object") return;
  let ob;
  if (typeof value.__ob__ !== "undefined") {
    ob = value.__ob__;
  } else {
    ob = new Observer(value);
  }
  return ob;
  // 关键是new Observer(library);
}
observe(livrary);
library.book1.name = "vue权威指南"; // 属性name已经被监听了,现在值为:“vue权威指南”
library.book2 = "没有此书籍"; // 属性book2已经被监听了,现在值为:“没有此书籍”

//下面是订阅器
export default class Dep {
  constructor() {
    this.subs = [];
  }

  addSub(sub) {
    this.subs.push(sub);
  }
  // 删除一个依赖
  removeSub(sub) {
    remove(this.subs, sub);
  }
  // 通知所有依赖更新
  notify() {
    const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}

/**
 * Remove an item from an array
 */
export function remove(arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item);
    if (index > -1) {
      return 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
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119

在 setter 函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。到此为止,一个比较完整 Observer 已经实现了,接下来我们开始设计 Watcher。

# 2. 实现 Watcher

我们说:谁用到了数据,谁就是依赖。这个“谁”,就是 watcher,他可以是一个 dom 节点中的 text 文本。 Vue1 中, 用到数据的 Dom 都是依赖 Vue2 中, 用到数据的组件都是依赖 getter 中收集依赖, setter 中触发依赖

  • 订阅者 Watcher 在初始化的时候需要将自己添加进订阅器 Dep 中
  • 而监听器 Observer 是在 get 函数执行了添加订阅者 Wather 的操作的,所以我们只要在订阅者 Watcher 初始化的时候触发对应的 get 函数去执行添加订阅者操作即可
  • 这里还有一个细节点需要处理,我们只要在订阅者 Watcher 初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在 Dep.target 上缓存下订阅者,添加成功后再将其去掉就可以了。订阅者 Watcher 的实现如下:
// 如
// new Watcher(this.vm, exp, function(value) {
//     self.updateText(node, value);
// });

function Watcher(vm, exp, cb) {
  this.cb = cb;
  this.vm = vm;
  this.exp = exp;
  this.value = this.get(); // 将自己添加到订阅器的操作
}

Watcher.prototype = {
  update: function () {
    this.run();
  },
  run: function () {
    var value = this.vm.data[this.exp];
    var oldVal = this.value;
    if (value !== oldVal) {
      this.value = value;
      this.cb.call(this.vm, value, oldVal);
    }
  },
  get: function () {
    Dep.target = this; // 缓存自己
    var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数
    Dep.target = null; // 释放自己
    return 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
26
27
28
29
30
31

这时候,我们需要对监听器 Observer 也做个稍微调整,主要是对应 Watcher 类原型上的 get 函数。需要调整地方在于 defineReactive 函数:

到此为止,简单版的 Watcher 设计完毕,这时候我们只要将 Observer 和 Watcher 关联起来,就可以实现一个简单的双向绑定数据了。

# 3. 实现 Compile(事件监听)

虽然上面已经实现了一个双向数据绑定的例子,但是整个过程都没有去解析 dom 节点,而是直接固定某个节点进行替换数据的,所以接下来需要实现一个解析器 Compile 来做解析和绑定工作。解析器 Compile 实现步骤:

  1. 解析模板指令,并替换模板数据,初始化视图
  2. 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器
  3. 为了解析模板,首先需要获取到 dom 元素,然后对含有 dom 元素上含有指令的节点进行处理
function Compile(el, vm) {
  this.vm = vm;
  this.el = document.querySelector(el);
  this.fragment = null;
  this.init();
}
Compile.prototype = {
  init: function () {
    this.fragment = this.nodeToFragment(this.el);
    this.compileElement(this.fragment);
    this.el.appendChild(this.fragment);
  },
  nodeToFragment: function (el) {
    var fragment = document.createDocumentFragment();
    var child = el.firstChild;
    while (child) {
      // 将Dom元素移入fragment中
      fragment.appendChild(child);
      child = el.firstChild;
    }
    return fragment;
  },
  compileElement: function (el) {
    var childNodes = el.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function (node) {
      var reg = /\{\{(.*)\}\}/;
      var text = node.textContent;

      if (self.isElementNode(node)) {
        self.compile(node);
      } else if (self.isTextNode(node) && reg.test(text)) {
        self.compileText(node, reg.exec(text)[1]);
      }

      if (node.childNodes && node.childNodes.length) {
        self.compileElement(node);
      }
    });
  },
  compileText: function (node, exp) {
    var self = this;
    var initText = this.vm[exp];
    this.updateText(node, initText);
    new Watcher(this.vm, exp, function (value) {
      self.updateText(node, value);
    });
  },
  updateText: function (node, value) {
    node.textContent = typeof value == "undefined" ? "" : 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
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

大概总结到这里,详情还请看:vue 的双向绑定原理及实现 (opens new window)

# Object.defineProperty 缺点

  1. 无法监听数组变化
// 解决方案:AOP思想
// 新建一个空数组,在该空数组上面创建七种方法,然后讲要监听的数组的原型指向该空数组
const aryMethods = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];
const arrayAugmentations = [];

aryMethods.forEach((method) => {
  // 这里是原生Array的原型方法
  let original = Array.prototype[method];

  // 将push, pop等封装好的方法定义在对象arrayAugmentations的属性上
  // 注意:是属性而非原型属性
  arrayAugmentations[method] = function () {
    console.log("我被改变啦!");

    // 调用对应的原生方法并返回结果
    return original.apply(this, arguments);
  };
});

let list = ["a", "b", "c"];
// 将我们要监听的数组的原型指针指向上面定义的空数组对象
// 别忘了这个空数组的属性上定义了我们封装好的push等方法
// Object.setPrototypeOf(list, arrayAugmentations)
list.__proto__ = arrayAugmentations;
list.push("d"); // 我被改变啦! 4

// 这里的list2没有被重新定义原型指针,所以就正常输出
let list2 = ["a", "b", "c"];
list2.push("d"); // 4
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
  1. 只能劫持对象属性,无法深层监听
  2. 无法对对象新增的 key/value 进行响应式处理
  3. 如果一个数据有 1000 个属性,那么就要给这 1000 个属性使用 Object.defineProperty,这样在初始化页面的时候会造成卡顿。如果用代理的话,那么只需要执行一次就可以了

# dep 和 watcher 为什么要互相收集

因为他们是多对多的关系

  1. dep 是一个数组,在 defineproperty 中监听数据变化,使用 notify 方法通知 watcher 去更新
  2. watcher 收集 dep
    1. 为了 computed 这个属性
      1. 假设:计算属性在 template 上面,但是依赖的 data 不在 template 上,那么 vnode 不会访问到所依赖的 data,依赖这些 data 的 dep 则无法更新 watcher
      2. 所以在 computed 属性执行的时候,会让 person 所依赖的 firstName 和 lastName 的 dep 去收集当前的 watcher
    2. 用于解除 watcher 的订阅
Last Updated: 6/3/2024, 1:08:34 AM