Vue 的响应式原理
· 3 min read
#
解析Vue 使用大篇幅介绍了他的响应式原理,So let's look how it work.
首先问两个问题:
- data 对象中的数据发生改变,Vue 是如何得知的(watch)
- 好的,假设 Vue 知道哪些数据 change 了,那怎么通知对应的 DOM 发生更新呢
我可真会提问题 🤷♂️
So,应该知道的是在 JavaScript 中有监听数据变化的 API Object.defineProperty
和 ES6 的 Proxy 了,
- Vue2
- Vue3
Object.defineProperty
ES6 Proxy
事实上,Vue2 就是通过前者实现的,just like
const data = { message: "可变数据", date: "June 27,2021",};
Object.keys(data).forEach((key) => { let value = data[key];
Object.defineProperty(data, key, { set(newValue) { value = newValue; }, get() { return value; }, });});
好的,第一个问题解决了。接下来要通知谁呢,换言之就是如何收集依赖,这个过程用到了发布/订阅者模式。 为了记录谁订阅了属性的改变,我们定义一个 Dep 类来管理依赖 ✍
// 发布者// Dep类管理依赖class Dep { constructor() { this.subscriptions = []; }
addSub(watcher) { this.subscriptions.push(watcher); }
notify() { this.subscriptions.forEach((item) => { item.update(); }); }}
// 订阅者// 中介类Watcher负责将更新通知到其他地方class Watcher { constructor(name) { this.name = name; }
update() { console.log(this.name + "已经update"); }}
// 封装数据监测Object.defineProperty(data, key, { set(newValue) { value = newValue; dep.notify(); }, get() { return value; },});
这就是 Vue 响应式的大致原理。
#
实现通过 120 行 JavaScript 代码实现 Vue 的双向绑定 🎉🎉
class Vue { constructor(options) { this.$options = options; this.$data = options.data; this.$el = options.el;
new Observer(this.$data); // 代理this.$data的数据 Object.keys(this.$data).forEach((key) => this._proxy(key)); // 处理el挂载 传入Vue实例 new Compiler(this.$el, this); }
_proxy(key) { Object.defineProperty(this, key, { configurable: true, enumerable: true, set(newValue) { this.$data[key] = newValue; }, get() { return this.$data[key]; }, }); }}
class Observer { constructor(data) { this.data = data;
Object.keys(data).forEach((key) => this.defineReactive(this.data, key, data[key]) ); }
defineReactive(data, key, val) { // 为每个属性key创建一个Dep对象 const dep = new Dep();
Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { if (Dep.target) { dep.addSub(Dep.target); } return val; }, set(newValue) { if (newValue === val) { return; } val = newValue; dep.notify(); }, }); }}
class Dep { constructor() { this.subs = []; }
addSub(sub) { this.subs.push(sub); }
notify() { this.subs.forEach((sub) => sub.update()); }}
const reg = /\{\{(.+)\}\}/;class Compiler { constructor(el, vm) { this.el = document.querySelector(el); this.vm = vm;
this.frag = this._createFragment(); this.el.appendChild(this.frag); }
_createFragment() { const frag = document.createDocumentFragment(); let child; while ((child = this.el.firstChild)) { this._compile(child); frag.appendChild(child); } return frag; }
_compile(node) { if (node.nodeType === 1) { const attrs = node.attributes; if (attrs.hasOwnProperty("v-model")) { const name = attrs["v-model"].nodeValue; node.addEventListener("input", (e) => (this.vm[name] = e.target.value)); } } if (node.nodeType === 3) { if (reg.test(node.nodeValue)) { const name = RegExp.$1.trim(); new Watcher(node, name, this.vm); } } }}
class Watcher { constructor(node, name, vm) { this.node = node; this.name = name; this.vm = vm; Dep.target = this; this.update(); Dep.target = null; }
update() { this.node.nodeValue = this.vm[this.name]; }}
That's all🖼