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
  • callreactive()Will generate aProxyobjectcounter.
  • calleffect()The internal function is called once by default() => {console.log(counter.num)}(hereinafter tofnInstead of), runfnTriggered whencounter.numnamelyget trap.get trapThe triggertrack(), can be intargetMapaddnumRely 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
  • whencounter.num = 1, will triggercountertheset trapTrigger (), 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