这是本专栏的第一篇文章,我们就从reactive这个API来打开vue3源码大门吧。众所周知,vue2定义响应式变量的方式一般是在data函数中定义。而在vue3中定义响应式变量一般是通过reactive或者ref(后面会讲到)来定义的。而reactive本质就是基于es6的新增APIProxy来实现数据响应式的。
一、Proxy
在介绍reactive之前我们先介绍一下Proxy这个API。Proxy可以代理一个对象,当我们改变访问代理对象时会触发get方法,改变代理对象的值的时候,会触发set方法。具体请看我的另一篇文章:[es6新增API Proxy]
二、reactive
import { mutableHandlers } from "./baseHandler";
export function reactive(target) {
if(!isObject(target)) return; // reactive的参数必须要是一个对象
// mutableHandlers: 进行一系列捕捉器操作
const proxy = new Proxy(target, mutableHandlers); // 代理传入的对象
return proxy;
}
// baseHandler.ts文件
export const mutableHandlers = {
get(target, key, receiver) {
let res = Reflect.get(target, key, receiver); // 重点:这里用Reflect是为了让取值时内部指向代理对象proxy,如果不用就指向源对象target
if(isObject(res)) {
return reactive(res); // 深度代理实现, 性能好, 只有取值触发get方法,且值是对象类型才递归,初始化不会递归
}
return res
},
set(target, key, value, receiver) {
let oldValue = target[key];
let result = Reflect.set(target, key, value, receiver);
return result;
}
}
以上是简单的reactive创建一个代理对象,但该对象是如何成为响应式的(数据更新触发视图更新)呢?请看如下代码:
import { track, trigger } from './effect';
export const mutableHandlers = {
get(target, key, receiver) {
+ track(target, 'get', key); // 做依赖收集的方法:当访问响应式对象时,会触发get方法,进行依赖收集
let res = Reflect.get(target, key, receiver); // 重点:这里用Reflect是为了让取值时内部指向代理对象proxy,如果不用就指向源对象target
if(isObject(res)) {
return reactive(res); // 深度代理实现, 性能好, 只有取值触发get方法,且值是对象类型才递归,初始化不会递归
}
return res
},
set(target, key, value, receiver) {
let result = Reflect.set(target, key, value, receiver);
// 给响应式对象某个属性赋值,会触发更新,即运行之前track时收集的方法
+ trigger(target, 'set', key, value, oldValue);
return result;
}
}
effect
以上新增了两个方法:track, trigger,一个是进行依赖收集,一个是触发更新。说到这两个方法就不得不说Vue的另一个API:effect了。vue2源码是基于三种watcher来实现响应式的,而vue3是基于effect来实现的(接下来要讲解的computed,watch,ref等都会依赖此模块)。在vue3中template模板其实就是一个个嵌套的effect函数。具体如下:
export let activeEffect = undefined; // 表示当前正在运行的effect,依赖收集时收集的就是这个
export function effect(fn, options:any = {}) {
// 这里fn可以根据状态变化,重新执行, effect可以嵌套使用
const _effect = new ReactiveEffect(fn, options.scheduler); // 创建响应式effect
_effect.run();
const runner = _effect.run.bind(_effect); // 绑定this执行
runner.effect = _effect; // 将effe绑定到runner函数上
return runner;
}
export class ReactiveEffect{
public parent = null; // 用来记录正在执行的effect的父级effect(effect可this.fn()以嵌套使用)
public active = true; // 表示这个effect默认为激活状态,ts这么写表示this.active = true
public deps = []; // 用于effect收集属性
constructor(public fn, public scheduler) { // 用户传递的参数会挂载到this上this.fn = fn
this.fn = fn;
this.scheduler = scheduler;
}
run() { // 执行effect
if(!this.active) return this.fn(); // 这里表示如果是非激活的情况,只需要执行函数,不需要进行依赖收集(收集当前属性和effect的映射关系)
// 这里做依赖收集(核心就是将当前的effect和稍后渲染的属性关联在一起)
try{
this.parent = activeEffect
activeEffect = this; // 重点: 把当前正在执行的effect赋值给全局变量activeEffect
return this.fn(); // 当稍后调用取值操作的时候,就可以获取到这个全局的activeEffect了
}finally{
activeEffect = this.parent; // effect执行完就为undefined
this.parent = null;
}
}
}
如上代码,在effect函数接受一个函数,并且通过ReactiveEffect类创建_effect实例。ReactiveEffect类的原型上定义了一个run方法,在该方法中会将正在执行的effect赋值给全局变量activeEffect,并且运行effect中传入的fn方法,如下测试代码:
<!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 src="./reactivity.global.js"></script>
<script>
const { reactive, effect } = VueReactivity
const personInfo = reactive({
name: '张三',
age: 18,
})
effect(() => {
document.getElementById('app').innerHTML = `名字:${personInfo.name} 年龄:${personInfo.age}`
})
setTimeout(() => {
personInfo.name = '李四'
}, 2000)
</script>
</body>
</html>
如上代码,当运行effect函数时,会通过ReactiveEffect创建一个_effect实例,并运行_effect.run方法,在该方法中会将正在执行的effect赋值给全局变量activeEffect,并且运行effect中传入的fn方法,fn方法中访问了响应式对象personInfo的name和age属性,这就会触发get方法从而运行track方法进行依赖收集,要收集的effect就是这时的全局变量activeEffect。两秒后,运行personInfo.name = '李四',会触发set方法,从而运行trigger方法进行视图更新。接下来我们再来讲一下track、trigger这两个方法,看看他是怎么进行依赖收集和视图更新的。
// 重点: 这是收集需要双向收集,不仅仅是属性收集effect, effect也要收集属性, 这样可以清理,
// 当effect删除了之后,对应的属性收集了这个属性也要删除
const targetMap = new WeakMap()
// 依赖收集方法
export function track(target, type, key) {
if(!activeEffect) return; // 正在执行的effect必须不为空才收集依赖
let depsMap = targetMap.get(target);
if(!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if(!dep) {
depsMap.set(key, (dep = new Set()));
}
trackEffects(dep)
// debugger
}
export function trackEffects(dep) {
if(activeEffect) {
let shouldTrack = !dep.has(activeEffect);
if(shouldTrack) {
dep.add(activeEffect); // 收集对应的依赖
}
}
}
// 设置完值,触发更新
export function trigger(target, type, key, value, oldValue) {
const depsMap = targetMap.get(target);
if(!depsMap) return;
let effects = depsMap.get(key);
if(effects) {
triggerEffects(effects)
}
}
export function triggerEffects(effects) {
effects = new Set(effects); // 让循环的effects和上一个effects使用不同的引用,防止循环引用造成死循环
effects.forEach(effect => {
// 注意: 这里要避免run触发trigger,要不会造成死循环
if(effect !== activeEffect) {
if(effect.scheduler) {
effect.scheduler(); // 如果用户传入了调度函数,则用用户的
}else {
effect.run(); // 否则默认刷新视图
}
}
})
}
vue3在进行依赖收集时,会全局构建依赖图。由于reactive必须接受一个对象,所以其先new WeakMap创建一个以传入的对象作为key的WekMap对象,之后再用new map创建一个以传入对象的属性作为key的对象,其值为Set集合,该集合就是存储activeEffect的。trigger就是根据对象及其对应属性找到对应集合进行执行更新。如下是具体全局收集依赖格式:
总结
vue3响应式数据运行原理步骤为:
- 我们先创建了一个响应式对象 new Proxy
- effect 默认数据变化要能更新,我们得先将正在执行的effect作为全局变量,渲染(取值),我们在get方法中进行依赖收集
- weakMap(对象: map(属性: set(effect)))
- 稍后用户发生数据变化,会通过对象属性来查找对应的effect集合,找到effect全部执行