Skip to content

Vue3原理深究!手写简易响应式系统

难点处理

注意点1:如何识别访问某个对象的key时,应该收集的fn是谁?

方案:在effect函数中,在执行fn进行收集之前,先临时存储需要被收集的fn(设置 activeEffect=fn),然后在trackadd这个activeEffect

js
++ let activeEffect = null; // 为了记录当前effect触发的那个key应该收集的fn是谁
function effect(fn) {
  if (fn) {
++  activeEffect = fn; //临时记录需要收集的那个fn
    console.warn("[current activeEffect is] ", activeEffect);
    fn();
  }
}

注意点2:触发依赖细致到具体某个key?

方案:修改deps数据结构,如:

js
deps = <weakMap> {
  data: <Map> {
     key: <Set> [fn1, fn2]
  }
}
js
/**
 * 为对象的key收集副作用(依赖)
 * deps格式简化如下:
 * deps = <weakMap> {
 *   data: <Map> {
 *      key: <Set> [fn1, fn2]
 *   }
 * }
 * @param {*} target
 * @param {*} key
 */
function track(target, key) {
  console.log("[track key] ", key);
  // 需要向 deps.get(target).get(key).add(fn)
  if (!activeEffect) {
    return;
  }

  // 获取对象
  let depDataMap = deps.get(target);
  if (!depDataMap) {
    depDataMap = new Map();
    deps.set(target, depDataMap);
  }

  // 获取对象某个key对应的依赖项
  let depDataMapKeySet = depDataMap.get(key);
  if (!depDataMapKeySet) {
    depDataMapKeySet = new Set();
    depDataMap.set(key, depDataMapKeySet);
  }

  depDataMapKeySet.add(activeEffect);
}

注意点3:避免收集age依赖时,会把name的依赖也收集起来的

方案:在effect里的fn执行(依赖收集)后,重置activeEffect = null;

js
let activeEffect = null; // 为了记录当前effect触发的那个key应该收集的fn是谁
function effect(fn) {
  if (fn) {
    activeEffect = fn; //临时记录需要收集的那个fn
    console.warn("[current activeEffect is] ", activeEffect);
    fn();
++  activeEffect = null; //避免收集age时,会把name也收集起来的bug
  } else {
    activeEffect = null;
  }
}

注意点4:如何实现深层次响应式?

方案:在get里判断得到的结果res是否为object,如果是则递归调用reactive。(return reactive(res));

js
let deps = new WeakMap();

/**
 * 引用类型响应式
 * @param {*} obj
 * @returns
 */
function reactive(obj) {
  return new Proxy(obj, {
    // 收集依赖
    get: function (target, key, receiver) {
      track(target, key);

      let res = Reflect.get(target, key, receiver);

++    if (typeof res == "object" && res !== null) {
++        return reactive(res); // 这里记得return
++    }

      return res;
    },
    // 触发依赖
    set: function (target, key, value, receiver) {
      Reflect.set(target, key, value, receiver);

      trigger(target, key);
    },
  });
}

注意点5:实现对原始值的响应式

方案:通过将原始值包装成一个对象(引用值),再调用reactive变成响应式,访问时通过 xxx.value 获取值。

js
/**
 * 对原始值的响应式方式
 * @param {原始值} val
 * return ReactiveObj
 */
function ref(val) {
  let wrapper = {
    value: val,
  };

  /**_isRef用于标识这是一个通过包装的引用类型 */
  Object.defineProperty(wrapper, "_isRef", { value: true });

  return reactive(wrapper);
}

完整代码

js
let activeEffect = null; // 为了记录当前effect触发的那个key应该收集的fn是谁
function effect(fn) {
  const doEffect = () => {
    activeEffect = fn; //临时记录需要收集的那个fn
    fn();
    activeEffect = null; //避免收集age时,会把name也收集起来的bug
  };

  doEffect();
}

let deps = new WeakMap();
/**
 * 引用类型响应式
 * @param {*} obj
 * @returns
 */
function reactive(obj) {
  return new Proxy(obj, {
    // 收集依赖
    get: function (target, key, receiver) {
      track(target, key);

      let res = Reflect.get(target, key, receiver);
      if (typeof res == "object" && res !== null) {
        console.log("res:", res);
        return reactive(res);
      }

      return res;
    },
    // 触发依赖
    set: function (target, key, value, receiver) {
      Reflect.set(target, key, value, receiver);

      trigger(target, key);
    },
  });
}

/**
 * 对原始值的响应式方式
 * @param {*} val
 * return ReactiveObj
 */
function ref(val) {
  let wrapper = {
    value: val,
  };

  /**_isRef用于标识这是一个通过包装的引用类型 */
  Object.defineProperty(wrapper, "_isRef", { value: true });

  return reactive(wrapper);
}

/**
 * 为对象的key收集副作用(依赖)
 * deps格式简化如下:
 * deps = <weakMap> {
 *   data: <Map> {
 *      key: <Set> [fn1, fn2]
 *   }
 * }
 * @param {*} target
 * @param {*} key
 */
function track(target, key) {
  console.log("[track key] ", key);
  // 需要向 deps.get(target).get(key).add(fn)
  if (!activeEffect) {
    return;
  }

  // 获取对象
  let depDataMap = deps.get(target);
  if (!depDataMap) {
    depDataMap = new Map();
    deps.set(target, depDataMap);
  }

  // 获取对象某个key对应的依赖项
  let depDataMapKeySet = depDataMap.get(key);
  if (!depDataMapKeySet) {
    depDataMapKeySet = new Set();
    depDataMap.set(key, depDataMapKeySet);
  }

  depDataMapKeySet.add(activeEffect);

  console.log("deps: ", deps);
}

/**
 * 触发副作用(依赖)执行
 * @param {*} target
 * @param {*} key
 */
function trigger(target, key) {
  console.log("[trigger key] ", key);
  deps
    .get(target)
    .get(key)
    .forEach((fn) => {
      fn && fn();
    });
}

DEMO

js
<!-- simpleReactive.js -->

let count = ref(0);
console.log("count ref:", count);
console.log("count.value:", count.value);
console.log("count._isRef:", count._isRef);

let data = reactive({
  name: "believer",
  age: 29,
  skills: {
    run: true,
    jump: true,
    drive: false,
  },
});

effect(() => {
  document.querySelector(
    "#reactive_res"
  ).innerText += `update [data.age] = ${data.age} \n\n`;
});

effect(() => {
  document.querySelector(
    "#reactive_res"
  ).innerText += `update [data.name] = ${data.name} \n\n`;
});

effect(() => {
  document.querySelector("#name").value = data.name;
});

effect(() => {
  document.querySelector(
    "#reactive_res"
  ).innerText += `update [data.skills.drive] = ${data.skills.drive} \n\n`;
});

effect(() => {
  document.querySelector("#count").innerText = "click times: " + count.value;
});

setTimeout(() => {
  data.age = 10;
  data.skills.drive = true;
}, 2000);

setTimeout(() => {
  data.name = "WC";
}, 3000);

// 用户操作
document.querySelector("#name").addEventListener("keyup", (e) => {
  data.name = e.target.value;
});

document.querySelector("#add").addEventListener("click", () => {
  data.age++;
  count.value++;
});

document.querySelector("#reduce").addEventListener("click", () => {
  data.age--;
  count.value++;
});
html
<!-- index.html -->

<!DOCTYPE html>
<html lang="zh">
<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>
    <h3>简易响应式实现</h3>
    <div id="count"></div>
    <hr>
    <input type="text" id="name">
    <button id="add">age++</button>
    <button id="reduce">age--</button>
    <hr>
    <div id="reactive_res"></div>
</body>
<script src="./simpleReactive.js"></script>
</html>

参考文章

深入响应式系统 | Vue.js狂肝半个月!1.3 万字深度剖析 Vue3 响应式

Released under the MIT License.