前端必刷系列之红宝书——第 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 版)

相关推荐
咬人喵喵13 分钟前
CSS Flexbox:拥有魔法的排版盒子
前端·css
LYFlied13 分钟前
TS-Loader 源码解析与自定义 Webpack Loader 开发指南
前端·webpack·node.js·编译·打包
yzp011215 分钟前
css收集
前端·css
暴富的Tdy15 分钟前
【Webpack 的核心应用场景】
前端·webpack·node.js
遇见很ok15 分钟前
Web Worker
前端·javascript·vue.js
elangyipi12316 分钟前
JavaScript 高级错误处理与 Chrome 调试艺术
开发语言·javascript·chrome
风舞红枫18 分钟前
前端可配置权限规则案例
前端
前端不太难21 分钟前
RN Navigation vs Vue Router:从架构底层到工程实践的深度对比
javascript·vue.js·架构
JHC00000026 分钟前
119. 杨辉三角 II
python·算法·面试
zhougl99628 分钟前
前端模块化
前端