目标

vue3响应式源码解析

Vue3的数据响应与劫持是基于现代浏览器所支持的代理对象Proxy实现的。

下面的例子简单说明vue3的响应式数据的原理,即通过Proxy对象分别对getset劫持,在 取值和赋值中间 分别插⼊劫持的⽅法,即 track 和 trigger ——依赖的跟踪和副作⽤的触发。

const initData = {value: 1}

const proxy = new Proxy(
    initData, // 被代理对象
    {
        // handler对象
        get(target, key) {
            // 进行 track
            return target[key]
        }

        set(target, key, value) {
            // 进行trigger
            return Reflect.set(target, key, value);
        }
    }
)

// proxy 即 直接访问修改对象
// 也可以称为响应式数据(reactive/ref)

基本说明

track: 收集依赖
trigger: 触发副作用函数

代码分析

通过下面的vue3实例代码运行,逐步分析源码。

// setup
import {ref, reactive, effect, computed} from "vue"

export default {
    ...
    setup(props, context) {
        const countRef = 0;
        const number = reactive({ num: 1 })
        effect(() => {
            console.log(countRef.value)
        })
        const increment = () => {
            countRef.value++
        }
        
        const result = computed(() => number.num ** 2)
        return { countRef, number, result, increment }
    }
}

先用流程图简单的描述下上面代码的执行过程:

image-20220312171242884

组件初始化,首先声明countRef变量与执行effect函数。effect会调用传入的参数函数fnfn函数执行到countRef.value会被getter劫持,进行track。在这个过程中会有一个全局变量targetMap,将countReffn建立关联关系。当countRef的值发生变化时,会触发以下过程:

image-20220312171428339

countRef的值发生变化时,会被setter劫持,进行trigger。简单来讲就是从全局变量targetMap来获取countRef对应的fn,然后执行fn函数的过程,当然这里省略了许多细节。

初始化阶段

  • 创建响应式数据
const countRef = ref(0)
const number = reactive({num: 1})
function ref(value) {
  return createRef(value, false);
}

function createRef(rawValue, shallow) {
   // 判断是否为Ref类型对象,如果是则直接返回
  if (isRef(rawValue)) {
    return rawValue;
  }
  return new RefImpl(rawValue, shallow);
}

createRef函数判断传入参数rawValue是否为引用类型,如果不是,则返回RefImpl类包装的实例。

  • RefImpl
class RefImpl {
  constructor(value, _shallow) {
    this._shallow = _shallow;
    this.dep = undefined;
    this.__v_isRef = true;
    this._rawValue = _shallow ? value : toRaw(value);
    this._value = _shallow ? value : convert(value);
  }
  get value() {
    trackRefValue(this); // track,进行依赖收集
    return this._value;
  }
  set value(newVal) {
    newVal = this._shallow ? newVal : toRaw(newVal);
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      this._value = this._shallow ? newVal : convert(newVal);
      triggerRefValue(this, newVal); // trigger,值更改时触发更新
    }
  }
}

RefImpl类,也是通过get/set来实现tracktrigger。这也解释了ref的包装值--通过value属性进行访问。基于同样思路来看reactive方法

function reactive(target) {
  // if trying to observe a readonly proxy, return the readonly version.
  // 如果目标对象的__v_isReadonly属性为true,直接返回原对象
  if (target && target["__v_isReadonly" /* IS_READONLY */ ]) {
    return target;
  }
  return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}

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

const mutableCollectionHandlers = {
  get: /*#__PURE__*/ createInstrumentationGetter(false, false)
};

// 全局map
const reactiveMap = new WeakMap();

// 主逻辑
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
  if (!isObject(target)) {
    {
      console.warn(`value cannot be made reactive: ${String(target)}`);
    }
    return target;
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (target["__v_raw" /* RAW */ ] &&
    !(isReadonly && target["__v_isReactive" /* IS_REACTIVE */ ])) {
    return target;
  }
  // 以下代码逻辑为重点:
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }
  // only a whitelist of value types can be observed.
  const targetType = getTargetType(target);
  if (targetType === 0 /* INVALID */ ) {
    return target;
  }
  const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
  proxyMap.set(target, proxy);
  return proxy;
}

createReactive方法是主逻辑,⽽且创建浅层响应的⽅法 shallowReactive ,只读⽅法 readonly 都⽤到该函数。

这⾥能看到,target: { num: 1 } 在此处被代理。如果之前已经被代理过( proxyMap 中有缓存),则直接返 回,否则缓存起来并返回。reactive ⽅法使⽤了 Proxy 来实现代理。

数据追踪

effect执行,并调用回调方法fn,由于fn内部访问了countRefvalue属性

effect(() =>{
    console.log(countRef.value)
})

这里触发了类RefImpl定义的get方法

class RefImpl {
    get value() {
        trackRefValue(this);
        return this._value;
    }    
}

// 这⾥有条件地使⽤ trackEffects 维护着 ref 实例属性 dep 与
// 活跃的effet的映射,说人话就是:包装的数据在第一次被effect内
// fn函数访问时,包装对象顺便把这个fn也存了下来
function trackRefValue(ref) {
  if (isTracking()) {
    ref = toRaw(ref);
    // 如果是第一次访问,dep属性为undefined,则创建dep属性收集依赖
    if (!ref.dep) {
      ref.dep = createDep();
    } {
      // 重点:触发副作用函数  
      trackEffects(ref.dep, {
        target: ref,
        type: "get" /* GET */ ,
        key: 'value'
      });
    }
  }
}

function trackEffects(dep, debuggerEventExtraInfo) {
  let shouldTrack = false;
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      dep.n |= trackOpBit; // set newly tracked
      shouldTrack = !wasTracked(dep);
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect);
  }
   //  activeEffect 是全局变量,在执⾏ effect 时会指向⼀个包含了 fn 的实例。
   //  换句话说,此处 dep.add(activeEffect)
   //   等效于 ref.dep.add(wrapper(fn)),wrapper 是过程的简化
  if (shouldTrack) {
    dep.add(activeEffect); // 做个标记 coordinate1
    activeEffect.deps.push(dep);
    if (activeEffect.onTrack) {
      activeEffect.onTrack(
        Object.assign(
          {
            effect: activeEffect,
          },
          debuggerEventExtraInfo
        )
      );
    }
  }
}

状态更新阶段

以 ref 创建的数据源为例, countRef.value++ 从下⾯开始

class RefImpl {
  set value(newVal) {
    newVal = this._shallow ? newVal : toRaw(newVal);
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      this._value = this._shallow ? newVal : convert(newVal);
      triggerRefValue(this, newVal); // 值得更新触发trigger
    }
  }
}

function triggerRefValue(ref, newVal) {
  ref = toRaw(ref);
  // 如果该ref没有dep属性,说明之前没有触发依赖收集。
  if (ref.dep) { // 回到上⾯标记的地⽅ coordinate1
    {
      triggerEffects(ref.dep, {
        target: ref,
        type: "set" /* SET */,
        key: "value",
        newValue: newVal,
      });
    }
  }
}

标记的位置证明包装值 ref(0) 通过 dep 对未来要执⾏的 fn 是存在引⽤关系的,⽽ triggerEffect ⽅法就 根据这个存在的关系,⼀旦 set 时就触发它!

function triggerEffects(dep, debuggerEventExtraInfo) {
  // spread into array for stabilization
  for (const effect of isArray(dep) ? dep : [...dep]) {
    if (effect !== activeEffect || effect.allowRecurse) {
      if (effect.onTrigger) {
        effect.onTrigger(extend({ effect }, debuggerEventExtraInfo));
      }
      if (effect.scheduler) {
        effect.scheduler(); // 这是 fn 在微任务队列中执⾏的地⽅
      } else {
        effect.run(); // 这是 fn 同步执⾏的地⽅
      }
    }
  }
}

在数据跟踪阶段,通过activeEffect把ref的dep属性将执行的fn关联起来,再到状态更新阶段,重新遍历执行ref的def关联的fn. 缕清主线后,再稍微关注⼀下 effect 的逻辑,就能把 scheduler, run 与 fn 联 系起来了:

function effect(fn, options) {
  if (fn.effect) {
    fn = fn.effect.fn;
  }
  const _effect = new ReactiveEffect(fn);
  if (options) {
    extend(_effect, options);
    if (options.scope) recordEffectScope(_effect, options.scope);
  }
  // effect(fn)首次执行
  if (!options || !options.lazy) {
    _effect.run();
  }
  const runner = _effect.run.bind(_effect);
  runner.effect = _effect;
  return runner;
}

这里传入的fn函数参数,被ReactiveEffect类包装成实例

const effectStack = [];

class ReactiveEffect {
  constructor(fn, scheduler = null, scope) {
    this.fn = fn;
    this.scheduler = scheduler;
    this.active = true;
    this.deps = [];
    recordEffectScope(this, scope);
  }
  run() {
    if (!this.active) {
      return this.fn();
    }
    if (!effectStack.includes(this)) { // 实例不在栈中时执行,避免重复触发执行fn
      try {
        effectStack.push((activeEffect = this));
        enableTracking();
        trackOpBit = 1 << ++effectTrackDepth;
        if (effectTrackDepth <= maxMarkerBits) {
          initDepMarkers(this);
        } else {
          cleanupEffect(this);
        }
        return this.fn();
      } finally {
        if (effectTrackDepth <= maxMarkerBits) {
          finalizeDepMarkers(this);
        }
        trackOpBit = 1 << --effectTrackDepth;
        resetTracking();
        effectStack.pop();
        const n = effectStack.length;
        activeEffect = n > 0 ? effectStack[n - 1] : undefined;
      }
    }
  }
  stop() {
    if (this.active) {
      cleanupEffect(this);
      if (this.onStop) {
        this.onStop();
      }
      this.active = false;
    }
  }
}

上⾯的 ref ⽅法创建数据与更新的⼀整套流程,其实 reactive 创建的数据,也有类似 的逻辑,区别就在于 Proxyhandler 部分:

const proxy = new Proxy(
    target,
    targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers
  );

baseHandlers 为例(这⾥是形参),找到实参 mutableHandlers

const mutableHandlers = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys,
};
// 我们可以断定,这⾥的 get/set 就是进⾏ track 和 trigger 的地⽅。找到它
const get = /*#__PURE__*/ createGetter();

function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    if (key === "__v_isReactive" /* IS_REACTIVE */) {
      return !isReadonly;
    } else if (key === "__v_isReadonly" /* IS_READONLY */) {
      return isReadonly;
    } else if (
      key === "__v_raw" /* RAW */ &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target;
    }
    const targetIsArray = isArray(target);
    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
       // arrayInstrumentations 内也有 track,不再展示,关注主线
      return Reflect.get(arrayInstrumentations, key, receiver);
    }
    const res = Reflect.get(target, key, receiver);
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res;
    }
    if (!isReadonly) {
      track(target, "get" /* GET */, key); // 出现了与 ref 拦截⼀样的逻辑
    }
    ...
    return res;
  };
}

function track(target, type, key) {
  if (!isTracking()) {
    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 = createDep()));
  }
  const eventInfo = { effect: activeEffect, target, type, key };
  trackEffects(dep, eventInfo); // 与 trackRefValue 殊途同归,略
}
  • set
const set = /*#__PURE__*/ createSetter()

function createSetter(shallow = false) {
  return function set(target, key, value, receiver) {
    let oldValue = target[key];
    if (!shallow) {
      value = toRaw(value);
      oldValue = toRaw(oldValue);
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value;
        return true;
      }
    }
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key);
    const result = Reflect.set(target, key, value, receiver);
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, "add" /* ADD */, key, value); // 与ref一样触发trigger
      } else if (hasChanged(value, oldValue)) {
        trigger(target, "set" /* SET */, key, value, oldValue);
      }
    }
    return result;
  };
}


function trigger(target, type, key, newValue, oldValue, oldTarget) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    // never been tracked
    return;
  }
  let deps = [];
  if (type === "clear" /* CLEAR */) {
    // collection being cleared
    // trigger all effects for target
    deps = [...depsMap.values()];
  } else if (key === "length" && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === "length" || key >= newValue) {
        deps.push(dep);
      }
    });
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      deps.push(depsMap.get(key));
    }
    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case "add" /* ADD */:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY));
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          deps.push(depsMap.get("length"));
        }
        break;
      case "delete" /* DELETE */:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY));
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
          }
        }
        break;
      case "set" /* SET */:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY));
        }
        break;
    }
  }
    
    
function trigger(target, type, key, newValue, oldValue, oldTarget) {
  ...
  const eventInfo = { target, type, key, newValue, oldValue, oldTarget };
  if (deps.length === 1) {
    if (deps[0]) {
      {
        triggerEffects(deps[0], eventInfo); // 与 triggerRefValue 殊途同归,略
      }
    }
  } else {
    const effects = [];
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep);
      }
    }
    {
      triggerEffects(createDep(effects), eventInfo);
    }
  }
}

其实 watch ⽅法,也是基于 effect 做的封装,不再赘述。

参考文章

Proxy


看见了
876 声望16 粉丝

前端开发,略懂后台;


下一篇 »
diff