Learn the learning record of VUE3 Mini-Vue implementation in STATION B. Video address written from scratch mini-vue
1. API implementation of Vue3 Reactive
Vue3 Reactive: An object is defined as a reactive object and can be dependent collected and processed accordingly.
import {reactive } from 'Vue'
let obj = (window.foo = reactive({ count: 0 }));
effect(() = > {
console.log('obj.count:', obj.count);
});
Copy the code
The results
The effect() method initializes a collection dependency that’s the difference with Watch and then implements reactive and effect.
1. Reactive and Effect
- Create a react.js file
A reactive() method accepts a target as a parameter and determines whether it is an object or a basic type
//reactive.js
export function reactive(target) {
// Determine whether it is an object or a primitive type
if(! isObject(target)) {return target;
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver); . Dependent collection operationreturn res;
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver); . Dependency update notifications correspond to recipientsreturnres; }}); }Copy the code
Vue2 is based on Object.defineProperty() for data reactive binding. Vue3 is based on proxy for data reactive processing. The isObject method of the above code is a utility method that determines whether a value is an object.
//utils.js
export function isObject(target) {
return typeof target === 'object'&& target ! = =null;
}
Copy the code
Question? So how do we implement the reactive form?
- First, you collect dependencies, and the time to collect them is
obj.count
When we do get on the property value, we collect the method’s dependencies on the propertytrack()
- When will the update be triggered? That of course is when the property value changes which is when the set is triggered
trigger()
Now let’s think about how effect is implemented. Effect first executes the corresponding callback once, binding the callback to the property object on which it depends, and then starts the callback again when the data of the dependent object is updated. Therefore, effect can be preliminarily defined as follows:
//effect.js
// The current callback method
let activeEffect;
// Pass in the callback method
export function effect(fn) {
// Another layer is wrapped here to cooperate with track collection dependency
const effectFn = () = > {
try {
activeEffect = effectFn;
return fn();
} finally {
//todo
activeEffect = undefined; }};// First execution
effectFn();
// Return contains the callback
return effectFn;
}
Copy the code
Implement trigger and track methods to define a global Map collection dependency structure roughly as follows
{[object]: {[dependent attribute value name]: {}//set holds callback methods that depend on the attribute value name}}Copy the code
const targetMap = new WeakMap(a);// Collect dependencies
export function track(target, key) {
// Determine if there are dependency callback methods that do not return directly
if(! activeEffect) {return;
}
// If map does not have the object, create it
let depsMap = targetMap.get(target) || new Map(a); targetMap.set(target, depsMap);// If the attribute value corresponds to the callback method Set, the Set is created
let deps = depsMap.get(key) || new Set(a); depsMap.set(key, deps);// Put callback methods that depend on this property into the set
deps.add(activeEffect);
}
// Trigger the callback
export function trigger(target, key) {
const depsMap = targetMap.get(target);
// Determine whether to listen on the object
if(! depsMap)return;
const deps = depsMap.get(key);
// Whether to listen for this property
if(! deps)return;
// Notify the callback method
deps.forEach((effectFn) = > {
effectFn();
});
}
Copy the code
The track() and trigger() methods above are used to deal with the way dependencies pass update notifications when collecting and processing data updates
The above method simply implements effect and Reactive; But for the following several special cases also need special treatment
reactive(reactive(obj))
This first checks if the object is proxied and returns if it is proxied
export function reactive(target) {
Reactive (reactive(obj))
if (isReactive(target)) {
return target;
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
// Return true when a __isReactive GET agent is received to indicate that the object has been proxied
if (key === '__isReactive') return true; }})}Reactive (reactive(obj))
export function isReactive(target) {
return!!!!! (target && target.__isReactive); }Copy the code
let a = reactive(obj) let b = reactive(obj)
// Used to store mappings between objects and proxy objects
Let a = reactive(obj) let b = reactive(obj
const proxyMap = new WeakMap(a);export function reactive(target) {
Let a = reactive(obj) let b = reactive(obj)
if (proxyMap.has(target)) {
returnproxyMap.get(target); }...constproxy = ()... .// Put it in the map
proxyMap.set(target, proxy);
}
Copy the code
- Recursion object
reactive({count1: 0, count: {count2: 2}})
export function reactive(target) {...const proxy = new Proxy(target, {
get(target, key, recevier){...//return res; Solving recursion depends on VUe3, which is a lazy treatment different from VUe2
returnisObject(res) ? reactive(res) : res; }})}Copy the code
- Object attribute assignment does not trigger a callback operation
export function reactive(target) {...const proxy = new Proxy(target, {
set(target, key, value, receiver) {
constoldValue = target[key]; .// Determine whether the new and old values have changed
if(hasChanged(oldValue, value)) { trigger(target, key); }}})}//utils.js
export function hasChanged(oldValue, value) {
returnoldValue ! == value && ! (Number.isNaN(oldValue) && Number.isNaN(value));
}
Copy the code
- Array object Length
Listen for the length object of the array
export function reactive(target) {...const proxy = new Proxy(target, {
set(target, key, value, receiver) {
letoldLength = target.length; .// Determine whether the new and old values have changed
if (hasChanged(oldValue, value)) {
trigger(target, key);
// Check whether the length of the array object changes
if (isArray(target) && hasChanged(oldLength, target.length)) {
trigger(target, 'length'); }}}})}//utils.js
export function isArray(target) {
return Array.isArray(target);
}
Copy the code
effect
Nested operating
We need to modify the Effect method to support nested operations by using a stack to hold triggered callback methods and prevent callback methods from binding error listeners
let activeEffect;
// Handle nested effects using a stack to store Activeeffects
const effectStack = [];
//effect an initial trigger method is used to bind and collect dependencies
export function effect(fn, options = {}) {
const effectFn = () = > {
try {
activeEffect = effectFn;
// Push operation
effectStack.push(activeEffect);
return fn();
} finally {
// After todo is executed, the stack is unloaded
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1]; }}; effectFn();return effectFn;
}
Copy the code
You can verify the following examples
const observed = (window.observed = reactive([1.2.3]));
effect(() = > {
console.log('index 3 is:', observed[3]);
});
effect(() = > {
console.log('length is:', observed.length);
});
// Push a few values in the console
Copy the code
const observed = (window.observed = reactive({count1: 1.count2: 2}));
effect(() = > {
effect(() = > {
console.log('observed.count2 is:', observed.count2);
});
console.log('observed.count1 is:', observed.count1);
});
//zai console change the values of count1 and count2 to see
Copy the code
2. Next implementationref
methods
The principle is pretty much the same as before. Ref is basically a wrapper listening for primitive types. We define a ref.js that defines a ref export method
export function ref(value) {
// Whether it is already represented
if (isRef(value)) {
return value;
}
// Returns a responsive wrapper object
return new RefImpl(value);
}
// Determine if the proxy has been changed
export function isRef(value) {
return!!!!! (value && value.__isRef); }Copy the code
RefImpl is a wrapper class in which we implement value-dependent collection and listening
// Encapsulate objects responsively
class RefImpl {
constructor(value) {
this.__isRef = true;
this._value = convert(value);
}
// Set and get proxy value properties because.value gets the value
get value() {
// Rely on collection
track(this.'value');
return this._value;
}
set value(newValue) {
// Also check whether the value has changed from before
if (hasChanged(newValue, this._value)) {
// Use convert to prevent an object from being passed in
// The update notification is triggered after the assignment
this._value = convert(newValue);
trigger(this.'value'); }}}// Check if it is an object or not, and switch to Reactive to process the proxy object
function convert(value) {
return isObject(value) ? reactive(value) : value;
}
Copy the code
let foo = (window.foo = ref(10));
effect(() = > {
console.log('foo is:', foo.value);
})
// Try changing the value of foo.value on the console
Copy the code
3. Next implementationcomputed
API methods
The general usage is the following
let foo = (window.foo = ref(10));
let c = window.c = computed(() = > {
console.log('foo!! ')
return foo.value * 2;
})
Copy the code
It only fires to do the calculation when it gets the value and it only fires once without changing the value of the dependency meaning there is a cache and it recalculates when the dependency changes
First we create a computed. Js definitioncomputed
Method and export here we’re using one_dirty
To determine whether a dependency change needs to be recalculated
Because computed does not trigger a callback for the first time, you need to modify the effect method and add an option to the passed parameter to suit your needs
Because computed requires that the _dirty value be updated after a dependency is updated rather than calling a callback immediately, you need to define your own callback operations. Here, pass a Scheduler in options to define the operations after the dependency is updated
export function computed(getter) {
// Also define a proxy wrapper class
return new ComputedImpl(getter);
}
class ComputedImpl {
constructor(getter) {
/ / stored value
this._value = undefined;
// Determine whether the dependency is updated and whether the value needs to be recalculated
this._dirty = true;
// Calculate method
this.effect = effect(getter, {
lazy: true.// Lazy calculation whether the first load
scheduler: () = > {
if (!this._dirty) {
this._dirty = true;
// The wrapped object itself is reactive
trigger(this.'value'); }}}); }get value() {
// Determine whether dependencies are updated
if (this._dirty) {
// recalculate the value
this._value = this.effect();
// Update complete
this._dirty = false;
// The wrapped object itself is reactive
track(this.'value');
}
return this._value; }}Copy the code
//effect.js
//effect an initial trigger method is used to bind and collect dependencies
export function effect(fn, options = {}) {
const effectFn = () = > {
try {
activeEffect = effectFn;
effectStack.push(activeEffect);
return fn();
} finally {
// After todo is executed
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1]; }};// Determine if the callback method needs to be run for the first time
if(! options.lazy) { effectFn(); }// Bind the scheduling method
effectFn.scheduler = options.scheduler;
return effectFn;
}
// Trigger the callback
export function trigger(target, key) {
const depsMap = targetMap.get(target);
if(! depsMap)return;
const deps = depsMap.get(key);
if(! deps)return;
deps.forEach((effectFn) = > {
// Check if there is a scheduling method
if (effectFn.scheduler) {
effectFn.scheduler(effectFn);
} else{ effectFn(); }}); }Copy the code
Computed is basically done but you need to consider setting up special cases to support the following types:
let foo = (window.foo = ref(10));
let c = (window.c = computed({
get() {
console.log('get');
return foo.value * 2;
},
set(value){ foo.value = value; }}));Copy the code
Revamp computed. Js
What computed needs to receive is an object with a set and a GET
export function computed(getterOrOption) {
let getter, setter;
// Check whether it is a function
if (isFunction(getterOrOption)) {
getter = getterOrOption;
setter = () = > [console.log('computed is readonly')];
} else {
/ / assignment
getter = getterOrOption.get;
setter = getterOrOption.set;
}
return new ComputedImpl(getter, setter);
}
class ComputedImpl {
constructor(getter, setter) {
this._setter = setter; . }...set value(value) {
// Execute setter methods
this._setter(value); }}//utils.js
export function isFunction(target) {
return typeof target === 'function';
}
Copy the code
OK finished!