Students, do you want to learn the data response principle of Vue but do not know how to start? Have you ever been discouraged by complex source code tutorials? If, like me, you’ve worked on a project and you want to get into the basics, you’ve come to the right place. This series of articles will lead you to implement a responsive system of your own, starting from the purely Vue responsive principle, without the interference of other factors.

A friendly note: since our code will go through multiple versions, I hope that you will be able to type the code involved when reading this article, so that you can understand.

Project address: Gitee

Serial address:

2: Array processing

3: Render watcher

4: The final chapter

The foreword 0.

Data models are just plain JavaScript objects. And when you change them, the view updates.

With Vue, we only need to change the data (state) and the view can be updated accordingly. This is a responsive system. To implement a responsive system of our own, we first need to understand what we need to do:

  1. Data hijacking: There are certain things we can do when data changes
  2. Dependency collection: We need to know the contents of those view layers (DOMWhat data does it depend on (state)
  3. Dispatch updates: How are those dependent on the data notified when they changeDOM

Next, we’ll take steps to implement a toy responsive system of our own

Data hijacking

Almost all articles and tutorials begin with the Vue responsive system: Vue uses object.defineProperty to hijack data. So, we also start with data hijacking, you may be confused about the concept of hijacking, no matter, after reading the following content, you will understand.

The usage of Object. DefineProperty is not introduced here, but students who do not understand can look up in MDN. Next, let’s define an a attribute for obj

const obj = {}

let val = 1
Object.defineProperty(obj, a, {
  get() { // This method is called the getter
    console.log('get property a')
    return val
  },
  set(newVal) { // This method is referred to as the setter
    if (val === newVal) return
    console.log(`set property a -> ${newVal}`)
    val = newVal
  }
})
Copy the code

A = 2 to set the new value, print set property a -> 2. This is equivalent to defining the value and assignment behavior of OBj. A, and overwriting the original behavior with custom getters and setters. This is what data hijacking means.

But there is a problem with the above code: we need a global variable to hold the value of the property, so we can write it like this

// value uses the default parameter value
function defineReactive(data, key, value = data[key]) {
  Object.defineProperty(data, key, {
    get: function reactiveGetter() {
      return value
    },
    set: function reactiveSetter(newValue) {
      if (newValue === value) return
      value = newValue
    }
  })
}

defineReactive(obj, a, 1)
Copy the code

What if OBj has multiple attributes? We can create a new class Observer to traverse this object

class Observer {
  constructor(value) {
    this.value = value
    this.walk()
  }
  walk() {
    Object.keys(this.value).forEach((key) = > defineReactive(this.value, key))
  }
}

const obj = { a: 1.b: 2 }
new Observer(obj)
Copy the code

What if obj has nested properties? We can use recursion to complete data hijacking for nested properties

// Entry function
function observe(data) {
  if (typeofdata ! = ='object') return
  / / call the Observer
  new Observer(data)
}

class Observer {
  constructor(value) {
    this.value = value
    this.walk()
  }
  walk() {
    // Iterate over the object and data hijack
    Object.keys(this.value).forEach((key) = > defineReactive(this.value, key))
  }
}

function defineReactive(data, key, value = data[key]) {
  // If value is an object, call observe recursively to monitor the object
  // If value is not an object, observe returns directly
  observe(value)
  Object.defineProperty(data, key, {
    get: function reactiveGetter() {
      return value
    },
    set: function reactiveSetter(newValue) {
      if (newValue === value) return
      value = newValue
      observe(newValue) // Set new values to be listened for}})}const obj = {
  a: 1.b: {
    c: 2
  }
}

observe(obj)
Copy the code

Now, if you’re a little confused about this part, here’s a quick recap:

Observe (obj).Exercises - - New Observer(obj).exercises: This.walk() Exercise defineReactive() Exercises ── exercise defineReactive(obj, a) Exercises ── Exercise observe(obj.a) DefineReactive (obj, a) = = defineReactive(obj, a) B) ├ ─ ─ execution observe (obj. B) found that obj. B is object ├ ─ ─ to perform new Observer (obj. B), traversing the obj. B properties, DefineReactive () Exercises ── Exercises defineReactive(obj.b, c) Exercises ── Exercises observe(obj.b.c) To find obj.b.c is not an object, Return to the rest of the code that executes defineReactive(obj. B, c) The code execution is overCopy the code

As you can see, the relationship between the above three functions is as follows:

The three functions call each other to form a recursion, which is different from normal recursion. Now, some of you might be thinking, well, why don’t you just call the render function in the setter to rerender the page, and then you’re done updating the page when the data changes? Yes, but the cost of doing so is that any change in the data will cause the page to be rerendered, which is too high. We want to have the effect that when data changes, only the DOM structure related to that data is updated, which brings us to the following: dependencies

2. Collect dependencies and distribute updates

Rely on

Before we dive into dependency collection, let’s take a look at what a dependency is. Take a real-life example: Shopping on Taobao. Now a taobao store has a graphics card (air) in the pre-sale stage, if we want to buy, we can click the pre-sale reminder, when the graphics card began to sell, Taobao push a message for us, we see the message, can start to buy.

This example is abstracted as a public-subscription model: buyers click on pre-sale alerts, which is equivalent to registering their own information (subscription) on Taobao, and Taobao will save buyers’ information in a data structure (such as array). When the graphics card is officially open for purchase, Taobao will notify all buyers: graphics card is sold (release), buyers will carry out some actions according to this news (such as buying back mining).

In a Vue responsive system, the graphics card corresponds to the data, so what is the buyer in this example? Is an abstract class: Watcher. You don’t need to know what the name means, just what it does: Each Watcher instance subscribers to one or more pieces of data, also known as Wacther dependencies. When the dependency changes, the Watcher instance receives a message that the data has changed and then executes a callback function to implement some functionality, such as updating the page (where the buyer does some action).

So the Watcher class can be implemented as follows

class Watcher {
  constructor(data, expression, cb) {
    // data: data object, such as obj
    // expression: an expression, such as b.c. Data and expression are used to obtain the data that watcher depends on
    // cb: the callback triggered when the dependency changes
    this.data = data
    this.expression = expression
    this.cb = cb
    // Subscribe data when initializing the Watcher instance
    this.value = this.get()
  }
  
  get() {
    const value = parsePath(this.data, this.expression)
    return value
  }
  
  // This method is executed when a message is received that the data has changed, thus calling cb
  update() {
    this.value = parsePath(this.data, this.expression) // Update the stored data
    cb()
  }
}

function parsePath(obj, expression) {
  const segments = expression.split('. ')
  for (let key of segments) {
    if(! obj)return
    obj = obj[key]
  }
  return obj
}
Copy the code

If you have any questions about when Watcher will be instantiated, that’s fine, we’ll get to that in a second, okay

In fact, there is one more point that we didn’t mention in the previous example: as mentioned in the video card example, Taobao stores the buyer information in an array, so our responsive system should also have an array to store the buyer information, namely watcher.

To summarize the functions we need to implement:

  1. I have an array for thatwatcher
  2. watcherThe instance needs to subscribe (dependency) data, that is, acquire or collect dependencies
  3. watcherTriggered when the dependency ofwatcher, which dispatches updates.

Each data should maintain its own array to house its own dependent Watcher, and we can define an array DEP in defineReactive so that each property has its own DEP through closures

function defineReactive(data, key, value = data[key]) {
  const dep = [] / / add
  observe(value)
  Object.defineProperty(data, key, {
    get: function reactiveGetter() {
      return value
    },
    set: function reactiveSetter(newValue) {
      if (newValue === value) return
      value = newValue
      observe(newValue)
      dep.notify()
    }
  })
}
Copy the code

At this point, we implement the first function, and then we implement the process of collecting dependencies.

Depend on the collection

Now let’s focus on the first render of the page (ignoring the render functions and the virtual DOM for a moment) : the render engine parses the template. For example, if the engine encounters an interpolation, what happens if we instantiate a Watcher? As you can see from Watcher’s code, when we instantiate, we execute the get method, which is used to get the data that it depends on, and we override the data access behavior by defining getters for each data, so that the getter function executes, If we add the current watcher in the getter to the DEP array (Taobao low registered buyer information), not to be able to complete the dependency collection!!

Note: The get method of new Watcher() has not finished executing by the time it reaches the getter.

New Watcher() executes constructor, calls the instance’s get method, which reads the value of the data, thus triggering the getter of the data. After the getter completes, the instance’s get method completes, returns the value, constructor completes. The instantiation is complete.

Some students may have a question: watcher is clearly collecting dependencies, should be watcher collecting data, how to become data deP collection watcher? Those who have this question can take a look at the previous example of Taobao (where user information was recorded), or take a deeper look at the publish-and-subscribe model.

With the above analysis, we only need to make a few changes to the getter:

get: function reactiveGetter() {
  dep.push(watcher) / / new
  return value
}
Copy the code

Again, where does watcher come from? We instantiated watcher in the template compiler function, and the getter cannot fetch the instance. The solution is as simple as placing the watcher instance globally, such as window.target. Therefore, Watcher’s get method is modified as follows

get() {
  window.target = this / / new
  const value = parsePath(this.data, this.expression)
  return value
}
Copy the code

To do this, change the dep.push(watcher) in the get method to dep.push(window.target).

Note that window.target = new Watcher() cannot be written like this. Window. target is still undefined because the watcher is not instantiated by the time the getter is executed

Dependency collection process: When rendering a page, you encounter interpolation, v-bind, etc., where you need data, you instantiate a Watcher, which evaluates the dependent data and triggers a getter, which then adds its own dependency watcher, thus completing dependency collection. We can understand that the Watcher is collecting dependencies, and the code implements this by storing the watcher dependent on itself in the data

Careful readers may notice that this method creates a new watcher for every interpolation encountered, so that each node corresponds to a watcher. In fact, this is the practice of vuE1.x, which updates by node with fine granularity. In vue2.x, each component corresponds to a Watcher. When the watcher is instantiated, it is passed in a rendering function instead of an expression. The rendering function is converted from the template of the component, so that the watcher of a component can collect all its dependencies and update them by component. Is a medium grained approach. There are many other things involved in implementing the responsive system of Vue 2.x, such as componentization, virtual DOM, etc. This article series focuses only on the data responsive principle and therefore cannot implement vue2.x, but the responsive principle is the same for both.

Distributed update

After implementing dependency collection, the last function we need to implement is to dispatch updates, which is to trigger watcher’s callback when the dependency changes. We know from the dependency collection section that the data that is fetched, that is, the getter that is triggered on the data, is the data that Watcher depends on. How do we notify Watcher when the data changes? I’m sure many of you already guessed it: send updates in setters.

set: function reactiveSetter(newValue) {
  if (newValue === value) return
  value = newValue
  observe(newValue)
  dep.forEach(d= > d.update()) // See the Watcher class for the new update method
}
Copy the code

3. Optimize code

1. The Dep

We can abstract the dep array into a class:

class Dep {
  constructor() {
    this.subs = []
  }

  depend() {
    this.addSub(Dep.target)
  }

  notify() {
    const subs = [...this.subs]
    subs.forEach((s) = > s.update())
  }

  addSub(sub) {
    this.subs.push(sub)
  }
}
Copy the code

The defineReactive function only needs to be modified accordingly

function defineReactive(data, key, value = data[key]) {
  const dep = new Dep() / / modify
  observe(value)
  Object.defineProperty(data, key, {
    get: function reactiveGetter() {
      dep.depend() / / modify
      return value
    },
    set: function reactiveSetter(newValue) {
      if (newValue === value) return
      value = newValue
      observe(newValue)
      dep.notify() / / modify}})}Copy the code

2. window.target

In watcher’s get method

get() {
  window.target = this // Window.target is set
  const value = parsePath(this.data, this.expression)
  return value
}
Copy the code

You may have noticed that we did not reset window.target. Some of you may think this is fine, but consider the following scenario: we have an object obj: {a: 1, b: 2} and we instantiate watcher1. Watcher1 depends on obj.a, so window. Target is watcher1. And then we visit OBJ.B, and what happens? Accessing obj.b triggers obj.b’s getter, which calls dep.depend(), and obj.b’s dep collects window.target, which is watcher1. This causes Watcher1 to depend on obj.b, which is not the case. To solve this problem, we make the following modifications:

Watcher's get method
get() {
  window.target = this
  const value = parsePath(this.data, this.expression)
  window.target = null // Add, and reset window.target after evaluation
  return value
}

// Dep depends
depend() {
  if (Dep.target) { / / new
    this.addSub(Dep.target)
  }
}
Copy the code

As you can see from the above analysis, window.target refers to the watcher instance in the current execution context. Due to the single-threaded nature of JS, only one watcher’s code is executing at a time, so window.target is the watcher that is currently being instantiated

3. The update method

We previously implemented the update method as follows:

update() {
  this.value = parsePath(this.data, this.expression)
  this.cb()
}
Copy the code

Recall that the vm.$watch method allows you to access this in a defined callback that receives the new and old values of the listening data, so make the following changes

update() {
  const oldValue = this.value
  this.value = parsePath(this.data, this.expression)
  this.cb.call(this.data, this.value, oldValue)
}
Copy the code

4. Learn the Vue source code

In the Vue source code — line 56, we see a variable named targetStack that looks like it has something to do with our window.target, and yes, it does. Consider a scenario where we have two nested parent and child components. When the parent component is rendered, a new watcher is created for the parent component. When the child component is discovered during the rendering process, the child component is rendered and a new watcher is created for the child component. In our implementation, when we create a parent watcher, window.target will point to the parent watcher, then create a child watcher, window.target will cover the child watcher, and when the child is rendered, we return to the parent watcher, Window. target becomes null, which causes a problem, so we use a stack structure to hold the watcher.

const targetStack = []

function pushTarget(_target) {
  targetStack.push(window.target)
  window.target = _target
}

function popTarget() {
  window.target = targetStack.pop()
}
Copy the code

The get method of Watcher is modified as follows

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

In addition, the use of dep. target instead of window.target in Vue to hold the current watcher doesn’t matter much, as long as there is a globally unique variable to hold the current watcher

Summarize the code

The code is summarized as follows:

// Call this method to detect the data
function observe(data) {
  if (typeofdata ! = ='object') return
  new Observer(data)
}

class Observer {
  constructor(value) {
    this.value = value
    this.walk()
  }
  walk() {
    Object.keys(this.value).forEach((key) = > defineReactive(this.value, key))
  }
}

// Data intercept
function defineReactive(data, key, value = data[key]) {
  const dep = new Dep()
  observe(value)
  Object.defineProperty(data, key, {
    get: function reactiveGetter() {
      dep.depend()
      return value
    },
    set: function reactiveSetter(newValue) {
      if (newValue === value) return
      value = newValue
      observe(newValue)
      dep.notify()
    }
  })
}

/ / rely on
class Dep {
  constructor() {
    this.subs = []
  }

  depend() {
    if (Dep.target) {
      this.addSub(Dep.target)
    }
  }

  notify() {
    const subs = [...this.subs]
    subs.forEach((s) = > s.update())
  }

  addSub(sub) {
    this.subs.push(sub)
  }
}

Dep.target = null

const TargetStack = []

function pushTarget(_target) {
  TargetStack.push(Dep.target)
  Dep.target = _target
}

function popTarget() {
  Dep.target = TargetStack.pop()
}

// watcher
class Watcher {
  constructor(data, expression, cb) {
    this.data = data
    this.expression = expression
    this.cb = cb
    this.value = this.get()
  }

  get() {
    pushTarget(this)
    const value = parsePath(this.data, this.expression)
    popTarget()
    return value
  }

  update() {
    const oldValue = this.value
    this.value = parsePath(this.data, this.expression)
    this.cb.call(this.data, this.value, oldValue)
  }
}

// Utility functions
function parsePath(obj, expression) {
  const segments = expression.split('. ')
  for (let key of segments) {
    if(! obj)return
    obj = obj[key]
  }
  return obj
}

// for test
let obj = {
  a: 1.b: {
    m: {
      n: 4
    }
  }
}

observe(obj)

let w1 = new Watcher(obj, 'a'.(val, oldVal) = > {
  console.log(` obj. From a${oldVal}(oldVal) turned out to be${val}(newVal)`)})Copy the code

4. Precautions

1. The closure

Vue is so powerful thanks to closures: Closures are formed in defineReactive so that each attribute of each object can hold its own value and dependent object DEP.

2. Will dependencies be collected as soon as getters are triggered

The answer is no. In the depend method of Dep, we see that dependencies are added only if dep. target is true. For example, watcher’s update method will trigger parsePath, but dep. target will be null and no dependencies will be added. Target is null all the time, and get is only called when watcher is instantiated. Therefore, in our implementation, A watcher’s dependency is already determined when it is instantiated, and any subsequent reading of the value does not increase the dependency.

3. Rely on nested object properties

Consider the following in the context of the above code:

let w2 = new Watcher(obj, 'b.m.n'.(val, oldVal) = > {
  console.log(` obj. B.M.N from${oldVal}(oldVal) turned out to be${val}(newVal)`)})Copy the code

We know that W2 depends on J.P. B.M.N, but does W2 depend on J.P. B, J.P. M? In other words, obj.b and obj.b.m, do they have w2 in the deP stored in their closures? The answer is yes. B = null, then it’s obvious that w2’s callback should be fired, indicating that w2 relies on the intermediate level of object properties.

New Watcher() will call Watcher’s get method and set Dep. Target to w2. Get will call parsePath.

function parsePath(obj, expression) {
  const segments = expression.split('. ') // segments:['b', 'm', 'n']
  // Loop values
  for (let key of segments) {
    if(! obj)return
    obj = obj[key]
  }
  return obj
}
Copy the code

The above code flow is as follows:

  1. A local variableobjFor the objectobjRead,obj.bIs triggeredgetterTo triggerdep.depend()(thedepisobj.bIn the closure ofdep),Dep.targetExists, add dependency
  2. A local variableobjforobj.bRead,obj.b.mIs triggeredgetterTo triggerdep.depend()(thedepisobj.b.mIn the closure ofdep),Dep.targetExists, add dependency
  3. A local variableobjFor the objectobj.b.mRead,obj.b.m.nIs triggeredgetterTo triggerdep.depend()(thedepisobj.b.m.nIn the closure ofdep),Dep.targetExists, add dependency

As you can see from the above code, W2 will depend on each item related to the target attribute, which is also logical.

5. To summarize

To summarize:

  1. callobserve(obj)That will beobjSet to a responsive object,Observe, observe, defineReactiveThe three call each other, recursivelyobjSet to a reactive object
  2. Instantiate when rendering the pagewatcherThe process is completed by reading the values that depend on the dataGet the dependency in the getter
  3. Triggered when the dependency changessetterTo dispatch the update, perform the callback, and completeDispatches updates in setters

Of a hole

Strictly speaking, the responsive system we have now completed cannot be used to render the page, because the watcher that actually renders the page does not need to set the callback function, which we call the render watcher. In addition, the render Watcher can take a render function instead of an expression and automatically rerender when dependencies change, which creates the problem of duplicate dependencies. In addition, another important thing that we haven’t covered yet is the handling of arrays.

If you don’t understand the questions mentioned above, it’s ok. Later articles in this series will solve these problems step by step. I hope you can continue to pay attention.

If you like, please see an officer point like it!!