响应系统demo

113 阅读6分钟

响应系统demo

github demo

核心代码

/**
 * 目前只是一个简单的处理响应式数据的程序。
 */
let tmp1, tmp2;
let data = {foo: 1, bar: 2};
export const obj = new Proxy(data, {
  get(target, key){
    track(target, key);
    // print(bucket, target);
    // 继续正常返回属性值
    return target[key];
  },
  set(target, key, newVal){
    // 第一步首先是人家设置以后,先让人家设置成功
    target[key] = newVal;
    // 然后将该属性对应的副作用函数都取出来一一执行。
    trigger(target, key);
    return true;
  }
})

// 用一个全局变量储存被注册的副作用函数
let activeEffect;
const effectStack = [];
const bucket = new WeakMap();
// effect函数用来注册副作用函数
export function effect(fn, options = {}){
  // 在函数作用域中设置函数变量effectFn,这里不能使用var声明符
  // 将effectFn作为副作用函数(通过effectFn调用fn)
  const effectFn = () => {
    // console.log('effectFn is runing!');
    cleanUp(effectFn);
    activeEffect = effectFn;
    effectStack.push(effectFn);
    // 这里的fn是真正的副作用函数,外层的effectFn也好,effect也好都是基于fn封装的副作用函数
    // 使用的是外观模式,好处:在调用副作用函数的时候只需要调用effectFn就行,解耦合。缺点:修改副作用函数的时候需要修改effectFn,违反了开闭原则
    const res = fn();
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
    // 将res作为fn函数的返回值
    return res;
  }
  // 记录用户自定义选项
  effectFn.options = options;
  // 这个数组用来记录和effectFn相关联的target.key对应的集合。
  effectFn.deps = [];
  // 执行effectFn
  if (!options.lazy) {
    effectFn();
  }
  // 返回副作用函数
  return effectFn
}

export function track(target, key) {
  // fn直接执行,没有通过effect执行,直接return。
  if (!activeEffect) return ;
  // 获取target里面被监听的属性
  let depsMap = bucket.get(target);
  // 如果bucket中对应的target不存在说明现在是第一次读取该对象,那就将该对象直接添加进去吧。
  // 添加的操作说明target需要被监听,但是现在还没有确定是target的哪个key需要被监听。
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()));
  }
  // 获取target.key对应的副作用函数,也就是在哪个函数里面读取了target.key
  let deps = depsMap.get(key);
  // 如果deps不存在说明target.key是第一次被读取。
  // 有可能是上下文之前在操作target的其他key,有可能是上下文第一次操作target.key。
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  // 将target.key对应的副作用函数记录在set集合中
  deps.add(activeEffect);
  // 这里将记录与副作用函数相关联的target.key对应的集合
  activeEffect.deps.push(deps);
}
export function trigger(target, key) {
  // 获取监听的targe对象对应的属性
  const depsMap = bucket.get(target);
  // 如果这个对象不需要被监听
  if (!depsMap) return ;
  // 根据监听的key获取target.key对应的副作用函数
  const effects = depsMap.get(key);
  // 将effects集合拷贝下来
  const effectsToRun = new Set();
  effects && effects.forEach(effectFn => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn);
    }
  })
  // 遍历另外一个集合,但是执行本集合下的副作用函数,避免死循环
  effectsToRun.forEach(effectFn => {
    // 如果存在用户自定义函数,调用用户自定义函数。其中用户自定义函数的参数是副作用函数
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn);
    } else {
      effectFn();
    }
  });
  // effects && effects.forEach(fn => fn());
}
// 从effectFn相关联的target.key对应的副作用函数集合中将effectFn删除
export function cleanUp(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  // 上面的操作从每个与之相关的集合中删除了effectFn,但是相关联的集合还在,下面就是重置effectFn数组,将相关的集合置为空
  effectFn.deps.length = 0;
}
// 上面是响应式系统的实现
//-------------------------------------------------//
// 下面是控制用户自定义函数fn执行次数的jobQueue

// 定义一个set记录用户自定义函数fn,利用了set自动去重的特性保证同一个函数只会在set中出现一次。
const jobQueue = new Set();
// 定义一个状态为fullfilled的promise实例。将jibQueue的执行过程当做微任务处理。
const p = Promise.resolve();

// 设置一个标志用来判断是否需要将onFullfilled处理函数压入队列
let isFlushing = false;

let flush = function flushJob(){
  // 如果isFlushing为true,直接返回函数
  if(isFlushing) return;
  // 在这里将isFlushing设置为true,如果obj.foo++执行两次的话(代码参考下面obj.foo++),不会每一次都去设置一个微任务。
  isFlushing = true;
  // 添加微任务
  p.then(() => {
    jobQueue.forEach(job => job());
  }).finally(() => {
    // 当对应的jobQueue函数(也就是对应的微任务)执行完之后,设置为false
    isFlushing = false;
  })

}

//-------------------------------------------------//
// 下面是响应式系统的测试即用户编辑的代码
// const effectFn = effect(() => obj.foo + obj.bar, { lazy: true });
// console.log(effectFn());

// module.exports = {
//   effect: effect,
//   obj: obj,
//   track: track,
//   trigger: trigger
// }

上面这段代码做了什么?

前情提要:

  • data:普通对象
  • key:访问的代理对象的属性
  • obj:data的代理对象
  • bucket:记录响应关系的数据结构

effect:封装副作用函数。

用户编写一个function fn,将其作为参数传入effect中。fn中对代理对象obj的访问将会被track函数拦截。

track:建立响应关系。

调用track函数将会收集当前的副作用函数(通过activeEffect变量记录),收集的过程为:当前访问的是哪个对象(data)的哪个属性(track函数拦截的key参数)?在哪个函数(通过activeEffect变量记录的副作用函数)中访问的这个属性?对应代码为:使用bucket的key记录data,bucket的value是depsMap(map类型变量)。depsMap的key记录访问的属性,depsMap的value(是set类型,可以对应多个副作用函数)记录副作用函数。

trigger:触发响应关系。

修改obj的属性值的时候,使用track建立的响应关系,在bucket中通过target和key找到副作用函数集合。然后触发集合中副作用函数的执行。

watch的实现

// 实现watch函数

// const { effect, obj, track, trigger } = require("./version10")
import { effect, obj, track, trigger } from './version10.js'

// source 是响应式数据,cb是回调函数
function watch(source, cb) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  let oldValue, newValue
  // 使用effect注册副作用函数时,开启lazy选项,并把返回值储存到effectFn中以便后续手动调用
  const effectFn = effect(
    () => getter(),
    {
      lazy: true,
      scheduler() {
        // 在scheduler中重新执行副作用函数,得到的是新的值
        // 因为在触发scheduler的时候已经修改了target.key的值,去执行effectFn能够获取到修改后的值,修改后的值是通过traverse来获取到的。
        newValue = effectFn();
        // 这里仍然存在问题,因为obj只有一个,newvalue和oldValue最终都指向obj,这里传入的oldValue和newValue应该是相同的。
        // 将旧的值和新的值作为回调函数的参数
        cb(newValue, oldValue);
        // 更新旧值,不然下一次会得到错误的旧值。
        // 下次响应式数据变化的时候不会再运行watch函数。
        oldValue = newValue
      }
    }
  )
  // 手动调用副作用函数,拿到的值就是旧值
  oldValue = effectFn();
}

// 使用traverse遍历函数,这个函数的目的是读取代理对象的属性,使用track拦截读取操作,建立副作用函数的响应关系。

function traverse (value, seen = new Set()) {
  // 如果读取的数据不是对象,或者已经被读取过了,或者值为空,那就什么都不做
  if (typeof value !== 'object' || value === null || seen.has(value)) {
    return ;
  }
  // 将读取到的数据添加到seen中,代表读取过了。
  seen.add(value);
  // 假设value能够通过forin语句遍历
  for (const k in value) {
    traverse(value[k], seen)
  }
  return value
}

/////////////////////////////////////////////

// 使用watch函数
watch(obj, ()=>{
  console.log('数据变化了!');
})

obj.foo ++

接下来我们参考ES5规范,将代码watch(obj, ()=>{console.log('数据变化了!');})的执行过程的内存图展示出来。

运行过程是动态的,以图片展示的过程是静态的。每张图片中都标注着代码位置,意味着代码运行至此处时对应的内存图。故事梗概是对当前内存图的讲解。

首遇watch运行

image.png

进入watch函数

image.png

进入effect函数

image.png

走出effect函数

image.png

因oldValue进的effectFn

image.png

我们来个小总结

image.png

进入fn函数

F4567522-B593-411a-8219-7EB4897A9262.png

走出fn函数

image.png

从obj.foo++再起航

image.png

先读取

image.png

进入set函数--end

image.png

我使用的是chrome测试环境来分析的运行内存图。在上图中省略了window的引用关系,window上级引用关系如下图所示:

image.png