Chapter four is the implementation of responsive systems

4.1 Reactive data and side effect function

  • effectThe execution of a function directly or indirectly affects the execution of other functions, we sayeffectThe function has side effects.
    • Modified global variables
    • It’s called reactive modificationobj.textIn the hope that the side effect function will be re-executed
const obj = { text: 'hello world' }
function effect(){
  // Effect reads obj.text
  document.body.innerText = obj.text
}

obj.text = 'hello vue3'
Copy the code

4.2 Basic implementation of responsive data

  • Basic implementation of responsiveness
    • When the side effect functioneffectWhen executed, the field is firedobj.textthereadOperation.
    • When modifyingobj.textIs used to trigger the fieldobj.texttheSet up theOperation.

  • Implementation approach
    • Create a bucket
    • Read time willeffectIn the bucket
    • Set toeffectTake out the bucket and execute
    • Proxy to Proxy objects, so as to achieve the simplest response
// The bucket that stores the side effects function
const bucket = new Set(a)// Raw data
const data = { text: 'hello world' }
// Proxy for raw data
const obj = new Proxy(data, {
  // Intercepts read operations
  get(target,key){
    // Add the effect function to the function bucket
    bucket.add(effect)
    // Returns the value of the attribute
    return target[key]
  },
  
  // Intercepts setup operations
  set(target,key,newVal){
    // Set the property value
  	target[key] = newVal
		// Remove the side effect function from the bucket and execute it
    bucket.forEach(fn= > fn())
    // If true is returned, the setup succeeded
    return true}})Copy the code
  • Drawback: We get the side effect function directly by the name effect, which is hard coded.

4.3 Design a perfect responsive system

  • Fix the flaw in the previous section: First fix the custom function name we passed in
  • Workaround: Use a global variable to store the registered side effect function
// Use a global variable to store the registered side effect function
let activeEffect
// the effect function registers the side effect function
function effect(fn){
  // Assign the side effect function fn to activeEffect when effect registers the side effect function
  activeEffect = fn
  // Execute the side effect function
  fn()
}
Copy the code
  • Further, you can pass in an anonymous side effect function as followseffectuse
effect(() = > {
  // An anonymous side effect function
  () = > {
    document.body.innerText = obj.text; }})Copy the code
  • In this way, we are rightProxyThe object of the proxy is modified
const obj = new Proxy(data,{
  get(target,key){
    // Collect the side effect functions stored in activeEffect into the bucket
    if(activeEffect){ / / new
      bucket.add(activeEffect)/ / new
    }/ / new
    return target[key]
  },
  set(target,key,newValue){
    target[key] = newVal
    bucket.forEach(fn= > fn())
    return true}})Copy the code
  • Thus, we have found that the function works in a small number of cases, but if we test the function a little bit. For example, inobjTo set a property that does not exist. See the code below:
  • As you can see from the code, the fields are read inside the anonymous side effect functionobj.textValue, in this case anonymous side effect function with fieldobj.textbetweenEstablish response linkage. However,obj.notExistThe nonexistent property is deferred and no connection is established with the anonymous side effect functionResponse to contact.butThe side effect function is also executed (which we don’t want to see).
effect(() = > {
  // Anonymous side effect function
  () = > {
    console.log('effect run') // Prints twice
    document.body.innerText = obj.text
  }
})

setTimeout(() = > {
  // The side effect function does not read the value of the notExist attribute
  obj.notExist = 'hello vue3'},1000)
Copy the code
  • To solve the above problem: Just establish an explicit relationship between the side effect function and the target field being manipulated.
  • Think about the data structure
    • The proxy object obj that is being manipulated (read)
    • The name of the field being operated on (read from) text
    • useeffectFunction registers the side effect function effectFn
target
		|
		|_________ key
								|
								| _________ effectFn
Copy the code
  • Let’s first look at the data structure diagram of the implementation result:

  • On second thought, we useWeakMap to replace old bucketswithMapTo storekeywithSetTo store the side effect functioneffect
// Bucket A bucket for storing side effects functions
const bucket = new WeakMap(a)const obj = new Proxy(data, {
  // Intercepts read operations
  get(target,key){
    // No activeEffect, return
    if(! activeEffect)return
    // Get depsMap from the bucket according to target, which is also a Map, key > effect
    let depsMap = bucket.get(target)
    // If depsMap does not exist, create a Map and associate it with target
    if(! depsMap) { bucket.set(target,(depsMap =new Map()))}// Retrieve the deps from depsMap based on the key, which is a Set type
    // This contains all the side effects associated with the current key: effects
    let deps = depsMap.get(key)
		if(! deps){// If deps does not exist, create a new Set and associate it with the key
      depsMap.set(key,(deps = new Set()))}// Finally add the currently active side effect function to the bucket
    deps.add(activeEffect)
    
    return target[key]
  },
  // Intercepts setup operations
  set(target,key,newValue){
    // Set the property value
    target[key] = newVal
    // Get depsMap from target, which is key --> effects
    const depsMap = bucket.get(target)
    if(! depsMap)return 
    // Get all effects according to key
    const effects = depsMap.get(key)
    // Execute the side effect function
    effects && effect.forEach(fn= > fn())
  }
})
Copy the code
  • One more step: to further optimize the code, we package it astrackThe function expressiontracking.triggerUsed to set interceptor functions to trigger changes.
  • The encapsulated code is as follows:
const obj = new Proxy(data,{
  // Intercepts read operations
  get(target,key){
    // Add the side effect function activeEffect to the function bucket that stores the side effects
    track(target,key)
    // Returns the value of the attribute
    return target[key]
  },
  // Intercepts setup operations
  set(target,key,newValue){
    // Set the property value
    target[key] = newValue
    // Take the side effect function out of the bucket and execute it
    trigger(target,key)
  }
})

// Call track within get interceptor to track changes
function track(target,key){
  // 没有activeEffect,直接return
  if(! activeEffect)return 
  let depsMap = bucket.get(target)
  if(! depsMap){ bucket.set(target,(depsMap= > new Map()))}let deps = new depsMap.get(key)
  if(! deps){ depsMap.set(key,(deps= > new Set()))
  }
  deps.add(activeEffect)
}

// Call trigget inside the set interceptor to trigger the change
function trigger(target,key){
  // Get depsMap from target, which is key --> effects
  const depsMap = bucket.get(target)
  if(! depsMap)return 
  // Get all effects according to key
  const effects = depsMap.get(key)
  // Execute the side effect function
  effects && effect.forEach(fn= > fn())
}
Copy the code

4.4 Branch switching and cleanup

  • Let’s start with a piece of code
  • wheneffectThere is a ternary expression inside the function, according to the fieldobj.okDifferent code branches are performed depending on the value of. So whenobj.okWhen the value changes, the code execution changes.
  • Pain point: Branch switching causes legacy side effects. Below the code is the relationship between the side effect function and the reactive data.
  • inobj.okWhen changed from true to false, the side effect function is triggered, ideallyeffectFnShould not be fieldobj.textThe corresponding dependency collection.
const data = { ok: true.text: "hello world" }
const obj = new Proxy(data, {/ *... * /})

effect(function effectFn(){
  document.body.innerText = obj.ok ? obj.text : 'not'
})
Copy the code
data
		|
  	|______ok
						|
						|_____ effectFn
    |
		|______ text
    				|
      			|_____ effectFn
Copy the code
  • Problem solved: Each time a side effect function executes, it can be removed from the set of all dependencies associated with it.
// Use a global variable to store the registered side effect function
let activeEffect

function effect(fn){
  const effectFn = () = > {
    // When effectFn executes, set it to the currently active side effect function
    activeEffect = effectFn
    fn()
  }

  // Activeeffect. deps is used to store all dependencies associated with the side effect function
  effectFn.deps = []
  effectFn()
}
Copy the code
  • At this time to usetrackThe function to collecteffectFn.depsA collection of dependencies in an array.
    • intrackFunction, will currently execute the side effect functionactiveEffectAdd to dependency setdepsIn, this shows thatdepsIs a collection of dependencies associated with the current side effect function.
    • We will againdepsAdded to theactiveEffect.depsTo complete the dependency collection.
function track(target,key){
  // No activeEffect, return
  if(! activeEffect)return
  let depsMap = bucket.get(target)
  if(! depsMap){ bucket.set(target,(depsMap =new Map()))}let deps = depsMap.get(key)
  if(! deps){ despMap.set(key,(deps= > new Set()))}// DEPS is a collection of dependencies associated with the current side effect function
  // Add it to the ActiveEffect.deps array
  activeEffect.deps.push(deps) / / new
}
Copy the code

  • In turn, each time the side effect function executes, according toeffectFn.depsGet all the dependency sets you want to associate, and remove the side effect function from the dependency set:
// Use a global variable to store the registered side effect function
let activeEffect

function effect(fn){
  const effectFn = () = > {
		// Call the cleanup function to do the cleanup
		cleanup(effectFn) / / new
    fn()
  }

  // Activeeffect. deps is used to store all dependencies associated with the side effect function
  effectFn.deps = []
  effectFn()
}
Copy the code
  • cleanup
    • The cleanup function takes a side effect function as an argument and iterates over the side effect function’seffectFn.depsArray, each entry of which is a dependency collection, and then removes the side effect function from the dependency collection, eventually resettingeffectFn.depsThe array.
function cleanup(effectFn){
  // iterate through the effectFn.deps array
  for(let i = 0; i < effectFn.deps.length; i++){const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = 0
}
Copy the code
  • Defect: Because we aretriggerFunction.forEachTo iterate over and execute the side effect function. Pattern ECMA specification in callforEachtraverseSetCollection, if a value has already been accessed, but the value is removed and readded to the collection, ifforEachThe traversal does not end, then the value is revisited, hence the presenceInfinite loop BUG.
// Modify the front trigger
function trigger(target,key){
  const depsMap = bucket.get(target)
  if(! depsMap)return 
  const effects = depsMap.get(key)
  effects && effects.forEach(fn= > fn()) // Problem code
}

// Modified: trigger, constructs another Set to iterate over it
function trigger(target,key){
  const depsMap = bucket.get(target)
  if(! despMaap)return
  const effects = depsMap.get(key)
  
  const effectsToRun = new Set(effects) / / new
  effectsToRun.forEach(effectFn= > effectFn()) / / new
  effects && effects.forEach(fn= > fn()) / / delete
}
Copy the code

4.5 Nested Effect and Effect stack

  • effectNesting can happen, such as when we nest render components. The parent component is rendered and calledeffectThe same is done when the child component is renderedeffect.
  • Disadvantages: The above implementation of reactive will exist when nested render components, dependencies are collected after the side effect function is overridden. namelyactiveEffectSide effect functions are nestedeffectWhen there will be the inner layercover.
// Use a global variable to store the currently active effect function
let activeEfffect

function effect(fn){
	const effectFn = () = > {
    cleanup(effectFn)
    // When effect is called to register the side effect function, the side effect function is copied to activeEffect
    activeEffect = effectFn
    fn()
  }
  
  // activeeffect. deps is used to store all the data sets associated with the subleased function
  effectFn.deps = []
  // Execute the side effect function
  effectFn()
}
Copy the code
  • The problem with the code above: even if the reactive data is read in the outer side function, the side function they collect is always the inner side function, and that’s the problem.
  • Workaround: We use a side effect stack to store side effects.activeEfffectPoint to the top of the stack. When the inner function completes, eject the function from the top of the stack and point the top of the stack to the previous side function.
// Use a global variable to store the currently active effect function
let activeEfffect

/ / stack effect
const effectStack = [] / / new

function effect(fn){
  const effectFn = () = > {
    cleanup(effectFn)
    // When effect is called to register the side effect function, the side effect function is copied to activeEffect
    activeEffect = effectFn
    // Call the side effect function before calling it
    effectStack.push(effect) / / new
    fn()
    
    // After the current side effect function is completed, the current side effect function is ejected from the stack and activeEffect is restored to the previous value
    effectStack.pop() / / new
    activeEffect = effectStack[effectStack.length - 1] / / new
  }
  
  // activeeffect. deps is used to store all the data sets associated with the subleased function
  effectFn.deps = []
  // Execute the side effect function
  effectFn()
}
Copy the code

4.6 Avoid infinite recursive loops

  • We need to add conditional guards to reactive functions, otherwiseeffectThe function is called recursively indefinitely.
const data = { foo:1 }
const obj = new Proxy(data, {/ * * * * * /})

effect(() = > obj.foo++) // Will cause an infinite loop
Copy the code
  • To cause an infinite loop, reading obj.foo triggers track and then executes the side effect function. Assigning it to obj.foo by 1 triggers the side effect function. When the side effect function is not completed, the next execution begins. So this leads to infinite recursion.
  • Fixed the problem: added conditional guards that do not trigger execution when track and trigger execute the same side effect function.
// New: trigger
function trigger(target,key){
  const depsMap = bucket.get(target)
  if(! despMaap)return
  const effects = depsMap.get(key)
  
  const effectsToRun = new Set()
  effects && effects.forEach((effectFn) = > {
    if(effectFn ! = activeEffect){ effectsToRun.add(effectFn) } }) effectsToRun.forEach(effectFn= > effectFn())
}
Copy the code

4.7 Scheduling And Execution

  • The scheduling execution mentioned in the book can be understood as givingeffectThe option function passes in the second option argument, passing the side effect function to the scheduler via the option argumentscheduler, so as to control the timing, times and ways of execution.
  • Example: Implementing output in a different order
const data = { foo : 1}
const obj = new Proxy(data, {/ * * * * * * /})

effect(() = > {
	console.log(obj.foo)
})

obj.foo++

console.log('It's over')

// execute result 1, 2 'end'

// Expectation: do not adjust the code so that it outputs 1 'end' 2
effect(() = > {console.log(obj.foo)}, 
      // options
       {
  				// The scheduler is a function
  				scheduler(fn)
				}
)
Copy the code
  • Just need tooptionHang on toeffectOn. Before triggering the side effect functioneffect.optionWhether or not to judge.
function trigger(Target, the key){
  /* ***** omit ***** */
  
  const effectsToRun = new Set()
  effects && effects.forEach((effectFn) = > {
    if(effectFn ! = activeEffect){ effectsToRun.add(effectFn) } })// The main code
  effectsToRun.forEach((effectFn) = > {
    If a side effect function has a scheduler, schedule the scheduler and pass the side effect function as an argument
    if(effect.options.scheduler){
      effectFn.options.scheduler(effectFn)
    } else {
      // If not, execute effectFn directly
      effectFn()
    }
  })
}
Copy the code
// Complete the printout requirements above


const data = { foo : 1}
const obj = new Proxy(data, {/ * * * * * * /})

effect(() = > {
	console.log(obj.foo),
  // options
  {
  	  scheduler(fn){
        // The function is put into the macro task queue
        setTimeout(fn)
      }
  }
})

obj.foo++

console.log('It's over')


// result 1 'end' 2
Copy the code
  • It is mentioned in the book that sometimes we only need to focus on the result rather than the process, and we can also use the scheduler to accomplish this, which is not listed in detail here. (about, p64)

4.8 Computing Attributes Computed and Lazy

  • We can act like a scheduler. HereeffectAdd alazyParameter, further to the side effect function to execute the value first save a copy. So implement the following code:
function effect(fn,option = {}){
  const effectFn = () = > {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    // Store the result of fn's execution in res
    const res = fn() 
    activeEffect = effectStack[effectStack.length - 1]
    // Use res as the return value of effectFn
    return res
  }
  effectFn.options = options
  effectFn.deps = []
  
  / / implementation of lazy
  if(! options.lazy){ effectFn() }return effectFn
}
Copy the code
  • Implements simple computed, but still has drawbacks
function computed(getter){
  // Create a lazy effect with the getter as the side effect function
  const effectFn = effect(getter, {
    lazy:true
  })
  
  const obj = {
    // effectFn is executed when value is read
    get value() {return effectFn()
    }
  }
  
  return obj
}


/ / use
const data = {foo:1.bar:2}
const obj = new Proxy(data,{/ * * * * * * /})

const sumRes = computed(() = > obj.foo + obj.bar)

console.log(sumRes.value) / / 3
Copy the code
  • Defects in computed: Multiple visitssumRes.valueThe value of, causeseffectFnExecute multiple times, even at this pointobj.fooandobj.barThere is no change in the value of.
console.log(sumRes.value) / / 3
console.log(sumRes.value) / / 3
console.log(sumRes.value) / / 3
Copy the code
  • To solve the problem of re-accessing effectFn multiple times, we now cache the values
function computed(getter){
  // value is used to cache the last calculated value
  let value
  // Dirty flag, used to indicate whether values need to be recalculated. True indicates that values are dirty and need to be computed
  let dirty = true
  
  const effectFn = effect(getter,{
    lazy:true
  })
  
  const obj = {
    get value() {// Only dirty values are computed and the resulting values are cached in value
      if(dirty){
        value = effectFn()
        // Set dirty to false and use the cached value directly for the next access
        dirty = false}}}}Copy the code
  • Bug: It is obvious from the above code that the dirty is not reset to false after each execution, so the first time you access sumres.value, the dirty will be false and never change. Therefore, any subsequent values accessed will be the first values accessed.

  • Solution: When the value changes, add a scheduler to Effect and set dirty to true.

function computed(getter){
  // value is used to cache the last calculated value
  let value
  // Dirty flag, used to indicate whether values need to be recalculated. True indicates that values are dirty and need to be computed
  let dirty = true
  
  const effectFn = effect(getter,{
    lazy:true.// Add a scheduler where dirty is reset to true
    scheduler(){
      dirty = true}})const obj = {
    get value() {// Only dirty values are computed and the resulting values are cached in value
      if(dirty){
        value = effectFn()
        // Set dirty to false and use the cached value directly for the next access
        dirty = false}}}}Copy the code
  • The above code,schedulerIs executed when the reactive data on which the getter depends changes. The next time you access the value, you call effectFn again.
  • Cons: When we’re in anothereffect, and then modify the value of the data, the data is not recalculated.
  • Reason: A calculated property has its own effect internally, and it executes lazily only when the value of the calculated property is actually read. For getters that calculate properties, the responsive data accessed in it only collects effects inside computed as dependencies. When the calculated property is used for another effect, effect nesting occurs, and the outer effect is not collected by the reactive data in the inner effect.
  • Fix the problem: manually call the track function to track
function computed(getter){
  // value is used to cache the last calculated value
  let value
  // Dirty flag, used to indicate whether values need to be recalculated. True indicates that values are dirty and need to be computed
  let dirty = true
  
  const effectFn = effect(getter,{
    lazy:true.// Add a scheduler where dirty is reset to true
    scheduler(){
      dirty =  true
      // Trigger is manually called when calculating attributes dependent on responsive data changes
      trigger(obj,value)
    }
  })
  
  const obj = {
    get value() {// Only dirty values are computed and the resulting values are cached in value
      if(dirty){
        value = effectFn()
        // Set dirty to false and use the cached value directly for the next access
        dirty = false
      }
      // Call the track function manually when reading value
      track(obj,value)
      return value
    }
  }
  
  return obj
}
Copy the code
  • When reading track and calculating data changes dependent on attributes, the scheduler function will be executed and trigger function will be manually invoked in the scheduler to trigger the response.

4.9 Implementation principle of Watch

  • The essence of Watch is to observe a responsive data, notify when the data changes and execute the corresponding callback function.
  • The essence of Watch is to useeffectoptions.scheduler
  • ifeffectThe incomingschedulerThe side effect function does not execute immediately, but waits for the scheduler to schedule it. natureschedulerA scheduling function is equivalent to a callback function.
function watch(source, cb){
  effect(
    // Triggers a read operation to establish a connection
  	() = > source.foo,
    {
      scheduler(){
        // When data changes, the callback function cb is called
        cb()
      }
    }
  )
}
Copy the code
  • Disadvantages: Only right nowsource.fooChange to listen, the following code encapsulates a generic read operation
  • Solution: inside watcheffectIn the calltraverseFunction performs a recursive read operation, thus replacingsource.fooThis kind of hard coding.
function watch(source, cb){
  effect(
    // Triggers a read operation to establish a connection
  	() = > traverse(source),
    {
      scheduler(){
        // When data changes, the callback function cb is called
        cb()
      }
    }
  )
}

function traverse(value, seen = new Set(a)){
  // Do nothing if the data to be read is raw or has already been read
  if(typeofvalue ! = ='object' || value === null || seen.has(value)) return
  // Adding data to seen means that it has been read iteratively, avoiding endless loops caused by circular references
  seen.add(value)
  // Leave out other data structures such as arrays for now
  // If value is an object, use for... In reads each value of the object and recursively calls traverse for processing
  for(const k in value){
    traverse(value[k],seen)
  }
  
  return vlaue 
}
Copy the code
  • This code allows you to read any property on an object and trigger the callback when any property changes.
  • Further extending the watch function: The watch function can also accept a getter function
watch(
 / / the getter function
 () = > obj.foo,
 // The callback function
  () = > {
    console.log('The value of obj.foo has changed')})Copy the code
  • If you need to implement a watch that supports passing in a getter, then inside the getter, the user can specify which responsive data the watch depends on, and only when that data changes will the callback function be executed.
function watch(source, cb){
  / / define getter
  let getter
  // If source is a function, the user is passing the getter, so assign source directly to the getter
  if(typeof source === 'function'){
    getter = source
  } else{
    // Otherwise, call as the original implementation
    getter = () = > traverse(source)
  }
  
  effect(
    // Triggers a read operation to establish a connection
  	() = > getter(source), / / change
    {
      scheduler(){
        // When data changes, the callback function cb is called
        cb()
      }
    }
  )
}
Copy the code
  • Now that you’ve implemented watch, you can pass in the getter function
  • Finally, we implement Watch to get the old and new values. Use the lazy option of the effect function.
function watch(source, cb){
  / / define getter
  let getter
  // If source is a function, the user is passing the getter, so assign source directly to the getter
  if(typeof source === 'function'){
    getter = source
  } else{
    // Otherwise, call as the original implementation
    getter = () = > traverse(source)
  }
  
  // Define old and new values
  let oldValue,newValue
  // When you register a side effect function with effect, turn on lazy and store the return value in effectFn for subsequent manual invocation
  
  const effectFn = effect(
    // Triggers a read operation to establish a connection
  	() = > getter(source), / / change
    {
      lazy: true.scheduler(){
        // Re-execute the side effect function in scheduler to get the new value
        newValue = effectFn()
				// Take the old and new values as arguments to the callback function
        cb(newValue, oldValue)
        // Update the old value, or you will get the wrong old value next time
        oldValue = newValue
      }
    }
  )
  
  // Call the side effect function manually and get the old value
  oldValue = effectFn()
}
Copy the code
  • Core addedlazyCreated a lazy executioneffect. The bottom line is to manually call effectFn to get the return valueThe old value, which is the value of the first execution. When change occurs and triggersschedulerThe scheduling function is called again when it executeseffectFnFunction execution results inThe new value. So we take the old and new values and pass them back to the function as argumentscb.
  • Be sure to update the old value with the new value, or you’ll get the wrong old value the next time you change it.

4.11 Side effects of expiration

  • Consider A scenario where, while using Watch, changes are made to OBj first, causing request A to execute. When request A comes back, we make A change to obj, and then we make request B, so when B comes back before A, A comes back after, we’ll finally get the value of request A. (This is not what we want, A is the expired request value.)

  • Fix the problem: Make watch accept the third argumentonInvalidate, which is a function similar to an event listener.
watch(obj,async (newValue,oldValue,onInvalidate) => {
  // Define a flag that indicates whether the current side effect function is expired. The default is false, indicating that it is not expired
  let expired = false
  // Call onInvalidate() to register an expired callback
  onInvalidate(() = > {
    // When expired, set expired to true
    expired = true
	})
  
  // Send a network request
  const res = await fetch('/path/to/request')
  if(! expired){ finalData = res } })Copy the code
  • The above code simply shows how to use the onInvalidate function to notify expiration execution. By definingexpiredFlag variable, also to indicate whether the currently executed side effect function is expired. Then callonInvalidateThe function registers an expiration callback that is subtracted when the execution of the side effect function expiresexpiredFlag variable set to true. The request result is assigned at the end.
  • In VUE, each time the side effect function is re-executed, it calls us throughonInvalidateRegistered expired callback.
function watch(source, cb,options = {}){
  / / define getter
  let getter
  // If source is a function, the user is passing the getter, so assign source directly to the getter
  if(typeof source === 'function'){
    getter = source
  } else{
    // Otherwise, call as the original implementation
    getter = () = > traverse(source)
  }
  
  // Define old and new values
  // Extract scheduler to a separate job function
  let oldValue,newValue
  
  Cleanup is used to store expired callbacks for user registrations
  let cleanup / / new
  
  function onInvalidate(fn){
    // Store expiration callbacks in cleanup
    cleanuo = fn
  }
  
  const job = () = > {
		newValue = effectFn()
    // Call the expired callback before calling cb
    cb(newValue,oldValue,onInvalidate)
    oldValue = newValue
  }
  
  // When you register a side effect function with effect, turn on lazy and store the return value in effectFn for subsequent manual invocation
  
  const effectFn = effect(
    // Triggers a read operation to establish a connection
  	() = > getter(), / / change
    {
      lazy: true.// Use the job function as the scheduler function
      scheduler: () = > {
        if(option.flush === 'post') {const p = Promise.resolve()
          p.then(job)
        }else {
          job()
				}
			}
    }
  )
  
  / / new
  if(options.immediate){
    job()
  }else{
    // Call the side effect function manually and get the old value
    oldValue = effectFn()
  }
Copy the code
/ / use

watch(obj,async(newValue, oldValue, onInvalidate) => {
	let expired = false
  onInvalidate(() = > {
    expired = false
  })
  
  const res = await fetch('/path/to/request')
  
  if(! expired){ finalData = res } })// For the first time
obj.foo++
setTimeOut(() = > {
  // make the second change in 200 seconds
  obj.foo++
},200)
Copy the code
  • For the first time, execute the wathC callback immediately. Because it is called in the callback functiononInvalidate, so an out-of-date callback is registered and request A is sent
  • Suppose it takes request A 1000ms to return the result. We change the value of obj.foo a second time at 200ms, which again causes the Watch callback to execute. Note that in the implementation, you want to check for the existence of an expired callback before executing the callback function. If so, the expired callback takes precedence.
  • Since we have already registered an expiration function when the Watch callback is executed for the first time, the previously registered expiration callback is executed prior to the second execution in the Watch callback.
  • This makes the variable inside the side effect function executed for the first timeexpiredThe value of atrueThat is, the execution of the side effect function expires.
  • The result is then discarded while waiting for the return of request A.

END

Personal blog summary address: github.com/codehzy/blo…