zoukankan      html  css  js  c++  java
  • Vue | 双向数据绑定

    什么是双向数据绑定?

    根据流程图实现一个MVVM:

    如上图所示,我们可以看到,整体实现分为四步:

    1、实现一个Compile,对指令进行解析,初始化视图,并且订阅数据的变更,绑定好更新函数

    2、实现一个Observer,对数据进行劫持,通知数据的变化

    3、实现一个Watcher,将其作为以上两者的一个中介点,在接收数据变更的同时,让Dep添加当前Watcher,并及时通知视图进行update。

    4、实现MVVM,整合以上三者,作为一个入口函数

    流程解读:

    第一步:创建MVVM、Compile类,并且利用createDocumentFragment将<div id="app"></div>下的标签放到JS文档碎片中去。

    第二步:对 标签 进行编译,将带有 v- 指令的标签和{{ }}的标签解析出来

    第三步:创建Observer类进行数据劫持、深度递归劫持,当data中设置值或者修改值的时候,利用Object.defineProperty对值进行监控。

    第四步:创建Watch类观察者,用新值和老值进行比对,如果发生变化,就调用更新方法,进行视图更新。

    第五步:将输入框v-model和视图绑定起来,输入框的值变化,同时页面中通过{{}}绑定的值也变化,实现双向数据绑定。

    1.Vue双向绑定原理

    vue.js 则是采用数据劫持结合发布者-订阅者模式的方式

    通过Object.defineProperty()来劫持各个属性的setter,getter

    在数据变动时发布消息给订阅者,触发相应的监听回调。

    我们先来看Object.defineProperty()这个方法:

    var obj  = {};
    Object.defineProperty(obj, 'name', {
            get: function() {
                console.log('我被获取了')
                return val;
            },
            set: function (newVal) {
                console.log('我被设置了')
            }
    })
    obj.name = '张三';//在给obj设置name属性的时候,触发了set这个方法
    var val = obj.name;//在得到obj的name属性,会触发get方法
    

    vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持

    那么在设置或者获取的时候我们就可以在get或者set方法里假如其他的触发函数,达到监听数据变动的目的

    2.实现最简单的双向绑定

    通过Object.defineProperty()可以实现数据劫持,是的属性在赋值的时候触发set方法

    <!doctype html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    <body>
        <div id="demo"></div>
        <input type="text" id="inp">
        <script>
            var obj  = {};
            var demo = document.querySelector('#demo')
            var inp = document.querySelector('#inp')
            Object.defineProperty(obj, 'name', {
                get: function() {
                    return val;
                },
                set: function (newVal) {//当该属性被赋值的时候触发
                    inp.value = newVal;
                    demo.innerHTML = newVal;
                }
            })
            inp.addEventListener('input', function(e) {
                // 给obj的name属性赋值,进而触发该属性的set方法
                obj.name = e.target.value;
            });
            obj.name = 'fei';//在给obj设置name属性的时候,触发了set这个方法
        </script>
    </body>
    </html>
    

    当然要是这么粗暴,肯定不行,性能会出很多的问题

    3.讲解vue如何实现

    原理图:

    observer用来实现对每个vue中的data中定义的属性循环用Object.defineProperty()实现数据劫持

    以便利用其中的setter和getter,然后通知订阅者,订阅者会触发它的update方法,对视图进行更新。

    在vue中v-model,v-name,{{}}等都可以对数据进行显示,也就是说假如一个属性都通过这三个指令了,那么每当这个属性改变的时候,相应的这个三个指令的html视图也必须改变

    于是vue中就是每当有这样的可能用到双向绑定的指令,就在一个Dep中增加一个订阅者,其订阅者只是更新自己的指令对应的数据

    也就是v-model='name'和{{name}}有两个对应的订阅者,各自管理自己的地方。每当属性的set方法触发,就循环更新Dep中的订阅者。

    4.vue代码实现

    observer实现,主要是给每个vue的属性用Object.defineProperty(),代码如下:
    function defineReactive (obj, key, val) {
        var dep = new Dep();
            Object.defineProperty(obj, key, {
                 get: function() {
                        //添加订阅者watcher到主题对象Dep
                        if(Dep.target) {
                            // JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
                            dep.addSub(Dep.target);
                        }
                        return val;
                 },
                 set: function (newVal) {
                        if(newVal === val) return;
                        val = newVal;
                        console.log(val);
                        // 作为发布者发出通知
                        dep.notify();//通知后dep会循环调用各自的update方法更新视图
                 }
           })
    }
            function observe(obj, vm) {
                Object.keys(obj).forEach(function(key) {
                    defineReactive(vm, key, obj[key]);
                })
            }
    
    实现compile: compile的目的就是解析各种指令称真正的html
    function Compile(node, vm) {
        if(node) {
            this.$frag = this.nodeToFragment(node, vm);
            return this.$frag;
        }
    }
    Compile.prototype = {
        nodeToFragment: function(node, vm) {
            var self = this;
            var frag = document.createDocumentFragment();
            var child;
            while(child = node.firstChild) {
                console.log([child])
                self.compileElement(child, vm);
                frag.append(child); // 将所有子节点添加到fragment中
            }
            return frag;
        },
        compileElement: function(node, vm) {
            var reg = /{{(.*)}}/;
            //节点类型为元素(input元素这里)
            if(node.nodeType === 1) {
                var attr = node.attributes;
                // 解析属性
                for(var i = 0; i < attr.length; i++ ) {
                    if(attr[i].nodeName == 'v-model') {//遍历属性节点找到v-model的属性
                        var name = attr[i].nodeValue; // 获取v-model绑定的属性名
                        node.addEventListener('input', function(e) {
                            // 给相应的data属性赋值,进而触发该属性的set方法
                            vm[name]= e.target.value;
                        });
                        new Watcher(vm, node, name, 'value');//创建新的watcher,会触发函数向对应属性的dep数组中添加订阅者,
                    }
                };
            }
            //节点类型为text
            if(node.nodeType === 3) {
                if(reg.test(node.nodeValue)) {
                    var name = RegExp.$1; // 获取匹配到的字符串
                    name = name.trim();
                    new Watcher(vm, node, name, 'nodeValue');
                }
            }
        }
    }
    
    watcher实现
    function Watcher(vm, node, name, type) {
        Dep.target = this;
        this.name = name;
        this.node = node;
        this.vm = vm;
        this.type = type;
        this.update();
        Dep.target = null;
    }
    
    Watcher.prototype = {
        update: function() {
            this.get();
            this.node[this.type] = this.value; // 订阅者执行相应操作
        },
        // 获取data的属性值
        get: function() {
            console.log(1)
            this.value = this.vm[this.name]; //触发相应属性的get
        }
    }
    
    实现Dep来为每个属性添加订阅者
    function Dep() {
        this.subs = [];
    }
    Dep.prototype = {
        addSub: function(sub) {
            this.subs.push(sub);
        },
        notify: function() {
            this.subs.forEach(function(sub) {
            sub.update();
            })
        }
    }
    

    梳理

    首先我们为每个vue属性用Object.defineProperty()实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep

    然后在编译的时候在该属性的数组dep中添加订阅者,v-model会添加一个订阅者,{{}}也会,v-bind也会,只要用到该属性的指令理论上都会

    接着为input会添加监听事件,修改值就会为该属性赋值,触发该属性的set方法,在set方法内通知订阅者数组dep

    订阅者数组循环调用各订阅者的update方法更新视图。

  • 相关阅读:
    Firemonkey 控件设定字型属性及颜色
    ListView 使用 LiveBindings 显示超过 200 条记录
    Firemonkey ListView 获取项目右方「>」(Accessory) 事件
    XE7 Update 1 选 iOS 8.1 SDK 发布 iPhone 3GS 实机测试
    Firemonkey Bitmap 设定像素颜色 Pixel
    Firemonkey 移动平台 Form 显示使用 ShowModal 范例
    XE7 提交 App(iOS 8)提示「does not contain the correct beta entitlement」问题修复
    XE7 Android 中使用 MessageDlg 范例
    导出 XE6 预设 Android Style (*.style) 档案
    修正 Memo 設定為 ReadOnly 後, 無法有複製的功能
  • 原文地址:https://www.cnblogs.com/pp-yang/p/12520898.html
Copyright © 2011-2022 走看看