Note: The code may have changed while I was writing this article. The code is more likely to change as you read this article. If there are inconsistencies and significant inconsistencies in the source code implementation or understanding, please point them out. Thank you very much.

On 10.5, the National Day holiday, righty released the alpha version of [email protected] code. I’ve got nothing else to do, so I’ve been learning TypeScript recently. It’s a good time to read some of the code.

Packages /vue/index = 7 lines of code. I searched several files until Runtime-core, and then SUDDENLY I knew. Comment lines, API like. No, I can’t. Anyway, I’m starting to get too much code. Feel the National Day to watch is certainly impossible, then pick a always interview to ask others the core of the principle of two-way binding to achieve it.

As you all know, Vue3 uses Proxy to replace defineProperty to implement data response update. How do you do that? Open the source directory and you can see at a glance that the core is packages/reactivity.

Reactivity

Click on its Readme, and through Google Translate, we can see what it basically means:

This package is embedded in vue’s renderer (@vue/ Run-time -dom). However, it can also be published separately and referenced by third parties (not dependent on Vue). However, if your renderer is exposed to the framework users, it may already have a built-in response mechanism, which is completely different from our reactivity and may not be compatible (I’m talking about you, react-dom).

For the API, just look at the source code or look at Types. Note: Other than Map, WeakMap, Set and WeakSet, some built-in objects cannot be observed (e.g., Date, RegExp, etc.).

Well, it’s not clear from the Readme alone what exactly it looks like. It is, after all, an alpha. That we still listen to it, direct source code.

A brush source code, a face meng

Looking at the reactivity entry file, you can see that it only exposes apis in six files. The values are: REF, reactive, computed, Effect, LOCK, and Operations. The lock file is an enumeration of the types of data operations, and the operations file is an enumeration of the types of data operations.

So the focus of reActivity is on the four files of REF, reactive, computed, and Effect, but these four files are not so simple. I spent half a day wanking from beginning to end, and found that I knew every letter; I know every word, thanks to Google; Almost all of these expressions are understandable to my half-baked TypeScript skills. But when it comes to functions, I’m a little confused….. Reactive in the ref, reactive in the ref, and then some weird operation inside the function, and then you get confused.

I just summed it up, largely because I don’t know what these key apis are for. Source CODE I do not understand, API meaning I do not understand. We know that you can’t solve a single binary equation.

So what do we do? There’s actually another equation, which is the single test. Reading from single test is an excellent way to read the source code. Not only can you quickly learn the meaning and usage of the API, but you can also learn many boundary cases. In the process of reading, will also think, if it is their own words, how to achieve, can deepen the understanding of the source code with learning.

Start with the single test

Because I little wanked down the source code, so roughly can know the order of reading. Of course, we can also estimate the order based on the number of lines of code. Reactive -> ref -> effect -> computed -> readonly -> Collections

Reactive

Reactive data, as its name suggests, means reactive data, which is the core of the library. So let’s see what it’s capable of.

First single test:

test('Object'.(a)= > {
  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'])})Copy the code

If we pass an object to reactive, it will return a new object. The two objects have the same type, the same length of data, but different references. Then we immediately understand that this must be the use of Proxy! vue@3 The core of the responsive system core.

So let’s look at Reactive’s statement:

Reactive only accepts object data and returns an UnwrapNestedRefs data type. Reactive only accepts object data and returns an UnwrapNestedRefs data type.

Second single test:

test('Array'.(a)= > {
  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

Reactive receives an array and returns a new array, not exactly equal to the original array, but with the same data. This is the same as the case of the objects in Test one. But this single test doesn’t consider nesting, I might add

test('Array'.(a)= > {
  const original: any[] = [{ foo: 1, a: { b: { c: 1 } }, arr: [{ d: {} }] }]
  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)
  / / observed. A. is reactive
  expect(isReactive(observed[0].a.b)).toBe(true)
  / / observed [0]. Arr [0]. D is reactive
  expect(isReactive(observed[0].arr[0].d)).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

The new data returned is still isReactive as long as the value of the property is an object.

The third single test is nothing to talk about, and the fourth single test, which tests nested objects, is covered in my second single test supplement.

Fifth single test:

test('observed value should proxy mutations to original (Object)'.(a)= > {
  const original: any = { foo: 1 }
  const observed = reactive(original)
  // set
  observed.bar = 1
  expect(observed.bar).toBe(1)
  expect(original.bar).toBe(1)
  // delete
  delete observed.foo
  expect('foo' in observed).toBe(false)
  expect('foo' in original).toBe(false)})Copy the code

** In this single test, we finally saw responsiveness. Reactive response data is returned after execution. Any write or delete operations performed on it can be synchronized to the original data. What about going the other way and just changing the original data?

test('observed value should proxy mutations to original (Object)'.(a)= > {
  let original: any = { foo: 1 }
  const observed = reactive(original)
  // set
  original.bar = 1
  expect(observed.bar).toBe(1)
  expect(original.bar).toBe(1)
  // delete
  delete original.foo
  expect('foo' in observed).toBe(false)
  expect('foo' in original).toBe(false)})Copy the code

We found that by modifying the original data directly, the response data can also get the latest data.

The sixth single test

test('observed value should proxy mutations to original (Array)'.(a)= > {
  const original: any[] = [{ foo: 1 }, { bar: 2 }]
  const observed = reactive(original)
  // set
  const value = { baz: 3 }
  const reactiveValue = reactive(value)
  observed[0] = value
  expect(observed[0]).toBe(reactiveValue)
  expect(original[0]).toBe(value)
  // delete
  delete observed[0]
  expect(observed[0]).toBeUndefined()
  expect(original[0]).toBeUndefined()
  // mutating methods
  observed.push(value)
  expect(observed[2]).toBe(reactiveValue)
  expect(original[2]).toBe(value)
})
Copy the code

The sixth single test demonstrated one of the great benefits of implementing responsive data through a Proxy: the ability to hijack all data changes to an array. Remember in vue@2, you had to manually set the array? In vue@3, finally do not have to do some strange operation, the heart of the update array.

Seventh single test:

test('setting a property with an unobserved value should wrap with reactive'.(a)= > {
  const observed: any = reactive({})
  const raw = {}
  observed.foo = raw
  expect(observed.foo).not.toBe(raw)
  expect(isReactive(observed.foo)).toBe(true)})Copy the code

Again, this is the second big benefit of having responsive data through a Proxy. In vue@2, reactive data must have a key declared at the beginning, and a default value must be set if it does not exist at the beginning. With this technical solution, the attribute values of vue@3’s responsive data can finally be added and removed at any time.

Eighth and ninth single test

test('observing already observed value should return same Proxy'.(a)= > {
  const original = { foo: 1 }
  const observed = reactive(original)
  const observed2 = reactive(observed)
  expect(observed2).toBe(observed)
})

test('observing the same value multiple times should return same Proxy'.(a)= > {
  const original = { foo: 1 }
  const observed = reactive(original)
  const observed2 = reactive(original)
  expect(observed2).toBe(observed)
})
Copy the code

These two single tests show that for the same raw data, the result returned by executing reactive multiple times or executing reactive nested will be the same corresponding data. A cache is maintained in the reactive file. The original data is used as key and the response data is used as value. If the key already has a value, the value is directly returned. That JS basic OK students should know that such a result can be achieved by WeakMap.

The tenth single test

test('unwrap'.(a)= > {
  const original = { foo: 1 }
  const observed = reactive(original)
  expect(toRaw(observed)).toBe(original)
  expect(toRaw(original)).toBe(original)
})
Copy the code

Through this single test, we learned about the toRaw API, which can obtain raw data through response data. That indicates that another WeakMap needs to be maintained for reverse mapping in the reactive file.

The eleventh single test, do not post the code, this single test enumerates the data type that cannot become the response data, namely JS five basic data type + Symbol (by oneself test, function also does not support). When data of the built-in special types, such as Promise, RegExp and Date, is passed to reactive, the original data will be returned without any error.

One last single test

test('markNonReactive'.(a)= > {
  const obj = reactive({
    foo: { a: 1 },
    bar: markNonReactive({ b: 2 })
  })
  expect(isReactive(obj.foo)).toBe(true)
  expect(isReactive(obj.bar)).toBe(false)})Copy the code

This refers to an API called markNonReactive, which makes it impossible for object data to become reactive. This API should be used infrequently in real business and may be used for specific performance optimizations.

After seeing reactive in a single test, we know something about reactive: it can accept an object or array and return new response data. The response data is like the shadow of the original data, and any operation on either side can be synchronized to the other.

But this… I don’t think it’s any good. But from the single test performance, is based on Proxy, do some boundary and nesting processing. This leads to a very critical question: how does it notify the view to update at vue@3? Or how does it inform its users that something needs to be done when the data changes in response? ** This behavior must be wrapped in the Proxy’s set/get handler. We don’t know yet, but we’ll have to keep looking at the other single tests.

Since we know from the beginning that the return value of reactive is of type UnwrapNestedRefs, at first glance it’s a special type of Ref, so let’s move on and look at refs. (In fact, this UnwrapNestedRefs is used to get the type of the generic type nested in refs. Remember that Unwrap is a verb. This is a bit of a wrap, but I’ll talk about source parsing later.)

Ref

Let’s look at the first test of ref:

it('should hold a value'.(a)= > {
  const a = ref(1)
  expect(a.value).toBe(1)
  a.value = 2
  expect(a.value).toBe(2)})Copy the code

So let’s look at the declaration of the ref function, any data that you pass in, it returns a ref data.

The value of the Ref data is the return type of the reactive function. Only reactive must require generics to inherit from objects (in js, reactive needs to be an object), whereas Ref data has no limit. In other words, the Ref type is a special data type based on Reactive data, and supports other data types in addition to objects.

Going back to the single test, we can see that passing a number to the ref function can also return a ref object whose value is the number passed at the time, and that it is allowed to change the value.

Let’s look at the second single test:

it('should be reactive'.(a)= > {
  const a = ref(1)
  let dummy
  effect((a)= > {
    dummy = a.value
  })
  expect(dummy).toBe(1)
  a.value = 2
  expect(dummy).toBe(2)})Copy the code

This single test is much more informative, with the effect concept suddenly added. Dummy is a function passed to effect that internally assigns the value(a.value) returned by ref to dummy. The function will then be executed once by default, making dummy 1. When a.value changes, the effect function is reexecuted, making dummy the latest value.

That is, if a method is passed to effect, it is executed immediately and then re-executed every time the internally dependent REF data changes. This clears up a puzzle from our previous reading of Reactive data: How do we notify the user of reactive data when it changes? Obviously, by effect. Whenever reactive data changes, it triggers the execution of the effect method that depends on it.

It feels like it’s not hard to do, so here’s what I would do:

  1. First you need to maintain a two-dimensional Map of effects;
  2. toeffectFunction passes a response function;
  3. This response function is executed immediately. If it internally references the response data, which I have hijacked set/ GET through the Proxy, I can collect the dependencies of this function and update the EFFECTS 2D Map
  4. When any subsequent changes to the REF data (triggering the set) occur, check the 2D Map, find the corresponding effect, and trigger them to execute.

The catch is that the ref function also supports non-object data, whereas the Proxy only supports objects. So in the library reactivity for non-object data will be a layer of objectified packaging, and then value through. Value.

Let’s look at the third single test:

it('should make nested properties reactive'.(a)= > {
  const a = ref({
    count: 1
  })
  let dummy
  effect((a)= > {
    dummy = a.value.count
  })
  expect(dummy).toBe(1)
  a.value.count = 2
  expect(dummy).toBe(2)})Copy the code

The original data passed to the ref function becomes an object, and any operation on its proxy data also triggers effect execution. After reading it, I first had a few curiosity:

  1. What if I nested one more layer?
  2. Because the original data is an object, if I modify the original data directly, will it be synchronized to the proxy data?
  3. If you modify the original data directly, will effect be triggered?

So I assumed 1. It can be nested, 2. It synchronizes, and 3. Modified order test to:

it('should make nested properties reactive'.(a)= > {
    const origin = {
      count: 1,
      b: {
        count: 1}}const a = ref(origin)
    Dummy tracks a.vale. count and dummyB tracks a.vale. b.count
    let dummy, dummyB
    effect((a)= > {
      dummy = a.value.count
    })
    effect((a)= > {
      dummyB = a.value.b.count
    })
    expect(dummy).toBe(1)
  	// Modify the first layer of the proxy data
    a.value.count = 2
    expect(dummy).toBe(2)

  	// Modify the nested data of the proxy object
    expect(dummyB).toBe(1)
    a.value.b.count = 2
    expect(dummyB).toBe(2)

  	// Modify the first layer of the original data
    origin.count = 10
    expect(a.value.count).toBe(10)
    expect(dummy).toBe(2)
  	// Modify the nested data of the original data
    origin.b.count = 10
    expect(a.value.b.count).toBe(10)
    expect(dummyB).toBe(2)})Copy the code

The result is exactly what I expected (actually I tried it out originally, just to write the article smoothly as I expected) :

  1. Modifying proxy data triggers an effect that depends on an object regardless of how it is nested
  2. When the original data is modified, the agent data can be synchronized when the agent data gets new data, but the effect that depends on the agent data will not be triggered.

So we can conclude that ** updates to ****Ref** ** will trigger the execution of an effect that depends on it. ** What about Reactive data? Let’s move on.

The fourth single test

it('should work like a normal property when nested in a reactive object'.(a)= > {
  const a = ref(1)
  const obj = reactive({
    a,
    b: {
      c: a,
      d: [a]
    }
  })
  let dummy1
  let dummy2
  let dummy3
  effect((a)= > {
    dummy1 = obj.a
    dummy2 = obj.b.c
    dummy3 = obj.b.d[0]
  })
  expect(dummy1).toBe(1)
  expect(dummy2).toBe(1)
  expect(dummy3).toBe(1)
  a.value++
  expect(dummy1).toBe(2)
  expect(dummy2).toBe(2)
  expect(dummy3).toBe(2)
  obj.a++
  expect(dummy1).toBe(3)
  expect(dummy2).toBe(3)
  expect(dummy3).toBe(3)})Copy the code

In the fourth single test, reactive was finally introduced. In the previous single test of reactive, simple objects were passed. Here, some of the attribute values in the passed object are Ref data. And by doing so, the.value value is no longer needed for the Ref data, even for internally nested Ref data. Using TS type derivation, we can clearly see that.

The return type to reactive is called UnwrapNestedRefs

. Since the generic T may be a Ref

, this return type means that the generic T unwraps the nested Ref. If the function reactive is passed a Ref data, the data type returned by the function is the same as the original data type of the Ref data. ** This is not how contact with TS people should be not understand, later source code parsing in detail.

In addition, this single test has solved our questions in the last single test. Modifying Reactive data will also trigger the update of effect.

Fifth single test

it('should unwrap nested values in types'.(a)= > {
  const a = {
    b: ref(0)}const c = ref(a)
  expect(typeof (c.value.b + 1)).toBe('number')})Copy the code

The fifth test was interesting. We found that for the nested Ref data, we only need to use.value initially, and the internal proxy data does not need to call.value repeatedly. It indicates that in the last single test, the nested Ref data passed to the reactive function can be unnested. In fact, it has nothing to do with the reactive function, but the ability of the Ref data itself. In fact, according to the derivation of TS type and type, we can also see that:

What if I had more layers, like this:

const a = {
  b: ref(0),
  d: {
    b: ref(0),
    d: ref({
      b: 0,
      d: {
        b: ref(0)}})}}const c = ref(a)
Copy the code

Is set to set to anyway, it was not a set of sets, depending on the type of TS deduction, we found that this kind of situation is no problem, as long as the beginning. The value can be at a time.

However, the first version of this capability, released on October 5, falls short. It cannot derive data nested in more than 9 layers. So this commit solves that problem, and those of you who are interested in TS type derivation can look at it.

The sixth single test

test('isRef'.(a)= > {
  expect(isRef(ref(1))).toBe(true)
  expect(isRef(computed((a)= > 1))).toBe(true)

  expect(isRef(0)).toBe(false)
  // an object that looks like a ref isn't necessarily a ref
  expect(isRef({ value: 0 })).toBe(false)})Copy the code

There’s not much to say about this single test, but there’s some useful information about computed, which we haven’t touched yet, but we know that for computed, it returns a ref data as well. In other words, if there is an effect that depends on computed returned data, the effect is executed when it changes.

One last single test

test('toRefs'.(a)= > {
  const a = reactive({
    x: 1,
    y: 2
  })

  const { x, y } = toRefs(a)

  expect(isRef(x)).toBe(true)
  expect(isRef(y)).toBe(true)
  expect(x.value).toBe(1)
  expect(y.value).toBe(2)

  // source -> proxy
  a.x = 2
  a.y = 3
  expect(x.value).toBe(2)
  expect(y.value).toBe(3)

  // proxy -> source
  x.value = 3
  y.value = 4
  expect(a.x).toBe(3)
  expect(a.y).toBe(4)

  // reactivity
  let dummyX, dummyY
  effect((a)= > {
    dummyX = x.value
    dummyY = y.value
  })
  expect(dummyX).toBe(x.value)
  expect(dummyY).toBe(y.value)

  // mutating source should trigger effect using the proxy refs
  a.x = 4
  a.y = 5
  expect(dummyX).toBe(4)
  expect(dummyY).toBe(5)})Copy the code

This single test is for the toRefs API. The difference between toRefs and ref is that ref changes the incoming data to type REF, whereas toRefs requires that the incoming data be object, and then converts the first level data of that object to type REF. I don’t know what it’s going to do, I just know what it’s going to do.

So far, the single test of ref has been read, and it can be felt that the most important purpose of REF is to realize the hijacking of non-object data. Other words, there seems to be no other special use. In fact, in the effect test file, only reactive data triggers the effect method.

Now let’s look at effect’s test file.

Effect

Effect’s behavior is already clear from the above test file. The main thing is to listen for changes in reactive data, triggering the execution of the listening function. The description is simple, but effect’s single measure is large, with 39 use cases, over 600 lines of code, and lots of boundary case considerations. So for effect, I won’t list them one by one. I’m going to go through it for you, and then I’m going to break it down into small points, and then I’m going to go straight to the key points, and then I’m going to post the corresponding test code if necessary.

Basic ability

  • The method passed to effect is executed immediately. (unless the second parameter passed {lazy: true}, this must look from the source, not covered, single test students are interested in, can go to mention PR).
  • reactiveYou can observe changes in the data on the prototype chain and be listened to by effect, and you can also inherit property accessors (get/set) on the prototype chain.
it('should observe properties on the prototype chain'.(a)= > {
  let dummy
  const counter = reactive({ num: 0 })
  const parentCounter = reactive({ num: 2 })
  Object.setPrototypeOf(counter, parentCounter)
  effect((a)= > (dummy = counter.num))

  expect(dummy).toBe(0)
  delete counter.num
  expect(dummy).toBe(2)
  parentCounter.num = 4
  expect(dummy).toBe(4)
  counter.num = 3
  expect(dummy).toBe(3)})Copy the code
  • Any read to any response data can be performed as a response, and any write to any response data can be listened for. Unless:
    • The keys to the updated data are built-in special Symbol values, such asSymbol.isConcatSpreadable(Rarely involved in daily use)
    • Although the write is performed, the data is not changed and the listening function is not triggered
it('should not observe set operations without a value change'.(a)= > {
  let hasDummy, getDummy
  const obj = reactive({ prop: 'value' })

  const getSpy = jest.fn((a)= > (getDummy = obj.prop))
  const hasSpy = jest.fn((a)= > (hasDummy = 'prop' in obj))
  effect(getSpy)
  effect(hasSpy)

  expect(getDummy).toBe('value')
  expect(hasDummy).toBe(true)
  obj.prop = 'value'
  expect(getSpy).toHaveBeenCalledTimes(1)
  expect(hasSpy).toHaveBeenCalledTimes(1)
  expect(getDummy).toBe('value')
  expect(hasDummy).toBe(true)})Copy the code
  • Operations on the raw data of the response data do not trigger the listening function
  • A listener function allows the introduction of another listener function.
  • Each effect execution returns a completely new listener function, even if the same function was passed.
it('should return a new reactive version of the function'.(a)= > {
  function greet() {
    return 'Hello World'
  }
  const effect1 = effect(greet)
  const effect2 = effect(greet)
  expect(typeof effect1).toBe('function')
  expect(typeof effect2).toBe('function')
  expect(effect1).not.toBe(greet)
  expect(effect1).not.toBe(effect2)
})
Copy the code
  • Can be achieved bystopAPI, terminating the listening function to continue listening. (It feels like we could add another onestartIf you are interested, you can give Him a PR.)
it('stop'.(a)= > {
  let dummy
  const obj = reactive({ prop: 1 })
  const runner = effect((a)= > {
    dummy = obj.prop
  })
  obj.prop = 2
  expect(dummy).toBe(2)
  stop(runner)
  obj.prop = 3
  expect(dummy).toBe(2)

  // stopped effect should still be manually callable
  runner()
  expect(dummy).toBe(3)})Copy the code

Special logic

  • ** can avoid infinite loops caused by implicit recursion, such as changes in the response data within the listening function or the interaction of multiple listening functions. It does not prevent explicit recursion, such as listening to function loop calls themselves.
it('should avoid implicit infinite recursive loops with itself'.(a)= > {
  const counter = reactive({ num: 0 })

  const counterSpy = jest.fn((a)= > counter.num++)
  effect(counterSpy)
  expect(counter.num).toBe(1)
  expect(counterSpy).toHaveBeenCalledTimes(1)
  counter.num = 4
  expect(counter.num).toBe(5)
  expect(counterSpy).toHaveBeenCalledTimes(2)
})

it('should allow explicitly recursive raw function loops'.(a)= > {
  const counter = reactive({ num: 0 })
  const numSpy = jest.fn((a)= > {
    counter.num++
    if (counter.num < 10) {
      numSpy()
    }
  })
  effect(numSpy)
  expect(counter.num).toEqual(10)
  expect(numSpy).toHaveBeenCalledTimes(10)})Copy the code
  • ** If the dependencies within effect are logically branched, the listening function will update the dependencies after each execution. ** As shown in the following: Whenobj.run 为 falseWhen,conditionalSpyRe-execute after updating the listener dependency, subsequent regardlessobj.propThe listener function is no longer executed.
it('should not be triggered by mutating a property, which is used in an inactive branch'.(a)= > {
  let dummy
  const obj = reactive({ prop: 'value', run: true })

  const conditionalSpy = jest.fn((a)= > {
    dummy = obj.run ? obj.prop : 'other'
  })
  effect(conditionalSpy)

  expect(dummy).toBe('value')
  expect(conditionalSpy).toHaveBeenCalledTimes(1)
  obj.run = false
  expect(dummy).toBe('other')
  expect(conditionalSpy).toHaveBeenCalledTimes(2)
  obj.prop = 'value2'
  expect(dummy).toBe('other')
  expect(conditionalSpy).toHaveBeenCalledTimes(2)})Copy the code

ReactiveEffectOptions

Effect also accepts a second parameter, ReactiveEffectOptions, as follows:

export interfaceReactiveEffectOptions { lazy? :booleancomputed? :booleanscheduler? :(run: Function) = > voidonTrack? :(event: DebuggerEvent) = > voidonTrigger? :(event: DebuggerEvent) = > voidonStop? :(a)= > void
}
Copy the code
  • Lazy: delays computation. If true, the effect passed in is not executed immediately.
  • computed: not in single test, I don’t know what to do, see the name may be withcomputedIt does matter. Let it go.
  • Scheduler: A scheduler function that takes the run input function passed to effect. If scheduler is passed, the listener function can be called through it.
  • onStopThrough:stopEvent that is fired when a listening function is terminated.
  • OnTrack: For debugging only. Triggered during dependency collection (get phase).
  • OnTrigger: For debugging purposes only. Fires after the update is triggered and before the listener function is executed.

The logic of Effect is a lot, but the core concepts are easy to understand. It is important to focus on some special internal optimizations, which you will need to focus on when reading the source code. And then there’s a computed that we’ve been exposed to but haven’t read yet.

Computed

Calculate the properties. Those of you who have written vue know what that means. Let’s see how that works in reactivity.

First single test

it('should return updated value'.(a)= > {
  constvalue = reactive<{ foo? :number} > ({})const cValue = computed((a)= > value.foo)
  expect(cValue.value).toBe(undefined)
  value.foo = 1
  expect(cValue.value).toBe(1)})Copy the code

A getter function is passed to the computed. The function internally relies on a Reactive data. After execution, the function returns a computed object whose value is the return value of the function. When its dependent Reactive data changes, the calculation data stays synchronized, like a Ref. In fact, in the REF test file we already know that computed results are also ref data.

ComputedRef inherits from Ref and has one more read-only effect attribute, of Type ReactiveEffect. As you can guess, the value of the effect property here is what effect returns when it executes on the computed function. In addition, the value is read-only, indicating that the value of the returned result of the computed is read-only.

Second single test

it('should compute lazily'.(a)= > {
  constvalue = reactive<{ foo? :number} > ({})const getter = jest.fn((a)= > value.foo)
  const cValue = computed(getter)

  // lazy
  expect(getter).not.toHaveBeenCalled()

  expect(cValue.value).toBe(undefined)
  expect(getter).toHaveBeenCalledTimes(1)

  // should not compute again
  cValue.value
  expect(getter).toHaveBeenCalledTimes(1)

  // should not compute until needed
  value.foo = 1
  expect(getter).toHaveBeenCalledTimes(1)

  // now it should compute
  expect(cValue.value).toBe(1)
  expect(getter).toHaveBeenCalledTimes(2)

  // should not compute again
  cValue.value
  expect(getter).toHaveBeenCalledTimes(2)})Copy the code

This single test tells us a lot about computed:

  • Different from theeffecttocomputedThe passedgetterFunction is not executed immediately, but only when the data is actually used.
  • Not every value needs to be recalledgetterFunction, andgetterIt is not retriggered when the data that the function depends on changes, but only when the computed data is used again after the dependent data has changedgetterFunction.

In the first single test, we assume that the effect property of ComputedRef is the listener generated by passing a getter function to the effect method. However, in the Effect single test, the listening function is executed as soon as the dependent data changes, which is not consistent with the computed performance here. There must be a catch!

At the end of the previous section Effect, we found that the second parameter to Effect is a configuration item, and one of the configurations, called computed, was not covered in the single test. It is estimated that this configuration item implements the delayed computation of the data calculated here.

The third single test

it('should trigger effect'.(a)= > {
  constvalue = reactive<{ foo? :number} > ({})const cValue = computed((a)= > value.foo)
  let dummy
  effect((a)= > {
    dummy = cValue.value
  })
  expect(dummy).toBe(undefined)
  value.foo = 1
  expect(dummy).toBe(1)})Copy the code

This single test proves the conjecture we made in the Ref chapter: if an effect is dependent on computed return data, the effect will execute when it changes.

What if computed returns haven’t changed, but the dependent data has changed? Does this result in effect execution? I assumed that for computed to stay the same, it would not cause the listener to re-execute, so I changed the order test:

it('should trigger effect'.(a)= > {
  constvalue = reactive<{ foo? :number} > ({})const cValue = computed((a)= > value.foo ? true : false)
  let dummy
  const reactiveEffect = jest.fn((a)= > {
    dummy = cValue.value
  })
  effect(reactiveEffect)
  expect(dummy).toBe(false)
  expect(reactiveEffect).toHaveBeenCalledTimes(1)
  value.foo = 1
  expect(dummy).toBe(true)
  expect(reactiveEffect).toHaveBeenCalledTimes(2)
  value.foo = 2
  expect(dummy).toBe(true)
  expect(reactiveEffect).toHaveBeenCalledTimes(2)})Copy the code

And then I realized I was wrong. The reactiveEffect depends on the cValue, and the cValue depends on the value. Whenever the value changes, the reactiveEffect will be triggered whether the cValue has changed or not. I feel that this can be optimized, and those who are interested in it can ask for PR.

The fourth single test

it('should work when chained'.(a)= > {
  const value = reactive({ foo: 0 })
  const c1 = computed((a)= > value.foo)
  const c2 = computed((a)= > c1.value + 1)
  expect(c2.value).toBe(1)
  expect(c1.value).toBe(0)
  value.foo++
  expect(c2.value).toBe(2)
  expect(c1.value).toBe(1)})Copy the code

This single test illustrates that a getter function for computed can depend on other computed data.

The fifth and sixth single tests belonged to changing flowers using computed. The idea is that using computed data is just as effective at triggering the execution of the listening function as using normal response data.

The seventh single test

it('should no longer update when stopped', () = > {constvalue = reactive<{ foo? : number }>({})const cValue = computed((a)= > value.foo)
  let dummy
  effect((a)= > {
    dummy = cValue.value
  })
  expect(dummy).toBe(undefined)
  value.foo = 1
  expect(dummy).toBe(1)
  stop(cValue.effect)
  value.foo = 2
  expect(dummy).toBe(1)})Copy the code

This single test also introduces the STOP API, which stops the response update of the computed data with stop(cvalue.effect).

The last two single tests

it('should support setter'.(a)= > {
  const n = ref(1)
  const plusOne = computed({
    get: (a)= > n.value + 1.set: val= > {
      n.value = val - 1
    }
  })
  expect(plusOne.value).toBe(2)
  n.value++
  expect(plusOne.value).toBe(3)
  plusOne.value = 0
  expect(n.value).toBe(- 1)
})
it('should trigger effect w/ setter'.(a)= > {
  const n = ref(1)
  const plusOne = computed({
    get: (a)= > n.value + 1.set: val= > {
      n.value = val - 1}})let dummy
  effect((a)= > {
    dummy = n.value
  })
  expect(dummy).toBe(1)
  plusOne.value = 0
  expect(dummy).toBe(- 1)})Copy the code

These two single tests are important. For computed, we just passed the getter function, and the value was read-only, so we couldn’t change the return value directly. Let’s see here that computed can also pass an object that contains both get and set methods. Get is the getter function, so it makes sense. The input to the setter function is the value of the data assigned to comptued Value. So in the above use case, plusone.value = 0, making n.value = 0-1, and triggering dummy to become -1.

At this point, we’re pretty much done with the concept of the ReActivity system. We’re left with readOnly and collections. Readonly is a read – only version of reactive. Reactive is a read – only version of reactive. Collections single test is to override the response updates of Map, Set, WeakMap, and WeakSet, so there should be little problem if we don’t look at it for the time being.

conclusion

Now that we have a clear understanding of the main internal apis, let’s review:

Reactive: A core method of the library that passes raw data of type object and returns data via Proxy. Reactive: A core method of the library that passes raw data of type object and returns data via Proxy. In the process, any read or write operations on the original data are hijacked. Thus, when changing the agent data, it can trigger the dependent listening function effect.

Ref: This is one of the most sensitive files to read code (it’s easy to get confused about its relationship to reactive code), but to really understand it, you need to read the code very carefully. It is advisable to leave it alone until other logic has been sorted out…. When it doesn’t exist. Just know that the most important thing about this file is that it provides a set of Ref types.

Effect: Takes a function and returns a new listener function, reactiveEffect. If the listening function internally relies on reactive data, the listening function is triggered when the data changes.

Computed: Computes data. An object that takes a getter function or has get/set behavior and returns responsive data. If it changes, the reactiveEffect is triggered.

Finally, I drew a rough picture for you to recall.

But this picture, I can’t guarantee, because I haven’t finished the source code. I’ll get around to writing a real source code parsing article this week.


In this article: Ant Insurance – Experience technology group – a Xiang

Gold address: Xiangxuechang