Welcome to CoderStan’s handwritten Mini-Vue3 column and join me in writing your own Mini-Vue3. This chapter will simply implement readonly, shallowReactive, and shallowReadonly in the ReActivity module. (Thanks to Atri CXR mini-Vue)

If you feel good, please support it. If you want to see other parts of the article, you can pay attention to me or pay attention to my handwritten mini-vue3 column. If you want to see the annotated source code, welcome to visit GitHub warehouse, and please also point a star to support it.

3. Realize the reactivity

3.7 Implement the most basicreadonly

Check the responsiveness API section of the Vue3 API documentation for an introduction to ReadOnly:

A read-only proxy that accepts an object (reactive or pure) or ref and returns the original object. A read-only proxy is deep: any nested property accessed is also read-only.

const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() = > {
  // For responsiveness tracing
  console.log(copy.count)
})

// Changing original triggers listeners that depend on replicas
original.count++

// Changing copies will fail and cause a warning
copy.count++ / / warning!
Copy the code

To implement readonly, first create the readonly test file readone.spec. ts in the SRC /reactivity/__tests__ directory and add the following test code:

describe('reactivity/readonly'.() = > {
  it('should make values readonly'.() = > {
    const original = { foo: 1 }
    // Create a readOnly responsive object
    const wrapped = readonly(original)
    console.warn = jest.fn()
    // readonly The responsive object is not equal to the original object
    expect(wrapped).not.toBe(original)
    expect(wrapped.foo).toBe(1)
    // readonly The property of a responsive object is read-only
    wrapped.foo = 2
    expect(wrapped.foo).toBe(1)
    Console. warn is called to warn when property values of readOnly responsive objects are changed
    expect(console.warn).toBeCalled()
  })
})
Copy the code

To pass the above test, implement and export readonly in react. ts in the SRC /reactivity/ SRC directory:

export function readonly(raw) {
  // Return the instance of Proxy
  return new Proxy(raw, {
    // Proxies the get of the original object
    get(target, key) {
      const res = Reflect.get(target, key)

      return res
    },
    // Proxies the set of the original object
    set() {
      // TODO warning!
      return true}})}Copy the code

Run the YARN test readonly command to run the test of readonly. The test passes. This completes the basic implementation of readonly.

Reactive and Readonly implementations have a lot of duplication and need to be optimized to remove the duplication and improve readability. Create a basehandlers. ts file in the SRC /reactivity/ SRC directory and pull out the code associated with creating the handlers used to construct the Proxy and the utility functions and caching using global variables:

// Cache get and set to prevent repeated calls to utility functions
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)

// The utility function used to generate get functions
function createGetter(isReadonly = false) {
  return function (target, key) {
    const res = Reflect.get(target, key)

    // Dependency collection is performed when reactive transformations are performed
    if(! isReadonly) {// Collect dependencies
      track(target, key)
    }

    return res
  }
}

// The utility function that generates the set function
function createSetter() {
  return function (target, key, value) {
    const res = Reflect.set(target, key, value)
    // Trigger dependencies
    trigger(target, key)
    return res
  }
}

// Reactive to the handlers
export const mutableHandlers = {
  get,
  set
}

// Readonly corresponding handlers
export const readonlyHandlers = {
  get: readonlyGet,
  set(target, key) {
    // Call console.warn to issue warnings
    console.warn(
      `Set operation on key "${key}" failed: target is readonly.`,
      target
    )
    return true
}
Copy the code

The implementation of Reactive and Readonly is then optimized to remove utility functions:

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

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

// The utility function used to create the Proxy instance
function createReactiveObject(raw, baseHandlers) {
  // Return the instance of Proxy
  return new Proxy(raw, baseHandlers)
}
Copy the code

3.8 implementationisReactive,isReadonlyandisProxy

See the responsive API section of the Vue3 API documentation for descriptions of isProxy, isReactive, and isReadonly:

isProxy

Check whether the object is a proxy created by Reactive or Readonly.

isReactive

Check whether the object is a reactive agent created by Reactive.

import { reactive, isReactive } from 'vue'
export default {
  setup() {
    const state = reactive({
      name: 'John'
    })
    console.log(isReactive(state)) // -> true}}Copy the code

It also returns true if the agent is created by ReadOnly but wraps another agent created by Reactive.

import { reactive, isReactive, readonly } from 'vue'
export default {
  setup() {
    const state = reactive({
      name: 'John'
    })
    // A read-only proxy created from a normal object
    const plain = readonly({
      name: 'Mary'
    })
    console.log(isReactive(plain)) // -> false

    // Read-only proxy created from reactive proxy
    const stateCopy = readonly(state)
    console.log(isReactive(stateCopy)) // -> true}}Copy the code

isReadonly

Check whether the object is a read-only proxy created by ReadOnly.

1) implementationisReactive

Before implementing isReactive, add the test code about isReactive to react.spec. ts.

describe('reactivity/reactive'.() = > {
  it('Object'.() = > {
    const original = { foo: 1 }
    const observed = reactive(original)
    expect(observed).not.toBe(original)
    Calling isReactive on a reactive object returns true
    expect(isReactive(observed)).toBe(true)
    Calling isReactive on an ordinary object returns false
    expect(isReactive(original)).toBe(false)
    expect(observed.foo).toBe(1)})})Copy the code

To pass the above test, implement and export isReactive in the reactivity. ts file in the SRC /reactivity/ SRC directory:

// Check whether an object is a reactive object created by Reactive
export function isReactive(value) :boolean {
  // Get the value of a special property of the object, which triggers get. The property name is __v_isReactive
  return!!!!! value['__v_isReactive']}Copy the code

You also need to modify the createGetter utility function in the basehandlers. ts file in the SRC /reactivity/ SRC directory:

function createGetter(isReadonly = false) {
  return function (target, key) {
    // When the property name is __v_isReactive, isReactive is being called. isReadonly
    if (key === '__v_isReactive') {
      return! isReadonly }/* Other code */}}Copy the code

Run the yarn test reactive command to run the test of reactive and check that the test passes. In this way, isReactive is implemented.

(2) implementationisReadonly

To implement isReadonly, add the following code to the readone.spec. ts test file:

describe('reactivity/readonly'.() = > {
  it('should make values readonly'.() = > {
    const original = { foo: 1 }
    const wrapped = readonly(original)
    console.warn = jest.fn()
    expect(wrapped).not.toBe(original)
    // Calling isReactive on readOnly reactive objects returns false
    expect(isReactive(wrapped)).toBe(false)
    // Calling isReadonly on readOnly responsive objects returns true
    expect(isReadonly(wrapped)).toBe(true)
    Calling isReactive on an ordinary object returns false
    expect(isReactive(original)).toBe(false)
    // Calling isReadonly on normal objects returns false
    expect(isReadonly(original)).toBe(false)
    expect(wrapped.foo).toBe(1)
    wrapped.foo = 2
    expect(wrapped.foo).toBe(1)
    expect(console.warn).toBeCalled()
  })
})
Copy the code

To pass the above test, implement and export isReadonly in the reactive. Ts file in the SRC /reactivity/ SRC directory:

// Check if the object is a Readonly responsive object created by readonly
export function isReadonly(value) :boolean {
  // Get the value of a special property of the object, which triggers get. The property name is __v_isReactive
  return!!!!! value['__v_isReadonly']}Copy the code

You also need to modify the createGetter utility function in the basehandlers. ts file in the SRC /reactivity/ SRC directory:

function createGetter(isReadonly = false) {
  return function (target, key) {
    // When the property name is __v_isReactive, isReactive is being called. isReadonly
    if (key === '__v_isReactive') {
      return! isReadonly }// If the property name is __v_isReadonly, isReadonly is being called and isReadonly is returned
    else if (key === '__v_isReadonly') {
      return isReadonly
    }

    /* Other code */}}Copy the code

Run the yarn test readonly command to run the readonly test. The test succeeds. In this way, isReadonly is implemented.

(3) implementationisProxy

Before implementing isProxy, add isProxy test code to react.spec. ts and readonly test file react.spec. ts respectively.

// reactive.spec.ts
describe('reactivity/reactive'.() = > {
  it('Object'.() = > {
    const original = { foo: 1 }
    const observed = reactive(original)
    expect(observed).not.toBe(original)
    expect(isReactive(observed)).toBe(true)
    expect(isReactive(original)).toBe(false)
    Calling isProxy on a reactive object returns true
    expect(isProxy(observed)).toBe(true)
    Calling isProxy on a normal object returns false
    expect(isProxy(original)).toBe(false)
    expect(observed.foo).toBe(1)})})Copy the code
// readonly.spec.ts
describe('reactivity/readonly'.() = > {
  it('should make values readonly'.() = > {
    const original = { foo: 1 }
    const wrapped = readonly(original)
    console.warn = jest.fn()
    expect(wrapped).not.toBe(original)
    expect(isReactive(wrapped)).toBe(false)
    expect(isReadonly(wrapped)).toBe(true)
    expect(isReactive(original)).toBe(false)
    expect(isReadonly(original)).toBe(false)
    // Calling isProxy on readOnly responsive objects returns true
    expect(isProxy(wrapped)).toBe(true)
    Calling isProxy on a normal object returns false
    expect(isProxy(original)).toBe(false)
    expect(wrapped.foo).toBe(1)
    wrapped.foo = 2
    expect(wrapped.foo).toBe(1)
    expect(console.warn).toBeCalled()
  })
})
Copy the code

To pass the above test, implement and export isProxy in the reactive. Ts file in the SRC /reactivity/ SRC directory:

// Check whether an object is a reactive object created by Reactive or Readonly
export function isProxy(value) :boolean {
  // Check by isReactive and isReadonly
  return isReactive(value) || isReadonly(value)
}
Copy the code

Run the yarn test reactive and yarn test readonly commands respectively to run the reactive and readonly tests. After the tests pass, isProxy is implemented.

④ Optimize the code

IsReactive and isReadonly need to be optimized by creating and exporting the enumeration type ReactiveFlags to hold these two strings:

// baseHandlers.ts
// The name of the special property used in isReactive and isReadonly
export const enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly'
}

function createGetter(isReadonly = false) {
  return function (target, key) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return! isReadonly }else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    }

    /* Other code */}}Copy the code
// reactive.ts
export function isReactive(value) :boolean {
  return!!!!! value[ReactiveFlags.IS_REACTIVE] }export function isReadonly(value) :boolean {
  return!!!!! value[ReactiveFlags.IS_READONLY] }Copy the code

3.9 perfectreactiveandreadonlyReactive transforms nested objects

Reactive and Readonly’s reactive transformations are “deep” and affect all nested properties, i.e. nested properties should also be reactive.

Add the following test code to reactive’s react.spec. ts and readonly’s readone.spec. ts respectively:

// reactive.spec.ts
describe('reactivity/reactive'.() = > {
  it('nested reactives'.() = > {
    const original = { foo: { bar: 1}}const observed = reactive(original)
    // Nested objects are reactive
    expect(isReactive(observed.foo)).toBe(true)})})Copy the code
// readonly.spec.ts
describe('reactivity/readonly'.() = > {
  it('should make nested values readonly'.() = > {
    const original = { foo: { bar: 1}}const wrapped = readonly(original)
    // Nested objects are reactive
    expect(isReadonly(wrapped.foo)).toBe(true)})})Copy the code

To pass the above tests, you need to improve the implementation of Reactive and readonly. You need to modify the createGetter function in the baseHandlers. Ts file in SRC /reactivity/ SRC as follows:

function createGetter(isReadonly = false) {
  return function (target, key) {
    /* Other code */

    const res = Reflect.get(target, key)

    if(! isReadonly) { track(target, key) }// If the property value is an object, reactive and readonly are used for reactive conversion
    if (typeof res === 'object'&& res ! = =null) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}
Copy the code

Run the yarn test reactive and yarn test readonly commands respectively to run the tests reactive and readonly, and see that the tests pass. This further improves the implementation of reactive and readonly.

Since it may be used multiple times, it is possible to separate a variable from an object into an isObject function. Add the following code to the index.ts file in the SRC /shared directory:

// Is used to determine whether a variable is an object
export const isObject = value= > typeof value === 'object'&& value ! = =null
Copy the code

Then use isObject to complete the createGetter utility function:

function createGetter(isReadonly = false) {
  return function (target, key) {
    /* Other code */

    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    /* Other code */}}Copy the code

3.10 implementationshallowReactiveandshallowReadonly

Check the responsiveness API section of the Vue3 API documentation for descriptions of shallowReactive and shallowReadonly:

shallowReactive

Create a reactive proxy that tracks the responsiveness of its own property, but does not perform deep reactive transformations of nested objects (exposing raw values).

const state = shallowReactive({
  foo: 1.nested: {
    bar: 2}})// Changing the nature of state itself is reactive
state.foo++
/ /... But nested objects are not converted
isReactive(state.nested) // false
state.nested.bar++ // non-responsive
Copy the code

Unlike Reactive, any property that uses ref is not automatically unpacked by the agent.

shallowReadonly

Create a proxy that makes its own property read-only but does not perform deep read-only conversions of nested objects (exposing raw values).

const state = shallowReadonly({
  foo: 1.nested: {
    bar: 2}})// Changing the property of state itself will fail
state.foo++
/ /... But it applies to nested objects
isReadonly(state.nested) // false
state.nested.bar++ / / for
Copy the code

Unlike readOnly, any property that uses ref is not automatically unpacked by the agent.

Before implementing shallowReactive and shallowReadonly, Create shallowReactive and shallowReadonly test files shallowReactive. Spec. ts and shallowReadonly.spec.ts in SRC /reactivity/__tests__. Add the following test code, respectively:

// shallowReactive.spec.ts
describe('shallowReactive'.() = > {
  test('should not make non-reactive properties reactive'.() = > {
    const props = shallowReactive({ n: { foo: 1 } })
    expect(isReactive(props.n)).toBe(false)})})Copy the code
// shallowReadonly.spec.ts
describe('reactivity/shallowReadonly'.() = > {
  test('should not make non-reactive properties reactive'.() = > {
    const props = shallowReadonly({ n: { foo: 1 } })
    expect(isReactive(props.n)).toBe(false)})})Copy the code

SRC /reactivity/ SRC createGetter (baseHandlers. Ts); SRC /reactivity/ SRC (baseHandlers. Ts);

function createGetter(isReadonly = false, shallow = false) {
  return function (target, key) {
    /* Other code */

    const res = Reflect.get(target, key)

    Dependencies are collected when reactive and shallowReactive are used for reactive transformations
    if(! isReadonly) {// Collect dependencies
      track(target, key)
    }

    // If shallowReactive and shallowReadonly are used for reactive conversion, the value is returned
    if (shallow) {
      return res
    }

    /* Other code */}}Copy the code

ShallowRreactive and shallowReadonly handlers are constructed in the basehandlers. ts file under SRC /reactivity/ SRC. These are obtained by replacing get property with mutableHandlers and readonlyHandlers:

Handlers are made up of mutableHandlers instead of get Properties
export const shallowHandlers = extend({}, mutableHandlers, {
  get: shallowGet
})

Handlers are given by readonlyHandlers instead of get property
export const shallowReadonlyHandlers = extend({}, readonlyHandlers, {
  get: shallowReadonlyGet
})
Copy the code

Finally, implement and export shallowRreactive and shallowReadonly in the reactivity. ts file in the SRC /reactivity/ SRC directory:

export function shallowReactive(raw) {
  return createReactiveObject(raw, shallowHandlers)
}

export function shallowReadonly(raw) {
  return createReactiveObject(raw, shallowReadonlyHandlers)
}
Copy the code

Run the yarn test shallowRreactive and yarn test shallowReadonly commands respectively to run the shallowRreactive and shallowReadonly tests. You can see that the tests pass. This implements shallowRreactive and shallowReadonly.