current position:Home>🤯 Take you to write Vue core source code (III)

🤯 Take you to write Vue core source code (III)

2022-04-29 20:33:57Xi Yuan_ 99ss

isReactive & isReadonly And use Jest Test to achieve data responsive ( One )

The mountain is not high , If there is a fairy, there is a name . The water is not deep , A dragon is a spirit . —— Liu yuxi 《 On My Modest Room 》

Preface

The first one is right reactive & effect The supplement to the has come to an end , I haven't read the last one here

reactive & effect And use Jest Test to achieve data responsive ( One )

reactive & effect And use Jest Test to achieve data responsive ( Two )

The passage of the whole article TDD Test-driven development , Take you step by step vue3 Source code , There is a complete code at the end of the article .

This article includes :

  1. Explain Readonly And the reconstruction of the existing code
  2. Explain isReactive How to realize
  3. Explain isReadonly How to realize
  4. The proxy object of the nested structure of the source data object

Realization Readonly

In the last article, we have realized reactive, Actually readonly yes reactive A special case of , It's just read-only . It also returns a proxy object , did not set operation , therefore readonly No dependency triggers , Since there is no dependency trigger , Then it doesn't need get Dependency collection of operations .

Let's take a look at our readonly The test case :

describe('readonly', () => {
  it('readonly not set', () => {
    let original = {
      foo: {
        fuck: {
          name: "i don't care",
        },
      },
      arr: [{color: '#fff'}]
    }
    let warper = readonly(original)
    expect(warper).not.toBe(original)
    expect(warper.foo.fuck.name).toBe("i don't care")
  })
  it('warning when it be call set operation', () => {
    let original = {
      username: 'ghx',
    }
    let readonlyObj = readonly(original)
    const warn = jest.spyOn(console, 'warn')
    //  to readonly do set operation , Will get a warning
    readonlyObj.username = 'danaizi'
    expect(warn).toHaveBeenCalled()
  })
})
 Copy code 

If you insist reactive Proxy object assignment for , Then it will get a warning (warn)

export function readonly<T>(target: T) {
  return new Proxy(target, {
    get(target, key) {
      let res = Reflect.get(target, key)
      //  No need to rely on collection , Deleted track() function 
      return res
    },
    set(target, key, value) {
      //  No need to trigger dependencies 
      console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`)
      return true
    },
  })
}
 Copy code 

Refactoring existing code

We've done this before reactive and readonly, At this time, we should look at the code , It is necessary for us to optimize whether there are repeated code segments in the code . The current shortage :reactive and readonly Have similar implementations , There are many same code snippets , You can pull it out sketch :

  1. reactive and readonly Pass in the same input parameter target
  2. reactive and readonly All back to proxy object
  3. reactive and readonly Of proxy Object has get and set Method , But the internal code implementation is a little different

In order to deal with these same code logic , We might as well create a new file baseHandlers.ts As Proxy The second input of handler The definition file for , And because all they return are new Proxy() object , So we can define a createReactiveObject function , Used to uniformly create proxy object , Improve the readability of the code .

// In baseHandlers.ts
export function createReactiveObject<T extends object>(target: T, handlers: ProxyHandler<T>) {
  return new Proxy(target, handlers)
}
 Copy code 
// In reactive.ts

export function reactive<T extends object>(target: T) {
  return createReactiveObject<T>(target, mutableHandlers)
}
//  In fact, there is no set Operation of the reactive
export function readonly<T extends object>(target: T) {
  return createReactiveObject<T>(target, readonlyHandlers)
}
 Copy code 

handlers Pass in as an object createReactiveObject, In this way, we can deal with reactive and readonly Different logic .

export const mutableHandlers: ProxyHandler<object> = {
  get: function(target: T, key: string | symbol) {
    let res = Reflect.get(target, key)
    //  Rely on collection 
    track(target, key as string)
    return res
  },
  set: function(target: T, key: string | symbol, value: any) {
    let success: boolean
    success = Reflect.set(target, key, value)
    //  Trigger dependency 
    trigger(target, key as string)
    return success
  },
}
export const readonlyHandlers: ProxyHandler<object> = {
  get: function(target: T, key: string | symbol) {
    let res = Reflect.get(target, key)
    return res
  },
  set(target, key, value) {
    console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`)
    return true
  },
}
 Copy code 

Look closely at the code above , We all have the same set and get, The internal also implements the value operation , But the difference is readonly Of set The operation throws warn, We don't need to deal with this . In order to distinguish between reactive and readonlyset and get Different logic , We need a logo isReadonly

Pull away the same set and get Code , We need to define set and get function , But we need to pass in an identifier isReadonly Distinguish whether the function is reactive The code logic is still readonly Code logic , At the same time, it cannot be set and get Add other input parameters to prevent damaging the readability of the code . We can define a higher-order function , This function returns set and set function , Participation is isReadonly.

// In baseHandlers.ts
//  Higher order function ,isReadonly The default is false
export function createGetter<T extends object>(isReadonly = false) {
  return function get(target: T, key: string | symbol) {
    
    let res = Reflect.get(target, key)
    
    if (!isReadonly) {
      //  Determine whether readonly
      //  Rely on collection 
      track(target, key as string)
    }
    return res
  }
}
export function createSetter<T extends object>() {
  return function set(target: T, key: string | symbol, value: any) {
    let success: boolean
    success = Reflect.set(target, key, value)
    //  Trigger dependency 
    trigger(target, key as string)
    return success
  }
}
 Copy code 

Then define different handlers object , Used to pass in as an input parameter createReactiveObject

// reactive Of handlers
export const mutableHandlers: ProxyHandler<object> = {
  get: createGetter(),
  set: createSetter(),
}
// readonly Of handlers
export const readonlyHandlers: ProxyHandler<object> = {
  get: createGetter(true),
  set(target, key, value) {
    console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`)
    return true
  },
}
 Copy code 

An optimization point is hidden here

Realization isReadonly

🧠 Let's think about it. Let's look at the current code , What is used to judge whether a proxy object is readonly

The answer is createGetter Input isReadonly, Before observing readonly The implementation of can know , Let the source data pass through Proxy After packaging , It's already in handler Of get Whether the proxy object is readonly Proxy object .

Since the get Only in operation can we get isReadonly, We might as well trigger get Let's do it .

Trigger get The operation has a premise , That is, you can trigger... By accessing the properties of the proxy object get operation . You might say that we can trigger... By randomly accessing the known properties of the proxy object get operation , then return Is it readonly The result is not on the line ? But if the user really only wants to access the properties of the proxy object, he doesn't want to know who you are readonly still reactive, This does not appear bug Did you?

To do this, we need to fabricate an attribute that does not exist in a proxy object , Call __v_isReadonly

Define a function isReadonly, Used to determine whether a proxy object is readonly Proxy object , This function triggers the of the proxy object get operation , Returns a Boolean value .

//  to value Make type comments , Give Way value There are several optional properties , Or damn it value Red  --isReactive Functions and isReadonly function   It's about you 
export interface Target {
  __v_isReadonly?: boolean;
}
export function isReadonly(value: unknown){
  return (value as Target)['__v_isReadonly']
}
 Copy code 

in addition , With __v_isReadonly attribute , We know that users want to pass get Operation to determine whether the proxy object is readonly, Still want to pass get The operation accesses the specified property value .

All we have to do is put isReadonlyreturn get out

export function createGetter<T extends object>(isReadonly = false) {
  return function get(target: T, key: string | symbol) {
    if(key === '__v_isReadonly'){
      return isReadonly
    }
    let res = Reflect.get(target, key)

    if (!isReadonly) {
      //  Determine whether readonly
      //  Rely on collection 
      track(target, key as string)
    }
    return res
  }
}
 Copy code 

The following is a isReadonly Test cases for

it('readonly not set', () => {
    let original = {
      foo: {
        fuck: {
          name: 'what',
        },
      },
      arr: [{color: '#fff'}]
    }
    let warper = readonly(original)
    expect(warper).not.toBe(original)
    expect(isReadonly(warper)).toBe(true)
    expect(isReadonly(original)).toBe(false)  
    //  Test nested objects reactive state 
    expect(isReadonly(warper.foo.fuck)).toBe(true)
    // expect(isReadonly(warper.foo.fuck.name)).toBe(true) //  because name Is a basic type, so isObject Would be false, Right now name You can't generate readonly, It involves the knowledge points in the future  isRef
    expect(isReadonly(warper.arr)).toBe(true)
    expect(isReadonly(warper.arr[0])).toBe(true)
    expect(warper.foo.fuck.name).toBe('what')
  })
 Copy code 

Starting the test was smooth ,isReadonly Pass in a proxy object , return true That's all right. , Um. ? How can the test fail when the incoming source data is executed ?

The reason is that the source data is not proxied , Does not trigger get operation , The result is isReadonly(original) Only return undefined, because original There is no such thing as __v_isReadonly attribute .

Then we just have to let it go back false Just fine . adopt !! Double exclamation mark , Turn it into a Boolean .undefined Will turn into false.

export function isReadonly(value: unknown){
  return !!(value as Target)['__v_isReadonly']
}
 Copy code 

Realization isReactive

isReactive It's simple , because createGetter The input parameter of is a Boolean value isReadonly, So it's not isReadonly, Namely isReactive.

Ideas and isReadonly equally , Just put isReadonly Switch to isReactive, And then through get operation , Returns a Boolean value .

export interface Target {
  __v_isReadonly?: boolean;
  __v_isReactive?: boolean;
}
export function isReactive(value: unknown) {
  
  return !!(value as Target)['__v_isReactive']
}
 Copy code 
export function createGetter<T extends object>(isReadonly = false) {
  return function get(target: T, key: string | symbol) {
    // isReactive and isReadonly  Are based on the passed in parameters  `isReadonly` To decide whether to return true | false Of 
    if (key === '__v_isReactive') {
      return !isReadonly
    } else if (key === '__v_isReadonly') {
      return isReadonly
    }
    let res = Reflect.get(target, key)
    //  Before, only one layer of surface was realized reactive, We now implement the of nested objects reactive
    if(isObject(res)){
      return isReadonly ? readonly(res) : reactive(res)
    }
    if (!isReadonly) {
      //  Determine whether readonly
      //  Rely on collection 
      track(target, key as string)
    }
    return res
  }
}
 Copy code 

Look at the status of strings , Do you feel bad . We use it typescript Of enum Management status , Enhance code readability .

export enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly'
}
export interface Target {
  [ReactiveFlags.IS_REACTIVE]?: boolean;
  [ReactiveFlags.IS_READONLY]?: boolean;
}
 Copy code 

After that, just put enum Of key Replace the exposed string above , It's not all said here .

Encountered nested object

When a nested object is encountered to generate a proxy object as source data , The sub object of the proxy object is called as a parameter isReactive Or call isReadonly, Returns the false, Because the objects inside are not represented .

Here are the test cases for this situation

it('nested reactive',()=>{
    let original = {
      foo: {
        name: 'ghx'
      },
      arr: [{age: 23}]
    }
    const nested = reactive(original)
    expect(isReactive(nested.foo)).toBe(true)    
    expect(isReactive(nested.arr)).toBe(true)    
    expect(isReactive(nested.arr[0])).toBe(true) 
    expect(isReactive(nested.foo)).toBe(true)    
    // expect(isReactive(nested.foo.name)).toBe(true)  //  It involves the knowledge points in the future  isRef
    
  })
 Copy code 

To pass the test case , We have to turn nested objects into reactive Proxy object .

When triggered get The result of the operation is res, Let's add a judgment , If you find that res No reactive perhaps readonly, also res It's the object , So recursively call reactive() perhaps readonly().

To determine whether it is an object, we define a isObject stay shared/index.ts in .

//  Judge value whether object perhaps array
export const isObject = (value: unknown) => {
  return value !== null && typeof value === 'object'
}
 Copy code 

Because in get Judged during operation res, We are createGetter() The article above

export function createGetter<T extends object>(isReadonly = false) {
  return function get(target: T, key: string | symbol) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    }
    let res = Reflect.get(target, key)
    //  Before, only one layer of surface was realized reactive, We now implement the of nested objects reactive
    if(isObject(res)){
      return isReadonly ? readonly(res) : reactive(res)
    }
    if (!isReadonly) {
      //  Determine whether readonly
      //  Rely on collection 
      track(target, key as string)
    }
    return res
  }
}
 Copy code 

Optimization point

take the reverse into consideration mutableHandlers and readonlyHandlers

// reactive Of handlers
export const mutableHandlers: ProxyHandler<object> = {
  get: createGetter(),
  set: createSetter(),
}
// readonly Of handlers
export const readonlyHandlers: ProxyHandler<object> = {
  get: createGetter(true),
  set(target, key, value) {
    console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`)
    return true
  },
}
 Copy code 

Each time the proxy object triggers proxy Of get When operating, it will call createGetter(),set The operation is the same . To optimize the code , Reduce to createGetter() Number of calls , Let's pull away alone createGetter() and createSetter(), Receive... With a constant .

//  Call here once createSetter and createGetter, In order not to use every time mutableHandlers Call repeatedly when 
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)
 Copy code 

therefore mutableHandlers and readonlyHandlers It should be rewritten as

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
}
export const readonlyHandlers: ProxyHandler<object> = {
  get: readonlyGet,
  set(target, key, value) {
    console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`)
    return true
  },
}
 Copy code 

If you don't understand why you want to pull out your classmates Look here

summary

  1. readonly Implementation and reactive The implementation of is a little similar , But it's a little different . No, set operation , No dependency collection , Trigger dependency .
  2. isReactive Implementation and isReadonly The principle of implementation is the same , It's all through createGetter() Input isReadonly Judgmental .
  3. When encountering the source data of nested objects, generate proxy objects , Children of proxy objects are also proxied . We determine whether it is an object, and then recursively call reactive() perhaps readonly() To achieve .

Last @ Thank you for reading

Don't read the past , Not afraid of the future .

Complete code

// In share/index.ts
//  Judge value whether object perhaps array
export const isObject = (value: unknown) => {
  return value !== null && typeof value === 'object'
}

 Copy code 
// In reactive.ts

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

export enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly'
}
//  to value Make type comments , Give Way value There are several optional properties , Or damn it value Red  --isReactive Functions and isReadonly function   It's about you 
export interface Target {
  [ReactiveFlags.IS_REACTIVE]?: boolean;
  [ReactiveFlags.IS_READONLY]?: boolean;
}

export function reactive<T extends object>(target: T) {
  return createReactiveObject<T>(target, mutableHandlers)
}
//  In fact, there is no set Operation of the reactive
export function readonly<T extends object>(target: T) {
  return createReactiveObject<T>(target, readonlyHandlers)
}

export function isReactive(value: unknown) {
  // target No, __v_isReactive This attribute , Why write target['__v_isReactive'] Well ? Because it triggers proxy Of get operation ,
  //  By judgment createGetter Incoming parameter isReadonly Is it true, otherwise isReactive by true
  //  Optimization point : use enum Management status , Enhance code readability 
  return !!(value as Target)[ReactiveFlags.IS_REACTIVE]
}
export function isReadonly(value: unknown){
  //  ditto 
  return !!(value as Target)[ReactiveFlags.IS_READONLY]
}

 Copy code 
// In baseHandlers.ts

import { track, trigger } from './effect'
import { reactive, ReactiveFlags, readonly } from './reactive'
import { isObject } from '../shared'
//  Call here once createSetter and getter, In order not to use every time mutableHandlers Call repeatedly when 
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)

//  Higher order function ,
export function createGetter<T extends object>(isReadonly = false) {
  return function get(target: T, key: string | symbol) {
    // isReactive and isReadonly  Are based on the passed in parameters  `isReadonly` To decide whether to return true | false Of 
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    }
    let res = Reflect.get(target, key)
    //  Before, only one layer of surface was realized reactive, We now implement the of nested objects reactive
    if(isObject(res)){
      return isReadonly ? readonly(res) : reactive(res)
    }
    if (!isReadonly) {
      //  Determine whether readonly
      //  Rely on collection 
      track(target, key as string)
    }
    return res
  }
}
export function createSetter<T extends object>() {
  return function set(target: T, key: string | symbol, value: any) {
    let success: boolean
    success = Reflect.set(target, key, value)
    //  Trigger dependency 
    trigger(target, key as string)
    return success
  }
}

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
}
export const readonlyHandlers: ProxyHandler<object> = {
  get: readonlyGet,
  set(target, key, value) {
    console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`)
    return true
  },
}
export function createReactiveObject<T extends object>(target: T, handlers: ProxyHandler<T>) {
  return new Proxy(target, handlers)
}

 Copy code 
// In readonly.spec.ts
import { readonly, isReadonly } from '../reactive'
describe('readonly', () => {
  it('readonly not set', () => {
    let original = {
      foo: {
        fuck: {
          name: 'what',
        },
      },
      arr: [{color: '#fff'}]
    }
    let warper = readonly(original)
    expect(warper).not.toBe(original)
    expect(isReadonly(warper)).toBe(true)
    expect(isReadonly(original)).toBe(false)
    //  Test nested objects reactive state 
    expect(isReadonly(warper.foo.fuck)).toBe(true)
    // expect(isReadonly(warper.foo.fuck.name)).toBe(true) //  because name Is a basic type, so isObject Would be false, Right now name You can't generate readonly, It involves the knowledge points in the future  isRef
    expect(isReadonly(warper.arr)).toBe(true)
    expect(isReadonly(warper.arr[0])).toBe(true)
    expect(warper.foo.fuck.name).toBe('what')
  })
  it('warning when it be call set operation', () => {
    let original = {
      username: 'ghx',
    }
    let readonlyObj = readonly(original)
    const warn = jest.spyOn(console, 'warn')
    readonlyObj.username = 'danaizi'
    expect(warn).toHaveBeenCalled()
  })
})

 Copy code 
// In reactice.spec.ts

import { reactive, isReactive } from '../reactive'

describe('reactive', () => {
  it('reactive test', () => {
    let original = { num: 1 }
    let count = reactive(original)
    expect(original).not.toBe(count)
    expect(count.num).toEqual(1)
    expect(isReactive(original)).toBe(false)
    expect(isReactive(count)).toBe(true)
  })
  it('nested reactive',()=>{
    let original = {
      foo: {
        name: 'ghx'
      },
      arr: [{age: 23}]
    }
    const nested = reactive(original)
    expect(isReactive(nested.foo)).toBe(true)
    expect(isReactive(nested.arr)).toBe(true)
    expect(isReactive(nested.arr[0])).toBe(true)
    expect(isReactive(nested.foo)).toBe(true)
    // expect(isReactive(nested.foo.name)).toBe(true) //  It involves the knowledge points in the future  isRef
    
  })
})

 Copy code 

copyright notice
author[Xi Yuan_ 99ss],Please bring the original link to reprint, thank you.
https://en.qdmana.com/2022/04/202204292033493914.html

Random recommended