前言
"数据绑定" 的关键在于监听数据的变化,vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。其实主要是用了ES5中的Object.defineProperty方法来劫持对象的属性添加或修改的操作,从而更新视图。
听说vue3.0 会用 proxy 替代 Object.defineProperty()方法。所以预先了解一些用法是有必要的。proxy 能够直接 劫持整个对象,而不是对象的属性,并且劫持的方法有多种。而且最后会返回劫持后的新对象。所以相对来讲,这个方法还是挺好用的。不过兼容性不太好。
一、defineProperty
ES5 提供了 Object.defineProperty 方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
【1】语法
Object.defineProperty(obj, prop, descriptor)
参数:
obj:必需,目标对象
prop:必需,需定义或修改的属性的名字
descriptor:必需,将被定义或修改的属性的描述符
返回值:
传入函数的对象,即第一个参数obj
【2】descriptor参数解析
函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符和存取描述符
数据描述:当修改或定义对象的某个属性的时候,给这个属性添加一些特性,数据描述中的属性都是可选的
- value:属性对应的值,可以使任意类型的值,默认为undefined
- writable:属性的值是否可以被重写。设置为true可以被重写;设置为false,不能被重写。默认为false
- enumerable:此属性是否可以被枚举(使用for...in或Object.keys())。设置为true可以被枚举;设置为false,不能被枚举。默认为false
- configurable:是否可以删除目标属性或是否可以再次修改属性的特性(writable, configurable, enumerable)。设置为true可以被删除或可以重新设置特性;设置为false,不能被可以被删除或不可以重新设置特性。默认为false。这个属性起到两个作用:1、目标属性是否可以使用delete删除 2、目标属性是否可以再次设置特性
存取描述:当使用存取器描述属性的特性的时候,允许设置以下特性属性
- get:属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。默认为 undefined。
- set:属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined。
【3】示例
- value
let obj = {}
// 不设置value属性
Object.defineProperty(obj, "name", {});
console.log(obj.name); // undefined
// 设置value属性
Object.defineProperty(obj, "name", {
value: "Demi"
});
console.log(obj.name); // Demi
- writable
let obj = {}
// writable设置为false,不能重写
Object.defineProperty(obj, "name", {
value: "Demi",
writable: false
});
//更改name的值(更改失败)
obj.name = "张三";
console.log(obj.name); // Demi
// writable设置为true,可以重写
Object.defineProperty(obj, "name", {
value: "Demi",
writable: true
});
//更改name的值
obj.name = "张三";
console.log(obj.name); // 张三
- enumerable
let obj = {}
// enumerable设置为false,不能被枚举。
Object.defineProperty(obj, "name", {
value: "Demi",
writable: false,
enumerable: false
});
// 枚举对象的属性
for (let attr in obj) {
console.log(attr);
}
// enumerable设置为true,可以被枚举。
Object.defineProperty(obj, "age", {
value: 18,
writable: false,
enumerable: true
});
// 枚举对象的属性
for (let attr in obj) {
console.log(attr); //age
}
- **configurable **
//-----------------测试目标属性是否能被删除------------------------//
let obj = {}
// configurable设置为false,不能被删除。
Object.defineProperty(obj, "name", {
value: "Demi",
writable: false,
enumerable: false,
configurable: false
});
// 删除属性
delete obj.name;
console.log(obj.name); // Demi
// configurable设置为true,可以被删除。
Object.defineProperty(obj, "age", {
value: 19,
writable: false,
enumerable: false,
configurable: true
});
// 删除属性
delete obj.age;
console.log(obj.age); // undefined
//-----------------测试是否可以再次修改特性------------------------//
let obj2 = {}
// configurable设置为false,不能再次修改特性。
Object.defineProperty(obj2, "name", {
value: "dingFY",
writable: false,
enumerable: false,
configurable: false
});
//重新修改特性
Object.defineProperty(obj2, "name", {
value: "张三",
writable: true,
enumerable: true,
configurable: true
});
console.log(obj2.name); // 报错:Uncaught TypeError: Cannot redefine property: name
// configurable设置为true,可以再次修改特性。
Object.defineProperty(obj2, "age", {
value: 18,
writable: false,
enumerable: false,
configurable: true
});
// 重新修改特性
Object.defineProperty(obj2, "age", {
value: 20,
writable: true,
enumerable: true,
configurable: true
});
console.log(obj2.age); // 20
- set 和 get
let obj = {
name: 'Demi'
};
Object.defineProperty(obj, "name", {
get: function () {
//当获取值的时候触发的函数
console.log('get...')
},
set: function (newValue) {
//当设置值的时候触发的函数,设置的新值通过参数value拿到
console.log('set...', newValue)
}
});
//获取值
obj.name // get...
//设置值
obj.name = '张三'; // set... 张三
二、Proxy
Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)
其实就是在对目标对象的操作之前提供了拦截,可以对外界的操作进行过滤和改写,修改某些操作的默认行为,这样我们可以不直接操作对象本身,而是通过操作对象的代理对象来间接来操作对象,达到预期的目的~
【1】语法
const p = new Proxy(target, handler)
【2】参数
target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
handler:也是一个对象,其属性是当执行一个操作时定义代理的行为的函数,也就是自定义的行为
【3】handler方法
handler 对象是一个容纳一批特定属性的占位符对象。它包含有 Proxy 的各个捕获器(trap),所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。
handler.getPrototypeOf() ===》 Object.getPrototypeOf 方法的捕捉器
handler.setPrototypeOf() ===》 Object.setPrototypeOf 方法的捕捉器
handler.isExtensible() ===》 Object.isExtensible 方法的捕捉器
handler.preventExtensions() ===》 Object.preventExtensions 方法的捕捉器
handler.getOwnPropertyDescriptor() ===》 Object.getOwnPropertyDescriptor 方法的捕捉器
handler.defineProperty() ===》 Object.defineProperty 方法的捕捉器
handler.has() ===》 in 操作符的捕捉器
handler.get() ===》 属性读取操作的捕捉器
handler.set() ===》 属性设置操作的捕捉器
handler.deleteProperty() ===》 delete 操作符的捕捉器
handler.ownKeys() ===》 Object.getOwnPropertyNames方法和 Object.getOwnPropertySymbols 方法的捕捉器
handler.apply() ===》 函数调用操作的捕捉器
handler.construct() ===》 new 操作符的捕捉器
【4】示例
let obj = {
name: 'name',
age: 18
}
let p = new Proxy(obj, {
get: function (target, property, receiver) {
console.log('get...')
},
set: function (target, property, value, receiver) {
console.log('set...', value)
}
})
p.name // get...
p = {
name: 'dingFY',
age: 20
}
// p.name = '张三' // set... 张三
三、defineProperty和Proxy对比
- Object.defineProperty只能劫持对象的属性,而Proxy是直接代理对象。
由于 Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性,如果属性值也是对象,则需要深度遍历。而 Proxy 直接代理对象,不需要遍历操作。
- Object.defineProperty对新增属性需要手动进行Observe。
由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象(改变属性不会自动触发setter),对其新增属性再使用 Object.defineProperty 进行劫持。
也正是因为这个原因,使用vue给 data 中的数组或对象新增属性时,需要使用 vm.$set 才能保证新增的属性也是响应式的。
- defineProperty会污染原对象(关键区别)
proxy去代理了ob,他会返回一个新的代理对象不会对原对象ob进行改动,而defineproperty是去修改元对象,修改元对象的属性,而proxy只是对元对象进行代理并给出一个新的代理对象。
四、简单实现数据双向绑定
【1】新建myVue.js文件,创建myVue类
class myVue extends EventTarget {
constructor(options) {
super();
this.$options = options;
this.compile();
this.observe(this.$options.data);
}
// 数据劫持
observe(data) {
let keys = Object.keys(data);
// 遍历循环data数据,给每个属性增加数据劫持
keys.forEach(key => {
this.defineReact(data, key, data[key]);
})
}
// 利用defineProperty 进行数据劫持
defineReact(data, key, value) {
let _this = this;
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
return value;
},
set(newValue) {
// 监听到数据变化, 触发事件
let event = new CustomEvent(key, {
detail: newValue
});
_this.dispatchEvent(event);
value = newValue;
}
});
}
// 获取元素节点,渲染视图
compile() {
let el = document.querySelector(this.$options.el);
this.compileNode(el);
}
// 渲染视图
compileNode(el) {
let childNodes = el.childNodes;
// 遍历循环所有元素节点
childNodes.forEach(node => {
if (node.nodeType === 1) {
// 如果是标签 需要跟进元素attribute 属性区分v-html 和 v-model
let attrs = node.attributes;
[...attrs].forEach(attr => {
let attrName = attr.name;
let attrValue = attr.value;
if (attrName.indexOf("v-") === 0) {
attrName = attrName.substr(2);
// 如果是 html 直接替换为将节点的innerHTML替换成data数据
if (attrName === "html") {
node.innerHTML = this.$options.data[attrValue];
} else if (attrName === "model") {
// 如果是 model 需要将input的value值替换成data数据
node.value = this.$options.data[attrValue];
// 监听input数据变化,改变data值
node.addEventListener("input", e => {
this.$options.data[attrValue] = e.target.value;
})
}
}
})
if (node.childNodes.length > 0) {
this.compileNode(node);
}
} else if (node.nodeType === 3) {
// 如果是文本节点, 直接利用正则匹配到文本节点的内容,替换成data的内容
let reg = /\{\{\s*(\S+)\s*\}\}/g;
let textContent = node.textContent;
if (reg.test(textContent)) {
let $1 = RegExp.$1;
node.textContent = node.textContent.replace(reg, this.$options.data[$1]);
// 监听数据变化,重新渲染视图
this.addEventListener($1, e => {
let oldValue = this.$options.data[$1];
let reg = new RegExp(oldValue);
node.textContent = node.textContent.replace(reg, e.detail);
})
}
}
})
}
}
【2】在html文件中引入myVue.js, 创建实例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="./mvvm.js" type="text/javascript"></script>
<title>Document</title>
</head>
<body>
<div id="app">
<div>我的名字叫:{{name}}</div>
<div v-html="htmlData"></div>
<input v-model="modelData" /> {{modelData}}
</div>
</body>
<script>
let vm = new myVue({
el: "#app",
data: {
name: "Demi",
htmlData: "html数据",
modelData: "input的数据"
}
})
</script>
</html>
【3】效果
文章每周持续更新,可以微信搜索「 前端大集锦 」第一时间阅读,回复【视频】【书籍】领取200G视频资料和30本PDF书籍资料