数据劫持
- 数据劫持,其实就是数据代理。
- 数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。
- 实现方法:
- defineProperty
- proxy
- Object.observe(已废弃,暂不研究)
- 大厂面试题!!!vue中的v-mode双向绑定是如何实现的?数据劫持
defineProperty实现数据劫持
let obj = {
myname:"张三"
}
console.log(obj); //张三
let value = obj['myname'];
let newObj = Object.defineProperty(obj,"myname",{
configurable:true, //配置可修改对象
enumerable:true, //配置枚举属性 可以循环键
get(){
console.log("触发get");
return value
},
set(newValue){
console.log("触发get",newValue);
// 错误的写法!!!
// obj['myname'] = newValue;
value = newValue;
}
})
console.log(obj); //李四
//需配置configurable:true
obj.myname = "李四";
console.log(obj); //李四
//循环键需配置enumerable:true
for(let key in newObj){
console.log(key); //myname
}
- defineProperty方法必须遍历对象的每个属性,使用 Object.defineProperty() 多数要配合 Object.keys() 和遍历:
Object.keys(obj).forEach(key => {
Object.defineProperty(obj, key, {
// ...
})
})
- 不能监听数组的变化
let arr = [1, 2, 3]
let obj = {}
Object.defineProperty(obj, 'arr', {
get() {
console.log('触发get',arr)
return arr
},
set(newVal) {
console.log('触发set', newVal)
arr = newVal
}
})
obj.arr.push(4) // 触发get [1,2,3]
obj.arr = [1, 2, 3, 4] // 触发set [1,2,3,4]
proxy实现数据劫持
- 在ES6中出现了Proxy。外界对某个对象的访问,都必须经过这层拦截。因此它是针对整个对象,而不是对象的某个属性,所以也就不需要对 keys 进行遍历。这解决了上述 Object.defineProperty() 需要遍历对象的每个属性的问题。
let obj = new Proxy({
name: "张三",
age: 20
}, {
get(target, name) {
console.log("触发了get",target[name]);
return target[name];
},
set(target, name, value) {
target[name] = value;
console.log("触发了set",target[name]);
}
})
console.log(obj.name); //触发了get 张三 张三
obj.name = "李四";
obj.age = 23;
console.log(obj); //触发了set 李四 触发了set 23
- 支持监听数组
let arr = [1, 2, 3]
let proxy = new Proxy(arr, {
get(target, key) {
console.log('get', key)
return Reflect.get(target, key)
},
set(target, key, value) {
console.log('set', key, value)
return Reflect.set(target, key, value)
}
})
proxy.push(4)
- 支持嵌套
let obj = {
info: {
name: 'eason',
blogs: ['webpack', 'babel', 'cache']
}
}
let handler = {
get (target, key, receiver) {
console.log('get', key)
// 递归创建并返回
if (typeof target[key] === 'object' && target[key] !== null) {
return new Proxy(target[key], handler)
}
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
console.log('set', key, value)
return Reflect.set(target, key, value, receiver)
}
}
let proxy = new Proxy(obj, handler)
proxy.info.name = 'Zoe'; // name: 'Zoe',
proxy.info.blogs.push('proxy');// blogs: ['webpack', 'babel', 'cache','proxy']
面试题
- 什么样的 a 可以满足 (a === 1 && a === 2 && a === 3) === true 呢?
- 思考:每次访问 a 返回的值都不一样,那么肯定会想到数据劫持。
let current = 0
Object.defineProperty(window, 'a', {
get () {
current++
return current
}
})
console.log(a === 1 && a === 2 && a === 3) //true
v-model双绑定实现原理
初次编译---->实例化watcher(Dep.target有值)---->触发get---->收集watcher---->设置数据 set---->触发dep的notify(watcher update方法) ---->逻辑处理返还到编译处理
class Kvue{
constructor(options){
this.$options = options;
this.$data = options.data;
this.observer(this.$data);
this.complier();
}
// 观察数据 劫持数据
observer(data){
Object.keys(data).forEach(key=>{
let value = data[key];
let _this = this;
let dep = new Dep();
Object.defineProperty(data,key,{
configurable:true,
enumerable:true,
get(){
// 收集依赖
// Dep.target 是 watch 对象
if(Dep.target){
dep.addSub(Dep.target);
}
return value;
},
set(newValue){
// 发布 触发每一个 对应的watcher的update方法
dep.notify(newValue);
value = newValue;
}
})
})
}
// 编译
complier(){
let ele = document.querySelector(this.$options.el);
// 获取子节点
this.complierChildnodes(ele);
}
complierChildnodes(ele){
let childNodes = ele.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;
// 文本节点替换data里的为数据
// node.textContent = node.textContent.replace(reg,this.$data[$1]);
// 实例化Watcher 及 触发 收集依赖
new Watcher(this.$data,$1,(newValue)=>{
let oldValue = this.$data[$1];
let reg = new RegExp(oldValue);
node.textContent = node.textContent.replace(reg,newValue);
});
node.textContent = node.textContent.replace(reg,this.$data[$1]);
}
}else if(node.nodeType === 1){
// 标签元素
// 获取属性
let attrs = node.attributes;
[...attrs].forEach(attr=>{
let attrName = attr.name;
let attrValue = attr.value;
if(attrName === "v-model"){
// 双绑定指令
node.value = this.$data[attrValue];
addEventListener("input",e=>{
// 已经被观察了
this.$data[attrValue] = e.target.value;
})
}else if(attrName === "v-html"){
// 初次编译
node.innerHTML = this.$data[attrValue];
//再次渲染
new Watcher(this.$data,attrValue,newValue=>{
node.innerHTML = newValue;
})
}else if(attrName === "v-text"){
node.textContent = this.$data[attrValue];
new Watcher(this.$data,attrValue,newValue=>{
node.innerHTML = newValue;
})
}
})
if(node.childNodes.length>0){
this.complierChildnodes(node);
}
}
})
}
}
// 依赖收集器
class Dep{
constructor(){
this.subs = [];
}
// 订阅
addSub(sub){
this.subs.push(sub);
}
// 发布
notify(newValue){
this.subs.forEach(sub=>{
sub.update(newValue);
})
}
}
// dep] --->[wather1,watcher2]-->notify-->(wather1,wather1):update方法
//订阅者
class Watcher{
constructor(data,key,cb){
Dep.target = this; //this 指向 new watch
data[key];
this.cb = cb;
Dep.target = null;
}
update(newValue){
this.cb(newValue);
}
}