手把手教你实现vue3.0的响应式

之前写了两篇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  
复制代码

上面已经可以拦截到对象属性的取值,赋值和删除了,我们来看看新增一个属性可否拦截

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)
})
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浏览器不支持
我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章