阅读 1485

vue3.0 pre-alpha之reactivity源码解析

上一篇文章中介绍了如何调试vue-next。接下来开始解读vue-nextreactivity模块

vue3.0中比较大的改动之一就是响应式的实现有Object.defineProperty改为Proxy实现。阅读之前可以先提前了解下Proxy

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

用法

先从单元测试了解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 trapget 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

思考:如果改变了counter.times的值,回调函数fn:() => {console.log(counter.num)}会不会执行呢?为什么?’

再次执行counter.num = 1num的值未改变,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使用collectionHandlers。Object和Array使用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,执行所有的回调。否则执行存储的回调。另外ADDDELETE会执行某些特殊的回调。

// 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
}
复制代码
文章分类
前端
文章标签