06-实现Vue数据劫持

1. 数据劫持

1-1. 术语解释

  访问或者修改对象的某个属性时,在访问和修改属性值时,除了执行基本的数据获取和修改操作以外,还基于数据的操作行为,以数据为基础去执行额外的操作;
  当前最经典的数据劫持应用就是数据渲染,各大前端框架的核心功能都是基于数据渲染来实现。

1-2. 通过vue实现数据劫持的例子

<div id="app">
    {{message}}
    <div v-html="htmlDate"></div>
    <input v-model="modelData" /> {{modelData}}
</div>
<script>
    let vm = new Vue({
        el: "#app",
        data: {
            message: "测试数据",
            htmlDate: "html数据",
            modelData: "双绑数据"
        }
    })
    setTimeout(() => {
        vm.message = "修改数据"
    }, 1000)
</script>

2. defineProperty实现数据劫持

  • 当数据不存在时,get会创建数据
let o = {};
Object.defineProperty(o, "message", {
    get() {
        console.log("get...");
        return "测试数据";
    },
    set(newValue) {
        console.log("set...", newValue);
    }
})
console.log(o);
>{}
  message: "测试数据"
 >get message: ƒ get()
 >set message: ƒ set(newValue)
 >__proto__: Object
>get...

2-1. configurable属性——是否允许对对象进行配置

// configurable: false
let o = {};
Object.defineProperty(o, "message", {
    configurable: false,
    get() {
        console.log("get...");
        return "测试数据";
    },
    set(newValue) {
        console.log("set...", newValue);
    }
})
delete o.message;
console.log(o);
>{}
  message: "测试数据"
 >get message: ƒ get()
 >set message: ƒ set(newValue)
 >__proto__: Object
>get...
// configurable: true
let o = {};
Object.defineProperty(o, "message", {
    configurable: true,
    get() {
        console.log("get...");
        return "测试数据";
    },
    set(newValue) {
        console.log("set...", newValue);
    }
})
delete o.message;
console.log(o);
>{}
 >__proto__: Object

2-2. enumerable属性——是否允许对对象进行枚举

// enumerable: false
let o = {};
Object.defineProperty(o, "message", {
    enumerable: false,
    get() {
        console.log("get...");
        return "测试数据";
    },
    set(newValue) {
        console.log("set...", newValue);
    }
})
for(let i in o){
    console.log(i)
}
>【无输出】
// enumerable: true
let o = {};
Object.defineProperty(o, "message", {
    enumerable: true,
    get() {
        console.log("get...");
        return "测试数据";
    },
    set(newValue) {
        console.log("set...", newValue);
    }
})
for(let i in o){
    console.log(i)
}
>message

3. 通过defineProperty实现vue数据劫持的过程

3-1. 数据渲染视图

<body>
    <div id="app">
        111{{message}}111
        <div>
            111
            <div>
                {{message}}
            </div>
        </div>
        <div v-html="htmlData"></div>
        <input v-model="modelData" /> {{modelData}}
    </div>
</body>

<script>
    let m = new myVue({
        el: "#app",
        data: {
            message: "测试数据",
            htmlData: "html数据",
            modelData: "绑定的数据"
        }
    })
</script>
class myVue {
    constructor(options) {
        this.$options = options;
        this.compile();
    }

    compile() {
        let el = document.querySelector(this.$options.el);
        this.compileNode(el);
    }

    compileNode(el) {
        let childNodes = el.childNodes;
        childNodes.forEach(node => {
            if (node.nodeType === 1) {
                // 便签
                let attrs = node.attributes;
                [...attrs].forEach(attr => {
                    let attrName = attr.name;
                    let attrValue = attr.value;
                    if (attrName.indexOf("v-" === 0)) {
                        attrName = attrName.substr(2);
                        if (attrName === "html") {
                            node.innerHTML = this.$options.data[attrValue];
                        } else if (attrName === "model") {
                            node.value = this.$options.data[attrValue];
                        }
                    }
                })
                if (node.childNodes.length > 0) {
                    this.compileNode(node);
                }
            } else if (node.nodeType === 3) {
                // 文本节点
                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]);
                }
            }
        });
    }
}

3-2. 数据劫持的实现

observe(data) {
    let keys = Object.keys(data);
    console.log(keys);
    keys.forEach(key => {
        this.defineReact(data, key, data[key]);
    })
}

defineReact(data, key, value) {
    Object.defineProperty(data, key, {
        configurable: true,
        enumerable: true,
        get() {
            console.log("get...");
            return value;
        },
        set(newValue) {
            console.log("set...", newValue);
            
        }
    })
}

3-3. 数据驱动视图更新

  • 事件绑定
this.addEventListener($1, e => {
	//重新渲染视图
	let oldValue = this.$options.data[$1];
	let reg = new RegExp(oldValue);
	node.textContent = node.textContent.replace(reg, e.detail);
})
  • 事件触发
let event = new CustomEvent(key, {
    detail: newValue
});
_this.dispatchEvent(event);

3-4. 双向绑定

  • 通过数据劫持监听数据data[key]
set(newValue) {
    console.log("set...", newValue);
    let event = new CustomEvent(key, {
        detail: newValue
    });
    _this.dispatchEvent(event);
    value = newValue;
}
  • 为每个key绑定其数据视图更新方法
this.addEventListener($1, e => {
	//重新渲染视图
	let oldValue = this.$options.data[$1];
	let reg = new RegExp(oldValue);
	node.textContent = node.textContent.replace(reg, e.detail);
})
  • 监听输入框的值并改变data[key]
if (attrName === "model") {
    node.value = this.$options.data[attrValue];
    node.addEventListener("input", e => {
        this.$options.data[attrValue] = e.target.value;
    })

  • 一旦输入框的值发生改变,就会改变data[key]中的值;defineProperty中的set()会监听到key对应的数据的变化;
    从而调用key所绑定的重新渲染视图的方法而改变视图中的数据;

3-5. 完整代码

<body>
    <div id="app">
        111{{message}}111
        <div>
            111
            <div>
                {{message}}
            </div>
        </div>
        <div v-html="htmlData"></div>
        <input v-model="modelData" /> {{modelData}}
    </div>
</body>

<script>
    let m = new myVue({
        el: "#app",
        data: {
            message: "测试数据",
            htmlData: "html数据",
            modelData: "绑定的数据"
        }
    })
    console.log(m.$options.data);
    setTimeout(() => {
        m.$options.data.message = "修改的数据";
    }, 1000)
</script>
class myVue extends EventTarget {
    constructor(options) {
        super();
        this.$options = options;
        this.compile();
        this.observe(this.$options.data)
    }

    observe(data) {
        let keys = Object.keys(data);
        keys.forEach(key => {
            this.defineReact(data, key, data[key]);
        })
    }

    defineReact(data, key, value) {
        let _this = this;
        Object.defineProperty(data, key, {
            configurable: true,
            enumerable: true,
            get() {
                console.log("get...", value);
                return value;
            },
            set(newValue) {
                console.log("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) {
                // 便签
                let attrs = node.attributes;
                [...attrs].forEach(attr => {
                    let attrName = attr.name;
                    let attrValue = attr.value;
                    if (attrName.indexOf("v-" === 0)) {
                        attrName = attrName.substr(2);
                        if (attrName === "html") {
                            node.innerHTML = this.$options.data[attrValue];
                        } else if (attrName === "model") {
                            node.value = this.$options.data[attrValue];
                            node.addEventListener("input", e => {
                                this.$options.data[attrValue] = e.target.value;
                            })
                        }
                    }
                })
                if (node.childNodes.length > 0) {
                    this.compileNode(node);
                }
            } else if (node.nodeType === 3) {
                // 文本节点
                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 => {
                        //重新渲染视图
                        console.log("渲染视图的key:",$1)
                        let oldValue = this.$options.data[$1];
                        let reg = new RegExp(oldValue);
                        node.textContent = node.textContent.replace(reg, e.detail);
                    })
                }
            }
        });
    }
}

4. Proxy实现数据劫持

let obj = {
    name: "张三",
    age: 20
}
let newObj = new Proxy(obj,{
    get(target,key){
        console.log("get...",target[key]);
        return target[key];
    },
    set(target,key,newValue){
        console.log("set...",target[key],"为",newValue);
        target[key] = newValue;
        return true;
    }
})

newObj.name;
>get... 张三

newObj.name = "李四";
>set... 张三 为 李四

4-1. 基于Proxy数据劫持对3.中代码的修改

  • 将3.中的observe()和defineReact()简化为observe() — 简化了循环操作
observe(data){
    this.$options.data = new Proxy(data,{
        get(target,key){
            return target[key];
        },
        set(target,key,newValue){
            target[key] = newValue;
            return true;
        }
    })
} 
  • 存在不能二次渲染视图的问题,因此需要绑定事件—在set()中添加绑定事件
set(target, key, newValue) {
    let event = new CustomEvent(key, {
        detail: newValue
    });
    _this.dispatchEvent(event);
    target[key] = newValue;
    return true;
}

4-2. 修改后的完整代码

class myVue extends EventTarget {
    constructor(options) {
        super();
        this.$options = options;
        this.compile();
        this.observe(this.$options.data)
    }

    observe(data) {
        let _this = this;
        this.$options.data = new Proxy(data, {
            get(target, key) {
                return target[key];
            },
            set(target, key, newValue) {
                let event = new CustomEvent(key, {
                    detail: newValue
                });
                _this.dispatchEvent(event);
                target[key] = newValue;
                return true;
            }
        })
    }

    compile() {
        let el = document.querySelector(this.$options.el);
        this.compileNode(el);
    }

    compileNode(el) {
        let childNodes = el.childNodes;
        childNodes.forEach(node => {
            if (node.nodeType === 1) {
                // 便签
                let attrs = node.attributes;
                [...attrs].forEach(attr => {
                    let attrName = attr.name;
                    let attrValue = attr.value;
                    if (attrName.indexOf("v-" === 0)) {
                        attrName = attrName.substr(2);
                        if (attrName === "html") {
                            node.innerHTML = this.$options.data[attrValue];
                        } else if (attrName === "model") {
                            node.value = this.$options.data[attrValue];
                            node.addEventListener("input", e => {
                                this.$options.data[attrValue] = e.target.value;
                            })
                        }
                    }
                })
                if (node.childNodes.length > 0) {
                    this.compileNode(node);
                }
            } else if (node.nodeType === 3) {
                // 文本节点
                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 => {
                        //重新渲染视图
                        console.log("渲染视图的key:", $1)
                        let oldValue = this.$options.data[$1];
                        let reg = new RegExp(oldValue);
                        node.textContent = node.textContent.replace(reg, e.detail);
                    })
                }
            }
        });
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue 实现数据劫持的方式是使用了 ES5 中的 Object.defineProperty() 方法,通过该方法给对象的属性添加 getter 和 setter 方法,从而实现数据的监测和控制。 具体实现步骤如下: 1. 创建一个 Observer 对象,用于对数据进行监听和劫持。 2. 在 Observer 中使用 Object.defineProperty() 方法,给数据对象的每个属性添加 getter 和 setter 方法。 3. 在 getter 方法中,收集依赖并返回属性值。 4. 在 setter 方法中,更新属性的值,并通知所有依赖进行更新。 下面是一个简单的实现示例: ```javascript function Observer(data) { this.data = data; this.walk(data); } Observer.prototype = { walk: function(data) { var self = this; Object.keys(data).forEach(function(key) { self.defineReactive(data, key, data[key]); }); }, defineReactive: function(data, key, val) { var dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { if (Dep.target) { dep.depend(); } return val; }, set: function(newVal) { if (newVal === val) { return; } val = newVal; dep.notify(); } }); } }; function Dep() { this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, depend: function() { if (Dep.target) { this.addSub(Dep.target); } }, notify: function() { this.subs.forEach(function(sub) { sub.update(); }); } }; Dep.target = null; ``` 在上述代码中,Observer 对象用于对数据对象进行监听和劫持,defineReactive() 方法使用 Object.defineProperty() 方法给数据对象的每个属性添加 getter 和 setter 方法,Dep 对象用于收集依赖和通知更新,而 Dep.target 用于存储当前的 Watcher 对象(用于收集依赖)。 这样,当数据对象的属性值发生变化时,就会自动触发 setter 方法,从而通知所有依赖进行更新。而当 Watcher 对象读取属性值时,就会触发 getter 方法,从而将 Watcher 对象收集到依赖列表中。这样,就实现数据劫持的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值