petite-vue源码剖析-逐行解读@vue-reactivity之Map和Set的reactive
本篇我们会继续探索reactive
函数中对Map/WeakMap/Set/WeakSet
对象的代理实现。
Map/WeakMap/Set/WeakSet的操作
由于WeakMap和WeakSet分别是Map和Set的不影响GC执行垃圾回收的版本,这里我们只研究Map和Set即可。
Set的属性和方法
size: number
为访问器属性(accessor property),返回Set对象中的值的个数add(value: any): Set
向Set对象队尾添加一个元素clear(): void
移除Set对象内所有元素delete(value: any): boolean
移除Set中与入参值相同的元素,移除成功则返回truehas(value: any): boolean
判断Set中是否存在与入参值相同的元素values(): Iterator
返回一个新的迭代器对象,包含Set对象中按插入顺序排列的所有元素keys(): Iterator
和values(): Iterator
一样的功效@@iterator
和values(): Iterator
一样的功效,for of
中调用entries(): Iterator
返回一个新的迭代器对象,包含Set对象中按插入顺序排列的所有元素,但为与Map使用一致每次迭代返回的内容为[value, value]
forEach(callbackFn: { (value: any, set: Set) => any } [, thisArg])
按插入顺序遍历Set对象的每一个元素
Map的属性和方法
size: number
为访问器属性(accessor property),返回Set对象中的值的个数set(key: any, value: any): Map
向Map对象添加或更新一个指定键的值clear(): void
移除Map对象内所有键值对delete(key: any): boolean
移除Map对象中指定的键值对,移除成功则返回truehas(key: any): boolean
判断Map中是否存在键与入参值相同的键值对values(): Iterator
返回一个新的迭代器对象,包含Map对象中按插入顺序排列的所有值keys(): Iterator
返回一个新的迭代器对象,包含Map对象中按插入顺序排列的所有键@@iterator
和entries(): Iterator
一样的功效,for of
中调用entries(): Iterator
返回一个新的迭代器对象,包含Map对象中按插入顺序排列的所有键值对forEach(callbackFn: { (value: any, key: any, map: Map) => any } [, thisArg])
按插入顺序遍历Map对象的每一个键值对get(key: any): any
返回Map对象中指定键对应的值,若没有则返回undefined
逐行看代码我是认真的
// reactive.ts
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: /*#__PURE__*/ createInstrumentationGetter(false, false)
}
由于Map/Set不像Object或Array那样可直接通过属性访问的方式获取其中的元素,而是通过add
,has
,delete
操作,因此需要像处理Array的slice
等方法那样代理Map/Set的这些方法。
// 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小课堂:as
断言——this as unknown as IterableCollections
在TypeScript中可通过类型声明定义变量的类型(其中包含复合类型),而类型推导则可以根据赋值语句中右侧字面量推导出变量的实际类型,或通过当前变量使用的场景推导出当前实际类型(尤其是定义为复合类型)。但有时无法通过当前使用场景执行精确的类型推导,这时开发者可以通过as
断言告知TypeScript编译器该变量当前使用范围的数据类型(要相信自己一定比编译器更了解自己的代码:D)。
那么as unknown
即表示将类型修改为unknown
,那么类型为unknown
是表示什么呢?unknown
是TypeScript3.0引入的top type(任何其他类型都是它的subtype),意在提供一种更安全的方式替代any
类型(any
类型是top type也是bottom type,使用它意味和绕过类型检查),具有如下特点:
- 任何其它类型都可以赋值给
unknown
类型的变量 unknown
类型的变量只能赋值给any
或unknown
类型的变量- 如果不对
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()
}
那么as unknown
后的as IterableCollections
意图就十分明显了,就是对变量进行类型收缩。this as unknown as IterableCollections
其实就是as IterableCollections
啦。
然后我们逐一看看代理方法的实现吧
Map
的get
方法
get
方法只有Map对象拥有,因此其中主要思路是从Map对象中获取值,跟踪键值变化后将值转换为响应式对象返回即可。
但由于要处理readonly(reactive(new Map()))
这一场景,添加了很多一时让人看不懂的代码而已。
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
和Set
的size
访问器属性
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
和Set
的has
方法
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
方法
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
的set
方法
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)
}
}
注意:get
和has
方法中会同时跟踪代理和非代理版本的键对应的元素变化,而set
方法则只会触发查找到的代理或非代理版本的键对应的元素变化。
deleteEntry
方法
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
}
注意:get
和has
方法中会同时跟踪代理和非代理版本的键对应的元素变化,而deleteEntry
方法则只会触发查找到的代理或非代理版本的键对应的元素变化。
Map
和Set
的clear
方法
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
和Set
的forEach
方法
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)
})
}
}
由于forEach
会遍历所有元素(Map对象则是所有键值对),因此跟踪ITERATE_KEY
即Map/Set对象元素个数发生变化则触发forEach
函数的执行。
迭代器对象相关方法
至此我们还没对entries
,values
,keys
和@@iterator
这些返回迭代器的对象方法进行代理,而源码中则在最后为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),用于创建迭代器(iterator)。
如下内置类型都实现了可迭代协议:
- 字符串
- 数组
- Set
- Map
- arguements对象
- NodeList等DOM集合类型
下面的语言特性将会接收可迭代协议返回的迭代器
for...of
循环- 数据解构(
const [a, b] = [1, 2]
) - 扩展操作符(
const a = [1,2], b = [...a]
) Array.from()
- 创建Set
- 创建Map
Promise.all()
接受可迭代对象Promise.race()
接受可迭代对象yield*
操作符
让对象支持可迭代协议其实很简单,只需实现返回迭代器的[Symbol.iterator]
方法即可。JavaScript Plain Old Object默认并没有支持可迭代协议,那么我们可以自行实现以下:
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)
迭代器协议(iterator protocol),提供不接受任何参数并返回IteratorResult
对象的next
方法,而IteratorResult
对象包含指向当前元素的value
属性和表示迭代是否已结束的done
属性,当done
属性值为true
时表示迭代已结束。
迭代器协议的实现正如上面可迭代协议的示例中那样,不过我们还可以将可迭代协议和迭代对象在同一个对象上实现。
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']
总结
本篇我们通过逐行阅读源码了解到reactive如何处理Map和Set对象了,下一篇我们将开始以effect
为入口进一步了解副作用函数是如何通过track
和trigger
记录依赖和触发的。
尊重原创,转载请注明来自:https://www.cnblogs.com/fsjohnhuang/p/16147725.html肥仔John
petite-vue源码剖析-逐行解读@vue-reactivity之Map和Set的reactive的更多相关文章
- petite-vue源码剖析-逐行解读@vue/reactivity之reactive
在petite-vue中我们通过reactive构建上下文对象,并将根据状态渲染UI的逻辑作为入参传递给effect,然后神奇的事情发生了,当状态发生变化时将自动触发UI重新渲染.那么到底这是怎么做到 ...
- petite-vue源码剖析-逐行解读@vue-reactivity之effect
当我们通过effect将副函数向响应上下文注册后,副作用函数内访问响应式对象时即会自动收集依赖,并在相应的响应式属性发生变化后,自动触发副作用函数的执行. // ./effect.ts export ...
- Vue源码分析(一) : new Vue() 做了什么
Vue源码分析(一) : new Vue() 做了什么 author: @TiffanysBear 在了解new Vue做了什么之前,我们先对Vue源码做一些基础的了解,如果你已经对基础的源码目录设计 ...
- [Vue源码]一起来学Vue模板编译原理(一)-Template生成AST
本文我们一起通过学习Vue模板编译原理(一)-Template生成AST来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学Vu ...
- [Vue源码]一起来学Vue模板编译原理(二)-AST生成Render字符串
本文我们一起通过学习Vue模板编译原理(二)-AST生成Render字符串来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学V ...
- [Vue源码]一起来学Vue双向绑定原理-数据劫持和发布订阅
有一段时间没有更新技术博文了,因为这段时间埋下头来看Vue源码了.本文我们一起通过学习双向绑定原理来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫 ...
- vue源码分析之new Vue过程
实例化构造函数 从这里可以看出new Vue实际上是使vue构造函数实例化,然后调用_init方法 _init方法,该方法在 src/core/instance/init.js 中定义 Vue.pro ...
- Vue源码思维导图------------Vue选项的合并之$options
本节将看下初始化中的$options: Vue.prototype._init = function (options?: Object) { const vm: Component = this / ...
- 大白话Vue源码系列(01):万事开头难
阅读目录 Vue 的源码目录结构 预备知识 先捡软的捏 Angular 是 Google 亲儿子,React 是 Facebook 小正太,那咱为啥偏偏选择了 Vue 下手,一句话,Vue 是咱见过的 ...
随机推荐
- python监控cpu 内存实现邮件微信报警
# qianxiao996精心制作 #博客地址:https://blog.csdn.net/qq_36374896 import psutil, time,smtplib,socket import ...
- ctf之POST
题目信息如下 可知该题考察post请求知识 直接将what=flag以post传参格式进行传参即可获得flag
- 有没有想过String为什么设计为不可变对象
1.声明为final类的目的: 主要目的就是保证String是不可变(immutable).不可变就是第二次给一个String 变量赋值的时候,不是在原内存地址上修改数据,而是重新指向一个新对象,新地 ...
- C++ 文件加解密
通过文件二进制数据 与密钥进行异或处理,可加密文件 #ifndef __ENCRYPT__HEAD__ #define __ENCRYPT__HEAD__ #include <fstream&g ...
- Spring Data Jpa使用QueryDsl接口出现的一些问题
1.QuerydslPredicateExecutor当实现此接口时,如果出现什么什么类没有找到的时候,请确认相关的querydsl依赖是否已经添加到maven依赖中 <dependency&g ...
- Python - 文档格式转换(CSV与JSON)
- 使用Ansible部署openstack平台
使用Ansible部署openstack平台 本周没啥博客水了,就放个云计算的作业上来吧(偷个懒) 案例描述 1.了解高可用OpenStack平台架构 2.了解Ansible部署工具的使用 3.使用A ...
- Rust 中的数据布局-repr
repr(Rust) 首先,所有类型都有一个以字节为单位的对齐方式,一个类型的对齐方式指定了哪些地址可以用来存储该值.一个具有对齐方式n的值只能存储在n的倍数的地址上.所以对齐方式 2 意味着你必须存 ...
- Day 19: EmberJS 入门指南
编者注:我们发现了有趣的系列文章<30天学习30种新技术>,正在翻译,一天一篇更新,年终礼包.下面是第19天的内容. 到目前为止,我们这一系列文章涉及了Bower.AngularJS.Gr ...
- 设计一个基于svg的涂鸦组件(一)
基于svg写了一个涂鸦组件,说项目之前先附上几张效果图: 项目地址:SVGraffiti 由于篇幅问题,本文先总体介绍一下项目的大概情况,重点介绍一下组件间的通信方式. 一.项目说明 该项目是基于we ...