This is the first day of my participation in the wenwen Challenge
reactive
As one of the most important reactive apis in VUe3, it is defined as passing in an object and returning a reactive proxy based on the original object, that is, returning oneProxy
, which is equivalent toVue2x
Version of the Vue.observer
.
- advantages
Reactive API is the standard DATA option. What are the advantages of the DATA option compared to reactive API?
First, reactive processing of data in Vue 2X is based on Object.defineProperty(), but it only listens for attributes of objects, not objects. So, when adding object attributes, you usually need to:
// vue2x adds attributes
Vue.$set(object, 'name', xmj)
Copy the code
Reactive API is based on ES2015 Proxy to implement the responsive processing of data objects. That is, in Vue3.0, attributes can be added to objects, and these attributes also have responsive effects, such as:
// Add attributes in Vue3.0
object.name = 'xmj'
Copy the code
Object. DefineProperty Listening on Object requires traversing recursively all keys. Therefore, the data to be listened in vue2. X needs to be defined in data first, and the new response data also needs to use $set to add listener. There are also problems with Array listening. Vue3 eliminates these concerns.
- Pay attention to the point
The important thing about using reactive apis is that when you return from setup, you need to do it in the form of an object, such as:
export default {
setup() {
const pos = reactive({
x: 0.y: 0
})
return {
pos: useMousePosition()
}
}
}
Copy the code
Alternatively, wrap the export with the toRefs API, in which case we can use expansion operators or destructions, for example:
export default {
setup() {
let state = reactive({
x: 0.y: 0
})
state = toRefs(state)
return {
...state
}
}
}
Copy the code
- usage
Start with unit testsreactive
usage
In Vue3.0 responsive code is placed in a separate module under the/Packages/Reactivity directory. Unit tests for each module are placed in the __tests__ folder. Find the reactive. Spec. Ts. The code is as follows:
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'])})// ...
})
Copy the code
As you can see, reactive. Ts provides the following approach:
reactive
: Converts raw data into a responsible object, that isProxy
Object. Support for raw data types:Object|Array|Map|Set|WeakMap|WeakSet
isReactive
: Checks whether the data can be responded totoRaw
: the corresponding data can be converted into original data.markNonReactive
: Marks the data as unresponsive.
In combination witheffect
use
Often used in conjunction with Reactive is Effect, which is a callback function that listens for changes in data. The effect unit test is as follows:
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)})})Copy the code
Reactive + Effect can be used in the following ways:
import { reactive, effect } from 'dist/reactivity.global.js'
letdummy <! -- Reactive listening object -->const counter = reactive({ num: 0}) <! -- effect --> effect(() = > (dummy = counter.num))
Copy the code
Principle of * * * *
From the unit test, it can be found that reactive and effect are react. ts and effection. ts respectively. Let’s start with these two files to understand the source code of reactivity.
Reactive + effect principle analysis
Take a look at the following example to see what is done.
import { reactive, effect } from 'dist/reactivity.global.js'
const counter = reactive({ num: 0.times: 0 })
effect(() = > {console.log(counter.num)})
counter.num = 1
Copy the code
- call
reactive()
Will generate aProxy
objectcounter
. - call
effect()
Internal function () is called once by default =>{console.log(counter.num)}
(replaced by fn below), triggered when fn is runcounter.num
namelyGet the trap. get trap
The triggertrack()
, can be intargetMap
addnum
Rely on.
// targetMap stores dependencies, similar to the structure used in the effect file
/ / {
// target: {
// key: Dep
/ /}
// }
// Target is a proxy object, and key is an attribute of the object that triggers the get behavior
// export type Dep = Set<ReactiveEffect>
// export type KeyToDepMap = Map<string | symbol, Dep>
// export const targetMap: WeakMap<any, KeyToDepMap> = new WeakMap()
// Get the targetMap value
{
counter: {
num: [fn]
}
}
Copy the code
When counter. Num = 1, the set trap of counter will be triggered. If num is inconsistent with oldValue, trigger() will be triggered. The callback function targetmap.counter. Num found in targetMap in trigger is fn. The callback performs fn
Reactive function
The core code in Reactice is createReactiveObject, which creates a proxy object
reactive(target: object) {
// Create a responsive object instead of readOnly. The created object is not the same as the source object
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}
Copy the code
createReactiveObject
Create a proxy object using proxy. Handlers, collection classes and other types use different handlers. CollectionTypes have values of Set, Map, WeakMap, and WeakSet uses collectionHandlers. Object and Array use baseHandlers
function createReactiveObject() {
Handlers, collection classes and other types use different handlers
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
Handlers are responsible for creating the proxy object
// So let's go to the Handlers implementation folder, baseHandlers first
/ / and not familiar with the proxy usage, can be familiar with the first document at https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
observed = new Proxy(target, handlers)
return observed
}
Copy the code
MutableHandlers (handler)
mutableHandlers: ProxyHandler<any> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
Copy the code
Get method of handler
Use reflect.get to get the original value of get, and recursively return the concrete proxy object if the value is an object. All track () does is plug the dependency into targetMap for the next time to find out if there is a dependency, and save the effect callback
function createGetter(isReadonly: boolean) {
return function get(target: any, key: string | symbol, receiver: any) {
// Get the result
const res = Reflect.get(target, key, receiver)
/ /...
// All this function does is plug dependencies into the map to find out if there are dependencies next time
// Save the effect callback
track(target, OperationTypes.GET, key)
// Determine if the value of get is an object. If so, wrap the object as a proxy (recursive).
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
Copy the code
Handler’s set method
The core logic is 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
// The core logic of the set behavior is trigger
if(! hadKey) { trigger(target, OperationTypes.ADD, key) }else if(value ! == oldValue) { trigger(target, OperationTypes.SET, key) }return result
}
Copy the code
Trigger method
TargetMap has the following data structure to store dependencies. If CLEAR is used, all callbacks are executed. Otherwise, the stored callback is performed. ADD and DELETE perform special callbacks.
// targetMap stores dependencies, similar to the structure used in the effect file
/ / {
// target: {
// key: Dep
/ /}
// }
// Target is a proxy object, and key is an attribute of the object that triggers the get behavior
// If counter. Num triggers get, num is key. Dep is the callback function, which is effect when counter.num is called
// This callback is deP and needs to be collected for next use.
Copy the code
function trigger(target: any, type: OperationTypes, key? : string | symbol, extraInfo? : any) {
const depsMap = targetMap.get(target)
// ...
const effects: Set<ReactiveEffect> = new Set(a)const computedRunners: Set<ReactiveEffect> = new Set(a)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
Depmap. get(key) fetches the dependency callback
if(key ! = =void 0) {
// Drop the dependency callback into 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) = > {
// Execute the callback function
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)
}
Copy the code
Effect method
Effect will be called directly if it is not lazy, passing in fn and generating targetMap dependencies from fn. Fn is called back when the data in the dependency changes.
export function effect(
fn: Function,
options: ReactiveEffectOptions = EMPTY_OBJ
) :ReactiveEffect {
// Determine if the callback has been wrapped
if ((fn as ReactiveEffect).isEffect) {
fn = (fn as ReactiveEffect).raw
}
// Wrap the callback, effect is the fn method, with many properties attached to the fn function.
const effect = createReactiveEffect(fn, options)
// Not lazy will be called directly once. But in the lazy case, effect is not called, so targetMap dependencies are not generated. Unable to call back. I wonder if this is a bug?
if(! options.lazy) { effect() }// The return value is used to stop
return effect
}
Copy the code
conclusion
So that’s it. We’ve finally worked out the logic of Reactive. Reading this part of the code is a bit difficult, because it involves a lot of low-level knowledge, otherwise it will be confused everywhere, but it is also a learning process, the process of exploration is also quite interesting.