In a nutshell, vue3 response is: 1. Effect collects effect for all attributes. 2. When the value of this property changes, effect is re-executed. Here is a simple vuE3 response by hand, in-depth understanding of it.
1. ReactiveApi implementation
Package/reactivity/SRC/index export response of various methods
export {
reactive,
shallowReactive,
shallowReadonly,
readonly
} from './reactive'
export {
effect
} from './effect'
export {
ref,
shallowRef,
toRef,
toRefs
} from './ref'
Copy the code
Package/reactivity/SRC/index/reactivity function currie
Vue3 uses proxy to realize data proxy. The core is to intercept get method and set method, collect effect function when obtaining value, and trigger corresponding effect re-execution when modifying value
import { isObject } from "@vue/shared"
import {
mutableHandlers,
shallowReactiveHandlers,
readonlyHandlers,
shallowReadonlyHandlers
} from './baseHandlers'
export function reactive(target){
return createReactiveObject(target,false,mutableHandlers)
}
export function shallowReactive(target){
return createReactiveObject(target,false,shallowReactiveHandlers)
}
export function readonly(target){
return createReactiveObject(target,true,readonlyHandlers)
}
export function shallowReadonly(target){
return createReactiveObject(target,true,shallowReadonlyHandlers)
}
const reactiveMap = new WeakMap(a);const readonlyMap = new WeakMap(a);// Core method createReactiveObject
// Reactive the API intercepts only object types
WeakMap will cache the proxy object and the corresponding proxy result, if it has been proxy, directly return it.
/ / reactiveMap readonlyMap corresponding response/read-only mapping table
export function createReactiveObject(target,isReadonly,baseHandlers){
if( !isObject(target)){
return target;
}
const proxyMap = isReadonly? readonlyMap:reactiveMap
const existProxy = proxyMap.get(target);
if(existProxy){
return existProxy;
}
const proxy = new Proxy(target,baseHandlers);
proxyMap.set(target,proxy);
return proxy;
}
Copy the code
package/reactivity/src/index/baseHandlers
Reactive, shallowReactive, readonly, shallowReadonly handler Set and get – > createGetter/createSetter get collected rely on set trigger updates, distinguish is new, modified.
// Implement new Proxy(target, handler)
import { extend, hasChanged, hasOwn, isArray, isIntegerKey, isObject } from "@vue/shared";
import { track, trigger } from "./effect";
import { TrackOpTypes, TriggerOrTypes } from "./operators";
import { reactive, readonly } from "./reactive";
const get = createGetter();
const shallowGet = createGetter(false.true);
const readonlyGet = createGetter(true);
const shallowReadonlyGet = createGetter(true.true);
const set = createSetter();
const shallowSet = createSetter(true);
export const mutableHandlers = {
get,
set
}
export const shallowReactiveHandlers = {
get: shallowGet,
set: shallowSet
}
let readonlyObj = {
set: (target, key) = > {
console.warn(`set on key ${key} falied`)}}export const readonlyHandlers = extend({
get: readonlyGet,
}, readonlyObj)
export const shallowReadonlyHandlers = extend({
get: shallowReadonlyGet,
}, readonlyObj)
function createGetter(isReadonly = false, shallow = false) { // Intercept the fetch function
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver); // Equivalent to target[key];
if(! isReadonly){// Collect dependencies and update corresponding views after data changes
track(target,TrackOpTypes.GET,key)
}
if(shallow){
return res;
}
if(isObject(res)){
// vue2 is recursive at first, vue3 is proxying when it is evaluated. Vue3's proxy mode is lazy proxy
return isReadonly ? readonly(res) : reactive(res)
}
returnres; }}function createSetter(shallow = false) { // Block setup
return function set(target, key, value, receiver) {
const oldValue = target[key]; // Get the old value
// Whether there is an attribute key in target
let hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target,key);
// The key is the index, and the index is within the length of the array
// Check if there is a key in the current target
const result = Reflect.set(target, key, value, receiver); // Target [key] = value
if(! hadKey){/ / new
trigger(target,TriggerOrTypes.ADD,key,value);
}else if(hasChanged(oldValue,value)){
// Modify and old value! = = new values
trigger(target,TriggerOrTypes.SET,key,value,oldValue)
}
// Notify effect of the corresponding property to re-execute when data is updated
returnresult; }}Copy the code
2. The effect
package/reactivity/src/index/effect
By default, the global variable activeEffect is executed once to save the current effect. The effectStack is used to record the current activeEffect
export function effect(fn, options: any = {}) {
const effect = createReactiveEffect(fn, options);
if(! options.lazy) { effect(); }return effect;
}
let uid = 0;
let activeEffect; // Store the current effect, which is currently running
const effectStack = []
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
if(! effectStack.includes(effect)) {// Ensure that effect is not added to the effectStack
try {
effectStack.push(effect);
activeEffect = effect;
return fn(); // The get method is used when the function is executed
} finally {
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
}
}
effect.id = uid++; // Make an effect flag to distinguish effect
effect._isEffect = true; // This is a reactive effect
effect.raw = fn; // leave the original function corresponding to effect
effect.options = options; // Save user properties on effect
return effect;
}
Copy the code
3. Track relies on collection
package/reactivity/src/index/effect
The difference is that watcher. Update of vue2 executes effect for any property change and does not re-execute effect for any unused data change
WeakMap Key: {age:12} value:(map) =>{age => set(effect)}
const targetMap = new WeakMap(a);// Collect the current effect function for an attribute in an object
export function track(target, type, key) {
if (activeEffect === undefined) {
This property does not collect dependencies because it is not used in effect
return;
}
let depsMap = targetMap.get(target);
if(! depsMap) {// If there is no targetmap.get (target)
targetMap.set(target, (depsMap = new Map));
}
let dep = depsMap.get(key);
if(! dep) {//如果没有depsMap.get(key)
depsMap.set(key, (dep = new Set))}if(! dep.has(activeEffect)) {// If there is no dep.has(activeEffect)dep.add(activeEffect); }}Copy the code
4. Trigger the update
The set method is triggered, and the trigger method is triggered to update the effect corresponding to the key
- Let’s see if it’s modified
Array length
Because changing the length has a bigger impact
If the length of the change is smaller than the collected index, then this index also needs to trigger effect re-execution
-
It could be objects
-
What if I change an index in the array?
If an index is added, a length update is triggered
package/reactivity/src/index/effect
import { isArray, isIntegerKey } from "@vue/shared";
import { TriggerOrTypes } from "./operators";
// Find the effect of the property to execute (array, object)
export function trigger(target, type, key? , newValue? , oldValue?) {
// If this attribute does not collect effects, no action is required
const depsMap = targetMap.get(target);
if(! depsMap)return;
const effects = new Set(a);// The effect is changed
const add = (effectsToAdd) = > {
if (effectsToAdd) {
effectsToAdd.forEach(effect= >effects.add(effect)); }}// I want to store all the effects to be executed in a new collection, and finally execute them together
// 1. Check whether the array length is changed
if (key === 'length' && isArray(target)) {
// If the corresponding length has a dependency collection needs to be updated
depsMap.forEach((dep, key) = > {
if (key === 'length' || key > newValue) { // If the length of the change is smaller than the collected index, then this index also needs to trigger effect re-execution
add(dep)
}
})
} else {
// Can be an object
if(key ! = =undefined) { // It must be modified, not added
add(depsMap.get(key)); // If it is a new one
}
// What if I change an index in the array?
switch (type) { // If an index is added, the length is updated
case TriggerOrTypes.ADD:
if (isArray(target) && isIntegerKey(key)) {
add(depsMap.get('length'));
}
}
}
effects.forEach((effect: any) = > effect())
}
Copy the code
5. Ref
A REF is essentially implemented through a class’s attribute accessor that wraps a common value type. Ref converts an ordinary type to an object with a value attribute that points to the original value.
import { hasChanged, isObject } from "@vue/shared";
import { track, trigger } from "./effect";
import { TrackOpTypes, TriggerOpTypes } from "./operations";
import { reactive } from "./reactive";
export function ref(value) { // ref Api
return createRef(value);
}
export function shallowRef(value) { // shallowRef Api
return createRef(value, true);
}
function createRef(rawValue, shallow = false) {
return new RefImpl(rawValue, shallow)
}
const convert = (val) = > isObject(val) ? reactive(val) : val; // Recursive response
class RefImpl {
public _value; // a _value attribute is declared but no value is assigned
public __v_isRef = true; // The generated instance is appended with a __v_isRef attribute to indicate that it is a ref attribute
constructor(public rawValue, public shallow) { // Add a modifier to indicate that the property is placed on the instance
this._value = shallow ? rawValue : convert(rawValue)// If it is deep, make everything inside responsive
}
// Property accessors for the class
get value() { // Proxy value will help us proxy to _value
track(this, TrackOpTypes.GET, 'value');
return this._value
}
set value(newValue) {
if (hasChanged(newValue, this.rawValue)) { // Check whether the old and new values change
this.rawValue = newValue; // The new value will be used as the old value
this._value = this.shallow ? newValue : convert(newValue);
trigger(this, TriggerOrTypes.SET, 'value', newValue); }}}Copy the code
6. ToRefs implementation
To convert an attribute in an object to a ref attribute, toRefs is based on ref, traversing the object plus ref
class ObjectRefImpl {
public __v_isRef = true;
constructor(public target, public key) {}
get value() {/ / agent
return this.target[this.key] If the original object is reactive, the collection will be relied on
}
set value(newValue) {this.target[this.key] = newValue; // Update is triggered if the original object is reactive}}// Convert the value of a key to ref
export function toRef(target, key) { // We can convert the value of an object to type ref
return new ObjectRefImpl(target, key)
}
export function toRefs(object){ // Object may pass an array or an object
const ret = isArray(object) ? new Array(object.length) :{}
for(let key in object){
ret[key] = toRef(object,key);
}
return ret;
}
Copy the code
7. Differences between VUe2 and VUe3
Vue2 recurses the data in data from the beginning, while VUe3 proxies the data when it is evaluated. Vue3’s proxy mode is lazy proxy. Having an attribute in an object collect its current effect function is equivalent to the Watcher in VUe2. Ref uses defineProperty internally and Reactive uses proxy internally.
For arrays, VUe2 uses the rewrite array method. Vue3 is the trigger method when it triggers an update. If an index is added, the length of the index will be updated. If the length of the array changed is smaller than that of the collected index, the index will also need to trigger effect re-execution.