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:57【Xi 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 :
- Explain Readonly And the reconstruction of the existing code
- Explain isReactive How to realize
- Explain isReadonly How to realize
- 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 :
reactive
andreadonly
Pass in the same input parametertarget
reactive
andreadonly
All back to proxy objectreactive
andreadonly
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 readonly
set 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 isReadonly
return 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
- 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 .
- isReactive Implementation and isReadonly The principle of implementation is the same , It's all through
createGetter()
InputisReadonly
Judgmental . - 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()
perhapsreadonly()
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
The sidebar is recommended
- Talking about nodejs server
- Node. js&lt; I & gt—— Encounter node and repl usage
- Vue basic API: calculation attribute + filter + listener
- 1-stm32 + mn316 (nb-iot) remote upgrade OTA (self built Internet of things platform) - STM32 uses HTTP to download program files and upgrade programs through mn316 (MCU program rotation check and update)
- Vue Axios response interception
- vue3 ref
- How does Vue transfer the data from the parent component to the child component intact?
- The back-end interface developed by springboot in idea and the Vue front-end developed by vscode. How to integrate Vue code into springboot?
- Fried cold rice series 4: scope and closure in JavaScript
- Typescript type compatibility learning
guess what you like
Summary of bugs encountered in front-end development
Chrome developer tool: performance analysis using web panel
Collation of common semantic elements and global attributes in HTML
Life cycle in Vue
5.1 fear of traffic jam? With a budget of less than 100000, these cars with adaptive cruise make it easy for you to travel
Docker compose deploy nginx configure SSL
The content of element type “mapper“ must match “(cache-ref|cache|resultMap*|parameterMap*|sql*|inse
-CSS-
Vue uses two-way binding to implement the user registration page
Is Infiniti qx60 worth less than 400000 yuan? It depends on the discount
Random recommended
- "Element Fangjian: first heart version" public beta welfare release, go to the great God app to receive red envelopes and prizes
- What is the role of webpack cli in webpack packaging
- Vue3 configuration method using Axios
- How to configure Google reverse proxy on nginx server
- Volume comparison between Vue and react
- What are the three ways to define components in react
- How to install and configure the blogging program Typecho on the nginx server
- How to configure load balancing for TCP in nginx server
- How to configure nginx server under Windows system
- How to configure AB to do stress testing for nginx server
- Analysis of location configuration in nginx server
- How to integrate Linux and redmine into the redmine system
- How to build the production environment of nginx + PHP with PHP FPM
- How to optimize the performance of nginx supporting SSL
- How to configure nginx server to prevent flood attack
- [Axios learning] basic use of Axios
- [Axios learning] Axios request mode, concurrent request, global configuration, instance and interceptor
- Use the virtual queue implemented by list to view the first element of the queue in Python without taking it out
- This dependency was not found and to install it, you can run: NPM install score JS
- Front end serial communication
- leedcode. 203 remove linked list elements
- Dialogue with Liu Dayong, information director of Jiuyang shares: key elements of enterprise digital intelligence transformation
- JQuery gets the method summary of parent element, child element and brother element
- Web Security: analysis of DOM XSS vulnerability source code of jquery
- The sales volume of Genesys in China is 283, less than 300, and the domestic sales volume is dismal
- This beast was blessed with skills to test drive the DHT version of Harvard beast
- Bootstrap and jQuery implement tree structure
- Fried cold rice series 5: recursion in JavaScript?
- 2022 open source summer | serverless devs accompany you to "become stronger"
- How to create a high-performance full screen red envelope rain
- Detailed process of spring boot free HTTPS project configuration!
- Create a simple vue3 project
- How to create a simple react project
- Vue custom text background
- Front end HTML
- leetcode:462. Minimum number of moves to make array elements equal II [sort + find the middle]
- City selection (provincial and urban cascade) plug-in v-distpicker component details and a full set of usage
- How to use js to achieve such a drag and zoom effect
- Quick report ~ node JS 18 coming! Let's see what's new
- Easily manage projects using Vue UI graphical interface