美文网首页
Vue 核心之数据双向绑定

Vue 核心之数据双向绑定

作者: 明里人 | 来源:发表于2019-08-20 10:13 被阅读0次

    Vue双向绑定原理:Vue内部通过 Object.defineProperty方法以属性拦截的方式,把data对象的每个数据的读写转化为getter / setter,当数据变化时通知视图更新。

    一、MVVM数据双向绑定

    MVVM数据双向绑定主要是指:数据变化更新视图,视图变化更新数据。


    image.png

    即:

    • 输入框内容变化时,Data 中的数据同步变化。View 导致 Data 的变化。(通过事件监听的方式来实现)
    • Data 中的数据变化时,文本节点的内容同步变化。Data 导致 View 的变化。(通过操作DOM实现)

    监听器 Observer 只要是让对象变的 "可观测",即每次读写数据时,我们能感知到数据被读取了或数据被改写了。Vue2.0源码中用到Object.defineProperty()来劫持各个数据属性的setter / getter。关于Object.defineProperty 方法,在 MDN 上是这么定义的:

    Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

    二、Object.defineProperty() 语法

    Object.defineProperty(obj, prop, descriptor)

    参数:

    • obj: 要在其上定义属性的对象。
    • prop:要定义或修改的属性的名称。
    • descriptor:将被定义或修改的属性描述符。

    返回值:被传递给函数的对象。
    属性描述符:
    Object.defineProperty() 为对象定义属性,分为数据描述符和存取描述符,两种形式不能混用。

    数据描述符和存取描述符均具有以下可选键值:

    • configurable:一个总开关,一旦将它设为false,就不能删除或重新设置defineProperty监听的属性。为true时可以进行删除或重新使用defineProperty设置新值。默认为false。
    • enumerable:当属性值为true时,该属性才能出现在对象枚举的属性中。默认为false。

    数据描述符具有以下可选键值:

    • value:该属性对应的值,可以为任意有效的 JavaScript值(数值、对象、函数等)。默认 undefined。
    • writable:设置属性值是否允许被赋值运算符改变。true为允许,false为不允许被重写。默认false。

    存取描述符具有以下可选键值:

    • get:用于给属性提供 getter 方法,当访问该属性时,该方法会被执行,执行时不需要传入参数,但可以拿到this对象。默认为undefined。
    • set:用于给属性提供 setter 方法,当属性修改时,该方法会被执行。该方法接收唯一参数,即该属性新的参数值。默认为undefined。
    通过 Object.defineProperty() 实现一个简单的输入框双向绑定:
    <!DOCTYPE html>
    <html lang="zh">
    <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>defineProperty实现绑定</title>
        <script type="text/javascript" src="./example.js"></script>
    </head>
    <body>
        <div id="myApp">
            <input type="text" id="myInput" />
            <div id="myDiv"></div>
        </div>
    </body>
    <script type="text/javascript">
        var myInput = document.getElementById('myInput');
        var myDiv = document.getElementById('myDiv');
        
        let obj = {
            value: '监听数据'
        }
        
        // 将初始化数据赋值给元素
        myInput.value = obj.value;
        myDiv.innerHTML = obj.value;
        
        Object.defineProperty(obj, 'value', {
            set(newVal) {
                // 监听对象属性值改变,更新div元素innerHTML属性
                myDiv.innerHTML = newVal;
            }
        })
        
        myInput.oninput = function(e) {
            // 更新对象值,来触发Object.defineProperty的set方法
            obj.value = e.target.value;
        }
    </script>
    </html>
    

    要了解Vue双向绑定原理,首先要明白三个概念:

    1、观察者( observer ):数据监听器,负责对数据对象的所有属性进行监听劫持,并将消息发送给订阅者进行数据更新。
    2、订阅者( watcher ):负责接收数据的变化,并执行更新视图(view)。数据与订阅者是一对多的关系。
    3、解析器( compile ):负责对你的每个节点元素指令进行扫描和解析,负责相关指令的数据绑定初始化及创造数据对应的订阅者(每个通过指令绑定该属性数据的元素都是一个订阅者)。
    html:
    <!DOCTYPE html>
    <html lang="zh">
    <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>双向绑定</title>
        <script type="text/javascript" src="./example.js"></script>
    </head>
    <body>
        <div id="myApp">
            <input type="button" value="加个!" z-on:click="fun1" />
            <input type="button" value="加个?" @click="fun2" />
            <input type="text" style="width:400px" z-model="site">
            <p z-html="site"></p>
            <p z-text="site"></p>
        </div>
    </body>
    <script type="text/javascript">
        var vm = new Example({
            el: '#myApp',
            data: {
                site: 'Vue双向绑定原理',
                age: 12,
                sex: '男'
            },
            methods: {
                fun1() {
                    this.site += '!'
                },
                fun2() {
                    this.site += '?'
                }
            }
        })
    </script>
    </html>
    
    example.js双向绑定代码:
    function Example(options) { // 创建构造函数Example,并接收对象结构体options
        this.$el = document.querySelector(options.el); // 获取指定挂载的元素
        this.$data = options.data; // 将数据挂载到实例
        this.$methods = options.methods; // 存放对象的方法
        this.binding = {}; // 所有与数据相关的订阅者对象都存放于此,$data下每个数据对应一个数组,用于对应多个订阅者
        
        this.observer(); // 调用观察者,对数据进行劫持
        this.compile(this.$el); // 对元素上绑定的指令如(v-model)进行解析,并创建订阅者.(所有绑定$data下该属性的元素都将成为该属性数据的订阅者)
    }
    
    // 观察者
    Example.prototype.observer = function() {
        if (!this.$data || typeof this.$data !== 'object') return;
        
        var value = ''; // 记录$data每个属性的属性值
        for (var key in this.$data) { // 遍历数据对象
            value = this.$data[key]; // 对象属性值
            this.binding[key] = []; // 初始化数据订阅者,一对多关系,为一个数组
            var binding = this.binding[key]; // 存放当前数据相关的所有订阅者
            
            // 开始监听劫持
            this.defineReactive(this.$data, key, value, binding); // 通过创建方法实现数据分离,私有化,实现闭包
        }
    }
    
    Example.prototype.defineReactive = function (data, key, value, binding) {
        Object.defineProperty(data, key, {
            get() {
                return value; // 返回当前值
            },
            set(newVal) { // newVal 为设置修改后的新值
                if (newVal !== value) {
                    value = newVal; // 更新数据
                    // 以后该属性数据值改变后都会执行一次数据更新
                    binding.forEach(watcher => {
                        watcher.update(); // 通知与本数据相关的订阅者们(即绑定该数据的DOM元素)进行视图更新
                    })
                }
            }
        })
    }
    
    // 解析器 (解析指令并创建订阅者)
    Example.prototype.compile = function(el) {
        var nodes = el.children; // 获取所有子节点(元素节点)
        for (var i = 0; i < nodes.length; i ++) { // 遍历子节点
            var node = nodes[i]; // 具体节点
            if (node.children.length > 0) { // 判断是否具有子节点
                this.compile(node); // 递归
            }
            
            if (node.hasAttribute("z-on:click")) { // 该节点是否拥有 z-on:click 指令
                var attrVal = node.getAttribute('z-on:click'); // 获取指令对应的方法名
                // 为元素绑定click事件,事件方法为$methods下的方法,并将this指向this.$data
                node.addEventListener('click', this.$methods[attrVal].bind(this.$data));
            }
            
            if (node.hasAttribute("@click")) { // 该节点是否拥有@click指令
                var attrVal = node.getAttribute('@click'); // 获取指令对应的方法名
                // 为元素绑定click事件,事件方法为$methods下的方法,并将this指向this.$data
                node.addEventListener('click', this.$methods[attrVal].bind(this.$data));
            }
            
            if (node.hasAttribute("z-model")) { // 该节点是否拥有z-model指令
                var attrVal = node.getAttribute('z-model'); // 获取指令对应的数据属性
                node.addEventListener("input", ((i) => { // 为指令添加input事件
                    this.binding[attrVal].push(new Watcher(node, "value", this, attrVal)); // 将该元素添加为当前数据的订阅者,并将数据初始值作用与绑定指令的元素上
    
                    return () => { // input事件处理函数
                        this.$data[attrVal] = nodes[i].value; // 更新$data的属性值,会在观察者中劫持
                    }
                })(i));
            }
            
            if (node.hasAttribute("z-html")) { // 该节点是否拥有z-html指令
                var attrVal = node.getAttribute('z-html'); // 获取指令对应的数据属性
                this.binding[attrVal].push(new Watcher(node, 'innerHTML', this, attrVal));
            }
            
            if (node.hasAttribute('z-text')) { // 该节点是否用拥有z-text指令
                var attrVal = node.getAttribute('z-text'); // 获取指令对应的数据属性
                this.binding[attrVal].push(new Watcher(node, 'innerText', this, attrVal));
            }
        }
    }
    
    // 订阅者
    function Watcher(el, attr, vm, val) {
        this.el = el; // 指令对应的元素
        this.attr = attr; // 要更改的元素属性
        this.vm = vm; // 指令所在实例
        this.val = val; // 指令绑定的值
        this.update(); // 更新视图view
    }
    // 数据变化,更新视图。
    Watcher.prototype.update = function() {
        this.el[this.attr] = this.vm.$data[this.val];
    }
    

    相关文章

      网友评论

          本文标题:Vue 核心之数据双向绑定

          本文链接:https://www.haomeiwen.com/subject/habjsctx.html