这是我参与更文挑战的第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.defineProperty
对Object
侦听需要遍历递归所有的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.ts
和effect.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.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
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
,执行所有的回调。否则执行存储的回调。另外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的逻辑完全理完了。阅读本部分的代码有点儿不容易,因为涉及的底层知识比较多,不然会处处懵逼,不过这也是一种学习,探索的过程也是挺有意思的。