takeaway

I remember when I first learned the Vue source code, jumping between defineReactive, Observer, Dep, Watcher, and other internal design sources, only to find that I couldn’t get around anymore. Vue development for a long time and a lot of fix and feature increased internal source is more and more huge, too many boundary condition and optimizing the design covers originally simplify the code design, is becoming more and more difficult for beginners to read source code, but the interview, the reactive principle of Vue is almost a Vue company senior front-end technology stack will ask one of the points.

This article through their own implementation of a responsive system, as far as possible to restore and Vue internal source code the same structure, but eliminate and render, optimization and so on related code, to the lowest cost of learning Vue responsive principle.

This article is based on Vue 2.4 version of the principle analysis, the subsequent version may be changed, but the principle is not very different.

preview

Source address: github.com/sl1673495/v…

Source address (js) github.com/sl1673495/v…

Sl1673495. github. IO /vue-reactiv…

reactive

The most common type of Vue is reactive data, defined in Vue

new Vue({
    data() {
        return {
            msg: 'Hello World'}}})Copy the code

When data changes, the view is also updated. In this article, I separate the processing of data into an API: Reactive, and implement this API together.

Effects to be achieved:

const data = reactive({
  msg: 'Hello World',})new Watcher((a)= > {
  document.getElementById('app').innerHTML = `msg is ${data.msg}`
})
Copy the code

When data. MSG changes, we need the innerHTML of the app node to be updated synchronously. A new concept called Watcher is added here, which is also a design within the Vue source code. This Watcher is essential for implementing a responsive system.

Before implementing these two apis, we need to clarify the relationship between them. The REACTIVE API defines reactive data, which is known to record who is reading it when a property (such as data.msg in this example) is read. The function that reads it must depend on it. In this case, the following function, because it reads data.msg and displays it on the page, can be said to rely on data.msg.

// Render function
document.getElementById('app').innerHTML = `msg is ${data.msg}`
Copy the code

This explains why we need to use new Watcher to pass in the render function. We can already see that Watcher is the key that helps us record the render function dependency.

MSG has been defined as responsive data. The get function triggered when reading data. MSG has been hijacked by us. In this get function, we record that data. MSG is dependent on this rendering function. Then return the value of data.msg.

This way, the next time data.msg changes, some of the logic Watcher has done inside will tell the rendering function to redo it. That’s how the reactive formula works.

Let’s start implementing the code

import Dep from './dep'
import { isObject } from '.. /utils'

// Define the object as reactive
export default function reactive(data) {
  if (isObject(data)) {
    Object.keys(data).forEach(key= > {
      defineReactive(data, key)
    })
  }
  return data
}

function defineReactive(data, key) {
  let val = data[key]
  // Collect dependencies
  const dep = new Dep()

  Object.defineProperty(data, key, {
    get() {
      dep.depend()
      return val
    },
    set(newVal) {
      val = newVal
      dep.notify()
    }
  })

  if (isObject(val)) {
    reactive(val)
  }
}

Copy the code

The code is simple. It just iterates through data’s keys, hijacking each key with get and set in defineReactive. Dep is a new concept. It is mainly used to do the above mentioned dep.depend() to collect the rendering function currently running and dep.notify() to trigger the rendering function to re-execute.

Dep can be regarded as a dependency collection basket. Whenever a rendering function is run to read a certain key of data, it will throw the rendering function into the key’s own basket. When the key value changes, it will find all the rendering functions in the key’s basket and execute again.

Dep

export default class Dep {
  constructor() {
    this.deps = new Set()
  }

  depend() {
    if (Dep.target) {
      this.deps.add(Dep.target)
    }
  }

  notify() {
    this.deps.forEach(watcher= > watcher.update())
  }
}

// Running watcher
Dep.target = null
Copy the code

This class uses Set to store data, adds dep. target to the deps Set (Depend), iterates through the deps Set (notify), and triggers updates for each watcher.

Dep. Target is a global variable that hangs on a Dep class. Js is single-threaded, so rendering functions like:

document.getElementById('app').innerHTML = `msg is ${data.msg}`
Copy the code

Before running, set the global dep. target to the watcher that stores the render function:

new Watcher((a)= > {
  document.getElementById('app').innerHTML = `msg is ${data.msg}`
})
Copy the code

In this way, data. MSG can find which render function watcher is currently running through dep. target on the run, so that it can collect its corresponding dependencies.

Here’s the key: Dep.target must be an instance of Watcher.

Because rendering functions can be nested, such as in Vue each component has its own watcher to store rendering functions, in the case of nested components:

// Parent component <template> <div> <Son component /> </div> </template>Copy the code

The watcher run path is: Start -> ParentWatcher -> SonWatcher -> ParentWatcher -> End.

Vue uses the stack data structure to record the trajectory of Watcher.

/ / watcher stack
const targetStack = []

// Push the last watcher onto the stack and update dep. target to the _target variable passed in.
export function pushTarget(_target) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

// Retrieve the previous watcher as dep. target and pop the previous watcher on the stack.
export function popTarget() {
  Dep.target = targetStack.pop()
}
Copy the code

With these auxiliary tools, you can take a look at Watcher in action

import Dep, { pushTarget, popTarget } from './dep'

export default class Watcher {
  constructor(getter) {
    this.getter = getter
    this.get()
  }

  get() {
    pushTarget(this)
    this.value = this.getter()
    popTarget()
    return this.value
  }

  update() {
     this.get()
  }
}

Copy the code

Recall the use of Watcher in the initial example.

const data = reactive({
  msg: 'Hello World',})new Watcher((a)= > {
  document.getElementById('app').innerHTML = `msg is ${data.msg}`
})
Copy the code

The getter that’s passed in is

() = > {document.getElementById('app').innerHTML = `msg is ${data.msg}`
}
Copy the code

In the constructor, the getter is recorded and get is executed

  get() {
    pushTarget(this)
    this.value = this.getter()
    popTarget()
    return this.value
  }
Copy the code

In this function,this is the watcher instance. Get begins by setting the watcher that stores the render function to the current dep.target, and then this.getter(), which is the render function

The hijacked get in defineReactive is triggered when data. MSG is read on the way to the render function:

Object.defineProperty(data, key, {
    get() {
      dep.depend()
      return val
    }
  })
Copy the code

Dep. depend

  depend() {
    if (Dep.target) {
      this.deps.add(Dep.target)
    }
  }

Copy the code

The dep. target collected is what pushTarget(this) collected at the start of the get function

new Watcher((a)= > {
  document.getElementById('app').innerHTML = `msg is ${data.msg}`
})
Copy the code

This is an instance of Watcher.

Suppose we execute an assignment like this:

data.msg = 'ssh'
Copy the code

The set function is hijacked:

  Object.defineProperty(data, key, {
    set(newVal) {
      val = newVal
      dep.notify()
    }
  })
Copy the code

The deP variable is printed in the console, and its internal DEPS property stores an instance of Watcher.

After running dep.notify, the Watcher update method is triggered and the render function is executed again, at which point the view is refreshed.

computed

Once you have implemented the reactive base API, you need to implement the computed API, which works like this:

const data = reactive({
  number: 1
})

const numberPlusOne = computed((a)= > data.number + 1)

// Render function watcher
new Watcher((a)= > {
  document.getElementById('app2').innerHTML = 'Computed: 1 + number is${numberPlusOne.value}
  `
})
Copy the code

Internally, vUE defines the computed property on the VM instance. Here we have no instance, so we use an object to store the returned value of computed, and use.value to get the real value of computed.

In this case, computed is actually a function, and the essence of Watcher is to store a function that needs to be triggered at a particular time. Inside the Vue, each computed property has its own instance of Watcher. It is hereafter called the computedWatcher

First look at the render function:

// Render function watcher
new Watcher((a)= > {
  document.getElementById('app2').innerHTML = 'Computed: 1 + number is${numberPlusOne.value}
  `
})
Copy the code

When I read the value of numberPlusOne during this render function

The dep. target is first set to the computedWatcher corresponding to numberPlusOne

The computedWatcher is special in this way

  1. Render Watcher can only be collected as a dependency in other DEP baskets, whilecomputedWatcherThe instance has its own DEP on it, which can collect other thingswatcherAs their own dependence.
  2. Lazy evaluation, initialization without running the getter.
export default class Watcher {
  constructor(getter, options = {}) {
    const { computed } = options
    this.getter = getter
    this.computed = computed

    if (computed) {
      this.dep = new Dep()
    } else {
      this.get()
    }
  }
}
Copy the code

The essence of a computed implementation is that, before a value is read, dep. target must be the watcher of the rendering function that is running.

The watcher of the currently running rendering function is first collected as a dependency into the DEP basket inside the computedWatcher.

Set its own computedWatcher to the global DEP.target and evaluate:

The evaluation function will be running

() => data.number + 1
Copy the code

The Dep. Target is a computedWatcher, and the Dep dependency basket of data.number is thrown into the computedWatcher.

The dependency at this point is that the DeP basket of Data. number holds the computedWatcher, and the DEP basket of the computedWatcher holds the render Watcher.

If data.number is updated, the update will be triggered level by level. ComputedWatcher update is triggered. The render Watcher is installed in the DEP of the computedWatcher, so simply triggering this.dep.notify() triggers the render Watcher update method to update the view.

The path to the update is data.number = 5 -> computedWatcher -> Render Watcher -> Update view

Let’s change the code:

// Watcher
import Dep, { pushTarget, popTarget } from './dep'

export default class Watcher {
  constructor(getter, options = {}) {
    const { computed } = options
    this.getter = getter
    this.computed = computed

    if (computed) {
      this.dep = new Dep()
    } else {
      this.get()
    }
  }

  get() {
    pushTarget(this)
    this.value = this.getter()
    popTarget()
    return this.value
  }

  // For computed use only
  depend() {
    this.dep.depend()
  }

  update() {
    if (this.computed) {
      this.get()
      this.dep.notify()
    } else {
      this.get()
    }
  }
}
Copy the code

Computed initialization:

// computed
import Watcher from './watcher'

export default function computed(getter) {
  let def = {}
  const computedWatcher = new Watcher(getter, { computed: true })
  Object.defineProperty(def, 'value', {
    get() {
      // First let computedWatcher collect render Watcher as its own dependency.
      computedWatcher.depend()
      // In the function passed in by the execution user, the responsive-type values are collected into the 'computedWatcher' again
      return computedWatcher.get()
    }
  })
  return def
}
Copy the code

If data.number is set to be hijacked, you can download the code and debug it step by step. After the set is triggered, you can see what the deP of number is.

watch

The use of watch is as follows:

watch(
  (a)= > data.msg,
  (newVal, oldVal) => {
    console.log('newVal: ', newVal)
    console.log('old: ', oldVal)
  }
)
Copy the code

The first parameter passed in is a function that needs to read the reactive property to make sure the dependency is collected so that the next time the reactive property changes, the new and old values are printed.

The Watcher for watch is called watchWatcher. The getter function passed in is () => data.msg. WatchWatcher still sets itself to dep. target before executing it, and when it reads data. MSG, it drops the watchWatcher into the dependency basket of data. MSG.

If data. MSG is updated, the watchWatcher update method is triggered

Directly on the code:

// watch
import Watcher from './watcher'

export default function watch(getter, callback) {
  new Watcher(getter, { watch: true, callback })
}

Copy the code

{watch: true, callback}}

export default class Watcher {
  constructor(getter, options = {}) {
    const { computed, watch, callback } = options
    this.getter = getter
    this.computed = computed
    this.watch = watch
    this.callback = callback
    this.value = undefined

    if (computed) {
      this.dep = new Dep()
    } else {
      this.get()
    }
  }
}
Copy the code

First, the watch option and callback are saved in the constructor, but nothing else is changed.

Then in the update method.

  update() {
    if (this.computed) {
     ...
    } else if (this.watch) {
      const oldValue = this.value
      this.get()
      this.callback(this.value, oldValue)
    } else{... }}Copy the code

Before calling this.get to update the value, save the old value, and pass the new value and the old value to the outside world by calling the callback function.

With just a few lines of code changes, we easily implemented a very important API: Watch.

Summary.

With the clever design of Watcher and Dep, the implementation of the responsive API inside Vue is very simple.