实现双向绑定的方式
- backbone 发布者-订阅者模式
- angular 脏检查
- vue 数据劫持结合发布者-订阅者模式
Vue 响应式原理
- 数据劫持 / 数据代理
- 依赖收集
- 发布订阅模式
响应式的具体步骤:
在 init 阶段 (defineReactive), Vue 对象属性 data 的属性会被 reactive 化,即会被设置 getter 和 setter 函数。所有被 Vue reactive 化的属性都有一个 Dep 对象与之对应。
- 需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化, 获取值会触发 getter, 进行依赖收集。
- compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
- Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:
- 在自身实例化时往属性订阅器 (dep) 里面添加自己
- 自身必须有一个
update()方法 - 待属性变动
dep.notify()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
- MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。
- 依赖的拆卸。Watch 实例中包含 2 对属性,名字分别为 depId, deps/newDepIds, newDeps。 其中 depId/newDepIds 是一个 Set 值,用来保证依赖只会被收集一次。另外每一次重新求值之后,根据新旧 Set 中的差异,会将已经不依赖的依赖删除收集的关系;在一个组件的 destroy 钩子被调用的时候,会主动执行 Watch 的拆卸函数,目的是将不再使用的依赖全部拆卸掉,避免后续不必要的计算。
var data = { name: "yiliang114" };
observe(data);
let name = data.name; // -> get value
data.name = "yyy"; // -> change value
function observe(obj) {
// 判断类型
if (!obj || typeof obj !== "object") {
return;
}
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
function defineReactive(obj, key, val) {
// 递归子属性
observe(val);
Object.defineProperty(obj, key, {
// 可枚举
enumerable: true,
// 可配置
configurable: true,
// 自定义函数
get: function reactiveGetter() {
console.log("get value");
return val;
},
set: function reactiveSetter(newVal) {
console.log("change value");
val = newVal;
},
});
}
以上代码简单的实现了如何监听数据的 set 和 get 的事件,但是仅仅如此是不够的,因为自定义的函数一开始是不会执行的。只有先执行了依赖收集,才能在属性更新的时候派发更新,所以接下来我们需要先触发依赖收集。
<div>{{name}}</div>
在解析如上模板代码时,遇到 {{name}} 就会进行依赖收集。
接下来我们先来实现一个 Dep 类,用于解耦属性的依赖收集和派发更新操作。
// 通过 Dep 解耦属性的依赖和更新操作
class Dep {
constructor() {
this.subs = [];
}
// 添加依赖
addSub(sub) {
this.subs.push(sub);
}
// 更新
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null;
当需要依赖收集的时候调用 addSub,当需要派发更新的时候调用 notify。
在组件挂载时,会先对所有需要的属性调用 Object.defineProperty(),然后实例化 Watcher,传入组件更新的回调。在实例化过程中,会对模板中的属性进行求值,触发依赖收集。
class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this;
this.cb = cb;
this.obj = obj;
this.key = key;
this.value = obj[key];
Dep.target = null;
}
update() {
// 获得新值
this.value = this.obj[this.key];
// 调用 update 方法更新 Dom
this.cb(this.value);
}
}
在执行构造函数的时候将 Dep.target 指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher 然后执行 update 函数。
function defineReactive(obj, key, val) {
// 递归子属性
observe(val);
let dp = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log("get value");
// 将 Watcher 添加到订阅
if (Dep.target) {
dp.addSub(Dep.target);
}
return val;
},
set: function reactiveSetter(newVal) {
console.log("change value");
val = newVal;
// 执行 watcher 的 update 方法
dp.notify();
},
});
}
以上所有代码实现了一个简易的数据响应式,核心思路就是手动触发一次属性的 getter 来实现依赖收集。
Vue 无法检测到对象属性的添加或删除。Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value)方法向嵌套对象添加响应式属性。
watch 属性,都是一个 new Watcher 实例。
进行观察的时候执行 defineReactive 函数,在每一次执行的时候,都是创建一个 Dep 依赖实例, get 属性中 如果存在 Dep.target (也就是响应函数的 watcher 实例)那就执行依赖收集,这个 Dep.target 的收集是在 new Watcher 的时候,new Watcher 第二个参数是一个函数表达值,会被记录下来,表明这是当前响应式的反馈。
每一个 watcher 里面有一个依赖数组,dep.depend() 依赖收集就是往 watcher 里依赖项。当然如果添加的属性是一个对象或者数组的话,继续添加依赖,在 watcher 表达式执行完毕之前,会清理掉 Dep.target ,防止会收集到错误的依赖。 然后在属性改变的时候,就会触发 set ,set 中基本上是观察新添加的属性之外最重要的就是 dep.notify 通知操作了。这个操作就是循环收集的所有 watcher ,执行 watcher 的 update 函数, update 函数如果设置了同步执行的话,就是直接重新执行一次表达式; 但是一般情况下,会执行 queueWatcher 将当前的 watcher 推入一个事件缓冲队列 queue 中, 这是通过 nextTick 实现的, 会一次更新队列中的所有 watcher 的表达式。这样就达到了响应式的目的。(队列执行的时候,会根据 watcher.id 进行去重)
Object.defineProperty
通过 Object.defineProperty(obj, prop, descriptor) 来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
- 对象属性描述符
- configurable 可配置性相当于属性的总开关,只有为 true 时才能设置,而且不可逆
- enumerable 是否可枚举,为 false 时 for..in 以及 Object.keys()将不能枚举出该属性
- 数据描述符
- value
- writable 是否可写,为 false 时将不能够修改属性的值
- 存取描述符
- get
- set
存在的问题
- 不能监听数组的变化 数组的以下几个方法不会触发 set,push、pop、shift、unshift、splice、sort、reverse
let arr = [1, 2, 3];
let obj = {};
Object.defineProperty(obj, "arr", {
get() {
console.log("get arr");
return arr;
},
set(newVal) {
console.log("set", newVal);
arr = newVal;
},
});
obj.arr.push(4); // 只会打印 get arr, 不会打印 set
obj.arr = [1, 2, 3, 4]; // 这个能正常 set
- 必须遍历对象的每个属性 使用 Object.defineProperty() 多数要配合 Object.keys() 和遍历,于是多了一层嵌套
Object.keys(obj).forEach((key) => {
Object.defineProperty(obj, key, {
// ...
});
});
- 必须深层遍历嵌套的对象 如果嵌套对象,那就必须逐层遍历,直到把每个对象的每个属性都调用 Object.defineProperty() 为止。 Vue 的源码中就能找到这样的逻辑 (叫做 walk 方法)。
Object.defineProperty 的缺陷
- Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
- 无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
- 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。
通过下标方式修改数组数据或者给对象新增属性并不会触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。
对于第一个问题,Vue 提供了一个 API 解决
export function set(target, key, val) {
// 判断是否为数组且下标是否有效
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 调用 splice 函数触发派发更新
// 该函数已被重写
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
// 判断 key 是否已经存在
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
const ob = target.__ob__;
// 如果对象不是响应式对象,就赋值返回
if (!ob) {
target[key] = val;
return val;
}
// 进行双向绑定
defineReactive(ob.value, key, val);
// 手动派发更新
ob.dep.notify();
return val;
}
Vue 重写方法来对数组的劫持实现派发更新。 例如 push、pop、splice 等方法。对于这些变异方法 vue 做了包裹,在原型上进行了拦截,调用原生的数组方法后,还会执行发布和变更的操作来触发视图的更新。
// 获得数组原型
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
// 重写以下函数
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
methodsToPatch.forEach(function (method) {
// 缓存原生函数
const original = arrayProto[method];
// 重写函数
def(arrayMethods, method, function mutator(...args) {
// 先调用原生函数获得结果
const result = original.apply(this, args);
const ob = this.__ob__;
let inserted;
// 调用以下几个函数时,监听新数据
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted);
// 手动派发更新
ob.dep.notify();
return result;
});
});
Vue3 的响应式
https://segmentfault.com/a/1190000022198316
其他
几种实现双向绑定的做法
- 发布者-订阅者模式(backbone.js)
- 脏检查 (angular.js)
- 数据劫持(vue.js) 是采用数据劫持结合发布者-订阅者模式的方式,通过
Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
Object.defineProperty
属性描述符包括:configurable(可配置性相当于属性的总开关,只有为 true 时才能设置,而且不可逆)、Writable(是否可写,为 false 时将不能够修改属性的值)、Enumerable(是否可枚举,为 false 时 for..in 以及 Object.keys()将不能枚举出该属性)、get(一个给属性提供 getter 的方法)、set(一个给属性提供 setter 的方法)
var o = { name: "vue" };
Object.defineProperty(o, "age", {
value: 3,
writable: true, //可以修改属性a的值
enumerable: true, //能够在for..in或者Object.keys()中枚举
configurable: true, //可以配置
});
Vue 的响应式
- 需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
- compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
- Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:
- 在自身实例化时往属性订阅器(dep)里面添加自己
- 自身必须有一个 update()方法
- 待属性变动 dep.notify()通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调,则功成身退。
- MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。
计算属性是如何做到属性值改变才重新计算(缓存)
- 初始化 props 和 data, 使用
Object.defineProperty把这些属性全部转为getter/setter。 - 初始化
computed, 遍历computed里的每个属性,每个 computed 属性都是一个 watcher 实例。每个属性提供的函数作为属性的 getter,使用 Object.defineProperty 转化。 Object.defineProperty getter依赖收集。用于依赖发生变化时,触发属性重新计算。- 若出现当前 computed 计算属性嵌套其他 computed 计算属性时,先进行其他的依赖收集
Proxy
Vue.js 3.x 中使用 Proxy 对象重写响应式系统。
- 无需一层层递归为每个属性添加代理,一次性监听所有属性,性能上更好。
- 监听动态新增、删除的属性
- 监听数组的索引和 length 属性
唯一缺陷可能就是浏览器的兼容性不好了。
Proxy
下面是 Proxy 的几个主要特点:
- 拦截器(handler):Proxy 对象的第一个参数是一个目标对象,第二个参数是一个拦截器对象,其中拦截器对象包含一些方法,用来拦截底层操作。
- 可代理的操作:Proxy 可以代理目标对象的各种操作,例如读取属性、写入属性、函数调用等。
- 自定义处理逻辑:在拦截器对象中,我们可以定义自己的处理逻辑,并在底层操作被触发时执行相应的处理逻辑。
下面通过代码示例来进一步说明 Proxy 的用法:
const person = {
name: "Tom",
age: 18,
};
const handler = {
get(target, key) {
console.log(`get ${key}`);
return target[key];
},
set(target, key, value) {
console.log(`set ${key} to ${value}`);
target[key] = value;
return true;
},
};
const proxyPerson = new Proxy(person, handler);
proxyPerson.name; // 触发 getter
proxyPerson.age = 20; // 触发 setter
下面的例子是拦截第一个字符为下划线的属性名,不让它被 for of 遍历。
let target = {
_bar: "foo",
_prop: "bar",
prop: "baz",
};
let handler = {
ownKeys(target) {
return Reflect.ownKeys(target).filter((key) => key[0] !== "_");
},
};
let proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
console.log(target[key]);
}
// "baz"
Proxy 代理器
他可以实现 js 中的“元编程”:在目标对象之前架设拦截,可以过滤和修改外部的访问。
它支持多达 13 种拦截操作,例如下面代码展示的set和get方法,分别可以在设置对象属性和访问对象属性时候进行拦截。
拦截方式
- get():拦截对象属性读取
- set():拦截对象属性设置,返回布尔值
- has():拦截对象属性检查
k in obj,返回布尔值 - deleteProperty():拦截对象属性删除
delete obj[k],返回布尔值 - defineProperty():拦截对象属性定义
Object.defineProperty()、Object.defineProperties(),返回布尔值 - ownKeys():拦截对象属性遍历
for-in、Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols(),返回
数组
- getOwnPropertyDescriptor():拦截对象属性描述读取
Object.getOwnPropertyDescriptor(),返回对象 - getPrototypeOf():拦截对象读取
instanceof、Object.getPrototypeOf()、Object.prototype.__proto__、Object.prototype.isPrototypeOf()、Reflect.getPrototypeOf(),返回对象 - setPrototypeOf():拦截对象设置
Object.setPrototypeOf(),返回布尔值 - isExtensible():拦截对象是否可扩展读取
Object.isExtensible(),返回布尔值 - preventExtensions():拦截对象不可扩展设置
Object.preventExtensions(),返回布尔值 - apply():拦截 Proxy 实例作为函数调用
proxy()、proxy.apply()、proxy.call() - construct():拦截 Proxy 实例作为构造函数调用
new proxy()
const handler = {
// receiver 指向 proxy 实例
get(target, property, receiver) {
console.log(`GET: target is ${target}, property is ${property}`);
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
console.log(`SET: target is ${target}, property is ${property}`);
return Reflect.set(target, property, value);
},
};
const obj = { a: 1, b: { c: 0, d: { e: -1 } } };
const newObj = new Proxy(obj, handler);
/**
* 以下是测试代码
*/
newObj.a; // output: GET...
newObj.b.c; // output: GET...
newObj.a = 123; // output: SET...
newObj.b.c = -1; // output: GET...
运行这段代码,会发现最后一行的输出是 GET ...。也就是说它触发的是get拦截器,而不是期望的set拦截器。这是因为对于对象的深层属性,需要专门对其设置 Proxy。
Proxy 与 Object.defineProperty 的对比
数据劫持: 在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果,数据劫持的典型应用就是双向数据绑定。
Object.defineProperty 虽然已经能够实现双向绑定了,但是他还是有缺陷的。
- 只能对属性进行数据劫持,所以需要深度遍历整个对象
- 对于数组不能监听到数据的变化
虽然 Vue 中确实能检测到数组数据的变化,但是其实是使用了 hack 的办法,并且也是有缺陷的。
Proxy 重点难点:
- 要使
Proxy起作用,必须针对实例进行操作,而不是针对目标对象进行操作 - 没有设置任何拦截时,等同于
直接通向原对象 - 属性被定义为
不可读写/扩展/配置/枚举时,使用拦截方法会报错 - 代理下的目标对象,内部
this指向Proxy代理
Object.defineProperty
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
Proxy
Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等),代理会将所有应用到它的操作转发到这个目标对象上。
let target = {};
let handler = {
get: function (obj, name) {
console.log("get");
return name in obj ? obj[name] : 37;
},
set: function (obj, name, value) {
console.log("set");
obj[name] = value;
},
};
// target: 用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
// handler: 一个对象,其属性是当执行一个操作时定义代理的行为的函数
let p = new Proxy(target, handler);
p.a = 1; // 进行set操作,并且操作会被转发到目标
p.b = undefined; // 进行set操作,并且操作会被转发到目标
console.log(p.a, p.b); // 1, undefined ,进行get操作
console.log("c" in p, p.c); // false, 37 进行get操作
console.log(target); // {a: 1, b: undefined}. 操作已经被正确地转发
Proxy 解决问题
- 针对对象 Proxy 是针对 整个对象 obj 的。因此无论 obj 内部包含多少个 key ,都可以走进 set。(并不需要通过 Object.keys() 的遍历),解决了 Object.defineProperty() 必须遍历对象的每个属性的问题。
let obj = {
name: "Eason",
age: 30,
};
let handler = {
get(target, key, receiver) {
console.log("get", key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log("set", key, value);
return Reflect.set(target, key, value, receiver);
},
};
let proxy = new Proxy(obj, handler);
proxy.name = "Zoe"; // set name Zoe
proxy.age = 18; // set age 18
Reflect.get 和 Reflect.set 可以理解为类继承里的 super,即调用原来的方法
- 支持数组 Proxy 不需要对数组的方法进行重载,省去了众多 hack,减少代码量等于减少了维护成本,而且标准的就是最好的
let arr = [1, 2, 3];
let proxy = new Proxy(arr, {
get(target, key, receiver) {
console.log("get", key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log("set", key, value);
return Reflect.set(target, key, value, receiver);
},
});
proxy.push(4);
// 能够打印出很多内容. 会触发多次 handler 的问题 vue 是怎么解决的?
// get push (寻找 proxy.push 方法)
// get length (获取当前的 length)
// set 3 4 (设置 proxy[3] = 4)
// set length 4 (设置 proxy.length = 4)
- 嵌套支持 Proxy 也是不支持嵌套的,这点和 Object.defineProperty() 是一样的。因此也需要通过逐层遍历来解决。Proxy 的写法是在 get 里面递归调用 Proxy 并返回
let obj = {
info: {
name: "eason",
blogs: ["webpack", "babel", "cache"],
},
};
let handler = {
get(target, key, receiver) {
console.log("get", key);
// 递归创建并返回
if (typeof target[key] === "object" && target[key] !== null) {
return new Proxy(target[key], handler);
}
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log("set", key, value);
return Reflect.set(target, key, value, receiver);
},
};
let proxy = new Proxy(obj, handler);
// 以下两句都能够进入 set
proxy.info.name = "Zoe";
proxy.info.blogs.push("proxy");
总结
Proxy / Object.defineProperty 两者的区别:
- 当使用 defineProperty,我们修改原来的 obj 对象就可以触发拦截,而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截
- defineProperty 必须深层遍历嵌套的对象。 Proxy 不需要对数组的方法进行重载,省去了众多 hack,减少代码量等于减少了维护成本,而且标准的就是最好的
Proxy 对比 defineProperty 的优势
- Proxy 的第二个参数可以有 13 种拦截方法,这比起 Object.defineProperty() 要更加丰富
- Proxy 作为新标准受到浏览器厂商的重点关注和性能优化,相比之下 Object.defineProperty() 是一个已有的老方法
- Proxy 的兼容性不如 Object.defineProperty() (caniuse 的数据表明,QQ 浏览器和百度浏览器并不支持 Proxy,这对国内移动开发来说估计无法接受,但两者都支持 Object.defineProperty())
- 不能使用 polyfill 来处理兼容性
接下来我们将会分别用 Proxy / Object.defineProperty 来实现双向绑定
用 Proxy 与 Object.defineProperty 实现双向绑定
<body>
hello,world
<input type="text" id="model" />
<p id="word"></p>
</body>
<script>
const model = document.getElementById("model");
const word = document.getElementById("word");
var obj = {};
const newObj = new Proxy(obj, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log("setting", target, key, value, receiver);
if (key === "text") {
model.value = value;
word.innerHTML = value;
}
return Reflect.set(target, key, value, receiver);
},
});
model.addEventListener("keyup", function (e) {
newObj.text = e.target.value;
});
</script>
Vue 的双向数据绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的,那么我们起码要做以下三个步骤:
- 实现一个监听器 Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
- 实现一个订阅者 Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
- 实现一个解析器 Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
流程图如下: 
实现 Observer
使用 Object.defineProperty 定义一个 Observer
function defineProperty(obj, key, value) {
Observer(value); // 递归遍历所有子属性
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
set(newValue) {
if (value === newValue) {
return;
}
value = newValue;
console.log(`set ${key}: ${newValue}`);
},
get() {
console.log(`get ${key}: ${value}`);
return value;
},
});
}
function Observer(data) {
if (!data || typeof data !== "object") {
// 非对象即终止遍历
return;
}
Object.keys(data).forEach(function (key) {
defineReactive(data, key, data[key]); // 监听所有对象属性
});
}
实现 Dep
创建一个用来存储订阅者 Watcher 的订阅器,订阅器 Dep 主要负责收集订阅者,然后再属性变化的时候执行对应订阅者的更新函数。
function Dep() {
this.list = [];
}
Dep.prototype = {
addSub: function (watcher) {
this.list.push(watcher);
},
notify: function () {
this.list.forEach(function (watcher) {
watcher.update();
});
},
};
实现 Watcher
既然实现了一个订阅器,那么就需要一个订阅者,订阅者 Watcher 在初始化的时候需要将自己添加进订阅器 Dep 中,
- 在自身实例化时往属性订阅器(dep)里面添加自己
- 待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发回调,更新视图
function Watcher(obj, key, cb) {
this.cb = cb;
this.obj = obj;
this.key = key;
// 此处为了触发属性的getter,从而在dep添加自己
this.value = this.get();
}
Watcher.prototype = {
update: function () {
this.run(); // 属性值变化收到通知
},
run: function () {
var value = this.get(); // 取到最新值
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.obj, value, oldVal); // 执行Compile中绑定的回调,更新视图
}
},
get: function () {
Dep.target = this; // 将当前订阅者指向自己
var value = this.obj[this.key]; // 触发getter,添加自己到属性订阅器中
Dep.target = null; // 添加完毕,重置
return value;
},
};
实现了订阅器和订阅者之后,需要将订阅器添加进入订阅者,将 Observer 改造以下植入订阅器。如果不好理解可以结合 watcher 一起看。
function defineProperty(obj, key, value) {
Observer(value); // 递归遍历所有子属性
var dep = new Dep(); // 生成一个Dep实例
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
set(newValue) {
if (value === newValue) {
return;
}
value = newValue;
console.log(`set ${key}: ${newValue}`);
dep.notify(); // 如果数据变化,通知所有订阅者
},
get() {
if (Dep.target) {
dep.addSub(Dep.target); // 在这里添加一个订阅者,这里的Dep.target是指订阅器本身
}
console.log(`get ${key}: ${value}`);
return value;
},
});
}
Observer 改造完成后,已经具备了监听数据, 添加订阅器和数据变化通知订阅者的功能。接下来就是将 watcher 添加进入订阅者,模拟实现 Compile,并进行数据初始化。
模拟实现 Compile
我们这里不解析指令所以直接写出 watcher,并添加进去订阅者
function inputChange(event) {
data.value = event.target.value
}
>
function clickChange() {
data.value = '你好 世界'
}
function renderInput(newValue) {
if (input) {
input.value = newValue
}
}
>
function renderText(newValue) {
if (text) {
text.innerHTML = newValue
}
}
new Watcher(data, 'value', renderInput)
new Watcher(data, 'value', renderText)
数据初始化
let data = {
value: "",
};
Observer(data);
这样一个简单的基于 Object.defineProperty 的双向数据绑定就完成了。
Object.defineProperty 和 proxy 的区别
Object.defineProperty
该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
// obj: 要在其上定义属性的对象。
// prop: 要定义或修改的属性的名称。
// descriptor: 将被定义或修改的属性的描述符。
Object.defineProperty(obj, prop, descriptor);
函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符和存取描述符。
这就意味着你可以:
Object.defineProperty({}, "num", {
value: 1,
writable: true,
enumerable: true,
configurable: true,
});
也可以:
var value = 1;
Object.defineProperty({}, "num", {
get: function () {
return value;
},
set: function (newValue) {
value = newValue;
},
enumerable: true,
configurable: true,
});
两者均具有以下两种键值:
configurable
当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,也能够被删除。默认为 false。
enumerable
当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。
数据描述符同时具有以下可选键值:
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
writable
当且仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变。默认为 false。
存取描述符同时具有以下可选键值:
get
一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。
set
一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。
descriptor这个字段是必须的,如果不进行任何配置,你可以这样:
var obj = Object.defineProperty({}, "num", {});
console.log(obj.num); // undefined