preface

As one of the must-test questions in the Vue interview, Vue’s responsive principle, presumably used by the Vue students are not unfamiliar, Vue official documents to pay attention to the responsive problems have also done a detailed description.

However, for those who are new to or don’t know much about it, they may still feel confused: why can’t the object attributes be added or deleted? Why not set array members by index? I believe that after reading this article, you will be suddenly enlightened.

This article will be combined with Vue source code analysis, for the whole responsive principle step by step in-depth. Of course, if you already have some knowledge and understanding of the reactive principle, you can go ahead and implement part of MVVM

The article repository and source code are at 🍹🍰 fe-Code, welcome to Star.

Vue is not exactly an MVVM model, so read it carefully.

While not entirely following the MVVM model, the design of Vue was inspired by it. Therefore, the variable name VM (short for ViewModel) is often used to represent Vue instances in documentation. – Vue website

Vue official responsive schematic town building.

thinking

Before getting into the subject, let’s consider the following code.

<template>
    <div>
        <ul>
            <li v-for="(v, i) in list" :key="i">{{v.text}}</li>
        </ul>
    </div>
</template>
<script>
    export default{
        name: 'responsive'.data() {
            return {
                list: []}},mounted() {
            setTimeout(_= > {
                this.list = [{text: Awesome!}, {text: Awesome!}, {text: Awesome!}];
            },1000);
            setTimeout(_= > {
                this.list.forEach((v, i) = > { v.text = i; });
            },2000)}}</script>
Copy the code

We know that in Vue, attributes defined in data are data-hijacked via Object.defineProperty to support publish subscriptions for related operations. In our case, data only defines list as an empty array, so Vue hijacks it and adds the corresponding getter/setter.

So at 1 s, reassigning to list with this.list = [{text: 666}, {text: 666}, {text: 666}] triggers the setter and notifys the corresponding observer (in this case template compilation) to make the update.

At 2 s, we iterate through the array again, changing the text property of each list member, and the view is updated again. List [I] = {text: I}, the data will update normally, but the view will not. As mentioned earlier, there is no support for setting array members by index.

But when we use v.text = I, the view updates normally. Why? As mentioned above, Vue can hijack the attributes of data, but the attributes of list members are not data hijack, why can update the view?

This is because when we do setters on a list, we first determine if the new value assigned is an object, and if it is, we hijack it again and add the same observer as the list.

Let’s modify the code a bit more:

// View adds v-if conditional judgment<ul>
    <li v-for="(v, i) in list" :key="i" v-if="v.status === '1'">{{v.text}}</li>
</ul>// The status attribute is added for 2 seconds. mounted() { setTimeout(_ => { this.list = [{text: 666}, {text: 666}, {text: 666}]; }, 1000); setTimeout(_ => { this.list.forEach((v, i) => { v.text = i; v.status = '1'; // Add state}); }}, 2000)Copy the code

As above, we added the v-if state judgment in the view, and set the state at 2 s. But instead of showing 0, 1, and 2 at 2s, as we expected, the view stays blank.

This is a mistake many novices make because there are often similar requirements. This is where we mentioned earlier that Vue cannot detect the addition or deletion of object attributes. What should we do if we want to achieve the desired results? Is simple:

// The status attribute is preset when the assignment is performed at 1 s.
setTimeout(_= > {
    this.list = [{text: Awesome!.status: '0'}, {text: Awesome!.status: '0'}, {text: Awesome!.status: '0'}];
},1000);
Copy the code

Of course, Vue also provides the vm.$set(target, key, value) method to handle the operation of adding attributes in certain cases, but it is not applicable here.

Vue responsive principle

We’ve given two examples of common mistakes and solutions, but we still only know what to do, not why.

Vue’s data hijacking relies on Object.defineProperty, so some of its features cause this problem. For those of you who don’t know this property look at MDN.

Basic implementation of Object.defineProperty

The object.defineProperty () method directly defines a new property on an Object, or modifies an existing property of an Object, and returns the Object. – MDN

Look at a basic case of data hijacking, which is the most fundamental dependency of responsiveness.

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true./ / can be enumerated
        configurable: true.get: function() {
            console.log('get');
            return val;
        },
        set: function(newVal) {
            // When setting, you can add corresponding operations
            console.log('set'); val += newVal; }}); }let obj = {name: 'Jackie Chan Brother'.say: 'I actually refused to do this game commercial,'};
Object.keys(obj).forEach(k= > {
    defineReactive(obj, k, obj[k]);
});
obj.say = 'And THEN I tried it, and I was like, wow, it was fun.';
console.log(obj.name + obj.say);
// Jackie Chan: ACTUALLY, I refused to shoot the advertisement for this game before. Then I tried it. Wow, I am so passionate, it is fun
obj.eat = 'banana'; // ** has no response
Copy the code

As you can see, Object.defineProperty is a hijacking of an existing attribute, so Vue requires that the required data be defined in data beforehand and cannot respond to the addition or deletion of Object attributes. Hijacked attributes have corresponding GET and set methods.

In addition, the Vue documentation states that due to JavaScript limitations, Vue does not support setting array members by index. In this case, it’s actually possible to just hijack an array by subscripting it.

let arr = [1.2.3.4.5];
arr.forEach((v, i) = > { // Hijack by subscript
    defineReactive(arr, i, v);
});
arr[0] = 'oh nanana'; // set
Copy the code

So why didn’t Vue do that? Utah’s official answer is performance. For a more detailed analysis of this point, you can move on. Why can’t Vue detect array changes?

Vue source code implementation

The following code is Vue version 2.6.10.

Observer

We know the basic implementation of data hijacking, and take a look at how Vue source code is done.

// observer/index.js
// Observer preprocessing method
export function observe (value: any, asRootData: ? boolean) :Observer | void {
  if(! isObject(value) || valueinstanceof VNode) { // Is it an object or a virtual DOM
    return
  }
  let ob: Observer | void
  // Check if there is an __ob__ attribute. If there is an __ob__ attribute, return an Observer instance
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if ( // Check if it is a pure objectshouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value) / / create the Observer
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

/ / the Observer instance
export class Observer { 
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep() // Add an instance of Dep to the Observer for dependency collection, auxiliary vm.$set/ array methods, etc
    this.vmCount = 0
    // Add an __ob__ attribute to the hijacked object, pointing to its own Observer instance. As a unique identifier of whether or not an Observer.
    def(value, '__ob__'.this)
    if (Array.isArray(value)) { // Check if it is an array
      if (hasProto) { // Determine whether the __proto__ attribute, used to handle array methods, is supported
        protoAugment(value, arrayMethods) / / inheritance
      } else {
        copyAugment(value, arrayMethods, arrayKeys) / / copy
      }
      this.observeArray(value) // Hijack an array member
    } else {
      this.walk(value) // Hijacking objects
    }
  }

  walk (obj: Object) { // This method is used only if the value is Object
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]) // Data hijacking method
    }
  }

  observeArray (items: Array<any>) { // If it is an array, call observe to process the array member
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]) // Process the array members in turn}}}Copy the code

__ob__ has a deP attribute, which is used as a collector for vm.$set, array push, etc. Vue then treats objects and arrays separately. Arrays only listen deeply to object members, which is why we can’t manipulate indexes directly. However, some array methods, such as push and pop, can respond properly. This is because of the above processing to determine whether the response object is an array. Let’s look at the code.

// observer/index.js
import { arrayMethods } from './array'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

// export function observe
if (Array.isArray(value)) { // Check if it is an array
  if (hasProto) { // Determine whether the __proto__ attribute, used to handle array methods, is supported
    protoAugment(value, arrayMethods) / / inheritance
  } else {
    copyAugment(value, arrayMethods, arrayKeys) / / copy
  }
  this.observeArray(value) // Hijack an array member
}
/ /...

// Inherit arrayMethods directly
function protoAugment (target, src: Object) { 
  target.__proto__ = src
}
// Copy the array methods in turn
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

// util/lang.js The def method looks like this, used to add attributes to objects
export function def (obj: Object, key: string, val: any, enumerable? : boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable:!!!!! enumerable,writable: true.configurable: true})}Copy the code

You can see that the key point is arrayMethods. Let’s continue:

// observer/array.js
import { def } from '.. /util/index'

const arrayProto = Array.prototype // Store the method on the array prototype
export const arrayMethods = Object.create(arrayProto) // Create a new object instead of changing the array prototype method directly

const methodsToPatch = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]

// Override the array method above
methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (. args) { // 
    const result = original.apply(this, args) // Execute the specified method
    const ob = this.__ob__ // Get the ob instance of the array
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2) // The first two arguments received by splice are subscripts
        break
    }
    if (inserted) ob.observeArray(inserted) // The new part of the original array needs to be reobserved
    // notify change
    ob.dep.notify() // Publish manually, using an __ob__ deP instance
    return result
  })
})
Copy the code

As you can see, Vue overwrites some of the array methods and publishes them manually when they are called. But we haven’t seen the data hijacking part of Vue yet. In the first observer function code, there is a defineReactive method. Let’s look at it:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function, shallow? : boolean) {
  const dep = new Dep() // instance a Dep instance

  const property = Object.getOwnPropertyDescriptor(obj, key) // Get the object's own properties
  if (property && property.configurable === false) { // There is no need to hijack if there are no attributes or attributes are not writable
    return
  }

  // Compatible with predefined getters/setters
  const getter = property && property.get
  const setter = property && property.set
  if((! getter || setter) &&arguments.length === 2) { // Initialize val
    val = obj[key]
  }
  // Defaults to listen on child objects, starting with observe and returning an __ob__ attribute, an Observer instance
  letchildOb = ! shallow && observe(val)Object.defineProperty(obj, key, {
    enumerable: true.configurable: true.get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val // Execute the default getter to get the value
      if (Dep.target) { // Rely on the collection key
        dep.depend() // Dependency collection takes advantage of function closures
        if (childOb) { // Add the same dependency if there are child objects
          childOb.dep.depend() // Observer this.dep = new dep ();
          if (Array.isArray(value)) { // call the array method if value is an array
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      // Compare the old value with the new value
      // newVal ! == newVal && value ! == value this is a little bit more interesting, but it's really for NaN
      if(newVal === value || (newVal ! == newVal && value ! == value)) {return
      }
      if(process.env.NODE_ENV ! = ='production' && customSetter) {
        customSetter()
      }
      if(getter && ! setter)return
      if (setter) { // Execute the default setter
        setter.call(obj, newVal)
      } else { // No default direct assignmentval = newVal } childOb = ! shallow && observe(newVal)// Whether to observe the newly set value
      dep.notify() // published to take advantage of function closures}})}// Handle arrays
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend() // If the array member has __ob__, add the dependency
    if (Array.isArray(e)) { // Array members are still arrays, recursive calls
      dependArray(e)
    }
  }
}
Copy the code

Dep

In the above analysis, we understand Vue data hijacking and array method rewriting, but we have a new question, what is Dep for? Dep is a publisher that can be subscribed to by multiple observers.

// observer/dep.js

let uid = 0
export default class Dep {
  statictarget: ? Watcher; id: number; subs:Array<Watcher>;

  constructor () {
    this.id = uid++ / / the only id
    this.subs = [] // Set of observers
  }
 // Add an observer
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
 // Remove the observer
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  
  depend () { // Core, if there is a dep. target, then the dependency collection operation
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    const subs = this.subs.slice() // Avoid contaminating the original collection
    // If the execution is not asynchronous, sort first to ensure that the observer executes in order
    if(process.env.NODE_ENV ! = ='production' && !config.async) {
      subs.sort((a, b) = > a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // Publish execution
    }
  }
}

Dep.target = null // Core, used for closures, holds specific values
const targetStack = []
// Assign the current Watcher to dep. target and add it to the target stack
export function pushTarget (target: ? Watcher) {
  targetStack.push(target)
  Dep.target = target
}
// Remove the last Watcher and assign the last remaining target stack to dep.target
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]}Copy the code

Watcher

Dep may not be easy to understand in isolation, but let’s look at it in conjunction with Watcher.

// observer/watcher.js

let uid = 0
export default class Watcher {
  // ...
  constructor (
    vm: Component, // Component instance object
    expOrFn: string | Function.// The expression, function, or string to observe, as long as it triggers the value operation
    cb: Function.// Callback after observed changes
    options?: ?Object./ / parametersisRenderWatcher? : boolean// is the observer of the render function
  ) {
    this.vm = vm // Watcher has a VM attribute that indicates which component it belongs to
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this) // Add an observer instance to the component instance's _Watchers attribute
    // options
    if (options) {
      this.deep = !! options.deep/ / depth
      this.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.sync// Synchronize execution
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb / / callback
    this.id = ++uid // uid for batching //
    this.active = true // Whether the observer instance is active
    this.dirty = this.lazy // for lazy watchers
    // Avoid relying on duplicate collection processing
    this.deps = []
    this.newDeps = []
    this.depIds = new Set(a)this.newDepIds = new Set(a)this.expression = process.env.NODE_ENV ! = ='production'
      ? expOrFn.toString()
      : ' '
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else { // a string similar to obj.a
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop / / empty functionprocess.env.NODE_ENV ! = ='production' && warn(
          `Failed watching path: "${expOrFn}"` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  get () { // Triggers the value operation, which triggers the getter for the property
    pushTarget(this) // Dep: assign a value to dep.target
    let value
    const vm = this.vm
    try {
      // Core, which adds watcher to the closure by running the observer expression, doing the evaluation, and firing the getter
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
        throw e
      }
    } finally {
      if (this.deep) { // If deep monitoring is required, perform the operation on value
        traverse(value)
      }
      // Clean up the dependency collection
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) { // Avoid relying on duplicate collections
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this) // dep adds subscribers
      }
    }
  }

  update () { / / update
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run() // Synchronization runs directly
    } else { // If not, join the asynchronous queue for execution
      queueWatcher(this)}}}Copy the code

At this point, we can summarize some of the overall flow of a responsive system, also known as the observer pattern: the first step, of course, is to hijack data through the Observer, and then subscribe where necessary (e.g. Template compilation), add an observer (watcher), and immediately add the observer to the Dep by triggering the getter for the specified property via the value operation (which takes advantage of closures for dependency collection), and then notify when the Setter fires. Notify all observers and update them accordingly.

One way to think about the observer model is that the Dep is like the nuggets, and the nuggets have many authors (equivalent to many attributes of Data). Naturally, we all play the role of watcher and follow the authors we are interested in in Dep. For example, we tell jiang SAN Crazy to remind me to read when jiang SAN crazy updates. So whenever there is new content in Jiang SAN Crazy, we will receive a reminder like this: Jiang SAN Crazy has released “2019 Front-end Advanced Road ***”, and then we can watch it.

However, each Watcher can subscribe to many authors, and each author will update the article. So will users who don’t follow Jiang get a reminder? Can’t, send reminder to already subscribed user only, and only jiang SAN crazy updated just remind, you subscribe is Jiang SAN crazy, but stationmaster updated need to remind you? Of course not. That’s what closures need to do.

Proxy

Proxy can be understood as a layer of “interception” before the target object. All external access to the object must pass this layer of interception. Therefore, Proxy provides a mechanism for filtering and rewriting external access. Ruan Yifeng’s Introduction to ECMAScript 6

We all know that Vue 3.0 replaces Object.defineProperty with a Proxy, so what are the benefits of doing so?

The benefits are obvious, such as the two existing problems with Vue above, the inability to respond to the addition and deletion of object attributes and the inability to manipulate array subscript directly, can be solved. The downside, of course, is compatibility issues that Babel has yet to solve.

Basic usage

We use Proxy to implement a simple data hijacking.

let obj = {};
/ / agent obj
let handler = {
    get: function(target, key, receiver) {
        console.log('get', key);
        return Reflect.get(target, key, receiver);
    },
    set: function(target, key, value, receiver) {
        console.log('set', key, value);
        return Reflect.set(target, key, value, receiver);
    },
    deleteProperty(target, key) {
        console.log('delete', key);
        delete target[key];
        return true; }};let data = new Proxy(obj, handler);
// Only data can be used after proxy, otherwise obj will not work
console.log(data.name); // get name 、undefined
data.name = 'Yin Tianqiu'; // set name
delete data.name; // delete name
Copy the code

In this case, obj is an empty object that can be added and removed by Proxy to get feedback. Let’s look at array proxies:

let arr = ['Yin Tianqiu'.'I'm an actor'.'Fluttering willow'.'Dead walk-on'];
let array = new Proxy(arr, handler);
array[1] = 'I'll feed you.'; // set 1 I will support you
array[3] = 'Mind your own business first, fool. '; // Set 3 Mind your own business, fool.
Copy the code

Array index Settings are also fully controlled, of course, the use of Proxy is not only these, there are 13 interception operations. Interested students can go to see ruan Yifeng teacher’s book, here is no longer wordy.

Proxy implements the observer pattern

We’ve analyzed the Vue source code and looked at the basics of observer mode. So how do you implement an observer with a Proxy? We can write it very simply:

class Dep {
    constructor() {
        this.subs = new Set(a);// Set type, guaranteed not to repeat
    }
    addSub(sub) { // Add subscribers
        this.subs.add(sub);
    }
    notify(key) { // Notify subscribers of updates
        this.subs.forEach(sub= >{ sub.update(); }); }}class Watcher { / / observer
    constructor(obj, key, cb) {
        this.obj = obj;
        this.key = key;
        this.cb = cb; / / callback
        this.value = this.get(); // Get old data
    }
    get() { // Sets the trigger closure to add itself to deP
        Dep.target = this; // Set dep. target to itself
        let value = this.obj[this.key];
        Dep.target = null; // Set the value to nul
        return value;
    }
    / / update
    update() {
        let newVal = this.obj[this.key];
        if (this.value ! == newVal) {this.cb(newVal);
            this.value = newVal; }}}function Observer(obj) {
    Object.keys(obj).forEach(key= > { // Do deep listening
        if (typeof obj[key] === 'object') { obj[key] = Observer(obj[key]); }});let dep = new Dep();
    let handler = {
        get: function (target, key, receiver) {
            Dep.target && dep.addSub(Dep.target);
            // Target exists, add it to the Dep instance
            return Reflect.get(target, key, receiver);
        },
        set: function (target, key, value, receiver) {
            let result = Reflect.set(target, key, value, receiver);
            dep.notify(); // Publish
            returnresult; }};return new Proxy(obj, handler)
}
Copy the code

The code is short, so it’s all in one piece. Target && Dep.addSub (dep.target) ensures that when the getter for each property fires, it is the current Watcher instance. If closures are hard to understand, consider the example of a for loop that outputs 1, 2, 3, 4, 5.

Take a look at the results:

let data = {
    name: 'slag glow'
};
function print1(data) {
    console.log('I', data);
}
function print2(data) {
    console.log('Me this year', data);
}
data = Observer(data);
new Watcher(data, 'name', print1);
data.name = 'John Steinbeck'; // I am Yang Guo

new Watcher(data, 'age', print2);
data.age = '24'; // I am 24 years old
Copy the code

MVVM

With all that talk, it’s time to practice. Vue greatly improves the productivity of front-end ER, we reference Vue this time to achieve a simple Vue framework.

Vue implementation principle – how to achieve bidirectional binding MVVM

What is MVVM?

A brief introduction to MVVM, a more comprehensive explanation, you can see here the MVVM pattern. The full name of MVVM is Model-view-ViewModel. It is an architectural pattern, which was first proposed by Microsoft and draws on the ideas of MVC and other patterns.

The ViewModel is responsible for synchronizing the Model data to the View and for synchronizing the View’s changes to the data back to the Model. The Model layer, as the data layer, only cares about the data itself, and does not care about how the data is operated and displayed. View is the View layer, which transforms the data model into a UI interface and presents it to the user.

Image from MVVM schema

How to implement an MVVM?

To figure out how to implement an MVVM, we need to at least know what an MVVM has. Let’s see what we want it to look like.

<body>
<div id="app">Name:<input type="text" v-model="name"> <br>Age:<input type="text" v-model="age"> <br>Career:<input type="text" v-model="profession"> <br>
    <p>Output: {{info}}</p>
    <button v-on:click="clear">empty</button>
</div>
</body>
<script src="mvvm.js"></script>
<script>
    const app = new MVVM({
        el: '#app'.data: {
            name: ' '.age: ' '.profession: ' '
        },
        methods: {
            clear() {
                this.name = ' ';
                this.age =  ' ';
                this.profession = ' '; }},computed: {
            info() {
                return I call `The ${this.name}This year,The ${this.age}, aThe ${this.profession}`; }}})</script>
Copy the code

Operation effect:

Well, it looks like it’s copying some of Vue’s basic features, such as bidirectional binding, computed, V-ON, and so on. To make it easier to understand, let’s draw a schematic.

What do we need to do now? Data hijacking, data brokering, template compilation, publish and subscribe — wait, don’t these terms look familiar? Isn’t that what we did when we analyzed the Vue source code? (Yeah, yeah, it’s a copy of Vue.) OK, we are familiar with data hijacking and publishing and subscribing, but template compilation is still unknown. No hurry, let’s get started.

new MVVM()

Following the schematic, the first step is new MVVM(), which is initialization. What do you do when you initialize it? As you can imagine, data hijacking and initialization of templates (views).

class MVVM {
    constructor(options) { / / initialization
        this.$el = options.el;
        this.$data = options.data;
        if(this.$el){ // If there is el, proceed to the next step
            new Observer(this.$data);
            new Compiler(this.$el, this); }}}Copy the code

There seems to be something missing, computed and methods also need to be dealt with and replaced.

class MVVM {
    constructor(options) { / / initialization
        // Β·Β·Β· Receive parameters
        let computed = options.computed;
        let methods = options.methods;
        let that = this;
        if(this.$el){ // If there is el, proceed to the next step
        // Delegate a computed key value to this so that you can access this.$data.info directly and run the method when it is evaluated
        // Note that computed requires an agent, not an Observer
            for(let key in computed){
                Object.defineProperty(this.$data, key, {
                    enumerable: true.configurable: true.get() {
                        returncomputed[key].call(that); }})}// Delegate methods directly to this to access this.clear
            for(let key in methods){
                Object.defineProperty(this, key, {
                    get(){
                        returnmethods[key]; }})}}}}Copy the code

In the above code, we put data in this.$data, but remember that we usually use this. XXX to access data. Therefore, data, like computing properties, needs a layer of proxy for easy access. The detailed process of calculating attributes will be covered in the context of data hijacking.

class MVVM {
    constructor(options) { / / initialization
        if(this.$el){
            this.proxyData(this.$data);
            / /... omitted}}proxyData(data) { // Data broker
        for(let key in data){
           $data.name = this.$data.name = this.$data.name
            Object.defineProperty(this, key, {
                enumerable: true.configurable: true.get(){
                    return data[key];
                },
                set(newVal){ data[key] = newVal; }})}}}Copy the code

Data hijacking, publish and subscribe

After initialization we still have two steps left to process.

new Observer(this.$data); // Data hijacking + publish subscribe
new Compiler(this.$el, this); // Template compilation
Copy the code

Data hijacking and publishing and subscribing, which we’ve been talking about for a long time, should be familiar, so let’s get rid of it.

class Dep { // Publish a subscription
    constructor(){
        this.subs = []; // Set the watcher
    }
    addSub(watcher){ / / add a watcher
        this.subs.push(watcher);
    }
    notify(){ / / release
        this.subs.forEach(w= >w.update()); }}class Watcher{ / / observer
    constructor(vm, expr, cb){
        this.vm = vm; / / instance
        this.expr = expr; // Observe the expression of the data
        this.cb = cb; // Update the triggered callback
        this.value = this.get(); // Save the old value
    }
    get(){ // Activate the data getter to add the subscription
        Dep.target = this; // Set to itself
        let value = resolveFn.getValue(this.vm, this.expr); / / value
        Dep.target = null; // Reset to null
        return value;
    }
    update(){ / / update
        let newValue = resolveFn.getValue(this.vm, this.expr);
        if(newValue ! = =this.value){
            this.cb(newValue);
            this.value = newValue; }}}class Observer{ // Data hijacking
    constructor(data){
        this.observe(data);
    }
    observe(data){
        if(data && typeof data === 'object') {
            if (Array.isArray(data)) { // If it is an array, iterate over each member of the array
                data.forEach(v= > {
                    this.observe(v);
                });
                // Vue also does some special processing here, such as overwriting array methods
                return;
            }
            Object.keys(data).forEach(k= > { // Observe each property of the object
                this.defineReactive(data, k, data[k]); }); }}defineReactive(obj, key, value) {
        let that = this;
        this.observe(value); // The value of the object property, if it is an object or array, observe again
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            enumerable: true.configurable: true.get(){ // Determine whether to add Watcher to collect dependencies
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set(newVal){
                if(newVal ! == value) { that.observe(newVal);// Observe the new value
                    value = newVal;
                    dep.notify(); / / release}})}}Copy the code

Resolvefn. getValue (resolveFn.getValue); resolveFn.getValue (resolveFn.getValue); resolveFn.getValue (resolveFn.getValue); resolveFn.getValue (resolveFn.getValue); resolveFn.getValue (resolveFn.getValue); Let’s take a closer look at this method.

resolveFn = { // Set of utility functions
    getValue(vm, expr) { // Returns data for the specified expression
        return expr.split('. ').reduce((data, current) = >{
            return data[current]; / / this/info, this [obj] [a]}, vm); }}Copy the code

As mentioned in our previous analysis, an expression can be either a string or a function (such as a rendering function) that triggers the value operation. We’re only thinking about strings here, so where do we have expressions like this? For example, {{info}}, for example, v-model=”name” after = is the expression. It could also be of the form OBJ.A. Therefore, reduce is used to achieve a continuous value effect.

Computed attribute computed

Initialization time left a question, because involves the release subscription, so here we are detailed analysis of the properties of trigger process calculation, the initialization, the template used to {{info}}, so at the time of template compilation, You need to trigger a this.info value operation to get the actual value to replace the {{info}} string. Let’s just add an observer to that as well.

    compileText(node, '{{info}}'.' ') // Suppose the compile method looks like this, with an initial value of null
    new Watcher(this.'info'.() = > {do something}) // We immediately instantiate an observer
Copy the code

What actions are triggered at this point? We know that when new Watcher(), it triggers a value. This. Info will be fetched, and we will delegate it at initialization.

for(let key in computed){
    Object.defineProperty(this.$data, key, {
        get() {
            returncomputed[key].call(that); }})}Copy the code

So in this case, you run a computed method directly, remember what a method looks like?

computed: {
    info() {
        return I call `The ${this.name}This year,The ${this., the age}, aThe ${this.profession}`; }}Copy the code

In this case, the name, age, and Profession operations are triggered.

defineReactive(obj, key, value) {
    / /...
    let dep = new Dep();
    Object.defineProperty(obj, key, {
        get(){ // Determine whether to add Watcher to collect dependencies
            Dep.target && dep.addSub(Dep.target);
            return value;
        }
        / /...})}Copy the code

This makes full use of the closure feature. Note that you are still in the process of evaluating info because it is a synchronous method, which means that the dep. target exists and is the Watcher for viewing info properties. As a result, the program adds info’s Watcher to the DEP of Name, AGE, and Profession, respectively. In this way, the Watcher of INFO is notified to revalue and update the view if any of these values changes.

Print the DEP for easy comprehension.

Template compilation

In this section, we will compile the HTML template syntax into real data and convert the instructions into corresponding functions.

You can’t avoid manipulating Dom elements during compilation, so a createDocumentFragment method is used to create document fragments. This actually uses the virtual DOM in Vue, and diff algorithms are used to do minimal rendering when updated.

The document fragment exists in memory, not in the DOM tree, so inserting child elements into the document fragment does not cause page backflow (calculation of element position and geometry). Therefore, using document fragments generally results in better performance. – MDN

class Compiler{
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el); // Get the app node
        this.vm = vm;
        let fragment = this.createFragment(this.el); // Convert the DOM to a document fragment
        this.compile(fragment); / / compile
        this.el.appendChild(fragment); // Put it back into the DOM after the changeover is complete
    }
    createFragment(node) { // Convert dom elements into document fragments
        let fragment = document.createDocumentFragment();
        let firstChild;
        // Go to the first child node and put it into the document fragment until there is none, then stop the loop
        while(firstChild = node.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment;
    }
    isDirective(attrName) { // Is it a command
        return attrName.startsWith('v-');
    }
    isElementNode(node) { // Is an element node
        return node.nodeType === 1;
    }
    compile(node) { // Compile the node
        let childNodes = node.childNodes; // Get all child nodes
        [...childNodes].forEach(child= > {
            if(this.isElementNode(child)){ // Is an element node
                this.compile(child); // Recursively iterate over the child nodes
                let attributes = child.attributes; 
                // Get all attributes of the element node, v-model class, etc
                [...attributes].forEach(attr= > { // For example, v-on:click="clear"
                    let {name, value: exp} = attr; // Struct get "clear"
                    if(this.isDirective(name)) { // Determine if it is an instruction attribute
                        let [, directive] = name.split(The '-'); // Structure fetch instruction part V-on :click
                        let [directiveName, eventName] = directive.split(':'); / / on, click
                        resolveFn[directiveName](child, exp, this.vm, eventName); 
                        // Execute the corresponding instruction method}})}else{ // Compile text
                let content = child.textContent; // Get the text node
                if(/ \ {\ {(. +?) \} \} /.test(content)) { // Check whether there is template syntax {{}}
                    resolveFn.text(child, content, this.vm); // Replace the text}}}); }}// A method to replace text
resolveFn = { // Set of utility functions
    text(node, exp, vm) {
        // Lazy matching to avoid taking the last curly brace when multiple templates are in a row
        / / {{name}} {{age}} without inert match will take all at a time "{{name}} {{age}}"
        ["{{name}}", "{{age}}"]
        let reg = / \ {\ {(. +?) \} \} /;
        let expr = exp.match(reg);
        node.textContent = this.getValue(vm, expr[1]); // Trigger view update at compile time
        new Watcher(vm, expr[1].() = > { // Setter triggers publication
            node.textContent = this.getValue(vm, expr[1]); }); }}Copy the code

While compiling the element node (this.pile (node)), we determine if the element attribute is a directive and invoke the corresponding directive method. So finally, let’s look at some simple implementations of the instructions.

  • Bidirectional binding V-Model
resolveFn = { // Set of utility functions
    setValue(vm, exp, value) {
        exp.split('. ').reduce((data, current, index, arr) = >{ // 
            if(index === arr.length-1) { // Set the value for the last member
                return data[current] = value;
            }
            return data[current];
        }, vm.$data);
    },
    model(node, exp, vm) {
        new Watcher(vm, exp, (newVal) = > { // Add observers, data changes, update views
            node.value = newVal;
        });
        node.addEventListener('input'.(e) = > { // Listen for input events (view changes) that fire to update data
            let value = e.target.value;
            this.setValue(vm, exp, value); // Set the new value
        });
        // Triggered at compile time
        let value  = this.getValue(vm, exp); node.value = value; }}Copy the code

Bidirectional binding should be easy to understand. It should be noted that setValue cannot be set directly with the return value of Reduce. Because at this point the return value, it’s just a value, it doesn’t do the job of reassigning.

  • Event binding V-ON

Remember how we handled the methods when we initialized?

for(let key in methods){
    Object.defineProperty(this, key, {
        get(){
            returnmethods[key]; }})}Copy the code

We delegate all methods to this, and we deconstruct the instructions into ‘on’, ‘click’, and ‘clear’ when we compile V-on :click=”clear”.

on(node, exp, vm, eventName) { // Listen for events on the corresponding node and invoke the corresponding method on this when triggered
    node.addEventListener(eventName, e= >{ vm[exp].call(vm, e); })}Copy the code

Vue provides many more instructions, such as v-if, which actually adds or removes DOM elements; V-show, where the display attribute of the operation element is block or None; V-html, which adds the instruction value directly to the DOM element, can be implemented using innerHTML, but this operation is unsafe and XSS risky, so Vue also recommends not exposing the interface to the user. There are also v-for, V-slot and other more complex instructions, you can explore for yourself.

conclusion

The full article code is at the article repository 🍹🍰fe-code. This issue focuses on the reactive principles of Vue, including data hijacking, publish and subscribe, the difference between Proxy and Object.defineProperty, etc., along with a simple MVVM. As an excellent front-end framework, Vue has a lot to learn, and every detail is worth studying. The follow-up will also bring a series of Vue, javascript and other front-end knowledge points of the article, interested students can pay attention to.

Refer to the article

  • Analysis of Vue implementation principle – how to achieve bidirectional binding MVVM
  • Vue source code analysis
  • About re, recommend yaoJavaScript Regex Mini-BookIt’s very easy to read

Communication group

Qq front-end communication group: 960807765, welcome all kinds of technical exchanges, looking forward to your joining

Afterword.

If you see here, and this article is a little help to you, I hope you can move a small hand to support the author, thank 🍻. If there is something wrong in the article, we are welcome to point out and encourage each other.

  • The article warehouse 🍹 🍰 fe – code
  • Social chat system (vue + Node + mongodb) – πŸ’˜πŸ¦πŸ™ˆVchat

More articles:

Front end advanced road series

  • Vue component communication mode complete version
  • JavaScript prototype and prototype chain and Canvas captcha practice
  • [2019 front-end advancement] Stop, you this Promise!

From head to toe combat series

  • 【 】 from head to foot WebRTC + Canvas to achieve a double collaborative Shared sketchpad essay | the nuggets technology
  • 【 From head to toe 】 a multiplayer video chat – front-end WebRTC combat (a)
  • A social chat system (vue + Node + mongodb) – πŸ’˜πŸ¦πŸ™ˆVchat

Welcome to pay attention to the public number front-end engine, the first time to get the author’s article push, there are a large number of front-end tycoon quality articles, committed to become the engine to promote the growth of the front-end.