• 【Vue3源码】2. 响应式原理 上 - reactive源码实现


    使用感受响应式

    在源码开始前,我们来尝试写个demo,使用一下 Reactive & effect

    DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Documenttitle>
    head>
    
    <body>
        <div id="app">div>
        <script type="module">
            // import { reactive, effect } from '/node_modules/@vue/reactivity/dist/reactivity.esm-browser.js'
            import { reactive, effect } from './reactivity.esm.js'
    
    		// reactive 创建一个响应式对象,即 proxy
            const state = reactive({ name: '小鱼', age: 18 })
            
            // effect 副作用函数,默认会执行一次,数据变化后会再次执行
            effect(() => {
                app.innerHTML = state.name + state.age;
            })
    
            setInterval(() => {
                state.age++;
            }, 1000);
        script>
    body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    在这里插入图片描述
    从图中可以看到,每过一秒 state.age 就加一,同时响应式展示出来。

    那么这种响应式是怎么做的呢?接下来我们一起看看Vue3的响应式原理和对应api,reactive & effect

    Vue3对比Vue2的变化

    • 在Vue2的时候使用 defineProperty 来进行数据的劫持, 需要对属性进行重写添加gettersetter 性能差
    • 当新增属性和删除属性时无法监控变化。需要通过$set$delete实现
    • 数组不采用 defineProperty 来进行劫持 (浪费性能,对所有索引进行劫持会造成性能浪费)需要对数组单独进行处理

    Vue3中使用 Proxy 来实现响应式数据变化。从而解决了上述问题。

    也就说,Vue3中使用了 Proxy (代理)来实现响应式,并解决了 Vue2 中响应式存在的问题。

    reactive 源码实现

    使用感受proxy

    下面我们来具体实现一下 reactive ,为了方便理解,我们继续从demo开始~

    let person = {
        name: '小鱼',
        get aliasName() {
            return '**' + this.name + '**'
        }
    }
    console.log(person.aliasName);   // **小鱼**
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这个 demo 很好理解,调用 person.aliasName 函数,返回格式为 **NAME** 的数据。我们继续引入 proxy 的概念

    let person = {
        name: '小鱼',
        get aliasName() {
            return '**' + this.name + '**'
        }
    }
    // 创建一个proxy
    const proxy = new Proxy(person, {
        /**
         * 当我取值时,调用该方法
         * @param target 去哪里取,指该对象
         * @param key 取什么属性
         * @param receiver 指的就是当前代理对象 proxy
         * @returns 对象上对应的属性
         */
        get(target, key, receiver) {
            console.log("调用get方法");
            return target[key]
        },
        /**
         * 当我赋值时,调用该方法
         * @param target 该对象
         * @param key 属性
         * @param value 要赋值的内容
         * @param receiver 当前代理对象 proxy
         * @returns true
         */
        set(target, key, value, receiver) {
            console.log("调用set方法");
            target[key] = value
            return true
        }
    })
    
    console.log(proxy.name);
    console.log(proxy.name = 'ddd');
    
    // 打印结果:
    // 调用get方法
    // 小鱼
    // 调用set方法
    // ddd
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    需要注意的是,我们是对 Proxy 进行取值、赋值操作,而不是对源对象 person 操作。

    这样,一个简单的响应式 Proxy 就写好了,但它有个bug

    let person = {
        name: '小鱼',
        get aliasName() {
            return '**' + this.name + '**'
        }
    }
    // 创建一个proxy
    const proxy = new Proxy(person, {
        get(target, key, receiver) {
            console.log(key);
            return target[key]
        },
        set(target, key, value, receiver) {
            target[key] = value
            return true
        }
    })
    
    console.log(proxy.aliasName);
    // 打印结果:
    // aliasName
    // **小鱼**
    
    // 正确的打印结果应该是:
    // aliasName
    // name
    // **小鱼**
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    诶?为什么说正确的打印结果应该是 aliasName、name、** 小鱼 ** 呢? 这个 name 是哪里来的呢?

    原来,在 get aliasName() 里面,有一个 this.name ,这个 this 指的是源对象 person, 这就出大问题了。

    如果我们在函数内做赋值操作 this.name = "大猪" ,我们的页面是不能做出响应式的,因为只有 proxy.name = "大猪" 才能收集依赖,现在的 this 指向并不是 proxy。

    那怎么办呢?我们要引入 Reflect 了

    let person = {
        name: '小鱼',
        get aliasName() {
            return '**' + this.name + '**'
        }
    }
    
    // 创建一个proxy
    const proxy = new Proxy(person, {
        get(target, key, receiver) {
            console.log("调用get方法");
            return Reflect.get(target, key, receiver)
        },
        set(target, key, value, receiver) {
            console.log("调用set方法");
            return Reflect.set(target, key, value, receiver)
        }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    引入Proxy、Reflect

    这样就完美啦!

    那么最后我们来把 reactive 源码写出来

    // reactive.ts文件
    import { isObject } from "@vue/shared";
    
    
    export function reactive(target) {
        // 非对象不处理
        if (!isObject(target)) {
            return target;
        }
    
    	// 将处理方法抽象出来
    	const mutableHandlers = {
    	    get(target, key, receiver) {
    	        return Reflect.get(target, key, receiver)
    	    },
    	    set(target, key, value, receiver) {
    	        return Reflect.set(target, key, value, receiver)
    	    },
    	}
    	
        // 代理,   通过代理对象操作属性,会去源对象上进行获取
        const proxy = new Proxy(target, mutableHandlers);
    
        return proxy
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    完善边界情况

    最后的最后,这段代码还不是那么的完善,我们来完善一下边界情况。看下面 demo

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    
    <body>
        <div id="app"></div>
        <script type="module">
            // import { reactive, effect } from '/node_modules/@vue/reactivity/dist/reactivity.esm-browser.js'
            import { reactive, effect } from './reactivity.esm.js'
    
            let state = reactive({ name: '小鱼', age: 18 })
            // reactive 创建一个响应式对象
            // effect 副作用函数,默认会执行一次,数据变化后会再次执行
    
    		// 1.同一个对象丢进去,返回的是同一个 proxy 还是新创建了一个 proxy?
            const p1 = reactive(state);
            const p2 = reactive(state);
           	console.log(p1 === p2);    // false
           	// 2.换了个指向,丢进去后返回的是同一个 proxy 还是新创建了一个 proxy?
            const p3 = reactive(p1);
            console.log(p1 === p3);    // false
        </script>
    </body>
    
    </html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    显然,以当前我们写的 reactive 函数来讲,这两种情况都是重新创建了一个 proxy。这不是我们希望的结果,我们希望函数能分辨出该源对象是否已经有 proxy 了,有的话就直接返回它的proxy,而不是重新创建。

    怎么做?第一种情况利用 weakMap 创建映射表解决,第二种情况利用 普通对象没有 get/set ,以此来区分是不是proxy解决。直接放源码。

    import { isObject } from "@vue/shared";
    
    export const enum ReactiveFlags {
        IS_REACTIVE = "__v_isReactive",
    }
    
    // 将处理方法抽象出来
    const mutableHandlers = {
        get(target, key, receiver) {
            if (ReactiveFlags.IS_REACTIVE == key) {
                return true;
            }
            return Reflect.get(target, key, receiver)
        },
        set(target, key, value, receiver) {
            return Reflect.set(target, key, value, receiver)
        },
    }
    
    const reactiveMap = new WeakMap();  // key只能是对象
    export function reactive(target) {
        // 非对象不处理
        if (!isObject(target)) {
            return target;
        }
        
    	// 此时如果有 get 会走 get函数
        if (target[ReactiveFlags.IS_REACTIVE]) {
            return target
        }
        
        // 已代理过的对象直接返回
        const existsProxy = reactiveMap.get(target);
        if (existsProxy) {
            return existsProxy;
        }
    
        // 代理,   通过代理对象操作属性,会去源对象上进行获取
        const proxy = new Proxy(target, mutableHandlers);
    
        reactiveMap.set(target, proxy);
        return proxy
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
  • 相关阅读:
    ROS2专题【01】:win10上安装ROS2
    【kafka】 | 03 | kafka、zk和cmak开机自启动
    (附源码)springboot嘉应房地产公司质量管理系统 毕业设计 453100
    根据最新上传的压缩包,解压写入数据库
    C++学习之多继承
    演讲比赛流程管理系统(看看你的星座会赢吗)
    vue3中使用cookie
    【数值分析】用幂法计算矩阵的主特征值和对应的特征向量(附matlab代码)
    下半年软考报名时间发布,你准备好了吗?
    Android之UI Automator框架源码分析(第九篇:UiDevice获取UiAutomation对象的过程分析)
  • 原文地址:https://blog.csdn.net/MinfCONS/article/details/127597199