说说 vue3 中的响应式设计原理,用js文件简单模拟实现vue3中的响应式

一、什么是响应式数据

响应式数据指的是当数据发生变化后,能够自动触发某些副作用的执行,从而达到某些目的。在应用中,这通常意味着当数据改变时,所有依赖于该数据的地方都会自动更新。这种响应式机制特别适用于动态内容更新、表单验证和处理、数据驱动的交互等场景。
众所周知,Vue通过Object.defineProperty或Proxy等方式来劫持数据对象的getter和setter,从而能够在数据变化时通知依赖它的代码部分进行更新。
那么今天我们就来用js文件来简单模拟一下vue3实现的响应式数据。

1、实现单个值的响应式

let price = 10, quantity = 2, total = 0;
const dep = new Set(); // ① 
const effect = () => { total = price * quantity };
const track = () => { dep.add(effect) };  // ②
const trigger = () => { dep.forEach( effect => effect() )};  // ③

track();
console.log(`total: ${total}`); // total: 0
trigger();
console.log(`total: ${total}`); // total: 20
price = 20;
trigger();
console.log(`total: ${total}`); // total: 40

上面代码通过 3 个步骤,实现对 total 数据进行响应式变化:

① 初始化一个 Set 类型的 dep 变量,用来存放需要执行的副作用( effect 函数),这边是修改 total 值的方法;

② 创建 track() 函数,用来将需要执行的副作用保存到 dep 变量中(也称收集副作用);

③ 创建 trigger() 函数,用来执行 dep 变量中的所有副作用;

在每次修改 price 或 quantity 后,调用 trigger() 函数执行所有副作用后, total 值将自动更新为最新值。

2、实现单个对象的响应式

let product = { price: 10, quantity: 2 }, total = 0;
const depsMap = new Map(); // ① 
const effect = () => { total = product.price * product.quantity };
const track = key => {     // ②
  let dep = depsMap.get(key);
  if(!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  dep.add(effect);
}

const trigger = key => {  // ③
  let dep = depsMap.get(key);
  if(dep) {
    dep.forEach( effect => effect() );
  }
};

track('price');
console.log(`total: ${total}`); // total: 0
effect();
console.log(`total: ${total}`); // total: 20
product.price = 20;
trigger('price');
console.log(`total: ${total}`); // total: 40

上面代码通过 3 个步骤,实现对 total 数据进行响应式变化:

① 初始化一个 Map 类型的 depsMap 变量,用来保存每个需要响应式变化的对象属性(key 为对象的属性, value 为前面 Set 集合);

② 创建 track() 函数,用来将需要执行的副作用保存到 depsMap 变量中对应的对象属性下(也称收集副作用);

③ 创建 trigger() 函数,用来执行 dep 变量中指定对象属性的所有副作用;

这样就实现监听对象的响应式变化,在 product 对象中的属性值发生变化, total 值也会跟着更新。

3、实现多个对象的响应式

let product = { price: 10, quantity: 2 }, total = 0;
const targetMap = new WeakMap();     // ① 初始化 targetMap,保存观察对象
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() );
  }
};

track(product, 'price');
console.log(`total: ${total}`); // total: 0
effect();
console.log(`total: ${total}`); // total: 20
product.price = 20;
trigger(product, 'price');
console.log(`total: ${total}`); // total: 40

上面代码通过 3 个步骤,实现对 total 数据进行响应式变化:

① 初始化一个 WeakMap 类型的 targetMap 变量,用来要观察每个响应式对象;

② 创建 track() 函数,用来将需要执行的副作用保存到指定对象( target )的依赖中(也称收集副作用);

③ 创建 trigger() 函数,用来执行指定对象( target )中指定属性( key )的所有副作用;

这样就实现监听对象的响应式变化,在 product 对象中的属性值发生变化, total 值也会跟着更新。

二、Proxy 和 Reflect(需要注意的是:这两个API不支持 IE)

在上面内容中,介绍了如何在数据发生变化后,自动更新数据,但存在的问题是,每次需要手动通过触发 track() 函数搜集依赖,通过 trigger() 函数执行所有副作用,达到数据更新目的。

这一节将来解决这个问题,实现这两个函数自动调用。

1、如何使用 Reflect

通常我们有三种方法读取一个对象的属性:

  1. 使用 . 操作符:leo.name ;
  2. 使用 [] : leo[‘name’] ;
  3. 使用 Reflect API:Reflect.get(leo, ‘name’) 。

这三种方式输出结果相同。

2、如何使用 Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。语法如下:

const p = new Proxy(target, handler)

参数如下:

  • target : 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler : 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
    我们通过官方文档,体验一下 Proxy API
let product = { price: 10, quantity: 2 };
let proxiedProduct = new Proxy(product, {
    get(target, key){
      console.log('正在读取的数据:',key);
    return target[key];
  }
})
console.log(proxiedProduct.price); 
// 正在读取的数据: price
// 10

然后结合 Reflect 使用,只需修改 get 和 set 函数:

let product = { price: 10, quantity: 2 };
let proxiedProduct = new Proxy(product, {
  get(target, key, receiver){
    console.log('正在读取的数据:',key);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver){
    console.log('正在修改的数据:', key, ',值为:', value);
    return Reflect.set(target, key, value, receiver);
  }
})
proxiedProduct.price = 20;
console.log(proxiedProduct.price); 
// 正在修改的数据: price ,值为: 20
// 正在读取的数据: price
// 20

这样便完成 get 和 set 函数来拦截对象的读取和修改的操作。为了方便对比 Vue 3 源码,我们将上面代码抽象一层,使它看起来更像 Vue3 源码:

function reactive(target){
  const handler = {  // ① 封装统一处理函数对象
    get(target, key, receiver){
      console.log('正在读取的数据:',key);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver){
      console.log('正在修改的数据:', key, ',值为:', value);
      return Reflect.set(target, key, value, receiver);
    }
  }
  
  return new Proxy(target, handler); // ② 统一调用 Proxy API
}

let product = reactive({price: 10, quantity: 2}); // ③ 将对象转换为响应式对象
product.price = 20;
console.log(product.price); 
// 正在修改的数据: price ,值为: 20
// 正在读取的数据: price
// 20

3、修改 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

三、activeEffect 和 ref

在上面代码中,还存在一个问题: track 函数中的依赖( effect 函数)是外部定义的,当依赖发生变化, track 函数收集依赖时都要手动修改其依赖的方法名。
这很不友好。
下面来解决这个问题。

1. 引入 activeEffect 变量

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

也可以来看看vue3的源码。

代码地址:
https://github.com/Code-Pop/vue-3-reactivity/blob/master/05-activeEffect.js

2、实现ref 方法

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

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

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

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

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

(2)使用对象的属性访问器(计算属性)
const ref = raw => {
  // 创建一个响应式对象,其 value 属性指向传入的 value  
  const state = reactive({ value: raw });
  const r = {
      get value() {
          track(r, 'value');
          return state.value;
      },

      set value(newVal) {
          state.value = newVal;
          trigger(r, 'value');
      }
  }
  return r;
}

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

代码地址:
https://github.com/Code-Pop/vue-3-reactivity/blob/master/06-ref.js

四、分享一个小demo,拿走不谢

<!DOCTYPE html>
<html lang="en">

    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>proxy代理</title>
    </head>

    <body>
        <div id="app"></div>
        <button id="add">+</button>
        <button id="jian">-</button>
        <button id="zhe">打五折</button>
        <hr>
        </hr>
        <button id="addList">添加</button>
        <ul id="list"></ul>
    </body>
    <script>
        // 模拟vue3中的reactive方法定义响应式对象
        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) {
                    // 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);
        }

        const ref = raw => {
            // 创建一个响应式对象,其 value 属性指向传入的 value  
            const state = reactive({ value: raw });
            const r = {
                get value() {
                    track(r, 'value');
                    return state.value;
                },

                set value(newVal) {
                    state.value = newVal;
                    trigger(r, 'value');
                }
            }
            return r;
        }

        let product = reactive({ price: 10, quantity: 2 });
        let list = reactive([1, 2, 3]);
        let salePrice = ref(product.price);
        // 使用自定义的reactive方法创建响应式对象 计算商品的总价
        effect(() => {
            document.querySelector('#app').innerHTML = `总价:${product.price * product.quantity}, 单价:${salePrice.value}`;
        });
        document.querySelector("#add").addEventListener("click", () => {
            product.price += 10;
        });
        document.querySelector("#jian").addEventListener("click", () => {
            product.price -= 10;
        });
        // 使用自定义reactive方法创建响应式数组  给数组添加元素
        effect(() => {
            document.querySelector('#list').innerHTML = "";
            list.forEach(item => {
                let itemDom = document.createElement('li');
                itemDom.innerHTML = item;
                document.querySelector('#list').appendChild(itemDom);
            });
        });
        document.querySelector("#addList").addEventListener("click", () => {
            list.push(Math.random());
        });
        // 使用自定义ref方法创建响应式数据,修改单价为5折
        document.querySelector("#zhe").addEventListener("click", () => {
            salePrice.value = salePrice.value * 0.5;
        });
    </script>

</html>
  • 14
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值