One, foreword

An overview of the

Data models are just plain old JavaScript objects. And when you modify them, the view is updated.

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 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 (DOM) depends on what data (state)
  3. Distribute updates: How do I notify those dependent on data when it changesDOM

Next, we will implement a toy responsive system of our own step by step

Front knowledge

1. Vue monitors all levels of data in data

  • How do I monitor data in objects?

Monitoring is implemented through setters and the data to be monitored is passed in at new Vue

  1. Object, Vue does not respond by default
  2. To respond to attributes added later, use the following API
Vue.set(target, propertyName/index, value)
vm.$set(target, propertyName/index, value)
Copy the code
  • How do I detect data in an array?

By wrapping the array update element, we essentially do two things:

  1. Call the method corresponding to the native array to update the array
  2. Reparse the template to update the page

To modify an element in a Vue array, do the following:

API: Push () pop() shift() unshift() splice() sort() reverse() vue.set () or vm.$setCopy the code

Note: Vue.set() and vm.$set() cannot add attributes to a VM or its root data object

2. Object.defineProperty()

Juejin. Cn/post / 699508…

  • withgetterandsetterMethod can listen on data, and access and Settings are captured by listeners
  • Getters fire when data is read, setters fire when data is modified

Data hijacking

Almost all articles and tutorials on Vue responsive systems start with the following: Vue uses Object.defineProperty for data hijacking. So, let’s also start with data hijacking. You might be a little confused about the concept of hijacking. That’s ok.

The usage of object.defineProperty is not explained here, you can check it on MDN if you don’t understand. Next, we define an A property for obj

const obj = {}

let val = 1
Object.defineProperty(obj, a, {
  get() { This method is referred to as the getter for the rest of the article
    console.log('get property a')
    return val
  },
  set(newVal) { This method is collectively referred to as setter in the following sections
    if (val === newVal) return
    console.log(`set property a -> ${newVal}`)
    val = newVal
  }
})
Copy the code

Thus, when we access obj. A, print get Property a and return 1, and when obj. A = 2 sets a new value, print Set Property a -> 2. This is equivalent to customizing the behavior of obj. A values and assignments, using custom getters and setters to override the original behavior, which is what data hijacking is all about.

Many of you might be wondering, why do we have one herevalInstead of directly in get and set functionsreturn obj.aandobj.a = val?

If we return obj. A directly from get, obJ. A will also call get once, which will result in an infinite loop. The same is true for set, so we use a third-party variable val to prevent an infinite loop.

But if we need to delegate more properties, it is not possible to define a third party variable for each property, which can be solved with closures

usedefineReactivefunction

So we define a function to encapsulate defineProperty to implement data hijacking. Using defineReactive, we don’t need to set temporary variables, but use closures

// value uses the default value of the parameter
function defineReactive(data, key, value) {
  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

Reactive handling of objects and arrays

  • An Observer is an objectObject or arrayWhere responsive processing takes place
  • DefineReactive: Intercept every one on the objectkeytheGet and set functionsThe place where
  • Observe: Entry to responsive processing

The flow goes something like this: observe-> Observer -> defineReactive -> observe-> Observer -> defineReactive Recursively

const { arrayMethods } = require('./array')
const obj = {
  a: 1.b: {
    c: 2
  }
}

observe(obj)

function observe(value) {
    // If an object or array is passed in, it is processed responsively
    if (Object.prototype.toString.call(value) === '[object Object]' || Array.isArray(value)) {
        return new Observer(value)
    }
}

// Observer objects, it is easier to build them using es6 classes -- to convert a normal object into an object whose properties at each level are responsive (detectable)
class Observer {
    constructor(value) {
        // Set an __ob__ object to the value object or array passed in
        // The __ob__ object is very useful. If the __ob__ object is attached to the value, the value has been processed in response
        Object.defineProperty(value, '__ob__', {
            value: this.// The value is this, which is an Observer instance of new
            enumerable: false.// Cannot be enumerated
            writable: true.// We can rewrite __ob__ with the assignment operator
            configurable: true // Can be overwritten and deleted
        })

        // Determine whether value is a function or an object
        if(Array.isArray(value)) {
            // Modify the array prototype if it is an array
            value.__proto__ = arrayMethods
            // If the array contains an array, you need to recurse
            this.observeArray(value)
        } else {
            // If it is an object, the walk function is executed to process the object responsively
            this.walk(value)
        }
    }

    walk(data) {
        // Get all the keys of the data object
        let keys = Object.keys(data)
        // The value of each key is processed responsively
        for(let i = 0; i < keys.length; i++) {
            const key = keys[i]
            const value = data[key]
            // Pass in the data object, key, and value
            defineReactive(data, key, value)
        }
    }

    observeArray(items) {
        // Iterate over the array passed in, if the array still contains the array, then recursive processing
        for(let i = 0; i < items.length; i++) {
            observe(items[i])
        }
    }
}

function defineReactive(data, key, value) {
    // Recursive important steps
    // Because there may be objects or arrays in the object, recursion is required
    observe(value)


    / / core
    // Intercept the get and set attributes of each key in the object
    // This implements the underlying principle that both read and write can be captured and responsive
    Object.defineProperty(data, key, {
        get() {
            console.log('Get value')
            return value
        },
        set(newVal) {
            if (newVal === value) return
            console.log('Set value')
            value = newVal
        }
    })
}
Copy the code

Because interception of array subscripts is too wasteful, it adds to the array judgment of the data arguments passed to the Observer constructor

// src/obserber/index.js
class Observer {
  / / observations
  constructor(value) {
    Object.defineProperty(value, "__ob__", {
      // The value refers to an instance of an Observer
      value: this.// Not enumerable
      enumerable: false.writable: true.configurable: true}); }}Copy the code

Before we rewrite the array prototype, we need to understand this code. What this code means is that we add an unenumerable __ob__ attribute to each responsive data and point to an Observer instance. Therefore, we can use this attribute to prevent the data that has already been observed by the responsive data from being observed again and again Reactive data can use __ob__ to get methods related to Observer instances, which is critical for arrays

// src/obserber/array.js
// Keep the array prototype
const arrayProto = Array.prototype;
// arrayMethods is then inherited from the array prototype
// Here is the idea of Slice-oriented programming (AOP) -- dynamically extending functionality without breaking encapsulation
export const arrayMethods = Object.create(arrayProto);
let methodsToPatch = [
  "push"."pop"."shift"."unshift"."splice"."reverse"."sort",]; methodsToPatch.forEach((method) = > {
  arrayMethods[method] = function (. args) {
    // The execution result of the prototype method is preserved here
    const result = arrayProto[method].apply(this, args);
    // This sentence is the key
    // this represents the data itself. For example, if the data is {a:[1,2,3]}, then we use a.paush (4). This is a ob, which is a.__ob__
    const ob = this.__ob__;

    // Add a new operation to the array
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
      default:
        break;
    }
    [// If there are new elements inserted is an array] Call the observeArray of the Observer instance to observe each item in the array
    if (inserted) ob.observeArray(inserted);
    // We can also detect that the array has changed and trigger the view update operation -- source code will be revealed later
    return result;
  };
});
Copy the code

Why objects and arrays are handled separately:

  • objectAttributes are usually few, hijacking every one of themSet and get, does not consume much performance
  • An array ofThere could be thousands of elements, if each one were hijackedSet and getIt definitely consumes too much performance
  • soobjectthroughdefinePropertyA normal hijackingSet and get
  • An array ofbyModify part of the method on the array prototypeTo achieveModify array trigger response

For this part, you may be a little dizzy, so let’s comb it out:

Observe (obj) ├─ New Observer(obj) And This.walk () DefineReactive () ├── defineReactive(OBj, a) ├─ ObJ. ├─ defineReactive(obj, a) Remainder of code ├─ defineReactive(obj, B) ├─ Observe (obj. B) ├─ New Observer(obJ. B).ObJ. B DefineReactive () ├─ Perform defineReactive(obJ. B, c) ├─ Perform ObJ. B.c Go straight back to ├─ Perform Remainder of ├─ perform Define Active (obj, b) code endCopy the code

As can be seen, the above three functions are called as follows:

The three functions call each other to form recursion, which is different from normal recursion. Some of you might be thinking, well, if I just call a render function in the setter to rerender the page, wouldn’t that be enough to update the page when the data changes? It can, but the price of doing this is that any data change will cause the page to be rerendered, which is too high a price. What we want to do is update only the DOM structures associated with the data as it changes, which brings us to dependencies

2. Collect dependencies and send updates

Rely on

Before getting into dependency collection, let’s look at what a dependency is. Take a life example: Taobao shopping. Now there is a video card (air) in a taobao shop in the pre-sale stage, if we want to buy, we can click the pre-sale reminder, when the video card is sold, Taobao push a message for us, we can see the message, we can start to buy.

To abstract this example, it’s a publish-subscribe model: when a buyer clicks on a pre-sale alert, he or she registers his or her information on Taobao (subscription), which taobao stores in a data structure (such as an array). When the graphics card is officially open for purchase, Taobao will notify all buyers: the graphics card is sold (released), and buyers will carry out some actions according to this information (such as buying back mining).

In a Vue responsive system, the graphics card corresponds to the data, so what does the buyer in this example correspond to? It’s an abstract class: Watcher. You don’t have to think about what the name means, just what it does: Each Watcher instance subscribs to one or more pieces of data, also known as Wacther dependencies (goods are the buyer’s dependencies); When a dependency changes, the Watcher instance receives a message that the data has changed and then executes a callback function to perform some function, such as updating the page (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: expression, such as b.c, can obtain the data that Watcher depends on according to data and expression // cb: This.data = data this.expression = expression this.cb = cb // Subscribe to data when initialingWatcher instance this.value = this.get()} Get () {const value = parsePath(this.data, this.expression) return value} Cb update() {this.value = parsePath(this.data, this.expression) cb()}} function parsePath(obj, expression) { const segments = expression.split('.') for (let key of segments) { if (! Obj) return obj = obj[key]} return obj} Copy codeCopy the code

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

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

To summarize the features we need to implement:

  1. There’s an array to store itwatcher
  2. watcherInstances need to subscribe to (dependency) data, that is, get dependencies or collect dependencies
  3. watcherTriggered when a dependency changeswatcherTo send out updates.

Each data should maintain its own array to hold its own dependent watcher. 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() } }) } Duplicate codeCopy the code

At this point, we have implemented the first feature, and then we implement the process of collecting dependencies.

Depend on the collection

Now let’s focus on the first rendering of the page (ignoring rendering functions, virtual DOM, etc.) : The rendering engine parses the template, and if the engine encounters an interpolation, what happens if we instantiate a Watcher? The get method gets the data that we depend on, and we rewrite the data access behavior, and we define a getter for each data, so the getter will execute, If we add the current watcher to the DEP array in the getter, we can complete the dependency collection!!

Note: The get method of new Watcher() is not finished when the getter is executed.

New Watcher () constructor, call the instance of the get method, the instance of the get method will read the value of the data, triggering the data of the getter, complete after getter, instances of the get method is performed, and the return value, the constructor is done, The instantiation is complete.

Watcher is the deP of data collection. Watcher is the deP of data collection. Watcher is the deP of data collection. Those of you who have this question can revisit the previous taobao example (where Taobao records user information) or take a closer look at the publish-subscribe model.

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

Get: function reactiveGetter() {dep.push(watcher) // add return valueCopy the code

So again, where does watcher come from? We’re instantiating Watcher in a template-compiled function, and we can’t get that instance in the getter. The solution is simply to place the watcher instance globally, such as window.target. Therefore, Watcher’s get method is modified as follows

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

So, change 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 undefined because the instantiation of watcher is not complete when the getter is executed

Dependency collection process: When rendering a page, it encounters interpolation expressions, and v-bind or other places that need data will instantiate a Watcher. The instantiated Watcher will evaluate the dependent data and trigger the getter, and the getter function of the data will add its own dependent Watcher to complete the dependency collection. We can think of watcher as collecting dependencies, and the code is implemented by storing the dependent Watcher in the data

Careful readers may notice that with this approach, a new Watcher is created for each interpolation encountered, so that each node has a watcher. This is actually what Vu1.x does, with fine-grained updates on a per-node basis. In ve2. X, there is a Watcher for each component. When instantiating the Watcher, it is no longer an expression, but a rendering function, which is converted from the template of the component. It’s a medium grained approach. There are a lot of other things involved in implementing a responsive system for VUe2. X, such as componentization, virtual DOM, etc. This series of articles will only focus on the principles of data responsiveness, so it cannot be implemented for VUe2.

Distributed update

After implementing dependency collection, the last thing we want to do is distribute updates, which trigger watcher’s callback when a dependency changes. We know from the dependency collection section that what data is fetched, that is, what data getter is triggered, tells watcher what data it depends on. How do we notify Watcher when data changes? As many of you have already guessed, send updates in the setter.

set: function reactiveSetter(newValue) { if (newValue === value) return value = newValue observe(newValue) dep.forEach(d => D.update ()) // Add the update method see Watcher class} copy codeCopy the code

3. Optimize code

1. The Dep

We can abstract the DEP array as 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 codeCopy 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 reactitter () {dev.depend ()}, set: Function reactiveSetter(newValue) {if (newValue === value) return value = newValue observe(newValue) dep.notify() // Modify }})} copy the codeCopy the code

2. window.target

In watcher’s get method

Get () {window.target = this // Sets window.target const value = parsePath(this.data, this.expression) return value} copy codeCopy the code

You may have noticed that we didn’t reset window.target. Some students might think this is fine, but consider the following scenario: we have an object obj: {a: 1, b: 2} and we instantiate watcher1, which depends on obj. Then we visit OBJ.B. What happens? Accessing obj. B triggers obJ. B’s getter, which calls DEP.depend (), and then OBj. B’s DEP collects window.target, which is watcher1, which causes Watcher1 to rely on obj. B, which is not the case. To solve this problem, we make the following modifications:

// Get () {window.target = this const value = parsePath(this.data, This. Expression) window.target = null // Dep depend() {if (dep.target) {// add this.addSub(dep.target)}}Copy the code

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

3. The update method

The update method we implemented earlier looks like this:

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

Recalling the vm.$watch method, we can access this in a defined callback that receives both the old and new 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)} copies the codeCopy the code

4. Learn about Vue source code

In the Vue source code — line 56, we see a variable called targetStack, which looks like it’s related to our window.target, and yes, it is. Imagine a scenario where we have two nested parent components. When the parent component is rendered, a new watcher of the parent component is created. When a child component is found during rendering, the child component is rendered, and a new watcher of the child component is created. In our implementation, when the parent component watcher is created, window.target will point to the parent component watcher, then create a child component watcher, window.target will overwrite the quilt component Watcher, the child component is rendered, and when the parent component Watcher is returned, Window.target becomes null, which is a problem, so we use a stack structure to hold watcher.

const targetStack = [] function pushTarget(_target) { targetStack.push(window.target) window.target = _target } function PopTarget () {window.target = targetstack.pop ()} copies the codeCopy the code

The Watcher get method is modified as follows

Get () {pushTarget(this) // Modify const value = parsePath(this.data, this.expression) popTarget() // modify return value} copy codeCopy the code

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

5. Summarize the code

The code is summarized as follows:

Function observe(data) {if (typeof data! == '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))}} 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() } }) 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)}} 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.a changed from ${oldVal}(oldVal) to ${val}(newVal) ')}) copies the codeCopy the code

4. Precautions

1. The closure

This is possible 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 the getter is triggered

The answer is no. In Dep’s Depend method, we see that dependencies are added only if dep. target is true. For example, watcher’s update method is triggered when an update is sent. This method also triggers parsePath, but dep. target is null and no dependency is added. PushTarget (this) is called to dep. target only when watcher is instantiated. Otherwise, dep. target is null and get is called only when watcher is instantiated. A Watcher dependency is defined at instantiation time, and any subsequent reading of the value does not increase the dependency.

3. Rely on nested object properties

Let’s combine the above code to consider the following question:

let w2 = new Watcher(obj, 'b.m.n', (val, OldVal) => {console.log(' obj.b.m.n from ${oldVal}(oldVal) to ${val}(newVal) ')}) copies the codeCopy the code

We know that w2 will depend on obj.b.m.n, but will w2 depend on obj.b, obj.b.m? Or, obj.b, and obj.b.m, do they have W2 in the DEP stored in their closure? The answer is yes. Without looking at the code, imagine that if we set obj.b = null, then it’s obvious that w2’s callback should be triggered, which means that W2 relies on the object properties in the middle.

When new Watcher() is called, the get method of Watcher will be called. Set dep. target to w2, and the get method will call parsePath.

Function parsePath(obj, expression) {const segments = expression.split('.') // Segments :['b', 'm', 'n'] // loop value for (let key of segments) {if (! Obj) return obj = obj[key]} return obj} Copy codeCopy the code

The above code flow is as follows:

  1. A local variableobjFor the objectobjRead,obj.bThe value of triggergetterTo triggerdep.depend()(thedepisobj.bIn the closure ofdep),Dep.targetIf yes, add a dependency
  2. A local variableobjforobj.bRead,obj.b.mThe value of triggergetterTo triggerdep.depend()(thedepisobj.b.mIn the closure ofdep),Dep.targetIf yes, add a dependency
  3. A local variableobjFor the objectobj.b.mRead,obj.b.m.nThe value of triggergetterTo triggerdep.depend()(thedepisobj.b.m.nIn the closure ofdep),Dep.targetIf yes, add a dependency

As you can see from the code above, it is logical for W2 to rely on each item associated with the target attribute.

5. To summarize

To sum up:

  1. callobserve(obj)That will beobjSet to a responsive object,Observe, observe, defineReactiveAll three call each other, recursivelyobjSet as a responsive object
  2. Instantiate when rendering the pagewatcherThe process completes by reading the values of the dependent dataGet the dependency in the getter
  3. Triggered when a dependency changessetterTo issue updates, perform callbacks, and completeDistribute updates in the setter

Of a hole

Strictly speaking, the responsive system we have now completed cannot be used to render pages, because the actual watcher for rendering pages does not require callback functions. We call it rendering Watcher. In addition, the render Watcher can accept a render function instead of an expression as an argument, and automatically re-render when the dependency changes, which creates the problem of duplicate dependencies. Another important thing we haven’t covered yet is the handling of arrays.

It doesn’t matter if you can’t understand the problems mentioned above. Later articles in this series will solve these problems step by step. I hope you can continue to pay attention to them.