The previous article showed you how to debug VUE-Next. Next, read vue-Next’s Reactivity module
One of the big changes in VUe3.0 is the reactive implementation of object.defineProperty instead of Proxy implementation. Read about Proxy before you read about Proxy.
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.0 eliminates these concerns.
usage
Learn about reactive from unit tests first
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 following code
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: Transforms raw data into responsive objects, known as Proxy objects. Support for raw data types:
Object|Array|Map|Set|WeakMap|WeakSet
- IsReactive: Checks whether data can be responded to
- ToRaw: The corresponding data can be converted into raw data.
- MarkNonReactive: Marks data as unresponsive.
Use with effect
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 --> counter = reactive({num: 0}) <! Dummy => (dummy = counter.num))Copy the code
The 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()
The internal function is called once by default() => {console.log(counter.num)}
(hereinafter tofn
Instead of), runfn
Triggered whencounter.num
namelyget 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 // Dep // Dep // Dep // Dep // Dep // Dep // Dep // Dep // Dep // Dep // Dep // Dep // Dep //export type Dep = Set<ReactiveEffect>
// export type KeyToDepMap = Map<string | symbol, Dep>
// exportConst targetMap: WeakMap<any, KeyToDepMap> = new WeakMap() // get after targetMap value {counter: {num: [fn]}}Copy the code
- when
counter.num = 1
, will triggercounter
theset trap
Trigger (), trigger(), targetmap.counter. Num callback function is fn in targetMap. The callback performs fn
Consider: if I change counter. Times, will the callback fn () => {console.log(counter. Num)} be executed? Why is that? ‘
Num = 1; fn = counter. Num = 1;
Source code parsing
Reactive function
The core code in Reactice is createReactiveObject, which creates a proxy object
Reactive (target: object) {// NoreadonlyCreate a reactive object, and the object created is not the same as the source objectreturn 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 createReactiveObjectHandlers = collectiontypes.has (target. Constructor); // Handlers = collectiontypes.has (target. collectionHandlers : So I'm going to go to the Handlers implementation folder, I'm going to go to the baseHandlers implementation folder, and if you're not familiar with proxy, https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy observed = new can get familiar with the document 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 functionGet (target: any key: string | symbol, receiver: any) {/ / the resulting const res = Reflect. Get (target, key, receiver), / /... 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 // Set behavior core logic 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 / / / /}} / / explain exactly what it is: under the three target is the proxy object, the key is object trigger after get action attribute / / such as counter. The num triggered the get behavior, num is key. Dep is the callback function. If counter. Num is called in effect, 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() 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 forSET | ADD | DELETE / / depsMap. Get out (key) depend on the callbackif(key ! ComputedRunners (effects, computedRunners, depsMap.get(key as string | symbol)) } // also runfor 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: ScheduleRun (effect, target, scheduleRun) => {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 functionEffect (fn: Function, options: ReactiveEffectOptions = EMPTY_OBJ): ReactiveEffect {// Determine whether the callback has been wrappedif((fn as ReactiveEffect).isEffect) {fn = (fn as ReactiveEffect).raw} Const effect = createReactiveEffect(fn, options) // If it is not lazy, it will be called 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 stopreturn effect
}
Copy the code