如何实现vue3.0的响应式呢?本文实战教你

风流意气都作罢 提交于 2020-08-09 11:16:06

之前写了两篇vue2.0的响应式原理,链接在此,对响应式原理不清楚的请先看下面两篇

和尤雨溪一起进阶vue

和尤雨溪一起进阶vue(二)

现在来写一个简单的3.0的版本吧

大家都知道,2.0的响应式用的是Object.defineProperty,结合发布订阅模式实现的,3.0已经用Proxy改写了

Proxy是es6提供的新语法,Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。

语法:

const p = new Proxy(target, handler)

target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

handler的方法有很多, 感兴趣的可以移步到MDN,这里重点介绍下面几个

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无法拦截数组的pushunshift,shiftpop,slice,splice等方法(2.0内部重写了这些方法,实现了拦截), proxy可以拦截拦截对象的新增属性,Object.defineProperty不可以(开发者需要手动调用$set)
  • 兼容性 : Object.defineProperty支持ie8+,Proxy的兼容性差,ie浏览器不支持
    本文的文字及图片来源于网络加上自己的想法,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!