There are tons of articles on VUE two-way data binding on the web, and this article is just a summary.

VUE bidirectional data binding uses documentFragment, object.defineproperty, proxy and publish and subscribe mode. The following introduces these knowledge points respectively, and then uses them to write a JS native bidirectional data binding case.

DocumentFragment

Create a new blank document fragment. DocumentFragments are DOM nodes. They are not part of the main DOM tree. The usual use case is to create a document fragment, attach elements to the document fragment, and then attach the document fragment to the DOM tree. In the DOM tree, the document fragment is replaced by all of its child elements. Because the document fragment exists in memory, not in the DOM tree, inserting child elements into the document fragment does not cause page reflow (calculations of element position and geometry). Therefore, the use of document fragments is often used to optimize performance.

Demo

<body>
    <ul data-uid="ul"></ul>
</body>

<script>
    let ul = document.querySelector(`[data-uid="ul"]`),
        docfrag = document.createDocumentFragment();
    
    const browserList = [
        "Internet Explorer"."Mozilla Firefox"."Safari"."Chrome"."Opera"
    ];
    
    browserList.forEach((e) => {
        let li = document.createElement("li");
        li.textContent = e;
        docfrag.appendChild(li);
    });
    
    ul.appendChild(docfrag);
</script>
Copy the code

defineProperty

The attributes of objects are divided into data attributes and accessor attributes. If you want to change the default properties of an Object, you must use the Object.defineProperty method, which takes three parameters: the Object of the attribute, the name of the attribute, and a descriptor Object.

Data attributes:

A data property contains a location from which a data value can be read and written, and a data property has four properties that describe its behavior.

  • Configurable: indicates whether the application can passdeleteRedefining a property by deleting it, changing its properties, or changing it to an accessor property. The default value is true.
  • Enumberable: indicates whether the attribute can be returned through a for-in loop. The default value istrue.
  • Writable: Indicates whether the value of an attribute can be modified. The default value istrue.
  • Value: Contains the data value for this property. When reading property values, read from this position; When finalizing the property value, save the new value in this location. The default value istrue.
Accessor properties:

Accessor properties do not contain data values; They contain a pair of getters and setters (two are not required). When the accessor property is read, the getter function is called, which returns a valid value; When the accessor property is written, the setter function, which determines how to process the data, is called and the new value is passed in. The accessor property has the following four properties.

  • Configurable: indicates whether the application can passdeleteDelete attributes to redefine attributes, can modify attributes, or can change attributes to data attributes. The default value istrue.
  • Enumerable: indicates whether the application can passfor-inThe loop returns the property. The default value istrue.
  • Get: The function that is called when a property is read. The default value isundefined.
  • Set: The function that is called when the property is finalized. The default value isundefined.

Demo

var book = {
    _year: 2018,
    edition: 1
};
Object.defineProperty(book, "year", {
    get: function() {return this._year;
    },
    set: function(newVal){
        if(newVal > 2008){ this._year = newVal; this.edition += newVal - 2008; }}}); book.year = 2019; console.log(book._year); //2019 console.log(book.edition); / / 12Copy the code

Object. DefineProperty defects:

  1. Data hijacking can only be carried out for attributes, and deep traversal is required for JS object hijacking.
  2. You can’t listen for changes to the data in arrays, but through somehackMethods to achieve, e.gpush,pop,shift,unshift,splice,sort,reverse.See the documentation for

proxy

The new ES6 approach, which can be understood as a layer of “interception” in front of the target object, provides a mechanism for filtering and overwriting external access to the object. The word Proxy is used to mean that it acts as a Proxy for certain operations. Proxy supports the following methods:

  • get(): intercepts reading of object properties.
  • set(): Intercepts the setting of object properties.
  • apply(): intercepts function calls,callandapplyOperation.
  • has(): This method takes effect when determining whether an object has an attribute and returns a Boolean value. It takes two parameters: the target object and the name of the property to be queried.
  • construct(): Used for interceptionnewCommand. Parameters:target(Target object),args(constructor argument object),newTarget(When creating an instance object,newThe constructor of the command function).
  • deleteProperty()Intercept:delete proxy[propKey]Returns a Boolean value.
  • defineProperty()Intercept:object.definePropertyOperation.
  • getOwnPropertyDescriptor()Intercept:object.getownPropertyDescriptor(), returns a property description object orundefined.
  • getPrototypeOf(): Used to intercept and retrieve object prototypes. Can interceptObject.prototype.__proto__,Object.prototype.isPrototypeOf(),Object.getPrototypeOf(),Reflect.getPrototypeOf(),instanceof
  • isExtensible()Intercept:Object.isExtensibleOperation to return a Boolean value.
  • ownKeys(): Intercepts the reading of an object’s own properties. Can interceptObject.getOwnPropertyNames(),Object.getOwnPropertySymbols(),Object.keys(),for... inCycle.
  • preventExtensions()Intercept:Object.preventExtensions(), returns a Boolean value.
  • setPrototypeOf()Intercept:Object.setPrototypeOfMethods.
  • revocable(): Returns a cancelableproxyInstance.

Demo

<body>
    <input type="text" id="input">
    <p id="p"></p>
</body>
<script>
    const input = document.getElementById('input');
    const p = document.getElementById('p');
    const obj = {};
    
    const newObj = new Proxy(obj, {
      get: function(target, key, receiver) {
        console.log(`getting ${key}! `);return Reflect.get(target, key, receiver);
      },
      set: function(target, key, value, receiver) {
        console.log(target, key, value, receiver);
        if (key === 'text') {
          input.value = value;
          p.innerHTML = value;
        }
        returnReflect.set(target, key, value, receiver); }}); input.addEventListener('keyup'.function(e) {
      newObj.text = e.target.value;
    });
</script>
Copy the code

Design pattern – Publish subscribe pattern

The observer and publish-subscribe modes are easy to mix, so here’s a quick distinction.

  • Observer pattern: An object (called subject) maintains a series of dependent objects (called observers), notifying them automatically of any changes in state (observers).
  • Publish subscribe mode: Based on a topic/event channel, the object that wants to receive notifications (called subscriber) subscribes to a topic through a custom event, and the object whose event is activated (called publisher) is notified by publishing a topic event.

Differences:

  • The Observer pattern defines a one-to-many dependency by requiring observers to subscribe to events that change content;
  • Publish/Subscribe uses a topic/event channel between the subscriber and publisher;
  • In observer mode, the observer is “forced” to perform content change events (Subject content events). In publish/subscribe mode, subscribers can customize event handlers;
  • There is a strong dependency between two objects in observer mode. Publish/subscribe the degree of coupling between two objects.

Demo

// vm.$on
export function eventsMixin(Vue: Class<Component>) {
    const hookRE = /^hook:/
    // The parameter type is a string or an array of strings
    Vue.prototype.$on = function (event: string | Array<string>, fn: Function) :Component {
        const vm: Component = this
        // The incoming type is array
        if (Array.isArray(event)) {
            for (let i = 0, l = event.length; i < l; i++) {
                this.$on(event[i], fn)
                // Pass in the appropriate callback}}else {
            (vm._events[event] || (vm._events[event] = [])).push(fn)
            // optimize hook:event cost by using a boolean flag marked at registration
            // instead of a hash lookup
            if (hookRE.test(event)) {
                vm._hasHookEvent = true}}return vm
    }

    // vm.$emit
    Vue.prototype.$emit = function (event: string) :Component {
        const vm: Component = this
        if(process.env.NODE_ENV ! = ='production') {
            const lowerCaseEvent = event.toLowerCase()
            if(lowerCaseEvent ! == event && vm._events[lowerCaseEvent]) { tip(`Event "${lowerCaseEvent}" is emitted in component ` +
                    `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
                    `Note that HTML attributes are case-insensitive and you cannot use ` +
                    `v-on to listen to camelCase events when using in-DOM templates. ` +
                    `You should probably use "${hyphenate(event)}" instead of "${event}". `)}}let cbs = vm._events[event]
        if (cbs) {
            cbs = cbs.length > 1 ? toArray(cbs) : cbs
            const args = toArray(arguments.1)
            for (let i = 0, l = cbs.length; i < l; i++) {
                try {
                    cbs[i].apply(vm, args)// Execute the callback passed in earlier
                } catch (e) {
                    handleError(e, vm, `event handler for "${event}"`)}}}return vm
    }
}
Copy the code

MVVM process analysis

The following native MVVM small framework mainly for Compile(template compilation), Observer(data hijacking), Watcher(data listening) and Dep(publish subscription) several parts to implement. The process can be referred to the following figure:

Mvm. HTML page, instantiating a VUE object


      
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="message.a">
        <ul>
            <li>{{message.a}}</li>
        </ul>
        {{name}}
    </div>
    <script src="mvvm.js"></script>
    <script src="compile.js"></script>
    <script src="observer.js"></script>
    <script src="watcher.js"></script>
    <script>
        let vm = new MVVM({
            el:'#app'.data: {
                message: {
                    a: 'hello'
                },
                name: 'haoxl'}})</script>
</body>
</html>
Copy the code

Mvvm. js is mainly used to hijack data and mount nodes to $EL and data to $data.

class MVVM{
    constructor(options) {
        // Mount the parameters to the MVVM instance
        this.$el = options.el;
        this.$data = options.data;
        // Start compiling if there is a template to compile
        if(this.$el){
            // Data hijacking - Change all attributes of an object to get and set methods
            new Observer(this.$data);
            // Delegate data from this.$data to this
            this.proxyData(this.$data);
            // Compile with data and elements
            new Compile(this.$el, this);
        }
    }
    proxyData(data){
        Object.keys(data).forEach(key= >{
            Object.defineProperty(this, key, {
                get(){
                    return data[key]
                },
                set(newValue){
                    data[key] = newValue
                }
            })
        })
    }
}
Copy the code

Observer.js uses Object.defineProerty to hijack data and respond to data changes in combination with a publish-and-subscribe pattern.

class Observer{
    constructor(data){
        this.observe(data);
    }
    observe(data){
        // Change the data attribute to set and get. If data is not an object, return it directly
        if(! data ||typeofdata ! = ='object') {return;
        }
        // To hijack the data one by one, obtain the key and value of the data first
        Object.keys(data).forEach(key= > {
            / / hijacked
            this.defineReactive(data, key, data[key]);
            this.observe(data[key]);// Recursive hijacking of objects in data
        });
    }
    defineReactive(obj, key, value) {
        let that = this;
        let dep = new Dep();// Each change corresponds to an array that holds all the updated operations
        Object.defineProperty(obj, key, {
            enumerable: true.configurable: true.// The method that fires when the value is specified
            get(){// The method to call when the value is specified
                Dep.target && dep.addSub(Dep.target)
                return value;
            },
            // The method that fires when an assignment is performed
            set(newValue){
                // Assign new values to attributes in data
                if(newValue ! == value){// If the object continues to hijack
                    that.observe(newValue);
                    value = newValue;
                    dep.notify();// Notify everyone that the data is updated}})}}//
class Dep{
    constructor() {// An array of subscriptions
        this.subs = []
    }
    // Add a subscription
    addSub(watcher){
        this.subs.push(watcher);
    }
    notify(){
        // Call watcher's update method
        this.subs.forEach(watcher= >watcher.update()); }}Copy the code

watcher.js

// The purpose of the observer is to add an observer to the element that needs to be changed, and execute the corresponding method when the data changes
class Watcher{
    constructor(vm, expr, cb){
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // Get the old value
        this.value = this.get();
    }
    getVal(vm, expr){
        expr = expr.split('. ');
        return expr.reduce((prev,next) = > {//vm.$data.a.b
            return prev[next];
        }, vm.$data)
    }
    get(){
        Dep.target = this;// Assign the instance to target
        let value = this.getVal(this.vm, this.expr);
        Dep.target = null;//
        return value;// Return the old value
    }
    // External exposure method
    update(){
        // Update will be triggered when the value changes, fetching the new value. The old value is already stored in value
        let newValue = this.getVal(this.vm, this.expr);
        let oldValue = this.value;
        if(newValue ! == oldValue){this.cb(newValue);// Call the watch callback}}}Copy the code

Compile. Js creates DOM nodes from the DocumentFragment document and then renders the data to this region using the regular parsing {{}}.

class Compile{
    constructor(el, vm){
        //el is the root node of the MVVM instance
        this.el = this.isElementNode(el) ? el:document.querySelector(el);
        this.vm = vm;
        // Start compiling if the element is available
        if(this.el) {
            //1. Move these real DOM fragments into memory
            let fragment = this.node2fragment(this.el);
            //2. Compile => Extract the desired element node V-model or text node {{}}
            this.compile(fragment);
            //3. Insert the compiled fragment into the page
            this.el.appendChild(fragment); }}/* Helper method */
    // Determine if it is an element
    isElementNode(node){
        return node.nodeType === 1;
    }
    // Is it a command
    isDirective(name){
        return name.includes('v-');
    }
    /* Core method */
    // Put everything in el into memory
    node2fragment(el){
        // Document fragmentation - document fragmentation in memory
        let fragment = document.createDocumentFragment();
        let firstChild;
        while(firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment;// Nodes in memory
    }
    // Compile the element
    compileElement(node){
        // Get all attributes of the node
        let attrs = node.attributes;
        Array.from(attrs).forEach(attr= > {
            // Check whether the attribute name contains v-
            let attrName = attr.name;
            if(this.isDirective(attrName)){
                // take the corresponding value and put it in the node
                let expr = attr.value;
                // There may be multiple instructions, such as V-model, V-text, v-html, so take the corresponding method to compile
                let [,type] = attrName.split(The '-');[v,model] [v,model]
                CompileUtil[type](node, this.vm, expr)
            }
        })
    }
    compileText(node){
        / / take {{}}
        let expr = node.textContent;
        let reg = /\{\{([^}]+)\}\}/g;
        if(reg.test(expr)){
            CompileUtil['text'](node, this.vm, expr);
        }
    }
    compile(fragment){
        // Children of the current parent node, containing text nodes, array like objects
        let childNodes = fragment.childNodes;
        // Convert to an array and loop through the type of each node
        Array.from(childNodes).forEach(node= > {
            if(this.isElementNode(node)) {// is the element node
                // Compile the element
                this.compileElement(node);
                // If it is an element node, you need to recurse again
                this.compile(node)
            }else{// is a text node
                // Compile text
                this.compileText(node); }}}})Temporarily only / / compile method of v - model and corresponding method {{}}
CompileUtil = {
    getVal(vm, expr){
        expr = expr.split('. ');
        return expr.reduce((prev,next) = > {//vm.$data.a.b
            return prev[next];
        }, vm.$data)
    },
    getTextVal(vm, expr){// Get the compiled text content
        return expr.replace(/\{\{([^}]+)\}\}/g, (... arguments)=>{return this.getVal(vm, arguments[1])
        })
    },
    text(node, vm, expr){// Text processing
        let updateFn = this.updater['textUpdater'];
        // convert {{message.a}} to the value inside
        let value = this.getTextVal(vm, expr);
        // Match {{}} with the re, and replace the value inside
        expr.replace(/\{\{([^}]+)\}\}/g, (... arguments)=>{// Add an observer when you encounter a variable in the template that needs to be replaced with a data value
            // When a variable is reassigned, the method that updates the value node to the Dom is called
            // The observe.js get method is called after new (instantiated)
            new Watcher(vm, arguments[1],(newValue)=>{
                // If the data changes, the text node needs to retrieve the dependent attributes to update the content of the text
                updateFn && updateFn(node,this.getTextVal(vm, expr)); })})// Execute if there is a text processing method
        updateFn && updateFn(node,value)
    },
    setVal(vm, expr, value){//[message,a] assigns a value to the text
        expr = expr.split('. ');// Split the object into an array
        / / convergence
        return expr.reduce((prev, next, currentIndex) = > {
            // If the assignment starts at the last item of the object, such as message:{a:1} will be broken into message.a = 1
            if(currentIndex === expr.length- 1) {return prev[next] = value;
            }
            return prev[next]// TODO
        },vm.$data);
    },
    model(node, vm, expr){// Input box processing
        let updateFn = this.updater['modelUpdater'];
        // Add a monitor. When data changes, the watch callback should be called
        new Watcher(vm, expr, (newValue)=>{
            // cb is called when the value changes, passing the new value back
            updateFn && updateFn(node,this.getVal(vm, expr))
        });
        // Add an input event to the input
        node.addEventListener('input', (e) => {
            let newValue = e.target.value;
            this.setVal(vm, expr, newValue);
        });
        // Execute if there is a text processing method
        updateFn && updateFn(node,this.getVal(vm, expr))
    },
    updater: {
        // Update the text
        textUpdater(node, value){
            node.textContent = value
        },
        // Update the value of the input boxmodelUpdater(node, value){ node.value = value; }}}Copy the code

Summary: First, Vue uses the DocumentFragment to hijack all the nodes contained in the root element. These nodes include not only tag elements, but also text and even newline carriage returns. Vue then uses defindProperty() to make all the data in the data accessor properties of Vue, so that every time the data is modified, the corresponding property’s get and set methods are triggered. Then compile and process the dom nodes hijacked, traverse all nodes, determine the nodeType according to nodeType, and determine whether the node needs to be compiled according to the node’s own attributes (whether there are v-model and other attributes) or the content of the text node (whether it conforms to the format of {{text interpolation}}). For the V-Model, the binding event changes the data in the Vue when entered. For the text node, put it into the observer list as an observer watcher. When the Vue data changes, there will be a topic object, which will release the change message to the observers in the list, and the observers will update themselves and change the display in the node, so as to achieve the purpose of bidirectional binding.