vue3学习(4)reactive源码阅读理解

这是我参与更文挑战的第1天,活动详情查看更文挑战

reactive作为vue3中最重要的几个响应式API,它的定义是为传入一个对象并返回一个基于原对象的响应式代理,即返回一个 Proxy,相当于 Vue2x 版本中的 Vue.observer

  • 优点

reactive API 是对标data选项,那么相比较data选项有哪些优点?

首先,在 Vue 2x 中数据的响应式处理是基于 Object.defineProperty() 的,但是它只会侦听对象的属性,并不能侦听对象。所以,在添加对象属性的时候,通常需要这样:

 // vue2x添加属性
    Vue.$set(object, 'name', xmj)
复制代码

reactive API 是基于 ES2015 Proxy 实现对数据对象的响应式处理,即在 Vue3.0 可以往对象中添加属性,并且这个属性也会具有响应式的效果,例如:

 // vue3.0中添加属性
    object.name = 'xmj'
复制代码

Object.definePropertyObject侦听需要遍历递归所有的key。所以在vue2.x中需要侦听的数据需要先在data中定义,新增响应数据也需要使用$set来添加侦听。而且对Array的侦听也存在一定的问题。在vue3就可以不用考虑这些问题。

  • 注意点

使用 reactive API 需要注意的是,当你在 setup 中返回的时候,需要通过对象的形式,例如:

 export default {
      setup() {
          const pos = reactive({
            x: 0,
            y: 0
          })

          return {
             pos: useMousePosition()
          }
      }
    }
复制代码

或者,借助 toRefs API 包裹一下导出,这种情况下我们就可以使用展开运算符或解构,例如:

export default {
      setup() {
          let state = reactive({
            x: 0,
            y: 0
          })
        
          state = toRefs(state)
          return {
             ...state
          }
      }
    } 
复制代码
  • 用法

先从单元测试了解reactive用法

vue3.0中响应式代码被放到单独的模块,代码在/packages/reactivity目录下。每个模块的单元测试都放在__tests__文件夹下。找到reactive.spec.ts。代码如下:

import { reactive, isReactive, toRaw, markNonReactive } from '../src/reactive'
import { mockWarn } from '@vue/runtime-test'

describe('reactivity/reactive', () => {
  mockWarn()

  test('Object', () => {
    const original = { foo: 1 }
    const observed = reactive(original)
    expect(observed).not.toBe(original)
    expect(isReactive(observed)).toBe(true)
    expect(isReactive(original)).toBe(false)
    // get
    expect(observed.foo).toBe(1)
    // has
    expect('foo' in observed).toBe(true)
    // ownKeys
    expect(Object.keys(observed)).toEqual(['foo'])
  })

  test('Array', () => {
    const original: any[] = [{ foo: 1 }]
    const observed = reactive(original)
    expect(observed).not.toBe(original)
    expect(isReactive(observed)).toBe(true)
    expect(isReactive(original)).toBe(false)
    expect(isReactive(observed[0])).toBe(true)
    // get
    expect(observed[0].foo).toBe(1)
    // has
    expect(0 in observed).toBe(true)
    // ownKeys
    expect(Object.keys(observed)).toEqual(['0'])
  })
  // ...
})
复制代码

可以大致看到reactive.ts提供了如下方法:

  • reactive: 将原始数据转化为可响应的对象,即Proxy对象。支持原始数据类型:Object|Array|Map|Set|WeakMap|WeakSet
  • isReactive: 判断是否可响应数据
  • toRaw:讲可相应数据转化为原始数据。
  • markNonReactive:标记数据为不可响应。

结合effect使用

经常和reactive结合起来使用的是effect,它是侦听到数据变化后的回调函数。effect单元测试如下:

import {
  reactive,
  effect,
  stop,
  toRaw,
  OperationTypes,
  DebuggerEvent,
  markNonReactive
} from '../src/index'
import { ITERATE_KEY } from '../src/effect'

describe('reactivity/effect', () => {
  it('should run the passed function once (wrapped by a effect)', () => {
    const fnSpy = jest.fn(() => {})
    effect(fnSpy)
    expect(fnSpy).toHaveBeenCalledTimes(1)
  })

  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)
  })

  it('should observe multiple properties', () => {
    let dummy
    const counter = reactive({ num1: 0, num2: 0 })
    effect(() => (dummy = counter.num1 + counter.num1 + counter.num2))

    expect(dummy).toBe(0)
    counter.num1 = counter.num2 = 7
    expect(dummy).toBe(21)
  })
})

复制代码

可总结出reactive + effect的使用方法:

import { reactive, effect } from 'dist/reactivity.global.js'
let dummy
<!-- reactive监听对象 -->
const counter = reactive({ num: 0 })
<!-- 数据变动回调effect -->
effect(() => (dummy = counter.num))
复制代码

**原理 **

从单元测试中可以发现,reactive函数和effect分别在reactive.tseffect.ts。接下来我们从这两个文件开始着手了解reactivity的源码。

reactive + effect原理解析

参考下面这个例子,看看里面都做了什么。

import { reactive, effect } from 'dist/reactivity.global.js'
const counter = reactive({ num: 0, times: 0 })
effect(() => {console.log(counter.num)})
counter.num = 1
复制代码
  • 调用reactive()会生成一个Proxy对象counter
  • 调用effect()时会默认调用一次内部函数() => {console.log(counter.num)}(下文以fn代替),运行fn时会触发counter.numget trap。get trap触发track(),会在targetMap中增加num依赖。
// targetMap 存储依赖关系,类似以下结构,这个结构会在 effect 文件中被用到
// {
//   target: {
//     key: Dep
//   }
// }
// 解释下三者到底是什么:target 就是被 proxy 的对象,key 是对象触发 get 行为以后的属性
// export type Dep = Set<ReactiveEffect>
// export type KeyToDepMap = Map<string | symbol, Dep>
// export const targetMap: WeakMap<any, KeyToDepMap> = new WeakMap()

// get之后targetMap值
{
    counter: {
        num: [fn]
    }
}
复制代码

counter.num = 1,会触发counterset trap trap,判断num的值和oldValue不一致后,触发trigger()trigger中在targetMap中找到targetMap.counter.num的回调函数是fn。回调执行fn

reactive函数

reactice中核心代码是createReactiveObject,作用是创建一个proxy对象

reactive(target: object) {
  // 不是 readonly 就创建一个响应式对象,创建出来的对象和源对象不等
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
    mutableCollectionHandlers
  )
}
复制代码

createReactiveObject

使用proxy创建一个代理对象。判断对象的构造函数得出 handlers,集合类和别的类型用到的 handler 不一样。collectionTypes的值为Set, Map, WeakMap, WeakSet使用collectionHandlersObjectArray使用baseHandlers

function createReactiveObject() {
 // 判断对象的构造函数得出 handlers,集合类和别的类型用到的 handler 不一样
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  // 创建 proxy 对象,这里主要要看 handlers 的处理了
  // 所以我们去 handlers 的具体实现文件夹吧,先看 baseHandlers 的
  // 另外不熟悉 proxy 用法的,可以先熟悉下文档 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
  observed = new Proxy(target, handlers)
  return observed
 }
复制代码

mutableHandlers(handler)

mutableHandlers: ProxyHandler<any> = {
  get: createGetter(false),
  set,
  deleteProperty,
  has,
  ownKeys
}
复制代码

handler的get方法

使用Reflect.get获取get的原始值,如果此值是对象,则递归返回具体的proxy对象。track()做的事情就是塞依赖到 targetMap 中,用于下次寻找是否有这个依赖,另外就是把effect的回调保存起来

function createGetter(isReadonly: boolean) {
  return function get(target: any, key: string | symbol, receiver: any) {
    // 获得结果
    const res = Reflect.get(target, key, receiver)
    // ....
   
    // 这个函数做的事情就是塞依赖到 map 中,用于下次寻找是否有这个依赖
    // 另外就是把 effect 的回调保存起来
    track(target, OperationTypes.GET, key)
    // 判断get的值是否为对象,是的话将对象包装成 proxy(递归)
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}
复制代码

handler的set方法

核心逻辑是trigger

function set(
  target: any,
  key: string | symbol,
  value: any,
  receiver: any
): boolean {
  // ...
  const result = Reflect.set(target, key, value, receiver)
  // ...
  // don't trigger if target is something up in the prototype chain of original
  // set 行为核心逻辑是 trigger
  if (!hadKey) {
    trigger(target, OperationTypes.ADD, key)
  } else if (value !== oldValue) {
    trigger(target, OperationTypes.SET, key)
  }
  return result
}
复制代码

trigger方法

targetMap的数据结构如下,用来存储依赖关系。 如果修改方式是CLEAR,执行所有的回调。否则执行存储的回调。另外ADD、DELETE会执行某些特殊的回调。

// targetMap 存储依赖关系,类似以下结构,这个结构会在 effect 文件中被用到
// {
//   target: {
//     key: Dep
//   }
// }
// 解释下三者到底是什么:target 就是被 proxy 的对象,key 是对象触发 get 行为以后的属性
// 比如 counter.num 触发了 get 行为,num 就是 key。dep 是回调函数,也就是 effect 中调用了 counter.num 的话
// 这个回调就是 dep,需要收集起来下次使用。
复制代码
function trigger(
  target: any,
  type: OperationTypes,
  key?: string | symbol,
  extraInfo?: any
) {
  const depsMap = targetMap.get(target)
  // ...
  const effects: Set<ReactiveEffect> = new Set()
  const computedRunners: Set<ReactiveEffect> = new Set()
  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
    // depsMap.get(key) 取出依赖回调
    if (key !== void 0) {
      // 把依赖回调丢到 effects 中
      addRunners(effects, computedRunners, depsMap.get(key as string | symbol))
    }
    // 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))
    }
  }
  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方法

effect在非lazy的情况下会直接调用effect也就是传入fn,根据fn生成targetMap依赖。当依赖中的数据发生变化时会回调fn

export function effect(
  fn: Function,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect {
  // 判断回调是否已经包装过
  if ((fn as ReactiveEffect).isEffect) {
    fn = (fn as ReactiveEffect).raw
  }
  // 包装回调,effect其实就是fn方法,在fn函数身上挂了很多属性。
  const effect = createReactiveEffect(fn, options)
  // 不是 lazy 的话会直接调用一次。但是lazy情况下,不调用effect,故而不会生成targetMap依赖。导致不能回调。不知道这是不是一个bug?
  if (!options.lazy) {
    effect()
  }
  // 返回值用以 stop
  return effect
}
复制代码

总结

那到此为止,终于把reactive的逻辑完全理完了。阅读本部分的代码有点儿不容易,因为涉及的底层知识比较多,不然会处处懵逼,不过这也是一种学习,探索的过程也是挺有意思的。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享