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
,isReadonly
andisProxy
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 perfectreactive
andreadonly
Reactive 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 implementationshallowReactive
andshallowReadonly
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.