Fancy Front End Fancy Front End
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)

Jonsam NG

让有意义的事变得有意思,让有意思的事变得有意义
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)
  • 开始上手
  • plan 计划
  • 基础

  • reactivity

    • 开始上手
    • Ref
    • Reactive
    • Handler
      • 目录
      • baseHandlers
      • collectionHandlers
      • Q&A
      • 文章小结
      • 参考链接
    • Effect
    • Computed
  • runtime-core

  • runtime-dom

  • vue3
  • reactivity
jonsam
2022-04-14
目录

Handler

# 目录

  • 目录
  • baseHandlers
    • get
    • set
    • deleteProperty
    • has
    • ownKeys
    • shallowGet
    • shallowSet
    • readonlyGet
    • shallowReadonlyGet
  • collectionHandlers
    • createInstrumentationGetter
    • createInstrumentations
    • get
    • size
    • has
    • add
    • set
    • delete
    • clear
    • forEach
  • Q&A
    • Proxy Handlers 类别与权限的关系?
    • 纯函数与 Tree Shaking
    • Proxy对数组的代理
    • Proxy 对 Map、Set 等集合对象的代理
  • 文章小结
  • 参考链接

在 Reactive 文章中,我们知道四种 reactive object 使用了不同的 handler,这篇文章我们从 baseHandlers 和 collectionHandlers 两个类别去分析 handler 的详细原理。结合 Reactive 文章,就可以明晰 creative object 的创建过程了。

# baseHandlers

baseHandlers 源码见文件 reactivity/baseHandlers.ts。baseHandlers 的作用是对 array、object 这些普通类型的响应式数据进行劫持。

# get

标签: 重要Deep Proxy

这个 handler 用于获取响应式数据的值。先看代码:

const get = /*#__PURE__*/ createGetter()
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // __v_isReactive 和 __v_isReactive、__v_raw 这几个标记是特殊的属性,如果获取这两个属性的值,就直接返回判断结果
    // 使用到这个 get 方法的一定是响应式的对象,那么智能是 reactive 或者 readonly。
    // 一下是获取 target 的内置标记属性的值
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
            ? shallowReactiveMap
            : reactiveMap
        )
          // 如果获取 __v_raw 的值,且传入的 receiver ProxyMap 是正确的,则返回 target
          // 注意:这里的 target 是 raw object。
          // 由 receiver === proxyMap.get(target) 可知,receiver 是 proxy object,target 是 raw object。
          .get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)
    // 如果不是 readonly,target 是数组且 key 在 arrayInstrumentations 对象中有记录
    // 这里是对 target 数组的一些方法进行改装,这里获取的属性是函数的一些方法
    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    // 获取到 target[key],这里同样适用于数组,因为数组的特殊属性在上面已经考虑
    const res = Reflect.get(target, key, receiver)
    // 如果 key 是内置的 Symbol 属性或者 key 是不许追踪响应性的,就直接返回
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }
    // Reactive target 则对target.key 进行依赖追踪
    // 注意:这里和创建 ref 时的追踪呼应
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    // 如果是 shallow 的,直接返回就可以了,因为顶层属性已经进行了依赖追踪
    if (shallow) {
      return res
    }
    // 如果属性值是 ref 类型,,则需要解包装(unwrapping)
    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      // target 是数字且 key 是 string int 如 '1', 这是不需要解包装
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }
    // typeof [] === "object"
    // 数组和对象都会走到这里
    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      // 数组和对象需要递归 proxy,这就实现了 deep proxy
      return isReadonly ? readonly(res) : reactive(res)
    }
    // 其他的类型,如基本值类型会走到这里
    return res
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
  • /*#__PURE__*/ 将 createGetter 标记为纯函数,有利于在打包时进行 Tree Shaking,减小打包体积。详见:纯函数与 Tree Shaking
  • createGetter 返回一个 getter 函数。setter 的处理逻辑见下图。
createGetter函数的原理图
  • 对内置的 proxy 属性进行处理:这些属性其实是在创建时传入的,比较特殊的返回 __v_raw 属性的值,获取这个属性需要传入与该 proxy object 相对应的 proxyMap,返回 target 是因为 target 本身就是 raw,注意这里传入 target 是 handlers.get () 默认的传参。
  • 这里比较有意思的是对响应式数组内部属性或方法的一些重写,我们可以看到,如果获取的是数组的下标的值,就会走和 object 一致的 track 程序,如果获取的是数组的一些方法属性就会单独的 track,换句话说也就是如果调用了响应式数组的方法就会被 track,为什么这样呢?难道是 Proxy Api 无法对数组的这些方法进行劫持吗?从 Proxy 对数组的监听 中我们知道,Proxy API 对于数组的方法也是可以劫持的。显然这里不使用与 object keys 一致的 track 方法,可以说是一种 hack 方法,这部分的分析我们在下文详述 arrayInstrumentations 。
  • 这里使用 Reflect.get 来获取对象的属性主要是因为 receiver,作为 Reflect.get 的第三个参数,receiver 将 this 进行了传递。同时反射的方式有利于函数式编程。参考:stackoverflow: JavaScript: Difference between Reflect.get() and obj['foo'] (opens new window)
  • 如果获取的值是一个 ref 对象就会解包装,只有获取数组下标值是除外的。官网对此做出了解释:Ref unwrapping only happens when nested inside a reactive Object. There is no unwrapping performed when the ref is accessed from an Array or a native collection type like Map。参见:vue3: ref-unwrapping (opens new window)

核心理解

最重要的是下面的代码(摘录的片段):


if (!isReadonly) {
  track(target, TrackOpTypes.GET, key)
}

if (isObject(res)) {
  // Convert returned value into a proxy as well. we do the isObject check
  // here to avoid invalid value warning. Also need to lazy access readonly
  // and reactive here to avoid circular dependency.
  // 数组和对象需要递归 proxy,这就实现了 deep proxy
  return isReadonly ? readonly(res) : reactive(res)
}
1
2
3
4
5
6
7
8
9
10
11
12

可以看到:

  • 除了 readonly 的不再具有响应式的数据,都是需要 track 的。也是就说响应式数据在 get handler 中都会被追踪和手机依赖,因为这个数据响应式的基础。那么我们可能会有这样的疑问,readonly 的数据既然不具有响应式,为什么还要单独的来控制,只要不给他响应性的能力不就可以了吗?需要注意的是 readonly !== none reactivity,readonly 的数据是只读的,不具有响应式只是一方面,最重要的还是要保证数据的只读性。一个数据只有先是非响应式的,然后才能是只读的,使用场景不同。
  • isObject 是判断了 object 和 array,需要注意的是,数组也会走这里的逻辑。根据数据是 readonly 还是 reactive,来进一步对数据做 deep proxy,才是响应式数据能够 deep reactivity 的原因。本质上这里是一种递归。

前置知识 - handler.get()

handler.get () 方法用于拦截对象的读取属性操作。

var p = new Proxy(target, {
  get: function(target, property, receiver) {
  }
});
1
2
3
4

以下是传递给 get 方法的参数,this 上下文绑定在 handler 对象上.

  • target:目标对象。
  • property:被获取的属性名。
  • receiver:Proxy 对象或者继承 Proxy 的对象

详见:MDN: handler.get() (opens new window)

前置知识 - Reflect 和 Reflect.get()

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handlers (en-US) 的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。Reflect 的所有属性和方法都是静态的(就像 Math 对象)。Reflect 对象提供的静态方法与 proxy handler methods (en-US) 的命名相同.Reflect 不支持 ie 浏览器。Reflect 让我们对对象的操作可以用函数来处理。

Reflect.get () 方法与从对象 (target [propertyKey]) 中读取属性类似,但它是通过一个函数执行来操作的。

Reflect.get(target, propertyKey[, receiver])
1
  • target:需要取值的目标对象
  • propertyKey:需要获取的值的键值
  • receiver:与 Proxy 中的 receiver 项对应,如果 target 对象中指定了 getter,receiver 则为 getter 调用时的 this 值。

参考:

  • MDN: Reflect (opens new window)
  • Reflect.get() (opens new window)
  • JS 的 Reflect 学习和应用 (opens new window)
  • 一起來了解 Javascript 中的 Proxy 與 Reflect (opens new window)

# arrayInstrumentations

arrayInstrumentations 是通过纯函数 createArrayInstrumentations 生成的:

const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()
// 对数组的方法进行改装(hack),以注入一些响应式的逻辑。
function createArrayInstrumentations() {
  const instrumentations: Record<string, Function> = {}
  // instrument identity-sensitive Array methods to account for possible reactive
  // values
  // 针对查找性的方法进行 hack
  ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
    // 从数组原型上获取到原方法
    const method = Array.prototype[key] as any
    // this 是指向 receiver 的,针对数组 receiver 要么传的是数组本身,要么不传,但是 receiver 的默认值就是 target
    // 因此这里的 this 是指向 target 的,也就是数组本身
    instrumentations[key] = function(this: unknown[], ...args: unknown[]) {
      // 获取数组的 row value,receiver 是 proxy object
      const arr = toRaw(this)
      // 对数组的每一项进行 track
      for (let i = 0, l = this.length; i < l; i++) {
        track(arr, TrackOpTypes.GET, i + '')
      }
      // we run the method using the original args first (which may be reactive)
      // 默认用户传的参数是 row vlaue。调用原函数。
      const res = method.apply(arr, args)
      if (res === -1 || res === false) {
        // if that didn't work, run it again using raw values.
        // 如果没找到,可能用户传的是 proxy value。这里自动帮用户转成 raw value
        return method.apply(arr, args.map(toRaw))
      } else {
        return res
      }
    }
  })
  // instrument length-altering mutation methods to avoid length being tracked
  // which leads to infinite loops in some cases (#2137)
  // 针对改变数组长度的方法进行 hack
  // 避免在某些情况下 track 会造成死循环的情况
  ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
    const method = Array.prototype[key] as any
    instrumentations[key] = function(this: unknown[], ...args: unknown[]) {
      // 在调用原方法时先关闭 track,调用完毕后再恢复上一次的 track 状态,避免造成死循环。
      // 注意:这里 this 并没有转成 raw object。这里关闭了 track,但是 trigger 仍然可以触发。
      pauseTracking()
      const res = method.apply(this, args)
      resetTracking()
      return res
    }
  })
  return instrumentations
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
  • 这里针对数组的方法进行了 hack,主要有两类,第一类是 'includes', 'indexOf', 'lastIndexOf' 这些 identity-sensitive Array methods,跟查找相关的身份敏感的方法,这些方法在调用时需要先对数组的每一项进行 track。 第二类是 'push', 'pop', 'shift', 'unshift', 'splice' 这些 length-altering mutation methods,会改变数组长度的方法,为了避免死循环,这里在调用原方法之前先暂停 tracK,在调用完毕之后在恢复上一次的 track 状态。造成死循环的原因请参照:Proxy 对数组的监听。
  • 看完这段代码,我们最大的疑惑可能就是:为什么第一类方法需要提前对数组的每一项进行 track? 第 2 类方法明明改变了数组却不用 trigger?第一类方法在调用的时候使用的是 raw object,并不会触发 track,所以在调用之前需要提交 track 每一项,第二类方法在调用的时候使用的是 proxy object,本身会触发 trigger,为了不引起死循环,才屏蔽了 track。

# set

这个 handler 用于增加或者更新响应式数据的值。先看代码:

const set = /*#__PURE__*/ createSetter()
function createSetter(shallow = false) {
  return function set(
    target: object, // raw target object
    key: string | symbol, // target property
    value: unknown, // new value for property
    receiver: object // proxy target object
  ): boolean {
    // 取出旧值
    let oldValue = (target as any)[key]
    if (!shallow) {
      // value 需要转成 raw value
      value = toRaw(value)
      oldValue = toRaw(oldValue)
      // 如果 target 不是数组,原值是 ref 对象现在传的不是 ref 对象,则仍然保持 ref
      // 注意:这里 return 了,并没有触发 trigger
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }
    // key 值是否在 target 中,如果是 array 的 int key 判断 key值是否合法
    // 注意:这里的 key 值为负值,如 '-100',情况已经在 isIntegerKey 排除了
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    // 设置新值
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    // 操作原型链上的数据,不引起 trigger
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // 增加新的属性
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 修改原属性
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
  • 这里有一个奇怪的地方,在非 shallow 情况下,如果 一个不是数字的 target 旧值是 ref 对象新值却不是 ref 对象,这里直接修改了 ref.value 就 return 了,并没有触发后面的 trigger 啊,那是不是就不会响应了。其实并不是,修改 ref 对象本身就会触发 trigger 的,详见 ref 篇,所以这里仍然是保持了响应式。
  • target === toRaw(receiver) 说明只有在 receiver 刚好是 target 的 proxy 时才触发 trigger,这个判断看起来很多余其实不然,需要注意的是如果在 target 的原型链上执行 set 操作,并不会触发 trigger。
  • 这里通过 hadKey 来判断是 ADD 操作还是 SET,不同的操作类型所触发的 effects 集合也不尽相同。track 和 trigger 部分将在 effect 篇中详述。

核心理解

get () 引起 track (),set () 引起 trigger (),这与 ref 中 getter 中 track,setter 中 trigger 一致。get 和 set 可以对应理解为对数据的读和写操作,在读取数据中通过 track 收集依赖回调,在写数据时通过 trigger 对收集的依赖进行消费,对依赖于这个数据的部分进行更新,这就是 vue 响应式原理的核心。

# deleteProperty

这个 handler 用于删除响应式数据的值。

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)
  // key 值存在且删除成功时 trigger,类型为 DELETE
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}
1
2
3
4
5
6
7
8
9
10

# has

这个 handler 用于获取响应式数据的值。

function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  // 如果 key 不是 Sumbol,或者不在 builtInSymbols 中,就 track
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    track(target, TrackOpTypes.HAS, key)
  }
  return result
}
1
2
3
4
5
6
7
8

has 属于读操作,在这里出发了 track。

前置知识

handler.has () 的触发的时机: The handler.has () method is a trap for the in operator.

参考:MDN: handler.has() (opens new window)

# ownKeys

这个 handler 用于遍历响应式数据的值(不包括值为 Symbol 项)。

function ownKeys(target: object): (string | symbol)[] {
  // 触发 track,类型为 ITERATE,key 参数不用重视,因为在 prod 用不到
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}
1
2
3
4
5

前置知识

handler.ownKeys () 的触发的时机:ownKeys 在 Object.keys() 执行时触发。

参考:MDN: handler.ownKeys() (opens new window)

# shallowGet

用于 hallowReactive。

const shallowGet = /*#__PURE__*/ createGetter(false, true)
1

参照上文,直接在 track 之后返回,不用 deep reactivity。

# shallowSet

用于 shallowReactive。

const shallowSet = /*#__PURE__*/ createSetter(true)
1

相比于 set 变化不大。

# readonlyGet

用于 readonly。

const readonlyGet = /*#__PURE__*/ createGetter(true)
1

跳过 track 并执行了 deep readonly。

# shallowReadonlyGet

用于 shallowReadonly。

const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)
1

跳过了 track 并且直接返回了。

# collectionHandlers

collectionHandlers 源码见文件 reactivity/collectionHandlers.ts。collectionHandlers 的作用是对 Map、Set、WeakSet、WeakMap 这些集合类型的响应式数据进行劫持。

# createInstrumentationGetter

用于 reactive、readonly、shallowReactive 和 shallowReadonly 四种响应式 API(所有)。

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  // 根据 isReadonly 和 shallow 选择不同 instrumentations。
  const instrumentations = shallow
    ? isReadonly
      ? shallowReadonlyInstrumentations
      : shallowInstrumentations
    : isReadonly
      ? readonlyInstrumentations
      : mutableInstrumentations

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

    // 如果获取内置方法属性就从 instrumentations 中获取,否则从 target 获取,这是普通的取值操作
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

这段代码机器简洁,核心代码就在 return 语句中,如果 key 值在 instrumentations 中就从 instrumentations 取,否则就从 target 中取。而且我们已经注意到非常疑惑的一点就是:对于 CollectionTypes,似乎只配置了 get handler,这是非常奇怪的,Proxy 对这些集合对象的拦截肯定是没有问题的,那这到底是为什么呢?我们先来运行一些测试代码:

const m = new Map([["name", 'any']]);
const p = new Proxy(m, {
  get(target, key, receiver) {
    const v = Reflect.get(...arguments);
    console.info('==> get', key);
    return typeof v === "function" ? v.bind(target) : v;
  },
  set(target, key, receiver) {
    const v = Reflect.set(...arguments);
    console.info('==> set', key);
    return typeof v === "function" ? v.bind(target) : v;
  },
})
// 以下代码在 console 中逐句运行
p.get("name");
// >> ==> get get
p.set('name', 'some')
// >> ==> get set
p.delete("name")
// >> ==> get delete
p.clear()
// >> ==> get clear
p.entries()
// >> ==> get entries
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

现在我们知道原因了: Proxy Api 对于集合对象只会触发 get handler,其他都不会触发 。也就是说,我们只需要拦截 get 然后根据 key 做不同的处理即可。从上面的代码中可以看到,根据 isReadonly 和 shallow 的值选择了不同的 instrumentations,这个 instrumentations 中就包含了对于不同 key 值的处理。

# createInstrumentations

这个一个工厂函数,这个函数创建 isReadOnly 和 shallow 不同场景下的 handlers。

function createInstrumentations() {
  // reactive
  const mutableInstrumentations: Record<string, Function> = {
    get(this: MapTypes, key: unknown) {
      return get(this, key)
    },
    get size() {
      return size((this as unknown) as IterableCollections)
    },
    has,
    add,
    set,
    delete: deleteEntry,
    clear,
    forEach: createForEach(false, false)
  }
  // shallowReactive
  const shallowInstrumentations: Record<string, Function> = {
    get(this: MapTypes, key: unknown) {
      return get(this, key, false, true)
    },
    get size() {
      return size((this as unknown) as IterableCollections)
    },
    has,
    add,
    set,
    delete: deleteEntry,
    clear,
    forEach: createForEach(false, true)
  }
  // readOnly
  const readonlyInstrumentations: Record<string, Function> = {
    get(this: MapTypes, key: unknown) {
      return get(this, key, true)
    },
    get size() {
      return size((this as unknown) as IterableCollections, true)
    },
    has(this: MapTypes, key: unknown) {
      return has.call(this, key, true)
    },
    add: createReadonlyMethod(TriggerOpTypes.ADD),
    set: createReadonlyMethod(TriggerOpTypes.SET),
    delete: createReadonlyMethod(TriggerOpTypes.DELETE),
    clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
    forEach: createForEach(true, false)
  }
  // shallowReadOnly
  const shallowReadonlyInstrumentations: Record<string, Function> = {
    get(this: MapTypes, key: unknown) {
      return get(this, key, true, true)
    },
    get size() {
      return size((this as unknown) as IterableCollections, true)
    },
    has(this: MapTypes, key: unknown) {
      return has.call(this, key, true)
    },
    add: createReadonlyMethod(TriggerOpTypes.ADD),
    set: createReadonlyMethod(TriggerOpTypes.SET),
    delete: createReadonlyMethod(TriggerOpTypes.DELETE),
    clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
    forEach: createForEach(true, true)
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

我们来看一下核心的几个函数是如何处理的:

# get

# size

# has

# add

# set

# delete

# clear

# forEach

# Q&A

# Proxy Handlers 类别与权限的关系?

权限 handler
read get | has | ownKeys
write set | deleteProperty

# 纯函数与 Tree Shaking

什么是纯函数?

在程序设计中,若一个函数符合以下要求,则它可能被认为是纯函数:

  • 此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由 I/O 设备产生的外部输出无关。
  • 该函数不能有语义上可观察的函数副作用,诸如 “触发事件”,使输出设备输出,或更改输出值以外物件的内容等。

什么是 Tree Shaking?

Tree Shaking:用于描述移除 JavaScript 上下文中的未引用代码 (dead-code)

为什么纯函数比较有利于 Tree Shaking?

/*#__PURE__*/ 标记表明被标记的代码是静态的,标记在函数前则表示被标记的函数是纯函数,纯函数在得到相同的输入后得到的输出是可预见的,打包器在遇到静态的代码时,就可以判断当前的代码是否有引用,没有引用的代码咋可以被安全的删除。而纯函数在得到确切的输入时,打包器就可以直接打包可预见的执行结果而将纯函数删除。这就是纯函数对于 Tree Shaking 的作用。之所以要认为的标记是因为打包器没有判断对目标函数做纯函数的判断,因为从表现上来看,村函数当然是被引用过了。

参考资料:

  • Wiki: Pure function (opens new window)
  • 【译】精通 JavaScript: 什么是纯函数(Pure Function)? (opens new window)
  • Webpack: Tree Shaking (opens new window)

# Proxy 对数组的代理

下面我们来测试下如下的代码:

const arr = [1,2,3];
const proxy = new Proxy(arr, {
  get: function (target, key, receiver) {
      console.log('get的key为 ===> ' + key);
      return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver){
      console.log('set的key为 ===> ' + key, value);
      return Reflect.set(target, key, value, receiver);
  }
})
// 注意:以下代码为逐行在 console 中执行。
proxy[0]
//>> get的key为 ===> 0
proxy[3] = 12
//>> set的key为 ===> 3 12
proxy.includes(1)
// >> get的key为 ===> includes
// >> get的key为 ===> length
// >> get的key为 ===> 0
proxy.indexOf(2)
// >> get的key为 ===> indexOf
// >> get的key为 ===> length
// >> get的key为 ===> 0
// >> get的key为 ===> 1

proxy.pop()
// >> get的key为 ===> pop
// >> get的key为 ===> length
// >> get的key为 ===> 3
// >> set的key为 ===> length 3
proxy.push(4)
// >> get的key为 ===> push
// >> get的key为 ===> length
// >> set的key为 ===> 3 4
// >> set的key为 ===> length 4
proxy.slice(0,1)
//  >> get的key为 ===> slice
//  >> get的key为 ===> length
//  >> get的key为 ===> constructor
//  >> get的key为 ===> 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

由如上的代码测试,我们可以看出:

  • Proxy 对于数组的属性和行为也具有拦截的作用,也就是说 Proxy API 是适用于数组的。
  • identity-sensitive Array methods (第一类)这类的数组方法,会触发多次 get (),但是不会触发 set (); length-altering mutation methods (第二类)这类的数组方法可能会触发多次的 get () 和 set ()。
  • 由于触发 get () 就会触发 track (),触发 set () 就会触发 trigger (),所以第二类方法会连续触发多次的 track () 和 trigger (),会造成死循环。
  • 如果我们把 receiver 打印出来的话,就会发现它其实是 target 的 proxy 对象,这也是 Proxy 和 Reflect 一起使用的好处。

# Proxy 对 Map、Set 等集合对象的代理

需要注意的是,集合元素是通过 get ()、add () 等方法操作,因此需要注意 this 的指向问题。

执行如下代码:

const m = new Map([["name", 'any']]);
const p = new Proxy(m, {
  get(target, key, receiver) {
    const v = Reflect.get(...arguments);
    console.log({key, v}, this);
    return v;
  }
})
p.get("name");
// >> {key: "get", v: ƒ}key: "get"v: ƒ ()arguments: (...)caller: (...)length: 1name: "get"[[Prototype]]: ƒ ()[[Scopes]]: Scopes[0][[Prototype]]: Object {get: ƒ}
1
2
3
4
5
6
7
8
9
10

这回报一个错误: Uncaught TypeError: Method Map.prototype.get called on incompatible receiver #<Map> 。这是因为此时的 this 指向了 get (),v 的值是一个函数。而我们需要使 v 中 this 指向 target,因为需要从 target 中取值。改成下面的代码:

const m = new Map([["name", 'any']]);
const p = new Proxy(m, {
  get(target, key, receiver) {
    const v = Reflect.get(...arguments);
    console.log({key, v}, this);
    return typeof v === "function" ? v.bind(target) : v;
  }
})
p.get("name");
// >> "any"
1
2
3
4
5
6
7
8
9
10

这样就可以顺利的代理 Map 等集合对象了。

# 文章小结

# 参考链接

  • MDN: Proxy() constructor (opens new window)
编辑 (opens new window)
上次更新: 2022/04/15, 00:23:56
Reactive
Effect

← Reactive Effect→

最近更新
01
渲染原理之组件结构与 JSX 编译
09-07
02
计划跟踪
09-06
03
开始上手
09-06
更多文章>
Theme by Vdoing | Copyright © 2022-2022 Fancy Front End | Made by Jonsam by ❤
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式