深入浅出Vue的响应性(上篇)

今天我们有个艰巨的挑战,那就是阅读Vue的reactive源码,一篇写完太长了,我想要长痛分成多次短痛,这样感受起来就像无痛(手动doge),只有先读的懂了,我们才能下手模仿;加油吧宝子们!!PS:本文中的代码都是简单版,多余的已被剪枝,更加简单清晰易懂; 首先我们用的例子🌰还是上上上篇那个,这段代码是我们经常用到的reactive和effect的方法,今天我们就一起来深入挖掘Vue到底做了什么事;

js 复制代码
<script>
    const { reactive, effect } = Vue;
    const obj = reactive({
      name: "张三",
    });
    console.log(obj);

    effect(() => {
      document.querySelector("#app").innerText = obj.name;
    });
    setTimeout(() => {
      obj.name = "李四";
    }, 2000);
  </script>

这是我们即将介绍的方法流程图,对照着看更容易理清思路;

首先来看reactive方法;

js 复制代码
export function reactive(target: object) {
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

当前reactive方法里传入的是{name: "张三"}对象;接下来看一下createReactiveObject方法;

js 复制代码
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // 读取proxyMap的实例,此时的proxyMap是一个weakMap{}
  const existingProxy = proxyMap.get(target)
  // targetType为1
  const targetType = getTargetType(target)
  // 重点代码
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // 此时target和proxy存入proxyMap,下次target存在直接用
  proxyMap.set(target, proxy)
  return proxy
}

createReactiveObject传入的target依然是{name: "张三"}对象;

第13行Proxy第一个参数传入target作为被代理对象;第二个参数传入先判断targetType,此时targetType不等于TargetType.COLLECTION,因此会用baseHandlers;,new Proxy生成一个proxy,然后被return回来;reactive函数结束之后会进入effect方法;

我们来看一下effect方法都做了什么事;

js 复制代码
export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  // ReactiveEffect是一个class,有run和stop方法
  const _effect = new ReactiveEffect(fn)
  if (!options || !options.lazy) {
    _effect.run()
  }
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

effect第一个参数fn是我们传入的() => { document.querySelector("#app").innerText = obj.name; }

ReactiveEffect的run方法:

js 复制代码
run() {
    try {
      // this是当前ReactiveEffect的实例,它的parent是activeEffect
      // 此时activeEffect是undefined
      this.parent = activeEffect
      activeEffect = this
      shouldTrack = true
      // 执行我们传入的方法,document.querySelector("#app").innerText = obj.name;
      return this.fn()
    }
  }

第6行的activeEffect被赋值,此时它的fn为下图

接着会进入到baseHandlers里有get方法叫createGetter;此时get方法里的target还是{name: "张三"}对象,key是name,receiver: Proxy(Object) {name: "张三"}; Vue响应性的核心是依赖收集和依赖触发;track方法就是依赖收集的过程;

js 复制代码
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // target是对象不是数组
    const targetIsArray = isArray(target)
    // res的值是张三
    const res = Reflect.get(target, key, receiver)

    if (!isReadonly) {
      // track方法非常重要!
      track(target, TrackOpTypes.GET, key)
    }
    return res
  }
}

接着来看下核心的track方法:

js 复制代码
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) {
    // 此时的targetMap是WeakMap{}
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      // 为targetMap赋值
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = createDep()))
    }
    trackEffects(dep, eventInfo)
  }
}

第2行的activeEffect就是上文的;

第7行为targetMap赋值之后就变成了:

这里的targeMap比较复杂,是下图一个结构:

最后执行trackEffects方法:

js 复制代码
export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  if (shouldTrack) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
  }
}

第7行的dep是这样的,和targetMap对比一下,会发现dep和和targetMap的value中第一个元素一样的,他们是指向同一个内存空间;

第8行里targetMap就保存了一个ReactiveEffect;这时已经完成了依赖收集的过程;依赖收集的本质上就是建立起targetMap和ReactiveEffect的关联;此时我们就可以在targetMap中用key来获取到ReactiveEffect;ReactiveEffect中存在一个fn函数,它就是我们传入的() => { document.querySelector("#app").innerText = obj.name; }方法;

track方法执行完了,再回到createGetter方法:

js 复制代码
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // target是对象不是数组
    const targetIsArray = isArray(target)
    // res的值是张三
    const res = Reflect.get(target, key, receiver)

    if (!isReadonly) {
      // track方法非常重要!
      track(target, TrackOpTypes.GET, key)
    }

    return res
  }
}

第13行return res,这时的res就是张三,这里也就是obj.name得到了张三

到这里就完成了effect的处理逻辑;

让我们总结一下effect的过程:

  1. 生成ReactiveEffect实例;
  2. 触发fn方法,从而激活getter;
  3. 建立了targetMap和activeEffect之间的联系;
相关推荐
格子软件1 小时前
2026年GEO优化系统源码级状态机与多模型调度拆解
java·前端·vue.js·人工智能·vue·geo
HUMHSX2 小时前
Vue 项目启动全流程解析:从入口文件到全局指令注册与页面渲染
前端·javascript·vue.js
有颜有货2 小时前
PMC生产排产的4种算法,一次讲清
java·服务器·前端
小虎牙0072 小时前
Android kotlin图片库Coil源码详解
android·前端
随风一样自由2 小时前
【前端领域】前端开发核心应用场景与落地实践
前端·前端框架
an317422 小时前
弹窗数据流设计的两种高阶架构实践
前端·vue.js·架构
谢尔登3 小时前
【React】 状态管理方案
前端·react.js·前端框架
用户2136610035723 小时前
Vue商品详情与放大镜组件
前端·javascript
半个落月3 小时前
从Tapas小Demo理清localStorage、事件与this
前端·javascript
李明卫杭州3 小时前
Vue2 中 v-model 处理不同数据结构的技巧
前端·javascript·vue.js