什么是数据劫持?
举一个vue的例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<!-- 插值表达式 -->
{{ message }}
<input v-model="message" />
<div v-html="htmlData"></div>
</div>
</body>
<script>
// 1.怎么实现数据驱动?把数据渲染到视图里;
let vm = new Vue({
el: "#app",
data: {
message: "测试数据",
htmlData: "<h1>标题</h1>"
}
})
// 2.二次渲染;数据劫持;
// console.log(vm._data)
setTimeout(function () {
vm._data.message = "修改的数据"
}, 1000);
</script>
</html>
数据劫持就是修改内容后,差值表达式能够知道修改并作出变化
1 通过defineProperty实现数据劫持
defineProperty创建对象、修改对象
let obj = {
name:"张三"
}
let value = obj["name"]
Object.defineProperty(obj,"name",{
configurable:true,
enumerable:false,
get(){
return value;
},
set(newValue){
console.log("set...",newValue);
value = newValue;
}
})
console.log(obj);
obj.name = "王五";
// configurable: false,外部无法获取对象属性
for (let key in obj) {
console.log(key)
}
delete obj.name;
参数:
- enumerable 是否可枚举 默认为true
为false时外部就无法获取这个对象内的属性 - configurable 是否可删除
当configurable: true时,对象属性就无法被删除
1.1 利用自定义事件实现vue数据劫持
class Vue extends EventTarget {
constructor(opts) {
super();
this.opts = opts;
this._data = this.opts.data;
this.observe(this._data);
this.compile();
}
observe(data) {
for (let key in data) {
let _this = this;
let value = data[key];
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
console.log("get...");
return value;
},
set(newValue) {
console.log("set...");
// 触发事件更新;
let event = new CustomEvent(key, {
detail: newValue
});
_this.dispatchEvent(event);
value = newValue;
}
});
}
}
// 初次编译到dom里;
compile() {
let ele = document.querySelector(this.opts.el);
let childNodes = ele.childNodes;
this.compileNode(childNodes);
}
compileNode(childNodes) {
childNodes.forEach(node => {
if (node.nodeType === 3) {
// 获取文本的内容;
let textContent = node.textContent;
// 分组匹配插入表达式里的变量
let reg = /\{\{\s*([^\{\}\s*]+)\s*\}\}/g;
if (reg.test(textContent)) {
let $1 = RegExp.$1;
console.log(this.opts.data[$1]);
node.textContent = node.textContent.replace(reg, this._data[$1]);
// 绑定自定义事件;
this.addEventListener($1, e => {
let newValue = e.detail;
let oldValue = this._data[$1];
let updateReg = new RegExp(oldValue);
node.textContent = node.textContent.replace(updateReg, newValue);
});
}
} else if (node.nodeType === 1) {
// 判断节点内是否有子节点;
if (node.childNodes.length > 0) {
// 有子节点
this.compileNode(node.childNodes);
}
}
});
}
}
1.2 通过发布订阅模式实现数据劫持
什么是发布订阅模式?
- 分别定义一个收集器和订阅者类
- 通过收集器收集订阅者
// 收集器;
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify(newValue) {
this.subs.forEach(sub => {
sub.update(newValue);
});
}
}
// 订阅者;
class Watcher {
constructor(data, key, cb) {
Dep.target = this;
data[key];
this.cb = cb;
Dep.target = null;
}
update(newValue) {
this.cb(newValue);
}
}
数据劫持
class Vue {
constructor(opts) {
this.opts = opts;
this._data = this.opts.data;
this.observe(this._data);
this.compile();
}
observe(data) {
for (let key in data) {
let dep = new Dep();
let value = data[key];
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
// 收集订阅者
console.log("get...");
// 如果有实例,就将实例push到收集器里
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue) {
console.log("set...");
dep.notify(newValue);
value = newValue;
}
});
}
}
// 初次编译到dom里;
compile() {
let ele = document.querySelector(this.opts.el);
let childNodes = ele.childNodes;
this.compileNode(childNodes);
}
compileNode(childNodes) {
childNodes.forEach(node => {
if (node.nodeType === 3) {
// 获取文本的内容;
let textContent = node.textContent;
let reg = /\{\{\s*([^\{\}\s*]+)\s*\}\}/g;
if (reg.test(textContent)) {
let $1 = RegExp.$1;
node.textContent = node.textContent.replace(reg, this._data[$1]);
// 触发
new Watcher(this._data, $1, (newValue) => {
let oldValue = this._data[$1];
let updateReg = new RegExp(oldValue);
node.textContent = node.textContent.replace(updateReg, newValue);
});
}
} else if (node.nodeType === 1) {
let attrs = node.attributes;
Array.from(attrs).forEach(attr => {
let attrName = attr.name;
let attrValue = attr.value;
if (attrName.indexOf("v-") === 0) {
if (attrName === "v-model") {
// console.log("有v-model指令")
node.value = this._data[attrValue];
node.addEventListener("input", e => {
// 触发视图更新;
this._data[attrValue] = e.target.value;
});
}
}
});
// 判断节点内是否有子节点;
if (node.childNodes.length > 0) {
// 有子节点
this.compileNode(node.childNodes);
}
}
});
}
}
// 实现 v-html 指令;
// 收集器;
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify(newValue) {
this.subs.forEach(sub => {
sub.update(newValue);
});
}
}
// 订阅者;
class Watcher {
constructor(data, key, cb) {
Dep.target = this; // 通过Dep的静态属性target指向watcher实例,判断收集器是否与其关联
data[key];
this.cb = cb;
Dep.target = null;
}
update(newValue) {
// 触发回调
this.cb(newValue);
}
}
2 改进:proxy 劫持数据
defineProperty劫持属性有一些缺点,比如说:
- 如果对象里有数组,数组更改无法劫持
- 新增对象属性无法触发视图更新
proxy设置修改属性
不需要再进行对象循环,直接通过内置对象在这里插入代码片
方法中的target, key ,newValue参数获取修改属性
let obj = {
name:"张三",
age:20
}
let newObj = new Proxy(obj,{
get(target,key){
console.log("get...");
return target[key];
},
set(target,key,newValue){
console.log("set...",newValue);
target[key] = newValue;
}
})
newObj.name = "修改的";
改进之后的vue
class Vue {
constructor(opts) {
this.opts = opts;
this._data = this.opts.data;
this.observe(this._data);
this.compile();
}
observe(data) {
let dep = new Dep();
this._data = new Proxy(data, {
get(target, key) {
if (Dep.target) {
dep.addSub(Dep.target);
}
return target[key];
},
set(target, key, newValue) {
dep.notify(newValue);
target[key] = newValue;
}
});
}
// 初次编译到dom里;
compile() {
let ele = document.querySelector(this.opts.el);
let childNodes = ele.childNodes;
this.compileNode(childNodes);
}
compileNode(childNodes) {
childNodes.forEach(node => {
if (node.nodeType === 3) {
// 获取文本的内容;
let textContent = node.textContent;
let reg = /\{\{\s*([^\{\}\s*]+)\s*\}\}/g;
if (reg.test(textContent)) {
let $1 = RegExp.$1;
node.textContent = node.textContent.replace(reg, this._data[$1]);
// 绑定自定义事件;
new Watcher(this._data, $1, (newValue) => {
// console.log("update",newValue);
let oldValue = this._data[$1];
let updateReg = new RegExp(oldValue);
node.textContent = node.textContent.replace(updateReg, newValue);
});
}
} else if (node.nodeType === 1) {
// console.log("标签");
let attrs = node.attributes;
[...attrs].forEach(attr => {
let attrName = attr.name;
let attrValue = attr.value;
if (attrName.indexOf("v-") === 0) {
if (attrName === "v-model") {
// console.log("有v-model指令")
node.value = this._data[attrValue];
node.addEventListener("input", e => {
// 触发视图更新;
this._data[attrValue] = e.target.value;
});
}
}
});
// 判断节点内是否有子节点;
if (node.childNodes.length > 0) {
// 有子节点
this.compileNode(node.childNodes);
}
}
});
}
}
// 实现 v-html 指令;
// 收集器;
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify(newValue) {
this.subs.forEach(sub => {
sub.update(newValue);
});
}
}
// 订阅者;
class Watcher {
constructor(data, key, cb) {
Dep.target = this;
data[key];
this.cb = cb;
Dep.target = null;
}
update(newValue) {
this.cb(newValue);
}
}