Skip to content

Latest commit

 

History

History
205 lines (161 loc) · 4.49 KB

5. 实现 readonly 功能.md

File metadata and controls

205 lines (161 loc) · 4.49 KB

实现 readonly 功能

在本小节中,我们将会实现 readonly API

1. happy path 单元测试

it('happy path', () => {
    // not set
    const original = { foo: 1, bar: 2 }
    const wrapped = readonly(original)
    expect(wrapped).not.toBe(original)
    expect(wrapped.bar).toBe(2)
    wrapped.foo = 2
    // set 后不会更改
    expect(wrapped.foo).toBe(1)
})

2. 实现 happy path

我们知道 readonly 和 reactive 的实现原理是一致的,都可以通过 Proxy 来实现一个包装类,唯一的区别在于,readonly 的不会被 track,而且 readonly 的属性值不可更改

那么该如何实现呢?

2.1 v1

// reactive.ts
export function readonly(raw) {
    return new Proxy(raw, {
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            return res
        },
        set() {
            return true
        },
    })
}

在这个版本下,我们就实现了最简单的 readonly 的实现。但是我们可以发现其实 reactive 和 readonly 的部分代码是一样的,就可以提取重复代码变为函数:

2.2 v2

import { track, trigger } from './effect'


// version 2 版本就可以将重复的代码提取出来
// 作为 createGetter 和 createSetter
function createGetter(isReadonly = false) {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    if (!isReadonly) {
      track(target, key)
    }
    return res
  }
}

function createSetter() {
  return function set(target, key, value, receiver) {
    const res = Reflect.set(target, key, value, receiver)
    trigger(target, key)
    return res
  }
}

export function reactive(raw) {
  return new Proxy(raw, {
    get: createGetter(),
    set: createSetter(),
  })
}

export function readonly(raw) {
  return new Proxy(raw, {
    get: createGetter(true),
    set() {
      return true
    },
  })
}

为了更好的管理代码,在 v3 中,我们还可以直接将 createSetter 和 createGetter 分层出去

2.3 v3

// reactivity/baseHandlers.ts
import { track, trigger } from './effect'

const get = createGetter()
const readonlyGet = createGetter(true)
const set = createSetter()

function createGetter(isReadonly = false) {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    // 在 get 时收集依赖
    if (!isReadonly) {
      track(target, key)
    }
    return res
  }
}

function createSetter() {
  return function set(target, key, value, receiver) {
    const res = Reflect.set(target, key, value, receiver)
    // 在 set 时触发依赖
    trigger(target, key)
    return res
  }
}

// mutable 可变的
export const mutableHandlers = {
  get,
  set,
}

export const readonlyHandlers = {
  get: readonlyGet,
  set(target, key, value) {
    return true
  },
}
import { mutableHandlers, readonlyHandlers } from './baseHandlers'

export function reactive(raw) {
  return new Proxy(raw, mutableHandlers)
}

export function readonly(raw) {
  return new Proxy(raw, readonlyHandlers)
}

这样就可以将实现与入口分离开来了。下面我们看到其实 reactive 和 readonly 它的创建方式是差不多的都是通过 new Proxy 的方式来创建,那么这些步骤我们也可以来分离开

2.4 v4

// reactive.ts

import { mutableHandlers, readonlyHandlers } from './baseHandlers'

function createActiveObject(raw, baseHandlers) {
  return new Proxy(raw, baseHandlers)
}

export function reactive(raw) {
  return createActiveObject(raw, mutableHandlers)
}

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

这样我们就实现了最少的代码了

3. 警告特性单元测试

这个单元测试,我们要让用户在设置一个 readonly prop value 时报一个警告

it('should warn when update readonly prop value', () => {
    // 这里使用 jest.fn
    console.warn = jest.fn()
    const readonlyObj = readonly({ foo: 1 })
    readonlyObj.foo = 2
    expect(console.warn).toHaveBeenCalled()
})

4. 实现警告特性

这里我们发现实现警告还是非常简单的,只需要找到 readonly proxy 的 set 即可

// baseHandlers.ts

export const readonlyHandlers = {
  get,
  set(target, key, value) {
    // 在这里警告
    console.warn(
      `key: ${key} set value: ${value} fail, because the target is readonly`,
      target
    )
    return true
  },
}

这个时候我们再跑一边测试,发现就完全没问题了。