现象+核心
现象:data变化会立即触发视图更新,视图view点击后会更新data中的数据
核心:Object.defineProperty(target,key,desc),通过 Object.defineProperty 遍历对象的每一个属性,把每一个属性变成一个 getter 和 setter 函数,读取属性的时候调用 getter,给属性赋值调用 setter。监听基本数据类型没问题,若是对象需要深度监听迭代,若是数组需要改写原型上的方法来触发视图更新。改写原型方法里面会先触发视图更新再修改this指向执行真正的方法。
Object.defineProperty(target,key,desc)
作用:新增或修改对象上的属性
- target属性所在的对象
- key属性名
- desc
- 数据属性:value configurable writable enumerable
- 访问器属性:get set,get返回一个内容,set赋值一个内容。属性的赋值和获取变为函数,从而可以监听。
var p1 = {
name: "lisi",
}
// configurable能否delete删除属性从而重新定义属性,能否修改属性特性,能否把属性改为访问器属性,默认false。
Object.defineProperty(p1, "name", {
configurable: false,
})
console.log(p1); //{ name: 'lisi' }
delete p1.name;
console.log(p1); //{ name: 'lisi' }
// writable:能否修改属性的值。默认值为false。
Object.defineProperty(p1, "age", {
writable: false,
value: 15,
})
console.log(p1.age); //15
p1.age = 20;
console.log(p1.age); //15
// enumerable:能否通过for in循环访问属性,默认false
Object.defineProperty(p1, "age", {
enumerable: false
})
for (var i in p1) {
console.log(p1[i]);
} // lisi
// get set
var book = {
_year: 2004,
edition: 1
}
Object.defineProperty(book, "year", {
get: function () {
return this._year
},
set: function (newYear) {
if (newYear > 2004) {
this._year = newYear;
this.edition += newYear - 2004
}
}
})
book.year = 2005;
console.log(book.edition); // 2
console.log(book._year); //2005
// 定义多个属性
var student = {};
Object.defineProperties(student, {
name: {
writable: false,
value: "lisi"
},
age: {
writable: true,
value: 16,
},
sex: {
get() {
return '男';
},
set(v) {
p1.sex = v
}
}
})
p1.sex = "男";
console.log(student.name + ":" + student.age);
console.log(p1.sex); // 男
student.sex = "女";
console.log(student.sex); //男
console.log(p1.sex); // 女
双向数据绑定实现
缺点:
-
不能监听到对象内部。
- 解决:复杂对象要深度监听,递归到底,一次性计算量大,若value下面嵌套多层对象,会一直执行深度监听函数,若特别深初始化就会卡死。改进:啥时候用啥时候监听
-
不能监听数组,要特殊处理
- 解决:重新定义数组原型,创建新对象A继承Array.prototype并新增方法fn,fn内部先触发视图更新再通过call执行真正的数组方法。若target是数组将target.proto=A,后续target.push实际调用的是新对象A上边改写后的push,改写后的push里面会触发视图更新+执行真正的push。
-
不能监听新增删除属性,不能走到Object.defineProperty,不会触发视图监听。
- 解决:Vue.set Vue.delete
扩展:vue3使用proxy实现响应式,但是有兼容性问题。
// 数据
const data = {
name: "aa",
age: 20,
info: {
address: "beijing"//深度监听,否则key只能传到info,不能监听到下一层的address
},
nums: [1, 2]//修改原型上的方法
}
function updateView() {
console.log("视图更新")
}
// 重新定义数组原型,创建新对象,原型指向oldArrayProperty,扩展新方法不会影响原型
const oldArrayProperty = Array.prototype;
const arrproto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach((methodName) => {
arrproto[methodName] = function () {
updateView()
// 调用原型上的方法
oldArrayProperty[methodName].call(this, ...arguments)
}
})
function definReactive(target, key, value) {
// 深度监听 observer里面会判断target是不是对象
// value是对象时候,address可以作为key传入
observer(value)
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 解决:data中key设置value新值可能从string类型变为对象,然后对这个对象动态修改,就不会触发视图更新,保证新值可以作为key被监听。
observer(newValue)
// 设置新值,value一直在闭包中,设置完之后,get可以获取到最新值
value = newValue
// 触发更新视图
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
// 不是对象
if (typeof target !== 'object' || target === null) {
// 不是对象 数组
return target
}
// 数组
if (Array.isArray(target)) {
// 改变target原型,调用改写后的方法,改写后的方法是先触发视图更新再调用Array原型上的方法
target.__proto__ = arrproto
}
// 遍历data重定义各个属性,添加get set
// target = {name: "aa",age: 20}
for (let key in target) {
definReactive(target, key, target[key])
}
}
// 监听数据
observer(data)
// 测试
data.name = 'bb'//触发set
data.age = 21//触发set
data.info.address = 'shanghai'//要深度监听
data.nums.push(4)//要改原型方法
data.x = '1000'//新增不能触发视图更新
delete data.name//删除不能触发视图更新