上一篇文章中介绍了如何调试
vue-next
。接下来开始解读vue-next
的reactivity
模块vue
vue3.0中比较大的改动之一就是响应式的实现有Object.defineProperty
改成Proxy
实现。阅读以前能够先提早了解下Proxy
。react
Object.defineProperty
对Object
侦听须要遍历递归全部的key
。因此在vue2.x中须要侦听的数据须要先在data中定义,新增响应数据也须要使用$set
来添加侦听。并且对Array
的侦听也存在必定的问题。在vue3.0
就能够不用考虑这些问题。bash
在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提供了以下方法:app
Object|Array|Map|Set|WeakMap|WeakSet
常常和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
的使用方法:post
import { reactive, effect } from 'dist/reactivity.global.js'
let dummy
<!-- reactive监听对象 -->
const counter = reactive({ num: 0 })
<!-- 数据变更回调effect -->
effect(() => (dummy = counter.num))
复制代码
从单元测试中能够发现,reactive
函数和effect
分别在reactive.ts
和effect.ts
。接下来咱们从这两个文件开始着手了解reactivity
的源码。单元测试
参考下面这个例子,看看里面都作了什么。测试
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.num
即get 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
,会触发counter
的set trap
trap,判断num的值和oldValue不一致后,触发trigger(),trigger中在targetMap中找到targetMap.counter.num的回调函数是fn。回调执行fn思考:若是改变了
counter.times
的值,回调函数fn:() => {console.log(counter.num)}
会不会执行呢?为何?’ui
再次执行
counter.num = 1
即num
的值未改变,fn
会不会执行呢?
reactice
中核心代码是createReactiveObject
,做用是建立一个proxy
对象
reactive(target: object) {
// 不是 readonly 就建立一个响应式对象,建立出来的对象和源对象不等
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}
复制代码
使用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: ProxyHandler<any> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
复制代码
使用
Reflect.get
获取get
的原始值,若是此值是对象,则递归返回具体的prox
y对象。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
}
}
复制代码
核心逻辑是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 } 复制代码
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
在非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
}
复制代码