逐步实现Vue响应(对象观察)
正常开发中,Vue的响应系统让我们停止操作DOM,只关心数据逻辑的处理,大大降低了代码的复杂度。响应系统也是Vue的核心。作为开发者,有必要了解它的实现原理!
简易版
以手表为切入点
手表是正常开发中使用率较高的功能。它的目的是观察数据,并在数据改变时执行我们预定义的回调。使用方法如下:
{ watch: { obj(val,oldVal) { console.log(val,old val);}}}上面观察到了Vue实例的obj属性,当它的值发生变化时,会打印出新的和旧的值。
因此,我们定义了一个手表功能:
functionwatch (data,key,CB) {//do某物} watch函数接收三个属性,分别是data:observed object key:observed attribute cb:的数据发生变化后要执行的回调Object.defineProperty。
由于回调将在数据更改后执行,因此有必要知道数据何时被修改。这是Object.defineProperty的函数,它定义了数据的访问器属性。读取数据时触发Get,修改数据时触发set。
我们定义了一个定义活动函数,用于使数据响应:
function defineReactive(数据,键){ let val=data[key];object . definepreproperty(data,key,{ configurable: true,enumerable: true,get : function(){ return val;},set:函数(NewVaL){ if(NewVaL===VaL){ return;} val=newVal} });}defineReactive函数为数据对象的键属性定义get和set。get返回属性键的值val,set中键的值被修改为新值newVal。到目前为止,关键属性没有什么特别的。
数据修改将触发set,因此cb必须在set中执行。但是set和cb之间似乎没有联系,所以让我们搭建一座桥梁来建立两者之间的联系:
让target=null我们全局定义一个目标变量,用来保存cb的值,然后在set中调用它。那么,cb什么时候才能保存在目标中呢?回到起点,我们需要调用watch函数来观察数据的键属性,并在值被修改时执行我们定义的回调cb。这是cb保存在目标中的时间:
功能手表(数据、按键、CB){ target=CB;} watch函数中的目标已经被修改了,但是如果我想再次调用watch函数,也就是说,当data[key]被修改时,我想执行两个不同的回调,或者我想再次观察数据的其他属性,该怎么办?目标的值必须保存在其他地方,然后才能再次修改。因为目标对于相同属性的不同回调或不同属性的回调是通用的。
我们有必要为key属性设置一个私有仓库来存储回调。实际上,定义函数有一些特殊之处:在函数内部定义了一个val变量,然后在get和set函数中都使用val变量,这就形成了一个闭包。定义函数的范围是键属性的私有范围,它是自然的私有仓库:
function defineReactive(数据,键){ let val=data[key];const dep=[];object . defineperoperty(data,key,{ configurable: true,enumerable: true,get : function(){ target dep . push(target));返回值;},set:函数(NewVaL){ if(NewVaL===VaL){ return;} dep.forEach(fn=fn(newVal,val));val=newVal} });}我们在defineReactive函数中定义了一个数组dep,它保存了每个属性键的回调集,也称为依赖集。在get函数中,依赖项被收集到dep中,而在set函数中,dep被循环执行每个依赖项。总结一下:在get中收集依赖项,在set中触发依赖项。
由于我们在get中收集依赖项,所以我们必须找到一种方法,在修改tatget时触发get,因此我们读取watch函数中的attribute key的值:
功能手表(数据、按键、CB){ target=CB;数据[密钥];target=null}接下来,我们测试代码:
完全没问题!
依靠
回顾简化版,我们提到了三个角色:defineReactive、dep和watch。其实每一个都有自己的功能,但是我们把三个代码耦合在一起,不方便进一步扩展和理解,所以我们把它们分类。
看守人
Observer也称为dependency,负责订阅数据,并在数据发生变化时执行一些操作:
class Watcher {构造函数(数据、键、CB){ this . VM=data;this.key=keythis.cb=cbthis . value=this . get();} get(){ Dep . target=this;const value=this . VM[this . key];Dep.target=null返回值;} update(){ const old value=this . value;this . value=this . VM[this . key];this.cb.call(this.vm,this.value,old val);}}首先读取构造函数中的attribute key的值,这会触发属性key的集合,然后将自身作为依赖项存储在其dep数组中。当然,在读取属性值之前,您需要将自己分配给桥Dep.target,这就是get方法的作用。最后是update方法,需要在订阅数据更改后执行。其主要目的是执行cb。因为cd需要更改后的新值作为参数,所以它需要再次读取属性值。
资料执行防止
Dep的职责是建立属性键和依赖观察器之间的关系,它的实例必须有一个属于属性键的唯一依赖集合框:
class Dep { constructor(){ this . subs=[];} addSub(sub){ this . subs . push(sub);} depend(){ Dep . taget this . addSub(Dep . target);} notify(){ for(subs的let sub){ sub . update();} }}subs是依赖项集合框。读取属性值时,依赖项被收集到depend方法的框中;修改属性值时,将在notify方法中遍历依赖集合框,并执行每个依赖更新方法。
观察者
defineReactive函数只做一件事,将数据转换为响应,我们定义了一个Observer类来聚合它的函数:
class Observer {构造函数(数据,键){ this.value=data定义活动(数据、密钥);}}function defineReactive(data,key){ let val=data[key];const DeP=new DeP();object . defineperoperty(data,key,{ configurable: true,enumerable: true,get : function(){ dep . depend();返回值;},set:函数(NewVaL){ if(NewVaL===VaL){ return;} dep . notify();val=newVal} });}Dep不再是纯数组,而是Dep类的一个实例。get函数中的依赖集合逻辑和set函数中的依赖触发器分别被dep.depend和dep.update替换,使得defineReactive函数的逻辑更加清晰。但是Observer类只调用构造函数中的defineReactive函数,没有效果。这当然是在为未来铺路!
测试代码:
观察所有属性
到目前为止,我们只关注了一个属性,一个对象可能有n个以上的属性,所以我们需要对其进行调整。
观察对象的所有属性
观察一个属性主要是定义它的访问器属性。对于我们的代码,它将执行defineReactive函数,因此修改Observer类:
class Observer { constructor(data){ this . value=data;if(isplayanobject(data)){ this . walk(data);} } walk(值){ const keys=Object.keys(值);for(键的字母键){ defineReactive(值,键);} } }函数isplayinobject(obj){ return({ }). tostring . call(obj)=='[Object Object]';}我们在Observer类中定义了一个walk方法,它的功能是遍历对象的所有属性,然后在构造函数中调用它。调用的前提是对象是一个纯对象,即对象是通过文字或新的object()初始化的,因为Array和Function也是对象。
测试代码:
深度观察
只要对象可以嵌套,即对象的一个属性值也可以是一个对象,这是我们的代码还做不到的。其实也很简单,只需要做一个递归遍历:
class Observer { constructor(data){ this . value=data;if(isplayanobject(data)){ this . walk(data);} } walk(值){ const keys=Object.keys(值);for(let key of key){ const val=value[key];if(isplayanobject(val)){ this . walk(val);} else { defineReactive(值,键);}}}}我们在走法上做了判断。如果key的属性值val是一个纯对象,我们调用walk方法遍历它的属性值。由于是深度观察,所以观察器类中键的用法也发生了变化,例如:‘a . b . c’,那么我们必须兼容这种嵌套的键编写:
类Watcher {构造函数(数据,路径,CB){ this . VM=data;this.cb=cbthis.getter=parsePath(路径);this . value=this . get();} get(){ Dep . target=this;const value=this . getter . call(this . VM);Dep.target=null返回值;} update(){ const old value=this . value;this . value=this . getter . call(this . VM,this . VM);this.cb.call(this.vm,this.value,old value);} }函数parsePath(路径){ if (/)。$_/.test(path)){ return;} const segments=path.split(' . ');return function(obj){ for(let segment of segment){ obj=obj[segment]} return obj;}}Watcher类实例添加getter属性,其值为parsePath函数的返回值。在parsePath函数中,返回一个匿名函数。匿名函数接收一个参数obj,最后返回obj作为返回值,所以这里的重点是匿名函数对obj做了什么。
只有一个.匿名函数中的迭代,迭代对象是segments,它是通过划分“.”得到的数组按路径。例如,如果路径是“a.b.c”,则段是[“a”、“b”、“c”]。迭代中只有一条语句,obj被赋值为obj的属性值,相当于一层一层的读取。例如,obj的初始值为:
Obj={a: {b: {c: 1} }}那么最终结果是:
使用obj=1读取属性值的目的是收集依赖关系。例如,如果我们想观察obj.a.b.c,目的就达到了。既然您知道getter是一个函数,那么就可以通过在get方法中执行getter来获取该值。
测试代码:
这里有一个细节,让我们看看Watcher类的get方法:
get(){ Dep . target=this;const value=this . getter . call(this . VM);Dep.target=null返回值;}执行this.getter函数时,Dep.target的值始终是当前依赖项,而this.getter函数逐层读取属性值,这个路径中的所有属性实际上都收集了当前依赖项。例如,在上面的示例中,如果在obj.a、obj.a.b、obj.a.b.c的dep中收集了属性“a.b.c”的依赖关系,则修改obj.a或obj.b将触发当前的依赖关系:
避免重复收集依赖项
观察表情
在Vue中,$watch方法的第一个参数还过得去:
这个。$ watch(()={ return this . a this . b;},(val,oldVal)={ console.log(val,old val);});这种写法相当于观察一个表达式,类似于Vue中的计算。依赖会被收集在属性A和属性b的dep中,无论其中任何一个是否被修改,只要表达式的值发生变化,依赖都会被触发。
为了与函数的引入兼容,我们稍微修改了Watcher类:
类Watcher {构造函数(data,pathOrFn,CB){ this . VM=data;this.cb=cbthis . getter=type of pathOrFn==' function '?pathOrFn : parsePath(pathOrFn);this . value=this . get();} .update(){ const old value=this . value;this . value=this . get();this.cb.call(this.vm,this.value,old value);}}对于第二个参数pathOrFn,我们首先判断它是否已经是一个函数,如果是,直接赋给this.getter,否则调用parsePath函数进行解析。在更新方法中,再次调用get方法来获取修改后的值。
测试代码:
结果好像有问题?产量1949倍!而且还在增加。一定有人在无限循环中。仔细回顾我们修改的点,在更新方法中,我们再次调用get方法,这触发了另一个依赖项集合。然后我们遍历Dep类的notify方法中的依赖集,每次触发依赖都会导致依赖再次被收集,这是一个无限循环!
发现问题就解决。我们需要检查依赖性的唯一性:
让uid=1;类Watcher {构造函数(数据,pathrfn){ this . id=uid;} } class Dep(){ construct(){ this . subs=[];this . subids=new Set();} .addSUb(sub){ const id=SUb . id;if(!this . subids . has(id)){ this . subs . push(sub);this . SUBIDs . add(id);}} .}因为我们必须进行唯一性检查,所以我们向Watcher类实例添加了一个唯一的id。在Dep类中,我们将属性subIds添加到构造函数中,其初始值为空Set,用于存储依赖id。然后,在addSub方法中,在将依赖项添加到Sub之前,判断依赖项的id是否已经存在。
测试代码:
输出只有一次,完全可以。
Vue中的意义
防止依赖关系的重复收集,除了防止上述的无限循环,在Vue中还有更重要的意义,比如下面的模板:
模板div p { { a } }/p p { { a } }/p p { { a } }/p p { { a } }/p/div/template在Vue中,除了watch选项的依赖关系之外,还有一种特殊的依赖关系叫做渲染函数依赖关系,其作用是在模板中的变量发生变化时更新VNode并重新生成DOM。在我们上面定义的模板中,A变量被使用了三次。修改A变量时,如果没有集合,渲染函数会执行三次,防止重复依赖!这是绝对必要的!而三次只是一个例子,但可能还有更多!
以上就是本文的全部内容。希望对大家的学习有帮助,支持我们。
版权声明:逐步实现Vue响应(对象观察)是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。