数据检测在Vue3中的实现
10月5日凌晨,Vue3的源代码正式发布,从官方消息来看:
当前版本是Pre-Alpha,仓库地址是Vue-next。您可以通过Composition API了解新版本的更多信息。当前版本单元测试与vue-next-coverage相关。
文章大纲:
Vue的核心之一是响应系统,它通过检测数据的变化来驱动更新视图。
如何实现响应对象
通过对对象的响应,可以检测到数据,从而告知外界数据的变化。如何实现响应对象:
Getter和setter defineProperty Proxy没有详细介绍前两个API的用法。单个存取器getter/setter的功能相对简单。但是,作为Vue2.x的API-definepreproperty,API本身存在很多问题。
在Vue2.x中,为了实现数据响应,需要对Object和Array采用不同的处理方法。
Type Object通过Object.defineProperty将属性转换为getter/setter,这个过程需要递归检测所有的对象键来实现深度检测。为了感知数组的变化,拦截了几种在数组原型上改变数组本身内容的方法。虽然实现了对Array的响应,但仍然存在一些问题或不方便的情况。同时,当defineProperty递归实现getter/setter时,也存在一些性能问题。
更好的实现是通过ES6提供的代理应用编程接口。
代理应用编程接口的一些细节
代理应用编程接口有更强大的功能。相对于老的defineProperty API,Proxy可以代理数组,API提供了多个陷阱,可以实现很多功能。
有两个trap: get和set,以及一些容易被忽略的细节。
细节1:陷阱默认行为
let data={ foo : ' foo ' } let p=new Proxy(data,{ get(target,key,receiver) { return target[key] },set(target,key,value,receiver){ console . log(' set value ')target[key]=value/?}})p.foo=123//set值表示通过代理返回的对象p对原始数据的操作,设置p时可以检测到变化。但是,这样写有一个问题。当代理对象数据是数组时,将会报告一个错误。
let data=[1,2,3]let p=new Proxy(数据,{ get(target,key,receiver) { return target[key] },set(target,key,value,The receiver){ console . log(' set value ')target[key]=value } })p . push(4)//VM 438336012未捕获类型错误: ' set ' on proxy : trap为属性' 3 '返回fallish将代码更改为:
let data=[1,2,3]let p=new Proxy(data,{ get(target,key,receiver) { return target[key] },set(target,key,value,The receiver){ console . log(' setvalue ')target[key]=valuereturn true } })p . push(4)//setvalue//打印两次。事实上,当代理对象是数组时,推送操作不仅操作当前数据,还会触发数组本身的其他属性更改。
let data=[1,2,3]let p=new Proxy(data,{ get(target,key,receiver){ console . log(' get value : ',key)返回target[key] },set(target,key,value,receiver){ console . log(' Set value : ',key,value)target[key]=value return true } })p . push(1)//get value : push//get value : length//Set value 3: 3 1//Set value从打印输出可以看出,push操作不仅将数组第三个下标的值设置为1,还将数组的长度值更改为4。同时,这个操作还会触发get来获取push和length属性。
我们可以通过Reflect返回陷阱对应的默认行为,设置相对简单,但是一些复杂的默认行为处理起来要复杂得多,所以Reflect的功能就出现了。
let data=[1,2,3]let p=new Proxy(data,{ get(target,key,receiver){ console . log(' get value : ',key) return Reflect.get(target,key,receiver) },set(target,key,value,receiver){ console . log(' set value : ',key,value) return Reflect.set(target,key,value,The receiver)})p . push(1)//get value : push//3360 length/
细节2:多次设置/获取触发器
从前面的例子可以看出,当代理对象为数组时,push操作会触发多次set执行,同时也会触发get操作,这一点非常重要,vue3很好地利用了这一点。
我们可以从另一个例子来看这个操作:
let data=[1,2,3]let p=new Proxy(data,{ get(target,key,receiver){ console . log(' get value : ',key) return Reflect.get(target,key,receiver) },set(target,key,value,receiver){ console . log(' set value : ',key,value) return Reflect.set(target,key,value,receiver)})p . unshift(' a ')//get value : unshift//get value :仔细看输出不难看出,get首先取数组的最后一个下标,打开一个新的下标3存储原来的最后一个数值,然后将原来的数值向后移动,将下标0设置为unshift的值A,导致多次集合运算。
虽然这显然不利于通知外部操作,但我们假设set中的console是一个触发外部渲染的渲染函数,因此此unshift操作将触发多个渲染。
稍后我们将讨论如何解决这个问题。继续。
细节3:代理只能代表一层
让数据={ foo: 'foo ',bar: { key: 1 },ary: ['a ',' b ']}让p=new Proxy(数据,{ get(目标,键,接收器){ console.log('get value: ',键)返回Reflect.get(目标,键,接收器)},set(目标,键,值,接收器){console.log ('set value: ',键,值)返回reflect.set(目标,键,接收器)。
可以看到,代理对象只能表示到第一层,对象内部的深度检测需要开发人员自己实现。对象内部的数组也是如此。
p . ary . push(' c ')//getvalue : ary也只进行了get操作,set没有察觉到。
我们注意到get/set有另一个参数:receiver,它实际上接收一个代理对象:
让数据={ a: { b: { c: 1 } } }让p=new Proxy(数据,{ get(目标、键、接收器){ console.log(接收器)const res=Reflect.get(目标、键、接收器)return res },set(目标、键、值、接收器){returnreflect.set(目标、键、值、接收器)})//Proxy { a : }.}}这里,接收者输出当前代理对象。请注意,这是一个代理对象。
让数据={ a: { b: { c: 1 } } }让p=new Proxy(数据,{ get(目标、键、接收器){ const res=Reflect.get(目标、键、接收器)console.log(res) return res },set(目标、键、值、接收器){ return Reflect.set(目标、键、值、接收器)} })//{ b : { c 3360 } 1 }当我们尝试输出Reflect.get返回的值时,我们将考虑到这一点,Ve3通过实现深度代理很好地利用了它。
代理解决细节
前面提到使用代理来检测数据更改,有几个细节,包括:
使用“反映”返回陷阱的默认行为。对于set操作,可能会改变代理对象的属性,导致set多次执行代理。对于对象内部的操作,set无法感知它,但get会执行。接下来,我们将尝试自己解决这些问题,然后分析Vue3如何解决这些细节。
SetTimeout解决重复的触发器
函数reactive(data,cb) {让timer=null返回new Proxy(data,{ get(target,key,receiver) } { return reflect . get(target,key,receiver)},set(target,key,value,receiver){ cleartime out(timer)timer=setTimeout(()={ CB()},0);返回反射。set (target,key,value,receiver)}})} let=[1,2] let p=reactive (ary,()={ console . log(' trigger ')})p . push(3)//trigger程序输出结果为一:trigger
这里实现了反应函数,它接收两个参数,第一个是委托数据,还有一个回调函数cb。这里我们简单打印cb中的触发器操作,模拟通知外部数据的变化。
有许多方法可以解决重复的cb调用,例如,通过标志来决定是否调用。这种情况下,使用timer setTimeout,每次调用cb前清除定时器,实现类似去抖的操作,也可以解决重复回调的问题。
解决数据深度检测问题
目前还有一个问题,就是深度数据检测,可以通过递归代理来实现:
函数reactive(data,CB){ let RES=null let timer=null RES=Array的数据实例?[]: { } for(let key in data){ if(type of data[key]==' object '){ res[key]=reactive(data[key],cb) } else { res[key]=data[key] } }返回新的Proxy(res,{ get(target,key){ return . reflect . get(target,key),val) { let res=Reflect.set(target,key,val)cleartime out(timer)timer=setTimeout(()={ CB(CB))},0)返回RES
这里我们可以输出代理对象p:
可以看出,深度代理后的对象都带有代理的标志。
在这里,我们解决了使用代理实现检测的一系列细节问题。虽然这些处理方法可以解决问题,但它们似乎不够优雅。特别是递归代理是一个潜在的性能隐患。当数据对象较大时,递归代理会消耗相对较大的性能,有些数据不需要检测,所以我们需要更仔细地控制数据检测。
接下来,我们将看看Vue3如何使用Proxy来检测数据。
Ve3中的反应性
Vue3项目结构采用lerna以monorepo的方式进行代码管理。目前,很多开源项目切换到monorepo模式。显而易见的特征是项目中会有一个包/文件夹。
Vue3做了很好的模块功能划分,使用TS。我们直接在包中找到响应数据模块:
其中,文件reactive.ts提供了响应功能,是实现响应的核心。同时,该功能也挂载在全局Vue对象上。
这里,稍微简化一下源代码:
const rawtoractive=new WeakMap()const reactiveToRaw=new WeakMap()//utilsfunction isObject(val){ val的返回类型==' object ' }函数hasOwn(val,key){ const hasOwn属性=object。原型。hasOwn属性返回hasOwn属性。调用(val,key)}//traps function createGetter(){返回函数get(target,key,receiver){ const RES=reflect。get(目标、键、接收器)ret(RES)?反应性(RES): RES } }函数集(目标、键、val、接收器){ const hadKey=hasOwn(目标、键)const old value=target[key]val=reactivetoraw。get(val)| | val const result=reflect。集合(目标、键、val、接收器)if(!hadKey) { console.log('trigger . ')} else if(val!==旧值){控制台。日志('触发器. ')}返回结果}//处理程序const mutableHandlers={ gett : createGetter(),set: set,}//entry function reactive(target){ return createreactive object(target,rawToReactive,reactiveToRaw,mutableHandlers),} function createReactiveObject(target,toProxy,ToRaw,base handlers){ let observed=toProxy。get(target)//原数据已经有相应的可响应数据,返回可响应数据如果(观察到!==void 0){ 0观察到返回} //原数据已经是可响应数据if(toraw。has(target)){ return target } observed=new Proxy(目标,基本处理程序)toproxy。设定(目标,观察)目标。设置(返回目标)返回观察到的} rawtoractive和reactiveToRaw是两个弱引用的地图结构,这两个地图用来保存原始数据和可响应数据,在函数createReactiveObject中toProxy和托劳传入的便是这两个地图。
我们可以通过它们,找到任何代理过的数据是否存在,以及通过代理数据找到原始的数据。
除了保存了代理的数据和原始数据,createReactiveObject函数仅仅是返回了新代理代理后的对象。重点在新代理中传入的处理者参数基本处理器。
还记得前面提到的代理实现数据侦测的细节问题吧,我们尝试输入:
让数据={ foo: 'foo ',ary: [1,2]}让r=无功功率(数据)r.ary.push(3)打印结果:
可以看到打印输出了一次引发.
问题一:如何做到深度的侦测数据的?
深度侦测数据是通过createGetter函数实现的,前面提到,当对多层级的对象操作时,设置并不能感知到,但是得到会触发,于此同时,利用Reflect.get()返回的"多层级对象中内层" ,再对"内层数据"做一次代理。
函数createGetter(){ 0返回函数获取(目标,键,接收器){ const res=Reflect.get(目标,键,接收器)返回isObject(res)?反应性(第:号决议)可以看到这里判断了显示返回的数据是否还是对象,如果是对象,则再走一次代理人,从而获得了对对象内部的侦测。
并且,每一次的代理人数据,都会保存在地图中,访问时会直接从中查找,从而提高性能。
当我们打印代理后的对象时:
可以看到这个代理后的对象内层并没有代理的标志,这里仅仅是代理外层对象。
输出其中一个存储代理数据的rawToReactive:
对于内层ary: [1,2]的代理,已经被存储在了rawToReactive中。
由此实现了深度的数据侦测。
问题二:如何避免多次扳机?
函数hasOwn(val,key){ const hasOwn属性=object。原型。hasOwn属性返回hasOwn属性。呼叫(val,key)}函数集(target,key,val,receiver) { console.log(target,key,val) const hadKey=hasOwn(target,key)const old value=target[key]val=reactivetoverraw。get(val)| | val const result=reflect。set(目标、键、val、接收器)if(!hadKey) { console.log('触发器.是添加操作类型)} else if(val!==oldValue) { console.log('触发器.是设置操作类型')}返回结果}关于多次引发的问题,vue处理得很巧妙。
在设置函数中hasOwn前打印console.log(目标,键,值).
输入:
让data=['a ',' b']让r=reactive (data) r. push ('c ')输出结果:
R.push('c ')触发器被设置为执行两次,一次用于值本身' c ',一次用于设置长度属性。
当设置值' c '时,传入的新索引键为2,目标为原始代理对象['a ',' c'],hasOwn(target,key)显然返回false,这是一个新操作。这时,触发.是可以执行的添加操作类型。
当传入的键是length,hasOwn(目标,键),length是它自己的属性,返回true,然后val!==oldValue,val为3,oldValue同时为target['length']和3,因此不会执行触发器输出语句。
因此,我们可以通过判断key是否是target的属性,并将val设置为target[key]来确定触发器的类型,避免冗余触发器。
摘要
实际上,本文主要研究如何使用代理来检测Vue3中的数据。然而,在分析源代码之前,有必要弄清楚Proxy本身的一些特性,因此给出了Proxy的大量前置知识。同时,我们也用自己的方式解决这些问题。
最后,我们比较了如何在Vue3中处理这些细节。可以看出,Vue3并不是简单地通过Proxy递归检测数据,而是通过get操作实现内部数据的代理,并结合WeakMap保存数据,这将大大提高响应数据的性能。
感兴趣的伙伴可以为递归Proxy和Vue3的这个实现做相应的基准测试,因为它们之间有很大的性能差距。
这篇文章仍然在很大程度上简化了reactive,但是要处理的细节实际上要复杂得多。更多细节还需要查看源代码。
以上就是本文的全部内容。希望对大家的学习有帮助,支持我们。
版权声明:数据检测在Vue3中的实现是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。