2
头图

In this article, we will continue to explore the proxy implementation of the Map/WeakMap/Set/WeakSet object in the --- reactive function.

Operation of Map/WeakMap/Set/WeakSet

Since WeakMap and WeakSet are versions of Map and Set that do not affect the garbage collection performed by GC, we only study Map and Set here.

Set properties and methods

  • size: number is the accessor property, returning the number of values in the Set object
  • add(value: any): Set Add an element to the tail of the Set object
  • clear(): void Remove all elements in the Set object
  • delete(value: any): boolean Remove the element in the Set with the same value as the input parameter, and return true if the removal is successful
  • has(value: any): boolean Determine whether there is an element in the Set that has the same value as the input parameter
  • values(): Iterator Returns a new iterator object containing all elements in the Set object in insertion order
  • keys(): Iterator --- has the same effect as values(): Iterator
  • @@iterator values(): Iterator the same effect as for of , called in ---6fcfc1a2e8c48eed795c3a79205a5d62---
  • entries(): Iterator Returns a new iterator object containing all the elements in the Set object in insertion order, but to be consistent with Map usage, the content returned by each iteration is [value, value]
  • forEach(callbackFn: { (value: any, set: Set) => any } [, thisArg]) Traverse each element of the Set object in insertion order

Map properties and methods

  • size: number is an accessor property, returning the number of values in the Set object
  • set(key: any, value: any): Map Add or update the value of a specified key to the Map object
  • clear(): void Remove all key-value pairs in the Map object
  • delete(key: any): boolean Remove the key-value pair specified in the Map object, return true if the removal is successful
  • has(key: any): boolean Determine whether there is a key-value pair in the Map with the same key as the input parameter value
  • values(): Iterator returns a new iterator object containing all the values in the Map object in insertion order
  • keys(): Iterator returns a new iterator object containing all the keys in the Map object in insertion order
  • @@iterator entries(): Iterator the same effect as for of , called in ---f1f3d39b1988e092fa7b6371e5e085ab---
  • entries(): Iterator returns a new iterator object containing all key-value pairs in the Map object in insertion order
  • forEach(callbackFn: { (value: any, key: any, map: Map) => any } [, thisArg]) Traverse each key-value pair of the Map object in insertion order
  • get(key: any): any Return the value corresponding to the specified key in the Map object, if not, return undefined

I'm serious about looking at the code line by line

 // reactive.ts

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}

Because Map/Set is not like Object or Array that can directly access its elements through properties, but through add , has , delete operation, Therefore, these methods of Map/Set need to be proxied like those of Array slice etc.

 // collectionHandlers.ts

type MapTypes = Map<any, any> | WeakMap<any, any>
type SetTypes = Set<any, any> | WeakSet<any, any>

// 代理Map/Set原生的方法
// 没有代理返回迭代器的方法??
const mutableInstrumentations = {
  get(this: MapTypes, key: unknown) {
    return get(this, key)
  }
  get size() {
    // 原生的size属性就是一个访问器属性
    return size(this as unknown as IterableCollections)
  },
  has,
  add,
  set,
  delete: deleteEntry, // delete 是关键字不能作为变量或函数名称
  clear,
  forEach: createForEach(false, false)
}

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    }
    else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    }
    else if (key === ReactiveFlags.RAW) {
      return target
    }

    // 代理Map/WeakMap/Set/WeakSet的内置方法
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

TypeScript Small Class : as Assertion this as unknown as IterableCollections
In TypeScript, the type of a variable (including composite types) can be defined through type declaration, and type deduction can deduce the actual type of the variable based on the literal on the right side of the assignment statement, or deduce the current actual type from the current variable usage scenario (Especially defined as composite types). But sometimes accurate type deduction cannot be performed through the current usage scenario. In this case, the developer can use the as assertion to inform the TypeScript compiler of the data type of the current scope of the variable (believe that you must know yourself better than the compiler does) code :D).
Then as unknown means that the type is changed to unknown , then the type is unknown What does it mean? unknown is the top type introduced by TypeScript3.0 (any other type is its subtype), intended to provide a safer way to replace any type ( any type is top type and bottom type, using it means and bypasses type checking), which has the following characteristics:

  1. Any other type can be assigned to a variable of type unknown
  2. A variable of type unknown can only be assigned to a variable of type any or unknown
  3. You cannot do anything else without performing type shrinking on a variable of type unknown
 // 1. 任何其它类型都可以赋值给`unknown`类型的变量 
let uncertain: unknown = 'Hello'
uncertain = 12
uncertain = { hello: () => 'Hello' }

// 2.`unknown`类型的变量只能赋值给`any`或`unknown`类型的变量 
let uncertain: unknown = 'Hello'
let noSure: any = uncertain
let notConfirm: unknown = uncertain

// 3. 如果不对`unknown`类型的变量执行类型收缩,则无法执行其它任何操作
let uncertain = { hello: () => 'Hello' }
uncertain.hello() // 编译报错 
// 通过断言as收缩类型
(uncertain as {hello: () => string}).hello()

let uncertain: unknown = 'Hello'
// 通过typeof或instanceof收缩类型
if (typeof uncertain === 'string') {
  uncertain.toLowerCase()
}

Then the intention of as unknown after as IterableCollections is very obvious, which is to perform type contraction on variables. this as unknown as IterableCollections is actually as IterableCollections .

Then let's take a look at the implementation of the proxy method one by one.

Map get method

get method is only owned by the Map object, so the main idea is to get the value from the Map object, track the change of the key value and convert the value to a responsive object and return it.
However, due to the need to deal with this scene readonly(reactive(new Map())) , a lot of code that is temporarily incomprehensible is added.

 const getProto = <T extends CollectionTypes>(v: T): any => Reflect.getProrotypeOf(v)

// 代理Map/WeakMap的get方法
function get(
  target: MapTypes, // 指向this,由于Map对象已经被代理,因此this为代理代理
  key: unknown,
  isReadonly = false,
  isShallow = false
) {
  /**
   * 1. 针对readonly(reactive(new Map()))的情况,
   *    target获取的是代理对象,而rawTarget的是Map对象
   * 2. 针对reactive(new Map())的情况,
   *    target和rawTarget都是指向Map对象
   */ 
  target = (target as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  /**
   * 若key为代理对象,那么被代理对象和代理对象的键都会被跟踪,即
   * const key = { value: 'foo' }
   * const pKey = reactive(key), 
   * const kvs = reactive(new Map())
   * kvs.set(pKey, 1)
   * 
   * effect(() => {
   *   console.log('pKey', kvs.get(pKey))
   * })
   * effect(() => {
   *   console.log('key', kvs.get(key))
   * })
   * 
   * kvs.set(pKey, 2)
   * // 回显 pkey 2 和 key 2
   * kvs.set(key, 3)
   * // 回显 key 2
   */  
  const rawKey = toRaw(key)
  if (key !== rawKey) {
    !isReadonly && track(rawTraget, TrackOpTypes.GET, key)
  }
  !isReadonly && track(rawTraget, TrackOpTypes.GET, rawKey)

  // 获取Map原型链上的has方法用于判断获取成员是否存在于Map對象上
  const { has } = getProto(rawTarget)
  const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
  /**
   * Map对象中存在则从Map对象或代理对象上获取值并转换为响应式对象返回。
   * 针对readonly(reactive(new Map()))为什么是从响应对象上获取值,而不是直接从Map对象上获取值呢?
   * 这是为了保持返回的值的结构,从响应式对象中获取值是响应式对象,在经过readonly的处理则返回的值就是readonly(reactive({value: 'foo'}))。
   */ 
  if (has.call(rawTarget, key)) {
    return wrap(target.get(key))
  }
  else if (has.call(rawTarget, rawKey)) {
    return wrap(target.get(rawKey))
  }
  else if (target !== rawTarget) {
    /**
     * 针对readonly(reactive(new Map())),即使没有匹配的键值对,也要跟踪对响应式对象某键的依赖信息
     * const state = reactive(new Map())
     * const readonlyState = readonly(state)
     * 
     * effect(() => {
     *  console.log(readonlyState.get('foo'))
     * })
     * // 回显 undefined
     * state.set('foo', 1)
     * // 回显 1
     */
    target.get(key)
  }

  // 啥都没有找到就默认返回undefined,所以啥都不用写
}

Map and Set size attribute

 function size(target: IterableCollections, isReadonly = false) {
  // 针对readonly(reactive(new Map())) 或 readonly(reactive(new Set()))只需获取响应式对象即可,因此reactive对象也会对size的访问进行相同的操作。
  target = (target as any)[RectiveFlags.RAW]
  // 跟踪ITERATE_KEY即所有修改size的操作均会触发访问size属性的副作用函数
  !iReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
  /**
   * 由于size为访问器属性因此若第三个参数传递receiver(响应式对象),而响应式对象并没有size访问器属性需要访问的属性和方法,则会报异常``。因此需要最终将Map或Set对象作为size访问器属性的this变量。
   */
  return Reflect.get(target, 'size', target)
}

Map and Set the has method

 function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
  const target = (this as any)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  // 和get方法代理一样,若key为代理对象则代理对象或被代理对象作为键的键值对发生变化都会触发访问has的副作用函数
  if (key !== rawKey) {
    !isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
  }
  !isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)

  return key === rawKey
    ? target.has(key)
    : target.has(key) || target.has(rawKey)
}

Set add method

 function add(this: SetTypes, value: unknown) {
  value = toRaw(value)
  const target = toRaw(this)
  const proto = getProto(target)
  const hadKey = proto.has.call(target, value)
  // 当Set对象中没有该元素时则触发依赖ITERATE_KEY的副作用函数,因此ADD操作会影响Set对象的长度
  if (!hadKey) {
    target.add(value)
    trigger(target, TriggerOpTypes.ADD, value, value)
  }

  return this
}

Map the set method

 function set(this: MapTypes, key: unknown, value: unknown) {
  value = toRaw(value)
  const target = toRaw(this)
  const { has, get } = getProto(target)

  // 分别检查代理和非代理版本的key是否存在于Map对象中
  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target.key)
  }

  const oldValue = get.call(target, key)
  target.set(key, value)
  if (!hadKey) {
    // 当Map对象中没有该元素时则触发依赖ITERATE_KEY的副作用函数,因此ADD操作会影响Map对象的长度
    trigger(target, TriggerOpTypes.ADD, key, value)
  }
  else if (hasChanged(value, oldValue)) {
    // 如果新旧值不同则触发修改,依赖该键值对的副作用函数将被触发
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
}

Note: get and has methods will track both the proxy and non-proxy version keys corresponding to element changes, while the set method will only trigger the found The element corresponding to the surrogate or non-surrogate version of the key changes.

deleteEntry method

 function deleteEntry(this: CollectionTypes, key: unknown) {
  const target = toRaw(this)
  const { has, get } = getProto(target)
  let hadKey = has.call(target, key)
  // 分别检查代理和非代理版本的key是否存在于Map/Set对象中
  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target.key)
  }

  // 如果当前操作的是Map对象则获取旧值
  const oldValue = get ? get.call(target, key) : undefined
  const result = target.delete(key)
  if (hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

Note: get and has methods will track the changes of the elements corresponding to the keys of the proxy and non-proxy versions at the same time, while the deleteEntry method will only trigger the found The element corresponding to the surrogate or non-surrogate version of the key changes.

Map and Set the clear method

 function clear(this: IterableCollections) {
  const target = toRaw(this)
  const hadItems = target.size !== 0
  const oldTarget = undefined
  const result = target.clear()
  if (hadItems) {
    trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
  }
  return result
}

Map and Set the forEach method

 function createForEach(isReadonly: boolean, isShallow: boolean) {
  return function forEach(
    this: IterableCollections,
    callback: Function,
    thisArg?: unknown
  ) {
    const observed = this as any
    const target = observed[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
    return target.forEach((value: unknown, key: unknown) => {
      // 将key和value都转换为代理对象
      return callback.call(thisArg, wrap(value), wrap(key), observed)
    })
  }
}

Since forEach will traverse all elements (Map objects are all key-value pairs), so tracking ITERATE_KEY that is, when the number of Map/Set objects changes, triggers forEach Execution of the function.

Iterator object related methods

So far we have not to entries , values , keys and @@iterator these methods return an object iterator proxy, and the source code is Add proxies for these methods at the end for mutableInstrumentations .

 const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator/*就是@@iterator*/]
iteratorMethods.forEach(method => {
  mutableInstrumentations[method as string] = createIterableMethod(
    method,
    false,
    false
  )
})
 function createIterableMethod(
  method: string | symbol,
  isReadonly: boolean,
  isShallow: boolean
) {
  return function(
    this: IterableCollections,
    ...args: unknown[]
  ): Iterable & Iterator {
    /**
     * 1. 针对readonly(reactive(new Map()))的情况,
     *    target获取的是代理对象,而rawTarget的是Map或Set对象
     * 2. 针对reactive(new Map())的情况,
     *    target和rawTarget都是指向Map或Set对象
     */ 
    const target = (this as any)[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)

    const targetIsMap = isMap(rawTarget)
    const isPair = method === 'entries' || (method === Symbol.iterator && targetIsMap)
    /**
     * 当调用的是Map对象的keys方法,副作用函数并没有访问值对象,即副作用函数只依赖Map对象的键而没有依赖值。
     * 而键只能增加或删除,值可增加、删除和修改,那么此时当且仅当键增删即size属性发生变化时才会触发副作用函数的执行。
     * 若依赖值,那么修改其中一个值也会触发副作用函数执行。
     */
    const isKeyOnly = method === 'keys' && targetIsMap
    const innerIterator = target[method](...args)
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    !isReadonly &&
      track(
        rawTarget,
        TrackOpTypes.ITERATE,
        isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
      )

    return {
      // 迭代器协议
      next() {
        const { value, done } = innerIterator.next()
        return done
          ? { value, done }
          : {
            value: isPair ? [wrap(value[0], wrap(value[1]))] : wrap(value),
            done
          }
      },
      // 可迭代协议
      [Symbol.iterator]() {
        return this
      }
    }
  }
}

iterable protocol

Iterable protocol for creating iterators.
The following built-in types all implement the iterable protocol:

  • string
  • array
  • Set
  • Map
  • arguements object
  • DOM collection types such as NodeList

The following language features will receive iterators returned by the iterable protocol

  • for...of loop
  • Data destructuring ( const [a, b] = [1, 2] )
  • spread operator ( const a = [1,2], b = [...a] )
  • Array.from()
  • Create Set
  • Create Map
  • Promise.all() accepts iterables
  • Promise.race() accepts iterables
  • yield* operator

Making an object support the iterable protocol is actually very simple, just implement the [Symbol.iterator] method that returns an iterator. JavaScript Plain Old Object does not support the iterable protocol by default, so we can implement the following by ourselves:

 const iterablizeKeys = (obj: {}) => {
  if (!obj[Symbol.iterator]) {
    obj[Symbol.iterator] = () => {
      const keys = Object.keys(obj) as const
      let i = 0

      // 返回一个迭代器
      return {
        next() {
          return { value: keys[i++], done: i > keys.length }
        }
      }
    }
  }

  return obj
} 

const iterableObj = iterablizeKeys({a: 1, b: 2})
for (let item of iterableObj) {
  console.log(item)
}
// 回显 a 
// 回显 b
Array.from(iterableObj) // 返回 ['a', 'b']

iterator protocol

The iterator protocol, which provides a next method that accepts no arguments and returns a IteratorResult object, while a IteratorResult object contains a value pointing to the current element value attribute and done 5eb6723ea7ef195e9beb1429531bc2ba---attribute indicating whether the iteration has ended, when the done attribute value is true , the iteration has ended.
The iterator protocol is implemented as in the iterable protocol example above, but we can also implement the iterable protocol and the iterable object on the same object.

 const iterablizeKeys = (obj: {}) => {
  if (!obj[Symbol.iterator]) {
    let iteratorState = {
      keys: []
      i: 0
    }
    // 迭代器协议
    obj.next = () => ({ value: iteratorState.keys[iteratorState.i++], done: iteratorState.i > iteratorState.key.length })

    // 可迭代协议
    obj[Symbol.iterator] = () => {
      iteratorState.keys = Object.keys(obj) as const
      iteratorState.i = 0

      // 返回一个迭代器
      return this
    }
  }

  return obj
} 

const iterableObj = iterablizeKeys({a: 1, b: 2})
for (let item of iterableObj) {
  console.log(item)
}
// 回显 a 
// 回显 b
Array.from(iterableObj) // 返回 ['a', 'b']

Summarize

In this article, we learned how reactive handles Map and Set objects by reading the source code line by line. In the next article, we will start with effect as the entry to further understand how the side effect function passes track and trigger record dependent and triggered.
Respect the original, please indicate the source for reprint: https://www.cnblogs.com/fsjohnhuang/p/16147725.html Fat Boy John

"Anatomy of Petite-Vue Source Code" booklet

"Petite-Vue Source Code Analysis" combines examples to interpret the source code line by line from online rendering, responsive system and sandbox model, and also makes a detailed analysis of the SMI optimization dependency cleaning algorithm using the JS engine in the responsive system. It is definitely an excellent stepping stone before getting started with Vue3 source code. If you like it, remember to forward it and appreciate it!


肥仔John
2.8k 声望1.8k 粉丝

《Petite-Vue源码剖析》作者