vue3 中的响应式设计原理 2

简介: vue3 中的响应式设计原理

4. 修改 track 和 trigger 函数

通过上面代码,我们已经实现一个简单 reactive() 函数,用来将普通对象转换为响应式对象。但是还缺少自动执行 track() 函数和 trigger() 函数,接下来修改上面代码:

const targetMap = new WeakMap();
let total = 0;
const effect = () => { total = product.price * product.quantity };
const track = (target, key) => { 
  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()));
  }
  dep.add(effect);
}
const trigger = (target, key) => {
  const depsMap = targetMap.get(target);
  if(!depsMap) return;
    let dep = depsMap.get(key);
  if(dep) {
    dep.forEach( effect => effect() );
  }
};
const reactive = (target) => {
  const handler = {
    get(target, key, receiver){
      console.log('正在读取的数据:',key);
      const result = Reflect.get(target, key, receiver);
      track(target, key);  // 自动调用 track 方法收集依赖
      return result;
    },
    set(target, key, value, receiver){
      console.log('正在修改的数据:', key, ',值为:', value);
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if(oldValue != result){
         trigger(target, key);  // 自动调用 trigger 方法执行依赖
      }
      return result;
    }
  }
  return new Proxy(target, handler);
}
let product = reactive({price: 10, quantity: 2}); 
effect();
console.log(total); 
product.price = 20;
console.log(total); 
// 正在读取的数据: price
// 正在读取的数据: quantity
// 20
// 正在修改的数据: price ,值为: 20
// 正在读取的数据: price
// 正在读取的数据: quantity
// 40

d8835e550d3b8368952e35904e0fddea.png

(图片来源:Vue Mastery)


三、activeEffect 和 ref

在上一节代码中,还存在一个问题: track 函数中的依赖( effect 函数)是外部定义的,当依赖发

生变化, track 函数收集依赖时都要手动修改其依赖的方法名。

比如现在的依赖为 foo 函数,就要修改 track 函数的逻辑,可能是这样:

const foo = () => { /**/ };
const track = (target, key) => {     // ②
  // ...
  dep.add(foo);
}

那么如何解决这个问题呢?

1. 引入 activeEffect 变量

接下来引入 activeEffect 变量,来保存当前运行的 effect 函数。

const foo = () => { /**/ };
const track = (target, key) => {     // ②
  // ...
  dep.add(foo);
}

然后在 track 函数中将 activeEffect 变量作为依赖:

const track = (target, key) => {
    if (activeEffect) {  // 1. 判断当前是否有 activeEffect
        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()));
        }
        dep.add(activeEffect);  // 2. 添加 activeEffect 依赖
    }
}

使用方式修改为:

effect(() => {
    total = product.price * product.quantity
});

这样就可以解决手动修改依赖的问题,这也是 Vue3 解决该问题的方法。完善一下测试代码后,如下:

const targetMap = new WeakMap();
let activeEffect = null; // 引入 activeEffect 变量
const effect = eff => {
  activeEffect = eff; // 1. 将副作用赋值给 activeEffect
  activeEffect();     // 2. 执行 activeEffect
  activeEffect = null;// 3. 重置 activeEffect
}
const track = (target, key) => {
    if (activeEffect) {  // 1. 判断当前是否有 activeEffect
        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()));
        }
        dep.add(activeEffect);  // 2. 添加 activeEffect 依赖
    }
}
const trigger = (target, key) => {
    const depsMap = targetMap.get(target);
    if (!depsMap) return;
    let dep = depsMap.get(key);
    if (dep) {
        dep.forEach(effect => effect());
    }
};
const reactive = (target) => {
    const handler = {
        get(target, key, receiver) {
            const result = Reflect.get(target, key, receiver);
            track(target, key);
            return result;
        },
        set(target, key, value, receiver) {
            const oldValue = target[key];
            const result = Reflect.set(target, key, value, receiver);
            if (oldValue != result) {
                trigger(target, key);
            }
            return result;
        }
    }
    return new Proxy(target, handler);
}
let product = reactive({ price: 10, quantity: 2 });
let total = 0, salePrice = 0;
// 修改 effect 使用方式,将副作用作为参数传给 effect 方法
effect(() => {
    total = product.price * product.quantity
});
effect(() => {
    salePrice = product.price * 0.9
});
console.log(total, salePrice);  // 20 9
product.quantity = 5;
console.log(total, salePrice);  // 50 9
product.price = 20;
console.log(total, salePrice);  // 100 18

思考一下,如果把第一个 effect 函数中 product.price 换成 salePrice 会如何:

effect(() => {
    total = salePrice * product.quantity
});
effect(() => {
    salePrice = product.price * 0.9
});
console.log(total, salePrice);  // 0 9
product.quantity = 5;
console.log(total, salePrice);  // 45 9
product.price = 20;
console.log(total, salePrice);  // 45 18

得到的结果完全不同,因为 salePrice 并不是响应式变化,而是需要调用第二个 effect 函数才会变化,也就是 product.price 变量值发生变化。


代码地址: vue-3-reactivity/05-activeEffect.js at master · Code-Pop/vue-3-reactivity · GitHub

2. 引入 ref 方法

熟悉 Vue3 Composition API 的朋友可能会想到 Ref,它接收一个值,并返回一个响应式可变的 Ref 对象,其值可以通过 value 属性获取。

ref:接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value。

官网的使用示例如下:

const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1

我们有 2 种方法实现 ref 函数:

  1. 使用 rective 函数
const ref = intialValue => reactive({value: intialValue});

这样是可以的,虽然 Vue3 不是这么实现。

  1. 使用对象的属性访问器(计算属性)

属性方式去包括:gettersetter

const ref = raw => {
  const r = {
    get value(){
      track(r, 'value');
      return raw;
    },
    set value(newVal){
      raw = newVal;
      trigger(r, 'value');
    }
  }
  return r;
}


使用方式如下:

let product = reactive({ price: 10, quantity: 2 });
let total = 0, salePrice = ref(0);
effect(() => {
    salePrice.value = product.price * 0.9
});
effect(() => {
    total = salePrice.value * product.quantity
});
console.log(total, salePrice.value); // 18 9
product.quantity = 5;
console.log(total, salePrice.value); // 45 9
product.price = 20;
console.log(total, salePrice.value); // 90 18

在 Vue3 中 ref 实现的核心也是如此。

代码地址: vue-3-reactivity/06-ref.js at master · Code-Pop/vue-3-reactivity · GitHub

四、实现简易 Computed 方法

用过 Vue 的同学可能会好奇,上面的 salePricetotal 变量为什么不使用 computed 方法呢?

没错,这个可以的,接下来一起实现个简单的 computed 方法。

const computed = getter => {
    let result = ref();
    effect(() => result.value = getter());
    return result;
}
let product = reactive({ price: 10, quantity: 2 });
let salePrice = computed(() => {
    return product.price * 0.9;
})
let total = computed(() => {
    return salePrice.value * product.quantity;
})
console.log(total.value, salePrice.value);
product.quantity = 5;
console.log(total.value, salePrice.value);
product.price = 20;
console.log(total.value, salePrice.value);

这里我们将一个函数作为参数传入 computed 方法,computed 方法内通过 ref 方法构建一个 ref 对象,然后通过 effct 方法,将 getter 方法返回值作为 computed 方法的返回值。


这样我们实现了个简单的 computed 方法,执行效果和前面一样。

五、源码学习建议

1. 构建 reactivity.cjs.js

这一节介绍如何去从 Vue 3 仓库打包一个 Reactivity 包来学习和使用。

准备流程如下:

  1. Vue 3 仓库下载最新 Vue3 源码;
git clone https://github.com/vuejs/vue-next.git
  1. 安装依赖:
yarn install
  1. 构建 Reactivity 代码:
yarn build reactivity

复制 reactivity.cjs.js 到你的学习 demo 目录:

上一步构建完的内容,会保存在 packages/reactivity/dist目录下,我们只要在自己的学习 demo 中引入该目录的 reactivity.cjs.js 文件即可。


学习 demo 中引入:

const { reactive, computed, effect } = require("./reactivity.cjs.js");

2. Vue3 Reactivity 文件目录

在源码的 packages/reactivity/src目录下,有以下几个主要文件:

  1. effect.ts:用来定义 effect / track / trigger
  2. baseHandlers.ts:定义 Proxy 处理器( get 和 set);
  3. reactive.ts:定义 reactive 方法并创建 ES6 Proxy;
  4. ref.ts:定义 reactive 的 ref 使用的对象访问器;
  5. computed.ts:定义计算属性的方法;


c19bb143cbea3d625dc6d7da2bdbb544.png

(图片来源:Vue Mastery)


六、总结

本文带大家从头开始学习如何实现简单版 Vue 3 响应式,实现了 Vue3 Reactivity 中的核心方法( effect / track / trigger / computed /ref 等方法),提高项目开发效率和代码调试能力。

相关文章
|
27天前
|
JavaScript 前端开发 安全
Vue 3
Vue 3以组合式API、Proxy响应式系统和全面TypeScript支持,重构前端开发范式。性能优化与生态协同并进,兼顾易用性与工程化,引领Web开发迈向高效、可维护的新纪元。(238字)
458 139
|
21天前
|
缓存 JavaScript 算法
Vue 3性能优化
Vue 3 通过 Proxy 和编译优化提升性能,但仍需遵循最佳实践。合理使用 v-if、key、computed,避免深度监听,利用懒加载与虚拟列表,结合打包优化,方可充分发挥其性能优势。(239字)
173 1
|
6月前
|
缓存 JavaScript PHP
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
883 5
|
2月前
|
开发工具 iOS开发 MacOS
基于Vite7.1+Vue3+Pinia3+ArcoDesign网页版webos后台模板
最新版研发vite7+vue3.5+pinia3+arco-design仿macos/windows风格网页版OS系统Vite-Vue3-WebOS。
327 11
|
1月前
|
JavaScript 安全
vue3使用ts传参教程
Vue 3结合TypeScript实现组件传参,提升类型安全与开发效率。涵盖Props、Emits、v-model双向绑定及useAttrs透传属性,建议明确声明类型,保障代码质量。
214 0
|
3月前
|
缓存 前端开发 大数据
虚拟列表在Vue3中的具体应用场景有哪些?
虚拟列表在 Vue3 中通过仅渲染可视区域内容,显著提升大数据列表性能,适用于 ERP 表格、聊天界面、社交媒体、阅读器、日历及树形结构等场景,结合 `vue-virtual-scroller` 等工具可实现高效滚动与交互体验。
395 1
|
3月前
|
缓存 JavaScript UED
除了循环引用,Vue3还有哪些常见的性能优化技巧?
除了循环引用,Vue3还有哪些常见的性能优化技巧?
218 0
|
4月前
|
JavaScript
vue3循环引用自已实现
当渲染大量数据列表时,使用虚拟列表只渲染可视区域的内容,显著减少 DOM 节点数量。
127 0
|
6月前
|
JavaScript API 容器
Vue 3 中的 nextTick 使用详解与实战案例
Vue 3 中的 nextTick 使用详解与实战案例 在 Vue 3 的日常开发中,我们经常需要在数据变化后等待 DOM 更新完成再执行某些操作。此时,nextTick 就成了一个不可或缺的工具。本文将介绍 nextTick 的基本用法,并通过三个实战案例,展示它在表单验证、弹窗动画、自动聚焦等场景中的实际应用。
542 17
|
7月前
|
JavaScript 前端开发 算法
Vue 3 和 Vue 2 的区别及优点
Vue 3 和 Vue 2 的区别及优点