Vue3 reactivity The source code is simple

Hello, I’m Jian Darui. Some time ago, the company did a brand new project with Vue3. It was a great honor to participate in the development of most of the functions and put Vue3 whole family bucket into practice once. To strike while the iron is hot, read the source code of the reactivity package in Vue3. There’s a lot to learn. Take this opportunity to share it. If there is any deficiency, I hope you can criticize and correct me.

The old and new contrast

This share is mainly Vue3 reactivity source part, so only compare Vue2 and Vue3 responsive source part.

Comparison of old and new principles:Object.definePropertywithProxy

Object. DefineProperty:

Those of you who have known Vue2 source code know it. In Vue2, change detection is implemented internally via Object.defineProperty. This method can directly define a new property on an object or modify an existing property. Takes three arguments, targetObject, key, and one descriptorObject for key, and returns the object passed to the function.

DescriptorObject The optional key values for descriptorObject:

  • configurable: Sets the configurability of the current property. Default is false.
  • enumerable: Sets the enumerability of the current property. Default is false.
  • value: Sets the value of the current property, undefined by default.
  • writable: Sets whether the current property can be changed. Default is false.
  • Get: The getter function for the current property, which is triggered when the property is accessed.
  • Set: Setter function for the current property, which is fired when the current property value is set.

Here attached is the brief source of defineReactive in Vue2, focusing on: get function and set function.


function defineReactive (obj, key, val, customSetter, shallow) {
  // omit some code...
  
  Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
          
        // Rely on collection
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      
      // Compare old and new values
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      } 
      if(getter && ! setter)return
        
      // Set a new value for object
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
        
      // Detect new valueschildOb = ! shallow && observe(newVal)// Trigger dependency, response
      dep.notify()
    }
  })
}
Copy the code

Problems with Object.defineProperty:

  • Directly modifyingArraythelengthAttributes have compatibility problems.
let demoArray = [1.2.3]
Object.defineProperty(demoArray, 'length', { 
    set: function() { 
        console.log('length changed! ')}})// Uncaught TypeError: Cannot redefine property: length
// at Function.defineProperty (<anonymous>)
// at <anonymous>:2:8
Copy the code
  • Directly totargetObjectAdd attributes or delete attributes,Object.definePropertyUnable to trigger dependencies.
let obj = { name: "jiandarui".age: 18 };
obj = Object.defineProperty(obj, 'age', {
  	configurable: true.get: function() {
        console.log("get value")},set: function(value) { 
        console.log('set value')
    } 
})
obj.gender = "man"; // The Getter is not triggered
delete obj.age // Setters are not fired
Copy the code
  • Only totargetObjectCannot detect the entiretargetObjectDetect.
function defineReactive(data, key, val) {
	Object.defineProperty(data, key, {
        configurable: true.enumerable: true.get: function() {
            // Do dependency collection here
            console.log("Dependent collection")
            returnval; },set: function(newVal) {
        	if(val === newVal) {
                return val;
            }
        	// Do change detection here
        	console.log("Change Detection") val = newVal; }})}Copy the code

To solve these three problems, Vue2 takes different measures:

  • Create an array interceptor for array changes.
  • For adding and deleting object attributes, create$set,$delete API.
  • forvalueFor the case of the object, adoptrecursiveIn the way of deep traversal, dependency collection is carried out.

Proxy:

As browser support for ES6 improves, Proxy is used in Vue3.

The proxy intercepts the entire targetObject directly, takes two parameters, target and handler, and returns a proxy object. Handler can be configured in 13 methods, including attribute lookup, assignment, enumeration, function call, stereotype, and attribute description related methods.

Let’s look at some code examples of proxy and the methods used by the handler in Vue3:

let obj = { name: "Luo Xiang"}
cosnt handler = {
    get: function(target, key, receiver) {
        console.log("get")},set: function(target, key, value, receiver) {
        console.log("set")
        return Reflect.set(target, key, value, receiver)
    }
}
let proxy = new Proxy(obj, handler);
proxy.name 
proxy.name = "Zhang";
Copy the code

Speaking of Proxy, Reflect has its gay friend in mind. The method on Reflect is basically the same as that on Object, but there are subtle differences.

The method on Reflect has the same name as the Proxy method. Target can be mapped.

We can call Reflect directly when we need to call a method on Object. Reflect intercepts a Javascript action directly.

  • get()
    • Used to intercept reading of object properties
    • Take three parameterstarget(Original object),key(Attributes to read),receiver(ProxyObject instance or inheritanceProxyThe object)
    • The return value can be customized
    • inheritable
let p = new Proxy({ name: "Luo Xiang" }, {
  get: function(target, key, receiver) {
    console.log("called: " + key);
    return Reflect.get(target, key, receiver); }});console.log(p.a); // "called: a"
Copy the code
  • set()
    • Used to set the value of an object property
    • Accept four parameterstarget,key,newValue(Newly set value),receiver
    • Returns true, strictly mode returnsfalseWill be submitted to theTypeErrorabnormal
let p = new Proxy({ name: "Luo Xiang".profession: "Drivers" }, {
  set: function(target, key, value, receiver) {
    console.log("called: " + key+ ":" + value);
    return Reflect.set(target, key, value, receiver); }}); p.profession ="Lawyer" // Called: profession
p.age = 18 // called: age: 18
console.log(p.age) / / 18
Copy the code
  • deleteProperty()
    • Used to intercept a property of an objectdeleteThe operation (makes up for itObject.definedPropertyProperties ofdeleteThe problem of operating without feeling).
    • Accepted parameters:targeT,key
    • Returns a Boolean value:trueSuccess,falsefailure
var p = new Proxy({}, {
  deleteProperty: function(target, prop) {
    console.log('called: ' + prop);
    return true; }});delete p.a; // "called: a"
Copy the code
  • has()
    • Used to intercept in operations
    • Accept parameterstarget,key
    • Returns a Boolean value,trueThere is,falseThere is no
    • Intercept only toinOperator in effect, yesfor... inLoop does not take effect
// ECMAScript 6 gets started
let stu1 = {name: 'Joe'.score: 59};
let stu2 = {name: 'bill'.score: 99};

let handler = {
  has(target, prop) {
    if (prop === 'score' && target[prop] < 60) {
      console.log(`${target.name}Fail `);
      return false;
    }
    return prop intarget; }}let oproxy1 = new Proxy(stu1, handler);
let oproxy2 = new Proxy(stu2, handler);

'score' in oproxy1
// Zhang SAN failed
// false

'score' in oproxy2
// true

for (let a in oproxy1) {
  console.log(oproxy1[a]);
}
/ / zhang SAN
/ / 59

for (let b in oproxy2) {
  console.log(oproxy2[b]);
}
Copy the code
  • ownKeys()
    • Used to intercept read operations on a senior property of an object. Operations that can be intercepted
      • Object.getOwnPropertyNames()
      • Object.getOwnPropertySymbols()
      • Object.keys()
      • Reflect.ownKeys()
      • for... incycle
    • parametertarget
    • The return result must be an array
let p = new Proxy({}, {
  ownKeys: function(target) {
    console.log('called');
    return ['a'.'b'.'c']; }});console.log(Object.getOwnPropertyNames(p)); // "called"
Copy the code

Summary (to be perfected)

Proxy is more powerful than Object.defineProperty. From the above example, you can see that Proxy can compensate for object.defineProperty’s dependency on collection and detection of changes, such as:

  • rightObjectProperty to add or delete operations
  • throughArrayIndex to modify or add elements

However, Proxy also has its own defects, here we leave a blank, will be filled in later. Let’s keep talking.

Comparison of old and new patterns: Observer versus agent

Observer model

Vue2 internally deals with the relationship between data and dependencies through the observer mode, which has the following characteristics:

  • One to many. There is a one-to-many dependency between objects, and when an object’s state changes, all dependent objects are notified and automatically updated
  • Reduce the coupling between target data and dependencies
  • It belongs to behavioral design pattern

Simply implement the observer pattern

  • The Subject ` class:
    • The observers’ attribute is used to maintain all observers
    • addThe observer () method is used to add an observer
    • notifyMethod is used to notify all observers
    • removeThe remove observer method is used to remove the observer
  • ObserverClass:
    • updateMethod is used to accept changes in state
/ / Subject object
class Subject {
    constructor() {
        // Store the observer
        this.observers = [];
    }
    add(observer) { 
    	this.observers.push(observer);
  	}
    // State change notifies observer
 	notify(. args) { 
    	var observers = this.observers;
    	for(var i = 0; i < observers.length; i++){ observers[i].update(... args); }}// Remove the observer
  	remove(observer){
    	var observers = this.observers;
    	for(var i = 0; i < observers.length; i++){
      		if(observers[i] === observer){
        		observers.splice(i,1); }}}}/ / the Observer object
class Observer{
    constructor(name) {
        this.name = name;
    }
    update(args) {
    	console.log('my name is '+this.name); }}let sub = new Subject();
let bigRio = new Observer(Kendarui);
let smallRio = new Observer('Swordsman');
sub.add(bigRio);
sub.add(smallRio);
sub.notify(); 
Copy the code

Vue2Observer mode in

  • The data in Vue2 is the object we want to observe, Watcher is the so-called dependency, and Dep is only responsible for the collection and distribution of Watcher.

  • In addition, watcher in Vue2 can also be the target data. It has a many-to-many relationship with Dep, not a one-to-many relationship.

Let’s review how these classes are designed in Vue2 again:

  • The Observer class:
    • Used to createObserverThe instance
    • walkMethod traverses the observed object, and willvalueEach term in the
    • observeArrayThe () method iterates through the array being observed, converting each item in the array to a response
class Observer {
  constructor (value) {
    this.value = value
    this.dep = new Dep()
    def(value, '__ob__'.this)
    if (Array.isArray(value)) {
        
      // Detect arrays
      this.observeArray(value)
    } else {
        
      // Detect objects
      this.walk(value)
    }
  }

  walk (obj) {
    // Iterate over the object for conversion
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  observeArray (items) {
    // Go through the number group to convert
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
Copy the code
  • Method Observe
    • observeMethod is used to createObserverExample of the workshop method
function observe (value, asRootData){
  if(! isOObject(value) || valueinstanceof VNode) {
    return
  }
  let ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if((Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value)
  ) {
    ob = new Observer(value)
  }

  return ob
}
Copy the code
  • DefineReactive method
    • rightvalLet’s do a recursive observation
    • throughObject.definePropertyforobj[key]forGetter,Setterintercept
    • For dependency collection and status distribution
function defineReactive (obj, key, val, customSetter, shallow) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  const getter = property && property.get
  const setter = property && property.set

  // recursively observe val
  letchildOb = ! shallow && observe(val)Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        // Rely on the collection for the current Watcher
        dep.depend()
        if (childOb) {
          // Subobservers collect dependencies
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      // Determine whether the old and new values are equal
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }

      if (setter) {
        // Set the new value
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // Detect new valueschildOb = ! shallow && observe(newVal)// Notification update
      dep.notify()
    }
  })
}
Copy the code
  • Depclass
    • The equivalent ofObserverwithWatcherAn intermediary between
    • Used to maintain relationships between data and dependencies
let uid = 0
class Dep {
  static target;
  id;
  subs;

  constructor () {
    this.id = uid++
    this.subs = []
  }
  // Add dependencies
  addSub (sub) {
    this.subs.push(sub)
  }
  // Remove dependencies
  removeSub (sub) {
    remove(this.subs, sub)
  }
  / / collection
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)}}// Iterate over notification dependencies
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Dep.target = null

Copy the code
  • Watcherclass
    • True dependency, executioncallbackFunction to respond to
    • maintenancedepwithwatcherMany-to-many relationships
    • Returns the new value
const targetStack = []

function pushTarget (target) {
  targetStack.push(target)
  Dep.target = target
}

function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]}class Watcher {
  constructor (vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm
    if (isRenderWatcher) {
        
      / / render watcher
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazy }this.cb = cb
    this.active = true
    this.dirty = this.lazy 

    // Maintain all dependencies associated with the current Watcher
    this.deps = []
 
    this.expression = process.env.NODE_ENV ! = ='production'
      ? expOrFn.toString()
      : ' '
      
    / / get the getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.lazy ? undefined : this.get()
  }

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
        throw e
      }
    } finally {
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
  
  // Add all dePs associated with the current Watcher instance
  // Watcher is many-to-many with dep
  addDep (dep) {
    dep.addSub(this)}// Remove the relationship with the current watcher by traversing the deP associated with the current watcher
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      dep.removeSub(this)}}// Respond
  update () {
    this.run()
  }
  
  // Execute the callback function
  run () {
    if (this.active) {
      const value = this.get()
      if( value ! = =this.value ||
        isObject(value) ||
        this.deep
      ) {
        const oldValue = this.value
        this.value = value
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }

  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
    
  // Go through all the DEPs associated with the current Watcher, that is, every DEP associated with the current Watcher, and add the current Watcher to the DEP
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  // Remove the current watcher from all dePs
  teardown () {
    if (this.active) {
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)}let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)}this.active = false}}}Copy the code

In the above classes, some unnecessary code has been omitted to ease the reading burden. However, it has been possible to show the basic structure & relations of several classes in Vue2’s observer mode.

The proxy pattern

The proxy pattern is a structural pattern in the design pattern. Through the proxy mode, we can create a proxy object based on the original object, which has the same interface as it. In the proxy object interface, we can do some extensibility operations, but not destroy the original object.

Proxy mode can be used when we need to do some control or enforcement over access to the original object.

Characteristics of the proxy mode:

  • Can control external access to the original object, can represent the original object, through the proxy object to control access to the original object.
  • Clear responsibility, high scalability, intelligent
    • Proxy objects are used to control access to the original object
    • Proxy objects can be used to enhance or extend interface functionality
  • It belongs to structural design pattern
  • For example, use virtual proxies to load images, forward/reverse proxies, static/dynamic proxies, and attribute verification.

Vue3 is a proxy-based Proxy mode. By configuring the handler we can control and enhance access to the original object.

The enhancedhanlder

  • getterWhen anTrack
    • determinetargetwitheffectThe relationship between
    • determineactiveEffectwithDepThe relationship between
    • returnvalue
  • setterWhen anTrigger
    • Get the correspondingeffects, traversal executioneffect
    • updateactiveEffect
    • updatevalue

Through analysis, it is not difficult to write code with the following logic:

  • Based on thetargetDetermine whether a proxy transformation is required
  • throughnew ProxyrighttargetFor the agent
  • Return the proxy instance
/ / configure handler
const handlers = { 
	get(target, key, receiver) {
		const res = Reflect.get(target, key, receiver)
		// get track
        track(target, key);
		return res;
	},
    set(target, key, value, receiver) {
        console.log("Set function")
        trigger(target, key, value, )
        Reflect.set(target, key, value, receiver); }}/ / track function
function track(target, key) {
   // Responsible for dependency collection
    console.log("track")}/ / the trigger function
function trigger(target, key, value) {
    // Responsible for the response
    console.log("trigger")}// The response conversion function
function createReactiveObject(target, handlers, proxyMap) {
    
   // 1. Proxy object types only
  if(! isObject(target)) {if (__DEV__) {
      console.warn(`value cannot be made reactive: The ${String(target)}`)}return target
  }
 
  // 2. Determine whether target has passed the proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 3. Perform proxy conversion
  const proxy = new Proxy(target,  handlers)
  
  // 4. Create a mapping between the target and the proxy instance for next judgment
  proxyMap.set(target, proxy)
    
  // 5. Return the proxy instance
  return proxy
}

Copy the code

As you can see from the above figure, Vue3’s dependency collection and response distribution are all done in handler, but there are a few issues that need to be addressed:

  • handlerHow is it configured for other operation types? Such asDelete, forEach.
  • For different data types,handlerIs it configured in the same way? What should I pay attention to?

For the above two questions, let’s leave them for now. That’s what we’re going to talk about. Let’s move on.

Change detection

Track: Relies on collection

New rely on

In Vue2, the dependency is Watcher. In the source code for Vue3, I did not find a Watcher class, but a new function, effect, which I can call the side effect function. By comparing watcher with Effect and effect with data. We can definitely call effect equivalent to watcher in Vue2, but more concise than the Watcher class.

Here’s a brief implementation of the Effect code and an analysis of how it works:

  • effectTo accept afnPass as a callback functioncreateReactiveEffectFunction cache
  • throughoptionsrighteffectconfigure
  • performeffectIt’s essentially creating a cachefntheeffectfunction
export function effect(fn, options) {
  const effect = createReactiveEffect(fn, options)
  if(! options.lazy) { effect() }return effect
}

function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
     // Omit some code
    returnfn() } effect.id = uid++ effect.allowRecurse = !! options.allowRecurse effect._isEffect =true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}
Copy the code

Where is the collection?

In Vue2, a Dep class was created to maintain the relationship between the data and watcher. In Vue3, Dep becomes a simple Set instance. At Track time, the current activeEffect is stored in the DEP. In Trigger, the corresponding DEP set can be obtained by key and then traversed.

Here is the shorthand code for track:

export function track(target, type, key) {
  if(! shouldTrack || activeEffect ===undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// 1. Try to obtain deP
  let dep = depsMap.get(key)
  if(! dep) {// 2. If not, create it
    depsMap.set(key, (dep = new Set()))}if(! dep.has(activeEffect)) {// 3. Add the current activeEffect to deP
    dep.add(activeEffect)
      
    Activeeffect. deps is an array that maintains the relationship between the current activeEffect and the DEP
    // Effect is also a many-to-many relationship with DEP, i.e.
    // a. An effect may exist in multiple DEPs
    // b. dep exists in effect.deps
    activeEffect.deps.push(dep)
  }
}
Copy the code

The relationship between data and dependencies

In Vue2, Observe, Dep and Watcher are used to maintain the relationship between value and Watcher.

But Vue3 doesn’t have the above classes. How does it maintain the relationship between value and effect?

Let’s look at some code:

import { reactive } from "vue"
let count = reactive(0)
let obj = reactive({
  name: "Sword Darui".age: 18.beGoogAt: "createBug".otherInfo: {
  	temp1: ["Basketball"."Football"."Table tennis"]
    temp2: {
      brother: ["Zhang"."Bill"].sister: ["Li hua"."Li li"].}}})/ / change the obj
obj.age = 27
obj.otherInfo.temp1.push("Badminton")
Copy the code

When the properties of obJ change, we need to perform all effects associated with it, triggering the response. In Vue, the relationship between state and dependency can be specific to the most basic key:value, whose structure is similar to that of state and Watcher in Vue2, except that the way of storing state and dependency is changed:

  • targetMapUse:WeakMapInstance for maintenanceWith KeyToDepMap targetObjectThe relationship between
  • KeyToDepMapUse:MapExample to maintain key andDepThe relationship between
  • DepUse:SetInstance to store all andkeyThe relevanteffect
  • effect.depsUse:ArrayInstance for storing all and currenteffectthedepThe instance

Trigger: sends a response

When we modify the response-transformed data, Setter functions are triggered, and dependent distribution work, such as DOM updates and watch/computed execution, needs to be done.

Trigger rely on

<template>
	<div>
    	{{proxy.name}}
	</div>
</template>
Copy the code

In the template, name is generated by Proxy. When proxy.name is assigned a new value, the Setter will be triggered, and DOM needs to be dynamically updated. Therefore, some dependent triggering operations can be performed in the Setter. We can do this by creating a trigger function and calling it in the setter function.

Through analysis, the main functions of Tigger function are as follows:

  • According to theTarget, the keyGets all that is to be executedeffect
  • According to thetypeOperation, make some case judgment, add need to traverse the executioneffect
  • Traverse the executioneffets, triggering the response
function trigger(target , type , key , newValue , oldValue , oldTarget) {
  // 1. Get the corresponding KeyTopDepMaps according to target
  const depsMap = targetMap.get(target)
  if(! depsMap) {// never been tracked
    return
  }

  const effects = new Set(a)// 2. Add effect to effects
  const add = (effectsToAdd) = > {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect= > {
        if(effect ! == activeEffect) { effects.add(effect) } }) } }// schedule runs for SET | ADD | DELETE
  if(key ! = =void 0) {
      
     // 3. Pass the key-related DEP to add. The DEP stores all key-related effects
     add(depsMap.get(key))
  }

  // 5. Execute effect, which executes the callBack function that was passed when effect was created
  const run = (effect) = > {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
  // 4. Execute effect
  effects.forEach(run)
}

function createReactiveObject(target, handlers) {
	let proxy = new Proxy(target, handlers)
	return proxy
}
/ / configure handler
const handlers = { 
	get(target, key, receiver) {
		const res = Reflect.get(target, key, receiver)
		track(target, key);
		return res;
	},
    set(target, key, newValue, receiver) {
        const res = Reflect.set(target, key, newValue, receiver);
        trigger(target, key, newValue)
        return res
	}
}

let target = { name: "Sword Darui" }

let proxyTarget = createReactiveObject(target, handlers)

const effect = patchDOM() {
    // Update the DOM
}
proxyTarget.name  // "track"
proxyTarget.name = "Jiandarui" // "trigger"
Copy the code

From the above code example, we can see that inside Vue3, track is performed in Getter functions and trigger is performed in Setter functions. Above we did not examine the internal implementation of these two key functions. In the next section we will examine how the current response handles data and dependencies. What are the details of the internal implementation of track and trigger?

Perfect handler & Track & Trigger

Through the analysis of Proxy, handler, track, trigger, effect, dependency and the relationship between data. Then we can do a simple combination and write a simplified version of reactive code

// 1. Target and dependency mapping
const targetMap = new WeakMap(a);// create effect function
export function effect(fn, options) {
  const effect = createReactiveEffect(fn, options)
  if(! options.lazy) { effect() }return effect
}

function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
     // Omit some code
    returnfn() } effect.id = uid++ effect.allowRecurse = !! options.allowRecurse effect._isEffect =true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

/ / 3. Track function
function track(target, type, key) {
  if(! shouldTrack || activeEffect ===undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}let dep = depsMap.get(key)
  if(! dep) { depsMap.set(key, (dep =new Set()))}if(! dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep) } }/ / 4. The trigger function
function trigger(target , type , key , newValue , oldValue , oldTarget) {
  const depsMap = targetMap.get(target)
  if(! depsMap) {// never been tracked
    return
  }

  const effects = new Set(a)const add = (effectsToAdd) = > {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect= > {
        if(effect ! == activeEffect) { effects.add(effect) } }) } }// schedule runs for SET | ADD | DELETE
  if(key ! = =void 0) {
      
     add(depsMap.get(key))
  }

  const run = (effect) = > {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
  effects.forEach(run)
}

// 5. Perform proxy conversion
function createReactiveObject(target, handlers, proxyMap) {
  if(! isObject(target)) {if (__DEV__) {
      console.warn(`value cannot be made reactive: The ${String(target)}`)}return target
  }
 
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  const proxy = new Proxy(target,  handlers)
  
  proxyMap.set(target, proxy)
    
  return proxy
}
// 6. Configure handler
const handlers = { 
	get(target, key, receiver) {
		const res = Reflect.get(target, key, receiver)
		track(target, key);
		return res;
	},
    set(target, key, newValue, receiver) {
        const res = Reflect.set(target, key, newValue, receiver);
        trigger(target, key, newValue)
        return res
	}
}

let target = {
    name: "jiandarui"
}
const proxyMap = new Map(a)let proxy = createReactiveObject(target, handlers, proxyMap);

/ / pseudo code
const effectFn = effect(() = > {
    // Responsible for rendering components}, {lazy: false})
Copy the code
  • When we first render, we do a read operation and gogetterDelta function, then it will passtrackComplete the collection of dependencies
  • Triggered when data changessetterDelta function, which will passtriggerFunction to respond

Object&ArrayChange detection

ObjectDepth proxy of

In Vue2, the defineReactive function recursively transforms data. Is this a problem in Vue3? Let’s start with a snippet of code:

let obj = {
    name: "Sword Darui".hobby: {
       one: "Basketball".two: "Swimming"}}let handler = {
    get(target, key, receiver) {
        console.log(` get:${key}`)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        console.log(` set:${key}`)
        Reflect.set(target, key, value, receiver)
    }
}
let proxyObj = new Proxy(obj, handler)
proxyObj.name
// get: name
/ / big sword
proxyObj.name = "jiandarui"
// set: name
// jiandarui
proxyObj.hobby.one
// get: hobby
/ / basketball
proxyObj.hobby.one = "basketball"
// get: hobby
// basketball
Copy the code

One = “basketball”, but the handler only intercepts the getter for the hobby property.

If the value corresponding to key in OBj is of Object type, Proxy can only perform single-layer interception. This is not what we expected.

If we encounter the following scenario:

<div>{{proxyObj.hobby.one}}</div>
Copy the code

When proxyobj.hobby. One changes, we expect DOM to be updated. Because proxyObj does only a single layer of proxying, Hobby does not go through the Proxy to be responsive. The update fails.

So how is Vue3 solved?

The answer is: recursive proxy, which is similar to Vue2, by judging the type of value, and then response conversion.

So what we need to do here is rewrite the getter,

  • Judge the get when you get itvalueWhether it isObject
  • If it isObject, and then run it againReactiveThe agent
let handler = {
    get(Target, key, receiver) {
        // omit some code.....
        
    	const res = Reflect.get(target, key, receiver)
    	if (isObject(res)) {
            // If it is an object type, a deeper conversion is performed
      		createReactiveObject(res)
    	}
    	return res
  	},
    set(target, key, value, receiver) {
        console.log(` set:${key}`)
        Reflect.set(target, key, value, receiver)
    }
}
Copy the code

An array oftrack&triggerThe problem of

Vue3 doesn’t have an array interceptor, but there’s another problem. Let’s look at some code:

let arr = [1.2.3]
let handler = {
   get: function(target, key, receiver) {
       console.log(`get:${key}`)
       return Reflect.get(target, key, receiver)
   },
   set: function(target, key, value, receiver) {
       	console.log(`set:${key}`)
      	return Reflect.set(target, key, value, receiver)
   }
}
let proxyArr = new Proxy(arr, handler)
proxyArr.push(4) 
// get:push
// get:length
// set:3
// set:length
proxyArr.pop()
// get:pop
// get:length
// get:3
// set:length
proxyArr.shift()
// get:shift
// get:length
// get:0
// get:1
// set:0
// get:2
// set:1
// set:length
proxyArr.unshift(5)
// get:unshift
// get:length
// get:1
// set:2
// get:0
// set:1
// set:0
// set:length
proxyArr.splice(2.3.4)
// get:splice
// get:length
// get:constructor
// get:2
// set:2
// set:length
Copy the code

As demonstrated by the above operations, we can find that a simple operation may trigger Getter or Setter functions for multiple times. This operation may not be a problem in ordinary business development, but may lead to dead recursion in Vue3.

Push&pop & Shift&unshift &splice are methods that allow you to add and delete arrays. Note that:

  • These methods modify the original array directly
  • And causes the array length property to change

Includes&indexOf &lastIndexOf are arrays used to determine whether the values to be searched exist, note that:

  • All three of these methods traverse an array in an array method implementation

The film opens new Window and opens it to the public.

When viewing an array with watchEffect, dead recursion occurs

According to the rules printed above, it can be found that:

  • Each time a method is called, it fires firstget, and finally triggerset

Create an arrayInstrumentations

How to translate the Instrumentations from vue3? (✿ ◡ ‿ ◡)

Then, can we make a state manager to avoid unnecessary tracks and triggers by judging whether track is needed?

  • throughhanderIn thegetFunction to interceptarrayThe method on the
  • Encapsulate the method on the prototype and do it separately before and after the results are obtainedtrackPause and reset throughtrackStackrecordshouldTrack
  • After obtaining the results, proceedtrack

Let’s take a look at the processed code and note the order in which the comments are marked:

function createReactiveObject(target, handlers) {
  let proxy = new Proxy(target, handlers)
  return proxy
}
const targetMap = new WeakMap(a)function track(target, key) {
  if(! shouldTrack) {return
  }
  console.log('-------track-------')
  // Omit some code
}

function trigger(target, key, newValue, oldValue) {
  console.log('trigger')}const handlers = {
  get(target, key, receiver) {
      
    // 1. Get the result first
    // Note: the Reflect passes the processed arrayInstrumentations
    const res = Reflect.get(arrayInstrumentations, key, receiver)
    
    / / 5. Track again
    track(target, key)
    // Check the trigger
    console.log(`get:${key}`)
    return res
  },
  set(target, key, newValue, receiver) {
    const res = Reflect.set(target, key, newValue, receiver)
    trigger(target, key, newValue)
    console.log(`set:${key}`)
    return res
  },
}

// Encapsulate the methods on the array prototype
constarrayInstrumentations = {} ; ['push'.'pop'.'shift'.'unshift'.'splice'].forEach((key) = > {
  const method = Array.prototype[key]
  arrayInstrumentations[key] = function (thisArgs = [], ... args) {
      
    // 2. Pause track
    // First push the last shouldTrack state to the trackStack
   	// shouldTrack = false
    pauseTracking()
      
    // 3. Get the result
    const res = method.apply(thisArgs, args)
    
    // 4
    // shouldTrack is the last state
    resetTracking()
    return res
  }
})

// to control the track function
let shouldTrack = true
const trackStack = []
// Pause switch
function pauseTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = false
}
// Reset the switch
function resetTracking() {
  const last = trackStack.pop()
  shouldTrack = last === undefined ? true : last
}

let arr = [1.2.3]
let proxyArr = createReactiveObject(arr, handlers)
proxyArr.push(4)
proxyArr.pop()
proxyArr.shift()
proxyArr.unshift(5)
proxyArr.splice(2.3.4)
// -------track-------
// get:push
// -------track-------
// get:pop
// -------track-------
// get:shift
// -------track-------
// get:unshift
// -------track-------
// get:splice
Copy the code

Shallow response conversion

As mentioned earlier, depth proxying is required through recursion for multiple layers of objects. However, there are some scenarios where we want reactive objects to only need shallow proxying. This requires:

  • rewritegetFunction:
    • To create acreateGetterFunction is used to pass arguments
    • useJSClosure of the cacheshallow, returns the get function
    • getFunction passes throughshallowDetermine if you need to be rightresOnce again,reactive
  • rewritesetFunction:
    • targetObjectIs the shallow response, whentargetObjectInternal properties do not need to set new values when they change,
const shallowReactiveHandlers = {
  get(target, key, receiver) {
    // reactiveMap and shallowReactiveMap are weakMap instances used to map target and proxy instances
    if (receiver === shallowReactiveMap.get(target)) {
      return target
    }

    const res = Reflect.get(target, key, receiver)
	// No need to determine the type of res
    return res
  },
  set(target, key, value, receiver, shallow) {
    let oldValue = target[key]
    if(! shallow) { value = toRaw(value) oldValue = toRaw(oldValue) oldValue.value = valuereturn true
    } else {
      // Shallow is true and oldValue is not updated regardless of whether targetObject is reactive or not
    }

    const result = Reflect.set(target, key, value, receiver)
    
    if (target === toRaw(receiver)) { 
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
    }
    return result
  }
}
Copy the code

Read only response conversion

In some scenarios where we want reactive objects to be readable and immutable, we need to configure a handler for read-only response conversion only;

  • When changes are made, you can run thehandlerIntercepts the modification and throws an exception if it is triggered.
  • becausetargetObjectIt is read-only, and there is no need to do more to ittrack. ingetFunction to determine whether it is neededtrack
export const readonlyHandlers = {
  get: get(target, key, receiver) {
    // Determine the target and key
    if (key === ReactiveFlags.IS_REACTIVE) {
      return! isReadonly }else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver === readonlyMap.get(target)
    ) {
      return target
    }

    const res = Reflect.get(target, key, receiver)

	// isReadonly is true and no longer track
    // Comment out track:
    /* track(target, TrackOpTypes.GET, key) */ 

    if (isObject(res)) {
      return readonly(res)
    }
    return res
  },
  // The modification operation is directly warned or ignored
  set(target, key) {
    if (__DEV__) {
      console.warn(
        `Set operation on key "The ${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  deleteProperty(target, key) {
    if (__DEV__) {
      console.warn(
        `Delete operation on key "The ${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true}}Copy the code

Shallow read-only response conversion

Similarly, shallow read-only response conversion:

  • Not in thetargetObjectMake a deep transformation
  • Intercept modification operations and either warn or ignore them

The code:

const shallowReadonlyHandlers = {
    get: (target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return! isReadonly }else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver === shallowReadonlyMap.get(target)
    ) {
      return target
    }

    const res = Reflect.get(target, key, receiver)

   	return res
  },
  set(target, key) {
    if (__DEV__) {
      console.warn(
        `Set operation on key "The ${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  deleteProperty(target, key) {
    if (__DEV__) {
      console.warn(
        `Delete operation on key "The ${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true}}Copy the code

Finishing refactoring

Looking back at the above code, we find that there are a lot of redundant operations in the code, which is very necessary to tidy up:

  • The varioushandlerIn, the functions configured are the same, but the logic inside the method may have been changed because of requirements
  • Duplicate code in method:getThe function,setThere are multiple code duplicates in the function
    • The main function of get is to getvalue,track
    • The main function of the set function is to setvalue,trigger
    • There’s no need for everyhandlerBoth repeat the core function of the two functions

Reconstruction method:

  • throughcreateGetter,createSetterFunction createsget,setFunction, which uses closures to get different attributes by passing parameters
  • Combine handlers using the different methods you create
// Handle the mapping between target and proxy
const reactiveMap = new WeakMap(a)const shallowReactiveMap = new WeakMap(a)const readonlyMap = new WeakMap(a)const shallowReadonlyMap = new WeakMap(a)const get = createGetter()
const shallowGet = createGetter(false.true)
const readonlyGet = createGetter(true)
const shallowReadonlyGet = createGetter(true.true)

function createGetter(isReadonly = false, shallow = false) {
    
  return function get(target, key , receiver) {
 
    if (key === ReactiveFlags.IS_REACTIVE) {
      return! isReadonly }else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
            ? shallowReactiveMap
            : reactiveMap
        ).get(target)
    ) {
      return target
    }

    const res = Reflect.get(target, key, receiver)

    if(! isReadonly) { track(target, TrackOpTypes.GET, key) }if (shallow) {
      return res
    }

    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

const set = createSetter()
const shallowSet = createSetter(true)

function createSetter(shallow = false) {
  return function set(target, key, value, receiver) {
    let oldValue = target[key]
    if(! shallow) { value = toRaw(value) oldValue = toRaw(oldValue)if(! isArray(target)) { oldValue.value = valuereturn true}}else {
      // Shallow is true and oldValue is not updated regardless of whether targetObject is reactive or not
      
    }

    const result = Reflect.set(target, key, value, receiver)
   
   if (hasChanged(value, oldValue)) {
      // Determine if value has changed, and then track
      trigger(target, TriggerOpTypes.SET, key, value, oldValue)
   }
      
    return result
  }
}

function deleteProperty(target, key) {
  const hadKey = hasOwn(target, key)
  const oldValue = target[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

function has(target, key) {
  const result = Reflect.has(target, key)
  track(target, TrackOpTypes.HAS, key)

  return result
}

function ownKeys(target) {
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

// Combine target Handler
export const mutableHandlers = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

const readonlyHandlers = {
  get: readonlyGet,
  set(target, key) {
    if (__DEV__) {
      console.warn(
        `Set operation on key "The ${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  deleteProperty(target, key) {
    if (__DEV__) {
      console.warn(
        `Delete operation on key "The ${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true}}const shallowReactiveHandlers= extend(
  {},
  mutableHandlers,
  {
    get: shallowGet,
    set: shallowSet
  }
)

const shallowReadonlyHandlers = extend(
  {},
  readonlyHandlers,
  {
    get: shallowReadonlyGet
  }
)
Copy the code

Change detection of Map&Set

We learned how to configure the Object & Array handler and implement the various methods. The same logic applies to Map and Set data:

  • Split up and deal with eachmethod
  • Configure as requiredhandler

By proxyThe Map, the SetThe instance

Let’s see what happens when map and set are operated by a proxy.

By proxymap

let map = new Map([[1.2], [3.4], [5.6]]);
let mapHandler = {
  get(target, key, receiver) {
     console.log(`key: ${key}`) // For of traversals need to be commented out
    if(key === "size") {
      return Reflect.get(target, "size", target)
    }
    var value = Reflect.get(target, key, receiver)
    // Check the value type
    console.log(typeof value)
      
    // Note that you need to change the this direction of value
    return typeof value == 'function' ? value.bind(target) : value
  },
  set(target, key, value, receiver) {
    console.log(`set ${key} : ${value}`)
    return Reflect.set(target, key, value, receiver)
  }
}
let proxyMap = new Proxy(map, mapHandler);
/ / the size attribute
console.log(proxyMap.size) 
/ / output:
// key: size  
/ / 3

/ / get methods
console.log(proxyMap.get(1))
/ / output:
// key: get
// value: function
/ / 2

/ / set methods
console.log(proxyMap.set('name'.'daRui')) 
/ / output:
// key: set  
// value: function  
// {1 => 2, 3 => 4, 5 => 6, "name" => "daRui"}

/ / from the method
console.log(proxyMap.has('name'))
/ / output:
// key: has
// value: function
// true

// delete
console.log(proxyMap.delete(1))
/ / output:
// key: delete
// value: function
// true

/ / keys
console.log(proxyMap.keys())
/ / output
// key: keys
// value: function
// MapIterator {3, 5, "name"}

/ / values
console.log(proxyMap.values())
/ / output
// key: values
// value: function
// MapIterator {4, 6, "daRui"}

/ / entries
console.log(proxyMap.entries())

// forEach
proxyMap.forEach(item= > {
  console.log(item)
});
/ / output
// key: entries
// value: function
// MapIterator {3 => 4, 5 => 6, "name" => "daRui"}
// key: forEach
// value: function
/ / 4
/ / 6
// daRui

// equivalent to entries()
Uncaught TypeError: Cannot convert a Symbol value to a string
for(let [key, value] of proxyMap) {
  console.log(key, value)
}
/ / output
/ / 3, 4
/ / 5, 6
// "name", "daRui"
Copy the code

By proxyset

let set = new Set([1.2.3.4.5])
let setHandler = {
  get(target, key, value, receiver) {
    if (key === 'size') {
      return Reflect.get(target, 'size', target)
    }
    console.log(`key: ${key}`)
    var value = Reflect.get(target, key, receiver)
    console.log(`value: The ${typeof value}`)
    return typeof value == 'function' ? value.bind(target) : 
  },
  set(target, key, value, receiver) {
    console.log(`set ${key} : ${value}`)
    return Reflect.set(target, key, value, receiver)
  },
}
let proxySet = new Proxy(set, setHandler)

// add
console.log(proxySet.add('name'.'daRui'))
/ / output
// key: add
// value: function 
// true
/ / 6

// has
console.log(proxySet.has('name'))
/ / output
// key: has
// value: function
// true
      
// size
console.log(proxySet.size)
/ / output
// key: size
/ / 6

// delete
console.log(proxySet.delete(1))
/ / output
// key: delete
// value: function
// true

// keys
console.log(proxySet.keys())
/ / output
// key: keys
// value: function
// SetIterator {2, 3, 4, 5, "name"}

// values
console.log(proxySet.values())
/ / output
// key: values
// value: function
// SetIterator {2, 3, 4, 5, "name"}

// entries
console.log(proxySet.entries())
/ / output
// key: entries
// value: function
// SetIterator {2 => 2, 3 => 3, 4 => 4, 5 => 5, "name" => "name"}
      
// equivalent to entries
proxySet.forEach((item) = > {
  console.log(item)
})
/ / output
// key: forEach
// value: function
/ / 2
/ / 3
/ / 4
/ / 5
// name

for (let value of proxySet) {
  console.log(value)
}
/ / output
// value: function
/ / 2
/ / 3
/ / 4
/ / 5
// name

// clear
console.log(proxySet.clear())
/ / output
// key: clear
// value: function
Copy the code
  • The above output results occur becauseproxyThe agent isThe Map, the SetAn instance of the
  • We’re calling a method on the instance, which triggers access to the methodgetterfunction
  • Therefore,Map & SetThe type oftargetObject, cannot be used withObject & ArrayThe samehandler
  • Need toMap & SetCreate a method and configure ithandler

Add and delete

  • map & setType Specifies the type to be configuredhandlerwithObject & ArraySame: reactive, shallow, read-only, shallow read-only
  • Instance methods need to be handled independently
  • Design ideas
    • Because instance methods trigger method access firstgetterfunction
    • So configuredhandlerObject only needs to have onegetterfunction
    • Intercepts methods inside get functions
    • Do it inside the methodtrack/triggerwork
    • throughmap/setThe original method to get value and return

Pre-knowledge supplement:

  • Vue3One will be set for each object that gets convertedReactiveFlags.RAWattribute
  • Value is the originaltargetObject
  • toRawfunction
    • returnreactivereadonlyOrigin of agenttargetObject
    • Can be used to read data temporarily without the overhead of proxy access/trace
    • It can also be used to write data without triggering changes
    • Principle: By recursion, offproxyFind the original value
function toRaw(observed) {
  return (
    (observed && toRaw((observed)[ReactiveFlags.RAW])) || observed
  )
}
Copy the code
  • Differently configuredhandlerThe corresponding response conversion function is used to process result
  • The corresponding conversion function can be obtained according to the parameter type
const toReactive = (value ) = > isObject(value) ? reactive(value) : value

const toReadonly = (value) = > isObject(value) ? readonly(value) : value

const toShallow = (value) = > value
// Get the prototype
const getProto = (v) = > Reflect.getPrototypeOf(v)
Copy the code

Here we go directly to the source code of Vue3 Reactive, which is omitted but retains the core logic.

Understand the main ideas first, then pay attention to the details.

  • getfunction
    • Used forThe Map instance is obtained by keyvalue`
    • A key of type map can be a reference or a proxy instance
    • target & keyAll need to be donetoRawoperation
    • Root parameter is required to determine whether to proceedtrackTo collect dependencies
    • You need to call the originaltargetthegetMethods to obtainvalue
    • Finally, you need to return the proxiedvalue
function get(target, key, isReadonly = false,isShallow = false) {
  // Get the original object, the original key
  target = target[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  
  /** * I used looseEqual to compare target[reactiveFlags. RAW] with rawTarget. * Returns true, no difference between the two. But I don't understand why Utah university designed it this way, please advise me */
  
  // Key changes, track key
  if(key ! == rawKey) { ! isReadonly && track(rawTarget, TrackOpTypes.GET, key) }// track rawKey! isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)const { has } = getProto(rawTarget)
  
  // Get the corresponding conversion function based on the parameters
  const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
  
  // Returns the result of the proxy
  if (has.call(rawTarget, key)) {
    return wrap(target.get(key))
  } else if (has.call(rawTarget, rawKey)) {
    return wrap(target.get(rawKey))
  } else if(target ! == rawTarget) { target.get(key) } }Copy the code
  • setfunction
    • Used forMapType add element
    • mapType adds an element, possibly newkey:value, it may be modified
    • Check whether the key exists
    • The set function will changetarget, need to be carried outtrigger, triggering the response
function set(this, key, value) {
  // Unproxy, get the original value & target
  value = toRaw(value)
  const target = toRaw(this)
  const { has, get } = getProto(target)
  
  // Check whether the key exists at target
  let hadKey = has.call(target, key)
  if(! hadKey) { key = toRaw(key) hadKey = has.call(target, key) }// Get the old value
  const oldValue = get.call(target, key)
  
  // Set key: vlaue
  target.set(key, value)
    
  // Call trigger to trigger the response
  if(! hadKey) {// There is no key
    trigger(target, TriggerOpTypes.ADD, key, value)
  } else if (hasChanged(value, oldValue)) {
      
    // There is a key, and the operation is set
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
  return this
}
Copy the code
  • hasfunction
    • Can be used toSet/MapExample Determines whether an element exists
    • hasProperties do not changetargetObject.
    • Belong to thetargetAccess operation of
    • The need tokeyfortrackoperation

function has(this, key, isReadonly = false) {
  // Get the original target key
  const target = (this)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  
  // Track according to judgment
  if(key ! == rawKey) { ! isReadonly && track(rawTarget, TrackOpTypes.HAS, key) } ! isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)// Call the original target's has method to determine whether the key exists
  return key === rawKey
    ? target.has(key)
    : target.has(key) || target.has(rawKey)
}
Copy the code
  • addfunction
    • Used forSetInstance adds an element that is unique
    • If addedvalueIf it does not previously exist, thetargetA change has occurred and needs to be performedtriggerAction triggers a response
function add(this, value) {
  // Get the original object
  value = toRaw(value)
  const target = toRaw(this)
  
  // Get the prototype, call the method on the prototype to determine whether value exists
  // Because the value added by set is unique
  // Trigger is required only when key does not exist
  const proto = getProto(target)
  const hadKey = proto.has.call(target, value)
  if(! hadKey) {// Add an element and call the trigger function to trigger the response
    target.add(value)
    trigger(target, TriggerOpTypes.ADD, value, value)
  }
  return this
}

Copy the code
  • sizeattribute
    • Used to return to theSet/MapTotal number of members
    • Not triggertargetThe change of the
    • But it needs to be donetrack
function size(target, isReadonly = false) {
  // Get the original target
  target = target[ReactiveFlags.RAW]
  // Track if it is not read-only! isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)// Return the result
  return Reflect.get(target, 'size', target)
}
Copy the code
  • clearfunction
    • Used to removeSet/MapAll members of the instance
    • Will changetarget, need to calltriggerfunction
function clear(this) {
  // Get the original object
  const target = toRaw(this)
  consthadItems = target.size ! = =0
  const oldTarget = __DEV__
    ? isMap(target)
      ? new Map(target)
      : new Set(target)
    : undefined
  // Call clear before triggering the response
  const result = target.clear()
  if (hadItems) {
    // Trigger is required if the original object is not empty
    trigger(target, TriggerOpTypes.CLEAR, undefined.undefined, oldTarget)
  }
  return result
}
Copy the code
  • deletefunction
    • Used to deleteSet/MapInstance member
    • Will change the originaltarget, you need totrigger
function deleteEntry(this, key) {
    
  // Get the original target
  const target = toRaw(this)
  const { has, get } = getProto(target)
  
  // Check whether the key or the original key exists
  let hadKey = has.call(target, key)
  if(! hadKey) { key = toRaw(key) hadKey = has.call(target, key) }// Get the original value
  const oldValue = get ? get.call(target, key) : undefined
  
  // Get the execution result
  const result = target.delete(key)
  if (hadKey) {
      
    // Trigger if the member already exists
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}
Copy the code

Traversal mode & iterative mode

  • forEachfunction
    • Belongs to the traverser pattern in design pattern
    • Used to traverse theSet/MapInstance, which accepts a callback, willkey & valueTo pass tocallback
    • For different response interfaces, we create them based on judgmentforEach
    • We can go through the parametersisReadonly.isShallowGet differentforEach
    • forEachThe function does not change the original object, just doestrackwork
function createForEach(isReadonly, isShallow) {
  return function forEach(this,callback,thisArg) {
    const observed = this
    // Get the original target
    const target = observed[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    
    // Get the response conversion function based on the parameters
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    
    // Track in non-read-only condition! isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)// Call the forEach method on the original object, passing the value & key to the callback
    return target.forEach((value, key) = > {
        
      // The value & key is converted when passed
      return callback.call(thisArg, wrap(value), wrap(key), observed)
    })
  }
}
Copy the code
  • 可迭代Iterator pattern
    • Map/SetThere are three ways to do this:Keys (), values(), entries()
    • All three methods follow the iterable protocol & iterator protocol
    • Vue3These three methods are also handled internally
    • Simulate these three methods by implementing iterators on your own
    • Inside the created iterator, it is retrieved by calling the original object’s methodresult
    • fortrack, and theresultResponse processing
    • Finally, in the returned iterable, the respond-transformedresult
    • The main logic is identified in the comments for the code below
function createIterableMethod(method, isReadonly, isShallow) {
  return function(this. args) {
      
    // Get the original object
    const target = (this)[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    
    // Determine if the original object is of Map type
    const targetIsMap = isMap(rawTarget)
    
    // Entries () returns an iterable two-dimensional array
    const isPair =
      method === 'entries' || (method === Symbol.iterator && targetIsMap)
    
    const isKeyOnly = method === 'keys' && targetIsMap
    
    // Get the inner iterator returned by the original object method
    constinnerIterator = target[method](... args)// Get the corresponding conversion function based on the parameters
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    
    // Run track when not read only! isReadonly && track(rawTarget, TrackOpTypes.ITERATE, isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY )// Return an iterable
    return {
      // Follow the iterator protocol
      next() {
        // Call the internal iterator to get the result
        const { value, done } = innerIterator.next()
        return done
          ? { value, done }
          : {
              // Perform a reactive conversion to value
              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
              done
            }
      },
      // Follow the iterable protocol
      [Symbol.iterator]() {
        return this}}}}const iteratorMethods = ['keys'.'values'.'entries'.Symbol.iterator]
// Iterate over the add method
iteratorMethods.forEach(method= > {
  mutableInstrumentations[method] = createIterableMethod(
    method,
    false.false
  )
  readonlyInstrumentations[method] = createIterableMethod(
    method,
    true.false
  )
  shallowInstrumentations[method] = createIterableMethod(
    method,
    false.true
  )
  shallowReadonlyInstrumentations[method] = createIterableMethod(
    method,
    true.true)})Copy the code

Those interested in iterators or iterable protocols can click on the link below

Iterator protocol: developer.mozilla.org/zh-CN/docs/…

Iterator: developer.mozilla.org/zh-CN/docs/…

Iterator: es6.ruanyifeng.com/#docs/itera…

createHandler

Now that we’ve seen how to create the various methods, it’s time to do something similar to Object & Array — configure different handlers.

For read-only operations that trigger changes, we use the createReadonlyMethod method
function createReadonlyMethod(type) {
  return function(this. args) {
    if (__DEV__) {
      const key = args[0]?`on key "${args[0]}"` : ` `
      console.warn(
        `${capitalize(type)} operation ${key}failed: target is readonly.`,
        toRaw(this))}return type === TriggerOpTypes.DELETE ? false : this}}const mutableInstrumentations = {
  get(this, key) {
    return get(this, key)
  },
  get size() {
    return size(this)
  },
  has,
  add,
  set,
  delete: deleteEntry,
  clear,
  forEach: createForEach(false.false)}const shallowInstrumentations = {
  get(this, key) {
    return get(this, key, false.true)},get size() {
    return size(this)
  },
  has,
  add,
  set,
  delete: deleteEntry,
  clear,
  forEach: createForEach(false.true)}const readonlyInstrumentations = {
  get(this, key) {
    return get(this, key, true)},get size() {
    return size(this.true)},has(this, key) {
    return has.call(this, key, true)},add: createReadonlyMethod(TriggerOpTypes.ADD),
  set: createReadonlyMethod(TriggerOpTypes.SET),
  delete: createReadonlyMethod(TriggerOpTypes.DELETE),
  clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
  forEach: createForEach(true.false)}const shallowReadonlyInstrumentations = {
  get(this, key) {
    return get(this, key, true.true)},get size() {
    return size((this.true)},has(this, key) {
    return has.call(this, key, true)},add: createReadonlyMethod(TriggerOpTypes.ADD),
  set: createReadonlyMethod(TriggerOpTypes.SET),
  delete: createReadonlyMethod(TriggerOpTypes.DELETE),
  clear: createReadonlyMethod(TriggerOpTypes.CLEAR),
  forEach: createForEach(true.true)}const iteratorMethods = ['keys'.'values'.'entries'.Symbol.iterator]
iteratorMethods.forEach(method= > {
  mutableInstrumentations[method] = createIterableMethod(
    method,
    false.false
  )
  readonlyInstrumentations[method] = createIterableMethod(
    method,
    true.false
  )
  shallowInstrumentations[method] = createIterableMethod(
    method,
    false.true
  )
  shallowReadonlyInstrumentations[method] = createIterableMethod(
    method,
    true.true)})// Create the getter function
function createInstrumentationGetter(isReadonly, shallow) {
  const instrumentations = shallow
    ? isReadonly
      ? shallowReadonlyInstrumentations
      : shallowInstrumentations
    : isReadonly
      ? readonlyInstrumentations
      : mutableInstrumentations
  
  / / the get function
  return (target, key, receiver) = > {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return! isReadonly }else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.RAW) {
      return target
    }
    
    // Notice the first argument passed here to reflect.get
    2. Instrumentations are created by us.
    Instrumentations; // Instrumentations; This is the context when the get function is called
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

/ / configure Handler
const mutableCollectionHandlers = {
  get: createInstrumentationGetter(false.false)}const shallowCollectionHandlers = {
  get: createInstrumentationGetter(false.true)}const readonlyCollectionHandlers = {
  get: createInstrumentationGetter(true.false)}const shallowReadonlyCollectionHandlers = {
  get: createInstrumentationGetter(true.true)}Copy the code

Let’s summarize the above process with two more pictures.

BaseHandlers:

CollectionHandlers:

API Implementation Principle

reactive

Now that we’ve seen how to configure the Handler, we’re going to create different response conversion functions, and creating functions is essentially configuring different handlers for the Proxy. Let’s rewrite the createReactiveObject function mentioned in the proxy mode in Section 1:

The response function written in Chapter 1

// The response conversion function
function createReactiveObject(target, handlers, proxyMap) {
    
   // 1. Proxy object types only
  if(! isObject(target)) {if (__DEV__) {
      console.warn(`value cannot be made reactive: The ${String(target)}`)}return target
  }
 
  // 2. Determine whether target has passed the proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 3. Perform proxy conversion
  const proxy = new Proxy(target,  handlers)
  
  // 4. Create a mapping between the target and the proxy instance for next judgment
  proxyMap.set(target, proxy)
    
  // 5. Return the proxy instance
  return proxy
}
Copy the code

Rewrite createReactiveObject:

  • willhandlersPass it as an argument to the function
  • Internally through judgmenttargetType, configurationhandlers

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap ) {
  if(! isObject(target)) {if (__DEV__) {
      console.warn(`value cannot be made reactive: The ${String(target)}`)}return target
  }
    
  // If target has already passed through the proxy, return directly
  // Exception for targetObject converted by readonly()
  if( target[ReactiveFlags.RAW] && ! (isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) {return target
  }
    
  // If the target already has the corresponding agent, return it directly
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
    
  // Only targetTypes in the whitelist can pass through the proxy
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
    
  // Pass different handlers to Proxy based on targetType
  // Map & Set type is passed to collectionHandlers
  // baseHandlers are passed to Object & Array
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  
  // Save the relationship between target and proxy for future judgment
  proxyMap.set(target, proxy)
  return proxy
}
Copy the code

reactive

  • Returns a reactive copy of the object
  • righttargetDo a “deep” proxy that affects all nestingproperty
function reactive(target) {
  If target is read-only, return target directly
  if (target && (target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}
Copy the code

shallowReactive

  • Create a reactive proxy that tracks only itself propertyThe responsiveness of
  • But deep reactive transformations of nested objects are not performed
function shallowReactive(target) {
  return createReactiveObject(
    target,
    false,
    shallowReactiveHandlers,
    shallowCollectionHandlers,
    shallowReactiveMap
  )
}
Copy the code

readonly

  • A read-only proxy that accepts an object (reactive or pure) or ref and returns the original object.
  • Read-only agents are deep: any nesting that is accessedpropertyIt’s also read-only.
function readonly(target) {
  return createReactiveObject(
    target,
    true,
    readonlyHandlers,
    readonlyCollectionHandlers,
    readonlyMap
  )
}
Copy the code

shallowReadonly

  • To create aproxyMake its ownpropertyAs read-only
  • Deep read-only conversion of nested objects is not performed
export function shallowReadonly(target) {
  return createReactiveObject(
    target,
    true,
    shallowReadonlyHandlers,
    shallowReadonlyCollectionHandlers,
    shallowReadonlyMap
  )
}
Copy the code

isReactive

  • Check whether the object is a reactive agent created by Reactive
  • One can be set to the target in the observed phase[ReactiveFlags.RAW]/[ReactiveFlags.IS_REACTIVE]attribute
function isReactive(value) {
  if (isReadonly(value)) {
    return isReactive((value)[ReactiveFlags.RAW])
  }
  return!!!!! (value && (value)[ReactiveFlags.IS_REACTIVE]) }Copy the code

isReadonly

  • Check whether the object is a read-only proxy created by ReadOnly.
  • The principle of same
function isReadonly(value) { return !! (value && (value)[ReactiveFlags.IS_READONLY]) }Copy the code

isProxy

  • Check whether the object is a proxy created by Reactive or Readonly
  • Internal is called isReactive | isReadonly method for judgment
function isProxy(value: unknown) :boolean {
  return isReactive(value) || isReadonly(value)
}
Copy the code

toRaw

  • I have already said that and will not repeat it

markRaw

  • Marks an object so that it will never be converted toproxy.Return the object itself.
  • Principle: Give ValueTo define aReactiveFlags.SKIPProperty and sets the value totrue
  • inreactiveIs used to judge the attribute
function markRaw (value) {
  def(value, ReactiveFlags.SKIP, true)
  return value
}
Copy the code

ref

When learning Vue3, I have a question: why do I have a ref when I already have a reactive function? Later read the source code to understand part of the reason.

In Reactive, we can’t cast base types, only object types can be brokered. How do you implement a proxy for the underlying type?

Create a Ref Class. Get & set interception is implemented by class instance.

Refs can be used to convert primitive types as well as object types, and refs complement the conversion of reactive base types in part because of other, higher-level considerations and design. I just can’t see it for the moment

Ref Class = Ref Class = Ref Class = Ref Class = Ref Class

// Convert by judging value
const convert = (val) = > isObject(val) ? reactive(val) : val

/ / Ref class
class RefImpl {
  private _value:

  public readonly __v_isRef = true

  constructor(private _rawValue, public readonly _shallow) {
      
    // Shallow is true and value is not converted
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }

  // Get to track
  get value() {
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  // Trigger when set
  set value(newVal) {
    if (hasChanged(toRaw(newVal), this._rawValue)) {
        
      // Trigger if the old and new values are different
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
        
      // Trigger the response
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}

// The factory function is used to create the ref instance
function createRef(rawValue, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}
Copy the code

With the createRef function in place, it’s time to create the API

ref

  • To accept avalueAnd returns a responsive and mutableref Object.
  • ref Object has a single property pointing to an internal value.value
function ref(value) {
  return createRef(value)
}
Copy the code

shallowRef

  • Create a trace itself.valueChanges in the ref, but does not make its value also reactive.
function shallowRef(value) {
  return createRef(value, true)}Copy the code

isRef

  • judgevalueWhether it isrefobject
  • Notice up here we areRefImpl ClassA read-only property set in__v_isRef
function isRef(r) {
  return Boolean(r && r.__v_isRef === true)}Copy the code

toRef

  • Create a new REF for a property on the source responsive object
  • When you need to proprefWhen I pass it to the composition function,toRefIt is useful to
class ObjectRefImpl {
  public readonly __v_isRef = true

  constructor(private readonly _object, private readonly _key) {}

  get value() {
    return this._object[this._key]
  }

  set value(newVal) {
    this._object[this._key] = newVal
  }
}

function toRef(object,key) {
  return isRef(object[key])
    ? object[key]
    : (new ObjectRefImpl(object, key))
}
Copy the code

toRefs

  • Converts a reactive object to a normal object, where each property of the resulting object refers to the original objectproperty theref.
  • Deconstructing a reactive object is useful when it is returned from a composed function
  • Principle: Traverse responsive object, calltoRefConvert each pairkey:value
function toRefs {
  if(__DEV__ && ! isProxy(object)) {console.warn(`toRefs() expects a reactive object but received a plain one.`)}const ret: any = isArray(object) ? new Array(object.length) : {}
  for (const key in object) {
    ret[key] = toRef(object, key)
  }
  return ret
}
Copy the code

customRef

  • Create a customrefAnd has explicit control over its dependency tracing and update triggering.
  • You need a factory function as an argument that acceptstracktriggerFunction as argument
  • And should return a string withgetsetThe object of
class CustomRefImpl {
  private readonly _get: ['get']
  private readonly _set: ['set']

  public readonly __v_isRef = true

  constructor(factory) {
    // Pass track &trigger to the factory function
    // Get the return get set function
    const { get, set } = factory(
      () = > track(this, TrackOpTypes.GET, 'value'),
      () = > trigger(this, TriggerOpTypes.SET, 'value'))this._get = get
    this._set = set
  }

  get value() {
    return this._get()
  }

  set value(newVal) {
    this._set(newVal)
  }
}
Copy the code

Off topic: When I looked at the design of this code, it was really clever

API implementation:

function customRef(factory){
  return new CustomRefImpl(factory)
}
Copy the code

triggerRef

  • Manually execute any effects associated with shallowRef
  • The inside is just manualtrigger
function triggerRef(ref) {
  trigger(toRaw(ref), TriggerOpTypes.SET, 'value', __DEV__ ? ref.value : void 0)}Copy the code

We can talkeffect(Important!!)

Earlier, we talked briefly about the effect function when we talked about change detection. However, the execution of effect throughout the response is not resolved. It is important to do this this time, because it is difficult to understand how computations work if effects are performed differently.

There are four levels of Effect in VUE3:

  • Responsible for rendering updatescomponentEffect
  • Responsible for handlingwatchthewatchEffect
  • Responsible for handlingcomputedcomputedEffect
  • User’s own useeffect APICreated wheneffct

This time we mainly talk about setupRenderEffect and calculated effect.

Here’s a snippet of code:

<div id="app">
  <input :value="input" @input="update" />
  <div>{{output}}</div>
</div>

<script>
const { ref, computed, effect } = Vue

Vue.createApp({
  setup() {
    const input = ref(0)
    const output = computed(function computedEffect() { return input.value + 5})
    
    // computedEffect & effect below is reexecuted
    const update = _.debounce(e= > { input.value = e.target.value*1 }, 50)
    
	effect(function callback() {
        // Rely on collection
        console.log(input.value)
    })
    return {
      input,
      output,
      update
    }
  }
}).mount('#app')
</script>

Copy the code

In the browser, from the template code above to rendering to a responsive page, Vue probably does a few things:

  • performsetupFunction,stateBecome responsive and mount the result to the component instance
  • Apply to the templatecompilerAnd render to view
  • incompilerIn the process, yesRead reactive data, the reading process will trigger **getterDelta function, it’s going to goRely on collection to work
  • This is triggered when data is entered into the formupdateEvents, changesinput, the change process will trigger **setterDelta function, it’s going to goResponse update **

Let’s look directly at how the trigger function is designed in the simple source code:

  • The trigger function basically gets all of the objects associated with targeteffect
  • I’m going to have to go througheffectAdd to the set to be traversed (de-duplicated)
  • Iterate through the set to execute alleffect, triggers the response.
  • Combined with sample code:
    • When used ininputAnd while I’m typing,
    • Will performupdateFunction,inputMake the change
    • The triggertrigger, collect all withinputAssociated effects, traversal executioneffects
    • That’s when it’s executedcomponentEffect,computedEffect, user-definedeffect
function trigger (target, type, key, newValue, oldValue, oldTarget) {
    const depsMap = targetMap.get (target);
    
    // No dependencies are returned directly
    if(! depsMap) {return;
    }
    const effects = new Set(a);// Add an effect to iterate over
    const add = effectsToAdd= > { 
      if (effectsToAdd) {
        effectsToAdd.forEach (effect= > {
          if(effect ! == activeEffect || effect.allowRecurse) { effects.add(effect); }}); }};if(key ! = =void 0) {
       add (depsMap.get (key));
    } 
    
    // the callback function for traversal
    const run = effect= > {
      if (effect.options.scheduler) {
        effect.options.scheduler (effect);
      } else {
          
        // Execute the effect function
        // The fn function passed when the effect function was created is executedeffect(); }}; effects.forEach (run); }function track (target, type, key) {
    if(! shouldTrack || activeEffect ===undefined) {
      return;
    }
    let depsMap = targetMap.get (target);
    if(! depsMap) { targetMap.set (target, (depsMap =new Map ()));
    }
    let dep = depsMap.get (key);
    if(! dep) { depsMap.set (key, (dep =new Set ()));
    }
    // Maintain the relationship between effect and DEP
    if(! dep.has (activeEffect)) { dep.add (activeEffect); activeEffect.deps.push (dep);if (activeEffect.options.onTrack) {
        activeEffect.options.onTrack ({
          effect: activeEffect, target, type, key, }); }}}Copy the code

What happens when you execute an effect? We then combine the source code and sample code for interpretation:

  • effectStackThe stack is mainly for maintenanceeffectwithdepThe nested relationship between.
  • Is responsible for updatingactiveEffect. (You can take a looktrackThe delta function is the same as what we said beforeeffectIts placedepThe relationship between
  • enableTracking & resetTrackingFunctions are used to controltrackThe state of the
  • becauseeffectIt also happens during executiontrackWork. At this point, you need to determine whether the current value is requiredstatewitheffectsBuilding dependencies
  • returnfn()The result of the executionfinallyInside the code that pops up the currenteffectUpdate the next oneeffect
  • Combined with the example code, we analyze:
    1. When you enter the number 5 in the input box,inputchange
    2. callbackFunction first push ==effectStackStatus:[callback] = =”callbackPerform == to accessinput.value= = >trackTo printinput.value: 5 == “callback ‘out of the stack
    3. componentEffectStack = =”effectStackStatus:[componentEffect ] = = > updateinputInside the value == “proceedtrack = = > updatediv, need to callcomputedthegetterFunction fetch value
    4. computedEffectStack = =” effectStackStatus:[componentEffect, computedEffect]= = > the currentactiveEffectiscomputedEffect= = >track == gets the getterThe value of the = ="computedEffectOut of stack == "proceedtrack`
    5. componentEffectThe stack = =”effectStackStatus: CurrentactiveEffectiscomponentEffect
    6. Complete the response process
const effectStack = [];
let activeEffect;
let uid = 0;
function createReactiveEffect (fn, options) {
    const effect = function reactiveEffect () { 
      if(! effect.active) {return fn ();
      }
      if(! effectStack.includes (effect)) {// Effect needs to be removed from its dePS first
        cleanup (effect);
        try {
          / / recovery track
          enableTracking ();
          / / into the stack
          effectStack.push(effect);
          // Set current effect
          // The activeEffect/DEP relationship is maintained
          activeEffect = effect;
            
          / / execution fn
          // Fn can be passed to componentEffect when creating watch
          // Getters for callback and computedEffect
          // It can also be a callback function passed by the user using the Effect API
          // These functions are likely to have internal access to state, and the procedure will trigger the getter,
          // then track the dependency collection
          return fn ();
        } finally {
          // Unstack the current effect
          effectStack.pop();
          / / reset the track
          resetTracking ();
          // Update next effect
          activeEffect = effectStack[effectStack.length - 1]; }}}; effect.id = uid++; effect.allowRecurse = !! options.allowRecurse; effect._isEffect =true;
    effect.active = true;
    effect.raw = fn;
    effect.deps = [];
    effect.options = options;
    return effect;
  }

  function cleanup (effect) {
    const {deps} = effect;
    if (deps.length) {
      for (let i = 0; i < deps.length; i++) {
        deps[i].delete (effect);
      }
      deps.length = 0; }}function enableTracking() {
    trackStack.push(shouldTrack)
    shouldTrack = true
  }

  function resetTracking() {
    const last = trackStack.pop()
    shouldTrack = last === undefined ? true : last
  }

Copy the code

The process can be convoluted, but it is regular. Note that the process of effection execution executes fn, which is likely to trigger track for dependency collection, and effecStack, which maintains the update order between effects, always updating the last effection first.

Now that you know how effects perform, it’s easy to understand computed.

computed

For computed tomography, let’s think about how to use it:

  • Can you givecomputeD agetter The function,
  • It will be based ongetterReturns an immutable responserefObject.
const count = ref(1)
const plusOne = computed(() = > count.value + 1)

console.log(plusOne.value) / / 2

plusOne.value++ / / error
Copy the code
  • Or, accept one that hasgetsetFunction to create writableref Object.
const count = ref(1)
const plusOne = computed({
  get: () = > count.value + 1.set: val= > {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) / / 0
Copy the code

You all know that the real power of computed data is lazy updating, which only happens when the values it depends on change. In fact, the process of analyzing effect has already been drawn out. When the dependent values change, the rendering compontentEffect function calls computedEffect to get the new values.

Computed mainly updates values based on dirty. When dirty is true, the return value needs to be recalculated. When dirty is false, the return value does not need to be recalculated.

When a data rendering view is used in a template, if the data is computed, then reading the data actually triggers the getter method of the computed property to get value, and then sets the _dirty property to false. It will not change until the next time the data on which the property depends has changed.

When we create a computed property, we configure an effect with lazy true and scheduler attributes. The Scheduler of computedEffect is responsible for resetting the _dirty attribute. And trigger the trigger.

When the data in the template changes, trigger is triggered and all effects are executed in response, and if effect.scheduler exists, the effect.scheduler function is executed to reset _dirty. When componentEffect, which is responsible for rendering, re-reads the value of the calculated property, it calls the computed getter method. In this case, dirty is true, and when the new value is returned, dirty is set to false.

Let’s take a look at the implementation of the simple version of the source code:

// Create computed API
function computed(getterOrOptions) {
  let getter
  let setter

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () = > {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  return newComputedRefImpl( getter, setter, isFunction(getterOrOptions) || ! getterOrOptions.set ) }// computed Class
class ComputedRefImpl {
  private _value! 
  private _dirty = true

  public readonly effect
  
  // Declare a reactive ref object as read-only
  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]

  constructor(getter, readonly _setter, isReadonly) {
    // 
    this.effect = effect(getter, {
      // Lazy is true and does not execute the getter immediately
      lazy: true.// Effects is traversed at the end of the trigger function
      // Execute run if effect.option.scheduler exists
      // The scheduler function is executed
      scheduler: () = > {
        
        if (!this._dirty) {
          / / reset _dirty
          this._dirty = true
          
          // The response is dispatched
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')}}})this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    
    const self = toRaw(this)
    if (self._dirty) {
        
      // Effect executes the getter to get the new value
      self._value = this.effect()
      // set _dirty to false
      self._dirty = fa
        lse
    }
    // track relies on collection
    track(self, TrackOpTypes.GET, 'value')
    return self._value
  }

  set value(newValue) {
    // Pass the new value to the user-configured setter function
    this._setter(newValue)
  }
}
Copy the code

The above code has been annotated to indicate the main logic of the code. Combined with effect’s execution logic, computed is easy to understand when you analyze it.

conclusion

So far we have analyzed the Vue3 Reactive source code. In the new reactive structure, Vue intercepts state getters or setters through proxies. The collection of dependencies is done by effect. Create responsive apis for different data types and response levels by configuring different handler objects.

  • If there are any mistakes in the article, I hope you can criticize and correct them. Darry was grateful.

  • If you have any questions, please leave them in the comments section or on my official account [Coder Rhapsody], and I will answer them patiently

  • If it feels good and helps you, a “like” is the biggest encouragement for me

Reference:

  • Github.com/vuejs/vue-n…
  • v3.vuejs.org/
  • Vue. Js