第二章、响应式系统的作用与实现(一)-(读书笔记)

235 阅读3分钟

这是我们读《vuejs设计与实现》第四章响应式系统的作用与实现,原书第二章、第三章简单的给我们说了一些简单的原理,我们只需要了解即可(感兴趣的话也可以跟着书中例子写一写),从第四章开始到第六章就会进入响应式设计模块,这个内容我们有必要记录一下,并且跟着实现相应的代码,方便我们后续深入的学习。

1、副作用函数

副作用函数:执行函数会直接或间接对其他函数的执行或修改别的变量,那我们称这是个副作用函数;下面两个例子是典型的副作用函数:

function effect() {
  document.getElementById('#app').innerHTML = 'hello world'
}
// 或者
var a = 1;
function change() {
  a = 2
}

2、响应式数据概念

响应式数据:当某个变量的值发生了变化,相关的副作用函数自动执行,我们就说这个变量属于响应式数据;如下面的例子:

const obj = { text: 'hellow world' }
function effect() {
  document.body.innerText = obj.text
}
// 如果obj.text值变化,能触发effect执行,那我们就认为obj是响应式数据

3、响应式系统的设计

针对与响应式数据,一般业内有两套方案,第一种自然是老版本的方式,通过Object.defineProperty属性拦截的方式来去实现(很经典的方式,当然也是vue面试老生常谈的八股文),第二种方式就是es6之后的Proxy代理的模式实现,针对上述案例,我们进行实现办法:

<!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>
      const bucket = new Set();
      const data = { text : 'hello world' };
      const obj = new Proxy(data, {
        get(target, key) {
          bucket.add(effect);
          return target[key];
        },
        set(target, key, value) {
          target[key] = value;
          bucket.forEach(effect => effect());
          return true;
        }
      });
      function effect() {
        console.log('此处被执行');
        document.getElementById('app').innerHTML = obj.text;
      }
      effect();
      
      setTimeout(() => {
        obj.other = '我添加新属性'
      }, 1000)
      setTimeout(() => {
        obj.text = '你变化啊~'
      }, 2000);
    </script>
  </body>
</html>

对于上面的做的响应式数据系统,我们可以明显的发现,假如我们增加了一个other属性,也会触发proxy代理里面的副作用函数,事实上我们想要响应式系统是如下结构的:

同时我们设计一下存储副作用函数的bucket(关于数据类型 Map,WeakMap,Set,后续我会出相关介绍的文章,此处暂且不表)

就是对应的key值触发相应的副作用函数,而不是一股脑全部执行,下面是关于改进后的响应式数据设计:

<!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>
    // 副作用函数
    let activeEffect;
    function effect(fn) {
      activeEffect = fn;
      fn();
    }
    // 数据准备
    const data = { text: 'hello world' }
    // 响应式函数容器
    const bucket = new WeakMap();
    const obj = new Proxy(data, {
      get(target, key) {
        tack(target, key);
        return target[key];
      },
      set(target, key, value) {
        target[key] = value;
        trigger(target, key);
        return true;
      }
    });
    // 向bucket里面注入副作用函数
    function tack(target, key) {
      // 没有acticeEffect
      if (!activeEffect) {
        return target[key];
      }
      // 判断下面有没有对应的对象相关的内容
      let depsMap = bucket.get(target);
      if (!depsMap) {
        depsMap = new Map();
        bucket.set(target, depsMap);
      }
      // 对应key值的内容
      let deps = depsMap.get(key);
      if (!deps) {
        deps = new Set();
        depsMap.set(key, deps);
      }
      deps.add(activeEffect);
    }
    // 副作用触发函数
    function trigger(target, key) {
      let depsMap = bucket.get(target);
      if (!depsMap) {
        return;
      }
      let deps = depsMap.get(key);
      deps && deps.forEach(fn => fn());
    }

    effect(() => {
      console.log('执行副作用函数 text')
      document.querySelector('#app').innerHTML = obj.text;
    });

    effect(() => {
      // 在获取obj.info时候,就会将副作用函数放入到对应的bucket中
      console.log('执行副作用函数 info', obj.info);
    });

    setTimeout(() => {
      obj.info = '开始变化';
    }, 2000);
  </script>
</body>
</html>

如果上述代码我们调整一下下面的内容:

const data = { text: 'hello world'isShow: true }
// 响应式数据设计这个一招上面例子所示即可
const obj = new Proxy(data , .....)
// 调整一下effect副作用函数
effect(() => {
  console.log('执行副作用函数')
  document.querySelector('#app').innerHTML = obj.isShow ? obj.text : 'not show text'
})

setTimeout(() => {
  obj.isShow = false
}, 1000)

setTimeout(() => {
  obj.text = 'can i show?'
}, 2000)

最后一个定时器,我们调整text值,我们发现他依然会触发副作用函数,事实上,这个时候obj.isShow是false,页面上面根本不会用到obj.text值,这是一次无效的副作用函数执行,具体原因就是,我们在创建副作用函数时候,在bucket里面在 text, isShow中放入了相同的副作用函数,所以我们对于响应式数据这个改造的方向就是能让他更智能一点,当我们用不到obj.text时候,修改obj.text值,去除无效的副作用函数;下面就是我们的第三个版本改造,具体如下所示:

<!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>
    // 副作用函数
    let activeEffect;
    function effect(fn) {
      const effectFn = () => {
        clearEffectDeps(effectFn);
        activeEffect = effectFn;
        fn()
      }
      effectFn.deps = []
      effectFn()
    }
    // 每次清除副作用函数列表里面的关联关系
    function clearEffectDeps(effectFn) {
      effectFn.deps.forEach(i => {
        i.delete(effectFn)
      })
      effectFn.deps.length = 0
    }
    // 数据准备
    const data = { text: 'hello world', ok: true };
    // 响应式函数容器
    const bucket = new WeakMap();
    const obj = new Proxy(data, {
      get(target, key) {
        tack(target, key);
        return target[key];
      },
      set(target, key, value) {
        target[key] = value;
        trigger(target, key);
        return true;
      }
    });
    // 向bucket里面注入副作用函数
    function tack(target, key) {
      // 没有acticeEffect
      if (!activeEffect) {
        return;
      }
      // 判断下面有没有对应的对象相关的内容
      let depsMap = bucket.get(target);
      if (!depsMap) {
        depsMap = new Map();
        bucket.set(target, depsMap);
      }
      // 对应key值的内容
      let deps = depsMap.get(key);
      if (!deps) {
        deps = new Set();
        depsMap.set(key, deps);
      }
      deps.add(activeEffect);
      // 将对应key值的副作用函数相关信息放入副作用函数
      activeEffect.deps.push(deps);
      console.log(bucket)
    }
    // 副作用触发函数
    function trigger(target, key) {
      let depsMap = bucket.get(target);
      if (!depsMap) {
        return;
      }
      let deps = depsMap.get(key);
      const effectsRun = new Set(deps);
      effectsRun.forEach(fn => fn());
    }

    effect(() => {
      console.log('触发副作用函数');
      document.querySelector('#app').innerHTML = obj.ok ? obj.text : 'not';
    });

    setTimeout(() => {
      obj.ok = false
    }, 1000);
    setTimeout(() => {
      obj.text = 'hello world2'
    }, 2000);
  </script>
</body>
</html>

如此就能达到一个基础的响应式数据雏形