vue3响应式原理浅析

Vue3是新一代的Vue响应式框架,其中一项重大的改变是重写了响应式模块,使用了新版浏览器提供的Proxy代替Object.defineProperty实现响应式,本文对新版本响应式原理进行了一些分析。

简单创建一个vue3项目

使用vite工具即可快速生成一个vue3项目,并且自带热更新功能:

$ npm init vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev

我们可以先使用Composition API在App.vue里写点内容,让页面可以显示一些内容。

<template>
  <div @click="eat">{{ meta.name }} ~~~ {{ meta.weight }}</div>
</template>

<script>
import { reactive, watch } from 'vue';

export default {
  name: 'App',
  setup() {
    // 生成响应式对象
    const meta = reactive({
      type: 'electrical',
      name: 'pikachu',
      weight: 8
    });

    const eat = () => meta.weight ++;
    
    // 监听了其中一个属性的变化
    watch(() => meta.weight, (value, oldValue) => {
      console.log(`${meta.name}'s weight changed from ${oldValue} to ${value}`);
    })

    return {
      meta,
      eat
    }
  }
}

在上面的例子中我们一共做了三个步骤,第一步是使用reactive函数将一个对象变成响应式的,第二步通过watch函数监听其中一个属性的变化,最后在模板的根元素绑定上改变该属性的方法。如果项目正常运行,每次点击显示出来的文本,weight应该都会增加1。下面我们一步步分析源码看看到底发生了什么。

生成响应式对象

调用reactive函数会生成一个响应式对象,reactive函数内部其实调用的是createReactiveObject方法,先看看这个函数。

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  ...
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // proxyMap是一个WeakMap,用于收集 <object>: <proxy> 的映射关系
  // 后面就可以方便地使用 proxyMap.get(object) 找到对象对应的proxy
  proxyMap.set(target, proxy)
  return proxy
}

针对我们这里提供的对象,createReactiveObject新建了一个Proxy对象,使用baseHandlers作为ProxyHandler,在proxyMap这个WeakMap中加入<原本的对象>: <proxy>这一组映射,并返回创建的Proxy实例。我们可以猜到这个baseHandlers里一定是劫持了对象的某些操作(毕竟这才是Proxy存在的意义嘛),至于究竟劫持了啥后面会提到。这里需要注意的是仅生成了一个proxy,而没有针对原始对象的对象属性值递归生成proxy(对象属性值也可能是个对象),这一点和Vue2有较大的区别 —— Vue2在通过原始对象得到响应式对象时会进行递归处理,一个层级很深的原始对象经过递归的响应式处理的消耗较大,Vue3显然针对此进行了优化,createReactiveObject仅仅将第一层变成响应式的,更深层次的响应式则会之后再处理。

调用watch函数

接下来是调用watch函数观察对象的变化,watch函数内部调用的其实是doWatch函数:

function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ,
  instance = currentInstance
): WatchStopHandle {

  let getter: () => any
  ... <source参数也支持直接是一个Reactive的对象,这里将对它们的取值处理成一个函数,可以先跳过>
  } else if (isFunction(source)) {
    if (cb) {
      // getter with cb
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } 
  }
  
  let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
  ...

  const runner = effect(getter, {
    lazy: true,
    onTrack,
    onTrigger,
    scheduler
  })
  ...

  // initial run
  if (cb) {
    if (immediate) {
      job()
    } else {
      oldValue = runner()
    }
  } 
  ...
}

从上面删减后的代码中可以看出,doWatch函数内部先是生成一个getter函数,这个函数调用的callWithErrorHandling函数也仅仅是调用了source方法(我们在watch中提供的第一个参数),并收集一些报错信息。

针对我们上面写的例程,程序会直接执行到oldValue = runner()这一行,runner是调用effect函数生成的, effect函数内部实际调用的是createReactiveEffect

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    ...
    if (!effectStack.includes(effect)) {
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  ...// 设置 effect 的一些属性
  effect.options = options
  return effect
}

createReactiveEffect返回一个函数(我们姑且将它记为ReactiveEffect函数),该函数执行的时候先设置activeEffect为自身,然后返回fn(),这里的fn就是runner提供的getter函数,也即执行我们调用watch函数的第一个参数,即() => meta.weight

在调用上面的取值函数时,meta是经reactive处理得到的proxy,访问它的属性值的时候将触发Proxy绑定的getHandler:

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    ...
    // 从原始对象上取出对应key的值
    const res = Reflect.get(target, key, receiver)
    ...
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    ...
    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }
    return res
  }
}

getHandler首先从原始对象上取出对应key的值res,然后调用了track函数,紧接着判断如果属性值res是对象,则调用reactive将其变成响应式的,最后返回了原始对象对应key的值。此处可以发现Vue3把原始对象被访问到的对象属性值变成响应式的时机是在访问该值之后,而不同于上述Vue2在初始化的时候就一把梭把所有对象属性都变成响应式。这是一种懒操作的优化方式,一方面不需要响应式的属性值不进行处理,另一方面在访问到特定属性值的时候再进行响应式处理,这对于多层级的数据模型是很受用的,能提高整体的运行速度。下面来看看track函数:

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  // targetMap是一个<object: map>的WeakMap
  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)
    ...
  }
}

这段代码中出现的activeEffect在上文已经提到过,是runner生成的ReactiveEffect函数,它的作用简单来说就是使用我们在watch中传递的第一个参数值取得原始对象上的属性值。targetMap是一个WeakMap,它是<原始对象: Map>的映射,其中的Map又是<原始对象的key: Set<ReactiveEffect>>的映射,这个名为dep的Set中保存了activeEffect,然后又在activeEffect的deps数组中保存了自己。熟悉Vue2的同学这里肯定不陌生了,Vue2中是通过Sub和Dep两个class去处理绑定关系的,而在Vue3中经过了简化,通过特定的数据结构就解决了问题。至于这里把dep和activeEffect进行绑定的意义,下面触发更新的时候就很重要。

触发更新

当点击文本触发eat事件,导致meta.weight ++的时候,会发生什么呢?这里变更了meta的属性值,而meta是一个Proxy,设置它的值将触发setHandler:

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    ...
    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, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

setHandler首先通过Reflect.set设置了原始对象的属性值,然后调用了trigger函数,它负责触发一些响应式操作:

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  const effects = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }

  if (type === TriggerOpTypes.CLEAR) {
    ...
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      // 从depsMap中拿到所有ReactiveEffect并加入effects这个Set中
      add(depsMap.get(key))
    }
    ...
  }

  const run = (effect: ReactiveEffect) => {
    ...
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  effects.forEach(run)
}

代码中的targetMap和depsMap在上文都出现过了,depsMap中保存有<原始对象的key: Set<ReactiveEffect>>的映射,这个函数关键的逻辑是add(depsMap.get(key))这一句,它从depsMap中拿到所有ReactiveEffect并加入effects这个Set中,然后针对每个ReactiveEffect运行了run函数。run函数并不是简单地直接执行ReactiveEffect,如果ReactiveEffect的options中提供了scheduler,则使用这个方法调用effect。scheduler其实是在doWatch函数中出现的,现在回过头看看它。

function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ,
  instance = currentInstance
): WatchStopHandle {

  let getter: () => any
  ... <source参数也支持直接是一个Reactive的对象,这里将对它们的取值处理成一个函数,可以先跳过>
  } else if (isFunction(source)) {
    if (cb) {
      // getter with cb
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } 
  }
  ...

  let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
  
  // job的逻辑触发更新的时候再看
  const job: SchedulerJob = () => {
    if (!runner.active) {
      return
    }
    if (cb) {
      // 先运行runner得到最新的值
      const newValue = runner()
      // 比较了新旧两个值,如果发生了变化,调用回调cb
      if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
        ...
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          // pass undefined as the old value when it's changed for the first time
          oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
          onInvalidate
        ])
        oldValue = newValue
      }
    }
    ...
  }
  ...
  // scheduler的逻辑触发更新的时候再看
  let scheduler: ReactiveEffectOptions['scheduler']
  if (flush === 'sync') {
    scheduler = job
  } else if (flush === 'post') {
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    // default: 'pre'
    scheduler = () => {
      if (!instance || instance.isMounted) {
        queuePreFlushCb(job)
      } else {
        // with 'pre' option, the first call must happen before
        // the component is mounted so it is called synchronously.
        job()
      }
    }
  }

  const runner = effect(getter, {
    lazy: true,
    onTrack,
    onTrigger,
    scheduler
  })
  ...

  // initial run
  if (cb) {
    if (immediate) {
      job()
    } else {
      oldValue = runner()
    }
  } 
  ...
}

首先看下job函数,它里面的cb其实就是我们在调用watch的时候提供的第二个参数,在例程中就是

(value, oldValue) => {
  console.log(`${meta.name}'s weight changed from ${oldValue} to ${value}`);
})

在提供了cb的情况下,job函数先运行runner()拿到最新的值(执行watch的第一个参数,详见前文对runner的介绍),然后比较了新旧两个值,如果发生了变化,则调用cb。

scheduler函数决定着调用job的时机,它的值根据flush选项分为三种,因为我们没有提供flush选项,默认值是'pre', 直接进入到else分支里面的逻辑,由于我们的组件已经mount了,需要执行queuePreFlushCb(job)

在例程中我们通过点击文本触发eat事件,对meta.weight的值进行变化发生在主线程,而对meta.weight值变化进行响应式处理的job函数并非在主线程执行,而是通过某种调度机制延后执行。queuePreFlushCb就是一个事件调度相关的函数,它内部使用了Promise.then的调度方式将待执行的job放到微任务中。因为本文篇幅优先,就不展开讲解事件调度相关的内容了。至此,job在微任务中得到执行,从而触发我们提供的cb函数,实现了响应式。
逻辑图示

简易实现

下面是一个简单的实现,注释中的五个步骤显示了实现的过程。没有处理包括添加属性和删除属性的响应式处理,scheduler也仅仅利用了最原始的同步任务来做,以后再专门开一文详细说说调度的内容吧。

const proxyMap = new WeakMap();
const targetMap = new WeakMap();
const handlers = {
  get(target, prop) {
    // 3. get handler 的实现
    track(target, prop);
    return Reflect.get(target, prop);
  },
  set(target, prop, value) {
    // 4. set handler 的实现
    const oldValue = Reflect.get(target, prop);
    const result = Reflect.set(target, prop, value);
    if (hasChanged(value, oldValue)) {
      trigger(target, prop, value, oldValue);
    }
    return result;
  }
}
let activeEffect = null;

const track = (obj, key) => {
  let depMap = targetMap.get(obj);
  if (!depMap) {
    targetMap.set(obj, (depMap = new Map()));
  }

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

  if (! depsSet.has(activeEffect)) {
    depsSet.add(activeEffect);
  }
}

const hasChanged = (newValue, oldValue) => {
  return newValue !== oldValue;
}

const trigger = (target, key, value, oldValue) => {
  const depsMap = targetMap.get(target);
  if (depsMap) {
    const depsSet = depsMap.get(key);
    if (depsSet) {
      depsSet.forEach(effect => {
        typeof effect.scheduler === 'function' && effect.scheduler();
      })
    }
  }
}

// 1. reactive的实现: 创建一个Proxy,将对象变成响应式
export const reactive = (obj) => {
  if (obj && typeof obj === 'object') {
    const p = new Proxy(obj, handlers);
    proxyMap.set(obj, p);
    return p;
  }
}

export const watch = (source, cb) => {
  let oldValue;
  // 5. 通过 effect.scheduler 执行 job
  const job = () => {
    const newValue = source();
    if (hasChanged(newValue, oldValue)) {
      cb(newValue, oldValue);
      oldValue = newValue;
    }
  }

  // 2. watch的初始化,获取初值
  const effect = (fn) => () => {
    activeEffect = effect;
    return source();
  }
  effect.scheduler = job;

  const runner = effect(source);
  if (cb) {
    oldValue = runner();
  }
}

使用一些代码测试,可以得到预期效果:

import { reactive, watch } from './index';

const demoObject = {
  name: 'kom',
  age: 8
}

const reactiveObj = reactive(demoObject);

watch(() => reactiveObj.age, (newValue, oldValue) => {
  console.log(newValue, oldValue);
})

watch(() => reactiveObj.name, (newValue, oldValue) => {
  console.log(newValue, oldValue);
})

reactiveObj.age ++; // 打印 9 8
reactiveObj.name = 'tim'; // 打印 tim kom
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值