Vue3.0 数据响应机制解析

1,570 阅读11分钟

Vue3.0 数据响应机制解析

简单画了一张图片(请叫我灵魂画手,哈哈哈)

从上面的图片其实大家就能看出,数据渲染解析分为两条路,

  1. 通过节点的方式,然后根据指令,然后实例efect然后调用effect
  2. 通过reactive,通过baseHandle然后进行依赖收集,广播触发

因为篇幅有限今天主要分享的是data => reactive => effect这条路径

代码解析

reactive

  1. 可以跟大家说一下我一般情况是如何进行源代码阅读的,首先你先要对阅读的源代码实现的功能有所了解,然后对于这个功能的实现你的思路是什么,然后带着你的思路去源码中寻找作者的实现思路,在阅读的过程中对比自己的想法,弥补自己的不足。
  2. 最开始阅读的时候,我们要先了解我们要阅读的源码的整体结构,工程配置,然后通过项目的根文件(一般是index.js)进行阅读,寻找到自己最熟悉的或者最想了解的功能模块,进行代码阅读。
  3. 另外一个比较重要的点可能是,源码在实现过程中代码写的比较抽象,所以我们最开始阅读的时候大可不必全部了解细节,知道函数作用、变量用途就可以了,对整体实现流程缕通顺后在进行细节的阅读。
  4. 为什么阅读源码对程序员有帮助,在我看来,我们在阅读的过程中一方面是了解作者的实现思路,另外一方面是学习代码的抽象能力,提升自己的代码质量。
  1. 首先我们看reactive.ts中的reactive方法做了什么
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
  )
}

去掉上面的两个if判断其实这个方法就做了一件事情就是调用了createReactiveObject这个方法

function createReactiveObject(
  target: unknown,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // 如果target 不是对象
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target already has corresponding Proxy
  // 如果target 已经存在在相应的代理中则返回
  let observed = toProxy.get(target)
  // 不为undefined 时返回observed
  if (observed !== void 0) {
    return observed
  }
  // target is already a Proxy
  // 如果target已经在代理中同样返回
  if (toRaw.has(target)) {
    return target
  }
  // only a whitelist of value types can be observed.
  // 如果target并不能被observe 则返回
  if (!canObserve(target)) {
    return target
  }

  // 如果collectionTypes继承于target,handlers为collectionHandlers
  // 不存在的话handlers为baseHandlers
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers

  // 创建一个Proxy对象
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

上面的代码我进行了一些简单的注释,同样我们把if判断中带有return的部分忽略掉不看的话,其实这个方法的代码实现的功能就非常明了了

function createReactiveObject(
  target: unknown,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // target already has corresponding Proxy
  // 如果target 已经存在在相应的代理中则返回
  let observed = toProxy.get(target)

  // 如果collectionTypes继承于target,handlers为collectionHandlers
  // 不存在的话handlers为baseHandlers
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers

  // 创建一个Proxy对象
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

我们在看上面简化的这段代码,实际上就是使用ES6中的Proxy,相信大家对于Vue2.0的响应式是通过Object.defineProperty来实现的,兼容主流浏览器和IE9已上的IE浏览器,能够监听数据对象的变化,但是监听不到对象属性的增删、数组元素和长度的变化,同时会在vue初始化的时候把所有的Observer都建立好,才能观察到数据对象属性的变化。这次3.0使用的ES6的Proxy可以做到监听对象属性的增删和数组元素和长度的修改,还可以监听Map、Set、WeakSet、WeakMap,同时还实现了惰性的监听,不会在初始化的时候创建所有的Observer,而是会在用到的时候才去监听。因此性能方面肯定是极大的提升!当然因为Proxy存在兼容性问题,所以3.0会对IE系列做兼容,对外api保持一致,底层通过Object.defineProperty实现。

baseHandle

通过上面的代码我们再来看Proxy中的handler是什么,const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandler,这里做了一下判断如果当前传入的target被初始化过,那么使用传入collectionHandlers,如果没有被初始化过那么使用传入的baseHandler。

那么我们回顾一下最开始我们提供的第一个代码块中createReactiveObject的入参分别都是什么:

return createReactiveObject(
   target,
   rawToReactive,
   reactiveToRaw,
   mutableHandlers,
   mutableCollectionHandlers
)

我们首先去看baseHandler(mutableHandlers)做了哪些操作:

export const mutableHandlers: ProxyHandler<object> = {
  // 数据收集
  get: createGetter(false),
  // 数据变化 ==> 广播
  set,
  // 删除
  deleteProperty,
  has,
  ownKeys
}

createGetter

function createGetter(isReadonly: boolean, unwrap = true) {
  return function get(target: object, key: string | symbol, receiver: object) {
    let res = Reflect.get(target, key, receiver)
    if (isSymbol(key) && builtInSymbols.has(key)) {
      return res
    }
    if (unwrap && isRef(res)) {
      res = res.value
    } else {
      track(target, OperationTypes.GET, key)
    }
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}

首先从代码上看,这个createGetter返回的是一个get方法,因为数据监听是使用Proxy做的,所以我们在对target进行获取内部信息时,使用了Reflect这个新的API,Reflect与Proxy是相辅相成的,在Proxy上有的方法,在Reflect就一定有,Reflect可以确保对象的属性能正确赋值,广义上讲,即确保对象的原生行为能够正常进行,这就是Reflect的作用,然后对res进行了判断,if (unwrap && isRef(res)) res = res.value在ref中同样做了track操作,看到这里大家肯定也能想到或者察觉到这个track是在做什么事情。 没错,track这个函数就是做了依赖收集。

set

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
  }
  const hadKey = 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
  // 在reactive维护了一个reactiveToRaw队列,存储了[proxy]:[target]这样的队列,这里检测下是否是使用createReactiveObject新建的proxy
  if (target === toRaw(receiver)) {
    /* istanbul ignore else */
    // 判断是否值改变,才触发更新
    if (__DEV__) {
      const extraInfo = { oldValue, newValue: value }
      if (!hadKey) {
        trigger(target, OperationTypes.ADD, key, extraInfo)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, OperationTypes.SET, key, extraInfo)
      }
    } else {
      if (!hadKey) {
        trigger(target, OperationTypes.ADD, key)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, OperationTypes.SET, key)
      }
    }
  }
  return result
}

大家通过上面的我代码中的注释就能看出set在做什么事情,没错就是做更新广播! 后面的deleteProperty、has、ownKeys实际上做的事情和上面get,set做的事情是一样的,这里就不做过多的赘述了。 后面我们单独去看track和tigger。我们来总结一下baseHandler的作用

  • get获取值,其次依赖收集
  • set设置值,其次更新广播、触发任务调度

那么到底什么是依赖收集,什么是任务调度,dom和data的关系又是什么呢!!??

effect

effect在vue3.0中到底做了什么呢,我们带着疑问去代码中寻找答案:

我们先来看下面这段代码:

// 初始化effect
export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }
  return effect
}

// 创建一个effect
function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    return run(effect, fn, args)
  } as ReactiveEffect
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
  if (!effect.active) {
    return fn(...args)
  }
  if (!effectStack.includes(effect)) {
    cleanup(effect)
    try {
      effectStack.push(effect)
      return fn(...args)
    } finally {
      effectStack.pop()
    }
  }
}

大家从上面的代码中能看出什么,实不相瞒,我看不出什么😭😭😭 但是,我从测试用例上面知道了这个effect的作用😝😝😝

effect测试用例

  it('should observe basic properties', () => {
    let dummy
    const counter = reactive({ num: 0 })
    effect(() => (dummy = counter.num))

    expect(dummy).toBe(0)
    counter.num = 7
    expect(dummy).toBe(7)
  })

computed测试用例

  it('should return updated value', () => {
    const value = reactive<{ foo?: number }>({})
    const cValue = computed(() => value.foo)
    expect(cValue.value).toBe(undefined)
    value.foo = 1
    expect(cValue.value).toBe(1)
  })

看完上面的代码是不是有了思路,哈哈哈哈哈哈 effect从字面上来看就是影响的意思,因此effect实际上做的就是在数据响应的过程中,处理产生的副作用,因此在每次触发get的时候收集effect,在set的时候触发effects!

下面来看一下大佬用effect实现的computed

function computed (fn) {
  let value = undefined
  const runner = effect(fn, {
    // 如果lazy不置为true的话,每次创建effect的时候都会立即执行一次
    // 而我们要实现computed显然是不需要的
    lazy: true
  })
  // 为什么要使用对象的形式,是因为我们最后需要得到computed的值
  // 如果不用对象的 get 方法的话我们就需要手动再调用一次 computed() 
  return {
    get value() {
      return runner()
    }
  }
}

// 使用起来是这样的

const value = reactive({})
const cValue = computed(() => value.foo)
value.foo = 1

console.log(cValue.value) // 1

下面我们再来看track和trigger的实现

// 数据依赖收集
export function track(target: object, type: OperationTypes, key?: unknown) {
  if (!shouldTrack || effectStack.length === 0) {
    return
  }
  const effect = effectStack[effectStack.length - 1]
  if (type === OperationTypes.ITERATE) {
    key = ITERATE_KEY
  }
  let depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    targetMap.set(target, (depsMap = new Map()))
  }
  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)
    if (__DEV__ && effect.options.onTrack) {
      effect.options.onTrack({
        effect,
        target,
        type,
        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
  }
  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  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 = isArray(target) ? 'length' : ITERATE_KEY
      addRunners(effects, computedRunners, depsMap.get(iterationKey))
    }
  }
  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)
}

所以effect做的事情就可以总结为:

  1. 当响应数据触发get时, 将所有get的target、key与effect建立对应关系,因为每个target可能会有多个副作用effect,所以建立一个deps依赖收集器,
  2. 因为deps是target的副作用集合,因此放在target上面并不合适,因此将这个对应关系存储在targetMap中。
  3. 当trigger触发是,则拿到target下对应所有的effect,遍历执行。

到这里我们就把vue3.0中的数据响应介绍的差不多了,但是好像我们说的这个跟dom没啥关系啊,dom上的数据最后是怎么变更的???

相信细心的同学发现了在trigger函数中定义了一个run函数:

const run = (effect: ReactiveEffect) => {
  scheduleRun(effect, target, type, key, extraInfo)
}

我们在来看scheduleRun这个函数做了什么:

function scheduleRun(
  effect: ReactiveEffect,
  target: object,
  type: OperationTypes,
  key: unknown,
  extraInfo?: DebuggerEventExtraInfo
) {
  if (__DEV__ && effect.options.onTrigger) {
    const event: DebuggerEvent = {
      effect,
      target,
      key,
      type
    }
    effect.options.onTrigger(extraInfo ? extend(event, extraInfo) : event)
  }
  if (effect.options.scheduler !== void 0) {
    effect.options.scheduler(effect)
  } else {
    effect()
  }
}

effect.options.scheduler(effect)没错这句代码会触发watch中的queuePostRenderEffect,而这个函数的作用是什么呢?

我们可以在renderer.ts中找到答案:

export const queuePostRenderEffect = __FEATURE_SUSPENSE__
  ? queueEffectWithSuspense
  : queuePostFlushCb

我們看看 queuePostRenderEffect 函數,本质是调用的 queuePostFlushCb

export function queuePostFlushCb(cb: Function | Function[]) {
  if (!isArray(cb)) {
    postFlushCbs.push(cb)
  } else {
    postFlushCbs.push(...cb)
  }
  queueFlush()
}

function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    nextTick(flushJobs)
  }
}

queuePostFlushCb 函数也比较简单,收集回调函数然后在nextTick,最后执行flushJobs。

我们在scheduler中发现存在2个队列:

  1. queue
  2. postFlushCbs

对应的添加函数为

  1. queueJob
  2. queuePostFlushCb

很显然,这是对应的两种更新时机的回调,而触发回调都是由flushJobs 完成:

function flushJobs(seen?: CountMap) {
  isFlushPending = false
  isFlushing = true
  let job
  if (__DEV__) {
    seen = seen || new Map()
  }
  while ((job = queue.shift())) {
    if (__DEV__) {
      checkRecursiveUpdates(seen!, job)
    }
    callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
  }
  flushPostFlushCbs(seen)
  isFlushing = false
  // some postFlushCb queued jobs!
  // keep flushing until it drains.
  if (queue.length || postFlushCbs.length) {
    flushJobs(seen)
  }
}

最后我们回头在看回调函数applyCb

const applyCb = cb
    ? () => {
        if (instance && instance.isUnmounted) {
          return
        }
        const newValue = runner()
        if (deep || hasChanged(newValue, oldValue)) {
          // cleanup before running cb again
          if (cleanup) {
            cleanup()
          }
          callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
            newValue,
            oldValue,
            registerCleanup
          ])
          oldValue = newValue
        }
      }
    : void 0

综上我们可以得到一下几点:

  1. reactivity 的作用在于处理对象的 proxy,在每个取值操作的地方 track。
  2. 当对象属性值发生变化的时候,触发trigger
  3. watch的巧妙之处在于取值时添加依赖传入回调函数、创建effect的同时,对回调进行scheduler处理,最后scheduler根据flush世纪来进行区分。

至此,我今天要跟大家分享的内容基本上就差不多了,最后我在阅读的过程中对源码添加了注释GitHub - Jhaidi/vue-next

大家有兴趣的可以关注我个人的微信公众号,内容同步发送在微信公众号中

wechatgroup.jpg?jhd_blog_image