Composition api by hand
Introduction
This article mainly emphasizes on rewriting composition api by hand. Four sets of api are included in the article, which are shallowReactive and reactive, shallowRef and ref, shallowReadonly and readonly and isRef, isReactive, isReadonly and isProxy respectively. The main implementation of the api is data hijacking operation, while the update operation has not yet been involved.
shallowReactive & reactive
//shallowReactive and reactive
//Define reactiveHandler
const reactiveHandler = {
//Get property
get(target, property) {
if (property === '_is_reactive') return true;
const result = Reflect.get(target, property);
console.log('Intercept get operation', property, result);
return result;
},
//Amend or add property
set(target, property, val) {
const result = Reflect.set(target, property);
console.log('Intercept amend operation or add operation', property, val);
return result;
},
//Delete property
deleteProperty(target, property) {
const result = Reflect.deleteProperty(target, property);
console.log('Intercept delete operation', property);
return result;
}
}
//Define shallowReactive function
//Parameter is a target object
function shallowReactive(target) {
//Determines if the target object is of type Object (object/array)
if (target && typeof target === 'object') {
return new Proxy(target, reactiveHandler);
}
//If the target is of primitive type, return target directly
return target;
}
//Define reactive function
function reactive(target) {
//Determines if the target object is of type Object (object/array)
if (target && typeof target === 'object') {
//Processes all the data in an array or object recursively via reactive
//Determine if current target is of type array
if (Array.isArray(target)) {
//Data traversal in array
target.forEach((item, index) => {
target[index] = reactive(item);
})
} else {
//Determine if current target is of type object
//Data traversal in object
Object.keys(target).forEach(key => {
target[key] = reactive(target[key]);
})
}
return new Proxy(target, reactiveHandler);
}
//If the target is of primitive type, return target directly
return target;
}
shallowReadonly and readonly
//shallowReadonly and readonly
//Define readonlyHandler
const readonlyHandler = {
get(target, property) {
if (property === '_is_readonly') return true;
const result = Reflect.get(target, property);
console.log('Intercept get operation', property, result);
return result;
},
set(target, property, val) {
console.warn('Read only, cannot amend data or add data');
return true;
},
deleteProperty(target, property) {
console.warn('Read only, cannot delete data');
return true;
},
}
//Define shallowReadonly function
function shallowReadonly(target) {
//Determine if current target is of type object
if (target && typeof (target) === 'object') {
return new Proxy(target, readonlyHandler);
}
return target;
}
//Define readonly function
function readonly(target) {
//Determine if current target is of type object
if (target && typeof (target) === 'object') {
//Determine if current target is of type array
if (Array.isArray(target)) {
target.forEach((item, index) => {
target[index] = readonly(item);
})
} else {
//Current target is of type object
Object.keys(target).forEach(key => {
target[key] = readonly(target[key]);
})
}
return new Proxy(target, readonlyHandler);
}
//If target is of type primary data, return target directly
return target;
}
shallowRef and ref
//shallowRef and ref
//Define shallowRef function
function shallowRef(target) {
//Save target
return {
_value: target,
get value() {
console.log('Intercept read operation')
return this._value;
},
set value(value) {
console.log('Intercept set operation and ready to update', value)
this._value = value;
}
}
}
//Define ref function
function ref(target) {
target = reactive(target);
return {
//Curent object is of type ref
_is_ref: true,
_value: target,
get value() {
console.log('Intercept read operation')
return this._value;
},
set value(value) {
console.log('Intercept set operation and ready to update', value)
this._value = value;
}
}
}
isRef, isReactive, isReadonly and isProxy
//isRef, isReactive, isReadonly and isProxy
//Define isRef function to determine if current object is of type ref
function isRef(object) {
return object && object._is_ref
}
//Define isReactive function to determine if current object is of type reactive
function isReactive(object) {
return object && object._is_reactive
}
//Define isReadonly function to determine if current object is of type readonly
function isReadonly(object) {
return object && object._is_readonly
}
//Define isProxy function to determine if current object is of type reactive or readonly
function isProxy(object) {
return isReactive(object) || isReadonly(object)
}
Test Results
shallowReactive
const proxy1 = shallowReactive({
name: "James",
car: {
color: "red ",
},
});
//Get data and set data successfully
proxy1.name += "=====";
//Get data successfully, but fail to set data
proxy1.car.color += "++++++";
//Delete data successfully
delete proxy1.name;
//Get data successfully, but fail to delete it
delete proxy1.car.color;
reactive
const proxy2 = reactive({
name: "James",
car: {
color: "red ",
},
});
//The reactive conversion is "deep": it affects all nested properties.
proxy2.name += "=====";
proxy2.car.color += "++++++";
delete proxy2.name;
delete proxy2.car.color;
shallowReadonly
const proxy3 = shallowReadonly({
name: "James",
car: {
color: ["red", "blue"],
},
});
// Read operation is valid
console.log(proxy3.name);
// Cannot change the value of name property
proxy3.name = "==";
// Delete is not valid
delete proxy3.name;
// Deep amend or delete is valid
proxy3.car.color[0] = "yellow";
delete proxy3.car.color[1];
readonly
const proxy4 = readonly({
name: "James",
car: {
color: ["red", "blue"],
},
});
//Read operation is valid
console.log(proxy4.name);
console.log(proxy4.car.color[0]);
// Change the value of property is not valid
proxy4.name = "Jack";
proxy4.car.color[0] = "grey";
shallowRef
const ref1 = shallowRef({
name: "James",
car: {
color: ["red", "blue"],
},
});
//Read operation is valid
console.log(ref1.value);
//Shallow amend is valid, while deep amend is not valid
ref1.value = "==";
ref1.value.car = "==";
ref
const ref2 = ref({
name: "James",
car: {
color: ["red", "blue"],
},
});
//Read operation is valid
console.log(ref2.value);
//Both shallow and deep amend are hijacked
ref2.value = "==";
//ref2.value.car.color[0] = "==";
ref2.value.car.color[0] = "==";
isRef, isReactive, isReadonly and isProxy
console.log(isRef(ref({})));
console.log(isReactive(reactive({})));
console.log(isReadonly(readonly({})));
console.log(isProxy(reactive({})));
console.log(isProxy(readonly({})));
Conclusion
About ref
- Takes an internal value and returns a reactive, mutable ref object with a single property, .value, which points to its internal value.
- ref objects are mutable, which means you can assign new values to.value. It is also reactive in the sense that all operations on.value are tracked and writing operations trigger side effects associated with them.
- If an object is assigned to ref, the object is turned into a deeply reactive object via reactive(). This also means that if the object contains nested refs, they will be deeply unpacked.
About reactive
- A reactive transformation is “deep” : it affects all nested properties. A reactive object will also deeply unpack any ref attributes while remaining reactive.
- It’s worth noting that ref unpacking is not performed when a ref element in a reactive array or a native collection type like Map is accessed.
- The returned object, and any objects nested within it, are wrapped in ES Proxy and are therefore not equal to the source object, so it is recommended to use only reactive proxies instead of raw objects.