1. 前言
在vue中,只要数据变化,页面就会重新渲染,这个是怎么做到的呢?
在创建vue实例时,vue会将data中的成员代理给vue实例,目的就是实现响应式,监控数据变化,然后执行某个事件函数。在vue2.0中使用的是Object.defineProperty来实现数据的劫持,配合发布-订阅者模式来实现。
2. Object.defineProperty
首先我们来看一下怎么使用Object.defineProperty,其实使用方法很简单。这个函数接收三个参数:
1.需要监控的对象
2.需要监控的对象的某个属性
3.一个配置对象
前面两个参数很好理解,一目了然,第三个配置对象有很多属性,在这里主要介绍他的两个函数:set、get。
当我们给一个对象添加属性的时候就会调用set函数,当我们读取某个对象的属性的时候就会调用get方法,请看下面这个小例子:
const data = {
name:'小曹',
age:18
}
Object.defineProperty(data, "name", {
get(){
console.log('读取属性');
return 'xxx';
},
set(value){
console.log('设置属性:', value)
}
})
console.log(data.name);//输出结果:读取属性 xxx
data.name = "靓仔";//输出结果:设置属性:靓仔
上面例子的第一个console是获取data的值,触发了get方法。而且诡异的是输出结果还并不是我们想象中的“小曹”,而是‘xxx’。这是因为,Object.defineProperty的get方法返回值就是监控的对象的值,所以我们会打印出‘xxx’。
第二个console是我们重新赋值,这时候触发了set方法,并且det方法接收一个参数,这个参数就是要设置的值,所以我们可以在set方法中对赋值进行控制。
使用这种方式只能监控一个属性,显然这并不符合我们的需求,所以可以使用循环遍历来实现
3. 监控对象属性
实现同时监控多个对象属性,我们可以使用for-in遍历,封装成一个函数:
function defineReactive(data, key, value){
Object.defineProperty(data, key, {
get(){
console.log('读取属性');
return value;
},
set(val){
console.log('设置属性:', value)
value = val;
}
})
}
function observer(data){
for(let key in data){
defineReactive(data, key, data[key]);
}
}
observer(data);
console.log(data.age);//18
console.log(data.name);//小曹
这样就可实现监控对象的读和写的操作。既然我们已经可以监控到对象的写这个操作,那么在vue中,每次重新赋值就会再次渲染页面,达到响应式。那么我们就可以在set函数里执行渲染函数,我们给这个渲染函数起名为render,render函数内部具体实现在这里我就不多说了,在这里就用一句话代替。渲染函数具体实现可以看这个篇文章https://blog.csdn.net/qq_44197554/article/details/105904564
。
在我们设置属性值的时候,如果两个值相等,那么我们还有重新渲染的必要吗?是不是就不需要渲染了,这个可以节省性能,所以我们在set函数中加个判断
set(val){
console.log('设置属性:', value)
//如果两值相等,则直接结束
if(value === val){
return;
}
value = val;
render();
}
4. 递归遍历
现在又有一个问题,如果我们的data对象中的某个属性值是一个对象,这样还能监控到吗,测试过后发现,是不能的,所以我们就需要用到递归了,请看下面代码:
const data = {
name:'小曹',
age:18,
obj:{
a:1
}
}
function defineReactive(data, key, value){
//进行递归遍历
observer(value);
Object.defineProperty(data, key, {
get(){
console.log('读取属性');
return value;
},
set(val){
console.log('设置属性:', value)
if(value === val){
return;
}
value = val;
render();
}
})
}
function observer(data){
//判断传入的是否为一个对象
if(typeof data === 'object'){
for(let key in data){
defineReactive(data, key, data[key]);
}
}
}
经过上面的递归遍历,不管data里嵌套多少层对象,都会被监控到
5. 数组
vue响应式中 Object.defineProperty没有观察数组,原因是太消耗新能。官方说性能与用户体验不成正比
所以我们就需要在observer函数中判断传入的data是否为一个数组
function observer(data){
//判断是否为数组
if(Array.isArray(data)){
return;
}
if(typeof data === 'object'){
for(let key in data){
defineReactive(data, key, data[key]);
}
}
}
那么在vue中操作数组是怎么实现的呢?是因为在vue中重写了数组的方法,所以当我们在vue中使用数组的方法时,就执行了render函数
//保存数组原型
const arrayProto = Array.prototype;
//克隆一个原型对象
const arrayMethods = Object.create(arrayProto);
//重写所有的函数
['push', 'pop', 'shift', 'unshift', 'sort', 'splice', 'reverse'].forEach(method => {
arrayMethods[method] = function (){
//改变重写函数的this指向
//展开传入的值
arrayProto[method].call(this, ...arguments);
render();
}
})
大家是不是有疑问,为什么要重新克隆一个数组的原型呢?
是因为只需要在使用数组变异方法的时候执行这些重写的方法,对于其他的数组不污染其原型。所以更改的是克隆出来的原型,而不是本来的原型。此时数组执行的还是本来原型上的方法,所以需要在observer函数中修改数组的原型指向
function observer(data){
if(Array.isArray(data)){
//改变数组原型指向
data.__proto__ = arrayMethods;
return;
}
if(typeof data === 'object'){
for(let key in data){
defineReactive(data, key, data[key]);
}
}
}
data.arr.push(100);
console.log(data.arr);//[1, 2, 3, 100]
(1)实现$set
$set方法的返回值就是要修改的值,该方法有三个参数:
1.data:要修改的对象或数组
2.key:要修改哪个属性
3.value:修改成什么
//实现$set方法
function $set(data, key, value){
//修改数组
if(Array.isArray(data)){
data.splice(key, 1, value);
return value;
}
//修改对象
defineReactive(data, key, value);
render();
return value;
}
(2)实现$delete
该函数接收两个参数:
1.data:要删除哪个对象的属性
2.key:删除哪个属性
//实现$delete
function $delete(data, key){
if(Array.isArray(data)){
data.splice(key, 1);
return;
}
delete data[key];
render();
}
6.劣势
因此使用Object.defineProperty实现响应式有几个劣势
1.天生就需要进行递归
2.监听不到数组不存在的索引的改变
3.监听不到数组长度的改变
4.监听不到对象的增删
在vue3.0中解决了递归观察的问题,使用proxy代理
7.所有源码
const data = {
name:'小曹',
age:18,
obj:{
a:1
},
arr:[1, 2, 3]
}
//保存数组原型
const arrayProto = Array.prototype;
//重新创建一个原型对象
const arrayMethods = Object.create(arrayProto);
//重写所有的函数
['push', 'pop', 'shift', 'unshift', 'sort', 'splice', 'reverse'].forEach(method => {
arrayMethods[method] = function (){
//改变重写函数的this指向
//展开传入的值
arrayProto[method].call(this, ...arguments);
render();
}
})
function defineReactive(data, key, value){
observer(value);
Object.defineProperty(data, key, {
get(){
console.log('读取属性');
return value;
},
set(val){
console.log('设置属性:', value)
if(value === val){
return;
}
value = val;
render();
}
})
}
function observer(data){
if(Array.isArray(data)){
//改变数组原型指向
data.__proto__ = arrayMethods;
return;
}
if(typeof data === 'object'){
for(let key in data){
defineReactive(data, key, data[key]);
}
}
}
function render(){
console.log('页面渲染了');
}
//实现$set方法
function $set(data, key, value){
//修改数组
if(Array.isArray(data)){
data.splice(key, 1, value);
return value;
}
//修改对象
defineReactive(data, key, value);
render();
return value;
}
//实现$delete
function $delete(data, key){
if(Array.isArray(data)){
data.splice(key, 1);
return;
}
delete data[key];
render();
}
observer(data);
data.arr.push(100);
console.log(data.arr);