1.介绍Object.defineProperty
vue2的响应式数据是依靠Object.defineProperty这个方法,用法如下:
const obj = {
username: "zz",
age: "zz",
};
// Object.defineProperty(对象名,对象键名,配置对象)
Object.defineProperty(obj, "username", {
get() {
// 调用obj.username 会运行这个函数
},
set() {
// 给obj.username赋值会运行这个函数
},
});
其中配置对象参数有多个可选值:
writable: 是否可以重写该属性
value: 当前值
get: 读取该值时会运行的函数
set: 给该值赋值时会运行的函数
enumerable: 是否可被迭代
configurable: 是否可以再次修改配置项
2.响应式原理
Object.defineProperty方法的配置对象里面有一个函数get,每次我们访问某个变量时就会通过get函数来获得。set函数在我们给某个变量赋值时也会触发这个函数。基于这套逻辑,我们就可以想到在get方法里去保存访问过这个变量的方法,在set方法里去触发访问这个变量的方法实现数据的更新,思路有了,接下来就是实现。
3.响应式原理的实现
假设页面上只有一个p元素,代码如下:
const pEl = document.querySelector("p");
const data = {
username: "zr",
};
pEl.innerHTML = data.username;
页面显示:
接下来我们基于上面的逻辑改造一下代码。
// 创建响应式数据对象
function observer(targetObj, key) {
// 保存值,不然get方法会无限递归
const val = targetObj[key];
Object.defineProperty(targetObj, key, {
get() {
return val;
},
set(newVal) {
val = newVal;
},
});
}
const data = {
username: "zr",
};
observer(data, "username");
function setUserNameVal() {
pEl.innerHTML = data.username;
}
setUserNameVal();
现在基本的逻辑框架已经出来了,现在的问题是我们无法知道当前是哪个方法在访问变量。解决办法就是创建一个全局变量,通过一个函数来控制这个变量,这个变量就是当前调用变量的方法。再在get方法里面保存这个方法到数组,在set方法里触发这个数组里所有的函数。下面是代码:
let currentFn = null;
// 更新数据函数
function update(fn) {
// 把当前运行的函数保存到全局变量中
currentFn = fn;
fn();
// 收集完依赖后清空
currentFn = null;
}
// 创建响应式数据对象
function observer(targetObj, key) {
// 保存值,不然get方法会无限递归
let val = targetObj[key];
const fnList = new Set();
Object.defineProperty(targetObj, key, {
get() {
// 如果是update函数访问则添加到依赖列表中,否则就是更新数据所触发的。
currentFn && fnList.add(currentFn);
return val;
},
set(newVal) {
val = newVal;
// 运行所有依赖该数据的函数 更新数据
fnList.forEach((fn) => {
fn();
});
},
});
}
const data = {
username: "zr",
};
observer(data, "username");
function setUserNameVal() {
pEl.innerHTML = data.username;
}
update(setUserNameVal);
4.结语
尤大大还得是尤大大,光一个响应式就蕴含了这么多开发技巧和思想在里面,更别说整个响应式系统和别的功能了,尤大大牛逼!