Now that we have simple renderer modules and responsive modules, we are going to step up from one technology to two.

Because the responsive module has very little coupling with other modules, we’ll start with that.

And, starting to build unit tests, we started to make the toy a little more formal.

Upgrade reactive

At present, there are three main aspects to be improved:

  1. Some checks on data corner cases.

  2. In the case of arrays, some arrays have built-in support for methods like push, Shift, and so on.

  3. The previous proxy handler only intercepted the set and get methods, but we also needed has, deleteProperty, and ownKeys.

Let’s take it one at a time, and think about what special cases we have to deal with.

The first step is to consider the possible types of targets passed in by Reactive (Target), in addition to the usual types of Object and Array.

There may also be Collection types such as Map, Set, WeakMap, etc., which require a different handler than the Object type to handle.

Finally, if a target is not one of these types or has a flag indicating that it does not need to be reactive (such as SKIP), it is considered invalid.

So we can update typescript declarations:

export const enum ReactiveFlags {
  SKIP = '__v_skip',
  IS_REACTIVE = '__v_isReactive',
  RAW = '__v_raw',}exportinterface Target { [ReactiveFlags.SKIP]? : boolean; [ReactiveFlags.IS_REACTIVE]? : boolean; [ReactiveFlags.RAW]? : any; }const enum TargetType {
  INVALID = 0,
  COMMON = 1,
  COLLECTION = 2,}Copy the code

With these flags, we can use them to quickly determine whether the target is a legitimate observation object, and whether it has already been observed as a proxy.

Such as using them to determine whether or not to reactive:

export function isReactive(value: unknown) :boolean {
  return!!!!! (value && (valueas Target)[ReactiveFlags.IS_REACTIVE]);
}
Copy the code

Check for these corner cases when createReactiveObject. Such as:

function createReactiveObject(target: Target, isReadonly: boolean, baseHandlers: ProxyHandler
       
        , collectionHandlers: ProxyHandler
        
       ) {
  // ...
  const targetType = getTargetType(target);
  if (targetType === TargetType.INVALID) {
    return target;
  }
  // ...
}
Copy the code

First think about what are the corner cases. Then you can go to the branch that starts with 05 and compare it with my new code.

These corner cases are actually very simple, but the point is that I want to take this opportunity to get unit testing up and running.

Create a new file: Packages /reactivity/__tests__/ react.spec.ts

You don’t need to build your own test environment, I’ve built a Jest based configuration from the first Branch, if you’ve been with it since the beginning.

Join us for our first quiz.

describe('reactivity/reactive'.() = > {
  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);
  });
}
Copy the code

If you don’t know jEST, you can go to the official website to see the basic grammar. You can write simple tests in a few hours. You can continue to write while learning.

After writing the first simple test, run jest Packages /reactivity/ –config=jest.config.js (you may need to install jest globally if you haven’t used jEST before).

Not surprisingly, you can see that the first test passed.

Support for array methods

As we mentioned earlier, Vue3 takes the Proxy approach rather than Vue2’s defineProperty method, making it much easier to implement listening arrays.

For an array push, let’s see what the proxy captures:

let array = [1.2.3];

const handler = {
  // intercept `get` method
  get: function (target, prop, reveiver) {
    console.log('the prop to get is: ', prop);
    return Reflect.get(... arguments); },// intercept `set` method
  set: function (target, prop, value, receiver) {
    console.log('the prop to set is: ', prop);
    return Reflect.set(target, prop, value, receiver); }};const proxy = new Proxy(array, handler);
// use proxy to access data
proxy.push(1);
/* terminal: the prop to get is: push the prop to get is: length the prop to set is: 3 the prop to set is: length */
Copy the code

The prop of the set is the index (key) of the new element and the length of the array.

(For convenience, the first get, the second get, the set is also the first set, and the second set is referred to as such.)

If an effect is already dependent on the length of the array, then all effects under the length key are triggered when pushed, and the effect content will be executed again.

The only thing to be careful about is that on the second get, the current code will track the length of the array and then establish a dependency on that length.

For example, if we have a push operation in an effect, the effect will depend on length (because of the second get).

Any time the length of the array changes, the contents of effect will be executed again, that is, the push in effect will be executed again, which will cause the length to change, and then…

So when we do an array like push, we want to stop track and reset it when we’re done.

You can save these fine-tuned array methods in an arrayInstrumentations.

const arrayInstrumentations: Record<string, Function> = {};

const LAM = ['push'.'pop'.'shift'.'unshift'.'splice'];
LAM.forEach((methodName) = > {
  const method = Array.prototype[methodName] as any;
  arrayInstrumentations[methodName] = function (this. args) {
    pauseTracking();
    const res = method.apply(this, args);
    resetTracking();
    return res;
  };
});
Copy the code

[add] [add] [add] [add] [add] [add] [add] [add] [add] [add] [add] [add] [add] [add]

// baseHandlers.ts
function get(target: Target, key: string | symbol, receiver: object) {
  // ...
  const targetIsArray = isArray(target);
  if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
    return Reflect.get(arrayInstrumentations, key, receiver);
  }
  // ...
}
Copy the code

In this way, the first GET actually returns our modified push (stopping track), and the actual execution of push will not cause the tracking of length due to the second GET, thus avoiding the endless loop of push in effect.

Next, when push raises the first set, an integer index (that is, a key) is captured.

The set or ADD handler is an ADD operation.

function set(target: object, key: string | symbol, value: unknown, receiver: object) {
  // ...
  const hadKey =
  isArray(target) && isIntegerKey(key)
  ? 
  Number(key) < target.length
  : hasOwn(target, key);
  // ...
}
Copy the code

Thus, when pushing a new value, hadKey is false and trigger of type ADD is executed.

In the trigger function, check ADD Operation to trigger all effects under Length if it is an integer key.

/* ... in trigger function */ 
if (type === TriggerOpTypes.ADD) {
  if (isIntegerKey(key)) {
    addEffects(depsMap.get('length')); }}// ...
Copy the code

In this way, the first set caused by push will trigger length-dependent effects.

Somebody said, what about the second set with key length? Will it cause additional execution effects?

It won’t.

Because in the set handler, we’re going to compare the value of the key to the new value. Only oldValue! Trigger only when == value.

When the second set is set, the value of the new length and the value of the new length are equal, so it will not trigger.

So that’s the idea of Array responsiveness. Write a small demo to test:

import { reactive, effect } from '.. /packages/reactivity/src/index';

const arr = [1.2.3];
const proxy = reactive(arr);

effect(() = > {
  console.log(proxy.length);
});

proxy.push(1);
// terminal:
/ / 3
/ / 4
Copy the code

In reactiveArray.spec.ts, it is a good practice to add unit tests after writing code.

In addition to the push, shift and other methods that change the Array (SET or ADD), there are also methods that do not change the Array, such as includes, indexOf, and lastIndexOf.

These methods are more like variations of GET and are mainly used for query purposes.

These methods also need to be rewritten. The core is to execute track() at the right time, and add it into arrayInstrumentations after fine-tuning.

Without further ado, if you understand the implementation of push, you should try to write your own includes, indexOf, and lastIndexOf.

If not, check out my new branch 05 (or Vue3 for the source code).

Other handler

We currently have traps for the proxy for set and GET operations, but there are no traps for other operations, such as delete type operations.

Detailed introduction can go to the Proxy on THE MDN to see, I am not the MDN repeater.

Once again, these actions are captured to establish a dependency for track, or trigger to tell a dependency’s corresponding effect to execute.

For example, if a deleteProperty intercepts a delete operation, we use Reflect to delete it from the raw data, and if it succeeds, we need to execute trigger.

function deleteProperty(target: object, key: string | symbol) :boolean {
  const hadKey = hasOwn(target, key);
  const oldValue = (target as any)[key];
  const result = Reflect.deleteProperty(target, key);
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue);
  }
  return result;
}
Copy the code

Has and ownKeys do not alter the raw data, but are more used for a query-like operation. So not trigger, but track:

function has(target: object, key: string | symbol) :boolean {
  const result = Reflect.has(target, key);
  track(target, TrackOpTypes.HAS, key);
  return result;
}

function ownKeys(target: object) : (string | number | symbol) []{
  track(
    target,
    TrackOpTypes.ITERATE,
    isArray(target) ? 'length' : ITERATE_KEY
  );
  return Reflect.ownKeys(target);
}
Copy the code

Again, I haven’t explained it, but if you don’t know which operations are caught by HAS and ownKeys under what circumstances, be sure to take a look at MDN.

As for the code, it is very clear at a glance. It is track. There is nothing to talk about in detail.

conclusion

The responsive module was upgraded to a second technology, and I added about 23 unit tests (all of which were already available in Vue3). I have to keep doing it, one step at a time.

If you follow through, you should be able to pass all the tests in branch 05-upgrade-Reactive-Module on Github.

In the next few days, I need to sort out my thoughts, especially the next step to upgrade the renderer, which involves DOM. There are a lot of details, so it is easy to write and write disorganized.

Second, the Github repo doesn’t even have a proper README and needs to be well maintained.

Give me two days to finish.