??我們想要一個數(shù)據發(fā)生改變時,與其相關的數(shù)據豹障、視圖模型自動發(fā)生變化冯事。首先要知道數(shù)值變化了。
??在Angular的方法是使用zone.js把如setTimeout血公、XHR桅咆、點擊事件等可能引起模型變化的異步操作用一個wrapFn包裹起來,每當有異步操作發(fā)生時Angular就知道數(shù)據可能變化了坞笙。再遍歷組件樹岩饼,通知組件進行變化檢測。若有變化則重新渲染頁面薛夜。
??而Vue采用的方式籍茧,則是利用Object.defineProperty定義setter,再確認數(shù)據變化后梯澜,通知相關的依賴寞冯。
觀察者模式
??先引入一個設計模式——觀察者模式。了解觀察者模式的話可以跳過直接看下文晚伙。我們假設有三種人:
- 好事者吮龄,他們對感興趣的事情很上心,發(fā)生了什么事情都想第一時間知道咆疗。
- 消息靈通的人漓帚,他們收集信息,提供給感興趣的好事者午磁。
-
觀察者尝抖,他們想狗仔隊一樣監(jiān)視著目標,一有發(fā)現(xiàn)就告訴消息靈通的人迅皇。
觀察者模式
??通過這種消息傳遞的方式昧辽,使得觀察者和好事者解耦,觀察者只管觀察登颓,好事者只管八卦搅荞。
接下來我們來抄襲Vue實現(xiàn)變化偵測,Vue是基于觀察者模式實現(xiàn)的框咙。
1.觀察者
??在js中咕痛,有兩種方式可以獲取對象的變化:Object.defineProperty、Proxy扁耐。Object.defineProperty只能獲取對象屬性的讀取暇检,是ES5規(guī)范內容瀏覽器兼容性好。Proxy更強大婉称,可以攔截對對象的各種訪問块仆、改變,但是ES6內容所以兼容較差王暗。由于我們只是為了了解響應式框架的原理悔据,不是做實用輪子,所以我們采用Proxy的方式實現(xiàn)俗壹。
??為了獲取對象的變化科汗,我們對讀、寫绷雏、刪除進行攔截头滔。這個代理怖亭,就相當于一個觀察者,監(jiān)視著對象的一舉一動坤检。
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感興趣箭跳,它需要知道m(xù)essage的值是什么它才知道要渲染成怎樣的視圖晨另,即它依賴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){
//發(fā)生變化時,通知感興趣的好事者
sub.update();
}
}
}
??如果有人讀過一個object的屬性逝段,我們就認為這個人對這個object是感興趣的垛玻。那么當這個object發(fā)生變動時,我們就要通知這些感興趣的人奶躯。此時我們改造一下defineReactive方法:
function defineReactive(obj: any): any {
const dep = new Dep();//創(chuàng)建一個依賴管理
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.好事者
??好事者會對一件事表示感興趣账嚎,當?shù)玫竭@事的消息時會作出反應。舉個例子儡蔓,我們創(chuàng)建一個好事者郭蕉,他表示對蔡徐坤感興趣,而當他知道蔡徐坤開始打籃球時喂江,會大嚷大叫:
new Watcher('蔡徐坤', (status)=>{
if(status === '打籃球'){
console.log('蔡徐坤來打籃球啦U傩狻!');
}
})
??為實現(xiàn)這樣的功能获询,可以寫出以下代碼:
class Watcher {
public cb : Function; //回調函數(shù)涨岁,這個人發(fā)現(xiàn)消息之后會做什么事情
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對應的值,會觸發(fā)proxy中的get
//進而將這個watcher添加到dep里 即讓消息靈通人的知道我感興趣
this.value = this.get();
}
public get(){
Dep.target = this;//記錄自己吉嚣,用于讓上文中的dep知道好事者是誰
const value = this.getter.call(this.vm, this.vm);//觸發(fā)了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
* [^]是排除字符組
* 這個正則意思是 排除字母 數(shù)組 . $
*/
const bailRE = /[^\w.$]/;
/**
* 將路徑字符串解析成對應的對象
*/
export function parsePath (path: string): any {
if (bailRE.test(path)) {//即如果路徑包含字母 數(shù)字 . $ 以外字符梢薪,為非法路徑
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
??到這里,整個流程已經完成了尝哆。但現(xiàn)在defineReactive只攔截了對象的屬性秉撇。但當對象的屬性的屬性發(fā)生變化時,是偵測不到的。例如下面這種情況:
let a = {
b : {
c : 'hello'
}
};
a.b.c = 'world';
我們可以定義一個Observer琐馆,創(chuàng)建觀察者來觀察傳入的值规阀。并遍歷傳入值的子屬性,將他們的行為都攔截下來:
class Observer {
public value : any;
public dep : Dep;
constructor(value : any){
this.value = value;
this.dep = new Dep();
def(value, '__ob__', this);//將value和observer關聯(lián)起來
if(!Array.isArray(value)){
this.value = defineReactive(value);
}
}
}
export function observe(value: any): any{
//如果這個值已經被觀察了啡捶,就無需再新建Observer 防止循環(huán)嵌套對象無限遞歸
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
??最后,我們利用上面做好的這套東西瞎暑,實現(xiàn)一個不完整的vm.$watch。首先与帆,我們會用 new ViewModel({data : {}})了赌,這樣的方式創(chuàng)建一個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來實現(xiàn):
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的值可以被監(jiān)控郭卫。組合以上代碼可以得到:
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變?yōu)轫憫降?
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);
}
})
}
現(xiàn)在我們好像已經完成一個簡單的變化偵測了砍聊。但如果執(zhí)行代碼,會發(fā)生什么事情呢贰军?程序會進行一次正確打印之后無限打印'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!';
??這一節(jié)完整的代碼在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屬性來滿足這種需求。但由于我們采用代理的方式實現(xiàn)舵盈,這些行為都能被攔截陋率,則不需要另外添加兩個方法來實現(xiàn)需求了。
老規(guī)矩上測試代碼:
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;
})
}
})