和尤雨溪一块儿进阶vuejavascript
如今来写一个简单的3.0的版本吧vue
你们都知道,2.0的响应式用的是Object.defineProperty
,结合发布订阅模式实现的,3.0已经用Proxy
改写了java
Proxy是es6提供的新语法,Proxy 对象用于定义基本操做的自定义行为(如属性查找、赋值、枚举、函数调用等)。react
语法:es6
const p = new Proxy(target, handler)
数组
target 要使用 Proxy 包装的目标对象(能够是任何类型的对象,包括原生数组,函数,甚至另外一个代理)。
handler 一个一般以函数做为属性的对象,各属性中的函数分别定义了在执行各类操做时代理 p 的行为。浏览器
handler的方法有不少, 感兴趣的能够移步到MDN,这里重点介绍下面几个bash
handler.has()
in 操做符的捕捉器。 handler.get() 属性读取操做的捕捉器。 handler.set() 属性设置操做的捕捉器。 handler.deleteProperty() delete 操做符的捕捉器。 handler.ownKeys() Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。 复制代码
基于上面的知识,咱们来拦截一个对象属性的取值,赋值和删除网络
// version1 const handler = { get(target, key, receiver) { console.log('get', key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log('set', key, value) let res = Reflect.set(target, key, value, receiver) return res }, deleteProperty(target, key) { console.log('deleteProperty', key) Reflect.deleteProperty(target, key) } } // 测试部分 let obj = { name: 'hello', info: { age: 20 } } const proxy = new Proxy(obj, handler) // get name hello // hello console.log(proxy.name) // set name world proxy.name = 'world' // deleteProperty name delete proxy.name 我是08年出道的前端老鸟,想交流经验能够进个人扣扣裙 519293536 有问题我都会尽力帮你们
上面已经能够拦截到对象属性的取值,赋值和删除了,咱们来看看新增一个属性能否拦截
proxy.height = 20
// 打印 set height 20 复制代码
成功拦截!! 咱们知道vue2.0新增data上不存在的属性是不能够响应的,须要手动调用$set
的,这就是Proxy
的优势之一
如今来看看嵌套对象的拦截,咱们修改info属性的age属性
proxy.info.age = 30 // 打印 get info 复制代码
只能够拦截到info,不能够拦截到info的age属性,因此咱们要递归了,问题是在哪里递归呢?
由于调用proxy.info.age会先触发proxy.info的拦截,因此咱们能够在get中拦截,若是proxy.info是对象的话,对象须要再被代理一次,咱们把代码封装一下,写成递归的形式
function reactive(target) { return createReactiveObject(target) } function createReactiveObject(target) { // 递归结束条件 if(!isObject(target)) return target const handler = { get(target, key, receiver) { console.log('get', key) let res = Reflect.get(target, key, receiver) // res若是是对象,那么须要继续代理 return isObject(res) ? createReactiveObject(res): res }, set(target, key, value, receiver) { console.log('set', key, value) let res = Reflect.set(target, key, value, receiver) return res }, deleteProperty(target, key) { console.log('deleteProperty', key) Reflect.deleteProperty(target, key) } } return new Proxy(target, handler) } function isObject(obj) { return obj != null && typeof obj === 'object' } // 测试部分 let obj = { name: 'hello', info: { age: 20 } } const proxy = reactive(obj) proxy.info.age = 30 复制代码
运行上面的代码,打印结果
get info
set age 30 复制代码
Bingo! 嵌套对象拦截到了
vue2.0用的是Object.defineProperty拦截对象的getter和setter,一次将对象递归到底, 3.0用Proxy,是惰性递归的,只有访问到某个属性,肯定了值是对象,咱们才继续代理下去这个属性值,所以性能更好
如今咱们来测试数组的方法,看看可否拦截到,以push方法为例, 测试部分代码以下
let arr = [1, 2, 3] const proxy = reactive(arr) proxy.push(4) 复制代码
打印结果
get push
get length
set 3 4 set length 4 复制代码
和预期有点不太同样,调用数组的push方法,不只拦截到了push, 还拦截到了length属性,set被调用了两次,在set中咱们是要更新视图的,咱们作了一次push操做,却触发了两次更新,显然是不合理的,因此咱们这里须要修改咱们的handler的set函数,区分一下是新增属性仍是修改属性,只有这两种状况才须要更新视图
set函数修改以下
set(target, key, value, receiver) {
console.log('set', key, value) let oldValue = target[key] let res = Reflect.set(target, key, value, receiver) let hadKey = target.hasOwnProperty(key) if(!hadKey) { // console.log('新增属性', key) // 更新视图 }else if(oldValue !== value) { // console.log('修改属性', key) // 更新视图 } return res } 复制代码
至此,咱们对象操做的拦截咱们基本已经完成了,可是还有一个小问题, 咱们来看看下面的操做
let obj = { some: 'hell' } let proxy = reactive(obj) let proxy1 = reactive(obj) let proxy2 = reactive(obj) let proxy3 = reactive(obj) let p1 = reactive(proxy) let p2 = reactive(proxy) let p3 = reactive(proxy) 复制代码
咱们这样写,就会一直调用reactive代理对象,因此咱们须要构造两个hash表来存储代理结果,避免重复代理
function reactive(target) { return createReactiveObject(target) } let toProxyMap = new WeakMap() let toRawMap = new WeakMap() function createReactiveObject(target) { let dep = new Dep() if(!isObject(target)) return target // reactive(obj) // reactive(obj) // reactive(obj) // target已经代理过了,直接返回,不须要再代理了 if(toProxyMap.has(target)) return toProxyMap.get(target) // 防止代理对象再被代理 // reactive(proxy) // reactive(proxy) // reactive(proxy) if(toRawMap.has(target)) return target const handler = { get(target, key, receiver) { let res = Reflect.get(target, key, receiver) // 递归代理 return isObject(res) ? reactive(res) : res }, // 必需要有返回值,不然数组的push等方法报错 set(target, key, val, receiver) { let hadKey = hasOwn(target, key) let oldVal = target[key] let res = Reflect.set(target, key, val,receiver) if(!hadKey) { // console.log('新增属性', key) } else if(oldVal !== val) { // console.log('修改属性', key) } return res }, deleteProperty(target, key) { Reflect.deleteProperty(target, key) } } let observed = new Proxy(target, handler) toProxyMap.set(target, observed) toRawMap.set(observed, target) return observed } function isObject(obj) { return obj != null && typeof obj === 'object' } function hasOwn(obj, key) { return obj.hasOwnProperty(key) } 复制代码
接下来就是修改数据,触发视图更新,也就是实现发布订阅,这一部分和2.0的实现部分同样,也是在get中收集依赖,在set中触发依赖
完整代码以下
class Dep { constructor() { this.subscribers = new Set(); // 保证依赖不重复添加 } // 追加订阅者 depend() { if(activeUpdate) { // activeUpdate注册为订阅者 this.subscribers.add(activeUpdate) } } // 运行全部的订阅者更新方法 notify() { this.subscribers.forEach(sub => { sub(); }) } } let activeUpdate function reactive(target) { return createReactiveObject(target) } let toProxyMap = new WeakMap() let toRawMap = new WeakMap() function createReactiveObject(target) { let dep = new Dep() if(!isObject(target)) return target // reactive(obj) // reactive(obj) // reactive(obj) // target已经代理过了,直接返回,不须要再代理了 if(toProxyMap.has(target)) return toProxyMap.get(target) // 防止代理对象再被代理 // reactive(proxy) // reactive(proxy) // reactive(proxy) if(toRawMap.has(target)) return target const handler = { get(target, key, receiver) { let res = Reflect.get(target, key, receiver) // 收集依赖 if(activeUpdate) { dep.depend() } // 递归代理 return isObject(res) ? reactive(res) : res }, // 必需要有返回值,不然数组的push等方法报错 set(target, key, val, receiver) { let hadKey = hasOwn(target, key) let oldVal = target[key] let res = Reflect.set(target, key, val,receiver) if(!hadKey) { // console.log('新增属性', key) dep.notify() } else if(oldVal !== val) { // console.log('修改属性', key) dep.notify() } return res }, deleteProperty(target, key) { Reflect.deleteProperty(target, key) } } let observed = new Proxy(target, handler) toProxyMap.set(target, observed) toRawMap.set(observed, target) return observed } function isObject(obj) { return obj != null && typeof obj === 'object' } function hasOwn(obj, key) { return obj.hasOwnProperty(key) } function autoRun(update) { function wrapperUpdate() { activeUpdate = wrapperUpdate update() // wrapperUpdate, 闭包 activeUpdate = null; } wrapperUpdate(); } let obj = {name: 'hello', arr: [1, 2,3]} let proxy = reactive(obj) // 响应式 autoRun(() => { console.log(proxy.name) })
我是08年出道的前端老鸟,想交流经验能够进个人扣扣裙 519293536 有问题我都会尽力帮你们
proxy.name = 'xxx' // 修改proxy.name, 自动执行autoRun的回调函数,打印新值 复制代码
最后总结下vue2.0和3.0响应式的实现的优缺点:
- 性能 : 2.0用
Object.defineProperty
拦截对象的属性的修改,在getter中收集依赖,在setter中触发依赖更新,一次将对象递归到底拦截,性能较差, 3.0用Proxy
拦截对象,惰性递归,性能好 Proxy
能够拦截数组的方法,Object.defineProperty
没法拦截数组的push
,unshift
,shift
,pop
,slice
,splice
等方法(2.0内部重写了这些方法,实现了拦截), proxy能够拦截拦截对象的新增属性,Object.defineProperty
不能够(开发者须要手动调用$set
)- 兼容性 :
Object.defineProperty
支持ie8+,Proxy
的兼容性差,ie浏览器不支持本文的文字及图片来源于网络加上本身的想法,仅供学习、交流使用,不具备任何商业用途,版权归原做者全部,若有问题请及时联系咱们以做处理