大家好,我是麦当当(小组队名起的感觉还不错),Vue的响应式用过的人都快手称赞,但是我只知其一不知其二。或许都知道用proxy代理啊,可是在整个设计系统上没有践行。那单单是proxy还有其他设计吗?
正好在《Vue.js设计与实现》中对响应式系统的设计由0到1设计,这样让人学到了。
理解什么叫响应式
在最新出版的《Vue.js设计与实现》中第四章解释了Vue的响应式构建。此书有了很好的定义。首先需要理解什么是响应式数据和副作用函数。
function effect (){
document.body.innerText='hello vue3'
}
effect()
effect函数的执行会直接修改body文本内容的值,这就称为产生了副作用的函数。举个例子:
let indexValue = 1
function effect(){
indexValue=2 //修改了全局变量,产生副作用
}
那么假设副作用函数中读取了某个对象的属性值
const mjc = {text:"hellow"}
function effect(){
document.body.innerText=mjc.text //访问了mjc属性触发读取操作
}
mjc.text = 'mjc'
这句话修改了mjc.text的值,如果能让effect函数重新执行一遍,那么这个对象就是响应式数据。这个定义我一开始也是懵逼的,响应式数据和副作用函数有什么关联?
Vue最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的JavaScript对象。而当你修改它们时,视图会进行更新,所订阅的会被更新。
换句话说,副作用函数其实就是一个订阅者,而响应式数据就是发布者。两者密切联系,发布者在数据变化的时候去通知订阅了对应数据的人。
初步封装一个响应式
//封装一个响应式的函数
let reactiveFns = [];
function watchFn(fn) {
reactiveFns.push(fn);
fn&& fn()
}
let obj = {
name: "mjc",
age:18
};
const objProxy = new Proxy(obj, {
get: function (target, key, receiver) {
return target[key]
},
set: function (target, key, newVal, receiver) {
target[key] = newVal
reactiveFns.forEach((fn) => {
fn();
})
},
});
//假设当前对obj.name进行响应式收集
watchFn(function () {
const newName = objProxy.name;
console.log("obj.name的执行");
console.log(objProxy.name);
});
//假设obj.name改变,自动执行桶中的fn
objProxy.name = "kobe";
假设当前对obj.name进行响应式,用proxy代理了obj对象的get和set的劫持操作,当访问name时get会返回数据,设置name会修改name的值,同时将reactiveFns桶遍历执行,通过注册一个watchFn副作用函数,将副作用函数存入到属于obj.name的桶中,一旦obj.name改变了,就去桶中取出来遍历执行副作用函数。
思路已经大致清晰了:
当读取操作发生时,将对应的副作用函数收集到桶里。
当设值操作发生时,将桶里的函数遍历取出执行。
设计完善一个响应式系统
现在再处理细节,为了设计一个更完善的响应系统考虑一下几点:
1.这个桶目前只能装name属性的,如果要装入age的呢?又如果还有别的对象响应呢?这个桶的结构就得分层次了如下
一个对象:
多个对象:
let obj = {
name: "mjc",
age: 18,
};
let info = {
address:"江门大学"
};
开始设计代码
//封装一个桶
let busket = new WeakMap(); //新增 存储所有响应式数据的副作用函数的桶
//封装存储全局注册的副作用函数
let reactiveFns = null; //修改为普通变量,注册副作用函数
function watchFn(fn) {
reactiveFns = fn; //新增 副作用执行一次注册到变量中
fn && fn
reactiveFns = null; //恢复初始状态null,便于判断收不收集访问没有注册的属性
}
let obj = {
name: "mjc",
age: 18,
};
const objProxy = new Proxy(obj, {
get: function (target, key, receiver) {
if (!reactiveFns) return target[key];//新增 判断有无注册这个函数
let targetDepend = busket.get(target);//新增 获取桶里被访问的对象
if (!targetDepend) {//新增 如果不存在,创建map对象存储
targetDepend = new Map();
busket.set(target, targetDepend);
}
let depend = targetDepend.get(key);//新增 通过key再去获取下一层对象
if (!depend) {//新增 如果没有,创建一个Set对象,可以去重
depend = new Set();
targetDepend.set(key, depend);
}
depend.add(reactiveFns);//新增 添加副作用函数进去深层桶中
return target[key];
},
set: function (target, key, newVal, receiver) {
target[key] = newVal;
let targetDepend = busket.get(target);//新增 通过桶拿到被访问
if (!targetDepend) return;//对象的桶,再从对象的桶中通过key取出
let depend = targetDepend.get(key);//属性
if (!depend) return;
depend.forEach((fn) => {
fn();
});
},
});
//假设当前对obj进行响应式收集
watchFn(function () {
const newName = objProxy.name;
console.log("obj.name的执行");
console.log(objProxy.name);
});
//假设obj.name改变,自动执行obj.name桶中的fn
objProxy.name = "kobe";
控制台运行代码
控制台运行代码,可以发现obj.name改变了,副作用函数执行了两次。一次是我们主动执行,这样才能访问副作用中的响应式。第二次由obj.name被设置触发。通过Set对象也防止了同一副作用重复执行。
为什么使用weakMap作为桶的原因:
浏览器window对象中的obj指向了堆内存中的地址,根据Gc机制引用计数,从根对象引用的对象不会被垃圾回收处理。而weakMap的对obj的引用是一个弱引用,Gc机制会判断弱引用指向的是否还有别的引用指向,如果obj = null,那么弱引用无法维持联系会被垃圾回收销毁,防止内存泄露。
再考虑边境情况,分别执行以下
objProxy.age = 22; //age属性没有跟副作用函数建立联系,当变化时不能触发副作用
由于拦截了proxy的set拦截器,从busket中取出会判断,自然不会响应
console.log(objProxy.age) //读取没有联系的age属性,不能收集非副作用注册的依赖,必须要经过副作用函数注册,实现订阅
访问没有与桶建立联系的age属性,不会触发收集依赖
不同副作用函数
- 注册多个副作用函数
watchFn(function () {
const newName = objProxy.name;
console.log("obj.name的执行");
console.log(objProxy.name);
});
watchFn(function () {
console.log(objProxy.name + "--------------------");
});
objProxy.name = "kobe";
-
分别注册不同的副作用属性函数
watchFn(function () {
const newName = objProxy.name;
console.log("obj.name的执行");
console.log(objProxy.name);
});
watchFn(function () {
console.log(objProxy.age + "--------------------");
});
//假设obj.name改变,自动执行桶中的fn
objProxy.age = 18;
-
注册同一副作用不同属性
watchFn(function () {
const newName = objProxy.name;
console.log("obj.name的执行");
console.log(objProxy.name);
console.log(objProxy.age + "--------------------");
});
objProxy.age = 22;
objProxy.name = "麦某人";//当name或者age改变时都会触发收集的副作用依赖
proxy代理的过程可以抽离出来优化一下
function reactive(obj) {
return new Proxy(obj, {
get: function (target, key, receiver) {
if (!reactiveFns) return target[key];
let targetDepend = busket.get(target);
if (!targetDepend) {
targetDepend = new Map();
busket.set(target, targetDepend);
}
let depend = targetDepend.get(key);
if (!depend) {
depend = new Set();
targetDepend.set(key, depend);
}
depend.add(reactiveFns);
return target[key];
},
set: function (target, key, newVal, receiver) {
target[key] = newVal;
let targetDepend = busket.get(target);
if (!targetDepend) return;
let depend = targetDepend.get(key);
if (!depend) return;
depend.forEach((fn) => {
fn();
});
},
});
}
那么不觉得有点熟悉的感觉了吗?比如vue3的composition API 中的reactive() ,当我们需要对某个对象进行响应式的时候,只需要如下调用:
const objProxy = reactive({
name: "mjc",
age: 18,
})
到了这一步都知道了,这就是vue响应式代理基本原理了,如果是vue2的话用Object.defineProperty() 遍历数据劫持一下,如下
function reactive(obj) {
Object.keys(obj).forEach((key) => {
let value = obj[key];
Object.defineProperty(obj, key, {
get: function () {
if (!reactiveFns) return value;
let targetDepend = busket.get(target);
if (!targetDepend) {
targetDepend = new Map();
busket.set(target, targetDepend);
}
let depend = targetDepend.get(key);
if (!depend) {
depend = new Set();
targetDepend.set(key, depend);
}
depend.add(reactiveFns);
return value;
},
set: function (newValue) {
value = newValue;
const depend = getDepend(obj, key);
depend.noticfy();
},
});
});
}
以上代码以及原理设计仅供参考。所以毫不夸张地说,理解了响应式原理,无论写什么代码,至少优势在我!