Vue的数据响应式是怎么一回事?其实理解了之前我写过的一篇《从根源理解Proxy对比defineProperty优势》(原文链接:http://t.csdnimg.cn/DipXh),应该对Vue利用defineProperty实现数据劫持数据监听的方式有一定的了解。那我们这篇文章就以Vue2的defineProperty实现数据劫持数据监听的原理为基础,进一步展开讲述Vue是怎么进一步利用该数据劫持的方法实现数据响应式的。
<script>
const vm=new Vue({
el:'#root',
data:{
firstName:'木',
lastName:'鱼',
isActive:true,
},
computed:{
fullName(){
return this.firstName+this.lastName
}
},
watch:{
isActive:{
immediate:true,
handler(newValue,oldValue){
console.log('isActive被修改了');
}
}
}
})
</script>
以这段代码为例,我们通过在computed、watch上指定某个函数或变量后,每当computed中的firstName、lastName发生变化时则会重新计算fullName,每当watch中的isActive发生变化,也会执行其handler函数。
实际上,对于数据响应式,Vue底层做的事情,就是监听指定的响应式数据(如定义在data上的数据),当监听的数据发生变化,则会自动重新运行依赖该数据的函数。
为何computed、watch等上指定的数据也是响应式的?因为computed、watch这些函数都有依赖到响应式数据,数据一旦发生变化,computed、watch就会重新执行相应的函数。
Vue做的事情也就是在数据变化时重新执行相关函数。那么为什么数据的变化会引起界面的更新?其实本质也是这个道理。界面是由render()函数运行渲染呈现的,而实际上Vue上定义的响应式数据都是囊括在render函数里的,因此响应式数据一旦发生改变,Vue底层就会自动重新运行依赖该数据的所有函数,包括render函数,因此页面也会重新渲染。该函数或变量所依赖的数据则都会是响应式的。
好了,明白了Vue数据响应式的本质,我们再来简述一下其实现的原理,也就是:响应式数据(被监听到的数据)发生变化,该怎么确保涉及到该数据的函数都执行一遍呢?
我们知道数据截取是基于defineProperty实现的。
在以下实现数据截取基础上我们逐一推进思考(如果看不懂以下代码请参照前篇 《从根源理解Proxy对比defineProperty优势》(原文链接:http://t.csdnimg.cn/DipXh)):
function observe(obj){
for(const key in obj){
let initialValue=obj[key]
Object.defineProperty(obj,key,{
get(){
console.log(`该函数的调用说明有人读取了${key}属性`)
return initialValue
},
set(value){
console.log(`该函数的调用说明有人将${key}属性的值修改为${value}`)
initialValue=value
}
})
}
}
1、备一个存放有依赖响应式数据的数组:响应式数据作为对象data的属性通过类似上述的observe函数截取到了,那么如何知道哪些函数调用了该数据呢?也就是要确认哪些函数调用了obj的key。那么我们可以定义一个数组funcs用于存放调用了这些数据的函数。当数据修改时(也就是set调用时),则遍历funcs数组里的函数,将每个数组里的函数都调用一遍。
2、通过"window.特定名称"拿到函数塞进数组中:那么我们怎么该怎么把调用的函数一个个push到funcs数组里呢?我们也不知道函数名称是什么,所以不如就将调用的函数都统一起一个名称,如叫"__func",框架也好实现了,只要调用到响应式数据的函数临时改名为__func,直接通过window.__func就可以拿到调用响应式数据的函数并将其往funcs数组里塞就行了。响应式数据key被调用了,则会触发get方法,这时再在get方法中用funcs.push(window.__func)即可(push时要注意保证数组元素单一性)。
//1、2思路代码实现如下
function observe(obj){
for(const key in obj){
let initialValue=obj[key]
let funcs=[]//用于存放所有依赖到响应式数据key的函数
Object.defineProperty(obj,key,{
get(){
console.log(`该函数的调用说明有人读取了${key}属性`)
// 通过window.__func拿到依赖该响应式数据的函数
if(window.__func && !funcs.includes(window.__func)){
funcs.push(window.__func)
}
return initialValue
},
set(value){
console.log(`该函数的调用说明有人将${key}属性的值修改为${value}`)
initialValue=value
// 只要响应式数据改变,依赖到该数据的数组全都要再执行一遍
for(let i in funcs){
funcs[i]()
}
}
})
}
}
那么只要我们通过__func拿到依赖响应式数据的函数,再通过以上代码,就可以实现将数据一变,依赖该数据的函数也跟着重新执行一遍了。
3、将所有调用响应式数据的函数临时改为统一的特定名称:那么接下来的问题就是如何把调用到响应式数据的函数临时改名为__func了。我们仅需在调用该函数的前将该函数赋值给window.__func即可,该函数执行完毕后,再将window.__func还原为null。我们可以通过定义一个autorun函数实现,如下:
function autorun(fn){
window.__func=fn
fn()
window.__func=null
}
比如fullName()有依赖到响应式数据,则通过autorun(fullName)执行。
我们以computed为例,实际上在computed上定义fullName(),Vue底层就会通过类似autorun(fullName)的方式处理computed中的函数。而fullName中有依赖到firstName、lastName都是定义在data中的,都是Vue监听范围里的数据,这意味着一旦被调用就会触发其get函数。
- 因此,当fullName被模板读取调用时,则会执行autorun(fullName),此时window.__func正是fullName;
- 而在执行fullName()时,fullName函数里会读取到firstName、lastName,则会调用到firstName、lastName的get函数;
- 则firstName、lastName的get函数会将window.__func(此时也就是fullName)push进各自的funcs数组里(注意每个key各配一个funcs数组存依赖该key的函数);
- 而后比如你修改了firstName,则firstName的set函数会执行,则所有依赖到firstName的函数(即firstName的funcs数组元素)都会被遍历执行一遍,因此fullName也会随之执行一遍,随之更新最新值。
这就是Vue响应式数据实现的整个过程。