使用感受响应式
在源码开始前,我们来尝试写个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 来进行数据的劫持, 需要对属性进行重写添加
getter
及setter
性能差。 - 当新增属性和删除属性时无法监控变化。需要通过
$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
}