深入浅出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 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch3 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光3 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   3 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   3 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web3 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常3 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇4 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr4 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho5 小时前
【TypeScript】知识点梳理(三)
前端·typescript