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

143 阅读5分钟

上一章我们了解基础的响应式系统设计办法,下面我们继续学习响应式系统的相关设计

1、关于effect嵌套与effect栈

effect副作用函数式可以被嵌套,如下所示:

effect(funtion1() {
  effect(function2() {
    ....
  })
})

为什么会出现副作用函数的嵌套呢?我们拿vuejs里面的组件来说明:

const Page = {
  render() {
    return (<Bar>
      <Content />        
    </Bar>)
  }
}

两个组件嵌套,其实在渲染过程中会分别执行组件的render函数,向下遍历内层的组件的时候,我们需要同样遍历组件的render方法,如下面伪代码所示:

effect(() => {
  Bar.render();
  effect(() => {
    Content.render()
  })
})

接着我们上一讲的设计的响应式系统,我们测试一下嵌套的effect执行情况:

/ 副作用函数
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 = { bar: 'hello world', content: '内容区域' };
// 响应式函数容器
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);
}
// 副作用触发函数
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());
}

let tmp1, tmp2;
effect(function funcion1() {
  console.log('funcion1 执行');

  effect(function funcion2() {
    console.log('funcion2 执行');
    tmp2 = obj.content;
  })

  tmp1 = obj.bar;
});

setTimeout(() => {
  obj.bar = '新内容';
}, 1000);
// 结果
// function1执行 ---(初始化执行)
// function2执行 ---(舒适化执行)
// function2执行

事实上我们最后定时器修改obj.bar的值,我们预想的结果是触发function1、function2的执行,然后我们只是执行了function2,原因就是我们其实在全局只放了一个activeEffect标志位,因此他存储的effect一定是最后一次的生成的副作用函数,其过程如下图所示:

解决这个问题,我们需要通过函数调用栈的方式处理,具体如下:

<!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;
      let effectStack = [];
      function effect(fn) {
        const effectFn = () => {
          clearEffectDeps(effectFn);
          activeEffect = effectFn;
          // 将当前副作用effectFn放入effectStack中
          effectStack.push(effectFn);
          // 执行副租用函数
          fn()
          // 出栈
          effectStack.pop();
          // 将activeEffect指向上一个effect
          activeEffect = effectStack[effectStack.length - 1];
        }
        effectFn.deps = []
        effectFn()
      }
      // 每次清除副作用函数列表里面的关联关系
      function clearEffectDeps(effectFn) {
        effectFn.deps.forEach(i => {
          i.delete(effectFn)
        })
        effectFn.deps.length = 0
      }
      // 数据准备
      const data = { bar: 'hello world', content: '内容区域' };
      // 响应式函数容器
      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);
      }
      // 副作用触发函数
      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()
        });
      }
      
      let tmp1, tmp2;
      effect(function funcion1() {
        console.log('funcion1 执行');
        
        effect(function funcion2() {
          console.log('funcion2 执行');
          tmp2 = obj.content;
        })
        
        tmp1 = obj.bar;
      });
      
      setTimeout(() => {
        obj.bar = '新内容';
      }, 1000);
      setTimeout(() => {
        obj.content = '新内容';
      }, 1000);
    </script>
  </body>
</html>

2、避免无线递归循环

上面还有一个问题,那就是我们同时进行读写操作obj.bar = obj.bar + 1,就会出现递归死循环,导致栈溢出;原因就是每次我们调用都是从 get->set 执行有发现需要 get ->set 这样就导致无线递归调用,解决办法就只

// 副作用触发函数
function trigger(target, key) {
  let depsMap = bucket.get(target);
  if (!depsMap) {
    return;
  }
  let deps = depsMap.get(key);
  const effectsRun = new Set();
  deps && deps.forEach(i => {
    if (i !== activeEffect) {
      effectsRun.add(i);
    }
  })
  effectsRun.forEach(fn => {
    fn()
  });
}

3、调度器

在这里,调度是指当trigger动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、顺序、次数及方式;那我们的响应式系统来说:

<!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;
    let effectStack = [];
    function effect(fn) {
      const effectFn = () => {
        clearEffectDeps(effectFn);
        activeEffect = effectFn;
        // 将当前副作用effectFn放入effectStack中
        effectStack.push(effectFn);
        // 执行副租用函数
        fn()
        // 出栈
        effectStack.pop();
        // 将activeEffect指向上一个effect
        activeEffect = effectStack[effectStack.length - 1];
      }
      effectFn.deps = []
      effectFn()
    }
    // 每次清除副作用函数列表里面的关联关系
    function clearEffectDeps(effectFn) {
      effectFn.deps.forEach(i => {
        i.delete(effectFn)
      })
      effectFn.deps.length = 0
    }
    // 数据准备
    const data = { bar: 1 };
    // 响应式函数容器
    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);
    }
    // 副作用触发函数
    function trigger(target, key) {
      let depsMap = bucket.get(target);
      if (!depsMap) {
        return;
      }
      let deps = depsMap.get(key);
      const effectsRun = new Set();
      deps && deps.forEach(i => {
        if (i !== activeEffect) {
          effectsRun.add(i);
        }
      })
      effectsRun.forEach(fn => {
        fn()
      });
    }

    effect(function funcion1() {
      console.log(obj.bar);
    });

    obj.bar++;
    console.log('结束了');
    
  </script>
</body>
</html>

他的执行结果是1 ,2 ,结束了, 如果我们想要调整这个代码执行的先后顺序为1,结束了,2,我们看上面的代码,显然是没有办法满足,那么我们就需要引入调度器这个概念进来,让其帮助我们在执行副作用函数时候做出一些调整,那么具体设计模式就需要调整如下:

effect(() => {
}, 
// 增加相应的配置项options       
{
  scheduler(fn) {
    // todo somethings
  }
})

同时我们需要将调度执行任务挂载到副作用函数上,方便我们在执行trigger时候去做相应的调度任务,我们调整effect副作用函数如下:

function effect(fn, option = {}) {
  const effectFn = () => {
    clearEffectDeps(effectFn);
    activeEffect = effectFn;
    // 将当前副作用effectFn放入effectStack中
    effectStack.push(effectFn);
    // 执行副租用函数
    fn()
    // 出栈
    effectStack.pop();
    // 将activeEffect指向上一个effect
    activeEffect = effectStack[effectStack.length - 1];
  }
  effectFn.deps = []
  // 增加调度任务模块
  effectFn.option = option;
  effectFn()
}

接下来,我们再去调整trigger执行过程的办法,其实也很简单,就是在我们执行副作用函数前,去判断是否有注册的调度器,如果有,我们就按照调度器的方式执行

// 副作用触发函数
function trigger(target, key) {
  let depsMap = bucket.get(target);
  if (!depsMap) {
    return;
  }
  let deps = depsMap.get(key);
  const effectsRun = new Set();
  deps && deps.forEach(i => {
    if (i !== activeEffect) {
      effectsRun.add(i);
    }
  })
  effectsRun.forEach(fn => {
    // 执行前,判断是否有调度器
    if (fn.option.scheduler) {
      fn.option.scheduler(fn);
    } else {
      fn();
    }
  });
}

那么针对我们上面提到的,想要将将函数的执行修改为1, 结束了,2这种方式,我们就可以通过调度器的方式调整副作用函数执行的优先级,最简单的方式定时器变成一个宏任务的方式调整他最后执行,具体如下:

effect(function funcion1() {
  console.log(obj.bar);
}, {
  scheduler(fn) {
    setTimeout(fn);
  }
});

在往复杂一点想,如果我们执行两个obj.bar ++,如下所示:

// ....
effect(function funcion1() {
  console.log(obj.bar);
}, {
//  scheduler(fn) {}
});
obj.bar++;
obj.bar++;
// 结果 1 2 3

在我们实际开发过程中,其实我们更想要的是1 3重复的执行我们就合并起来

// 创建调度任务容器
let workers = new Set();
// 创建一个微任务队列
const p = Promise.resolve();
// 是否标识正在刷新队列
let isFlushing = false;
 // 刷新队列
function fushWorkers() {
  if (isFlushing) return
  isFlushing = true
  p.then(() => {
    workers.forEach(w => w())
  }).finally(() => {
    isFlushing = false
  })
}
// 调整调度器
// ....
effect(function funcion1() {
  console.log(obj.bar);
}, {
  scheduler(fn) {
    workers.add(fn);
    flushWorkers();
  }
});

简单的来说分为以下几个步骤:

  • 首先:我们创建一个workers任务队列,用来存放我们的副作用函数任务,这块用到Set()主要是需要 Set()会自动去重,所以会保证Set()中通类型的副作用函数始终只有一个;
  • 其次:我们通过Promise.resolve()创建一个微任务,用来保证我们的程序执行顺序在任务最后,保证本次副作用函数数量是全量需要触发的;
  • 再者:我们在通过ifFlushing标识确定所有内容都会被放入到workers内;
  • 最后:当主任务结束,将会进入我们的微任务阶段,执行完毕后,再将isFlushing重置为false,方便我们下次执行新的任务

上面是一个基础版本的响应式系统设计办法