响应系统设计—Part 1

162 阅读3分钟

说明

【vue.js 设计与实现】 霍春阳 学习笔记

概念解释

  • 响应式数据
    数据发生变化后,依赖改数据的数据也会随之变化。
    如果data是响应式数据,那么data.count发生变化后document.body.innerHTML也会更新。

    let data = {
        name: '舞狮牛牛',
        price: 10,
        count: 1,
    }
    function effect() {
        console.log('执行副作用函数');
        let money = proxyData.count * proxyData.price;
        let text = `采购信息:总金额 = ${money}`;
        document.body.innerHTML = text;
    }
    
  • 副作用函数
    会产生副作用的函数,它的执行会直接或间接的影响其他函数的执行。
    比如effect,执行后会影响document.body.innerHTML,会影响其他读取document.body.innerHTML的函数,我们就说它产生了副作用。

实现

Version 0.1 — 基础版

如果data是响应式数据,当副作用函数执行后,money会更新。为了达到这样的效果,我们能怎么做?
我们注意到:

  • 副作用的执行会触发data的读取
  • data的读取会触发get
  • data的修改会触发set 我们可以通过proxy拦截到对象的get和set操作,在get时将副作用函数暂存起来,set时重新执行
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="main"></div>
    <script type="module" src="reactive-1.js"></script>
  </body>
</html>
/*reactive-1.js*/
// 存储副作用函数,set可以去重
const bucket = new Set();
const proxyData = new Proxy(data, {
    get(data, key) {
        console.log(`触发get,key = ${key}`);
        bucket.add(effect);
        return data[key];
    },

    set(data, key, newVal) {
        console.log(`触发set,key = ${key}`);
        data[key] = newVal;
        bucket.forEach(effect => {
            effect();
        });
        return true;
    }
});

function effect() {
    console.log('执行副作用函数');
    let money = proxyData.count * proxyData.price;
    let text = `采购信息:总金额 = ${money}`;
    document.body.innerHTML = text;
}

window.onload = function() {
    effect();
    setTimeout(() => {
        console.log('-- update count before --');
        proxyData.count = 10;
        console.log('-- update count after --');
    }, 1000);
}

Xnip2022-05-31_08-39-16.jpg

Xnip2022-05-31_08-39-27.jpg 但此时存在以下几个问题:

  1. 副作用函数是收集是硬编码的,现有的effect名称变更了,代码还需要修改才能按照预期运行

  2. 代码的复用性低

    • 副作用函数还需要手动执行(window.onload中),每增加一个副作用函数就要写一遍
      /*新增*/
      function showDetail(){
        let detail = `数量 = ${proxyData.count}, 单价 = ${proxyData.price}`;
        console.log(detail);
      }
      
      const proxyData = new Proxy(data, {
          get(data, key) {
              console.log(`触发get,key = ${key}`);
              bucket.add(effect);
              /*新增*/
              bucket.add(showDetail); 
      
              return data[key];
          },
          ...
      });
      
      window.onload = function() {
          effect();
          /*新增*/
          showDetail();
      };
      
    • 如果希望再创建一个响应式对象,还需要再次写Proxy读取、设置,再添加一个bucket
  3. 对象每个属性的变更都会引起effect的重新执行,尽管这个属性和effect没什么关系

    window.onload = function() {
    effect();
    setTimeout(() => {
        console.log('-- update name before --');
       proxyData.name = 10;
        console.log('-- update name after --');
    }, 1000);
    

    Xnip2022-05-31_08-50-55.jpg

Version 0.2 — 进阶版

为了解决第1、2.1个问题,我们将effect的添加改成动态的——增加一个注册函数和一个全局变量,将创建proxy对象的过程包装一下

/* 新增 */
let activeEffect = null;

/* 新增 */
/** 
 * 注册副作用函数
 * @param {Function} effectFn 
 */
function effect(effectFn) {
    activeEffect = effectFn;
    effectFn();
}

// 存储副作用函数,set可以去重
const bucket = new Set();
/* 新增 */
/**
 * 创建响应式对象
 * @param {*} obj 
 * @returns 
 */
function reactive(obj){
    return new Proxy(obj, {
        get(obj, key) {
            console.log(`触发get,key = ${key}`);
            /* 修改 */
            if(activeEffect){
                bucket.add(activeEffect);
            }
            return obj[key];
        },
    
        set(obj, key, newVal) {
            console.log(`触发set,key = ${key}`);
            obj[key] = newVal;
            bucket.forEach(effect => {
                effect();
            });
            return true;
        }
    });
}

const proxyData = reactive({
    name: '牛牛手办',
    price: 10,
    count: 1,
});

function calculateTotalCount() {
    console.log('执行副作用函数');
    let money = proxyData.count * proxyData.price;
    let text = `采购信息:总金额 = ${money}`;
    document.body.innerHTML = text;
}

window.onload = function() {
    /** 修改 */
    effect(calculateTotalCount);

    setTimeout(() => {
        console.log('-- update count before --');
        proxyData.count = 10;
        console.log('-- update count after --');
    }, 1000);
}

这次修改提高了代码的复用性。

再来解决第3个问题,问题产生的原因是我们将副作用函数和对象进行了关联,但实际这个副作用函数只需要和对象的某几个属性做关联。希望的关系是:

Xnip2022-06-01_08-41-13.jpg 这个树形的关系,可用以下结构来表示:

Xnip2022-06-01_08-56-38.jpg

为什么这里使用WeackMap而不是Map?
WeakMap的key只能是对象,是弱引用。key所指的对象可以被垃圾回收,回收后对应的key和value就访问不到了。所以WeakMap适合存储那些只有当key所引用的对象存在时(没被回收)才有价值的信息。

修改后的代码:

/* 修改 存储副作用函数 */
const bucket = new WeakMap();

/**
 * 创建响应式对象
 * @param {*} obj 
 * @returns 
 */
function reactive(obj){
    return new Proxy(obj, {
        get(target, key) {
            console.log(`触发get,key = ${key}`);
            /* 修改 存入副作用函数*/
            if(activeEffect){
                let targetMap = bucket.get(target);
                if(!targetMap){
                    targetMap = new Map();
                    bucket.set(target, targetMap);
                }
                let effectSet = targetMap.get(key);
                if(!effectSet){
                    effectSet = new Set();
                    targetMap.set(key, effectSet);
                }
                effectSet.add(activeEffect);
            }

            return target[key];
        },
    
        set(target, key, newVal) {
            console.log(`触发set,key = ${key}`);
            target[key] = newVal;
            
            /* 修改 取出并执行副作用函数 */
            const targetMap = bucket.get(target);
            if(!targetMap) return true;
            const effectSet = targetMap.get(key);
            if(!effectSet) return true;
            effectSet.forEach(effectFn => {
                effectFn();
            });
            return true;
        }
    });
}

get和set中副作用的代码太多了,且此功能和get、set独立,我们将它抽取出来,让代码更整洁。完整版本:

let activeEffect = null;

/** 
 * 注册副作用函数
 * @param {Function} effectFn 
 */
function effect(effectFn) {
    activeEffect = effectFn;
    effectFn();
}

// 存储副作用函数
const bucket = new WeakMap();

/**
 * 收集target对象key的副作用函数
 * @param {Object} target 
 * @param {String|Symbol} key 
 * @return {void}
 */
function track(target, key){
    if(!activeEffect) return;
    let targetMap = bucket.get(target);
    if(!targetMap){
        targetMap = new Map();
        bucket.set(target, targetMap);
    }
    let effectSet = targetMap.get(key);
    if(!effectSet){
        effectSet = new Set();
        targetMap.set(key, effectSet);
    }
    effectSet.add(activeEffect);
}

/**
 * 触发target对象key的副作用函数执行
 * @param {Object} target 
 * @param {String|Symbol} key 
 * @return {void}
 */
function trigger(target, key) {
    const targetMap = bucket.get(target);
    if(!targetMap) return;
    const effectSet = targetMap.get(key);
    if(!effectSet) return;
    effectSet.forEach(effectFn => {
        effectFn();
    });
}

/**
 * 创建响应式对象
 * @param {*} obj 
 * @returns 
 */
function reactive(obj){
    return new Proxy(obj, {
        get(target, key) {
            console.log(`触发get,key = ${key}`);
            // 存入副作用函数
            track(target, key);

            return target[key];
        },
    
        set(target, key, newVal) {
            console.log(`触发set,key = ${key}`);
            target[key] = newVal;
            // 取出并执行副作用函数
            trigger(target, key);
            return true;
        }
    });
}

const proxyData = reactive({
    name: '牛牛手办',
    price: 10,
    count: 1,
});

function calculateTotalCount() {
    console.log('执行副作用函数');
    let money = proxyData.count * proxyData.price;
    let text = `采购信息:总金额 = ${money}`;
    document.body.innerHTML = text;
}

window.onload = function() {
    effect(calculateTotalCount);

    setTimeout(() => {
        console.log('-- update count before --');
        proxyData.count = 10;
        console.log('-- update count after --');
    }, 1000);
    
    setTimeout(() => {
        console.log('-- update name before --');
        proxyData.name = '皇冠牛牛';
        console.log('-- update name after --');
    }, 1500);
}

Xnip2022-06-01_09-43-58.jpg