3.1 A slightly improved Vue.js responsive system

Summary of the previous article: Design and Implementation of 3.0 Responsive System

1. Set a reasonable effect side effect function

As mentioned above, if we directly use the simple effect function as a side effect function, if a side effect function is not called effect, wouldn’t it be impossible to find it. The solution is also very simple, we set a global variable to register the side effect function, the code is as follows:

let activeEffect = null
function effect(fn) {<!-- -->
  // When the effect function is called to register the side effect function, assign the side effect function fn to activeEffect
  activeEffect = fn
  fn()
}

At this time, the effect function is only used for registration. It receives a parameter fn, which is the side effect function to be registered. You can use effect< in the following way /code> function:

effect(
  () => {<!-- -->document.body.innerText = obj.text}
)

At this time, an anonymous function is passed to effect as a side effect function, and this anonymous function will be assigned to activeEffect, so you only need to pass activeEffect each time Just put it in the bucket. like:

const bucket = new Set()
const obj = new Proxy(data, {<!-- -->
  get(target, key) {<!-- -->
    // If the object has a side effect function, put it in the bucket
    if(activeEffect) bucket.set(target, activeEffect)
    return target[key]
  },
  set(target, key, newVal) {<!-- -->
    // The side effect function execution of each assignment to take out the object
    bucket. forEach(fn => fn())
    target[key] = newVal
    return true
  }
})

The execution logic in the above code can be clearly seen, since the side effect function is stored in activeEffect, you can directly use activeEffect when get Functions are put into buckets so that there is no need to depend on the name of the side effect function.

2. More detailed binding of side effect functions

All the side-effect functions we mentioned above are actually directly bound to the object, because we only set a bucket for the object, so every time you change any value in the object, all the side-effect functions in the bucket will be executed.

What problems will this cause? The most obvious one is performance waste. If a large object has many side-effect functions for each element, then we will execute all side-effect functions when we change an unrelated element.

At this time, we can change the way of bucketing, use a WeakMap as the bucket, each key in the WeakMap is a responsive object, and its value is a Map, and the key in the Map is the attribute name in the object. The value of the key is Set, and at this time, the Set is loaded with the side effect function of the object element. as the picture shows:

The code is implemented as follows:

// WeakMap description see below
const bucket = new WeakMap()
  const obj = new Proxy(data, {<!-- -->
    get(target, key) {<!-- -->
      // No side effect function returns directly
      if(!activeEffect) return target[key]
      // Get the Map of all elements in the current object
      let depsMap = bucket. get(target)
      // Create a new one when the current object does not have a Map
      if(!depsMap) {<!-- -->
        bucket.set(target, (depsMap = new Map()))
      }
      // Get the Set of all side-effect functions of the current element
      let deps = depsMap. get(key)
      // If not, create and add a side effect function
      if(!deps) depsMap.set(key, (deps = new Set()))
      deps. add(activeEffect)
      return target[key]
    },
    set(target, key, newVal) {<!-- -->
      target[key] = newVal
      const depsMap = bucket. get(target)
      if(!depsMap) return
      const deps = depsMap. get(key)
      deps & amp; & amp; deps.forEach(fn => fn())
      return true
    }
  })
}

In fact, the code looks very complicated. In fact, there are only three containers, WeakMap, Map, and Set. Here, WeakMap is used to put all objects as key values (keys), and the map is a Map. This Map is based on the object's All element attribute names are used as keys, and its value is a Set, which stores all side effects of the current key.

This way each side effect function is finely bound to an element of an object.

3. About WeakMap

Here we also need to talk about the difference between WeakMap and Map. In fact, the mapping relationship between the two is the same. The difference is that WeakMap is a weak reference. What is a weak reference? Suppose we have an object that is only referenced by a Map. At this time, the object has no other references. At this time, the object will not be reclaimed by the garbage collector. Because the Map exists, the reference to the object will always exist, but if this The object is only referenced by a WeakMap, and will be reclaimed by the garbage collector when there are no other references. That is, WeakMap will not affect the garbage collector, as the following code can explain well:

const weakMap = new WeakMap()
const map = new Map()
(function () {<!-- -->
  const foo = {<!-- -->foo: 1}
  const bar = {<!-- -->bar: 1}

  map.set(foo, 1)
  weakMap.set(bar, 1)
})()

In the immediate execution function, the two objects foo are referenced by map, and bar is referenced by weakMap. After execution, bar will be recycled, but foo still exists.

Another point of WeakMap is that there is no iterator, and it cannot iterate through values directly like Map, so generally WeakMap is often used for mapping that does not affect the object itself, or for marking.

It is also well understood to use WeakMap as the outermost bucket in Vue. This will not cause the object to be referenced by the bucket for a long time and cannot be recycled. The bucket will not affect the execution of the program itself.

4. Branch switching and cleanup

Before introducing branch switching, you can encapsulate the above code first, encapsulate the collection of side effects in the get interception function into a track function, and set The trigger of the side effect function in the code> function is divided into the trigger function, as follows:

// Use buckets to put all objects containing side-effect functions into
  const bucket = new WeakMap()
  const obj = new Proxy(data, {<!-- -->
    get(target, key) {<!-- -->
      // trace function
      track(target, key)
      return target[key]
    },
    set(target, key, newVal) {<!-- -->
      target[key] = newVal
      // trigger function
      trigger(target, key)
      return true
    }
  })

  function track(target, key){<!-- -->
    // No side effect function returns directly
    if(!activeEffect) return target[key]
    // Get the Map of all elements in the current object
    let depsMap = bucket. get(target)
    // Create a new one when the current object does not have a Map
    if(!depsMap) {<!-- -->
      bucket.set(target, (depsMap = new Map()))
    }
    // Get the Set of all side-effect functions of the current element
    let deps = depsMap. get(key)
    // If not, create and add a side effect function
    if(!deps) depsMap.set(key, (deps = new Set()))
    deps. add(activeEffect)
  }

  function trigger(target, key) {<!-- -->
    const depsMap = bucket. get(target)
    if(!depsMap) return
    const deps = depsMap. get(key)
    deps & amp; & amp; deps.forEach(fn => fn())
  }
}

In this way, it is much simpler and clearer in the proxy about Proxy.

Let's look at a piece of code below:

const data = {<!-- -->ok: true, text: 'hello world'}
const obj = new Proxy(data, {<!-- -->/* omitted */})

effect(() => {<!-- -->
  // If obj.ok is true, read the value of obj.text
  document.body.innerText = obj.ok? obj.text: 'not'
})

In the responsive system we wrote, this side effect function will be associated with obj.ok and obj.text at the same time, and this side effect function will be triggered whether obj.ok or obj.text is modified. But if you think about it carefully, is it really necessary? In fact, it is not necessary.

In this code, if obj.ok is true, then this side effect function is associated with obj.text, otherwise it is not associated, because the text content of body is always not.

So how to optimize this point? In fact, we said in the previous article that a responsive data execution process is:

  1. Modify the value of obj.content, which will trigger the execution of the effect function.
  2. Trigger the effect function, which gets the value of obj.content.

I mentioned the second point here before, whether triggering the effect function will definitely get the value of obj.content, the answer is yes, if you don’t get the value of obj.content here, you don’t need to create responsive data.

Based on this, we delete the side effect function from all elements before executing the side effect function, and then execute the side effect function. If the side effect function needs to read the element value of the current object during the execution of the side effect function, this will recreate the side effect function.

In this way, our code needs to add three parts, the first part is cleanup used to delete the side effect function from all elements, and the second part is to add an array to the side effect function for recording and this side effect function Map of all related elements, the third part is to record the elements associated with the side effect during track.

function cleanup(effectFn) {<!-- -->
    // traverse the collection containing the side effect function effctFn
    for(let i=0;i<effectFn.deps.length;i ++ ) {<!-- -->
      const deps = effectFn.deps[i]
      // Delete the effctFn side effect function in the collection, and it will be recreated when it is executed
      deps.delete(effectFn)
    }
    // The collection containing this side effect function is currently 0
    effectFn.deps.length = 0
}

function effect(fn) {<!-- -->
    const effectFn = () => {<!-- -->
      activeEffect = effectFn
      //Clear first and then execute, naturally forming a branch switch
      cleanup(effectFn)
      fn()
    }
    effectFn. deps = []
    effectFn()
  }

function track(target, key){<!-- -->
    // No side effect function returns directly
    if(!activeEffect) return target[key]
    // Get the Map of all elements in the current object
    let depsMap = bucket. get(target)
    // Create a new one when the current object does not have a Map
    if(!depsMap) {<!-- -->
      bucket.set(target, (depsMap = new Map()))
    }
    // Get the Set of all side-effect functions of the current element
    let deps = depsMap. get(key)
    // If not, create and add a side effect function
    if(!deps) depsMap.set(key, (deps = new Set()))
    deps.add(key, activeEffect)

// Here, the Map related to the side effect function is recorded
    activeEffect.deps.push(deps)
  }

In fact, the idea of branch switching is very simple. It is to delete all the current side-effect functions from the associated elements before the side-effect function is executed, and then execute the side-effect function. If the element is read during execution, the side-effect function will be re-associated. Naturally formed a branch switch.

In this way, our responsive system has improved a lot. In fact, there are still many problems that have not yet been resolved, such as how to execute nested effects, how to schedule side-effect functions, and whether there will be infinite recursion. Explain later

syntaxbug.com © 2021 All Rights Reserved.