preface

Vue (pronounced /vju/, similar to View) is the world’s top open source software developed by Yuxi, one of the few Chinese developers. It is a set of progressive frameworks for building user interfaces. Vue is designed to be applied layer by layer from the bottom up. MVVM responsive programming model avoids direct DOM operation and reduces the complexity of DOM operation.

Speaking of Vue’s responsiveness principle, we can start with its compatibility. Vue does not support browsers below IE8, because Vue2.x is based onObject.definePropertyObject. DefineProperty is a non-shim feature in ES5, which is why Vue does not support IE8 and later browsers; Vue passes Object.definePropertygetter/setterListen for collected dependencies to update view data by notifying changes when properties are accessed and modified

The above is the figure on the official website, which is not very clear. Let’s make a detailed analysis below. In fact, the responsive changes of Vue data mainly involve Observer, Watcher and Dep. So understanding Vue responsiveness requires understanding how these three classes are related to each other; And how they work, responsible for the logical operations. So let’s analyze the reactive principle of Vue from the code of a simple Vue instance. The following is a simplified version of the analysis

Observer vs. publish subscribe

Observer model

class Subject { constructor() { this.observers = []; } add(observer) { this.observers.push(observer); } notify(... args) { this.observers.forEach(observer => observer.update(... args)); } } class Observer { update(... args) { console.log('do something'); } } const sub = new Subject(); /* System */ sub.add(new Observer()); Sub.add (new Observer()); */ sub. Notify (); /* Double Eleven, notify all booked people to grab goods */Copy the code

Publish and subscribe model

class SubPub { constructor() { this.observers = {}; } subscribe(topic, callback) { if (! this.observers[topic]) { this.observers[topic] = []; } this.observers[topic].push(callback); } publish(topic, ... args) { let callbacks = this.observers[topic] || []; callbacks.forEach(cb => cb.call(this, ... args)); } } const subject = new SubPub(); Mediation / * * / subject. The subscribe (' two rooms one hall, () = > {the console. The log (' * * '); }); Subscribe (' three bedrooms, one bedroom ', () => {console.log(' three bedrooms, one bedroom ')}); Subscribe (' console.log ', () => {console.log(' console.log '); /* Subscribe (' console.log '); }); /* I want to buy a house */ subject.publish(' publish '); /* When there is a room with two bedrooms and one living room, the agent will notify Zhang SAN and Wang Wu to look at it */Copy the code

The difference between

  • Observer communication parties are clearly aware of each other’s presence, and publication and subscription are brokered through a message center.
  • The observer’s code is highly coupled.
  • The code for the observer is synchronous, whereas the publish-subscribe is more asynchronous due to the existence of a message center.

As can be seen from our implementation code, in fact, the biggest difference between observer and publish and subscribe is whether the two subjects of the message know each other and whether they communicate the message by proxy (that is, whether there is a middleman to earn the price difference).

Implementation examples

<div id="app"> <h3>{{ msg }}</h3> <p>{{ count }}</p> <h1>v-text</h1> <p v-text="msg"></p> <input type="text" v-model="count"> <button type="button" v-on:click="increase">add+</button> <button type="button" v-on:click="changeMessage">change message! </button> <button type="button" v-on:click="recoverMessage">recoverMessage! </button> </div>Copy the code

The source code to achieve

<script> function __isNaN(a, b) { return Number.isNaN(a) && Number.isNaN(b); } class miniVue {constructor(constructor = {}) {constructor(constructor = {}) {}} class miniVue {constructor(constructor = {}) {} class miniVue {constructor(constructor = {}) {} this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el; this.$data = options.data; this.$methods = options.methods; this.proxy(this.$data); $data new Observer(this.$data); new Compiler(this); } //proxy data object proxy(data) {// Since we are proxy for each attribute, Keys (data). ForEach (Key => {object.defineProperty (this, key, {enumerable: true, different: 64x);} true, get() { return data[key]; }, set(newValue) {// We need to determine if the value does not change the assignment, need to exclude NaN (// NaN! == NaN) if (newValue === data[key] || __isNaN(newValue, data[key])) return; data[key] = newValue; Constructor () {constructor() {this.deps = new Set(); } add(dep) {if (dep &&dep.update) this.deps.add(dep); if (dep &&dep.update) this.deps.add(dep); } notify() {// Publish a notification through a DEP and then call the update method of each DEP, This.deps.foreach (dep => dep.update())}} class Watcher {//3 parameters, the current component instance VM,state, and a callback function. Constructor (vm, key, cb) {this.vm = vm; this.key = key; this.cb = cb; // Dependent class dep. target = this; This.__old = vm[key]; // We use a variable to store the old value. Dep.target = null; } update() {let newValue = this.vm[this.key]; Compared with old values do, / / you don't need to perform the next step without changing the if (newValue = = = this. __old | | __isNaN (newValue, enclosing __old)) return; // call back the newValue this.cb(newValue); __old = newValue; this.__old = newValue; }} class constructor {constructor(data) {// Implement this. } // Walk (data) {if (typeof data! == 'object' || ! data) return; Keys (data).foreach (key => this.definereactive (data, key, data[key])); } defineReactive(data, key, value) {// Get the current this to avoid using vm later, let vm = this; // Call the walk method recursively, because the object could also be this.walk(value); // instantiate the collection dependency class let dep = new dep (); Object.defineProperty(data, key, { configurable: true, enumerable: Dep. Target && dep.add(dep.target); true, get() {// Collect dependencies on the Dep class. return value; }, the set (newValue) {/ / here also to judge the if (newValue = = = value | | __isNaN (value, newValue)) return; // Otherwise change the value value = newValue; // newValue can also be an object, so recursive vm.walk(newValue); // Dep class dep.notify(); }})}} class Compiler {constructor(vm) {this.el = vm.$el; // The current component instance this.vm = vm; $methods = vm.$methods; This.compile (vm.$el); } compile(el) {// Get all children (including text nodes) let childNodes = el.childnodes; From (childNodes).foreach (node => {if (this.istextNode (node)) { this.compileText(node); } else if (this.isElementNode(node)) { this.compileElement(node); This.compile (node.childnodes && node.childnodes.length) this.compile(node); this.compile(node.childnodes && node.childnodes.length) this.compile(node); }) } compileText(node) { let reg = /\{\{(.+?) \}\}/ let value = node.textContent; if (reg.test(value)) { let key = RegExp.$1.trim(); node.textContent = value.replace(reg, this.vm[key]); new Watcher(this.vm, key, val => { node.textContent = val; })}} compileElement(node) {let attrs = node.attributes; If (attrs.length) {array.from (attrs).foreach (attr => { Let attrName = attr. Name; if (this.isDirective(attrName)) { attrName = attrName.indexOf(":") > -1 ? attrName.substr(5) : attrName.substr(2); let key = attr.value; this.update(node, key, attrName, this.vm[key]); Update (node, key, attrName, value) {if (attrName === 'text') {node.textContent = value; new Watcher(this.vm, key, NewValue => node.textContent = newValue)} else if (attrName === 'model') {// Perform v-model operation node.value = value; new Watcher(this.vm, key, newValue => node.value = newValue); node.addEventListener('input', (e) => { this.vm[key] = node.value; })} else if (attrName === 'click') {node.addeventListener (attrName, this.methods[key].bind(this.vm)); } } isDirective(dir) { return dir.startsWith('v-'); } isTextNode(node) { return node.nodeType === 3; } isElementNode(node) { return node.nodeType === 1; } } </script> <script> const app = new miniVue({ el: "#app", data: { msg: "hello,mini vue.js", count: 666 }, methods: { increase() { this.count++; }, changeMessage() { this.msg = "hello,eveningwater!" ; }, recoverMessage() { console.log(this) this.msg = "hello,mini vue.js"; }}}); </script>Copy the code

conclusion

As a review, the core of the Vue responsive principle is Observer, Dep, Watcher.

A reactive binding is performed in the Observer, and when the data is read, the get method is triggered and Dep is performed to collect the dependencies, that is, the Watcher.

When the data is changed, the set method is triggered to perform the update through all the corresponding dependencies (Watcher). Watch and computed, for example, perform developer-defined callback methods.