This is the first day of my participation in the wenwen Challenge

reactiveAs 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 toVue2xVersion 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 testsreactiveusage

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 isProxyObject. Support for raw data types:Object|Array|Map|Set|WeakMap|WeakSet
  • isReactive: Checks whether the data can be responded to
  • toRaw: the corresponding data can be converted into original data.
  • markNonReactive: Marks the data as unresponsive.

In combination witheffectuse

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
  • callreactive()Will generate aProxyobjectcounter.
  • calleffect()Internal function () is called once by default =>{console.log(counter.num)}(replaced by fn below), triggered when fn is runcounter.numnamelyGet the trap. get trapThe triggertrack(), can be intargetMapaddnumRely 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.