前端必刷系列之红宝书——第 9 章

"红宝书" 通常指的是《JavaScript 高级程序设计》,这是一本由 Nicholas C. Zakas(尼古拉斯·扎卡斯)编写的 JavaScript 书籍,是一本广受欢迎的经典之作。这本书是一部翔实的工具书,满满的都是 JavaScript 知识和实用技术。

不管你有没有刷过红宝书,如果现在还没掌握好,那就一起来刷红宝书吧,go!go!go!

系列文章:

第一部分:基本知识(重点、反复阅读)

  1. 前端必刷系列之红宝书------第 1、2 章
  2. 前端必刷系列之红宝书------第 3 章
  3. 前端必刷系列之红宝书------第 4、5 章
  4. 前端必刷系列之红宝书------第 6 章

第二部分:进阶内容(重点、反复阅读)

  1. 前端必刷系列之红宝书------第 7 章
  2. 前端必刷系列之红宝书------第 8 章
  3. 前端必刷系列之红宝书------第 9 章

第 9 章 代理与反射

代理(Proxy)和反射(Reflect)是 ECMAScript 6(ES6)引入的两个新特性,用于操作和拦截 JavaScript 对象的行为。

概念

代理(Proxy):

代理是一个用于定义基本操作行为的对象,它允许你在对象上创建一个代理层,以拦截和定制对象的操作。代理对象可以用来拦截对目标对象的访问、修改、添加、删除等操作

反射(Reflect):

反射是一组新的内置对象和方法,它提供了对对象的底层操作,可以被 Proxy 拦截器调用。Reflect 对象的方法和 Proxy 拦截器的方法是一一对应的。

js 复制代码
// 创建一个简单的代理
let target = { value: 42 };

let handler = {
  get: function (target, prop, receiver) {
    console.log(`Getting ${prop}`);
    return Reflect.get(target, prop, receiver);
  },
  set: function (target, prop, value, receiver) {
    console.log(`Setting ${prop} to ${value}`);
    return Reflect.set(target, prop, value, receiver);
  },
};

let proxy = new Proxy(target, handler);

proxy.value; // 获取 value,输出: Getting value
proxy.value = 100; // 设置 value 为 100,输出: Setting value to 100

应用

Vue3

在 Vue 3 中,Proxy 是一个关键的特性,用于实现响应式系统。Vue 3 的响应式系统在设计上使用了 Proxy 来劫持对象的访问和修改操作,从而实现了数据的响应式更新。

  1. 数据劫持: Vue 3 中通过使用 Proxy 对象,可以劫持数据对象的读取和修改操作。这允许 Vue 追踪对响应式对象的访问,并在数据发生变化时自动触发相应的更新。
  2. 依赖追踪: Vue 3 利用 Proxy 捕获数据的读取操作,从而建立起一个依赖图。每个数据的读取操作都会被记录为一个依赖,当数据发生变化时,依赖会被通知,触发更新。
  3. 观察者模式: Vue 3 的响应式系统中使用了观察者模式,Proxy 对象被用作观察者,负责观察被劫持的数据对象。当数据变化时,观察者会通知相关的订阅者执行更新操作。

设计原理

  1. Proxy 代理: Vue 3 中使用 Proxy 对象来代理数据对象。Proxy 对象允许拦截对象的底层操作,例如读取和修改属性。
  2. Reflect 反射: Vue 3 在 Proxy 拦截器中广泛使用了 Reflect 对象。Reflect 对象提供了一个与 Proxy 拦截器一一对应的方法,用于执行默认操作。
  3. 依赖追踪: Vue 3 使用了一个全局的响应式状态管理对象,称为 ReactiveEffect,用于跟踪正在执行的响应式函数以及当前正在访问的依赖项。
  4. 响应式函数: 当访问一个响应式对象的属性时,Vue 3 会创建一个响应式函数,并将该函数与正在执行的响应式函数进行关联。这样就建立了一个依赖关系,当数据变化时,相关的响应式函数会被触发。
  5. 批量更新: 为了提高性能,Vue 3 中引入了批量更新的概念。即使数据发生多次变化,Vue 3 会在下一个微任务中批量执行更新,以减少不必要的计算和渲染操作。
TS 复制代码
// packages/reactivity/src/reactive.ts#L241-L278
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>,
) {
    // 判断目标对象是否为对象
    // 确保只有对象才能被转换成响应式对象。
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  // 如果目标对象已经具有代理对象,并且不是只读的响应式对象,直接返回目标对象。
  // 这是为了避免重复创建代理对象。
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  // 如果 `proxyMap` 中已经有了目标对象到代理对象的映射关系,直接返回已有的代理对象。
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only specific value types can be observed.
  // 使用 `getTargetType` 函数判断目标对象的类型,
  // 如果是无效类型,直接返回目标对象。
  // 这里的类型判断主要用于确定使用哪种代理处理器。
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // 使用 `Proxy` 构造函数创建代理对象,
  // 根据目标对象的类型选择相应的代理处理器。
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
  )
  // 将目标对象与创建的代理对象进行映射,以便后续直接返回已有的代理对象。
  proxyMap.set(target, proxy)
  // 返回创建的代理对象。
  return proxy
}
TS 复制代码
// packages/reactivity/src/baseHandlers.ts#L89-L237
class BaseReactiveHandler implements ProxyHandler<Target> {
  constructor(
    protected readonly _isReadonly = false,
    protected readonly _shallow = false,
  ) {}
  // `get` 方法用于拦截目标对象的属性访问操作。
  // 根据属性名和当前的代理对象,进行不同的处理,
  // 包括标识是否是只读、是否是浅层、是否是数组等情况。
  // 还涉及到对属性值的追踪(`track`)和返回新的代理对象。
  get(target: Target, key: string | symbol, receiver: object) {
    const isReadonly = this._isReadonly,
      shallow = this._shallow
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      return shallow
    } else if (key === ReactiveFlags.RAW) {
      if (
        receiver ===
          (isReadonly
            ? shallow
              ? shallowReadonlyMap
              : readonlyMap
            : shallow
              ? shallowReactiveMap
              : reactiveMap
          ).get(target) ||
        // receiver is not the reactive proxy, but has the same prototype
        // this means the reciever is a user proxy of the reactive proxy
        Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)
      ) {
        return target
      }
      // early return undefined
      return
    }

    const targetIsArray = isArray(target)

    if (!isReadonly) {
        // 如果目标对象是数组并且 `key` 是数组相关的内置方法,
        // 则使用 `Reflect.get` 获取相应的内置方法。
      if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
        return Reflect.get(arrayInstrumentations, key, receiver)
      }
      if (key === 'hasOwnProperty') {
        return hasOwnProperty
      }
    }

    const res = Reflect.get(target, key, receiver)
    
    // 如果 `key` 是特定的 Symbol 或不可追踪的键,
    // 则直接返回目标对象上的属性值。
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

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

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - skip unwrap for Array + integer key.
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }

    // 如果 `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.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

class MutableReactiveHandler extends BaseReactiveHandler {
   // 构造函数接收一个可选参数 `shallow`,用于标识是否是浅层响应式对象。
   // 调用父类 `BaseReactiveHandler` 的构造函数,
   // 并将 `_isReadonly` 设置为 `false`,
   // `_shallow` 设置为传入的 `shallow` 值。
  constructor(shallow = false) {
    super(false, shallow)
  }

  set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object,
  ): boolean {
      // 获取目标对象上 `key` 对应的旧值 `oldValue`。
    let oldValue = (target as any)[key]
    // 如果不是浅层响应式且新旧值都不是只读对象,并且值有变化,
    // 将新旧值都转换为原始值(去除响应式包装)。
    if (!this._shallow) {
      const isOldValueReadonly = isReadonly(oldValue)
      if (!isShallow(value) && !isReadonly(value)) {
        oldValue = toRaw(oldValue)
        value = toRaw(value)
      }
      // 如果目标对象不是数组且 `key` 对应的旧值是 Ref 对象而新值不是 Ref 对象,
      // 将 Ref 对象的值修改为新值。
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        if (isOldValueReadonly) {
          return false
        } else {
          oldValue.value = value
          return true
        }
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    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
    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
  }

    // `deleteProperty` 方法用于拦截目标对象的属性删除操作
  deleteProperty(target: object, key: string | symbol): boolean {
    const hadKey = hasOwn(target, key)
    const oldValue = (target as any)[key]
    const result = Reflect.deleteProperty(target, key)
    if (result && hadKey) {
      trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
    }
    return result
  }
    
    // `has` 方法用于拦截目标对象的 `in` 操作符。
  has(target: object, key: string | symbol): boolean {
    const result = Reflect.has(target, key)
    if (!isSymbol(key) || !builtInSymbols.has(key)) {
      track(target, TrackOpTypes.HAS, key)
    }
    return result
  }
  
  // `ownKeys` 方法用于拦截目标对象的 `Object.keys`、`Object.getOwnPropertyNames` 等操作。
  ownKeys(target: object): (string | symbol)[] {
    track(
      target,
      TrackOpTypes.ITERATE,
      isArray(target) ? 'length' : ITERATE_KEY,
    )
    return Reflect.ownKeys(target)
  }
}

其他

1. 访问控制

通过代理,你可以实现对对象属性的访问控制,例如只读或只写属性:

js 复制代码
let person = { name: 'John', age: 30 };

let readOnlyPerson = new Proxy(person, {
  get: function (target, prop) {
    console.log(`Accessing ${prop}`);
    return Reflect.get(target, prop);
  },
  set: function (target, prop, value) {
    console.log(`Setting ${prop} is not allowed`);
    return false; // 不允许设置属性值
  },
});

readOnlyPerson.name; // 访问 name 属性,输出: Accessing name
readOnlyPerson.age = 31; // 尝试设置 age 属性,输出: Setting age is not allowed

2. 数据验证

使用代理来实现数据验证,确保只有符合条件的数据可以被设置:

js 复制代码
let user = { username: 'john_doe', password: 'secret123' };

let secureUser = new Proxy(user, {
  set: function (target, prop, value) {
    if (prop === 'password' && typeof value !== 'string') {
      console.log('Invalid password format');
      return false;
    }
    return Reflect.set(target, prop, value);
  },
});

secureUser.password = 'newPassword'; // 设置密码,有效
secureUser.password = 123; // 设置无效,输出: Invalid password format

3. 缓存代理

通过代理实现缓存,可以在访问某个值时检查缓存是否已有该值,避免重复计算:

js 复制代码
function expensiveOperation() {
  // 模拟耗时计算
  console.log('Performing expensive operation');
  return Math.random();
}

let cachedValue = null;

let cachedProxy = new Proxy({}, {
  get: function (target, prop) {
    if (prop === 'value') {
      if (!cachedValue) {
        cachedValue = expensiveOperation();
      }
      return cachedValue;
    }
  },
});

console.log(cachedProxy.value); // 第一次调用,输出: Performing expensive operation
console.log(cachedProxy.value); // 第二次调用,直接使用缓存值

4. 日志记录

使用代理记录对象属性的访问和修改操作,用于调试或日志记录:

js 复制代码
let loggedObject = new Proxy({}, {
  get: function (target, prop) {
    console.log(`Getting property ${prop}`);
    return Reflect.get(target, prop);
  },
  set: function (target, prop, value) {
    console.log(`Setting property ${prop} to ${value}`);
    return Reflect.set(target, prop, value);
  },
});

loggedObject.name = 'John'; // 设置属性,输出: Setting property name to John
console.log(loggedObject.name); // 获取属性,输出: Getting property name

等其他应用场景......

未完待续...

参考资料

《JavaScript 高级程序设计》(第 4 版)

相关推荐
杰哥在此11 分钟前
Python知识点:如何使用Multiprocessing进行并行任务管理
linux·开发语言·python·面试·编程
汪子熙12 分钟前
Angular 服务器端应用 ng-state tag 的作用介绍
前端·javascript·angular.js
Envyᥫᩣ21 分钟前
《ASP.NET Web Forms 实现视频点赞功能的完整示例》
前端·asp.net·音视频·视频点赞
Мартин.4 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。6 小时前
案例-表白墙简单实现
前端·javascript·css
数云界6 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd6 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常6 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer6 小时前
Vite:为什么选 Vite
前端
小御姐@stella6 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js