preface

In the last article in this series

Take you to thoroughly understand the response type principle of Vue3! TypeScript implements proxy-based reactive libraries from scratch. In the

We explained in detail the principle of common objects and arrays to achieve responsive, but Proxy can do much more than this, for ES6 new Map, Set, WeakMap, WeakSet can also achieve responsive support.

But for this part of the hijacking, the logic in the code is a completely separate set, and this article looks at how to implement this requirement based on the function hijacking implementation.

Proxy WeakMap Reflect Symbol. Iterator Proxy WeakMap Reflect Symbol. Iterator

Why special

In the previous article, if we read the attributes of the responsive data data through data.a, the get(target, key) in Proxy hijacking would be triggered.

Target is the original object corresponding to data, and key is A

We can register a dependency on key: a at this time, and then use reflect. get(data, key) to read the raw data and return it.

To review:

/** hijack get access to collect dependencies */
function get(target: Raw, key: Key, receiver: ReactiveProxy) {
  const result = Reflect.get(target, key, receiver)
  
  // Collect dependencies
  registerRunningReaction({ target, key, receiver, type: "get" })

  return result
}
Copy the code

Imagine this scenario when our reactive object is a Map data type:

const data = reactive(new Map([['a'.1]]))

observe((a)= > data.get('a'))

data.set('a'.2)
Copy the code

Get (‘a’) instead of data.get(‘a’), what happens if the get from the previous article remains the same?

The target in get(target, key) is the original map object, the key is get,

Reflect.get returns the map.get function, and the dependency is registered with the get key. This is not what we want, we want the dependency to be registered with the A key.

So the approach here is function hijacking. Imagine hijacking all access to the key in the map. For example, if the user uses the map.get function, then the get function is free to do anything like collect dependencies

The next goal is to replace all access to Map and Set apis (such as HAS, GET, Set, add) with our own written methods, so that users can use these apis without being aware of them, but internally hijacked by our own code.

implementation

Let’s change the directory structure from the previous article to look like this:

src/handlers
Handlers and objects├ ─ ─ base. TsHandlers: map and set├ ─ ─ collections. Ts// Export all└ ─ ─ index. TsCopy the code

The entrance

First look at the handlers/index.ts entry

import { collectionHandlers } from "./collections"
import { baseHandlers } from "./base"
import { Raw } from "types"

// @ts-ignore
Fetch the Handlers of the Proxy based on the type of the object
export const handlers = new Map([[Map, collectionHandlers],
  [Set, collectionHandlers],
  [WeakMap, collectionHandlers],
  [WeakSet, collectionHandlers],
  [Object, baseHandlers],
  [Array, baseHandlers],
  [Int8Array, baseHandlers],
  [Uint8Array, baseHandlers],
  [Uint8ClampedArray, baseHandlers],
  [Int16Array, baseHandlers],
  [Uint16Array, baseHandlers],
  [Int32Array, baseHandlers],
  [Uint32Array, baseHandlers],
  [Float32Array, baseHandlers],
  [Float64Array, baseHandlers],
])

Handlers */
export function getHandlers(obj: Raw) {
  return handlers.get(obj.constructor)
}

Copy the code

We have a Map: Handlers, we export a getHandlers method, we get the second Proxy parameter, handlers, based on the type of data we pass in,

BaseHandlers were covered in detail in the first article.

This article is mainly about collectionHandlers.

collections

Let’s take a look at the entry to Collections:

Handlers actually pass the second argument to Proxy with only a GET
// Transfer all user access to the map get and set APIS to the above hijacking function
export const collectionHandlers = {
  get(target: Raw, key: Key, receiver: ReactiveProxy) {
    // return to the hijacked API above
    target = hasOwnProperty.call(instrumentations, key)
      ? instrumentations
      : target
    return Reflect.get(target, key, receiver)
  },
}
Copy the code

Handlers have only one GET, so access to any API on a map or set (has, GET, set, add) is transferred to our own API. This is an application of function hijacking.

The key lies in the instrumentations object, our own implementation of these apis.

Hijack the implementation of the API

Get and set

export const instrumentations = {
  get(key: Key) {
    // Get the raw data
    const target = proxyToRaw.get(this)
    // Get the __proto__ of the raw data onto the prototype chain
    const proto: any = Reflect.getPrototypeOf(this)
    // Register get type dependencies
    registerRunningReaction({ target, key, type: "get" })
    // Call the get method on the prototype chain to evaluate and then continue to define responses for complex types
    return findReactive(proto.get.apply(target, arguments))
  },
  set(key: Key, value: any) {
    const target = proxyToRaw.get(this)
    const proto: any = Reflect.getPrototypeOf(this)
    // Is the new key
    const hadKey = proto.has.call(target, key)
    // Get the old value
    const oldValue = proto.get.call(target, key)
    // Find the result
    const result = proto.set.apply(target, arguments)
    if(! hadKey) {// When a new key is added, the observation function is triggered with type: add
      queueReactionsForOperation({ target, key, value, type: "add"})}else if(value ! == oldValue) {// When the value of an existing key changes, the observation function is triggered with type: set
      queueReactionsForOperation({ target, key, value, oldValue, type: "set"})}return result
  },
}

/** If the return value is a complex type, it is further defined as a response */
function findReactive(obj: Raw) {
  const reactiveObj = rawToProxy.get(obj)
  // Define responsivity only when observing functions are being run
  if (hasRunningReaction() && isObject(obj)) {
    if (reactiveObj) {
      return reactiveObj
    }
    return reactive(obj)
  }
  return reactiveObj || obj
}
Copy the code

The core GET and SET methods are almost identical to the implementation in the previous article, with findReactive ensuring that the value returned by GET further defines reactive data, thereby implementing deep responses.

At this point, this use case can run:

const data = reactive(new Map([['a'.1]]))
observe((a)= > console.log('a', data.get('a')))

data.set('a'.5)
// Re-print a 5
Copy the code

Next, implement it for some specific apis:

has

  has (key) {
    const target = proxyToRaw.get(this)
    const proto = Reflect.getPrototypeOf(this)
    registerRunningReactionForOperation({ target, key, type: 'has' })
    return proto.has.apply(target, arguments)},Copy the code

add

Add is a typical process for adding keys, which triggers the loop related observation functions.

  add (key: Key) {
    const target = proxyToRaw.get(this)
    const proto: any  = Reflect.getPrototypeOf(this)
    const hadKey = proto.has.call(target, key)
    const result = proto.add.apply(target, arguments)
    if(! hadKey) { queueReactionsForOperation({ target, key,value: key, type: 'add'})}return result
  },
Copy the code

delete

Delete is also implemented in much the same way as deleteProperty in the previous article, triggering the loop related observation function.

  delete (key: Key) {
    const target = proxyToRaw.get(this)
    const proto: any = Reflect.getPrototypeOf(this)
    const hadKey = proto.has.call(target, key)
    const result = proto.delete.apply(target, arguments)
    if (hadKey) {
      queueReactionsForOperation({ target, key, type: 'delete'})}return result
  },
Copy the code

clear

  clear () {
    const target: any = proxyToRaw.get(this)
    const proto: any = Reflect.getPrototypeOf(this)
    consthadItems = target.size ! = =0
    const result = proto.clear.apply(target, arguments)
    if (hadItems) {
      queueReactionsForOperation({ target, type: 'clear'})}return result
  },
Copy the code

When firing the observation function, some special processing is done for the clear type, which is also the observation function related to firing the loop.

export function getReactionsForOperation ({ target, key, type }) {
  const reactionsForTarget = connectionStore.get(target)
  const reactionsForKey = new Set()

+ if (type === 'clear') {
+ reactionsForTarget.forEach((_, key) => {
+ addReactionsForKey(reactionsForKey, reactionsForTarget, key)
+})
  } else {
    addReactionsForKey(reactionsForKey, reactionsForTarget, key)
  }

 if (
    type === 'add' 
    || type === 'delete' 
+ || type === 'clear'
) {
    const iterationKey = Array.isArray(target) ? 'length' : ITERATION_KEY
    addReactionsForKey(reactionsForKey, reactionsForTarget, iterationKey)
  }

  return reactionsForKey
}
Copy the code

Clear, each key collected observation function to get, and the loop observation function also get, can be said to trigger the most complete.

Observe the observekey function. If you read an observekey, you need to perform the observekey function again.

forEach

forEach (cb, ... args) {const target = proxyToRaw.get(this)
    const proto = Reflect.getPrototypeOf(this)
    registerRunningReaction({ target, type: 'iterate' })
    const wrappedCb = (value, ... rest) = >cb(findObservable(value), ... rest)returnproto.forEach.call(target, wrappedCb, ... args) },Copy the code

ForEach’s hijacking was a little more difficult.

The first time you register a dependency, you use the key iterate, which is easy to understand because it’s iterate.

In this way, when the user adds or deletes the collection data in the future, or uses the clear operation, the observation function of forEach will be triggered again

Focus on the next two pieces of code:

const wrappedCb = (value, ... rest) = >cb(findObservable(value), ... rest)returnproto.forEach.call(target, wrappedCb, ... args)Copy the code

WrappedCb wraps the CB function that the user passed to forEach, and then to forEach on the collection object prototype chain, another function hijacking. The user passes in map.foreach (cb), and we end up calling map.foreach (wrappedCb).

In this wrappedCb, we define the original value that should be obtained in CB as responsive data to the user through findObservable, so that the user can also collect and rely on the responsive operation in forEach. We have to praise the clever design.

keys && size

  get size () {
    const target = proxyToRaw.get(this)
    const proto = Reflect.getPrototypeOf(this)
    registerRunningReaction({ target, type: 'iterate' })
    return Reflect.get(proto, 'size', target)
  },
  keys () {
    const target = proxyToRaw.get(this)
    const proto: any = Reflect.getPrototypeOf(this)
    registerRunningReaction({ target, type: 'iterate' })
    return proto.keys.apply(target, arguments)},Copy the code

Keys and size return values that do not need to be defined as responsive, so simply return the original values.

values

Let’s look at another example that requires special treatment

  values () {
    const target = proxyToRaw.get(this)
    const proto: any = Reflect.getPrototypeOf(this)
    registerRunningReaction({ target, type: 'iterate' })
    const iterator = proto.values.apply(target, arguments)
    return patchIterator(iterator, false)},Copy the code

It is important to note that the values method of the collection object returns an iterator object, map. values.

This iterator object returns the next value in the Map each time it is called next()

In order to make the value obtained by next() also become a reactive proxy, we need to use patchIterator to hijack iterator

// Hijack iterator into a responsive iterator
function patchIterator (iterator) {
  const originalNext = iterator.next
  iterator.next = (a)= > {
    let { done, value } = originalNext.call(iterator)
    if(! done) { value = findReactive(value) }return { done, value }
  }
  return iterator
}
Copy the code

This is classic function-hijacking logic, taking the original {done, value} value and defining it as a reactive proxy.

Now that you understand the concept, the remaining associated handlers are easy to understand

entries

  entries () {
    const target = proxyToRaw.get(this)
    const proto: any = Reflect.getPrototypeOf(this)
    registerRunningReaction({ target, type: 'iterate' })
    const iterator = proto.entries.apply(target, arguments)
    return patchIterator(iterator, true)},Copy the code

There is also special treatment for the corresponding entries. When passing the iterator to the patchIterator, it needs to be marked with a special mark. This is the entries, and look at the changes made to the patchIterator:

/** / function patchIterator (iterator, isEntries) { const originalNext = iterator.next iterator.next = () => { let { done, value } = originalNext.call(iterator) if (! done) {+ if (isEntries) {
+ value[1] = findReactive(value[1])
      } else {
        value = findReactive(value)
      }
    }
    return { done, value }
  }
  return iterator
}
Copy the code

Each entry to the entries operation is an array of [key, val], so the subscript [1] defines only the value as responsive, and there is no special handling of keys.

Symbol.iterator

  [Symbol.iterator] () {
    const target = proxyToRaw.get(this)
    const proto: any = Reflect.getPrototypeOf(this)
    registerRunningReaction({ target, type: 'iterate' })
    const iterator = proto[Symbol.iterator].apply(target, arguments)
    return patchIterator(iterator, target instanceof Map)},Copy the code

Here again, the [symbol. iterator] built-in object is triggered during the for of operation, as shown in the MDN documentation at the beginning of this article. So use the iterator hijacking idea above.

The second parameter of patchIterator also needs special treatment because the “for of” operation on the Map data structure returns entries.

TypeScript small eggs

Since this article covers maps, it occurs to me that it is not friendly to do type inference for maps in TS, such as the following methods:

function createMap<T extends object.K extends keyof T> (obj: T) {
  const map = new Map<K, T>()
  Object.keys(obj).forEach((key) = > {
    map.set(key as K, obj[key])
  })
  return map
}

// The type is {
// a: number;
// b: string;
// }
const a = createMap({a: 1.b: '2'}).get('a')
Copy the code

Since Map is assigned by calling set, TS cannot make a good type inference and accurately infer the corresponding type of key value. What if we use the hijacking idea in this paper?

conclusion

The code for this article is in this repository github.com/sl1673495/p…

The idea of function hijacking appears in a variety of front-end libraries, which is almost an advanced must learn a skill, I hope that through the study of this article, you can understand some of the powerful functions of function hijacking. You can also imagine how responsive Vue3 is with proxies.