近期有空看了下 vue3 源码和相关书籍,同时将笔记同步到了博客。后面忘了可以再看看。为了理清核心主线逻辑,代码均移除了次要&边界逻辑。

一. 基本实现

设定基本场景,单击 add 按钮后,counter.num值变化,触发 effect 内匿名函数执行,达到响应式效果;

<button onclick="add()">add</button>
<div id="value"></div>
<script>
  // 1.获取响应式对象
  const counter = reactive({ num: 0 });
  // 2.副作用函数
  effect(() => {
    console.log("effect", counter);
    document.getElementById("value").innerText = counter.num;
  });
  // 3.触发
  let i = 1;
  window.add = () => {
    counter.num = i++;
  };
</script>

在线演示

See the Pen vue-reactivity-1 by 唐鸽 (@tggcs) on CodePen.

源码拆分

  • 定义当前活跃副作用函数、依赖收集桶;
// 全局变量
const targetMap = new WeakMap();
let activeEffect;
  • 实现依赖收集、触发,即track,trigger;

vue3 用 proxy 代理,最基本的就是在 get,set 内设置收集和触发的机制;

// 实现响应式对象,拦截get,set,加入track,trigger
function track(target, type, key) {
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }

  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }

  dep.add(activeEffect);

  console.log("track", targetMap);
}

function trigger(target, type, key, newValue) {
  const depsMap = targetMap.get(target);

  console.log("\n trigger", target, type, key, newValue);
  depsMap.get(key).forEach((effect) => {
    effect.run();
  });
}

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      track(target, "get", key);
      return res;
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver);
      trigger(target, "set", key, value);
      return res;
    },
  });
}
  • 绑定&触发副作用函数;
class ReactiveEffect {
  fn;
  constructor(fn) {
    this.fn = fn;
  }
  run() {
    activeEffect = this;
    return this.fn();
  }
}

function effect(fn) {
  const _effect = new ReactiveEffect(fn);
  _effect.run();
}

bucket 桶结构

依赖收集桶结构&deps 绑定

二.嵌套的 effect

设定基本场景,单击 add 按钮后,counter.num1值变化, 但打印如下;

<button onclick="add()">add</button>
<script>
  const counter = reactive({ num1: 0, num2: 0 });
  effect(() => {
    effect(() => {
      console.log(`num2:${counter.num2}`);
    });
    console.log(`num1:${counter.num1}`);
  });
  console.log(targetMap);
  let i = 1;
  window.add = () => {
    counter.num1 = i++;
  };
</script>
num2: 0;

parent 变量解决嵌套

没有见到num1:1的打印,是因为依赖收集时,activeEffect是内层副作用函数(且由 set 做了去重),按 vue3 设计,加一个 parent 变量即可;

...
run() {
  try {
    // parent记录下嵌套的父级activeEffect
    this.parent = activeEffect;
    activeEffect = this;
    return this.fn();
  } finally {
    activeEffect = this.parent;
    this.parent = undefined;
  }
}
...

在线演示

See the Pen vue-reactivity-2 by 唐鸽 (@tggcs) on CodePen.

三.依赖清理

改用二进制标记位的方式,较之前全部清除再收集的方式有更良好的性能(仅删除不需要的副作用函数);

设定基本场景,单击 add 按钮后,counter.visible值切换,visible===false时,targetMap 桶内counter.num2的 dep 集合应该清空;

<button onclick="toggle()">toggle</button>
<script>
  const counter = reactive({ visible: true, num2: 0 });

  effect(() => {
    if (counter.visible) {
      console.log("num2", counter.num2);
    }
  });

  let visible = true;
  function toggle() {
    visible = !visible;
    counter.visible = visible;
  }
</script>

源码拆分

  • 添加三个变量,用于控制
let effectTrackDepth = 0; // 当前所在副作用函数层级
let trackOpBit = 1; // 所在层级的二进制标记
const maxMarkerBits = 30; // 设定effect最大嵌套30层
  • 通过已被收集新的依赖两个标记来筛选剔除无效副作用函数

副作用函数执行前,执行initDepMarkers,将存在deps打上已被收集标记;副作用函数执行前,执行finalizeDepMarkers,将effect已被收集 && 非新的依赖 删除掉;

initDepMarkers() {
  ...
  deps[i].w |= trackOpBit // set was tracked
}
finalizeDepMarkers() {
  ...
  if (wasTracked(dep) && !newTracked(dep)) {
    dep.delete(effect)
  }
}
run() {
  try {
    ...
    if (effectTrackDepth <= maxMarkerBits) {
      initDepMarkers(this)
    }
    return this.fn();
  } finally {
    if (effectTrackDepth <= maxMarkerBits) {
      finalizeDepMarkers(this)
    }
    ...
  }
}

See the Pen vue-reactivity-2 by 唐鸽 (@tggcs) on CodePen.

  • 位操作

参考 权限设计应用

四.Ref

为了实现原始值的响应式,vue3提供了 ref api; 设定如下场景

const a = ref(1)
effect(() => {
  console.log('effect', a.value)
})
a.value = 2

源码拆分

通过返回新实例,并劫持其value属性的getter&setter即可;并加入tracker\&trigger;

function ref(value) {
  return new RefImpl(value)
}

class RefImpl {
  _value
  constructor(value) {
    this._value = (value)
  }

  get value() {
    trackRefValue(this)
    return this._value
  }
  set value(newVal) {
    this._value = newVal
    triggerRefValue(this, newVal)
  }
}

在线演示

See the Pen vue-reactivity-3 by 唐鸽 (@tggcs) on CodePen.

五.readonly & shallowReadonly

六.shallowReactive

七.computed

八.watch