前言
总所周知,前端最核心的,就是改变页面内容。所以出现了jquery这样的便于我们操作dom的工具。
但是如果开发中数据一改变我们就遍历dom节点,然后进行更新的话,数据一多起来,其实是很不方便的;也不利于我们将工作重心放在业务的处理上,全都在“如何操作dom”上面去了;
那么我们想一想,有没有一种可能,我改变数据之后,页面中使用到这个数据的dom节点就自动更新数据的;
嘿嘿,还真有,那就是利用Object的defineProperty属性;
defineProperty介绍
该方法作用是精确地添加或修改对象的属性;
比如:以下的语法含义,就是给obj对象的prop属性添加剂一个值
语法
Object.defineProperty(obj, prop, descriptor)
- obj 要定义属性的对象。
- prop 要定义或修改的属性的名称或 Symbol。
- descriptor 要定义或修改的属性描述符。
详解第三个参数 -- descriptor
descriptor是属性描述符,是一个对象;其中有两个可选键值:
- get
属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
默认为 undefined。
- set
属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
默认为 undefined。
看到这个“调用此函数”,有想到什么吗?
对了,就是我们熟悉的回调函数,其触发时机就是我们访问和修改值的时候;
所以我们可以大胆假设:
如果我们想要改变值的时候自动进行dom的更新,那我们只需要在set的回调函数里面加入修改dom的逻辑就好啦~
数据劫持
根据上面我们对defineProperty
的介绍,我们基于它来实现一个简单的数据绑定吧~
wait wait;为啥要叫数据劫持呢,其实就是因为我们在访问/修改值的时候,拦截了这个逻辑,然后在里面加入了我们自己的一些逻辑(比如更新dom), 也就是对我们的属性进行一个响应式的绑定;
做了一不太恰当的比喻,一辆卡车本来在是装的一车苹果(get),要更改装的东西,也只能往里面加点水果(set),但是我现在硬要在往里面塞点人进去(更新dom); 我们能怎么办,只能把卡车拦截下来,然后手动给塞进去了呗~
小小比喻,各位看官海涵海涵~
先来一个超级基础的劫持
假设有一个对象let obj = {}
然后我们希望我使用obj.a
和obj.a = 2
的时候,都能触发get和set里面的回调,以加入我们自己逻辑;
怎么做呢;
我们看看实现:
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get: function () {
console.log("get ", key); // 回调函数
return val;
},
// 这里有个闭包 用于缓存新set的值,然后get才能拿到
set: function (newVal) {
if (newVal !== val) {
val = newVal;
console.log("set", key); // 回调函数
} else {
return;
}
},
});
}
复制代码
然后我们只需要对对象obj进行劫持即可
let obj = { a: { b: "b" }, c: "c" };
observe(obj);
obj.a.b;
obj.a.b = 2;
obj.c;
复制代码
如果对象有多个属性怎么办呢
此时,我们简单粗暴的可以
defineReactive(obj, "a", 1);
defineReactive(obj, "b", 2);
defineReactive(obj, "c", 3);
复制代码
如此循环重复的工作, 当然可以交给代码啦!!
我们可以用Object.keys(obj)
来枚举对象所有的key来进行响应式的绑定,看看下面代码 直接就可以把obj = {a:1, b:2 ,c:3}
变成响应式的数据;
Object.keys(obj).forEach(function (key) {
defineReactive(obj, key, obj[key]);
});
复制代码
但是遇到嵌套对象呢
比如我们有一个对象obj = {a: { b: 1} }
我们直接用defineReactive
,只能把a变成响应式的,b的get/set不会出发回调;
// 我们试试以下结果
Object.keys(obj).forEach(function (key) {
defineReactive(obj, key, obj[key]);
})
obj.a;
obj.b;
复制代码
所以我们需要进行一个递归操作;
// defineReactive(obj, key, val)
// 我们判断当这个val是对象时,我们继续进行响应式绑定
// 然后就有了以下代码
function defineReactive(obj, key, val) {
observe(val);
Object.defineProperty(obj, key, {
get: function () {
console.log("get ", key);
return val;
},
set: function (newVal) {
if (newVal !== val) {
val = newVal;
console.log("set", key);
} else {
return;
}
},
});
}
function observe(obj) {
if (typeof obj !== "object" || obj === null) {
return;
}
Object.keys(obj).forEach(function (key) {
defineReactive(obj, key, obj[key]);
});
}
复制代码
现在我们执行代码,结果如下
如果我们给属性赋值为对象呢
比如我们
let obj = {a:1}
obj.a = {b: 2}
obj.a.b
复制代码
会输出什么呢,"set a; get b"吗
并不是
稍微改一下下
// 将set函数稍微修改一下
set: function (newVal) {
if (newVal !== val) {
console.log("set ", key);
// 如果newVal是对象,再次进行响应式处理
observe(newVal);
val = newVal;
} else {
return;
}
}
复制代码
如果给对象新加一个属性呢
同上一条一样,因为没有进行响应式绑定(这是defineProperty天生的不足,它拦截不到新加的属性),所以我们需要再处理一下;Vue是给我们提供了一个新的api,Vue.set()
,我们也可以实现一个set函数
function set(obj, key, val) {
defineReactive(obj, key, val);
}
let obj = { a: 1 };
set(obj, "c", "c");
obj.c;
复制代码
数组怎么进行响应式绑定呢
因为数组不能被defineProperty劫持,所以我们得找一个新的方法来进行劫持;
什么东西可以劫持数组的方法呢?
bingo ! 数组原型链,因为数组的方法都在 Array.prototype上,换句话说,我们可以“劫持原型链”
具体方法
- 找到数组原型
- 覆盖那些能够修改数组的更新方法,使其可以通知更新
- 将得到的新的原型设置到数组
代码实现
第一步 找到原型
// 找到数组原型
const originalProto = Array.prototype
// 备份一份, 修改备份
const arrayProto = Object.create(originalProto)
复制代码
第二步 覆盖
['push', 'pop', 'shift', 'unshift', 'reverse', 'slice', 'sort'].forEach(
(method) => {
arrayProto[method] = function () {
// 原始操作
originalProto[method].apply(this, arguments)
// 覆盖操作:通知更新
console.log('数组执行 =>', method)
}
}
)
复制代码
第三步 响应式绑定
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return
}
// 在observe中加入以下数组的逻辑
if (Array.isArray(obj)) {
// 覆盖原型, 替换7个更新操作
obj.__proto__ = arrayProto
// 对数组内部元素进行响应式绑定
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
observe(obj[i])
}
} else {
new Observer(obj)
}
}
复制代码
之后数组使用原型链上的7个方法进行更新时,就会触发 console.log('数组执行 =>', method)
,真实的场景这里应该是换成更新dom的逻辑,类似于对象的set