vue3响应式简而言之就是:
1.effect中的所有属性,都会收集 effect。
2.当这个属性值发生变化,会重新执行 effect。
下面通过手写简易vue3响应式,深入理解吧。
1.reactiveApi 实现
package/reactivity/src/index导出响应式的各种方法
export {
reactive,
shallowReactive,
shallowReadonly,
readonly
} from './reactive'
export {
effect
} from './effect'
export {
ref,
shallowRef,
toRef,
toRefs
} from './ref'
package/reactivity/src/index/reactivity函数柯里化
Vue3中采用
proxy实现数据代理, 核心就是拦截get方法和set方法,当获取值时收集effect函数,当修改值时触发对应的effect重新执行
import { isObject } from "@vue/shared"
import {
mutableHandlers,
shallowReactiveHandlers,
readonlyHandlers,
shallowReadonlyHandlers
} from './baseHandlers'
export function reactive(target){
return createReactiveObject(target,false,mutableHandlers)
}
export function shallowReactive(target){
return createReactiveObject(target,false,shallowReactiveHandlers)
}
export function readonly(target){
return createReactiveObject(target,true,readonlyHandlers)
}
export function shallowReadonly(target){
return createReactiveObject(target,true,shallowReadonlyHandlers)
}
const reactiveMap = new WeakMap();
const readonlyMap = new WeakMap();
//核心方法 createReactiveObject
//reactive 这个 api 只能拦截对象类型
//WeakMap将要代理的对象 和对应代理结果缓存起来,如果已经被代理了,直接返回即可。
//reactiveMap/readonlyMap 对应响应/只读映射表
export function createReactiveObject(target,isReadonly,baseHandlers){
if( !isObject(target)){
return target;
}
const proxyMap = isReadonly? readonlyMap:reactiveMap
const existProxy = proxyMap.get(target);
if(existProxy){
return existProxy;
}
const proxy = new Proxy(target,baseHandlers);
proxyMap.set(target,proxy);
return proxy;
}
package/reactivity/src/index/baseHandlers 实现
reactive、shallowReactive、readonly、shallowReadonly对应的handler:set和get->createGetter/createSetter
get收集依赖
set触发更新,区分是新增、修改。
// 实现 new Proxy(target, handler)
import { extend, hasChanged, hasOwn, isArray, isIntegerKey, isObject } from "@vue/shared";
import { track, trigger } from "./effect";
import { TrackOpTypes, TriggerOrTypes } from "./operators";
import { reactive, readonly } from "./reactive";
const get = createGetter();
const shallowGet = createGetter(false, true);
const readonlyGet = createGetter(true);
const shallowReadonlyGet = createGetter(true, true);
const set = createSetter();
const shallowSet = createSetter(true);
export const mutableHandlers = {
get,
set
}
export const shallowReactiveHandlers = {
get: shallowGet,
set: shallowSet
}
let readonlyObj = {
set: (target, key) => {
console.warn(`set on key ${key} falied`)
}
}
export const readonlyHandlers = extend({
get: readonlyGet,
}, readonlyObj)
export const shallowReadonlyHandlers = extend({
get: shallowReadonlyGet,
}, readonlyObj)
function createGetter(isReadonly = false, shallow = false) { // 拦截获取功能
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver); // 等价于target[key];
if(!isReadonly){
// 收集依赖,等会数据变化后更新对应的视图
track(target,TrackOpTypes.GET,key)
}
if(shallow){
return res;
}
if(isObject(res)){
// vue2 是一上来就递归,vue3 是当取值时会进行代理 。 vue3的代理模式是懒代理
return isReadonly ? readonly(res) : reactive(res)
}
return res;
}
}
function createSetter(shallow = false) { // 拦截设置功能
return function set(target, key, value, receiver) {
const oldValue = target[key]; // 获取老的值
//target中是否有属性key
let hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target,key);
//修改的是数组,key是索引,索引在数组长度内
//修改的是对象,看一下当前target中有没有这个属性key
const result = Reflect.set(target, key, value, receiver); //等价于 target[key] = value
if(!hadKey){
// 新增
trigger(target,TriggerOrTypes.ADD,key,value);
}else if(hasChanged(oldValue,value)){
// 修改 并老值!==新值
trigger(target,TriggerOrTypes.SET,key,value,oldValue)
}
// 当数据更新时 通知对应属性的effect重新执行
return result;
}
}
2.effect实现
package/reactivity/src/index/effect
响应的 effect,可以做到数据变化重新执行,默认会先执行一次
全局变量 activeEffect 保存当前的 effect
使用 effectStack 栈来记录当前的 activeEffect
export function effect(fn, options: any = {}) {
const effect = createReactiveEffect(fn, options);
if (!options.lazy) {
effect();
}
return effect;
}
let uid = 0;
let activeEffect; // 存储当前的effect,当前正在运行的effect
const effectStack = []
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
if (!effectStack.includes(effect)) { // 保证effect没有加入到effectStack中
try {
effectStack.push(effect);
activeEffect = effect;
return fn(); // 函数执行时会取值 会执行get方法
} finally {
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
}
}
effect.id = uid++; // 制作一个effect标识 用于区分effect
effect._isEffect = true; // 用于标识这个是响应式effect
effect.raw = fn; // 保留effect对应的原函数
effect.options = options; // 在effect上保存用户的属性
return effect;
}
3.track依赖收集实现
package/reactivity/src/index/effect
区别在于 vue2 任何属性变化 watcher.update 都会执行
effect 没被用到的数据变化 effect 不会重新执行
effect 等价于 vue2 中的 watcher
track 方法 收集依赖 将 key 和对应的 effect 关联起来
weakMap key: {age:12} value:(map) =>{age => set(effect)}
const targetMap = new WeakMap();
// 让某个对象中的属性 收集当前他对应的effect函数
export function track(target, type, key) {
if (activeEffect === undefined) {
// 此属性不用收集依赖,因为没在effect中使用
return;
}
let depsMap = targetMap.get(target);
if (!depsMap) {
//如果没有targetMap.get(target)
targetMap.set(target, (depsMap = new Map));
}
let dep = depsMap.get(key);
if (!dep) {
//如果没有depsMap.get(key)
depsMap.set(key, (dep = new Set))
}
if (!dep.has(activeEffect)) {
//如果没有dep.has(activeEffect)
dep.add(activeEffect);
}
}
4.trigger触发更新
触发set方法,trigger方法触发更新,让key对应的effect执行
-
看修改的是不是
数组的长度因为改长度影响比较大 如果更改的长度小于收集的索引,那么这个索引也需要触发effect重新执行 -
可能是对象
-
如果修改数组中的
某一个索引怎么办?
如果添加了一个索引就触发长度的更新
package/reactivity/src/index/effect
import { isArray, isIntegerKey } from "@vue/shared";
import { TriggerOrTypes } from "./operators";
// 找属性对应的effect 让其执行 (数组、对象)
export function trigger(target, type, key?, newValue?, oldValue?) {
// 如果这个属性没有 收集过effect,那不需要做任何操作
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = new Set(); // 这里对effect去重了
const add = (effectsToAdd) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => effects.add(effect));
}
}
// 我要将所有的 要执行的effect 全部存到一个新的集合中,最终一起执行
// 1. 看修改的是不是数组的长度 因为改长度影响比较大
if (key === 'length' && isArray(target)) {
// 如果对应的长度 有依赖收集需要更新
depsMap.forEach((dep, key) => {
if (key === 'length' || key > newValue) { // 如果更改的长度 小于收集的索引,那么这个索引也需要触发effect重新执行
add(dep)
}
})
} else {
// 可能是对象
if (key !== undefined) { // 这里肯定是修改, 不能是新增
add(depsMap.get(key)); // 如果是新增
}
// 如果修改数组中的 某一个索引 怎么办?
switch (type) { // 如果添加了一个索引就触发长度的更新
case TriggerOrTypes.ADD:
if (isArray(target) && isIntegerKey(key)) {
add(depsMap.get('length'));
}
}
}
effects.forEach((effect: any) => effect())
}
5.ref实现
ref本质就是通过
类的属性访问器来实现的,可以将一个普通值类型进行包装。ref将普通的类型转化成一个对象,这个对象中有value属性,指向原来的值。
import { hasChanged, isObject } from "@vue/shared";
import { track, trigger } from "./effect";
import { TrackOpTypes, TriggerOpTypes } from "./operations";
import { reactive } from "./reactive";
export function ref(value) { // ref Api
return createRef(value);
}
export function shallowRef(value) { // shallowRef Api
return createRef(value, true);
}
function createRef(rawValue, shallow = false) {
return new RefImpl(rawValue, shallow)
}
const convert = (val) => isObject(val) ? reactive(val) : val; // 递归响应式
class RefImpl {
public _value; //表示 声明了一个_value属性 但是没有赋值
public __v_isRef = true; // 产生的实例会被添加 __v_isRef 表示是一个ref属性
constructor(public rawValue, public shallow) { // 参数中前面增加修饰符 标识此属性放到了实例上
this._value = shallow ? rawValue : convert(rawValue)// 如果是深度 需要把里面的都变成响应式的
}
// 类的属性访问器
get value() { // 代理 取值取value 会帮我们代理到 _value上
track(this, TrackOpTypes.GET, 'value');
return this._value
}
set value(newValue) {
if (hasChanged(newValue, this.rawValue)) { // 判断老值和新值是否有变化
this.rawValue = newValue; // 新值会作为老值
this._value = this.shallow ? newValue : convert(newValue);
trigger(this, TriggerOrTypes.SET, 'value', newValue);
}
}
}
6.toRefs实现
将对象中的属性转换成ref属性,toRefs基于ref,遍历对象加上ref
class ObjectRefImpl {
public __v_isRef = true;
constructor(public target, public key) {}
get value(){ // 代理
return this.target[this.key] // 如果原对象是响应式的就会依赖收集
}
set value(newValue){
this.target[this.key] = newValue; // 如果原来对象是响应式的 那么就会触发更新
}
}
// 将某一个key对应的值 转化成ref
export function toRef(target, key) { // 可以把一个对象的值转化成 ref类型
return new ObjectRefImpl(target, key)
}
export function toRefs(object){ // object 可能传递的是一个数组 或者对象
const ret = isArray(object) ? new Array(object.length) :{}
for(let key in object){
ret[key] = toRef(object,key);
}
return ret;
}
7.vue2、vue3区别
vue2 是一上来就对data中的数据进行递归,vue3 是当取值时会进行代理。 vue3 的代理模式是懒代理。让某个对象中的属性,收集当前他对应的 effect 函数,相当于 vue2 中的 watcher。
ref内部使用的是defineProperty,reactive内部采用的proxy。
对于数组的处理,vue2采用了重写数组方法。vue3则是trigger方法触发更新时,进行判断,如果添加了一个索引就触发长度的更新,如果更改的数组长度小于收集的索引,那么这个索引也需要触发effect重新执行。