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

news/2024/7/10 0:47:32 标签: vue, javascript, js

之前写了两篇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浏览器不支持
    本文的文字及图片来源于网络加上自己的想法,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理

http://www.niftyadmin.cn/n/1603446.html

相关文章

Java全家桶的这些知识,不用学了

众所周知,Java 的知识体系繁冗复杂,但是有很多知识在实际工作中几乎没有人用。 很多人在学习过程中,却经常把有限的时间和精力花在了这些“没有用”的知识上,事倍功半。 下面我捋一捋 Java 中那些不建议学习的知识点&#xff0c…

如何用vue-Element-ui实现左侧无限级菜单?本文详细教你

#最近项目中,要用到element-ui的无限级分类菜单,根据角色生成不同的递归数据,查阅了网上很多资料,发现很多都不太完整并且没有很多的延伸性。 ###梳理递归数据 我们一般拿到后台的数据是:1.扁平化数据格式 2.递归式数据格式 复制…

上线1小时访问破万,清华团队的Java大厂面试手册,带你起飞

现在的Java面试真就老八股文了。我光是整理题目就理了半天,答案背也背不完,在我快要放弃的时候看了这份马士兵教育老师整理的大厂面试题。不愧是大厂的高级别大佬,把Java面试题和所有知识点都讲得很通透。 先上这些内容给大家看一下。 目录…

Vue组件为什么data必须是一个函数呢?本文案例详解

前言 我们需要先复习下原型链的知识,其实这个问题取决于 js ,而并非是 vue 。 function Component(){this.data this.data } Component.prototype.data {name:jack,age:22, } 复制代码 首先我们达成一个共识(没有这个共识,请…

如何手 Vue的手势组件呢?本文教你

前言 最近需要使用手指捏合扩大的手势操作,找了几个组件,要么对 Vue 适配不好,要么量级太大,决定自己手写手势操作。 项目与效果预览 思路 直接在 DOM 上绑定 touchstart 、touchmove、touchend 不仅要绑定这几个事件&#xff…

别再写满屏的 get set 了,太 Low,试试 MapStruct 高级玩法

别再写满屏的 get-set 了,太 Low!MapStruct 高级玩法,这篇栈长带你上正道! 1、自定义映射 当我们映射 DTO 的时候,如果某些参数的值 MapStruct 的映射配置不能满足要求,可以使用自定义方法。 新增两个 DTO…

教你如何用24个ES6方法解决实际开发的JS问题?本文详解

本文主要介绍 24 中 es6 方法&#xff0c;这些方法都挺实用的&#xff0c;本本请记好&#xff0c;时不时翻出来看看。 1.如何隐藏所有指定的元素 const hide (el) > Array.from(el).forEach(e > (e.style.display none));// 事例:隐藏页面上所有<img>元素? hi…

为什么start方法才能启动线程,而run不行?

我们都知道&#xff0c;一个线程直接对应了一个Thread对象,在刚开始学习线程的时候我们也知道启动线程是通过start()方法,而并非run()方法。 那这是为什么呢&#xff1f; 如果你熟悉Thread的代码的话,你应该知道在这个类加载的时候会注册一些native方法 一看到native我就想起…