【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>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'

		// reactive 创建一个响应式对象,即 proxy
        const state = reactive({ name: '小鱼', age: 18 })
        
        // effect 副作用函数,默认会执行一次,数据变化后会再次执行
        effect(() => {
            app.innerHTML = state.name + state.age;
        })

        setInterval(() => {
            state.age++;
        }, 1000);
    </script>
</body>

</html>

在这里插入图片描述
从图中可以看到,每过一秒 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);   // **小鱼**

这个 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

需要注意的是,我们是对 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
// **小鱼**

诶?为什么说正确的打印结果应该是 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)
    }
})

引入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
}

完善边界情况

最后的最后,这段代码还不是那么的完善,我们来完善一下边界情况。看下面 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>

显然,以当前我们写的 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
}
好的,我会尽力回答你的问题。首先,我们需要了解Vue3的响应式系统是如何工作的。Vue3使用了一个名为`Reactive`的函数来实现响应式。 `Reactive`函数的作用是将一个普通的JavaScript对象转换成响应式的对象。当响应式对象的属性被修改时,所有依赖该属性的地方都会自动更新。 下面是`Reactive`函数的实现: ```javascript function Reactive(obj) { const handlers = { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver); track(target, prop); return isObject(value) ? Reactive(value) : value; }, set(target, prop, value, receiver) { const oldValue = Reflect.get(target, prop, receiver); let result = true; if (oldValue !== value) { result = Reflect.set(target, prop, value, receiver); trigger(target, prop); } return result; }, deleteProperty(target, prop) { const result = Reflect.deleteProperty(target, prop); trigger(target, prop); return result; } }; return new Proxy(obj, handlers); } ``` `Reactive`函数接受一个普通的JavaScript对象作为参数,返回一个响应式的对象。在实现,我们使用了ES6的Proxy对象来实现响应式。 在`get`处理器,我们使用了`track`函数来收集依赖。`track`函数的作用是将当前正在执行的计算函数添加到依赖列表。 在`set`处理器,我们首先获取旧值,然后判断新值是否与旧值相同。如果不同,我们使用`trigger`函数来触发更新。`trigger`函数的作用是遍历依赖列表,执行所有计算函数。 在`deleteProperty`处理器,我们使用`trigger`函数来触发更新,因为删除属性也可能导致依赖更新。 在以上代码,我们还使用了`isObject`函数来判断一个值是否为对象。该函数的实现如下: ```javascript function isObject(value) { return typeof value === 'object' && value !== null; } ``` 这个函数非常简单,它只是判断一个值是否为对象。如果是对象,我们就递归调用`Reactive`函数来将该对象转换成响应式。 总之,这就是Vue3的响应式系统的实现原理。通过`Reactive`函数和Proxy对象,我们可以将一个普通的JavaScript对象转换成响应式的对象,并实现自动更新。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值