浅谈Vue数据响应
Vue中可以用$watch实例方法观察一个字段,当该字段的值发生变化时,会执行指定的回调函数(即观察者),实际上和watch选项作用相同。如下:
vm.$watch('box',()=>{
console.log('box变了')
})
vm.box='newValue'//'box变了'
以上例切入,我想实现一个功能类似的方法myWatch。
如何知道我观察的属性被修改了?
——Object.defineProperty方法
该方法可以为指定对象的指定属性设置getter-setter函数对,通过这对getter-setter可以捕获到对属性的读取和修改操作。示例如下:
constdata={
box:1
}
Object.defineProperty(data,'box',{
set(){
console.log('修改了box')
},
get(){
console.log('读取了box')
}
})
console.log(data.box)//'读取了box'
//undefined
data.box=2//'修改了box'
console.log(data.box)//'读取了box'
//undefined
如此,便拦截到了对box属性的修改和读取操作。
但res为undefined,data.box=2的修改操作也无效。
get与set函数功能不健全
故修改如下:
constdata={
box:1
}
letvalue=data.box
Object.defineProperty(data,'box',{
set(newVal){
if(newVal===value)return
value=newVal
console.log('修改了box')
},
get(){
console.log('读取了box')
returnvalue
}
})
console.log(data.box)//'读取了box'
//1
data.box=2//'修改了box'
console.log(data.box)//'读取了box'
//2
有了这些,myWatch方法便可实现如下:
constdata={
box:1
}
functionmyWatch(key,fn){
letvalue=data[key]
Object.defineProperty(data,key,{
set(newVal){
if(newVal===value)return
value=newVal
fn()
},
get(){
returnvalue
}
})
}
myWatch('box',()=>{
console.log('box变了')
})
data.box=2//'box变了'
但存在一个问题,不能给同一属性添加多个依赖(观察者):
myWatch('box',()=>{
console.log('我是观察者')
})
myWatch('box',()=>{
console.log('我是另一个观察者')
})
data.box=2//'我是另一个观察者'
后面的依赖(观察者)会将前者覆盖掉。
如何能够添加多个依赖(观察者)?
——定义一个数组,作为依赖收集器:
constdata={
box:1
}
constdep=[]
functionmyWatch(key,fn){
dep.push(fn)
letvalue=data[key]
Object.defineProperty(data,key,{
set(newVal){
if(newVal===value)return
value=newVal
dep.forEach((f)=>{
f()
})
},
get(){
returnvalue
}
})
}
myWatch('box',()=>{
console.log('我是观察者')
})
myWatch('box',()=>{
console.log('我是另一个观察者')
})
data.box=2//'我是观察者'
//'我是另一个观察者'
修改data.box后,两个依赖(观察者)都执行了。
若上例data对象需新增两个能够响应数据变化的属性foobar:
constdata={
box:1,
foo:1,
bar:1
}
只需执行以下代码即可:
myWatch('foo',()=>{
console.log('我是foo的观察者')
})
myWatch('bar',()=>{
console.log('我是bar的观察者')
})
但问题是,不同属性的依赖(观察者)都被收集进了同一个dep,修改任何一个属性,都会触发所有的依赖(观察者):
data.box=2//'我是观察者' //'我是另一个观察者' //'我是foo的观察者' //'我是bar的观察者'
我想可以这样解决:
constdata={
box:1,
foo:1,
bar:1
}
constdep={}
functionmyWatch(key,fn){
if(!dep[key]){
dep[key]=[fn]
}else{
dep[key].push(fn)
}
letvalue=data[key]
Object.defineProperty(data,key,{
set(newVal){
if(newVal===value)return
value=newVal
dep[key].forEach((f)=>{
f()
})
},
get(){
returnvalue
}
})
}
myWatch('box',()=>{
console.log('我是box的观察者')
})
myWatch('box',()=>{
console.log('我是box的另一个观察者')
})
myWatch('foo',()=>{
console.log('我是foo的观察者')
})
myWatch('bar',()=>{
console.log('我是bar的观察者')
})
data.box=2//'我是box的观察者'
//'我是box的另一个观察者'
data.foo=2//'我是foo的观察者'
data.bar=2//'我是bar的观察者'
但实际上这样更好些:
constdata={
box:1,
foo:1,
bar:1
}
lettarget=null
for(letkeyindata){
constdep=[]
letvalue=data[key]
Object.defineProperty(data,key,{
set(newVal){
if(newVal===value)return
value=newVal
dep.forEach(f=>{
f()
})
},
get(){
dep.push(target)
returnvalue
}
})
}
functionmyWatch(key,fn){
target=fn
data[key]
}
myWatch('box',()=>{
console.log('我是box的观察者')
})
myWatch('box',()=>{
console.log('我是box的另一个观察者')
})
myWatch('foo',()=>{
console.log('我是foo的观察者')
})
myWatch('bar',()=>{
console.log('我是bar的观察者')
})
data.box=2//'我是box的观察者'
//'我是box的另一个观察者'
data.foo=2//'我是foo的观察者'
data.bar=2//'我是bar的观察者'
声明target全局变量作为依赖(观察者)的中转站,myWatch函数执行时用target缓存依赖,然后调用data[key]触发对应的get函数以收集依赖,set函数被触发时会将dep里的依赖(观察者)都执行一遍。这里的getset函数形成闭包引用了上面的dep常量,这样一来,data对象的每个属性都有了对应的依赖收集器。
且这一实现方式不需要通过myWatch函数显式地将data里的属性一一转为访问器属性。
但运行以下代码,会发现仍有问题:
console.log(data.box) data.box=2//'我是box的观察者' //'我是box的另一个观察者' //'我是bar的观察者'
四个myWatch执行完之后target缓存的值变成了最后一个myWatch方法调用时所传递的依赖(观察者),故执行console.log(data.box)读取box属性的值时,会将最后缓存的依赖存入box属性所对应的依赖收集器,故而再修改box的值时,会打印出'我是bar的观察者'。
我想可以在每次收集完依赖之后,将全局变量target设置为空函数来解决这问题:
constdata={
box:1,
foo:1,
bar:1
}
lettarget=null
for(letkeyindata){
constdep=[]
letvalue=data[key]
Object.defineProperty(data,key,{
set(newVal){
if(newVal===value)return
value=newVal
dep.forEach(f=>{
f()
})
},
get(){
dep.push(target)
target=()=>{}
returnvalue
}
})
}
functionmyWatch(key,fn){
target=fn
data[key]
}
myWatch('box',()=>{
console.log('我是box的观察者')
})
myWatch('box',()=>{
console.log('我是box的另一个观察者')
})
myWatch('foo',()=>{
console.log('我是foo的观察者')
})
myWatch('bar',()=>{
console.log('我是bar的观察者')
})
经测无误。
但开发过程中,还常碰到需观测嵌套对象的情形:
constdata={
box:{
gift:'book'
}
}
这时,上述实现未能观测到gift的修改,显出不足。
如何进行深度观测?
——递归
通过递归将各级属性均转为响应式属性即可:
constdata={
box:{
gift:'book'
}
}
lettarget=null
functionwalk(data){
for(letkeyindata){
constdep=[]
letvalue=data[key]
if(Object.prototype.toString.call(value)==='[objectObject]'){
walk(value)
}
Object.defineProperty(data,key,{
set(newVal){
if(newVal===value)return
value=newVal
dep.forEach(f=>{
f()
})
},
get(){
dep.push(target)
target=()=>{}
returnvalue
}
})
}
}
walk(data)
functionmyWatch(key,fn){
target=fn
data[key]
}
myWatch('box',()=>{
console.log('我是box的观察者')
})
myWatch('box.gift',()=>{
console.log('我是gift的观察者')
})
data.box={gift:'basketball'}//'我是box的观察者'
data.box.gift='guitar'
这时gift虽已是访问器属性,但myWatch方法执行时data[box.gift]未能触发相应getter以收集依赖,data[box.gift]访问不到gift属性,data[box][gift]才可以,故myWatch须改写如下:
functionmyWatch(exp,fn){
target=fn
letpathArr,
obj=data
if(/\./.test(exp)){
pathArr=exp.split('.')
pathArr.forEach(p=>{
obj=obj[p]
})
return
}
data[exp]
}
如果要读取的字段包括.,那么按照.将其分为数组,然后使用循环读取嵌套对象的属性值。
这时执行代码后发现,data.box.gift='guitar'还是未能触发相应的依赖,即打印出'我是gift的观察者'这句信息。调试之后找到问题:
myWatch('box.gift',()=>{
console.log('我是gift的观察者')
})
执行以上代码时,pathArr即['box','gift'],循环内obj=obj[p]实际上就是obj=data[box],读取了一次box,触发了box对应的getter,收集了依赖:
()=>{
console.log('我是gift的观察者')
}
收集完将全局变量target置为空函数,而后,循环继续执行,又读取了gift的值,但这时,target已是空函数,导致属性gift对应的getter收集了一个“空依赖”,故,data.box.gift='guitar'的操作不能触发期望的依赖。
以上代码有两个问题:
- 修改box会触发“我是gift的观察者”这一依赖
- 修改gift未能触发“我是gift的观察者”的依赖
第一个问题,读取gift时,必然经历读取box的过程,故触发box对应的getter无可避免,那么,box对应getter收集gift的依赖也就无可避免。但想想也算合理,因为box修改时,隶属于box的gift也算作修改,从这一点看,问题一也不算作问题,划去。
第二个问题,我想可以这样解决:
functionmyWatch(exp,fn){
letpathArr,
obj=data
if(/\./.test(exp)){
pathArr=exp.split('.')
pathArr.forEach(p=>{
target=fn
obj=obj[p]
})
return
}
target=fn
data[exp]
}
data.box.gift='guitar'//'我是gift的观察者'
data.box={gift:'basketball'}//'我是box的观察者'
//'我是gift的观察者'
保证属性读取时target=fn即可。
那么:
constdata={
box:{
gift:'book'
}
}
lettarget=null
functionwalk(data){
for(letkeyindata){
constdep=[]
letvalue=data[key]
if(Object.prototype.toString.call(value)==='[objectObject]'){
walk(value)
}
Object.defineProperty(data,key,{
set(newVal){
if(newVal===value)return
value=newVal
dep.forEach(f=>{
f()
})
},
get(){
dep.push(target)
target=()=>{}
returnvalue
}
})
}
}
walk(data)
functionmyWatch(exp,fn){
letpathArr,
obj=data
if(/\./.test(exp)){
pathArr=exp.split('.')
pathArr.forEach(p=>{
target=fn
obj=obj[p]
})
return
}
target=fn
data[exp]
}
myWatch('box',()=>{
console.log('我是box的观察者')
})
myWatch('box.gift',()=>{
console.log('我是gift的观察者')
})
现在我想,假如我有以下数据:
constdata={
player:'JamesHarden',
team:'HoustonRockets'
}
执行以下代码:
functionrender(){
document.body.innerText=`Thelastseason'sMVPis${data.player},he'sfrom${data.team}`
}
render()
myWatch('player',render)
myWatch('team',render)
data.player='KobeBryant'
data.team='LosAngelesLakers'
是不是就可以将数据映射到页面,并响应数据的变化?
执行代码发现,data.player='KobeBryant'报错,究其原因,render方法执行时,会去获取data.player和data.team的值,但此时,target为null,那么读取player时对应的依赖收集器dep便收集了null,导致player的setter调用依赖时报错。
那么我想,在render执行时便主动去收集依赖,就不会导致dep里收集了null。
细看myWatch,这方法做的事情其实就是帮助getter收集依赖,它的第一个参数就是要访问的属性,要触发谁的getter,第二个参数是相应要收集的依赖。
这么看来,render方法既可以帮助getter收集依赖(render执行时会读取playerteam),而且它本身就是要收集的依赖。那么,我能不能修改一下myWatch的实现,以支持这样的写法:
myWatch(render,render)
第一个参数作为函数执行一下便有了之前第一个参数的作用,第二个参数还是需要被收集的依赖,嗯,想来合理。
那么,myWatch改写如下:
functionmyWatch(exp,fn){
target=fn
if(typeofexp==='function'){
exp()
return
}
letpathArr,
obj=data
if(/\./.test(exp)){
pathArr=exp.split('.')
pathArr.forEach(p=>{
target=fn
obj=obj[p]
})
return
}
data[exp]
}
但,对team的修改未能触发页面更新,想来因为render执行读取player收集依赖后target变为空函数,导致读取team收集依赖时收集到了空函数。这里大家的依赖都是render,故可将target=()=>{}这句删去。
myWatch这样实现还有个好处,假如data中有许多属性都需要通过render渲染至页面,一句myWatch(render,render)便可,无须如此这般繁复:
myWatch('player',render)
myWatch('team',render)
myWatch('number',render)
myWatch('height',render)
...
那么最终:
constdata={
player:'JamesHarden',
team:'HoustonRockets'
}
lettarget=null
functionwalk(data){
for(letkeyindata){
constdep=[]
letvalue=data[key]
if(Object.prototype.toString.call(value)==='[objectObject]'){
walk(value)
}
Object.defineProperty(data,key,{
set(newVal){
if(newVal===value)return
value=newVal
dep.forEach(f=>{
f()
})
},
get(){
dep.push(target)
returnvalue
}
})
}
}
walk(data)
functionmyWatch(exp,fn){
target=fn
if(typeofexp==='function'){
exp()
return
}
letpathArr,
obj=data
if(/\./.test(exp)){
pathArr=exp.split('.')
pathArr.forEach(p=>{
target=fn
obj=obj[p]
})
return
}
data[exp]
}
functionrender(){
document.body.innerText=`Thelastseason'sMVPis${data.player},he'sfrom${data.team}`
}
myWatch(render,render)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。