Finally, rendering Watcher. After reading this article, you should be able to implement a responsive system that can be represented on the page.

Source code address: gitee

Series of articles:

1. Fundamentals

2. Array processing

4. The final chapter

Vue project summary series of articles:

  1. The infrastructure
  2. Login and permission control

Continuously updated…

What is rendering Watcher

There are many types of Watcher in vUE, and the watcher we implemented earlier is similar to vue.$watch, which executes a callback function when the dependency changes. Rendering Watcher does not require a callback function. Rendering Watcher receives a rendering function instead of a dependency expression, and automatically executes the rendering function when the dependency changes

new Watcher(app, renderFn)
Copy the code

So how do we re-execute the rendering function when the dependency changes? We need to make some changes to Watcher’s constructor first

constructor(data, expOrFn, cb) {
  this.data = data
  / / modify
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn
  } else {
    this.getter = parsePath(expOrFn)
  }
  this.cb = cb
  this.value = this.get()
}

// parsePath's transformation returns a function
function parsePath(path) {
  const segments = path.split('. ')
  return function (obj) {
    for (let key of segments) {
      if(! obj)return
      obj = obj[key]
    }
    return obj
  }
}
Copy the code

In this case, this.getter is a value function, and get changes

get() {
  pushTarget(this)
  const data = this.data
  const value = this.getter.call(data, data) / / modify
  popTarget()
  return value
}
Copy the code

To re-execute the render function depending on the change, an update needs to be made during the distribution update phase, so the update method also needs to be modified:

update() {
  // re-execute the get method
  const value = this.get()
  // Render watcher's value is undefined because the render function returns no value
  // Therefore value and this.value are both undefined and will not enter if
  // If the dependency is an object, an update is triggered
  if(value ! = =this.value || isObject(value)) {
    const oldValue = this.value
    this.value = value
    this.cb.call(this.vm, value, oldValue)
  }
}

function isObject(target) {
  return typeof target === 'object'&& target ! = =null
}
Copy the code

You may wonder why you can’t just use this.getter.call(this.data) to re-execute the render function, which involves re-collecting dependencies as discussed below. But before we do that, we need to address a problem: duplicate collection of dependencies

Repeated dependency

Look at an example like this

<div>
  {{ name }} -- {{ name }}
</div>
Copy the code

If we render the template, then render Watcher will rely on name twice. Because name is read twice and getter is fired twice, dep. target is the current watcher. In the Depend method,

depend() {
  if (Dep.target) {
    dep.addSub(Dep.target)
  }
}
Copy the code

Dependencies are collected twice, and two rerenders are triggered when the name changes. So VUE takes the following approach

Start by adding an ID for each DEP

let uid = 0

constructor() {
  this.subs = []
  this.id = uid++ / / add
}
Copy the code

Watcher added four attributes: deps, depIds, newDeps, and newDepIds

this.deps = []             // store the deP from the last evaluation
this.depIds = new Set(a)// Store the id of the deP that was stored when it was evaluated last time
this.newDeps = []          // store your own deP when storing this evaluation
this.newDepIds = new Set(a)// store the id of the deP when storing this evaluation
Copy the code

After each value, deP and newDep are swapped and newDep is emptied, as described below

The idea is that when a Watcher needs to be collected, it’s up to the Watcher to decide if it needs to be collected by the DEP. In the example above, we assume that when name is evaluated, Watcher is collected by DEP1, and when name is evaluated a second time, Watcher finds that it has already been collected by DEP1 and does not collect again

// dep.depend
depend() {
  if (Dep.target) {
    Dep.target.addDep(this) // Let Watcher decide if he is collected by deP}}// watcher.addDep
addDep(dep) {
  const id = dep.id
  // If you have not been collected by deP during the evaluation process, enter if
  if (!this.newDepIds.has(id)) {
    // Record in watcher to collect your own DP
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)}}}Copy the code

Now to explain the last if, consider the case of rerendering: Watcher depends on name, name has changed, so the get method on watcher is going to reevaluate name, and when it goes into addDep, newDepIds is empty, so it’s going to go into if, it’s going to go to the last if, because the first time it evaluates, Dep has already collected the Watcher, so it should not be added again. This if is what it does.

Vue Tech Insider summed it up well:

  1. NewDeps and newDepIds are used to avoid repeated dependencies during re-evaluation, such as {{name}} — {{name}}

  2. Deps and depIds are used to avoid repeated dependencies during re-rendering of values

The get method clears newDeps and newDepIds

cleanUpDeps() {
    // Swap depIds and newDepIds
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    / / empty newDepIds
    this.newDepIds.clear()
    // Swap deps and newDeps
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    / / empty newDeps
    this.newDeps.length = 0
  }
Copy the code

Dependency re-collection

Dependency recollection as I understand it consists of two parts: collecting new dependencies and removing invalid dependencies. The Dep. Target method is present only during the execution of the get method. The Dep. Target method is present only during the execution of the get method. Instead of get.call (), we used the get method in the update method to refire the render function. It is also necessary to re-collect the dependencies, as in the case of V-IF, so the current reactive system is an improvement over the previous version of fixed dependencies.

To remove invalid dependencies, you can add the following code to cleanUpDeps

cleanUpDeps() {
  / / add
  let i = this.deps.length
  while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)}}let tmp = this.depIds
  // ...
}
Copy the code

At the end of evaluation (that is, the end of dependency collection), if it is found that some DEPs collected themselves in the last evaluation but did not collect themselves in the evaluation this time, it indicates that the data does not need to collect themselves, and it can be deleted from the DEP

// Dep.js
removeSub(sub) {
  remove(this.subs, sub)
}

function remove(arr, item) {
  if(! arr.length)return
  const index = arr.indexOf(item)
  if (index > -1) {
    return arr.splice(index, 1)}}Copy the code

In this way, our responsive system is more complete

conclusion

The rendering Watcher is not much different from other Watcher systems, except that it automatically executes the rendering function when a dependency changes.

The next article will make a simple template compiler to integrate our responsive system with page rendering, and will implement v-Model bi-directional binding, please pay attention.

If you feel the article is ok, please click a “like”!!