14. Vue 2.0 two-way binding principle

14. Vue 2.0 two-way binding principle

premise

There are many ways to achieve two-way binding. Vue adopts data holding combined with publish and subscribe design mode to achieve.

Use Object.defineProperty() to hijack the setter and getter of each property, publish a message to subscribers when the data changes, and trigger the corresponding listener callback.

Object.defineProperty() introduction

Function: A more popular way of saying is to manipulate object properties

Official: Define a new property directly on an object, or modify an existing property of an object, and return this object.

This method has three parameters:

Object.defineProperty(obj, prop, descriptor)

obj The object whose properties are to be defined.

prop The name or Symbol of the property to be defined or modified.

descriptor The attribute descriptor to be defined or modified.

Has the following attributes:

  • configurable: Can the descriptor be modified, that is, can the other attributes of the descriptor be modified again

  • enumerable: can the attribute be enumerated, that is, can the a attribute be for

  • writable: Can the attribute value be modified, that is, can the obj.a = 1 be modified like this

  • value: the value of the attribute

  • get: It is a function, when the attribute is accessed, the function is automatically called, and the return value of the function is the value of the attribute

  • set: is a function, when the attribute is modified, the function is automatically called, the function has one and only one parameter, the new value assigned

*The writable attribute of the value attribute in the descriptor and the set attribute of the get attribute are mutually exclusive.

Attribute holding

Vue's data data holding is achieved by using get and set attributes. The get function is triggered when the object property is read, and the set function is triggered when the value is assigned

* There cannot be a read operation in get, otherwise the loop will be endless, so when using get set, a variable is always needed

example:

let obj = {}; let name = "xx" ; Object .defineProperty(obj, "n" , { get () { console .log( "Read" , name); return name; }, set ( newName ) { console .log( "Settings" , newName); value = newName; } }); //Trigger the get function, the return value of get is the property value console .log(obj.n); //Trigger the set function, the value of value becomes xxxx, the property value has not changed obj.n = "xxxx" ; console .log(obj.n); Copy code

Object.defineProperty() extension

Back in Vue development, we often encounter a scenario where two-way binding cannot be performed to modify the value of an array. The reason is that the get set of defineProperty() cannot monitor the new modification operation of the object array.

Vue monitors the changes of the array by finding ways to change the original array, and then hijacking these methods.

The general steps are as follows: Array => new prototype (to perform detection behavior) => prototype of the array

There is a scenario, such as an array inside an array, then recursive thinking is used to solve this problem

This is why there is in vueset/set/ delete and can only be detected by using specific methods for arrays

The overall idea of two-way binding

1. Implement a data listener, the main function is to listen to all attributes, called Observer in Vue

2. Then through the subscription-release design ideas to notify the update

(The subscription publishing model defines a one-to-many dependency relationship. One refers to the publisher, such as a topic object, and multiple refers to subscribers. Dependency is the dependence of the subscriber on the publisher; multiple subscribers monitor a topic object at the same time . When the status of the publisher, the topic object, changes, the subscribers will be notified of the change, and the subscribers will update their status accordingly.)

3. Define a subscriber Dep to collect changes in these properties to notify subscribers

4. What kind of processing needs to be done for different events, so a ComPile (instruction parser) is also needed, such as pull-down operations, input operations, etc.

5. Finally implement a Watcher (subscriber), mainly to receive different operations, for those objects, update the view

Implement Observer (listener)

function observe ( data ) { if (data && typeof data === "object" ){ Object .keys(data).forEach( ( key )=> { defineReactive(data, key, data[key]); }) } } function defineReactive ( data,key,val ) { //Recursively, monitor sub-objects observe(val) Object .defineProperty(data, key, { enumerable : true , configurable : false , get : function () { return val; }, set : function ( value ) { val = value; }, }); } Copy code

Implement the subscriber

//Dep function Dep () { this .subs = []; } Dep.prototype = { addSub : function ( sub ) { this .subs.push(sub); }, notify : function () { this .subs.forEach( function ( sub ) { sub.update(); }); }, }; Copy code

Stuffed into the listener

function defineReactive ( data,key,val ) { var dep = new Dep() //Recursively, listen to sub-objects observe(val) Object .defineProperty(data, key, { enumerable : true , configurable : false , get : function () { return val; }, set : function ( value ) { dep.notify() }, }); } Copy code

Implement watcher (subscriber)

Ideas: 1. Insert the listener when instantiating 2. Implement the update() method 3. When the notification is reached, call the update() method to trigger the callback of the compiler (comPlie) 4. End

//Watcher function Watcher ( vm, exp, cb ) { this .cb = cb; this .$vm = vm; this .exp = exp; //In order to trigger the getter of the property, add yourself to the dep, combined with Observer Easier to understand this .value = this .get(); //add yourself to the subscriber operation } Watcher.prototype = { update : function () { this .run(); //Receive notification of property value changes }, run : function () { var value = this .get(); // Get the latest value var oldVal = this .value; //Speaking of triggering the set function before, the attribute value has not changed if (value !== oldVal) { this .value = value; this .cb.call( this .$vm, value, oldVal); //Execute the callback bound in Compile and update the view } }, get : function () { Dep.target = this ; //Point the current subscriber to yourself, cache var value = this .$vm[ this .exp]; //Force the listener to trigger and add yourself to the attribute subscriber Dep.target = null ; //After adding, reset and release return value; }, }; Copy code

Then the corresponding get in the defineReactive method should also be modified

function defineReactive ( data, key, val ) { var dep = new Dep() observe(val); //monitor sub-property Object .defineProperty(data, key, { .... get : function () { //Because you need to add a watcher in the closure, you can define a global target attribute in Dep, temporarily store the watcher, and remove it after adding Dep.target && dep.addDep(Dep.target); return val; }, .... }); } Copy code

Simple integration

<div id= "name" ></div> < script > function Vue ( data, el, exp ) { this .data = data; observe(data); el.innerHTML = this .data[exp]; //initialize the value of the template data new Watcher( this , exp, function ( value ) { el.innerHTML = value; }); return this ; } var ele = document .querySelector( "#name" ); var vue = new Vue( { name : "hello world" , }, ele, "name" ); setInterval ( function () { vue.data.name = "chuchur" + new Date () * 1 ; }, 1000 ); </Script > copy the code

But in the process of our use, this.xxx is used to operate, not this.data.xxx, so we also need to add a proxy, Vm proxy vm.data

function Vue ( options ) { this .$options = options || {}; this .data = this .$options.data; //Property proxy, implement vm.xxx -> vm.data.xxx var self = this ; Object .keys( this .data ).forEach( function ( key ) { //this.data ==>this self.proxy(key); //bind proxy properties }); observe( this .data, this ); el.innerHTML = this .data[exp]; //Initialize the value of the template data new Watcher( this , exp, function ( value ) { el.innerHTML = value; }); return this ; } //Also use the defineProperty() method Vue.prototype = { proxy : function ( key ) { var self = this ; Object .defineProperty( this , key, { enumerable : false , configurable : true , get : function proxyGetter () { return self.data[key]; }, set : function proxySetter ( newVal ) { self.data[key] = newVal; } }); } } Copy code

Finally, the implementation of the parser Compile

The idea is as follows:

  1. Parse the template instructions, replace the template data, and initialize the view
  2. Bind the node corresponding to the template instruction to the corresponding update function, and initialize the corresponding subscriber
Compile.prototype = { ...... isDirective : function ( attr ) { return attr.indexOf( 'v-' ) == 0 ; }, isEventDirective : function ( dir ) { return dir.indexOf( 'on:' ) === 0 ; }, //Process v-instruction compile : function ( node ) { var nodeAttrs = node.attributes, self = this ; [].slice.call(nodeAttrs).forEach( function ( attr ) { //Rule: the command is named v-xxx //such as the command in <span v-text="content"></span> is v-text var attrName = attr.name; //v-text if (self.isDirective(attrName)) { var exp = attr.value; //content var dir = attrName.substring( 2 ); //text if (self.isEventDirective (dir)) { //Event instruction, such as v-on:click self.compileEvent(node, self.$vm, exp, dir); } else { //Ordinary commands such as: v-model, v-html, currently only deal with v-model self.compileModel(node, self.$vm, exp, dir); } //After processing, kill v-on:, v-model and other element attributes node.removeAttribute(attrName) } }); }, compileEvent : function ( node, vm, exp, dir ) { var eventType = dir.split( ':' )[ 1 ]; var cb = vm.$options.methods && vm.$options.methods[exp]; if (eventType && cb) { node.addEventListener(eventType, cb.bind(vm), false ); } }, compileModel : function ( node, vm, exp, dir ) { var self = this ; var val = this .$vm[exp]; this .updaterModel(node, val); new Watcher( this .$vm, exp, function ( value ) { self.updaterModel(node, value); }); node.addEventListener( 'input' , function ( e ) { var newValue = e.target.value; if (val === newValue) { return ; } self.$vm[exp] = newValue; val = newValue; }); }, updaterModel : function ( node, value, oldValue ) { node.value = typeof value == 'undefined' ? '' : value; }, } Copy code

At last

If this article can give you a little help, I hope xdm can give a thumbs up