Vue3源码学习之路-实现reactivity

Vue3响应式

  • 在Vue2的时候使用defineProperty来进行数据的劫持,需要对属性进行重写setter和getter性能差
  • 当新增属性和删除属性时无法监听变化,需要通过$set和$delete实现
  • 数组不采用defineProperty来劫持(对所有索引进行劫持将浪费性能),所以对数组需要进行单独处理
  • Vue3中采用Proxy来实现响应式,从而解决了上述问题

使用Vue3中的reactivity

pnpm install vue -w

创建reactivity/dist/index.html文件,引入官方reactivity

<!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="../../../node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
    <script>
      // effect 副作用函数,如果此函数依赖的数据变化将会重新执行
      // reactive 将数据变成响应式,其实就是一个Proxy
      // shallowReactive 浅响应式
      // readonly 只读的
      // shallowReadonly readonly shallowReactive 都是基于reactive
      const { effect, reactive, shallowReactive, readonly, shallowReadonly } =
        VueReactivity;

      // 创建一个响应式数据
      const state = reactive({ name: 'bowen', age: 18, desc: { money: 0 } });
      // 对象中的对象也是一个Proxy
      // console.log(state.desc);

      // 浅响应式
      // const littleState = shallowReactive({ name: 'bowen', age: 18, desc: { money: 0 } });
      // 浅响应式中对象里的对象不是Proxy
      // console.log(littleState.desc);

      // 只读将不能更改属性值
      // const state = readonly({ name: 'bowen', age: 18, desc: { money: 0 } });

      // 深层属性可以更改但是不会触发响应式
      // const state = shallowReadonly({ name: 'bowen', age: 18, desc: { money: 0 } });

      // 副作用逻辑
      // effect 默认会执行一次,对响应式数据进行取值
      // 取值的过程中数据会依赖于当前的effect
      // name和age变化将会重新执行effect函数
      effect(() => {
        app.innerHTML = `我叫${state.name},今年${state.age}岁。`;
      });

      setTimeout(() => {
        state.age = 24;
      }, 2000);
    </script>
  </body>
</html>

创建共享方法模块

创建shared/src/index.ts

// 判断是否是不为null的对象
export const isObject = function (obj) {
  return typeof obj === 'object' && obj !== null;
};

实现reactive

创建reactivity/src/baseHandler.ts文件

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

// 响应式标识枚举
export const enum ReactiveFlags {
  IS_REACTIVE = '__vue_isReactive__',
}

export const mutableHandlers = {
  get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return true;
    }

    track(target, 'get', key);

    const result = Reflect.get(target, key, receiver);

    if(isObject(result)) {
      // 深度代理
      return reactive(result);
    }

    return result;
  },
  set(target, key, value, receiver) {
    const oldValue = target[key];
    const result = Reflect.set(target, key, value, receiver);
    if (oldValue !== value) {
      // 值变化了,要进行更新
      trigger(target, 'set', key, value, oldValue);
    }
    return result;
  },
};

创建reactivity/src/reactive.ts文件

import { isObject } from '@vue/shared';
import { mutableHandlers, ReactiveFlags } from './baseHandler';

// 代理缓存,防止同一个对象生成多个代理
// WeakMap不会导致内存泄漏,key只能是对象
const reactiveMap = new WeakMap();

/**
 * 将数据转换成响应式数据
 * 实现同一对象多次代理只返回同一个代理
 * 代理对象二次代理将直接返回传入的代理
 */
export function reactive(target) {
  // 只能做对象的代理
  if (!isObject(target)) {
    return;
  }

  // 如果传入的是已经代理过的Proxy无需再次代理
  if (target[ReactiveFlags.IS_REACTIVE]) {
    return target;
  }

  // 传入的对象是否已经进行了代理
  const existProxy = reactiveMap.get(target);
  if (existProxy) {
    return existProxy;
  }

  // 不会对属性进行重新定义
  // 只是对取值和赋值进行代理
  const proxy = new Proxy(target, mutableHandlers);

  reactiveMap.set(target, proxy);

  return proxy;
}

实现effect

创建reactivity/src/effect.ts文件

export let activeEffect = undefined;

// 创建响应式effect
class ReactiveEffect {
  // 默认是激活状态
  public active = true;
  // 保证effect嵌套情况下的正确关联
  public parent = null;
  // 收集依赖了哪些值 effect记录属性
  public deps = [];
  
  constructor(public fn) {}

  // 执行effect
  public run() {
    // 如果是非激活的状态,只需要执行函数,不需要进行依赖收集
    if (!this.active) {
      return this.fn();
    }

    try {
      this.parent = activeEffect;
      // 依赖收集,将当前的effect和渲染的属性关联在一起
      activeEffect = this;
      // 当调用取值操作的时候就可以获取到全局的activeEffect
      return this.fn();
    } finally {
      activeEffect = this.parent;
      this.parent = null;
    }
  }

  public stop() {
    this.active = false;
  }
}

export function effect(fn) {
  // fn可以根据状态变化重新执行
  // effect可以嵌套

  const _effect = new ReactiveEffect(fn);

  // 默认先执行一次
  _effect.run();
}

/**
 * 属性收集
 * 一个effect对应多个属性
 * 一个属性对应多个effect
 * 对象中的某个属性可能对应多个effect
 * weekMap = { 对象: Map{ name: Set } }
 */

const targetMap = new WeakMap();

export function track(target, type, key) {
  if (!activeEffect) {
    return;
  }

  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()));
  }

  let shouldTrack = !dep.has(activeEffect);
  if (shouldTrack) {
    dep.add(activeEffect);
    // effect记录属性,清理时用
    activeEffect.deps.push(dep);
  }
}

export function trigger(target, type, key, value, oldValue) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    // 触发的值不在模板中使用
    return;
  }

  // 获取属性对应的key
  const effects = depsMap.get(key);
  if (effects) {
    effects.forEach((effect) => {
      if (effect !== activeEffect) {
        // 屏蔽掉相同的调用
        effect.run();
      }
    });
  }
}

导出自己的方法

reactivity/src/index.ts

export { effect } from './effect';
export { reactive } from './reactive';

使用reactivity

创建reactivity/dist/index.html文件,引入自己的reactivity

<!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 { effect, reactive } = VueReactivity;

      // 创建一个响应式数据
      const obj = { name: 'bowen', age: 18, desc: { money: 0 } };
      const state = reactive(obj);

      effect(() => {
        document.getElementById(
          'app'
        ).innerHTML = `我是${state.name},今年${state.age}岁.`;
      });

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

依赖收集问题

例子:reactivity/dist/index.html

<!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 { effect, reactive } = VueReactivity;
      const obj = { name: 'bowen', age: 18, flag: true };
      const state = reactive(obj);

      effect(() => {
        console.log('渲染');
        document.getElementById('app').innerHTML = state.flag ? state.name : state.age;
      });

      setTimeout(() => {
        state.flag = false;
        setTimeout(() => {
          console.log('修改了name,但是这时显示的是age,所以不应该执行渲染函数');
          state.name = 'xc';
        }, 1000);
      }, 1000);
    </script>
  </body>
</html>

清空依赖,重新收集

当执行函数时,我们应该清空收集的依赖,然后进行重新收集

reactivity/src/effect.ts中新增函数cleanupEffect

function cleanupEffect(effect) {
  const { deps } = effect;
  deps.forEach((item) => {
    item.delete(effect);
  });
  effect.deps.length = 0;
}

reactivity/src/effect.ts中ReactiveEffect执行函数时执行cleanupEffect

try {
  this.parent = activeEffect;
  // 依赖收集,将当前的effect和渲染的属性关联在一起
  activeEffect = this;
  // 清空依赖属性,重新收集
  cleanupEffect(this);
  // 当调用取值操作的时候就可以获取到全局的activeEffect
  return this.fn();
} finally {
  activeEffect = this.parent;
  this.parent = null;
}

reactivity/src/effect.ts中trigger执行effects时先拷贝一份再执行解决Set死循环问题

let effects = depsMap.get(key);
if (effects) {
  effects = [...effects];
  effects.forEach((effect) => {
    if (effect !== activeEffect) {
      // 屏蔽掉相同的调用
      effect.run();
    }
  });
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值