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 asvalues(): Iterator
-
@@iterator
values(): Iterator
the same effect asfor 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 asfor 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, returnundefined
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:
- Any other type can be assigned to a variable of type
unknown
- A variable of type
unknown
can only be assigned to a variable of typeany
orunknown
- 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!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。