实现 readonly 功能

  1. 实现 readonly 的代码很简单
export function readonly (raw)  {
  return new Proxy(raw, {
    get(target,key) {
      const res = Reflect.get(target,key)
      return res
    },
    set() {
      return true // ✅
    }
  })
}
  1. 根据 TDD(Test Driven Development) 的思维,我们进行优化,抽离 get
// 仅可以抽离 reactive 的 getter 
function get(target,key) { // ✅
  const res = Reflect.get(target,key)
  // 依赖收集 
  track(target,key)
  return res
}
  1. 上面的代码仅可以抽离 reactive 里面的 get , 而 readonly 里面的 get 是不需要进行 track 的,因为它本来也不会更改也不会有 trigger
    所以我们想到可以使用高阶函数,外面的函数返回里面的函数,外面函数的参数控制是否使用track,里面的函数接收 target,key, value
function createGetter(isReadonly = false) { // ✅
  return function get (target, key, value) {
    if(!isReadonly) { // ✅
      track(target, key)
    }
    return Reflect.get(target, key)
  }
}

export function reactive (raw)  {
  return new Proxy(raw, {
    get: createGetter(), // ✅
    set(target, key, value) {
      const res = Reflect.set(target, key, value)
      trigger(target, key)
      return res
    }
  })
}

export function readonly (raw)  {
  return new Proxy(raw, {
    get: createGetter( true ), // ✅
    set() {
      return true
    }
  })
}
  1. 我们把 reactive 和 readonly 的 get 方法抽离成了 createGetter, 我们还可以把 reactive 的 set 方法抽离出一样
    的形式
function createSetter() { // ✅
  return function set(target, key, value) {
    const res = Reflect.set(target, key, value)
    trigger(target, key)
    return res
  }
}
export function reactive (raw)  {
  return new Proxy(raw, {
    get: createGetter(),
    set: createSetter() // ✅
  })
}
  1. 我们观察发现,proxy 里面的 handler 也可以进行抽离,于是我们把这一块抽离到 baseHandlers.ts 文件
// baseHandlers.ts
import { track, trigger } from "./effect"

function createGetter(isReadonly = false) { // ✅
  return function (target, key, value) {
    if(!isReadonly) {
      track(target, key)
    }
    return Reflect.get(target, key)
  }
}
function createSetter() { // ✅
  return function set(target, key, value) {
    const res = Reflect.set(target, key, value)
    trigger(target, key)
    return res
  }
}

export const mutableHandlers = { // ✅
  get: createGetter(),
  set: createSetter()
}
export const readonlyHandlers = { // ✅
  get: createGetter( true ),
  set() {
    return true
  }
}
// reactive.ts
import { mutableHandlers, readonlyHandlers } from "./baseHandlers"

export function reactive (raw)  {
  return new Proxy(raw, mutableHandlers) // ✅
}
export function readonly (raw)  {
  return new Proxy(raw, readonlyHandlers) // ✅
}
  1. 后来我们发现 new Proxy 也可以进行抽离出公共的部分,抽离以后更易读, 可复用
export function reactive (raw)  {
  return createActiveObject(raw, mutableHandlers) // ✅
}

export function readonly (raw)  {
  return createActiveObject(raw, readonlyHandlers) // ✅
}

function createActiveObject(raw, baseHandlers) { // ✅
  return new Proxy(raw, baseHandlers)
}
  1. 针对 baseHandlers.ts 文件中的,get set 我们没有必要每次拦截都进行创建,可以使用缓存的技术
export const mutableHandlers = {
  get: createGetter(),
  set: createSetter()
}
// 修改后,每次初始化创建一次,以后拦截都会进行复用
const get = createGetter() // ✅
const set = createSetter() // ✅
export const mutableHandlers = { // ✅
  get,
  set
}
  1. 实现 readonly 的警告功能
export const readonlyHandlers = {
  get: readonlyGet,
  set(target, key, value) {
    console.warn(`${key}不可以修改,因为 ${target}是readonly`) // ✅
    return true
  }
}
  1. 测试
import { isReadonly, reactive, readonly } from "../reactive"

describe('readonly',()=>{
  it('readonly',()=>{
    // not set
    const original = reactive({ foo: 1, bar: { baz: 2 }})
    const wrapped = readonly(original)
    expect(wrapped).not.toBe(original)
    expect(wrapped.foo).toBe(1)
  })

  it('warn when call set',()=>{
    // 验证 console.warn 有没有被触发
    // mock 
    console.warn = jest.fn()
    const user = readonly({
      age: 10
    })
    user.age = 11
    expect(console.warn).toHaveBeenCalled()

  })
  test('readonly nested',()=>{
    const original = { foo: 1, bar: { baz: 2 }}
    const wrapped = readonly(original)
    expect(wrapped).not.toBe(original)
    expect(isReadonly(wrapped)).toBe(true)
    expect(isReadonly(original)).toBe(false)
    expect(isReadonly(wrapped.bar)).toBe(true)
    expect(isReadonly(original.bar)).toBe(false)
    expect(wrapped.foo).toBe(1)
  })
})
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐