index.html
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title></head><body><div id="app"><h3>姓名是:{{name}}</h3><h3>姓名是:{{age}}</h3><h3>info的值是:{{info.a}}</h3><div>name的值:<input type="text" v-model="name"></div><div>info.a的值:<input type="text" v-model="info.a"></div></div><script src="./vue.js"></script><script>const vm = new Vue({el: "#app",data: {name: "zs",age: 20,info: {a: "1",c: "2"}}})console.log(vm);</script>
</body></html>
vue.js
class Vue {constructor(options) {this.$data = options.data// 调用数据劫持的方法observe(this.$data)// 属性代理// 通过属性代理,直接输入vm就能输出vm.$data的内容Object.keys(this.$data).forEach(key => {Object.defineProperty(this, key, {enumerable: true,configurable: true,get() {return this.$data[key]},set(newVal) {this.$data[key] = newVal}})})// 调用模板编译的方法compile(options.el, this)}
}// 数据劫持的方法
const observe = obj => {// 递归终止条件if (!obj || Object.prototype.toString.call(obj) !== "[object Object]") returnconst dep = new Dep()Object.keys(obj).forEach(key => {let val = obj[key]// 递归保证对象内部属性被劫持observe(val)// 需要为key所对应的属性添加访问器属性Object.defineProperty(obj, key, {enumerable: true,configurable: true,get() {// new watcher的实例就被放在dep.subs这个数组中Dep.target && dep.addSubs(Dep.target)return val},set(newVal) {val = newVal// 重新赋值之后需要再次劫持observe(val)// 通知每一个订阅者更新自己的文本dep.notify()}})})22
}// 模板编译的方法
const compile = (el, vm) => {// 获取el对应的DOM元素vm.$el = document.querySelector(el);// 创建文档碎片,提高DOM操作的性能const fragment = document.createDocumentFragment()let node// 把所有子节点放入文档碎片中while (node = vm.$el.firstChild) {fragment.appendChild(node)}// 定义负责对DOM模板进行编译的方法const replace = node => {// 定义匹配插值表达式的正则const regMustache = /\{\{\s*(\S+)\s*\}\}/// 如果当前的node节点是一个文本子节点,需要进行正则替换if (node.nodeType === 3) {const content = node.textContent;// 匹配{{}}const execResult = regMustache.exec(content);if (execResult) {// 获得最新值const newVal = execResult[1].split(".").reduce((newObj, key) => newObj[key], vm)// 替换更新node.textContent = content.replace(regMustache, newVal)// 在这里,创建watcher的实例,// 需要(回调cb记录更新自己,最新的数据,对应的数据的key)const w = new Watcher(newVal => node.textContent = content.replace(regMustache, newVal),vm,execResult[1])}// 终止递归条件return}// 如果是dom节点,并且是输入框if (node.nodeType === 1 && node.tagName.toUpperCase() === "INPUT") {// 获取节点的所有属性const attrs = Array.from(node.attributes)// 判断是否存在v-model属性const findResult = attrs.find(attr => attr.name === 'v-model')// 获取到v-model的值,并更新到页面中if (findResult) {//获取到当前v-model的属性值 name info.aconst expStr = findResult.valueconst newVal = expStr.split(".").reduce((newObj, key) => newObj[key], vm)// 更新输入框node.value = newVal// 在这里,创建watcher的实例new Watcher(newValue => node.value = newValue, vm, expStr)// 监听文本框的input输入事件,拿到文本框最新的值,把最新的值,更新到vm上即可node.addEventListener('input', e => {const keyArr = expStr.split('.')// 使用 slice() 截取数组(这里截取除了最后一个)// 即 拿到前面一个对象// 给info.a赋值,需要拿到info // 给name赋值,需要拿到vmconst obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)console.log(obj);// keyArr[keyArr.length - 1] => 取数组["info","a"]最后一个值 "a" // 即 使用 info.a 进行修改obj[keyArr[keyArr.length - 1]] = e.target.value})}}// 否则,证明不是文本节点,可能是一个DOM元素,需要进行递归处理node.childNodes.forEach(child => replace(child))}// 模板编译replace(fragment)// 重新渲染页面vm.$el.appendChild(fragment)}// 订阅者的类
class Watcher {// cb回调函数中,记录着当前Watcher如何更新自己的文本内容// 但是,只知道如何更新自己还不行,还必须拿到最新的数据,// 因此,还需要在new Watcher 期间,把Vm也传递进来(因为Vm中保存着最新的数据)// 除此之外,还需要知道,在Vm身上众多的数据中,哪个数据,才是当前自己所需要的数据,// 因此,必须在new Watcher期间,指定watcher 对应的数据的名字keyconstructor(cb, vm, key) {this.cb = cbthis.vm = vmthis.key = key// 下面三行负责把创建的Watcher 实例存到Dep实例的subs数组中Dep.target = this //target为自定义属性key.split('.').reduce((newObj, k) => newObj[k], vm)Dep.target = null}// 触发回调函数的方法 发布者通知我们更新update() {const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)this.cb(value)}}
// 依赖收集的类/收集Watcher订阅者的类
class Dep {constructor() {//设置存放订阅者信息的数组this.subs = []}//向subs数组中添加订阅者的信息addSubs(watcher) {this.subs.push(watcher)}//发布通知的方法notify() {this.subs.forEach(watcher => watcher.update())}
}
vue3为什么要用Proxy替代Object.defineProperty
- 在 Vue 中,Object.defineProperty 无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。
- Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。
而要取代它的Proxy有以下两个优点;
-
可以劫持整个对象,并返回一个新对象
-
有13种劫持操作