Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue3 响应式原理 #55

Open
lq782655835 opened this issue Sep 16, 2020 · 0 comments
Open

Vue3 响应式原理 #55

lq782655835 opened this issue Sep 16, 2020 · 0 comments
Labels

Comments

@lq782655835
Copy link
Owner

Vue3 响应式原理 - Ref/Reactive/Effect源码分析

目前vue3还没完全稳定下来,许多rfcs都有变化的可能。本文基于目前最新(2019-11-07)fork的 vue源码进行原理分析。官方提供了在Vue2.x尝试最新Vue3功能的插件库:Vue Composition API (以前该库叫vue-function-api,现在叫composition-api)。

众所周知,Vue3使用ES6 Proxy替代ES5 Object.defineProperty实现数据响应式,这也是Vue最为核心的功能之一。Vue3相比Vue2.x,API变化很大,提出了Vue Composition API。但在响应式原理实现方面,源码依然还是依赖收集 + 执行回调,只不过api变化后,形式也有点变化。想了解vue 2.x实现方式,可以看下笔者以前写的 Vue2.x源码分析 - 响应式原理

你必须知道的Vue3 RFCS ChangeLog

如果较少关注vue3征求意见稿vue rfcs,可能大部分人对vue3还停留在Vue Function API。作者尤雨溪专门为这重大改变的API做过详细的叙述,并特意翻译了中文Vue Function-based API RFC。目前Vue 官方发布了最新的3.0 API 修改草案,并在充分采纳社区的意见后,将Vue Function API 更正为 Vue Composition API.

1. 重大变化点

  1. state更名为reactive
    • reactive等价于 Vue 2.x 的Vue.observable()
  2. value更名为ref,并提供isRef和toRefs
    • 使用ref来创建包装对象进行传递
  3. computed可传入get和set,用于定义可更改的计算属性
  4. effect 更名为 watchEffect

Vue官方团队建议在组合函数中都通过返回ref对象。

2. 了解Vue Composition API

import { reactive, computed, toRefs, watchEffect } from "vue";
export default {
  setup() {
    const event = reactive({
      capacity: 4,
      attending: ["Tim", "Bob", "Joe"],
      spacesLeft: computed(() => { return event.capacity - event.attending.length; })
    });
    watchEffect(() => console.log(event.capacity))
    function increaseCapacity() {
      event.capacity++;
    }
    return { ...toRefs(event), increaseCapacity };
  }
};

3. 建议阅读资料

源码解析

1. ref

先从入口ref看起,ref常用于基本类型,reactive用于引用类型。如果ref传入对象,其实内部会自动变为reactive。

ref本质上是把js 基本类型(string/number/bool)包装为引用对象,使得具有响应式特性。

export function ref(raw: unknown) {
  if (isRef(raw)) {
    return raw
  }
  // ref常用于基本类型,reactive用于引用类型。如果ref传入对象,其实内部会自动变为reactive
  raw = convert(raw)

  // 基本类型,转为含有getter/setter的对象
  const r = {
    _isRef: true, // 判断isRef
    // 基本类型无法被追踪,所以使用ref包装为object,使得可以被追踪
    get value() {
      track(r, OperationTypes.GET, '')
      return raw
    },
    set value(newVal) {
      raw = convert(newVal)
      trigger(r, OperationTypes.SET, '')
    }
  }
  return r as Ref
}

const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val

同时ref支持把reactive转为refs对象 - toRefs

export function toRefs<T extends object>(
  object: T
): { [K in keyof T]: Ref<T[K]> } {
  const ret: any = {}
  for (const key in object) { // for in 展开一层
    ret[key] = toProxyRef(object, key)
  }
  return ret
}

function toProxyRef<T extends object, K extends keyof T>(
  object: T,
  key: K
): Ref<T[K]> {
  return {
    _isRef: true,
    get value(): any {
      return object[key]
    },
    set value(newVal) {
      object[key] = newVal
    }
  }
}

2. reactive

再来看下vue3的响应式reactive源码:

先认识下以下4个全局存储,使用weakmap存储起普通对象和生成的响应式对象,因为很多地方都需要用到判断以及取值。其中rawToReactive和reactiveToRaw是一组,只不过key和value互相对调。

// 防止重复设置响应式对象,建立store存起来
// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>() // 原始object对象:封装的响应式对象
const reactiveToRaw = new WeakMap<any, any>() // 封装的响应式对象:原始object对象
// 只读响应式
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()

下面是reactive入口,如果传入参数是只读响应式,或者是用户设置的只读类型,返回处理。大部分都会走createReactiveObject方法:

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (readonlyToRaw.has(target)) {
    return target
  }
  // target is explicitly marked as readonly by user
  if (readonlyValues.has(target)) {
    return readonly(target)
  }
  // 给普通对象创建响应式对象
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

如下面注释解释,大部分代码都是为了做边界和重复处理。最重要的还是创建proxy对象:
observed = new Proxy(target, mutableHandlers)

function createReactiveObject(
  target: unknown, // 原始对象
  toProxy: WeakMap<any, any>, // 全局rawToReactive
  toRaw: WeakMap<any, any>, // 全局reactiveToRaw
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // 必须是对象
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }

  // 重复的对象引用,最终都返回初始的监听对象,这就是创建全局store的原因之一
  // target already has corresponding Proxy
  let observed = toProxy.get(target)
  if (observed !== void 0) {
    return observed
  }
  // target is already a Proxy
  if (toRaw.has(target)) {
    return target
  }

  // vue对象、vnode对象等不能被创建为响应式
  // only a whitelist of value types can be observed.
  if (!canObserve(target)) {
    return target
  }

  // 真正创建代理Proxy对象并返回
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers // [Set, Map, WeakMap, WeakSet]对象走这个handles
    : baseHandlers // 大部分走baseHandle
  observed = new Proxy(target, handlers)
  // 创建完马上全局缓存
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

所以还是看代理对象mutableHandlers中的处理:

export const mutableHandlers: ProxyHandler<object> = {
  get: createGetter(false),
  set,
  deleteProperty,
  has,
  ownKeys
}

get、has、deleteProperty、ownKeys代理方法中,都调用了track函数,用来收集依赖,这个下文讲;而set调用了trigger函数,当响应式数据变化时,收集的依赖被执行回调。从原理看,这跟vue2.x是一致的。

看下最常用的get、set。get中除常规边界处理外,最重要是根据代理值的类型,对object类型进行递归调用reactive

function createGetter(isReadonly: boolean) {
  return function get(target: object, key: string | symbol, receiver: object) {
     // 获取到代理的值
    const res = Reflect.get(target, key, receiver)
    if (isSymbol(key) && builtInSymbols.has(key)) {
      return res
    }

    // 如果是ref包裹的对象,直接返回解包后的值
    if (isRef(res)) {
      return res.value
    }
    // track是逻辑和视图变化重要的一块
    track(target, OperationTypes.GET, key)
    // 值类型,直接返回值;对象类型,递归响应式reactive(res)
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}

set函数里除了代理set方法外,最重要的莫过于当值改变时,触发trigger方法,下文详细讲述该函数。

function set(
  target: object,
  key: string | symbol,
  value: unknown,
  receiver: object
): boolean {
  value = toRaw(value)
  const oldValue = (target as any)[key]
  if (isRef(oldValue) && !isRef(value)) {
    oldValue.value = value
    return true
  }
  // prxoy
  const hadKey = hasOwn(target, key) // 是否target本来旧有key属性,等价于:key in target
  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, OperationTypes.ADD, key) // 触发新增
      } else if (hasChanged(value, oldValue)) {
        trigger(target, OperationTypes.SET, key) // 触发修改
      }
  }
  return result
}

3. track/trigger

这里是vue3响应式源码的难点。但原理跟vue2.x基本一致,只不过实现方式上有些不同。
track用于收集依赖deps(依赖一般收集effect/computed/watch的回调函数),trigger 用于通知deps,通知依赖这一状态的对象更新。

3.1 举个例子解释

如下代码,使用effect或computed api时,里面使用了count.num,意味着这个effect依赖于count.num。当count.num set改变值时,需要通知该effect去执行。那什么时候count.num收集到effect这个依赖呢?
答案是创建effect时的回调函数。如果回调函数中用到响应式数据(意味着会去执行get函数),则同步这个effect到响应式数据(这里是count.num)的依赖集中。

其流程是(全文重点):1. effect/computed函数执行 -> 2. 代码有书写响应式数据,调用到get,依赖收集 -> 3. 当有set时,依赖集更新。

const count = reactive({ num: 0 })
// effect默认没带lazy参数,先会执行effect
effect(() => {
  // effect用到对应响应式数据时,count.num get就已经收集好了该effect依赖
  // 同理,使用computed api时,
  console.log(count.num)
})
// computed依赖于count.num,也意味着该computed是count.num的依赖项
const computedNum = computed(() => 2 * count.num))
count.num = 7

3.2 对应源码解释

理解了上面这个案例,源码阅读就能顺畅的多。

先挑effect实现过程,再来看依赖收集track函数和执行依赖函数trigger。effect api主要用effect包装了回调函数fn,并默认执行fn回调函数,最终执行run(effect, fn, args)。

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // 回调fn函数,包装成effect
  const effect = createReactiveEffect(fn, options)
  // 默认不是懒加载,lazy=fasle,执行effect函数。
  if (!options.lazy) {
    effect()
  }
  return effect
}

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    return run(effect, fn, args) // 创建effect时,执行run
  } as ReactiveEffect
  effect._isEffect = true // 判断是effect
  effect.active = true // effect支持手动stop,此时active会被设置为false
  effect.raw = fn
  effect.scheduler = options.scheduler
  effect.onTrack = options.onTrack
  effect.onTrigger = options.onTrigger
  effect.onStop = options.onStop
  effect.computed = options.computed
  effect.deps = []
  return effect
}

再看run函数内容。其实就是执行回调函数时,先对effect入栈,使得当前effectStack有值。这个就非常巧妙,当执行fn回调时,回调函数的代码中又会去访问响应式数据(reactive),这样又会执行响应数据的get方法,get方法又会去执行后文讲的trick方法,trick进行依赖收集。

依赖收集哪些东西呢?就是收集当前的effect回调函数。这个回调函数(被effect包装)不就是刚被存储在effectStack么,所以在后续trick函数中可以看到使用effectStack栈。当执行完回调函数,再进行出栈。

通过使用栈数据结构,以及对代码执行的时机,非常巧妙的就把当前effect传递过去,最终被响应式数据收集到依赖集中。

function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
  if (!effect.active) {
    return fn(...args)
  }

  // 通常都是走这里,执行回调,同时不同时机effect入栈/出栈
  if (!effectStack.includes(effect)) {
    cleanup(effect)
    // 这里的try finally很巧妙
    // 入栈 -> 回调函数执行(使用栈,相当于把effect传递过去了) -> 出栈
    try {
      effectStack.push(effect)
      return fn(...args)
    } finally {
      effectStack.pop()
    }
  }
}

再来看看依赖收集trick/trigger具体实现细节。

先来看下几个存储变量,主要是依赖收集时用到的:

// 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.
export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<any, Dep>
// 原始对象: new Map({key1: new Set([effect1, effect2,...])}, {key2: Set2}, ...)
// key是原始对象里的属性, 值为该key改变后会触发的一系列的函数, 比如渲染、computed
export const targetMap = new WeakMap<any, KeyToDepMap>()

track函数进行数据依赖采集, 以便于后面数据更改能够触发对应的函数。

// 收集target key的依赖
// get: track(target, OperationTypes.GET, key)
export function track(target: object, type: OperationTypes, key?: unknown) {
  // 定义的computed、effect api都会推入effectStack栈中
  if (!shouldTrack || effectStack.length === 0) {
    return
  }

  // 调用effect/computed api时,能拿到effect对象(即依赖的回调函数)
  const effect = effectStack[effectStack.length - 1]

  if (type === OperationTypes.ITERATE) {
    key = ITERATE_KEY
  }

  // targetMap = {target1: deps = {key1: [], key2: [], ...}},两层嵌套
  // 初始化target
  let depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 初始化target.key。键是target.key,值是依赖的effect数组,是个集合。
  let dep = depsMap.get(key!)
  if (dep === void 0) {
    depsMap.set(key!, (dep = new Set()))
  }
  // 依赖收集
  if (!dep.has(effect)) {
    dep.add(effect)
    effect.deps.push(dep)
  }
}

trigger,将track收集到的effect函数集合,添加到runners中(二选一放进effects或computedRunners中),并通过scheduleRun执行effect:

// set: trigger(target, OperationTypes.SET, key)
export function trigger(
  target: object,
  type: OperationTypes,
  key?: unknown,
  extraInfo?: DebuggerEventExtraInfo
) {
  const depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    // never been tracked
    return
  }
  // 把拿到的depsMap.get(key),二选一放进effects或computedRunners中。
  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  // 根据不同的OperationTypes,把effect=depsMap.get(key)放进runners中
  if (type === OperationTypes.CLEAR) {
    // collection being cleared, trigger all effects for target
    depsMap.forEach(dep => {
      addRunners(effects, computedRunners, dep)
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      addRunners(effects, computedRunners, depsMap.get(key))
    }
    // also run for iteration key on ADD | DELETE
    if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
      const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
      addRunners(effects, computedRunners, depsMap.get(iterationKey))
    }
  }

  // 执行runners,即执行effects
  const run = (effect: ReactiveEffect) => {
    scheduleRun(effect, target, type, key, extraInfo)
  }
  // Important: computed effects must be run first so that computed getters
  // can be invalidated before any normal effects that depend on them are run.
  computedRunners.forEach(run)
  effects.forEach(run)
}

// 添加runner时,二选一
function addRunners(
  effects: Set<ReactiveEffect>,
  computedRunners: Set<ReactiveEffect>,
  effectsToAdd: Set<ReactiveEffect> | undefined
) {
  if (effectsToAdd !== void 0) {
    effectsToAdd.forEach(effect => {
      if (effect.computed) {
        computedRunners.add(effect)
      } else {
        effects.add(effect)
      }
    })
  }
}

总结

响应式数据,就是当数据对象改变时(set),有用到数据对象的地方,都会自动执行响应的逻辑。比如effect/computed/watch等js api用到数据对象,则执行对应的回调函数。而视图view用到数据对象时,则重新vnode diff,最后自动进行dom更新(即视图更新)。

而Vue3响应式源码跟Vue2.x源码流程基本一致,依然是利用在使用响应式数据时,执行数据的get方法,收集相关的依赖(依赖可以是回调函数,如effect/computed,也可以是视图自动更新);在数据进行变化的时候,执行数据的set方法,把收集的依赖都依次执行。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant