第四章 响应式系统的实现

54 阅读5分钟

最基础的一个响应式系统

// 存储副作用函数的桶
const bucket = new Set();

// 原始数据
const data = { text: "hello world" };
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 effect 添加到存储副作用函数的桶中
    bucket.add(effect);
    // 返回属性值
    return target[key];
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal;
    // 把副作用函数从桶里取出并执行
    bucket.forEach((fn) => fn());
  },
});

function effect() {
  document.body.innerText = obj.text;
}
effect();
  • 利用 proxy 的夹子(get, set) 来劫持数据的读取, 读取数据时来收集副作用函数, 设置数据时出发副作用函数

一个完善的响应式系统

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

// 原始数据
const data = { foo: 1 };
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key);
    // 返回属性值
    return target[key];
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal;
    // 把副作用函数从桶里取出并执行
    trigger(target, key);
  },
});
// 读取属性时,收集依赖
function track(target, key) {
  if (!activeEffect) return;
  let depsMap = bucket.get(target);
  if (!depsMap) {
    // detsMap 是 Map类型 Map的键target的属性名, 值是属性对应副作用函数
    bucket.set(target, (depsMap = new Map()));
  }
  let deps = depsMap.get(key);
  if (!deps) {
    // deps 是Set 类型, 存储的是副作用函数集合
    depsMap.set(key, (deps = new Set()));
  }
  // deps 收集对应作用函数
  deps.add(activeEffect);
  // 副作用函数 也会记录 服务于哪些deps
  activeEffect.deps.push(deps);
}

// 设置属性时,执行副作用函数
function trigger(target, key) {
  // 获取当前对象对应的副作用函数集合
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  // 获取当前对象的属性对应的副作用函数集合
  const effects = depsMap.get(key);

  const effectsToRun = new Set();
  // 收集所有的副作用函数, 当前只在执行的副作用函数就不要再次执行了,  避免陷入死循环
  // set add  和 remove 避免同时执行
  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(effectFn => effectFn())
}

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect;
// effect 栈, 类似函数调用栈, 在effect 嵌套的情况下, 可以保证关联到对应的副作用函数
const effectStack = [];

function effect(fn, options = {}) {
  const effectFn = () => {
    // 先清除当前副作用函数所有的依赖项
    cleanup(effectFn)
    // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
    activeEffect = effectFn
    // 在调用副作用函数之前将当前副作用函数压栈
    effectStack.push(effectFn)
    // 获取执行结果
    const res = fn()
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
    effectStack.pop()
    // 副作用函数执行完成后,立即更新当前的副作用函数的指向
    activeEffect = effectStack[effectStack.length - 1]
    // 返回fn 的执行结果
    return res
  }
  // 将 options 挂在到 effectFn 上
  effectFn.options = options
  // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  if (!options.lazy) {
    effectFn()
  }

  return effectFn
}

// 删除副作用函数所有的依赖项
function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}

响应式系统的数据结构

  1. WeakRef 对象允许您保留对另一个对象的弱引用,而不会阻止被弱引用对象被 GC 回收
  2. WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
  3. WeakMap 的 key 只能是 Object 类型, 当 key 没有其他地方被引用时,数据就会被销毁.

data-struct.png

computed 实现

function computed(getter) {  // getter = () => obj.foo + obj.bar
  // value 用于缓存上次的值,提升性能
  let value;
  // 是否脏值
  let dirty = true;

  const effectFn = effect(getter, {
    // 副作用函数无需立即执行
    lazy: true,
    // 调度器函数,
    scheduler() {
      //  obj.foo , obj.bar 会将effectFn 收集为依赖, 
      // 当obj.foo 和 obj.bar 值发生改变是 会通过调度器执行effectFn, 添加脏值标记,
      if (!dirty) {
        //
        dirty = true;
        // 并重新计算 computed的值
        trigger(obj, "value");
      }
    },
  });

  const obj = {
    // 读取
    get value() {
      // 如果是脏值重新计算新值,首次执行必定会重新计算
      if (dirty) {
        // 执行effect 并获取到结果,会被后面的track 收集为依赖
        value = effectFn();
        dirty = false;
      }
      // 把计算属性的obj 添加到响应式数据桶中
      track(obj, "value");
      return value;
    },
  };

  return obj;
}

// 调用
const sumRes = computed(() => obj.foo + obj.bar) 
console.log(sumRes.value)
  • computed 计算属性,实际上一个懒执行的副作用函数, 计算属性 通过对象的get取值时,手动执行副作用函数即可
  • 当计算属性依赖的响应式数据 发生变化时,通过调度器scheduler 将ditry 设置为true ,下次取值就会重新算取新值
  • 思考: 现在知道 使用计算属性时 为啥要 xxx.value 了吧?

watch 的实现

// 递归读取一个对象所有的属性,出发track,收集依赖, 
function traverse(value, seen = new Set()) {
  if (typeof value !== 'object' || value === null || seen.has(value)) return
  seen.add(value)
  for (const k in value) {
    traverse(value[k], seen)
  }
  return value
}

function watch(source, cb, options = {}) {
  let getter
  // 监听的源 可能是对象  也可能是函数
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }

  let oldValue, newValue

  const job = () => {
    // 记录新增
    newValue = effectFn()
    cb(oldValue, newValue)
    oldValue = newValue
  }

  const effectFn = effect(
    // 执行 getter
    () => getter(),
    {
      lazy: true,
      scheduler: () => {
        // 控制执行的时机
        if (options.flush === 'post') {
          // 利用了异步的微任务队列机制
          const p = Promise.resolve()
          p.then(job)
        } else {
          job()
        }
      }
    }
  )

  // 是否立即执行
  if (options.immediate) {
    job()
  } else {
    // 执行并 记录本次执行的结果
    oldValue = effectFn()
  }
}

watch(() => obj.foo, (newVal, oldVal) => {
  console.log(newVal, oldVal)
}, {
  immediate: true,
  flush: 'post'
})

回调的触发时机

  1. 当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
  2. 如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项;

个人理解:

通过上面的代码, 可以看出 computed 和 watch 都是基于effect方法来实现的, 主要是利用了 Effect函数在执行会读取响应数据,被收集为依赖, 当数据改变时,再通过该trigger去调用Effect,Effect优先有调度器scheduler来执行,而在调度器的内部, 就顺便 来computed 脏值dirty的状态, 和 顺便执行一下 watch的callback!