Better reading experience

This article will give you a comprehensive understanding of vUE’s rendering Watcher, computed and User Watcher. In fact, computed and user Watcher are based on Watcher. Let everyone fully understand the realization principle and the core idea. So this article will implement the following function points:

  • Implement data responsiveness
  • Based on the renderingwatherAchieve the first data rendering to the interface
  • Data depends on collection and updating
  • Implement data update trigger renderingwatcherExecute to update the UI interface
  • Based on thewatcherimplementationcomputed
  • Based on thewatcherimplementationuser watcher

Without further ado, let’s look at the final example below.

And then we’ll go straight to work.

The preparatory work

First we prepare an index. HTML file and a vue.js file. Let’s look at the code for index. HTML

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Fully understand VUE's rendering watcher, computed, and User atcher</title>
</head>
<body>
  <div id="root"></div>
  <script src="./vue.js"></script>
  <script>
    const root = document.querySelector('#root')
    var vue = new Vue({
      data() {
        return {
          name: 'Joe'.age: 10}},render() {
        root.innerHTML = `The ${this.name}----The ${this.age}`}})</script>
</body>
</html>
Copy the code

Index.html has a div node with the id root, which is the parent node, and then in the script tag, we introduce vue.js, which provides the vue constructor, and then instantiate vue, which takes an object with the data and render functions, respectively. Then let’s look at the vue.js code:

function Vue (options) {
  this._init(options) / / initialization
  this.$mount() // Execute the render function
}
Vue.prototype._init = function (options) {
  const vm = this
  vm.$options = options // Mount options to this
  if (options.data) {
    initState(vm) // Data responsive
  }
  if (options.computed) {
    initComputed(vm) // Initialize the calculated properties
  }
  if (options.watch) {
    initWatch(vm) // Initialize watch}}Copy the code

This._init() and this.$mount(). The this._init method is to initialize the configuration we passed in, This includes data initialization initState(VM), computed attributes initialization initComputed(VM), and customized watch initialization initWatch(VM). The this.$mount method renders the render function to the page. We’ll write about these methods later to give you an idea of the structure of the code. Now let’s formally fill in the methods we wrote above.

Implement data responsiveness

To implement these Watcher methods, the first step is to implement data responsiveness, which is to implement initState(VM) above. I’m sure you’re all familiar with reactive code, so I’ll just post it up.

function initState(vm) {
  let data = vm.$options.data; // Get the configured data attribute value
  // Determine whether data is a function or another type
  data = vm._data = typeof data === 'function' ? data.call(vm, vm) : data || {};
  const keys = Object.keys(data);
  let i = keys.length;
  while(i--) {
    // All data read from this is intercepted into this._data
    // For example, this.name equals this._data.name
    proxy(vm, '_data', keys[i]);
  }
  observe(data); // Data observation
}

// Data observation function
function observe(data) {
  if(! (data ! = =null && typeof data === 'object')) {
    return;
  }
  return new Observer(data)
}

// All data read from this is intercepted into this._data
// For example, this.name equals this._data.name
function proxy(vm, source, key) {
  Object.defineProperty(vm, key, {
    get() {
      return vm[source][key] // this.name is the same as this._data.name
    },
    set(newValue) {
      return vm[source][key] = newValue
    }
  })
}

class Observer{
  constructor(value) {
    this.walk(value) // Set get set for each attribute
  }
  walk(data) {
    let keys = Object.keys(data);
    for (let i = 0, len = keys.length; i < len; i++) {
      let key = keys[i]
      let value = data[key]
      defineReactive(data, key, value) // Set get set for the object}}}function defineReactive(data, key, value) {
  Object.defineProperty(data, key, {
    get() {
      return value
    },
    set(newValue) {
      if (newValue == value) return
      observe(newValue) // Set the response to the new value
      value = newValue
    }
  })
  observe(value); // Recursively set get set for data
}
Copy the code

All the important points are in the comments. The main core is to set get and set for the data recursed to data, and then set the data proxy so that this.name equals this._data.name. After setting up the data observation, we can see the data as shown below.

console.log(vue.name) / / zhang SAN
console.log(vue.age) / / 10
Copy the code

Ps: Array data observation we go to improve ha, here focuses on the implementation of Watcher.

For the first time to render

With the data observation done, we can render the render function into our interface. Prototype.$mount() : $this.$mount();

// Mount method
Vue.prototype.$mount = function () {
  const vm = this
  new Watcher(vm, vm.$options.render, () = > {}, true)}Copy the code

Wather is a function that executes the render function on Watcher and inserts data into the root node. Let’s look at the simplest implementation of Watcher

let wid = 0
class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm // Mount the VM to the current this
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn // Mount exprOrFn to the current this, where exprOrFn equals vm.$options.render
    }
    this.cb = cb // Attach cb to this
    this.options = options // Mount options to the current this
    this.id = wid++
    this.value = this.get() $options.render()
  }
  get() {
    const vm = this.vm
    let value = this.getter.call(vm, vm) // Point this to the VM
    return value
  }
}
Copy the code

We can now get data from this. Name in render and insert it into root.innerhtml. The phased work is done. As shown below, the completed first render ✌️

Data depends on collection and updating

First data collection, we need to have a place to collect, which is our Dep class, so let’s see how we can implement this Dep.

// Rely on collection
let dId = 0
class Dep{
   constructor() {
    this.id = dId++ // Each instantiation generates an ID
    this.subs = [] // Let the DEP instance collect the watcher
  }
  depend() {
    // dep. target is the current watcher
    if (Dep.target) {
      Dep.target.addDep(this) // Select deP from watcher and deP from watcher}}notify() {
    // Trigger the update
    this.subs.forEach(watcher= > watcher.update())
  }
  addSub(watcher) {
    this.subs.push(watcher)
  }
}

let stack = []
// Push the current watcher to stack and record the current watcer
function pushTarget(watcher) {
  Dep.target = watcher
  stack.push(watcher)
}
// Clear the current watcher after running
function popTarget() {
  stack.pop()
  Dep.target = stack[stack.length - 1]}Copy the code

The classes collected by Dep are implemented, but how we collect them is to instantiate Dep in our data observation get and let Dep collect the current Watcher. Here we go step by step:

  • 1. On topthis.$mount()In the code, we runnew Watcher(vm, vm.$options.render, () => {}, true)At this time we can atWatcherPerform insidethis.get()And then executepushTarget(this)“, you can execute this sentenceDep.target = watcher, put the currentwatchermountDep.targetOn. So let’s see how we can do that.
class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn
    }
    this.cb = cb
    this.options = options
    this.id = wid++
    this.id = wId++
    this.deps = []
    this.depsId = new Set(a)// DeP has already collected the same watcher
    this.value = this.get()
  }
  get() {
    const vm = this.vm
    pushTarget(this)
    let value = this.getter.call(vm, vm) // Execute the function
    popTarget()
    return value
  }
  addDep(dep) {
     let id = dep.id
     if (!this.depsId.has(id)) {
       this.depsId.add(id)
       this.deps.push(dep)
       dep.addSub(this); }}update(){
     this.get()
  }
}
Copy the code
  • 2, knowDep.targetWhat comes after, and then the above code runsthis.get(), which is equivalent to runningvm.$options.renderIn therenderInner loop executionthis.name, which triggersObject. DefineProperty · the getMethod, in which we can do some dependency collection (dep.depend), the following code
function defineReactive(data, key, value) {
  let dep = new Dep()
  Object.defineProperty(data, key, {
    get() {
       if (Dep.target) { // If the value is watcher
         dep.depend() // Let watcher save deP, and let DEP save watcher, two-way save
       }
      return value
    },
    set(newValue) {
      if (newValue == value) return
      observe(newValue) // Set the response to the new value
      value = newValue
      dep.notify() // Notify render Watcher to update}})// Recursively set get set for data
  observe(value);
}
Copy the code
  • 3, calleddep.depend()It’s actually calledDep.target.addDep(this)At this time,Dep.targetEqual to the currentwatcherAnd then it will be executed
addDep(dep) {
  let id = dep.id
  if (!this.depsId.has(id)) {
    this.depsId.add(id)
    this.deps.push(dep) // The current Watcher collects deP
    dep.addSub(this); // The current DEP collects the current watcer}}Copy the code

It’s a little bit convoluted in both directions, so you can understand it a little bit better. Let’s take a look at what the collected DES looks like.

  • 4, data update, callThis. name = 'li si'When back to triggerObject.defineProperty.setMethod, directly called insidedep.notify()And then loop through all of themwatcer.updateMethod to update allwatcherFor example, this is a re-executionvm.$options.renderMethods.

With the dependency to collect data updates, we also added a timed method to the index. HTML to modify the data attribute:

// index.html
<button onClick="changeData()"> change name and age</button>// -----
/ /... Omit code
function changeData() {
  vue.name = 'bill'
  vue.age = 20
}
Copy the code

The operation effect is shown below

At this point we’re rendering Watcher and it’s all done.

To realize the computed

First, we configure a computed script tag in index.html that looks like this:

const root = document.querySelector('#root')
var vue = new Vue({
  data() {
    return {
      name: 'Joe'.age: 10}},computed: {
    info() {
      return this.name + this.age
    }
  },
  render() {
    root.innerHTML = `The ${this.name}----The ${this.age}----The ${this.info}`}})function changeData() {
  vue.name = 'bill'
  vue.age = 20
}
Copy the code

In the code above, note that computed is used in Render.

In vue.js, I wrote the following line of code earlier.

if (options.computed) {
  // Initialize the calculated properties
  initComputed(vm)
}
Copy the code

Let’s now implement this initComputed, with the following code

// Initialize computed
function initComputed(vm) {
  const computed = vm.$options.computed // Get computed configuration
  const watchers = vm._computedWatchers = Object.create(null) // Mount the _computedWatchers attribute to the current VM
  // cycle computed for each attribute
  for (const key in computed) {
    const userDef = computed[key]
    // Determine whether it is a function or an object
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // Create a computed watcher for each computed object. Note {lazy: true}
    // Then mount to the vm._computedWatchers object
    watchers[key] = new Watcher(vm, getter, () = > {}, { lazy: true })
    if(! (keyin vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}
Copy the code

You know that computed is cached, so when you create a watcher, you pass a configuration {lazy: true}, and you can differentiate it from a computed watcher, and then you receive the object in watcer

class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn
    }
    if (options) {
      this.lazy = !! options.lazy// Designed for computed
    } else {
      this.lazy = false
    }
    this.dirty = this.lazy
    this.cb = cb
    this.options = options
    this.id = wId++
    this.deps = []
    this.depsId = new Set(a)this.value = this.lazy ? undefined : this.get()
  }
  // Omit a lot of code
}
Copy the code

Value = this.lazy? Use undefined: this.get() as you can see, computed watcher creation does not point to this.get. Only executed in the render function.

Now we can’t get a value in the render function through this.info because we haven’t mounted it to the VM yet, and defineComputed(VM, key, userDef) does this to mount computed to the VM. So let’s implement that.

// Set the comoputed set
function defineComputed(vm, key, userDef) {
  let getter = null
  // Determine whether it is a function or an object
  if (typeof userDef === 'function') {
    getter = createComputedGetter(key)
  } else {
    getter = userDef.get
  }
  Object.defineProperty(vm, key, {
    enumerable: true.configurable: true.get: getter,
    set: function() {} // Set = set ()})}// Create computed functions
function createComputedGetter(key) {
  return function computedGetter() {
    const watcher = this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {// Add subscription Watchers to computed attributes
        watcher.evaluate()
      }
      // Add the render watcher to the attribute's subscription, which is critical
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
Copy the code

Use watcher to evaluate() and watcher.depend(). Use watcher to evaluate() and watcher.depend().

class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn
    }
    if (options) {
      this.lazy = !! options.lazy// Designed for computed
    } else {
      this.lazy = false
    }
    this.dirty = this.lazy
    this.cb = cb
    this.options = options
    this.id = wId++
    this.deps = []
    this.depsId = new Set(a)// DeP has already collected the same watcher
    this.value = this.lazy ? undefined : this.get()
  }
  get() {
    const vm = this.vm
    pushTarget(this)
    // Execute the function
    let value = this.getter.call(vm, vm)
    popTarget()
    return value
  }
  addDep(dep) {
    let id = dep.id
    if (!this.depsId.has(id)) {
      this.depsId.add(id)
      this.deps.push(dep)
      dep.addSub(this); }}update(){
    if (this.lazy) {
      this.dirty = true
    } else {
      this.get()
    }
  }
  // Get and this.dirty = false
  evaluate() {
    this.value = this.get()
    this.dirty = false
  }
  // All attributes collect the current watcer
  depend() {
    let i = this.deps.length
    while(i--) {
      this.deps[i].depend()
    }
  }
}
Copy the code

So once we’ve done that, let’s talk about the process,

  • 1, first inrenderThe function reads itthis.info, this will triggercreateComputedGetter(key)In thecomputedGetter(key);
  • 2. Then judgewatcher.dirty, the implementation ofwatcher.evaluate();
  • 3, into thewatcher.evaluate()I really want to executethis.getMethod, which is executedpushTarget(this)The currentcomputed watcherPush it into the stack and push itDep.target is set to currentThe computed watcher `;
  • 4. Then runthis.getter.call(vm, vm)Equivalent to runningcomputedtheinfo: function() { return this.name + this.age }, this method;
  • 5,infoThe function will read itthis.name, which triggers data responsivenessObject.defineProperty.getStudent: HerenameWill do the dependency collection, thewatcerCollect the correspondingdepThe above; And returnsName = 'zhang SAN'The value of theageCollect the same;
  • 6. Execute dependencies after collection is completepopTarget(), put the currentcomputed watcherClears from the stack, returns the calculated value (‘ three +10’), andthis.dirty = false;
  • 7,watcher.evaluate()When it’s done, it’s judgedDep.targetIsn’t ittrueIf there is, there isRender the watcher, they performwatcher.depend()And then letwatcherThe inside of thedepscollectRender the watcherThis is the advantage of two-way saving.
  • 8, at this timenameAll collectedcomputed watcherRender the watcher. Then set thenameWill go to the update implementationwatcher.update()
  • 9. If socomputed watcherIt’s not going to be executed again, it’s just going to bethis.dirtySet totrue, if the data changeswatcher.evaluate()forinfoUpdate, no change in wordsthis.dirtyisfalse, will not be implementedinfoMethods. This is the computed caching mechanism.

After that, let’s look at the implementation:

Here conputed object set configuration is not implemented, you can see the source code

Watch implementation

First configure watch in the script tag and configure the following code:

const root = document.querySelector('#root')
var vue = new Vue({
  data() {
    return {
      name: 'Joe'.age: 10}},computed: {
    info() {
      return this.name + this.age
    }
  },
  watch: {
    name(oldValue, newValue) {
      console.log(oldValue, newValue)
    }
  },
  render() {
    root.innerHTML = `The ${this.name}----The ${this.age}----The ${this.info}`}})function changeData() {
  vue.name = 'bill'
  vue.age = 20
}
Copy the code

Now that you know the computed implementation, it’s easy to customize the watch implementation, so let’s implement initWatch directly

function initWatch(vm) {
  let watch = vm.$options.watch
  for (let key in watch) {
    const handler = watch[key]
    new Watcher(vm, key, handler, { user: true}}})Copy the code

Then modify Watcher to look directly at Wacher’s complete code.

let wId = 0
class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn
    } else {
      this.getter = parsePath(exprOrFn) // user watcher 
    }
    if (options) {
      this.lazy = !! options.lazy// Designed for computed
      this.user = !! options.user// For User Wather
    } else {
      this.user = this.lazy = false
    }
    this.dirty = this.lazy
    this.cb = cb
    this.options = options
    this.id = wId++
    this.deps = []
    this.depsId = new Set(a)// DeP has already collected the same watcher
    this.value = this.lazy ? undefined : this.get()
  }
  get() {
    const vm = this.vm
    pushTarget(this)
    // Execute the function
    let value = this.getter.call(vm, vm)
    popTarget()
    return value
  }
  addDep(dep) {
    let id = dep.id
    if (!this.depsId.has(id)) {
      this.depsId.add(id)
      this.deps.push(dep)
      dep.addSub(this); }}update(){
    if (this.lazy) {
      this.dirty = true
    } else {
      this.run()
    }
  }
  // Get and this.dirty = false
  evaluate() {
    this.value = this.get()
    this.dirty = false
  }
  // All attributes collect the current watcer
  depend() {
    let i = this.deps.length
    while(i--) {
      this.deps[i].depend()
    }
  }
  run () {
    const value = this.get()
    const oldValue = this.value
    this.value = value
    / cb/execution
    if (this.user) {
      try{
        this.cb.call(this.vm, value, oldValue)
      } catch(error) {
        console.error(error)
      }
    } else {
      this.cb && this.cb.call(this.vm, oldValue, value)
    }
  }
}
function parsePath (path) {
  const segments = path.split('. ')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if(! obj)return
      obj = obj[segments[i]]
    }
    return obj
  }
}
Copy the code

And finally look at the effect

Of course, many configurations are not implemented, such as options.immediate or options.deep. It’s too long. Oneself also lazy ~ ~ ~ end scatter flower

Detailed code: github.com/naihe138/wr…