Data response principle and implementation of vue3

The publication of vue3 has attracted the attention of a large number of front-end personnel. If you can't learn it properly, you have to learn hard. This article briefly introduces the "data response principle of vue3" and the simple realization of its reactive, effect and computed functions, hoping to help you understand the vue3 response. Not much to say, look at the following chestnut code and the results of its operation.

<div id="root"></div>
<button id="btn">Age+1</button>
const root = document.querySelector('#root')
const btn = document.querySelector('#btn')
const ob = reactive({
  name: 'Zhang San',
  age: 10
})

let cAge = computed(() => ob.age * 2)
effect(() => {
  root.innerHTML = `<h1>${ob.name}---${ob.age}---${cAge.value}</h1>`
})
btn.onclick = function () {
  ob.age += 1
}

With the code above, every time a button is clicked, obj.age + 1 is given and the effect is executed, and the corresponding ob.age * 2 is executed for calculating attributes, as follows:

So, for the chestnuts above, make some small goals, and then achieve them one by one, as follows:

  • 1. Implementing reactive function
  • 2. Implementing effect function
  • 3. Connect reactive and effect in series
  • 4. Implementing computed function

Implementing reactive function

reactive is actually a data response function, which is implemented internally through the proxy api of es6.
The following is actually a proxy interception of an object with a few lines of code.

const handlers = {
  get (target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set (target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver)
  }
}
function reactive (target) {
  observed = new Proxy(target, handlers)
  return observed
}
let person = {
  name: 'Zhang San',
  age: 10
}

let ob = reactive(person)

But there are drawbacks in doing so. 1. Repeat ob = reactive(person) many times and it will always execute new Proxy, which is not what we want. Ideally, the proxy object should be cached and returned directly to the cached object for the next visit. 2. Similarly, ob = reactive(person) should be cached as well as ob = reactive (ob). Let's change the above reactive function code.

const toProxy = new WeakMap() // Cached Proxy Objects
const toRaw = new WeakMap() // Caching proxied objects
// handlers, as above, are omitted here for the sake of space.
function reactive (target) {
  let observed = toProxy.get(target)
  // If it is cached proxy
  if (observed) {
    return observed
  }
  if (toRaw.has(target)) {
    return target
  }
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed) // Cache observed
  toRaw.set(observed, target) // Cache target
  return observed
}

let person = {
  name: 'Zhang San',
  age: 10
}

let ob = reactive(person)
ob = reactive(person) // All returns are cached
ob = reactive(ob) // All returns are cached

console.log(ob.age) // 10
ob.age = 20
console.log(ob.age) // 20

This calls reactive() back to our first proxy object (ps: WeakMap is a weak reference). The cache is ready, but there is a new problem. If the agent target object level nesting is deep, the proxy above can not do deep proxy. for example

let person = {
  name: 'Zhang San',
  age: 10,
  hobby: {
    paly: ['basketball', 'football']
  }
}
let ob = reactive(person)
console.log(ob)

From the print results above, we can see that the hobby object does not have our handlers agent above, that is to say, when we do some dependency collection on hobby, there is no way, so let's rewrite the handlers object.

// Object Type Judgment
const isObject = val => val !== null && typeof val === 'object'
const toProxy = new WeakMap() // Cached Proxy Objects
const toRaw = new WeakMap() // Caching proxied objects
const handlers = {
  get (target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    // TODO: effect collection
    return isObject(res) ? reactive(res) : res
  },
  set (target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver)
    // TODO: trigger effect
    return result
  }
}
function reactive (target) {
  let observed = toProxy.get(target)
  // If it is cached proxy
  if (observed) {
    return observed
  }
  if (toRaw.has(target)) {
    return target
  }
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed) // Cache observed
  toRaw.set(observed, target) // Cache target
  return observed
}

The above code adds return isObject (res)? Reactive (res): res to get, meaning that when an object is accessed, if the type is "object", then the reactive agent is continued to be called. The above is also the complete code of our reactive function.

Implementing effect function

Here we are a step closer to our goal. To implement the effect function, let's first look at the use of effect.

effect(() => {
  root.innerHTML = `<h1>${ob.name}---${ob.age}---${cAge.value}</h1>`
})

The first feeling seems very simple, that is, the function is passed in as a parameter, and then call the incoming function, and finish. The following code is the simplest implementation

function effect(fn) {
  fn()
}

But at this point, everyone can see the shortcomings. Is this just one execution? How does it relate to response? And how can the latter computed be based on this implementation? Wait. With a lot of problems, this series of problems can be solved by rewriting the effect and adding the effect function.

function effect (fn, options = {}) {
  const effect = createReactiveEffect(fn, options)
  // Instead of understanding the calculation, you don't need to call the effect at this point
  if (!options.lazy) {
    effect()
  }
  return effect
}
function createReactiveEffect(fn, options) {
  const effect = function effect(...args) {
    return run(effect, fn, args) // It executes fn
  }
  // Hang some attributes to effect
  effect.lazy = options.lazy
  effect.computed = options.computed
  effect.deps = []
  return effect
}

In the createReactiveEffect function: create a new effect function, and hang the effect function on some attributes to prepare for later computed. This effect function calls the run function (which has not been implemented at this time), and finally returns a new effect.

In the effect function: If you decide that options.lazy is false, you call the above to create a new effect function, which calls the run function.

Connect reactive and effect in series

In fact, the function of the run function that has not been written above is to connect the logic of reactive and effect, and to achieve it next, the goal is a step closer.

const activeEffectStack = [] // Declare an array to store the current effect, which you need to subscribe to
function run (effect, fn, args) {
  if (activeEffectStack.indexOf(effect) === -1) {
    try {
      // Push effect into an array
      activeEffectStack.push(effect)
      return fn(...args)
    }
    finally {
      // Clear up the effects that have been collected and prepare for the next effect
      activeEffectStack.pop()
    }
  }
}

The above code pushes the incoming effect into an active EffectStack array and executes the incoming fn(...args), where fn is

fn = () => {
  root.innerHTML = `<h1>${ob.name}---${ob.age}---${cAge.value}</h1>`
}

Execute the fn above to access ob.name, ob.age, cAge. value (computed), which triggers the getter of proxy, which is the handlers.get function below.

const handlers = {
  get (target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    // effect collection
    track(target, key)
    return isObject(res) ? reactive(res) : res
  },
  set (target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver)
    const extraInfo = { oldValue: target[key], newValue: value }
    // trigger effect
    trigger(target, key, extraInfo)
    return result
  }
}

The smart buddy sees that the role of trace in the handlers.get function above is dependent on collection, while trigger in handlers.set is dispatched and updated.
Complete the trace function code below

// Storage effect
const targetMap = new WeakMap()
function track (target, key) {
  // push in the effect from above
  const effect = activeEffectStack[activeEffectStack.length - 1]
  if (effect) {
    let depsMap = targetMap.get(target)
    if (depsMap === void 0) {
      depsMap = new Map()
      // TagetMap If there is no target Map, set one
      targetMap.set(target, depsMap)
    }
    let dep = depsMap.get(key)
    if (dep === void 0) {
      dep = new Set()
      // If there is no key in depsMap, Set one
      depsMap.set(key, dep)
    }
    if (!dep.has(effect)) {
      // Collect current effect s
      dep.add(effect)
      // effect collects the current dep
      effect.deps.push(dep)
    }
  }
}

See here, everybody else. The above code means that we get the current effect from the active EffectStack in the run function, and depsMap from the targetMap if there is an effect. If there is no targetMap, we set up a targetMap.set(target, depsMap), and then we get the key set from the depsMap. If there is no key in the depsMap, we can get the key set from the depsMap. Set up a depsMap.set(key, dep). Here are the effects and effects before the collection to collect the current Dep. After the collection, the data structure of the targetMap looks like the following.

// The purpose of track is to complete the following data structure
targetMap = {
  target: {
    name: [effect],
    age: [effect]
  }
}
// ps: targetMap is WeakMap data structure, which is represented by objects for intuition and understanding.
//     [effect] is a Set data structure, which is expressed in arrays for intuition and understanding.

After track is executed, handlers.get returns to res, after a series of collections, fn is executed, run function finally executes {active EffectStack. pop ()} because the effect collection has been completed, clearing it for the next effect collection.

Dependency collection is complete, but when we update the data, such as ob.age += 1, changing the data triggers the getter of proxy, which calls the handlers.set function, which executes the trigger(target, key, extraInfo). The trigger function is as follows

// Trigger of effect
function trigger(target, key, extraInfo) {
  // Get all the target subscriptions
  const depsMap = targetMap.get(target)
  // Not Subscribed
  if (depsMap === void 0) {
    return;
  }
  const effects = new Set() // Ordinary effect s
  const computedRunners = new Set() // computed effect
  if (key !== void 0) {
    let deps = depsMap.get(key)
    // Get each effect that deps subscribes to and put it in the corresponding Set
    deps.forEach(effect => {
      if (effect.computed) {
        computedRunners.add(effect)
      } else {
        effects.add(effect)
      }
    })
  }
  const run = effect => {
    effect()
  }
  // Loop call effect
  computedRunners.forEach(run)
  effects.forEach(run)
}

The above code means to get the effect of the corresponding key, then execute the effect, then run, then execute fn, then the process above get, and finally get the new data after the change, and then change the view.

Here's a simple flow chart to help understand. I can't understand it. Let's take it as an example. Warehouse code Pull it down, debuger executes it once

targetMap = {
  name: [effect],
  age: [effect]
}
ob.age += 1 -> set() -> trigger() -> age: [effect] -> effect() -> run() -> fn() -> getget() -> Render view

Implementing computed function

Let cAge = computed () => ob. age * 2), when we write effect above, we mention many times that we are preparing for computer. In fact, computer is based on effect. Let's look at the code below.

function computed(fn) {
  const getter = fn
  // Manually generate an effect and set parameters
  const runner = effect(getter, { computed: true, lazy: true })
  // Return an object
  return {
    effect: runner,
    get value() {
      value = runner()
      return value
    }
  }
}

It's worth noting that we have a judgment in the effect function above.

if (!options.lazy) {
  effect()
}

If options.lazy is true, it will not be executed immediately, which is equivalent to let cAge = computed () => ob. age * 2) and will not execute the runner function immediately until cAge.value is actually executed.

Finally, all functions are drawn as a flow chart.

If there are any mistakes in the article, please point out that I have fishing time will be corrected.

So far, we have accomplished all the small goals, scattering flowers (

ps:

Source address (you can clone down to execute it once)

Blog post address (here is a new reading experience, there are also Wechat, Welcome to Tiao)

Keywords: Javascript

Added by langer on Thu, 10 Oct 2019 08:28:38 +0300