blog-frontend icon indicating copy to clipboard operation
blog-frontend copied to clipboard

@vue/reactivity源码学习

Open Caaalabash opened this issue 4 years ago • 1 comments

1.前置知识

1.1 Reflect的意义

JavaScript 语言内置的 Reflect 对象上的函数是为 Proxy 准备的,Proxy 的 handler 的各种 trap 分别对应 Reflect 上的同名方法。 在 Proxy 的 handler 中使用 Reflect 上对应的方法来实现默认行为。

function logAll(o) {
    const handler = {}
    for (const op of Object.getOwnPropertyNames(Reflect)) {
        handler[op] = (...args) => {
            try {
                return Reflect[op](...args)
            } finally {
                console.log(op)
            }
        }
    }
    return new Proxy(o, handler)
}

1.2 WeakMap

特点:

  • WeakMap的key只能是Object类型
  • WeakMap的key持有的是"弱引用",这意味在没有其他引用存在时,垃圾回收能正确进行
  • WeakMap的key是不可枚举的

1.3 如何调试

  • clone vue-next
  • 在项目根目录运行 yarn dev reactivity
  • 创建一个html文件, 然后去source面板操作
<script src="reactivity.global.js"></script>
<script>
    const { reactive, computed } = VueReactivity
    const r = reactive({ number: 1 })
    debugger
    const c = computed(() => r.number)
    console.log(c.value)
</script>

阅读源码最简单有效的办法就是调试了

2. 翻译翻译reactive({ number: 1 })

结论:reactive({ number: 1 })等价于new Proxy({ number: 1 }, mutableHandlers)

function reactive(target) {
  return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers);
}

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
  const reactiveFlag = isReadonly ? "__v_readonly" : "__v_reactive";
  const observed = new Proxy(target, baseHandlers);
  def(target, reactiveFlag, observed);
  return observed;
}

结论:mutableHandlers包含get/set/deleteProperty/has/onwKeys五个捕捉器,他们除了完成默认行为之外,还会调用trigger/track方法

function get(target, key, receiver) {
  const res = Reflect.get(target, key, receiver);
  track(target, "get" /* GET */, key);
  // Proxy只会代理对象的第一层, 如果Reflect.get的返回值是对象,则再通过reactive方法做代理,实现更深层级的响应式
  if (isObject(res)) {
    return reactive(res)
  }
  return res;
}

function set(target, key, value, receiver) {
  const oldValue = target[key];
  value = toRaw(value);
  const hadKey = hasOwn(target, key);
  const result = Reflect.set(target, key, value, receiver);
  if (!hadKey) {
    trigger(target, "add" /* ADD */, key, value);
  }
  else if (hasChanged(value, oldValue)) {
    trigger(target, "set" /* SET */, key, value, oldValue);
  }
  return result;
}

function deleteProperty(target, key) {
  const hadKey = hasOwn(target, key);
  const oldValue = target[key];
  const result = Reflect.deleteProperty(target, key);
  if (result && hadKey) {
      trigger(target, "delete" /* DELETE */, key, undefined, oldValue);
  }
  return result;
}

function has(target, key) {
  const result = Reflect.has(target, key);
  track(target, "has" /* HAS */, key);
  return result;
}

function ownKeys(target) {
  track(target, "iterate" /* ITERATE */, ITERATE_KEY);
  return Reflect.ownKeys(target);
}

const mutableHandlers = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

track/trigger的实际作用不能孤立来看,首先需要了解computed的实现。

3. 翻译翻译Computed

用过computed的人都模糊的知道:响应式数据更新了,计算属性就会更新,这里有两点需要搞明白:

  • 计算属性什么时候更新:是立刻更新,还是触发get的时候更新?
  • 如何知道函数依赖了那些响应式数据的属性?也就是收集依赖

3.1 计算属性什么时候更新:是立刻更新,还是触发get的时候更新?

computed源码如下,这里注意constructorget

// 主要处理入参,赋值给getter和setter
function computed(getterOrOptions) {
  let getter;
  let setter;
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions;
    setter = NOOP;
  }
  else {
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
  }
  return new ComputedRefImpl(getter, setter, isFunction(getterOrOptions) || !getterOrOptions.set);
}

class ComputedRefImpl {
  constructor(getter, _setter, isReadonly) {
    this._setter = _setter;
    // 众所周知,计算属性是有"缓存"的,依赖值没有改变时,当然只计算一次,_dirty可以理解为"是否需要重新计算"
    this._dirty = true;
    this.__v_isRef = true;
    // 可以简单推测出effect的作用:getter函数中依赖的响应式数据的属性更新了,就将_dirty设置为"需要重新计算"的状态
    this.effect = effect(getter, {
      lazy: true,
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true;
          trigger(toRaw(this), "set" /* SET */, 'value');
        }
      }
    });
    this["__v_isReadonly" /* IS_READONLY */] = isReadonly;
  }
  get value() {
    // 需要重新计算,从effect中拿到值,否则返回缓存
    if (this._dirty) {
      this._value = this.effect();
      this._dirty = false;
    }
    track(toRaw(this), "get" /* GET */, 'value');
    return this._value;
  }
  // 单纯的调用一下提供的setter,不重要
  set value(newValue) {
    this._setter(newValue);
  }
}

结论:计算属性不是在依赖的响应式数据变化后立即更新的,而是get value时按需更新

3.2 如何收集依赖?

承接上文,在脑海中建立一个computed => effect的映射,effect接收的第一个函数,也就是computed中的getter

function effect(fn, options = EMPTY_OBJ) {
  // 避免二次包装
  if (isEffect(fn)) {
    fn = fn.raw;
  }
  const effect = createReactiveEffect(fn, options);
  // 是否需要立即执行,从这里可见,computedRef的构造函数中初始化`effect`不会立即执行
  if (!options.lazy) {
    effect();
  }
  return effect;
}

const effectStack = [];
let activeEffect;
let uid = 0;

function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
    // 避免重复入栈
    if (!effectStack.includes(effect)) {
      // 收集依赖前,先清理一次之前effect的依赖
      cleanup(effect);
      try {
        // 当前effect入栈
        effectStack.push(effect);
        // 维护activeEffect
        activeEffect = effect;
        // computed中的getter被执行,触发了响应式数据的getter,从而执行了`track`方法,收集到依赖
        return fn();
      }
      finally {
        // 当前effect出栈
        effectStack.pop();
        // 维护activeEffect
        activeEffect = effectStack[effectStack.length - 1];
      }
    }
  };
  effect.id = uid++;
  effect._isEffect = true;
  effect.active = true;
  effect.raw = fn;
  effect.deps = [];
  effect.options = options;
  return effect;
}

结论:在执行effect函数时,最终会调用原始函数fn,此时会触发响应式数据的getter, 进而调用track函数,完成依赖收集

4. 依赖表

依赖收集的数据结构targetMap整体是一个WeakMapkey为响应式对象,valueMap<被依赖的属性,Set<ReactiveEffect>>

// The main WeakMap that stores {target -> key -> dep} connections.
// Conceptually, it's easier to think of a dependency as a Dep class
// which maintains a Set of subscribers, but we simply store them as
// raw Sets to reduce memory overhead.
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()

4.1 track函数

维护依赖表

function track(target, type, key) {
  if (!shouldTrack || activeEffect === undefined) {
    return;
  }
  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()));
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
  }
}

4.2 trigger函数

从依赖表中取出对应的effects,执行

function trigger(target, type, key, newValue, oldValue, oldTarget) {
  // 这一堆代码就是找effects罢了
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }
  const effects = new Set();
  const add = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect) {
          effects.add(effect);
        }
      });
    }
  };
  if (key !== void 0) {
    add(depsMap.get(key));
  }
  // 如果提供了调度函数,就执行调度函数
  const run = (effect) => {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect);
    }
    else {
      effect();
    }
  };
  // 全都执行一遍
  effects.forEach(run);
}

5. 整体流程回顾

  • const r = reactive({ number: 1 })
    • 创建了一个响应式数据:实际等于new Proxy(target, mutableHandlers)mutableHandlers包含get/set/ownKeys/has/deleteProperty捕获器,他们会调用track/trigger方法
  • const c = computed(() => r.number)
    • 创建了一个计算属性:实际上初始化了一个effect(() => r.number, { lazy: true, scheduler: 略 })函数, 但是并没有执行,此时c.value实际为undefined
  • console.log(c.value)
    • 首次获取计算属性的值,让effect(() => r.number, { lazy: true, scheduler: 略 })得到执行,并在最终执行() => r.number时,触发了响应式数据cgetter
    • 收集到了依赖:track(r, 'get', 'number')
    • 此时依赖表为:
      reactiveObj reactiveAttribute effects
      r number effect(() => r.number, { lazy: true, scheduler: 略 })
  • r.number = 10
    • 触发了响应式对象r的setter
    • 触发更新:trigger(r, 'set', 'number', 10, 1)
    • 从依赖表中取出对应reactiveObj, reactiveAttribute下的effects执行
    • 在这个例子中执行effect.options.scheduler函数,将computedRef_dirty属性设置为"需要更新的状态"
  • console.log(c.value)
    • 第二次获取计算属性的值,由于_dirty为"需要更新的状态",所以再次执行effect函数获取新值

Caaalabash avatar Aug 23 '20 13:08 Caaalabash

排面

wuweijia avatar Oct 14 '20 09:10 wuweijia