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

  1. 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?

  1. First, you collect dependencies, and the time to collect them isobj.countWhen we do get on the property value, we collect the method’s dependencies on the propertytrack()
  2. When will the update be triggered? That of course is when the property value changes which is when the set is triggeredtrigger()

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

  1. 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
  1. 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
  1. Recursion objectreactive({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
  1. 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
  1. 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
  1. effectNested 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 implementationrefmethods

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 implementationcomputedAPI 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 definitioncomputedMethod and export here we’re using one_dirtyTo 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!