likes
comments
collection
share

vue3源码阅读与实现: 响应式系统-reactive模块

作者站长头像
站长
· 阅读数 24

reactive模块

模块总览

vue3源码阅读与实现: 响应式系统-reactive模块

vuereactive函数可以创建复杂数据类型的响应式数据,在vue官网中提到reactive有以下几个短板

  1. 有限的值类型: reactive只能处理复杂数据类型的响应式
  2. 不能替换整个响应式对象: 使用reactive创建一个响应式数据之后,不能对这个响应式数据重新赋值,即使赋值的是与第一次相同的数据,也会丢失第一次创建的响应式连接
  3. 解构: 由reactive创建的响应式对象,解构后,数据会丢失响应性

让我们带着问题,查看vue到底是如何实现的reactive函数

debugger

通过debugger查看reactive模块执行过程,使用下面的测试用例,用例有三个关注点:

  1. 创建reactive响应式数据
  2. 访问响应式数据
  3. 设置响应式数据

debugger时重点关注vue在这三个关键点做了什么事情

测试用例:

//在packages/vue/examples/test/reactive.html
<html>
  <head>
      ...
    <script src="../../dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      const { reactive, effect } = Vue
      debugger
      const data = reactive({ name: '哈哈哈' }) // 创建reactive响应式数据

      debugger
      effect(() => {
        document.querySelector('#app').innerHTML = data.name // 访问响应式数据
      })

      setTimeout(() => {
        debugger
        data.name = '嘿嘿' // 设置响应式数据
      }, 2000)
    </script>
  </body>
</html>
  1. npm run build对源码进行打包

  2. 打开测试用例并open with live server运行,进入debugger

  3. vue3源码阅读与实现: 响应式系统-reactive模块 点击1进入下一个debugger

    点击2: setp next over function

    点击3:setp next into function

    点击4:setp out of current function

在次强调一遍调试时,只关注核心,没有进入的条件判断分支无需关注

关注点1:创建响应式数据

点击3进入reactive函数,查看创建响应式数据的逻辑

  1. reactive函数中,返回了createReactiveObject()的返回值

vue3源码阅读与实现: 响应式系统-reactive模块

  1. 进入createReactiveObject(),最后返回了一个新创建的proxy实例,并将这个实例以target为键,proxy实例为值保存在reactiveMap中,与target形成映射

vue3源码阅读与实现: 响应式系统-reactive模块

  1. proxy实例的配置由baseHandlers提供,至于baseHandlersgetset的逻辑,之后触发的时候再去看

总结

reactive函数做了两件事情:

  1. 创建proxy实例
  2. 保存proxy实例到reactiveMap

关注点2:依赖收集

点击按钮1进入下一个debugger,在这里使用effect(fn)函数并在回调中触发name属性的get

  1. 进入effect(fn)函数,该函数中创建了ReactiveEffect实例_effect,

vue3源码阅读与实现: 响应式系统-reactive模块

  1. ReactiveEffect类中fn属性保存了传递给构造函数的参数fn,除了fn属性外,还有个run函数,至于这个run函数做了什么,暂时不去考虑

vue3源码阅读与实现: 响应式系统-reactive模块

  1. 实例创建完成后,调用了_effectrun方法

  2. 进入run方法,将当前实例保存在了全局变量activeEffect中,这个全局变量保存的就是当前key所谓的依赖.保存后最后调用了fn函数,也就是effect(fn)函数的回调fn,由于这个函数中我们访问了name属性,所以会触发响应式数据的get,然后进入get的逻辑

vue3源码阅读与实现: 响应式系统-reactive模块

  1. get逻辑在createGetter函数中,在这个函数中:

    1. 通过const res = Reflect.get(target, key, receiver)拿到属性的值

    2. 然后进入track(target, TrackOpTypes.GET, key)函数,这个函数是重点他是专门做依赖收集的

vue3源码阅读与实现: 响应式系统-reactive模块

  1. track函数中,首先获取到全局变量activeEffect的值,这个变量在执行effect(fn)函数的时候已经赋值为ReactiveEffect实例了,所以这里拿到的就是刚刚创建的ReactiveEffect实例

  2. 接下来就从targetMap中取出target对应的依赖,是一个Map对象,如果没有就创建一个新的Map,然后将key和activeEffect保存在map中,其中targetMap保存的就是所有数据的所有依赖,其数据结构伪代码是这样的:

    targetMap: weakMap{
        target1: map{
            key1:set[ReactiveEffect实例1,ReactiveEffect实例2....]
            key2:set[]
        },
        target2: map{
            key1:set[]
            key2:set[]
        },
    }
    
    这里的target就是测试用例中的: {name:'哈哈哈'}
    target中的key1就是name
    key1对应的set中存放的是每一个ReactiveEffect实例,
    ReactiveEffect实例有一个fn属性,存放的就是effect(fn)函数的fn
    

    有了这个数据结构,就建立起了每个key和依赖的关系,当key的值变化时,根据这个结构,来触发对应的依赖,对应的源码为:

vue3源码阅读与实现: 响应式系统-reactive模块

  1. 最后返回了第五步res,此时页面上应该能显示出name的值哈哈哈

这样就完成了依赖收集的过程

总结

effect函数做了1件事情

  1. 根据依赖创建ReactiveEffect实例,并保存在全局变量activeEffect

get函数做了2件事情

  1. 获取target中的值并返回
  2. 通过activeEffect变量获取当前key对应的依赖,将依赖存放在targetMap

关注点3:依赖触发

点击按钮1进入最后一个debugger;在这里我对name属性重新赋值了一下,所以会触发响应式数据的set

  1. 进入createSetter()函数,执行set对应的逻辑,首先获取该属性的旧值

vue3源码阅读与实现: 响应式系统-reactive模块

  1. 通过const result = Reflect.set(target, key, value, receiver)设置了新值到target

vue3源码阅读与实现: 响应式系统-reactive模块

  1. 然后根据hasChanged函数,判断该属性的值是否发生了变化,如果变化需要触发依赖,然后进入依赖触发函数trigger(target, TriggerOpTypes.SET, key, value, oldValue),这个函数也是重点

vue3源码阅读与实现: 响应式系统-reactive模块

  1. trigger中,首先从targetMap.get(target)拿到target对应的所有依赖depsMap,在根据depsMapkey拿到key所对应的所有依赖dep,最后通过triggerEffects,触发dep中所有ReactiveEffect实例的run函数,实际上触发的就是依赖收集时保存的fn函数

vue3源码阅读与实现: 响应式系统-reactive模块

vue3源码阅读与实现: 响应式系统-reactive模块

vue3源码阅读与实现: 响应式系统-reactive模块

总结

set函数做了两件事情:

  1. 为属性设置新的值
  2. 判断属性的值是否发生了变化,如果变化,则进行依赖触发操作: 获取targetMap中收集对应依赖,然后执行依赖实例的run函数

总结

整个流程已经走完了,对其中的一些重要点进行总结一下:

重要的全局变量/类:

  1. reactiveMap:存放target和其proxy实例的映射
  2. targetMap: 存放所有依赖
  3. activeEffect:存放当前触发getterkey对应的依赖
  4. ReactiveEffect: 创建effect实例,实例在fn属性中保存了依赖函数,每当执行实例的run属性时,都会先赋值activeEffect为this,然后执行fn触发依赖收集,这个类非常重要,可以简单记为创建出来的effect实例就是依赖,响应式数据收集依赖时就是收集的这个实例

整体流程:

  1. reactive函数通过proxy实现每个key对应的get行为和set行为监听,在触发某个keygetter时收集依赖存储在targetMap中,在触发某个keysetter时触发targetMap中存储的对应依赖
  2. 全局变量activeEffect是响应式数据和依赖产生联系的一个桥梁: effect函数创建依赖实例ReactiveEffect,并保存在全局变量activeEffect中,由于getter行为是在effect函数的回调触发的,所以effect函数总是在getter触发前先执行,先给activeEffect赋值,因此,getter触发时,activeEffect存放的一定式是触发这次getterkey的依赖.这样getter中收集依赖时才能保证:key与依赖实例准确对应起来

实现reactive模块

reactive函数

主要用来创建proxy实例,

packages/reactivity/src/reactive.ts,

import { isObject } from "@vue/shared";
import { mutableHandlers } from "./basehandlers";

// 缓存target对应的proxyObj
const reactiveMap: WeakMap<object, any> = new WeakMap();
export function reactive(target: object) {
  return createReactiveObject(target, mutableHandlers, reactiveMap);
}

// 根据target生成proxy实例,并构建两者的缓存映射
function createReactiveObject(
  target: object,
  mutableHandlers: ProxyHandler<object>,
  proxyMap: WeakMap<object, any>
) {
  let existingProxy = proxyMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }

  existingProxy = new Proxy(target, mutableHandlers);
  // 构建target和proxy的映射
  proxyMap.set(target, existingProxy);

  return existingProxy;
}

basehandlers模块

在这里创建了get处理函数和set处理函数,在get中进行依赖收集,在set中进行依赖触发

packages/reactivity/src/basehandlers.ts

import { track, trigger } from "./effect";

const get = createGetter();
const set = createSetter();

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
};

function createGetter() {
  return function (target: object, key: string | symbol, receiver: object) {
    const res = Reflect.get(target, key, receiver);

    // 依赖收集
    track(target, key);

    return res;
  };
}
function createSetter() {
  return function (
    target: object,
    key: string | symbol,
    value: any,
    receiver: object
  ) {
    Reflect.set(target, key, value, receiver);

    // 触发依赖
    trigger(target, key, value);

    return true;
  };
}

effect模块

这里主要做:

  1. 创建全局变量targetMap保存所有依赖,
  2. 创建全局变量activeEffect保存触发当前key的依赖
  3. 创建ReactiveEffect类,用来生成reactiveEffect实例
  4. 进行依赖收集track
  5. 进行依赖触发trigger

packages/reactivity/src/effect.ts

import { isArray } from "@vue/shared";
import { createDep, Dep } from "./deps";

type KeyToMap = Map<any, Set<ReactiveEffect>>;
// 存放每个target和其对应的所有依赖
let targetMap = new WeakMap<any, KeyToMap>();
// 存放当前收集到的ReactveEffect
export let activeEffect: ReactiveEffect | undefined;

/**
 * @message: 依赖收集,存储target每个属性对应的依赖
 */
export function track(target: object, key: string | symbol) {
  // 如果没有依赖对象,则不用收集
  if (!activeEffect) {
    return;
  }

  let depsMap = targetMap.get(target);
  if (!depsMap) {
    // depsMap不存在,说明是第一次收集到依赖,创建一个新的来存储key对应的依赖对象
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }

  // 多依赖处理
  let dep = depsMap.get(key);
  if (!dep) {
    dep = createDep();
    depsMap.set(key, dep);
  }
  trackEffets(dep);
}

/**
 * @message: 依赖触发,触发track收集的所有依赖
 */
export function trigger(target: object, key: string | symbol, value: any) {
  const depsMap = targetMap.get(target); // 获取target对应的所有依赖
  if (!depsMap) {
    return;
  }
  const dep = depsMap.get(key); // 根据setter修改的key,找到key对应的所有依赖,并触发
  if (!dep) {
    return;
  }
  triggerEffects(dep);
}

/**
 * @message: 将依赖创建为依赖对象,并保存到全局变量activeEffect
 */
export function effect<T = any>(fn: () => T) {
  const _effect = new ReactiveEffect(fn);
  _effect.run(); // 调用run 保存这个实例到activeEffect,并执行依赖
}

// 包含依赖实例
export class ReactiveEffect<T = any> {
  public fn: () => T;
  constructor(fn: () => T) {
    this.fn = fn; // 将依赖函数保存在fn中,这样每个实例都能保存对应的依赖,保存这个实例,就可以拿到依赖函数了
  }

  run() {
    const preCache = activeEffect
    activeEffect = this; // 指定当前处理的依赖,以便收集的时候获取

    this.fn();
    activeEffect = preCache // 依赖收集完毕,将activeEffect还原
  }
}
/**
 * @message: 添加依赖到dep中
 */
export function trackEffets(dep: Dep) {
  dep.add(activeEffect!);
}

/**
 * @message: 触发所有的依赖
 */
export function triggerEffects(dep: Dep) {
  const effects = isArray(dep) ? dep : [...dep];
  for (const effect of effects) {
    triggerEffect(effect);
  }
}
/**
 * @message: 触发指定依赖
 */
export function triggerEffect(effect: ReactiveEffect) {
  effect.run();
}

deps模块

主要用来生成一个存放依赖的Set

packages/reactivity/src/deps.ts

import { ReactiveEffect } from "./effect";

export type Dep = Set<ReactiveEffect>;
/**
 * @message:依据effects生成dep实例,保存key对应的所有依赖
 */
export function createDep(effets?: ReactiveEffect[]): Dep {
  const dep = new Set<ReactiveEffect>(effets);
  return dep;
}

到此简易版的reactive模块就实现好了,可以在vue下创建之前debugger的测试用例来测试一下.

总结

回到本节刚开始的三个问题:

  1. 有限的值类型: reactive函数通过proxy来创建响应式数据,proxy只能代理对象,因此reactive无法处理简单数据类型,
  2. 不能替换整个响应式对象: 直接替换响应式对象将丢失之前创建的proxy实例的引用
  3. 解构: 只有对proxy代理对象的get,set操作才能被监听到,因此,将proxy对象解构后,对解构数据进行get,set无法触发proxy代理对象的get,set.那么数据自然就丢失响应性了
转载自:https://juejin.cn/post/7395473411651256359
评论
请登录