一、文章简介
写这篇文章起源于 vue2.js 数据侦测,参考书籍和多个网上资料,加上自己的领悟,遂写下这篇关于 对象的变化侦测和触发依赖 的文章。写的不足之处,希望道友对我指点。
对象变化侦测思考步骤分为:
- 数据劫持 ---- defineReactive;
- 每一个对象劫持属性所绑定的依赖(函数或模板)---- Watcher;
- 对 单个对象属性 绑定的所有依赖(函数或模板) 收集 ---- Dep;
- 深度(逐层遍历)对 对象内的属性 进行数据劫持 ---- Observer;
- 其他函数(抛在外面方便调用):
5.1 ---- 向外抛删除依赖的函数,可改删除成功回调: remove;
5.2 ---- 删除依赖函数(由于多次调用,我选择放在外面), delKeyDep;
5.3 ---- 获取对象属性值 getPatchData 等; - 对象变化侦测申明;
- 对象属性绑定依赖。
二、实现代码
将上述步骤转化为代码如下所示(方便理解就 稍微 进行简化):
第一步 进行1~4操作:
// 数据劫持
function defineReactive(data, key, val) {
if('object' == typeof val) new Observer(val); // 判断劫持属性值是否为对象,如果是 进行深度数据劫持
let dep = new Dep(); // 对每个 劫持对象属性,附带当对象发生变化时 对应 保存的依赖(函数或模板)数组
Object.defineProperty(data, key, {
enumerable: true, // 该属性是否可被枚举
configurable: true, // 该属性的配置后续是否可进行更改
get: function () { // 用于获取属性值
if(window.target) {
// 如果依赖不存在,添加依赖
if(-1 == dep.subs.indexOf(window.target)) {
dep.addSub(window.target); // 当对劫持属性进行获取值时,如果存在需要添加的依赖,进行添加依赖操作
}else {
if(window.boolBelKeyDep) { // 如果window.boolBelKeyDep 为真,删除依赖
dep.removeSub(window.target);
return;
}
}
}
return val
},
set: function (newVal) { // 设置属性值
if (newVal === val) return;
val = newVal; // 用新值替换旧值
dep.notify(); // 当劫持属性值发生改变时,通知绑定的依赖
}
})
}
// 每一个对象劫持属性所绑定的依赖(函数或模板),及cb
class Watcher {
/**
*
* @param {*} vm
* @param {String} expr
* @param {Function} cb
*/
constructor (vm, expr, cb) {
this.vm = vm;
this.cb = cb;
this.getter = getPatchData(expr); // 第一次通过get添加依赖
this.value = this.getValue(); // 存储值
}
getValue() {
window.target = this;
let value = this.getter(this.vm);
window.target = undefined;
return value
}
update() {
const oldValue = this.value;
this.value = this.getValue();
this.cb(this.value, oldValue)
}
}
/**
* 对 单个对象属性 绑定的所有依赖(函数或模板) 收集
*/
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
remove(this.subs, sub)
}
/**
* 当检测到对象属性发生变化,进行通知所有依赖
*/
notify() {
const arr = this.subs.slice()
for(let i = 0; i < arr.length; i++) {
arr[i].update()
}
}
}
/**
* 深度(逐层遍历)对 对象内的属性 进行数据劫持
*/
class Observer {
constructor(data) {
this.value = data;
if(Array.isArray(data)) {
// 劫持数组
}else {
this.walk(this.value)
}
}
/**
* (逐层遍历)对象内的属性, 逐一进行数据劫持
* @param {object} obj
*/
walk(obj) {
const keys = Object.keys(obj);
for(let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[ keys[i] ]);
}
}
}
第二步 进行5操作,主要用于抛出需要外部调用的方法:
/**删除依赖 */
function remove(subs, sub) {
if(subs.length && subs.length > 0) {
const index = subs.indexOf(sub);
if(index > -1) {
subs.splice(index, 1);
return; // '删除依赖回调'
}
}
}
// 在get中进行判断,是否删除依赖
function delKeyDep(vm, expr, delWatch) {
let objK = getPatchData(expr);
objK(vm, delWatch);
window.target = undefined;
window.boolBelKeyDep = false;
}
// 获取劫持对象属性值,且绑定依赖
function getPatchData(expr) {
// const reg = /\[^\w.$/;
// if(!reg.test(expr)) return;
if(0 == expr.indexOf('.')) return;
const arr = expr.split('.');
return function(vm, delWatch) { // 第一个参数表示监听对象(如 obj),第二个参数表示删除的依赖
if(!vm) return;
let obj = vm;
for(let i = 0; i < arr.length; i++){
if(delWatch && i == arr.length - 1) { // 当 delWatch 存在时,则为删除依赖,window.boolBelKeyDep 表示进行删除依赖操作
window.target = delWatch;
window.boolBelKeyDep = true;
}
obj = obj[arr[i]] // 当循环对劫持数据进行获取值时,会触发 defineReactive 函数内的get函数,即 如果依赖存在,添加依赖到依赖数组内
}
return obj
}
};
第三步 6~7操作,申明侦测对象,进行依赖操作
{ // 使用对象属性变化侦测
var obj = {
name: '',
age:'?',
hobby: {
allLength: '?'
}
}
new Observer(obj); // 目标对象进行数据侦测
var watch = new Watcher(obj, 'name', function(newVal, oldVal){ // 侦测目标对象属性 添加依赖
console.log(newVal, oldVal)
})
var allLengthWatch = new Watcher(obj, 'hobby.allLength',function(newVal, oldVal){
console.log('newVal, oldVal: ', newVal, oldVal);
});
}
三、效果图
附上效果图吧
对象属性新增依赖效果图:
对象属性删除依赖效果图: