preface

Recently I wrote a simple version of Vue3 to play, of course, there is no patch and compilation part (due to limited ability) 😂, so like to share what I learned in this process. This article is mainly how to achieve a simple Vue3 way to explain the source code design and basic principles.

What to do

Think about what you want to do before you do it, so here’s a simple list:

  1. Understand the Setup option
  2. Implement reactive core functions
  3. Implement asynchronous queue mechanism
  4. Implement effect function
  5. Implement the watch, computed, and watchEffect functions

These are the main parts of Vue3 that I summarized after looking at the source code, which is also the part that we often come into contact with in daily development. Next, let’s go through these lists and understand the general principles of Vue3 as we implement them step by step.

Initialization option setup

Here’s a look at the use of setup in Vue3:

<template>
  <div>
    <h1 @click="onClick">{{ msg }}</h1>
    <h2>{{ state.name }}</h2>
    <h2>{{ state.age }}</h2>
    <h2>{{ doubleAge }}</h2>
  </div>
</template>

import { defineComponent, reactive, onBeforeMount, computed } from 'vue'

export default defineComponent({
  name: 'App'.props: {
    msg: String,},setup(props) {
    const state = reactive({
      name: 'tangmouren'.age: 18
    })

    const doubleAge = computed(() = > state.age * 2)

    const onClick = () = > {
      console.log(msg)
    }

    onBeforeMount(() = > {
      console.log(props.msg)
    })

    return {
      state,
      doubleAge,
      onClick
    }
  }
})
Copy the code

Let’s analyze how it works.

First of all, it’s a function. It returns the data and methods needed by the component through an object. This is a bit like the data option in Vue2, where a function returns a data object. In fact, they both return what the component needs. Therefore, these functions are executed during component initialization, the returned object is retrieved, processed, and finally mounted to the component instance.

Here’s a simple implementation:

function Vue(options) {
  const { setup } = options
  const setupResult = setup()
  this.ctx = setupResult
}
Copy the code

This data and methods can then be accessed through the CTX during component rendering. Here is an example of Vue3 template compilation:


  <div>
    <h1 @click="onClick">{{ msg }}</h1>
    <h2>{{ state.name }}</h2>
    <h2>{{ state.age }}</h2>
    <h2>{{ doubleAge }}</h2>
  </div>

  // Will compile to:

export function render(_ctx, _cache, $props, $setup, $data, $options) {
   return (_openBlock(), _createBlock("div".null, [
    _createVNode("h1", { onClick: _ctx.onClick }, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */["onClick"]),
    _createVNode("h2".null, _toDisplayString(_ctx.state.name), 1 /* TEXT */),
    _createVNode("h2".null, _toDisplayString(_ctx.state.age), 1 /* TEXT */),
    _createVNode("h2".null, _toDisplayString(_ctx.doubleAge), 1 /* TEXT */)))}Copy the code

Reactive core functions

We’ve already written a few lines of code, and the “grand” project has already begun. Next comes the core of Vue, which I think any Vue developer must understand.

The well-worn principle of responsiveness

Let’s start by reviewing one of Vue’s key concepts: responsive design. The three most important elements in the responsive principle are Watcher, Dep and Observer. It is these three elements that implement the observer pattern (publisher/subscriber pattern). How does this mode work in Vue2/Vue3? Let’s first understand these questions:

In the framework, who is the publisher and who is the subscriber: The object that intercepts data is the publisher. An object/function that contains a task (function) is the subscriber (Vue2 is Watcher, Vue3 is Effect).

The relationship between publishers and subscribers: Publishers collect relevant subscribers, and when publishers take action, they notify the collected subscribers to perform the task.

How publishers collect subscribers: VNodes are created and read during rendering, so once some of the publisher’s data is accessed, it stores the currently active subscribers into the corresponding Dep (this process is called dependency collection).

For relying on the collection to read the data, take a look at the above code for compiling the template into the render function (this compilation is handled by vue-loader). Since creating a VNode requires reading the data, it triggers the dependency collection.

Implementation differences between Vue2 and Vue3

Vue3 will definitely not change much in terms of responsive design, so the general logic is basically the same as Vue2. However, some optimization and changes have been made in Vue3’s core logic.

Mainly as follows:

  1. Used in Vue2Object.definePropertyImplementation of data interception, and in Vue3 is usedProxyTo broker the data.
  2. DepIt is no longer stored in closures, but usedWeakMap, Map, SetThese data structures are used for global storage.
  3. It used to beWatcherTo implement subscribers, in Vue3 is adoptedeffectThis side effect function. So in the process of relying on the collection, the collection is no longerWatcherbuteffectFunction.

Implement the Observer

Get the theory straight, and we can get down to business. Let’s start by realizing the first player of the three elements, the observer.

What does it do:

  1. Proxy intercepts read, write, add, delete and other operations on a data (Proxy does not only intercept read and write. If you are interested, you can go to MDN to learn more about it. This article only considers read and write operations)
  2. When read, collect the currently activeeffectFunction inDepIn the
  3. When modified, executeDepAll of theeffectfunction

In the Composition API, a reactive function is provided to implement Vue’s reactive system, which proxies incoming objects for reactive processing and returns the proxy of their objects. So write a reactive function:

// Proxy handler
const handler = {
  get: getter,
  set: setter
}

function reactive(target) {
  if (typeoftarget ! = ='object') return target

  return createReactiveObject(target)
}

function createReactiveObject(target) {
  // Process the current object responsively and return the proxy
  return new Proxy(target, handler)
}
Copy the code

It is not hard to see here that the publisher created is a Proxy. The Proxy intercepts the access of the target object. Currently, only the data is read and written, so only the specific logic of SET and GET is implemented.

get

The main intercept logic of GET is that when data is accessed, the value of the target object is first read, and then activeEffect is collected for the key being accessed and stored in the corresponding Dep.

Here we implement the logic of reading values (hint: keep an eye out for objects) :

// Effect currently active
let activeEffect

// Read interception
function getter(target, key, receiver) {
  // Read the value of the corresponding property of the target object
  const res = Reflect.get(target, key, receiver)
  // Let this key collect the currently active effects
  track(target, key)

  // Consider the possibility that the attributes accessed are objects
  // Further reactive processing is required
  // This is also an optimization of Vue3, more on this later
  if (typeof res === 'object') {
    return reactive(res)
  }

  return res
}
Copy the code

The focus is on the logic of track, which is the key of dependency collection. It will find the corresponding DEP according to the key of the current object being accessed, and then collect the current activeEffect into it.

// Collect dependencies
function track(target, key) {
  if (activeEffect === undefined) {
    return
  }
  // Find the deP collection of the current object
  let depsMap = targetMap.get(target)
  // If not, create a new deP collection
  if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}// Each key has its own deP
  let dep = depsMap.get(key)
  if(! dep) { depsMap.set(key, (dep =new Set()))}if(! dep.has(activeEffect)) {// save the activeEffect
    dep.add(activeEffect)
  }
}
Copy the code

The above code basically implements the function of collecting dependencies when data is accessed. However, there are two more points to note:

  1. One is used in the codetargetMap, depsMap, depThese data structures
  2. The value accessed is handled when an object is

For the first point, Vue3 changes the storage of Dep, so you can assume that these data structures are only used to store and find Dep. The implementation of DEP will be explained later.

And then the second point, this is an important point.

Why is it different when the value corresponding to the accessed key is an object?

Here’s how Vue2 responds to an object. Vue2 provides a data option, which is usually an object or a function that returns an object. In Vue2, reactive processing will deeply traverse the object provided by data. Once the value of an attribute is an object, it will continue to recurse down. Use the Object.defineProperty API to intercept processing. The general implementation is as follows:

function observer(obj) {
  Object.keys(obj).forEach(key= > {
    reactive(obj, key)
  })
}

function reactive(obj, key) {
  if (typeofobj[key] ! = ='object') {
    Object.defineProperty(obj, key, {
      get() {
        //
      },
      set() {
        //}})}else {
    // If it is an object, continue recursive processing
    observer(obj[key])
  }
}
Copy the code

This way, there are two big performance problems:

  1. If the object level is very deep, the recursive process can be very costly. Object.defineproperty requires iterating through each key of an Object, and recursive iterating is required if the key corresponds to an Object.

  2. Another problem with such a fully recursive and responsive approach is that if the object is deep and many of its properties are not used in the template, the ability to add dependency collection and notify subscribers to unused keys becomes meaningless.

Here’s an example I’ve seen at work:

I often use some third-party frameworks in my work, such as Echart and Antv data visualization frameworks, as well as BMap and AMap map frameworks. But I found that some of my friends in our company would do something like this by accident, for example, he used a visual chart in the component:

import { Chart } from '@antv/g2'

export default {
  name: 'About'.data() {
    return {
      barChart: {}}}.mounted() {
    // Assign the created chart instance directly to barChart in data
    this.barChart = new Chart({
      container: 'container'.width: 600.height: 300
    })

    // Let's look at the output
    console.log(this.barChart)

    const data = [
      { year: '1951'.sales: 38 },
      { year: '1952'.sales: 52 },
      { year: '1956'.sales: 61 },
      { year: '1957'.sales: 145}]this.barChart.data(data)
    this.barChart.scale('sales', {
      nice: true
    })
    this.barChart.tooltip({
      showMarkers: false
    })
    this.barChart.interaction('active-region')
    this.barChart.interval().position('year*sales')
    this.barChart.render()
  }
}
Copy the code

It seems like there’s nothing wrong with writing this, but let’s take a look at barChart’s output:

You can see that all the properties in the object diagram instance are treated responsively. If you think about it, isn’t it a waste of performance to recursively respond to such a large object and then use none of its data on the page? Of course, this is just an example, and I’m sure most people won’t make this mistake, because if you don’t need responsive data, there’s no need to write it in data.

In Vue3, however, reactive does not initiate deep processing, but acts only on the first layer. For deep objects, reactive processing takes place in GET. That is, if the underlying object is not accessed, it will never be processed responsively, which greatly improves performance and saves unnecessary running overhead.

Here’s an example:


/ / in the template
<template>
  <div>{{ state.num }}</div>
  <div>{{ state.age }}</div>
</template>


// Declare a reactive object in setup
 const state = reactive({
    num: 100.age: 18.person: {
      a: 1}})Copy the code

Because there is no access to state.person.a in the template, all person objects are not processed responsively, and no event is triggered regardless of how you change its value.

set

Now that the ability to rely on collections has been implemented, it’s time to work out the logic of notifying subscribers. Since the publisher has already collected the relevant subscribers into the corresponding DEP queue, the logic of notification is simply to find the corresponding DEP and pull out the effect functions one by one to execute.

Code implementation:

function setter(target, key, value, receiver) {
  / / modify the value
  const res = Reflect.set(target, key, value, receiver)
  // Triggers the execution of the collected effects
  trigger(target, key)
  return res
}

function trigger(target, key) {
  // find the deP set
  const depsMap = targetMap.get(target)

  if(! depsMap)return

  // Find the corresponding deP
  const dep = depsMap.get(key)

  dep.forEach(effect= > {
    // If effect has a scheduling mechanism, execute it that way
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      // Execute this' effect 'directly
      effect()
    }
  })
}
Copy the code

The set execution process is to first modify the value, then execute trigger to find the DEP corresponding to the current key, and finally iterate through the subscriber effect in the execution deP.

Just try it out

Most of the logic for reactive processing above has been implemented, so let’s test it briefly. We assume that the current activeEffect is an update render update function:

let activeEffect = function update() {
  console.log('Render view! ')}// In case the setters are triggered without this option being reported
// This option will be used later when implementing effect
activeEffect.options = {}
Copy the code

Then create a component:

const vm = new Vue({
  setup() {
    const state = reactive({
      num: 100.person: {
        a: 1}})return {
      state
    }
  }
})

// Simulate the process of render accessing data
console.log(vm.ctx.state.num)
console.log(vm.ctx.state.person.a)

// Modify the data
vm.ctx.state.num = Awesome!
vm.ctx.state.person.a = 999
Copy the code

Take a look at the results:

Nice, the data in the component is successfully collected and triggered when accessed.

To test the case, if the underlying object’s data is not used in the template, will it be processed as responsive? To see exactly which data triggers the update, let’s change the original code slightly:

// Change the update function
let activeEffect = function update(key) {
  console.log(`${key}-- Triggered update view! `)
}

activeEffect.options = {}
Copy the code
// The logic of trigger executing effect has also been changed slightly
if (effect.options.scheduler) {
  // ...
} else {
  // Pass the current key to update
  effect(key)
}
Copy the code

Example:

// call num only
console.log(vm.ctx.state.num)

// Modify the data
vm.ctx.state.num = Awesome!
vm.ctx.state.person.a = 999
Copy the code

And the results:

Very nice, the person is not accessed, it is not processed responsively, so the update function cannot be triggered.

Realize the Dep

Having implemented an Observer for the three important elements in Vue, let’s implement the Dep, a place to store subscribers.

To start this section, you should first have an understanding of WeakMap, Map, and Set data structures. Vue2 creates a Dep inside a function when each key is intercepted. The Dep for each key is stored in a function closure.

function reactive(obj, key) {
  // Create a deP inside the function for each key
  const dep = new Dep()

  if (typeofobj[key] ! = ='object') {
    Object.defineProperty(obj, key, {
      get() {
        //
      },
      set() {
        //}})}else {
    observer(obj[key])
  }
}
Copy the code

In Vue3, I found the Dep implementation much clearer and simpler than Vue2. It’s just a couple of data structures. You can go back and look at the variables targetMap, depsMap, dep that we used to implement the getter. Let’s look at this targetMap, which is actually a global WeakMap:

const targetMap = new WeakMap(a)Copy the code

This is used to store the mapping between data objects (target) and deP sets (depsMap). As it says in the code:

let depsMap = targetMap.get(target)

if(! depsMap) {// Create a depsMap if you don't have one
  targetMap.set(target, (depsMap = new Map()))}// Each key has its own deP
let dep = depsMap.get(key)
if(! dep) { depsMap.set(key, (dep =new Set()))}Copy the code

When you use Reactive on an object, the reference to the current object is set to a key of targetMap, and a new Map, depsMap, is created and set to the value of that key. Because WeakMap’s key is a reference to an object, the corresponding depsMap can be found directly through target in the subsequent getter or setter logic. So you can think of targetMap as storing a target -> depsMap mapping.

What the hell is depsMap? In fact, it is similar to WeakMap, which is also a corresponding relationship of storage. In the reactive principle, a key corresponds to a DEP, so depsMap stores all the keys of an object and the relationship between each key and the DEP. During dependency collection, the target is used to find the depsMap, the key is used to find the DEP, and the current activeEffect is stored in the DEP.

Below I draw a simple schematic diagram, the specific relationship is as follows:

Implementation effect

Finally, the subscriber effect is the final element.

This is also a very important role in Vue3’s responsivity principle. Because so much of the component’s work is driven by the execution of the subscribers. So how exactly do these subscribers work?

Vue2 / Vue3 subscriber differences

The Vue2 subscriber is implemented through the Watcher class, and the Dep class has a static property of Target that records which Watcher is currently active. When a Watcher instance is created, the instance assigns its own reference to the target, and the current dep.target is collected when a dependency collection occurs during rendering. The same is true for Vue3, where Watcher becomes an effect function and dep. target becomes an activeEffect variable.

The effect function used in Vue3 to implement subscribers. It allows you to pass in a function and some configuration items and return a wrapped new function. For the sake of differentiation, we call this new function returned reactiveEffect. When executed, this reactiveEffect extends a new logic that assigns itself to the activeEffect without modifying the logic of the function passed in.

In Vue3 we create an effect render function for a component like this:

// instance is a component instance
instance.update = effect(
  // Need to wrap the rendering function
  function componentEffect() {
    console.log('Render component')}, {// Execute immediately after creation
    lazy: false})Copy the code

When a component is mounted, the render function is wrapped with this effect, generating a Render effect and executing it immediately. Render Effect points activeEffect to itself before executing componentEffect. The activeEffect is then collected from the data accessed during the component’s rendering process, so once the data is updated, the view’s rendering update can be triggered.

So how does this effect function add extra logic without invading the original code?

Packaging function

Let’s start with a concept called the Wrapper function. It is a common programming technique in functional programming.

Write a normal function:

function run() {
  console.log('run')}Copy the code

Now I need this function to print the start time of execution, but not change the original function code. At this point, the Wrapper function can be used.

// Implement a wrapper function
function wrapper(fn) {
  return function() {
    // Write the front logic that needs to be extended
    console.log(new Date())
    returnfn(... arguments) } }// Get a new function
const runAndPrintDate = wrapper(run)

runAndPrintDate()

// Output result:
// Fri May 07 2021 16:22:52 GMT+0800
// run
Copy the code

In this way, the new logic can be extended without modifying the code of the original function. In fact, the effect function is implemented in the same way.

Effect to realize

Specific implementation of effect:

// effect executes the stack
const effectStack = []

function effect(fn, options) {
  // Create a wrapped function
  const effect = createReactiveEffect(fn, options)
  // lazy determines whether to execute immediately
  if(! options.lazy) { effect() }return effect
}

// createReactiveEffect is the Wrapper function
function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
    if(! effectStack.includes(effect)) {try {
        // Effect is pushed to the execution stack
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        // Effect is expected to exit the stack after execution
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }

  effect.id = uid++
  effect.raw = fn
  effect.options = options

  return effect
}
Copy the code

There are two things to note about this code: the effectStack and options parameters.

An effectStack is an execution stack that records effect, and it primarily addresses the case of an effect nested call, a concept discussed in implementing Computed.

In Vue, there are many types of subscribers, and they have different requirements in different application scenarios. Therefore, an Options is needed to configure different situations. For example, when we created the Render effect for the component, we configured a lazy false property. Because you want to render the VNodeTree as soon as the render effect is created when the component is first mounted. However, some scenarios do not require it to be executed immediately upon creation, such as when creating listeners or evaluating properties. However, in the options configuration, the most important attribute is scheduler. It can be seen that an important step in the execution of the trigger function is to determine whether there is a scheduler when all effects in the DEP are executed, and if so, the execution will be scheduled by scheduler.

So why scheduler design? Let’s focus on the implementation of the asynchronous queue mechanism in Vue.

Implement asynchronous queue mechanism

Regardless of what the asynchronous queue mechanism is, let’s look at the original implementation of the code. Let’s write an example:

const vm = new Vue({
  setup() {
    const state = reactive({
      num: 100.person: {
        a: 1}})return {
      state
    }
  }
})

// Simulate the render effect generated when the component is mounted
effect(
  function componentEffect() {
    // Access values during simulation rendering
    console.log(vm.ctx.state.person.a)
    console.log('Render component')}, {lazy: false})// Modify the data
while (vm.ctx.state.person.a <= 100) {
  vm.ctx.state.person.a++
}

// Print "Render component" 100 times
Copy the code

In this example, if you change the state.person.a variable 100 times, the ‘render component’ will be printed 100 times. Remember that rendering components actually have a very complicated process – patch.

This process creates a new VNodeTree and performs a recursive comparison with the old VNodeTree to figure out the best way to manipulate the DOM, but this process is a bit cumbersome and requires a lot of recursive processing. Performing this process every time the data is modified is a waste of performance, and would be much faster than manipulating the DOM directly.

How does this work in Vue?

implementation

Because there is an asynchronous queue mechanism in Vue, tasks that use this mechanism are cached until all data has been modified before they are executed.

The core of this mechanism is, as its name suggests, “queuing” and “asynchrony.” The so-called “queue” is an array of cached tasks. All tasks scheduled by this mechanism will be cached in the queue and de-duplicated. “Asynchronous” is the way to execute tasks in an array. It pushes a method that executes all tasks in the “queue” into the microtask queue until the macro task completes, thus ensuring that the “summary” execution of all tasks is delayed. So how does this work?

Let’s first implement the process of caching tasks:

// Task queue
const queue = []

function queueJob(job) {
  if(! queue.includes(job)) { queue.push(job) queueFlush() } }function queueFlush() {
  // Execute the task
}
Copy the code

Now that we’re done caching the task, we can assign the queueJob to the Scheduler.

// Simulate the rendering effect generated when the component is mounted
effect(
  function componentEffect() {
    console.log('Render component')}, {lazy: false.scheduler: queueJob
  }
)
Copy the code

In this way, when modifying data, the current effect is stored in the queue first, and each effect is guaranteed to be unique. But there is a question, when is the best time to execute the task in queue?

Suppose we write queueFlush directly like this:

function queueFlush() {
  // Execute the task
  queue.forEach(job= > job())
  queue = []
}
Copy the code

This queue is meaningless, because the code is all synchronous, and even if it is saved, every change will be rendered directly, so you have to find a way to delay execution until all the assignment logic is done. This is done asynchronously, delaying rendering execution. So when is the best time to delay? Of course, this is when the current macro task completes and the microtask starts. So timing your execution in microtasks is best.

Let’s change our code:

// Task queue
const queue = []

// Indicates whether a microtask queue is enabled
let isFlushPending = false
// Indicates whether the task in queue is executing
let isFlushing = false

function queueJob(job) {
  if(! queue.includes(job)) { queue.push(job) queueFlush() } }function queueFlush() {
  if(! isFlushPending && ! isFlushing) { isFlushPending =true
    // Push the method that executes the task (flushJobs) into the microtask queue using the then method
    Promise.resolve().then(flushJobs)
  }
}

// Execute the task
function flushJobs() {
  isFlushPending = false
  isFlushing = true
  queue.forEach(job= > job())
  queue.length = 0
  isFlushing = false
}
Copy the code

To clarify the above logic, suppose that in a single interaction event, many pieces of data for a component are changed. The trigger logic corresponding to these data will be fired, and then the corresponding effect will be scheduled and executed using scheduler. The first data modification executes queueJob, pushes the render effect of the component to the task queue, and executes queueFlush, pushes the task flush function flushJobs to the microtask queue with a mark. Indicates that a refresh task function has been pushed in the microtask. The second, or even NTH, change won’t push flushJobs into the microtask, and there will only be a render effect in the queue. Once the macro task is completed, the microtask is executed in accordance with the js event loop mechanism, and flushJobs is pushed into the execution stack. This is the workflow in which multiple data changes are made and only one rendering is performed.

Here is a simple flow chart:

After the execution of this round of interactive events is completed, the JS execution stack starts to take tasks from the micro-task queue to be executed. In this way, all tasks in the queue are executed and the whole asynchronous update process is finished.

test

Here we change the effect we created:

// Simulate the rendering effect generated when the component is mounted
effect(
  function componentEffect() {
    // Access values during simulation rendering
    console.log(vm.ctx.state.person.a)
    console.log('Render component')}, {lazy: false.scheduler: queueJob
  }
)
Copy the code

Continue with the above example:

/ / modify the value
while (vm.ctx.state.person.a <= 100) {
  vm.ctx.state.person.a++
}
Copy the code

Results:

Very nice, even if the value is changed 100 times, it will render only the state of the last change.

Realize the watch

According to the responsive principle, the data can be collected by the render effect during the rendering process, and once the data is updated, it will automatically trigger the rendering again. Is there a way to manually add events to some data, and then do it automatically when the data changes?

The Watch API addresses this need. It allows us to listen for responsive data and then perform callbacks when the data changes. It depends on Effect to achieve, because the function wrapped by Effect can point the current activeEffect to itself before execution, so the Watch effect created by Watch will perform a GET on the value to be monitored during execution. This allows the monitored data to collect the Watch effect, which triggers a callback in the scheduler when the data changes.

Code implementation:

function watch(getter, callback) {
  if (typeofgetter ! = ='function') {
    return
  }

  let _getter = getter
  let oldValue

  /** * Performs the callback * calculates the new value, caches the old value */
  function job() {
    const newValue = getter()
    callback(newValue, oldValue)
    oldValue = newValue
  }

  // Create watch Effect
  const runner = effect(_getter, {
    lazy: true.scheduler: () = > {
      // Execute asynchronously
      queueJob(job)
    }
  })

  oldValue = runner()
}
Copy the code

Let’s test it out:

const vm = new Vue({
  setup() {
    const state = reactive({
      num: 100
    })

    watch(
      () = > state.num,
      (newVal, oldVal) = > {
        console.log('Trigger listener', newVal, oldVal)
      }
    )

    return {
      state
    }
  }
})

// Simulate the rendering effect generated when the component is mounted
effect(
  function componentEffect() {
    // Access values during simulation rendering
    console.log(vm.ctx.state.num)
    console.log('Render component')}, {lazy: false.scheduler: queueJob
  }
)

vm.ctx.state.num = 200
Copy the code

Take a look at the results:

The implementation of Watch is done.

To realize the computed

The compute properties API is a feature of Vue. It can create a computed object, and then the internal value can be computed based on the dependent data to obtain new values and trigger page rendering.

Take a look at the complete code implementation this time:

function computed(options) {
  let _getter
  let _setter
  let _computed
  let _value
  let _dirty = true

  if (typeof options === 'function') {
    _getter = options
    _setter = () = > {
      console.warn('computed _ is readonly')}}else {
    _getter = options.get
    _setter = options.set
  }

  // Create computed Effect
  let runner = effect(_getter, {
    lazy: true.scheduler: () = > {
      if(! _dirty) {// When the dependent reactive data changes
        // Computed is marked as dirty
        _dirty = true
        trigger(_computed, 'value')
      }
    }
  })

  _computed = {
    get value() {
      // Recalculate only if the data is dirty
      if (_dirty) {
        _value = runner()
        _dirty = false
      }
      track(_computed, 'value')
      return _value
    },
    set value(newValue) {
      return _setter(newValue)
    }
  }

  return _computed
}
Copy the code

The principles and implementation of computed are a little bit more complicated and convoluted to understand, so I’m going to go straight to the code I’ve already written. The code above is divided into three main parts: standardizing parameters, creating a Runner function, and creating a computed object and returning it.

A standardized parameter handles the different cases where the user passes in a getter, because the API allows the user to pass in a separate getter, or an object that contains a getter and setter. Here we focus on the Runner function, and the computed object created.

Runner function

The runner function is a wrapper around the getter. When it executes, it changes the actvieEffect to point to itself, and then executes the getter to calculate the new value, allowing the responsive data dependent within the getter to be collected to the Runner. When the data changes, the scheduler on runner is executed.

The computed object

This object maintains a value. What happens when computed value is accessed during rendering?

Because dirty defaults to true when initialized, runner is executed to calculate the new value and get the data dependent in the getter collected to Runner, followed by track(_computed, ‘value’), Let computed Value collect actvieEffect, because accessing value happens during rendering, so value collects the render effect function.

Now there are two questions:

  1. What does the getter rely on to collect data to runner
  2. Why can computed data still be collected after running runnerrender effect

First question. When the dependent data changes, the logic in Runner’s scheduler is executed, which marks dirty as true to tell computed that it is a dirty value, does not immediately recalcate the new value, and triggers render updates. When computed is accessed during rerendering and dirty is true, the calculation is recalculated.

Second question. Although computed access occurs during rendering, the current actvieEffect is the Render effect. But when runner is executed, runner already points the current actvieEffect to himself, so why does subsequent track(_computed, ‘value’) collect render effect?

Because of this nested use of effect, Vue designs an effectStack. Take a look back at the code in the Effect section:

if(! effectStack.includes(effect)) {try {
    // Effect is pushed to the execution stack
    effectStack.push(effect)
    activeEffect = effect
    return fn()
  } finally {
    // Effect is expected to exit the stack after execution
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]}}Copy the code

The execution of effect pushes itself onto the stack and points the actvieEffect to itself. When it finishes, it pushes itself off the stack and the actvieEffect points to the top of the stack, which is the last effect. This design solves the problem of nested effect calls.

So let’s imagine a situation here. When the Runner completes, if the current actvieEffect is not reverted to the previous effect, then the Runner has been collected in computed collection. This is a bad dependency collection, and the normal logic would be: Runner relies on data collection, computed collects the rendering function, so that the dependency data can be modified to tell computed and trigger computed collected rendering functions to re-render, but because Runner does not exit the stack correctly after executing, Causes computed and no render function to trigger.

Implement watchEffect

The watchEffect implementation is relatively simple.

function watchEffect(cb) {
  return effect(cb, {
    scheduler: queueJob
  })
}
Copy the code

This API is more like an effect wrapper, it immediately executes the incoming callback function, and if any reactive data is used in the callback function, that data is collected into the watchEffect and the data change triggers its execution.

The last

The first time to write this kind of super-long article 😂. I feel that I still have a lot of deficiencies in logic sorting, maybe some things in it are not very clear, if what is wrong, or not very clear, I hope you can give advice in the comment section. If there is a little bit of help for you, can you click like to pay attention to a wave, I will continue to work hard.