我们想要一个数据发生改变时,与其相关的数据、视图模型自动发生变化。首先要知道数值变化了。
在Angular的方法是使用zone.js把如setTimeout、XHR、点击事件等可能引起模型变化的异步操作用一个wrapFn包裹起来,每当有异步操作发生时Angular就知道数据可能变化了。再遍历组件树,通知组件进行变化检测。若有变化则重新渲染页面。
而Vue采用的方式,则是利用Object.defineProperty定义setter,再确认数据变化后,通知相关的依赖。
观察者模式
先引入一个设计模式——观察者模式。了解观察者模式的话可以跳过直接看下文。我们假设有三种人:
- 好事者,他们对感兴趣的事情很上心,发生了什么事情都想第一时间知道。
- 消息灵通的人,他们收集信息,提供给感兴趣的好事者。
-
观察者,他们想狗仔队一样监视着目标,一有发现就告诉消息灵通的人。
通过这种消息传递的方式,使得观察者和好事者解耦,观察者只管观察,好事者只管八卦。
接下来我们来抄袭Vue实现变化侦测,Vue是基于观察者模式实现的。
1.观察者
在js中,有两种方式可以获取对象的变化:Object.defineProperty、Proxy。Object.defineProperty只能获取对象属性的读取,是ES5规范内容浏览器兼容性好。Proxy更强大,可以拦截对对象的各种访问、改变,但是ES6内容所以兼容较差。由于我们只是为了了解响应式框架的原理,不是做实用轮子,所以我们采用Proxy的方式实现。
为了获取对象的变化,我们对读、写、删除进行拦截。这个代理,就相当于一个观察者,监视着对象的一举一动。
function defineReactive(obj: any): any {
return new Proxy(obj, {
get: function (target, property, receiver) {
console.log('属性被读');
return Reflect.get(target, property, receiver);
},
set: function (obj, prop: (keyof Object), value, receiver) {
if (value === obj[prop]) {
return false;
}
console.log('属性被修改');
return Reflect.set(obj, prop, value, receiver);
},
deleteProperty: function (target: any, p: string | number | symbol) {
console.log('属性被删除');
return Reflect.deleteProperty(target, p);
}
})
}
2.消息灵通的人
我们通过打印得知了对象的变化,但这并没什么卵用。我们需要谁对它感兴趣。例如,我们有如下模板时,这个视图模型就对message感兴趣,它需要知道message的值是什么它才知道要渲染成怎样的视图,即它依赖message了。
<span>{{ message }}</span>
为此,我们要搞一个消息灵通的人来用于记录及管理感兴趣的好事者。定义一个类Dep(dep for dependency):
export class Dep {
static target;//用来存放好事者
public subs : Array<any>;
constructor (){
this.subs = [];
}
public addSub(sub){
this.subs.push(sub);
}
public removeSub(sub){
//有一个好事者说不感兴趣了
}
public depend(){
//假设我们用target这个全局变量存放一个好事者
//我们把它添加到感兴趣的人群里
this.addSub(Dep.target);
}
public notify(){
for(let sub of this.subs){
//发生变化时,通知感兴趣的好事者
sub.update();
}
}
}
如果有人读过一个object的属性,我们就认为这个人对这个object是感兴趣的。那么当这个object发生变动时,我们就要通知这些感兴趣的人。此时我们改造一下defineReactive方法:
function defineReactive(obj: any): any {
const dep = new Dep();//创建一个依赖管理
return new Proxy(obj, {
get: function (target, property, receiver) {
dep.depend();//告诉dep,有人感兴趣
return Reflect.get(target, property, receiver);
},
set: function (obj, prop: (keyof Object), value, receiver) {
if (value === obj[prop]) {
return false;
}
dep.notify();//让dep通知感兴趣的人,有值被改了
return Reflect.set(obj, prop, value, receiver);
},
deleteProperty: function (target: any, p: string | number | symbol) {
dep.notify();//让dep通知感兴趣的人,有值被删除了
return Reflect.deleteProperty(target, p);
}
})
}
3.好事者
好事者会对一件事表示感兴趣,当得到这事的消息时会作出反应。举个例子,我们创建一个好事者,他表示对蔡徐坤感兴趣,而当他知道蔡徐坤开始打篮球时,会大嚷大叫:
new Watcher('蔡徐坤', (status)=>{
if(status === '打篮球'){
console.log('蔡徐坤来打篮球啦!!');
}
})
为实现这样的功能,可以写出以下代码:
class Watcher {
public cb : Function; //回调函数,这个人发现消息之后会做什么事情
public vm : ViewModel;
private getter: Function;//用来获取感兴趣的消息
private value: any;//消息
constructor (
expOrFn : string | Function,
cb : Function
){
this.cb = cb;
if(typeof expOrFn === 'function'){
this.getter = expOrFn;
}else{
this.getter = parsePath(expOrFn);
}
//get()方法会去访问expOrFn对应的值,会触发proxy中的get
//进而将这个watcher添加到dep里 即让消息灵通人的知道我感兴趣
this.value = this.get();
}
public get(){
Dep.target = this;//记录自己,用于让上文中的dep知道好事者是谁
const value = this.getter.call(this.vm, this.vm);//触发了proxy的get!
Dep.target = undefined;
return value;
}
public update(){
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}
}
/**
* \w为 a-z A-Z 0-9
* [^]是排除字符组
* 这个正则意思是 排除字母 数组 . $
*/
const bailRE = /[^\w.$]/;
/**
* 将路径字符串解析成对应的对象
*/
export function parsePath (path: string): any {
if (bailRE.test(path)) {//即如果路径包含字母 数字 . $ 以外字符,为非法路径
return
}
const segments = path.split('.')
return function (obj : any) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
Observer
到这里,整个流程已经完成了。但现在defineReactive只拦截了对象的属性。但当对象的属性的属性发生变化时,是侦测不到的。例如下面这种情况:
let a = {
b : {
c : 'hello'
}
};
a.b.c = 'world';
我们可以定义一个Observer,创建观察者来观察传入的值。并遍历传入值的子属性,将他们的行为都拦截下来:
class Observer {
public value : any;
public dep : Dep;
constructor(value : any){
this.value = value;
this.dep = new Dep();
def(value, '__ob__', this);//将value和observer关联起来
if(!Array.isArray(value)){
this.value = defineReactive(value);
}
}
}
export function observe(value: any): any{
//如果这个值已经被观察了,就无需再新建Observer 防止循环嵌套对象无限递归
if(hasOwn(value, '__ob__') && value.__ob__ instanceof Observer){
return;
}else{
return (new Observer(value)).value;
}
}
export function defineReactive(obj: any ): any{
const dep = new Dep();
const keys = Object.keys(obj);
for(let key of keys){
if(typeof obj[key] === 'object'){
//如果子属性是对象,我们需要递归添加代理
obj[key] = observe(obj[key]);
}
}
return new Proxy(obj, {
get: function (target, property, receiver) {
dep.depend();
return Reflect.get(target, property, receiver);
},
set: function(obj, prop: (keyof Object), value, receiver){
if(value === obj[prop]){//值无变化
return false;
}
const result = Reflect.set(obj, prop, value, receiver);
dep.notify();
return result;
},
deleteProperty: function(target: any, p: string | number | symbol){
return Reflect.deleteProperty(target, p);
}
});
}
我们可以写一个测试代码测试一下:
test('observe a object', () => {
const obj = {
a : "123",
b : {
test : {
text : "hello"
}
}
}
new Observer(obj);
expect(hasOwn(obj, '__ob__')).toBe(true);
expect(hasOwn(obj.b, '__ob__')).toBe(true);
expect(hasOwn(obj.b.test, '__ob__')).toBe(true);
});
vm.$watch
最后,我们利用上面做好的这套东西,实现一个不完整的vm.$watch。首先,我们会用 new ViewModel({data : {}}),这样的方式创建一个vm对象,并将data加载到vm上。
class ViewModel{
public _data : Object = {};
public _watchers : Array<Watcher> = [];
public $options : any;
constructor(options: any){
this.$options = options;
this._data = this.$options.data;
}
}
我们希望,可以通过vm.key这种方式来访问到vm._data.key。同理用Proxy来实现:
new Proxy(vm, {
get: function (target, property, receiver) {
if( property in target._data){//如果_data里有同名的属性,则读取_data里的值
return Reflect.get(target._data, property, receiver);
}
return Reflect.get(target, property, receiver);
},
set: function(target, property: (keyof Object), value, receiver){
if( property in target._data){
return Reflect.set(target._data, property, value);
}
return Reflect.set(target, property, value, receiver);
},
deleteProperty: function(target, property){
if( property in target._data){
return Reflect.deleteProperty(target._data, property);
}
return Reflect.deleteProperty(target, property);
}
})
最后,我们希望data的值是响应式的,且vm提供$watch方法使得data的值可以被监控。组合以上代码可以得到:
class ViewModel{
public _uid : number;
public _data : Object = {};
public _watchers : Array<Watcher> = [];
public $options : any;
constructor(options: any){
this._uid = _vmUid++;
this.$options = options;
return initState(this);
}
public $watch(expOrFn : string | Function, cb : Function){
const watcher = new Watcher(this, expOrFn, cb);
this._watchers.push(watcher);
}
}
export function initState(vm: ViewModel) {
const opts = vm.$options;
if(opts.data){
vm = initData(vm);
}
return vm;
}
function initData(vm: ViewModel) {
let data = vm.$options.data;
vm._data = defineReactive(data);//将data变为响应式的
return new Proxy(vm, {
get: function (target, property, receiver) {
if( property in target._data){
return Reflect.get(target._data, property, receiver);
}
return Reflect.get(target, property, receiver);
},
set: function(target, property: (keyof Object), value, receiver){
if( property in target._data){
return Reflect.set(target._data, property, value);
}
return Reflect.set(target, property, value, receiver);
},
deleteProperty: function(target, property){
if( property in target._data){
return Reflect.deleteProperty(target._data, property);
}
return Reflect.deleteProperty(target, property);
}
})
}
现在我们好像已经完成一个简单的变化侦测了。但如果执行代码,会发生什么事情呢?程序会进行一次正确打印之后无限打印'text changed!'!思考一下为什么。
const vm = new ViewModel({
data: {
text: 'hello world!'
}
});
vm.$watch('text',(value : any, oldValue : any)=>{
console.log(value);
console.log(oldValue);
});
(vm as any)['text'] = 'text changed!';
这一节完整的代码在github 可以看到哦。
最后的最后,编写测试代码验证结果:
test('watch', async ()=>{
const vm = new ViewModel({
data: {
text: 'hello world!'
}
});
const result = await watchChanged() as any;
expect(result.oldValue).toBe('hello world!');
expect(result.value).toBe('text changed!');
function watchChanged(){
return new Promise((resolve)=>{
vm.$watch('text',(value : any, oldValue : any)=>{
console.log(value, oldValue);
resolve({
value,
oldValue
})
});
(vm as any)['text'] = 'text changed!';
})
}
})
vm.$set、vm.$delete
由于Vue采用的Object.defineProperty对属性进行读写的拦截。所以它不能侦测到属性的删除以及data添加新属性。所以Vue提供了set和delete属性来满足这种需求。但由于我们采用代理的方式实现,这些行为都能被拦截,则不需要另外添加两个方法来实现需求了。
老规矩上测试代码:
test('watch add property', async ()=>{
const vm = new ViewModel({
data: {
message : {}
}
});
const result = await watchChanged() as any;
expect(result.oldValue).toBe(undefined);
expect(result.value).toBe('hello!');
function watchChanged(){
return new Promise((resolve)=>{
vm.$watch('message.a',(value : any, oldValue : any)=>{
resolve({
value,
oldValue
})
});
(vm as any).message.a = 'hello!';
})
}
})
test('watch delete property', async ()=>{
const vm = new ViewModel({
data: {
message : {
a : 'hello!'
}
}
});
const result = await watchChanged() as any;
expect(result.oldValue).toBe('hello!');
expect(result.value).toBe(undefined);
function watchChanged(){
return new Promise((resolve)=>{
vm.$watch('message.a',(value : any, oldValue : any)=>{
resolve({
value,
oldValue
})
});
delete (vm as any).message.a;
})
}
})