编程知识 cdmana.com

Chapter I interpretation of vue3 reactivity source code

Vue3 reactivity The source code is in simple language

*

Hello everyone , I'm jiandari . Some time ago, the company used Vue3 Made a new project , It is my pleasure to , Participated in the development of most functions , Just put Vue3 The whole family practiced it all . Strike while the iron is hot , Read it Vue3 In the source reactivity Package source code . A lot of gains , Take this opportunity to , Share it . If there is any deficiency , I hope you will criticize and correct me .

*
 Catalog
Catalog

A contrast between the old and the new

The main purpose of this sharing is Vue3 Of reactivity The source part of , So only compare Vue2 And Vue3 The responsive source code part of .

Comparison of old and new principles :Object.defineProperty And Proxy

Object.defineProperty

I know Vue2 All the students know the source code . stay Vue2 in , Its interior is through Object.defineProperty To achieve change detection . This method can directly define a new attribute on an object or modify an existing attribute . Accept three parameters , Namely targetObjectkey And one for key Of descriptorObject, The return value is the object passed to the function .

descriptorObject Key values that can be selected :

  • configurable: Sets the configurability of the current property , Default false.
  • enumerable: Sets the enumerability of the current property , Default false.
  • value: Set the value of the current property , Default undefined.
  • writable: Sets whether the current property can be changed , Default false.
  • get: Of the current property getter function , When accessing this property , Will trigger the function .
  • set: Of the current property setter function , When setting the current property value , Will trigger the function .

Here attached Vue2 in defineReactive Function brief source code , Focus on get Functions and set function .

function defineReactive (obj, key, val, customSetter, shallow{
  //  Omitted code ...
  
  Object.defineProperty(obj, key, {
    enumerabletrue,
    configurabletrue,
    getfunction 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
    },
    setfunction reactiveSetter (newVal{
      const value = getter ? getter.call(obj) : val
      
      //  Comparison of old and new values
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      } 
      if (getter && !setter) return
        
      //  by object Set new value
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
        
      //  Detect new values
      childOb = !shallow && observe(newVal)
        
      //  Trigger dependency , Respond to
      dep.notify()
    }
  })
}

Object.defineProperty The problem of

  • Directly modifying Array Of length Property has compatibility issues .
let demoArray = [1,2,3]
Object.defineProperty(demoArray, 'length', { 
    setfunction(
        console.log('length changed!')
    } 
})
// Uncaught TypeError: Cannot redefine property: length
// at Function.defineProperty (<anonymous>)
// at <anonymous>:2:8
  • Direct to targetObject Add or delete attributes , Object.defineProperty Cannot trigger dependency .
let obj = { name"jiandarui"age18 };
obj = Object.defineProperty(obj, 'age', {
   configurabletrue,
    getfunction({
        console.log("get value")
    },
    setfunction(value
        console.log('set value')
    } 
})
obj.gender = "man"//  There is no trigger  Getter
delete obj.age //  There is no trigger  Setter
  • Only right targetObject Detect the properties of , Not for the whole targetObject To detect .
function defineReactive(data, key, val{
 Object.defineProperty(data, key, {
        configurabletrue,
        enumerabletrue,
        getfunction({
            //  Do dependency collection here
            console.log(" Rely on collection ")
            return val;
        },
        setfunction(newVal{
         if(val === newVal) {
                return val;
            }
         //  Do change detection here
         console.log(" change detection ")
         val = newVal;
     }
    })
}

To solve these three problems ,Vue2 Different measures have been taken in :

  • For array changes , Create an array interceptor .
  • For adding and deleting object attributes , establish $set$delete API.
  • in the light of value The situation of being the object , use recursive The way , Depth traversal , Do dependency collection .

Proxy

With the browser on ES6 Increased support , stay Vue3 Used in Proxy.

proxy It can be directly applied to the whole targetObject To intercept , It takes two arguments , Namely targethandler, And returns a proxy object .handler Configurable methods are ***13*** Kind of , Involving attribute lookup 、 assignment 、 enumeration 、 Function call 、 Prototype 、 Property describes the related methods .

Now we can learn about... Through code examples proxy, And Vue3 in handler Several methods used :

let obj = { name" Luo Xiang "}
cosnt handler = {
    getfunction(target, key, receiver{
        console.log("get")
    },
    setfunction(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 San ";
*

Here it is Proxy, I have to mention its good friends :Reflect.Reflect The method is basically the same as Object identical , But there are subtle differences .

Reflect The method and the method of Proxy The method name is the same . It can be done to target mapping .

When you need to call Object Method on , We can call Reflect.Reflect It's equivalent to directly to Javascript The operation of does a layer of interception .

*
  • get()
    • Used to intercept the reading of object properties
    • Accept three parameters target( The original object )、key( Properties to read )、receiver(Proxy Object instance or inheritance Proxy The object of )
    • The return value can be customized
    • inheritable
let p = new Proxy({ name" Luo Xiang " }, {
  getfunction(target, key, receiver{
    console.log("called: " + key);
    return Reflect.get(target, key, receiver);
  }
});

console.log(p.a); // "called: a"
  • set()
    • Used to set the value of the object property
    • Accept four parameters targetkeynewValue( The newly set value )、 receiver
    • return true, Strict mode returns false Will be submitted to the TypeError abnormal
let p = new Proxy({ name" Luo Xiang "profession" The driver " }, {
  setfunction(target, key, value, receiver{
    console.log("called: " + key+ ": " + value);
    return Reflect.set(target, key, value, receiver);
  }
});
p.profession = " The lawyer " // called: profession:  The lawyer
p.age = 18 // called: age: 18
console.log(p.age) // 18
  • deleteProperty()
    • Used to intercept changes to object properties delete operation ( Make up for Object.definedProperty Property right delete The problem of no sense of operation ).
    • Accept parameters : target、 key
    • Returns a Boolean value : true success , false Failure
var p = new Proxy({}, {
  deletePropertyfunction(target, prop{
    console.log('called: ' + prop);
    return true;
  }
});

delete p.a; // "called: a"
  • has()
    • To intercept in operation
    • Accept parameters targetkey
    • Returns a Boolean value , true There is , false non-existent
    • Interception is only for in The operator takes effect , Yes for...in The cycle doesn't work
// ECMAScript 6 introduction 
let stu1 = {name' Zhang San 'score59};
let stu2 = {name' Li Si 'score99};

let handler = {
  has(target, prop) {
    if (prop === 'score' && target[prop] < 60) {
      console.log(`${target.name}  fail, `);
      return false;
    }
    return prop in target;
  }
}

let oproxy1 = new Proxy(stu1, handler);
let oproxy2 = new Proxy(stu2, handler);

'score' in oproxy1
//  Zhang San   fail,
// 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]);
}
let p = new Proxy({}, {
  ownKeysfunction(target{
    console.log('called');
    return ['a''b''c'];
  }
});

console.log(Object.getOwnPropertyNames(p)); // "called"

summary ( To be improved )

Proxy Compare with Object.defineProperty More powerful . With the example above , It can be seen that Proxy Can make up Object.defineProperty In dependency collection , Detect defects in change , such as :

  • Yes Object Add or delete properties
  • adopt Array Subscript to modify or add elements

however Proxy It also has its own shortcomings , Let's leave a blank here , It will be added later . Let's go on .

Comparison of old and new models : Observer mode and agent mode

Observer mode

Vue2 The relationship between data and dependencies is handled internally through the observer pattern , The characteristics of the observer model :

  • ** One to many .** There are one to many dependencies between multiple objects , When the state of an object changes , All objects that depend on him are notified and automatically updated
  • Reduce the coupling between target data and dependencies
  • It's a behavioral design pattern

Simple implementation of observer mode

  • Subject` class :
    • observers` Property is used to maintain all observers
    • add Method is used to add an observer
    • notify Method is used to notify all observers
    • remove Method is used to remove the observer
  • Observer class :
    • update Method is used to accept changes in state
// Subject  object 
class Subject {
    constructor() {
        //  Store observers
        this.observers = [];
    }
    add(observer) { 
     this.observers.push(observer);
   }
    //  State change , Notify observer
  notify(...args) { 
     var observers = this.observers;
     for(var i = 0;i < observers.length;i++){
        observers[i].update(...args);
     }
   }
 //  Remove observer
   remove(observer){
     var observers = this.observers;
     for(var i = 0; i < observers.length; i++){
        if(observers[i] === observer){
          observers.splice(i,1);
        }
     }
   }
}


// 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(' Jian Darui ');
let smallRio = new Observer(' Jian Xiaorui ');
sub.add(bigRio);
sub.add(smallRio);
sub.notify(); 

Vue2 The observer pattern in

 Borrow the big guy's picture , Invasion and deletion
Borrow the big guy's picture , Invasion and deletion
  • Vue2 in Data is what we want to observe ,Watcher It's called dependency , and Dep Just responsible for Watcher Collection and distribution of .

  • another Vue2 in watcher It can also be target data . It is associated with Dep It's a many to many relationship , Not one to many .

Let's review again Vue2 How to design these classes in :

  • Observer class
    • Used to create Observer example
    • walk Method to traverse the observed object , take value Turn each item in into a response
    • observeArray Method is used to traverse the observed array , Turn each item in the array into a response
class Observer {
  constructor (value) {
    this.value = value
    this.dep = new Dep()
    def(value, '__ob__'this)
    if (Array.isArray(value)) {
        
      //  Detect array
      this.observeArray(value)
    } else {
        
      //  Detected objects
      this.walk(value)
    }
  }

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

  observeArray (items) {
    //  Traverse the array for conversion
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
  • Observe Method
    • observe Method is used to create Observer Example workshop method
function observe (value, asRootData){
  if (!isOObject(value) || value instanceof 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
}
  • defineReactive Method
    • Yes val Make recursive Observations
    • adopt Object.defineProperty, Yes obj[key] Conduct GetterSetter Intercept
    • Do dependency collection 、 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

  //  Recursive observation val
  let childOb = !shallow && observe(val)
  
  Object.defineProperty(obj, key, {
    enumerabletrue,
    configurabletrue,
    getfunction reactiveGetter ({
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        //  Rely on collection   For the current Watcher
        dep.depend()
        if (childOb) {
          //  Son Observer Collection dependency
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    setfunction reactiveSetter (newVal{
      const value = getter ? getter.call(obj) : val
      //  Judge the old and the new value Whether it is equal or not
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }

      if (setter) {
        //  Set new value
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      //  Detect new values
      childOb = !shallow && observe(newVal)
      //  Notice Update
      dep.notify()
    }
  })
}
  • Dep class
    • amount to Observer And Watcher Intermediary between
    • Used to maintain the relationship between data and dependencies
let uid = 0
class Dep {
  static target;
  id;
  subs;

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

Dep.target = null

  • Watcher class
    • Real dependence , perform callback function , To respond
    • maintain dep And watcher many-to-many
    • 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) {
        
      //  Rendering watcher
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
    }
    this.cb = cb
    this.active = true
    this.dirty = this.lazy 

    //  Maintenance and current watcher All the dependencies that are relevant
    this.deps = []
 
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
      
    //  obtain 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 "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
  
  //  Add and current Watcher All related to the instance dep
  // watcher And dep Many to many
  addDep (dep) {
    dep.addSub(this)
  }
    
  //  Traversal and current watcher dependent dep, Remove the connection with the current watcher The relationship between
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      dep.removeSub(this)
    }
  }
  
  //  To respond
  update () {
    this.run()
  }
  
  //  perform 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
  }
    
  // Traverse the current Watcher All that is relevant deps, That is, with the current watcher Every relevant dep, And the current watcher Added to the dep
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  //  From all dep Remove current watcher
  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
    }
  }
}

In the above class code , Omitted some unnecessary code , In order to reduce the burden of reading . But it has been able to show Vue2 The basic structure of several classes in the observer pattern & Relationship .

The proxy pattern

Agent pattern is a kind of structural pattern in design pattern . Through proxy mode, we can base on the original object , Create a proxy object with the same interface , In the interface of the proxy object , We can do some extensible operations , But it doesn't destroy the original object .

When we need to do something about the access to the original object Control or strengthen when , You can use proxy mode .

Characteristics of agent model :

  • You can control external access to the original object , Can represent the original object , Control access to the original object through proxy objects .
  • Clear responsibilities 、 High scalability 、 Intelligent
    • Proxy objects are used to control external access to the original object
    • You can use proxy objects , Enhance or expand interface functions
  • It belongs to structural design mode
  • example : Use a virtual proxy to load pictures 、 just / Reverse proxy 、 static / A dynamic proxy 、 Property verification .

Vue3 The response is based on Proxy The agency model of . adopt To configure handler We can access the original object Control & enhance .

Vue3 The proxy pattern
Vue3 The proxy pattern

Enhanced hanlder

  • getter When an Track
    • determine target And effect The relationship between
    • determine activeEffect And Dep The relationship between
    • return value
  • setter When an Trigger
    • Get the corresponding effects, Traverse the execution effect
    • to update activeEffect
    • to update value

Through analysis , It is not difficult for us to write code with the following logic :

  • Through to target Judge , Whether proxy conversion is required
  • adopt new Proxy Yes target Acting as agent
  • Return the proxy instance to
//  To configure handler
const handlers = { 
 get(target, key, receiver) {
  const res = Reflect.get(target, key, receiver)
  // get When 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")
}

// trigger function
function trigger(target, key, value{
    //  Responsible for responding
    console.log("trigger")
}

//  Response conversion function
function createReactiveObject(target, handlers, proxyMap{
    
   // 1.  Only proxy object types
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
 
  // 2.  Judge target Whether it has been represented
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 3.  Perform proxy conversion
  const proxy = new Proxy(target,  handlers)
  
  // 4.  establish target Mapping to proxy instances , It's convenient to judge next time
  proxyMap.set(target, proxy)
    
  // 5. Returns the proxy instance
  return proxy
}

Through the graph above , We can see that ,Vue3 Dependency collection in & Response and distribution are all in handler In the do , But there are a few questions we need to determine :

  • handler How is it configured for other operation types ? such as delete、forEach.
  • For different data types , handler Are they configured in the same way ? What do you need to pay attention to ?

For the two questions above , Let's keep . We will talk about , Let's talk about .

change detection

Track: Rely on collection

New dependencies

stay Vue2 in , Dependence is watcher, stay Vue3 In the source code , I didn't find out Watcher class , Instead, a new function appears effect, It can be called side effect function . by force of contrast watcher And effect And effect Relationship with data . You can definitely call effect amount to Vue2 Medium watcher, But more than Watcher Class is more concise .

Put it here effect A brief implementation of the code , And analyze its ideas :

  • effect Accept one fn As a callback function, pass createReactiveEffect Function to cache
  • adopt options Yes effect To configure
  • perform effect In fact, it is to create a cache fn Of 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({
     //  Omitted code
    return fn()
  }
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

Where is the collection ?

stay Vue2 in , In order to maintain data and watcher The relationship between , Specifically created Dep class . And in the Vue3 in Dep Become a simple Set example . stay Track When , Current activeEffect It's stored in dep in . stay Trigger When , adopt key Get the corresponding dep aggregate , Then go through the execution .

Put it here track Short code for :

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 get dep
  let dep = depsMap.get(key)
  if (!dep) {
      
    // 2. If not, create
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
      
    // 3. Add current activeEffect to dep
    dep.add(activeEffect)
      
    // 4.activeEffect.deps Is an array , Used to maintain the current activeEffect It's different from the dep The relationship between
    //  You can see here :effect And dep It's also a many to many relationship , namely :
    //     a.  One effect There could be multiple dep in
    //     b. dep It exists in effect.deps in
    activeEffect.deps.push(dep)
  }
}

The relationship between data and dependencies

stay Vue2 in , It's through Observe、Dep、Watcher To maintain the value And watcher The relationship between .

however Vue3 There are no above classes in the . How does it maintain value And effect The relationship between ?

Let's look at a piece of code :

import { reactive } from "vue"
let count = reactive(0)
let obj = reactive({
  name" Jian Darui ",
  age18,
  beGoogAt"createBug",
  otherInfo: {
   temp1: [" Basketball "" football "" Table tennis "]
    temp2: {
      brother: [" Zhang San "" Li Si "],
      sister: [" Li Hua "" Li Li "],
    }
  }
})
//  change obj
obj.age = 27
obj.otherInfo.temp1.push(" badminton ")

When obj When the properties of change , We need to perform all related effect, Trigger response .Vue in ,state Relationship with dependency , Can be specific to the most basic key:value, Its structure and Vue2 in state And watcher The structure of is similar , Just storing state And the way of dependence :

 The relationship between data and dependencies
The relationship between data and dependencies
  • targetMap: Use WeakMap example , For maintenance targetObject And KeyToDepMap The relationship between
  • KeyToDepMap: Use Map example , For maintenance key And Dep The relationship between
  • Dep: Use Set example , Used to store all and key dependent effect
  • effect.deps: Use Array example , Used to store all current effect Of dep example

Trigger: Respond to dispatch

When we modify the data after response transformation , Will trigger Setter function , At this time, we need to do the dependent distribution work , such as DOM to update 、watch/computed Implementation .

Trigger dependency

<template>
 <div>
     {{proxy.name}}
 </div>
</template>

In the template name It's through Proxy Agent generated , When proxy.name When assigning a new value , Will trigger Setter, At this time, it needs to be updated dynamically DOM, Therefore, in Setter You can do some dependent trigger operations in . We can create a trigger function , stay setter Call in function .

Through analysis ,tigger The main function is

  • according to target、key Get all to execute effect
  • according to type operation , Make some situation judgments , Add the required traversal effect
  • Traverse the execution effets, Trigger response
function trigger(target , type , key , newValue , oldValue , oldTarget{
  // 1. according to target Get the corresponding KeyTopDepMaps
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  const effects = new Set()
  
  // 2. Responsible for effect Added to the 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. Will work with key dependent dep Pass to add,dep It stores all and key dependent effect
     add(depsMap.get(key))
  }

  // 5. Responsible for the execution of effect, At this time, the original creation... Will be executed effect when , Delivered callBack function
  const run = (effect) => {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
  // 4.  Traverse the execution effect
  effects.forEach(run)
}

function createReactiveObject(target, handlers{
 let proxy = new Proxy(target, handlers)
 return proxy
}
//  To 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" Jian Darui " }

let proxyTarget = createReactiveObject(target, handlers)

const effect = patchDOM() {
    //  Is responsible for updating DOM
}
proxyTarget.name  // "track"
proxyTarget.name = "Jiandarui" // "trigger"

Through the code example above , We can know ,Vue3 Inside , Will be in Getter In function track, stay Setter In function trigger. Above, we did not study the internal implementation of these two key functions , In the next section, let's study how the current response deals with data and dependencies ?track And trigger What are the details of the internal implementation of ?

perfect handler & track & trigger

Through to Proxy,handler、track、trigger、effect、 The relationship between dependency and data These analyses . Then we can make a simple combination , Write a simple version of responsive code

// 1. be responsible for target Mapping with dependencies 
const targetMap = new WeakMap();

// 2. establish 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({
     //  Omitted code
    return fn()
  }
  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.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()
  
  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: ${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.  To 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()
let proxy = createReactiveObject(target, handlers, proxyMap);

//  Pseudo code
const effectFn = effect(() => {
    //  Responsible for rendering components
},{lazyfalse})
  • When rendering for the first time , Will read , set out getter function , Then it will pass track Complete dependent collection
  • When the data changes , Will trigger setter function , And then it goes through trigger Function to respond

Object&Array Change detection

Object Deep proxy

stay Vue2 in ,defineReactive The function will be right. data Do recursive conversion . that Vue3 Is there such a problem in ? Let's take a look at the code first :

let obj = {
    name" Jian 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
//  Jian Darui
proxyObj.name = "jiandarui"
// set: name
// jiandarui
proxyObj.hobby.one
// get: hobby
//  Basketball
proxyObj.hobby.one = "basketball"
// get: hobby
// basketball

In the above code , We clearly passed proxyObj.hobby.one = "basketball", Endow new values , but handler Only intercepted hobby Attribute getter operation .

If obj in key Corresponding value by Object type , be Proxy Only single layer interception can be carried out . This is not what we expected .

If we encounter the following scenario :

<div>{{proxyObj.hobby.one}}</div>

When proxyObj.hobby.one After the change , We expect DOM updated . because proxyObj Only single-layer agents were performed ,hobby No way Proxy Change to responsive . Will cause the update to fail .

that Vue3 How to solve it ?

The answer is : Do recursive proxy , The idea is similar to Vue2 similar , By judgment value The type of , Then perform response conversion .

Here we need to rewrite getter function ,

  • stay get Judge when you get it value Is it Object
  • If it is Object, Then do it again Reactive agent
let handler = {
    get(target, key, receiver) {
        //  Omitted code .....
        
     const res = Reflect.get(target, key, receiver)
     if (isObject(res)) {
            //  If it's an object type , Then carry out deep conversion
        createReactiveObject(res)
     }
     return res
   },
    set(target, key, value, receiver) {
        console.log(`set:${key}`)
        Reflect.set(target, key, value, receiver)
    }
}

Array track&trigger The problem of

Vue3 Although there is no array interceptor in , But there is another problem , Let's look at a piece of code :

let arr = [1,2,3]
let handler = {
   getfunction(target, key, receiver{
       console.log(`get:${key}`)
       return Reflect.get(target, key, receiver)
   },
   setfunction(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

Demonstrate through the above operations , We can find a simple operation , May trigger multiple Getter Function or Setter function , If this operation is in the ordinary business development process, there may be no problem , But in Vue3 May lead to dead recursion .

push&pop&shift&unshift&splice These methods can add and delete the array . It should be noted that :

  • These methods directly modify the original array
  • And will cause the array length Changes in attributes

includes&indexOf&lastIndexOf These three methods are arrays used to determine whether there is a value to find , It should be noted that :

  • These three methods are implemented in the array method Traverse array
*

Interested students can see the relevant issue: Portal 1 (opens new window) Portal 2(opens new window)

When you pass watchEffect When looking at an array , Dead recursion happened

*

Through the rules printed above , You can find :

  • Every time a method is called , Will trigger first get, The final trigger set

Create array Instrumentations

*

Comment area , Please advise me ,vue3 Medium Instrumentations How to translate ?(*◡‿◡)

*

Can we make a state manager , By judging whether track, To avoid unnecessary track and trigger

  • adopt hander Medium get Function interception array The method on the
  • Encapsulate the methods on the prototype , Before and after getting the results , Do it separately track Pause and reset , adopt trackStack Record shouldTrack
  • After getting the results , It's going on track

Let's look at the processed code , Be careful The order of notes

function createReactiveObject(target, handlers{
  let proxy = new Proxy(target, handlers)
  return proxy
}
const targetMap = new WeakMap()

function track(target, key{
  if (!shouldTrack) {
    return
  }
  console.log('-------track-------')
  //  Omitted code
}

function trigger(target, key, newValue, oldValue{
  console.log('trigger')
}

const handlers = {
  get(target, key, receiver) {
      
    // 1.  Get the results first
    //  Be careful : here Reflect What is passed is processed  arrayInstrumentations
    const res = Reflect.get(arrayInstrumentations, key, receiver)
    
    // 5.  Again track
    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
  },
}

//  Intercept and encapsulate the methods on the array prototype
const arrayInstrumentations = {}
;['push''pop''shift''unshift''splice'].forEach((key) => {
  const method = Array.prototype[key]
  arrayInstrumentations[key] = function (thisArgs = [], ...args{
      
    // 2.  Pause track
    //  Start with the last shouldTrack state push to trackStack
    // shouldTrack = false, Not trigger track
    pauseTracking()
      
    // 3. To get the results
    const res = method.apply(thisArgs, args)
    
    // 4.  recovery track
    // shouldTrack Take the last state
    resetTracking()
    return res
  }
})

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

let arr = [123]
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

Shallow response conversion

Mentioned earlier , For multiple layers Object, Deep proxy through recursion is required . But in some scenarios, we want responsive objects to only need shallow proxy . This requires :

  • rewrite get function :
    • Create a createGetter Function is used to pass parameters
    • Use JS The closure of , cache shallow, return get function
    • get The function passes through shallow Judge whether you need to be right res Again reactive
  • rewrite set function :
    • targetObject Shallow response , When targetObject There is no need to set a new value when the internal property changes ,
const shallowReactiveHandlers = {
  get(target, key, receiver) {
    // reactiveMap、shallowReactiveMap yes weakMap example , Used for mapping target With proxy instances
    if (receiver === shallowReactiveMap.get(target)) {
      return target
    }

    const res = Reflect.get(target, key, receiver)
 //  There is no need to judge res The type of
    return res
  },
  set(target, key, value, receiver, shallow) {
    let oldValue = target[key]
    if (!shallow) {
      value = toRaw(value)
      oldValue = toRaw(oldValue) 
      oldValue.value = value
      return true
    } else {
      // shallow by true, No matter targetObject Is it responsive , No longer update settings oldValue Update
    }

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

Read only response conversion

In some scenarios, we want responsive objects to be readable and unchangeable , We need to configure a system that only performs read-only response conversion handler;

  • When a change occurs , Can be in handler Intercept modification operations in , If triggered, an exception is thrown directly .
  • because targetObject Is read-only , There is no need to do it again track. stay get Function to determine whether track
export const readonlyHandlers = {
  getget(target, key, receiver) {
    //  Yes target and key Do judgment
    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  by  true  No longer  track
    //  Comment out  track operation :
    /* track(target, TrackOpTypes.GET, key) */ 

    if (isObject(res)) {
      return readonly(res)
    }
    return res
  },
  //  Modify the operation to warn directly   Or ignore
  set(target, key) {
    if (__DEV__) {
      console.warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  deleteProperty(target, key) {
    if (__DEV__) {
      console.warn(
        `Delete operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  }
}

Shallow read-only response conversion

In the same way , Shallow read-only response conversion :

  • It's not right targetObject Make a deep conversion
  • Intercept modification operations , Give a warning directly or ignore

Code up :

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 "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  deleteProperty(target, key) {
    if (__DEV__) {
      console.warn(
        `Delete operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  }
}

Organize and refactor

Look back at the code above , We find that there are a lot of redundant operations in the code , It is very necessary to sort it out :

  • each handler in , The configured functions are the same , It's just that you may think it's about demand , Logical changes have been made inside the method
  • Duplicate code in method : get function 、 set There are multiple duplicate codes in the code of the function
    • get The main function of the function is to get value, Conduct track
    • set The main function of the function is to set value, Conduct trigger
    • There's no need for everyone to handler Repeat the core functions of the two functions

refactoring :

  • adopt createGettercreateSetter Function creation getset function , Use closures , Functions that get different properties by passing parameters
  • Use the different methods created to combine handler
//  Used for processing target And proxy Mapping between 
const reactiveMap = new WeakMap()
const shallowReactiveMap = new WeakMap()
const readonlyMap = new WeakMap()
const shallowReadonlyMap = new WeakMap()

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 = value
        return true
      }
    } else {
      // shallow by true, No matter targetObject Is it responsive , No longer update settings oldValue Update
      
    }

    const result = Reflect.set(target, key, value, receiver)
   
   if (hasChanged(value, oldValue)) {
      //  Judge value Is there a change , Proceed again 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 goals  Handler
export const mutableHandlers = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

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

const shallowReactiveHandlers= extend(
  {},
  mutableHandlers,
  {
    get: shallowGet,
    set: shallowSet
  }
)

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

Map&Set Change detection

We learned from Object & Array Of handler Configuration and implementation of each method . Same logic , Can also be applied to Map、Set Data of type :

  • Split and process each method
  • Configure... As needed handler

Represented Map、Set example

Go straight to the code , Let's see map、set after proxy After agency , What happens when you operate ?

Represented map

let map = new Map([[12], [34], [56]]);
let mapHandler = {
  get(target, key, receiver) {
     console.log(`key: ${key}`//  Conduct  for of  When traversing, you need to comment out
    if(key === "size") {
      return Reflect.get(target, "size", target)
    }
    var value = Reflect.get(target, key, receiver)
    //  see  value  type
    console.log(typeof value)
      
    //  Be careful : Here we need to pay attention to change value Of this Point to
    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);
// size  attribute
console.log(proxyMap.size) 
//  Output :
// key: size  
// 3

// get  Method
console.log(proxyMap.get(1))
//  Output :
// key: get
// value: function
// 2

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

// has  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  Method
console.log(proxyMap.keys())
//  Output
// key: keys
// value: function
// MapIterator {3, 5, "name"}

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

// entries  Method
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

//  amount to entries()
//  Need comment  console,  Otherwise throw  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"

Represented set

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: ${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 {2345"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"}
      
//  amount 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
  • The output above appears because proxy The agent is Map、Set Example
  • We call the method on the instance , Will trigger the access method getter function
  • so Map & Set Type of targetObject, Cannot be used with Object & Array same handler
  • Need to be for Map & Set Create method , And configuration handler

Additions and deletions

  • map & set Type to be configured handler And Object & Array identical : Response type 、 Shallow 、 read-only 、 Shallow read only
  • Instance methods need to be handled independently
  • Design thinking
    • Because instance methods trigger method access first getter function
    • So the configuration handler The object only needs to have one getter function
    • stay get Function internal interception method
    • Do... Inside the method track/trigger Work
    • adopt map/set The original way to get value, And back to

Pre knowledge supplement :

  • Vue3 For each converted object , Set up a ReactiveFlags.RAW attribute
  • The value is the original targetObject
  • toRaw function
    • return reactive or readonly The original of the agent targetObject
    • It can be used to temporarily read data without proxy access / Tracking overhead
    • It can also be used to write data to avoid triggering changes
    • principle : By recursion , Take off the proxy, Original value found
function toRaw(observed{
  return (
    (observed && toRaw((observed)[ReactiveFlags.RAW])) || observed
  )
}
  • Different configurations handler, We need to use the corresponding response conversion function to deal with 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)
*

Let's go straight to Vue3 reactive The source code in , Of course, the code is omitted , But the core logic will be preserved .

First understand the main ideas , Paying attention to details .

*
  • get function
    • be used for Map Examples are based on key obtain value`
    • `map Type of key It can be a reference type , It may also be a proxy instance
    • target & key All need to be done toRaw operation
    • The root whisker parameter is required to judge whether it is necessary to perform track, Collect related dependencies
    • You need to call the original target Of get Method to get value
    • Finally, you need to return the proxy value
function get(target, key, isReadonly = false,isShallow = false{
  //  Get the original object , original key
  target = target[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  
  /**
  *  I go through it myself  looseEqual function   Contrast  target[ReactiveFlags.RAW]  And  rawTarget.
  *  return true, There is no difference between the two . But I don't understand why Youda designed it like this , I hope you can give me some advice
  */

  
  // key  change , track key
  if (key !== rawKey) {
    !isReadonly && track(rawTarget, TrackOpTypes.GET, key)
  }
    
  // track rawKey
  !isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
  const { has } = getProto(rawTarget)
  
  //  Obtain the corresponding conversion function according to the parameters
  const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
  
  //  Returns the proxy result
  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)
  }
}

  • set function
    • be used for Map Type add element
    • map Type add element , It may be new key:value, It could be a modification
    • Need to be right key Whether there is a judgment
    • set The function will change target, Need to carry out trigger, Trigger response
function set(this, key, value{
  //  Take off the proxy, Get the original value & target
  value = toRaw(value)
  const target = toRaw(this)
  const { has, get } = getProto(target)
  
  //  Judge  key Does it exist in target
  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target, key)
  }
    
  //  Get old value
  const oldValue = get.call(target, key)
  
  //  Set up  key: vlaue
  target.set(key, value)
    
  //  call trigger, Trigger response
  if (!hadKey) {
      
    //  Not before key, What you are doing is adding
    trigger(target, TriggerOpTypes.ADD, key, value)
  } else if (hasChanged(value, oldValue)) {
      
    //  Pre existing key, Setting operation is performed
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
  return this
}
  • has function
    • Can be used for Set/Map Instance to determine whether an element exists
    • has Property does not change targetObject,
    • Belong to right target Access operation of
    • Need to be right key Conduct track operation

function has(this, key, isReadonly = false{
  //  Get the original target key
  const target = (this)[ReactiveFlags.RAW]
  const rawTarget = toRaw(target)
  const rawKey = toRaw(key)
  
  //  According to judgment track
  if (key !== rawKey) {
    !isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
  }
  !isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)
  
  //  Calls to the original target Of has Methods to judge key Whether there is
  return key === rawKey
    ? target.has(key)
    : target.has(key) || target.has(rawKey)
}
  • add function
    • be used for Set Add element to instance , The added element is unique
    • If you add value It didn't exist before , Will make target A change has taken place , Need to carry out trigger operation , Trigger 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 judge value Whether there is
  //  because set Added value It's all unique
  //  So only  key  When it doesn't exist, you need  trigger
  const proto = getProto(target)
  const hadKey = proto.has.call(target, value)
  if (!hadKey) {
    //  Additive elements , call trigger function , Trigger response
    target.add(value)
    trigger(target, TriggerOpTypes.ADD, value, value)
  }
  return this
}

  • size attribute
    • For return Set/Map The total number of members of
    • Not trigger target The change of
    • But it needs to be done track
function size(target, isReadonly = false{
  //  Get the original target
  target = target[ReactiveFlags.RAW]
  //  If not read-only   Is to track
  !isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
  //  Return results
  return Reflect.get(target, 'size', target)
}
  • clear function
    • Used to remove Set/Map All members of the instance
    • Will change target, Need to call trigger function
function clear(this{
  //  Get the original object
  const target = toRaw(this)
  const hadItems = target.size !== 0
  const oldTarget = __DEV__
    ? isMap(target)
      ? new Map(target)
      : new Set(target)
    : undefined
  //  Call before trigger response clear
  const result = target.clear()
  if (hadItems) {
    //  If the original object is not empty , You need to trigger
    trigger(target, TriggerOpTypes.CLEAR, undefinedundefined, oldTarget)
  }
  return result
}
  • delete function
    • Used to delete Set/Map A member of an instance
    • Will change the original target, need trigger
function deleteEntry(this, key{
    
  //  Get the original target
  const target = toRaw(this)
  const { has, get } = getProto(target)
  
  //  Judge key  perhaps   original key Whether there is
  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 execution results  
  const result = target.delete(key)
  if (hadKey) {
      
    //  If the member previously existed , be trigger
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

Traversal pattern & Iterative mode

  • forEach function
    • It belongs to the ergodic pattern in the design pattern
    • Used to traverse the Set/Map example , Accept a callback , Will key & value Pass to callback
    • For different response interfaces , We create... Based on judgment forEach
    • We can go through the parameters isReadonly, isShallow Get different forEach
    • forEach Function does not change the original object , Just do it track Work
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 according to the parameters
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    
    //  Non read only   Conduct track
    !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
    
    //  Call... On the original object forEach  Method , take value & key Pass to callback
    return target.forEach((value, key) => {
        
      //  Pass on value & key Convert when
      return callback.call(thisArg, wrap(value), wrap(key), observed)
    })
  }
}
  • iterable Iterator pattern
    • Map/Set There are three methods for the example of : keys()、values()、entries()
    • All three methods follow an iterative protocol & Iterator protocol
    • Vue3 These three methods are also handled internally
    • By implementing the iterator itself , Simulate these three methods
    • Inside the iterator created , Get... By calling the method of the original object result
    • Conduct track, Also on result Response processing
    • Finally, in the returned iteratable object , Returns the response converted result
    • The main logic has been identified in the comments of the following code
function createIterableMethod(method, isReadonly, isShallow{
  return function(this, ...args{
      
    //  Get the original object
    const target = (this)[ReactiveFlags.RAW]
    const rawTarget = toRaw(target)
    
    //  Determine whether the original object is Map type
    const targetIsMap = isMap(rawTarget)
    
    // entries()  What is returned is an iteratable two-dimensional array
    const isPair =
      method === 'entries' || (method === Symbol.iterator && targetIsMap)
    
    const isKeyOnly = method === 'keys' && targetIsMap
    
    //  Gets the internal iterator returned by the original object method
    const innerIterator = target[method](...args)
    
    //  Obtain the corresponding conversion function according to the parameters
    const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
    
    //  Non read only , Conduct track
    !isReadonly &&
      track(rawTarget, 
            TrackOpTypes.ITERATE, 
            isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY 
           )
    
  //  Returns an iteratable object
    return {
      //  Follow the iterator protocol
      next() {
        //  Call the internal iterator to get the result
        const { value, done } = innerIterator.next()
        return done
          ? { value, done }
          : {
              //  Yes value Make a responsive conversion
              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
              done
            }
      },
      //  Follow an iterative protocol
      [Symbol.iterator]() {
        return this
      }
    }
  }
}

const iteratorMethods = ['keys''values''entries'Symbol.iterator]
//  Traverse 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
  )
})
*

Students interested in iterators or iterative protocols can click the following link

iterator protocol :https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols

iterator :https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Iterators_and_Generators

iterator :https://es6.ruanyifeng.com/#docs/iterator

*

establish Handler

Above, we have understood the creation of various methods , The next step is to do something similar Object & Array Work of the same type —— Configure different handler.

//  For read-only operations that trigger changes , We all use it createReadonlyMethod Method creation 
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(thistrue)
  },
  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(truefalse)
}

const shallowReadonlyInstrumentations = {
  get(this, key) {
    return get(this, key, true, true)
  },
  get size() {
    return size((thistrue)
  },
  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(truetrue)
}

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
  )
})
//  establish getter function
function createInstrumentationGetter(isReadonly, shallow{
  const instrumentations = shallow
    ? isReadonly
      ? shallowReadonlyInstrumentations
      : shallowInstrumentations
    : isReadonly
      ? readonlyInstrumentations
      : mutableInstrumentations
  
  // 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
    }
    
    //  Pay attention here to Reflect.get The first parameter passed
    // instrumentations  We created it  【 The dashboard 】( As soon as it's translated, it tastes bad ~)
    // instrumentations Method  this  It's called get Function context
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

//  To configure Handler
const mutableCollectionHandlers = {
  get: createInstrumentationGetter(falsefalse)
}

const shallowCollectionHandlers = {
  get: createInstrumentationGetter(falsetrue)
}

const readonlyCollectionHandlers = {
  get: createInstrumentationGetter(truefalse)
}

const shallowReadonlyCollectionHandlers = {
  get: createInstrumentationGetter(truetrue)
}


Finally, let's summarize the above process with two more pictures !

baseHandlers

baseHandlers
baseHandlers

collectionHandlers

collectionHandlers
collectionHandlers

API Realization principle

reactive

We already know how to configure Handler Next, create different response conversion functions , To create a function is to Proxy, Different configurations handler. Let's rewrite the proxy pattern mentioned in the first chapter createReactiveObject function :

The response function written in the first chapter

//  Response conversion function 
function createReactiveObject(target, handlers, proxyMap{
    
   // 1.  Only proxy object types
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
 
  // 2.  Judge target Whether it has been represented
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 3.  Perform proxy conversion
  const proxy = new Proxy(target,  handlers)
  
  // 4.  establish target Mapping to proxy instances , It's convenient to judge next time
  proxyMap.set(target, proxy)
    
  // 5. Returns the proxy instance
  return proxy
}

rewrite createReactiveObject

  • take handlers Pass as parameter to function
  • Through internal judgment target The type of , To configure handlers

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
    
  //  If target Has been returned directly through the proxy
  //  after readonly() The conversion targetObject exception
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
    
  //  If target There are already corresponding agents   Go straight back to
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
    
  //  Only those on the white list  targetType  You can go through an agent
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
    
  //  according to targetType  to Proxy Pass on different handler
  // Map & Set Type transmission collectionHandlers
  // Object & Array Type transmission  baseHandlers
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  
  //  preservation target  And  proxy The relationship between , Convenient for next judgment
  proxyMap.set(target, proxy)
  return proxy
}

reactive

  • Returns a responsive copy of an object
  • Yes target Conduct “ Deep level ” agent , Affect all nested property
function reactive(target{
  //  If target Is read-only , Go straight back to target
  if (target && (target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

shallowReactive

  • Create a responsive proxy , Only track itself property Responsiveness
  • But do not perform deep, responsive transformations of nested objects
function shallowReactive(target{
  return createReactiveObject(
    target,
    false,
    shallowReactiveHandlers,
    shallowCollectionHandlers,
    shallowReactiveMap
  )
}

readonly

  • Accept an object ( Responsive or pure object ) or ref And returns the read-only proxy of the original object .
  • Read only agents are deep : Any nested accessed property It's also read-only .
function readonly(target{
  return createReactiveObject(
    target,
    true,
    readonlyHandlers,
    readonlyCollectionHandlers,
    readonlyMap
  )
}

shallowReadonly

  • Create a proxy, Make its own property As read-only
  • But do not perform deep read-only conversion of nested objects
export function shallowReadonly(target{
  return createReactiveObject(
    target,
    true,
    shallowReadonlyHandlers,
    shallowReadonlyCollectionHandlers,
    shallowReadonlyMap
  )
}

isReactive

  • Check whether the object is created by reactive Create a responsive proxy for
  • Can be in observed Stage to target Set up a [ReactiveFlags.RAW]/[ReactiveFlags.IS_REACTIVE] attribute
function isReactive(value{
  if (isReadonly(value)) {
    return isReactive((value)[ReactiveFlags.RAW])
  }
  return !!(value && (value)[ReactiveFlags.IS_REACTIVE])
}

isReadonly

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

isProxy

  • Check whether the object is created by reactive or readonly Created proxy
  • The interior is actually called isReactive | isReadonly Method to judge
function isProxy(value: unknown): boolean {
  return isReactive(value) || isReadonly(value)
}

toRaw

  • I've said that before , I won't repeat

markRaw

  • Mark an object , Make it never convert to proxy. Returns the object itself .
  • principle : to v alue Define a ReactiveFlags.SKIP Property and set the value to true
  • stay reactive Judge this attribute when
function markRaw (value{
  def(value, ReactiveFlags.SKIP, true)
  return value
}

ref

Study Vue3 when , There is a question : There is already one reactive Function , Why is there another ref Well ? Later, I saw the source code to understand part of the reason .

stay reactive in , We can't convert the base type , Only object types can be proxied . And how to implement the proxy for the basic type ?

Create a Ref Class. Implement... Through class instances get & set Intercept operation of .

*

ref It can be used for primitive type and object type conversion ,ref make up reactive The conversion of the base type should be part of the reason , In particular, there should be other high-level considerations and designs . I just can't see it for the time being ~

*

Ref Class The implementation principle of is not complicated , We can directly look at its source code :

//  By judgment value convert 
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 by true, incorrect value convert
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }

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

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

//  Factory function   Used to create ref example
function createRef(rawValue, shallow = false{
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

With createRef function , The next step is to create API

ref

  • Accept one value And returns a responsive and variable ref object .
  • ref Object has a single... That points to an internal value property .value
function ref(value{
  return createRef(value)
}

shallowRef

  • Create a tracking self .value Changing ref, But it doesn't make its value responsive .
function shallowRef(value{
  return createRef(value, true)
}

isRef

  • Judge value Is it ref object
  • Notice above that we are RefImpl Class A read-only property set in __v_isRef
function isRef(r{
  return Boolean(r && r.__v_isRef === true)
}

toRef

  • For a... On the source responsive object property Create a new ref
  • When it is necessary to prop Of ref When passed to a composite function , toRef It 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))
}

toRefs

  • Convert a responsive object to a normal object , Where each property They all point to the original object property Of ref.
  • When a responsive object is returned from a composite function , Deconstructing responsive objects is very useful
  • principle : Traversal of responsive objects , call toRef Convert each pair key: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
}

customRef

  • Create a custom ref, And explicitly control its dependency tracking and update trigger .
  • You need a factory function as an argument , This function receives track and trigger Function as parameter
  • And it should return a message with get and set The object of
class CustomRefImpl {
  private readonly _get: ['get']
  private readonly _set: ['set']

  public readonly __v_isRef = true

  constructor(factory) {
    //  Pass... To the factory function  track function  & trigger function
    //  Get the returned  get set function
    const { getset } = 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)
  }
}
*

Digression : When I looked at the design of this code , It feels so clever

*

API Realization

function customRef(factory){
  return new CustomRefImpl(factory)
}

triggerRef

  • Manual execution and shallowRef Any role of Association (effect)
  • The internal is actually manual trigger
function triggerRef(ref{
  trigger(toRaw(ref), TriggerOpTypes.SET, 'value', __DEV__ ? ref.value : void 0)
}
Ref
Ref

We can talk effect( important !!!)

When we talked about change detection earlier , Let's briefly say effect function . But it's not true effect Analyze the execution process in the whole response . This time we must make up , Because if it's different effect Implementation process of , It's hard to understand computed Principle .

stay vue3 There will be four levels of effect

  • Responsible for rendering updates componentEffect
  • Responsible for handling watch Of watchEffect
  • Responsible for handling computed Of computedEffect
  • Users use it by themselves effect API Created when effct

This time we mainly say setupRenderEffectcomputedEffect.

Here's a piece 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})
    
    //  Will trigger  computedEffect &  Below effect Re execution
    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>

In the browser , From the above template code to rendering to a page that can respond to interaction ,Vue I'll probably do a few things :

  • perform setup function , take state Change to responsive , And mount the result to the component instance
  • On the template compiler, And render to the view
  • stay compiler In the process , Meeting Read responsive data , The process of reading is triggered ** getter function , It will Rely on collection ** Work
  • When entering data in a form , It will trigger update event , change input, The change process is triggered ** setter function , It will Respond to updates **

Next, let's look directly at how to design in the simplified source code trigger Functional :

  • trigger The function is mainly to get and target All of the associated effect
  • Will need to traverse effect, Add to the... To be traversed set aggregate ( It's going to be weightless )
  • Traverse set, To perform all effect, Trigger response .
  • Combined with the example code :
    • When used in input When entering in the box ,
    • Will execute update function , Yes input Make changes
    • Trigger trigger, Collect all information related to input dependent effects, Traverse the execution effects
    • It will be implemented at this time componentEffectcomputedEffect、 User defined effect
function trigger (target, type, key, newValue, oldValue, oldTarget{
    const depsMap = targetMap.get (target);
    
    //  There are no related dependencies   return
    if (!depsMap) { 
      return;
    }
    const effects = new Set ();
    
    //  Add the... To be traversed  effect
    const add = effectsToAdd => { 
      if (effectsToAdd) {
        effectsToAdd.forEach (effect => {
          if (effect !== activeEffect || effect.allowRecurse) {
            effects.add(effect);
          }
        });
      }
    };
    
    if (key !== void 0) {
       add (depsMap.get (key));
    } 
    
    //  Traversal callback function
    const run = effect => {
      if (effect.options.scheduler) {
        effect.options.scheduler (effect);
      } else {
          
        //  perform effect function
        //  Will execute the creation effect Function passed fn function
        effect();
      }
    };
    
    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 effect And dep The relationship between
    if (!dep.has (activeEffect)) {
      dep.add (activeEffect);
      activeEffect.deps.push (dep);
      if (activeEffect.options.onTrack) {
        activeEffect.options.onTrack ({
          effect: activeEffect,
          target,
          type,
          key,
        });
      }
    }
  }

Then execute effect The process of , What will happen ? We then interpret it in combination with the source code and sample code :

  • effectStack Stack is mainly used to maintain effect And dep Nested relationship between .
  • Is responsible for updating activeEffect.( May have a look track The function is the same as what we said earlier effect It's different from where you are dep The relationship between )
  • enableTracking & resetTracking Functions are used to control track The state of
  • because effect The execution process will also be track Work . At this time, you need to judge whether it needs to be the current state And effects Building dependencies
  • return fn() Result , perform finally Code in , Pop up the current effect, Update next effect
  • Combined with the example code, we analyze :
    1. When entering a number in the input box 5, input A change has taken place
    2. callback Function is put on the stack first ==》 effectStack state : [callback]==》 callback perform ==》 visit input.value ==》 Conduct track, Print input.value: 5==》callback` Function stack
    3. componentEffect Push ==》 effectStack state : [componentEffect ]==》 to update input Internal value ==》 Conduct track==》 to update div, Need to call computed Of getter Function to get the value
    4. computedEffect Push ==》 effectStack state : [componentEffect ,computedEffect ]==》 At present activeEffect yes computedEffect ==》 Conduct track==》 obtain ``getter Value ==》computedEffect Out of the stack ==》 Conduct track`
    5. componentEffect Out of the stack ==》 effectStack state : At present activeEffect yes componentEffect
    6. Complete the whole 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)) {
        //  First, you need to effect In its place deps Remove
        cleanup (effect);
        try {
          //  recovery track
          enableTracking ();
          //  Push
          effectStack.push(effect);
          //  Set up current effect
          // fn The process of execution will be maintained activeEffect And dep The relationship between
          activeEffect = effect;
            
          //  perform fn
          //  there fn It can be componentEffect function 、 establish watch Delivered on callback、computedEffect Of getter function
          //  It can also be used by users effect API  Delivered callback function
          //  These functions are probably internal to state Visit , The process of execution is triggered getter, That is to say track, Do dependency collection
          return fn ();
        } finally {
          //  Out of the stack , Pop up current effect
          effectStack.pop();
          //  Reset 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
  }

The above process may be around , But it's regular , It should be noted that :effect The process of execution , Will execute fn,fn It is likely to trigger track, Do dependency collection , and effecStack, Mainly maintenance effect Update order between , Always update the last stack first effect.

I see. Above effects Implementation process of , It is convenient to understand the following computed 了 .

computed

Say computed, Let's first think about how to use :

  • You can give computed Pass a getter function ,
  • It will be based on getter The return value of , Returns an immutable response ref object .
const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ //  error
  • perhaps , Accept one with get and set Object of function , Used to create writable ref object .
const count = ref(1)
const plusOne = computed({
  get() => count.value + 1,
  setval => {
    count.value = val - 1
  }
})

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

You all know computed What's powerful is lazy updates , Only when the value on which it depends changes , To update . In fact, the above analysis effect The process has been , Cash out . When its dependent value changes , Responsible for rendering compontentEffect Function will call computedEffect Get the new value .

computed Mainly based on dirty To update the value . When dirty by true when , Indicates that the return value needs to be recalculated , When dirty by false when , Note that the return value does not need to be recalculated .

When a data rendered view is used in the template , If this data is computed Produced , Then the operation of reading data is actually triggering the calculation of attributes getter Method to get value, After getting, the value of the property will be calculated _dirty Attribute is set to false, Until the data that the attribute depends on changes next time , To change .

When we create a computed Attribute , In fact, it is configured with lazy by true, And there are scheduler Attribute effect.computedEffect Of scheduler Mainly responsible for resetting _dirty attribute . And trigger trigger.

When the data in the template changes , Will trigger trigger, To respond , Carry out all the effects, If effect.scheduler There is , execute effect.scheduler function , Reset _dirty. Responsible for rendering componentEffect When re reading the value of the calculated attribute , Would call computed Of getter Method . At this time dirty by true, Return to new value after ,dirty Set as false.

Next, let's look at the implementation of the simplified source code :

//  establish 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 new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  )
}


// computed Class
class ComputedRefImpl {
  private _value! 
  private _dirty = true

  public readonly effect
  
  //  Response declared read-only ref object
  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]

  constructor(
    getter,
    readonly _setter,
    isReadonly
  ) {
    // 
    this.effect = effect(getter, {
      // lazy by true, Not immediately  getter function
      lazytrue,
      
      //  stay trigger Function   Finally, effects Traversal
      //  perform run function , If effect.option.scheduler Functions exist
      //  Will execute  scheduler  function
      scheduler() => {
        
        if (!this._dirty) {
          //  Reset _dirty
          this._dirty = true
          
          //  Respond to dispatch
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })

    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    
    const self = toRaw(this)
    if (self._dirty) {
        
      //  perform effect The perform getter, Get new value
      self._value = this.effect()
      // _dirty  Set as false
      self._dirty = fa
        lse
    }
    // track  Rely 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)
  }
}

The above code has been annotated to indicate the main logic of the code , combination effect Execution logic , It is not difficult to understand when analyzing computed 了 .

summary

So far we have finished the analysis Vue3 reactive Source code analysis , In the new responsive structure ,Vue adopt Proxy To intercept state Of getter perhaps setter operation . adopt effect Complete dependent collection . By configuring different handler object , Create responses to different data types and different response levels API.

*
  • If there are any mistakes in the article , I hope you will criticize and correct me . Dari, thank you very much .

  • If you have any questions during reading , You can leave messages in the comment area or my official account. , I will answer patiently

  • It feels good after reading , It does help you , A compliment is my biggest encouragement

*

Reference resources :

  • https://github.com/vuejs/vue-next
  • https://v3.vuejs.org/
  • 《 Explain profound theories in simple language Vue.js》

版权声明
本文为[Jian Darui]所创,转载请带上原文链接,感谢
https://cdmana.com/2021/11/20211109094851834c.html

Scroll to Top