为了账号安全,请及时绑定邮箱和手机立即绑定

Vue双向数据绑定篇

标签:
Vue.js

一、Vue简介

1.1 Vue是什么

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
由于笔者水平有限,如有不足和不正确的地方,请评论指出。

1.2 Vue解决了什么问题

  • 数据的双向绑定

  • 组件化管理

1.3 怎么学习Vue

官网是最好的资料

二、 MVVM

2.1 顺便摘要下廖雪峰JavaScript教程的一段前端的发展史

在上个世纪的1989年,欧洲核子研究中心的物理学家Tim Berners-Lee发明了超文本标记语言(HyperText Markup Language),简称HTML,并在1993年成为互联网草案。从此,互联网开始迅速商业化,诞生了一大批商业网站。
最早的HTML页面是完全静态的网页,它们是预先编写好的存放在Web服务器上的html文件。浏览器请求某个URL时,Web服务器把对应的html文件扔给浏览器,就可以显示html文件的内容了。

如果要针对不同的用户显示不同的页面,显然不可能给成千上万的用户准备好成千上万的不同的html文件,所以,服务器就需要针对不同的用户,动态生成不同的html文件。一个最直接的想法就是利用C、C++这些编程语言,直接向浏览器输出拼接后的字符串。这种技术被称为CGI:Common Gateway Interface。

很显然,像新浪首页这样的复杂的HTML是不可能通过拼字符串得到的。于是,人们又发现,其实拼字符串的时候,大多数字符串都是HTML片段,是不变的,变化的只有少数和用户相关的数据,所以,又出现了新的创建动态HTML的方式:ASP、JSP和PHP——分别由微软、SUN和开源社区开发。
在ASP中,一个asp文件就是一个HTML,但是,需要替换的变量用特殊的<%=var%>标记出来了,再配合循环、条件判断,创建动态HTML就比CGI要容易得多。

但是,一旦浏览器显示了一个HTML页面,要更新页面内容,唯一的方法就是重新向服务器获取一份新的HTML内容。如果浏览器想要自己修改HTML页面的内容,就需要等到1995年年底,JavaScript被引入到浏览器。

有了JavaScript后,浏览器就可以运行JavaScript,然后,对页面进行一些修改。JavaScript还可以通过修改HTML的DOM结构和CSS来实现一些动画效果,而这些功能没法通过服务器完成,必须在浏览器实现。

第一阶段,直接用JavaScript操作DOM节点,使用浏览器提供的原生API:

var dom = document.getElementById('name');
dom.innerHTML = 'Homer';
dom.style.color = 'red';

第二阶段,由于原生API不好用,还要考虑浏览器兼容性,jQuery横空出世,以简洁的API迅速俘获了前端开发者的芳心

$('#name').text('Homer').css('color', 'red');

第三阶段,MVC模式,需要服务器端配合,JavaScript可以在前端修改服务器渲染后的数据。
现在,随着前端页面越来越复杂,用户对于交互性要求也越来越高,想要写出Gmail这样的页面,仅仅用jQuery是远远不够的。MVVM模型应运而生。

MVVM最早由微软提出来,它借鉴了桌面应用程序的MVC思想,在前端页面中,把Model用纯JavaScript对象表示,View负责显示,两者做到了最大限度的分离。

把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model。

其实从jq语法的引入操作DOM结构,变的容易的多了。但是如果能直接该表javaScript对象就能导致DOM结构做出对应的变化,那该多好呀,而MVVM就把开发者从DOM的繁琐步骤中解脱出来了,而更加关注Mode的变化。

三、步步为营

3.1 主流双向绑定的做法

手动绑定
脏值检查(angular.js)
数据劫持

具体的做法可以参考javascript实现数据双向绑定的三种方式

3.2 简要概述以上做法:

双向绑定从本质上来说无非两部分 Model->View 与  View->Model

3.2.1 首先是Model->View的思路

model无非是个Object,或者是如Vue里面是个全局的vm.data
view 在html上无疑是个树形的标签结构,所以也就是node这样结构
最直接的做法遍历。
先看下最基本的vue代码
html

 <div id="app">    <input type="text" v-model="input" id="input">
    {{text}}    <p>{{input}}</p>
    <p id="show"></p></div>

可以看到Vue里面绑定数据无非两种,<input type="text" v-model="input" id="input">  其中 v-model加载<>中,也就是给标签增加新的属性,和data-的方式增加属性一般无二,(PS:顺便提及小程序中函数传参,运用就是这样的方法)。
So, a:for也罢,v-model也罢,或者其他各种种种无非是标识符不同而已,万变不离其中。第二部分就是关于'{{}}',因为其实在标签内部,比如<p>{{input}}</p>可以看到, {{input}}并不作为app的子节点,所以当为元素节点的是,判断是否有子节点,有则再次调用scan函数。
所以有了,第一简单的方法就是每次改变data数值的时候,直接再次调用scan函数(PS:因为scan方法因为 先遍历node列表,再遍历该节点的属性,所以会是双层遍历)
也就是简单绑定的方法

        /**
         * 设置数据后扫描
         */
        function mvSet(key, value){
            data[key] = value;
            scan();
        }

第二种脏值检查
直接封装和执行$digest()$apply()

/**
     * 脏循环检测
     * @param  {[type]} elems [description]
     * @return {[type]}       [description]
     */
    var digest = function(elems) {        /**
         * 扫描带指令的节点属性
         */
        for (var i = 0, len = elems.length; i < len; i++) {            var elem = elems[i];            for (var j = 0, len1 = elem.attributes.length; j < len1; j++) {                var attr = elem.attributes[j];                if (attr.nodeName.indexOf('q-event') >= 0) {                    /**
                     * 调用属性指令
                     */
                    var dataKey = elem.getAttribute('ng-bind') || undefined;                    /**
                     * 进行脏数据检测,如果数据改变,则重新执行指令,否则跳过
                     */
                    if(elem.command[attr.nodeValue] !== data[dataKey]){

                        command[attr.nodeValue].call(elem, data[dataKey]);
                        elem.command[attr.nodeValue] = data[dataKey];
                    }
                }
            }
        }
    }

第三种方式 采用Object.defineProperty对数据对象做属性get和set的监听,但是需要注意的是为了保存传进来的数值,并且避免无效循环,采用如下方法用于独立的函数,value来存储对应的对应的数值。

function defineProperty(vm, key, val){    Object.defineProperty(vm, key, {        get: function (){            return val;
        },        set: function (newValue){            document.getElementById("show").innerHTML = newValue;            document.getElementById("input").value = newValue;            if(newValue === val){                return;
            }
            val = newValue;
        }
    });
} 
function observe(data, vm){    Object.keys(data).forEach(function(key){
        defineProperty(vm, key, data[key]);
    });

3.2.2 View->Model

View到Model无非一些可以改变的标签,比如input等,而view到Model基本的思路都是原生的事件的一些方法。比如如下代码。

document.getElementById('input').addEventListener('keyup', function (e) {
            obj.txt = e.target.value;
        });

3.2.3 关于设计模式

从Model->View以及后面的从 View->Model相信大家也能看到,其实这三种绑定方式,最大区别体现在Model->层。虽然我们可以通过遍历的方式对应地修改对应的标签的属性。也能通过我们自己指定的标识符比如’v-model‘, 'ng-text','{{}}',甚至比如采用自己的名称的前缀比如笔者的'sl-text'等等来采用需要双向绑定的标签元素采用列表的统一管理,这样能减少遍历次数,也可以对于v-model绑定的属性,通过列表添加到该标签,作为其的一个属性,但是是否还能进一步优化。
引用一张''Header First"设计模式上的观察者模式一图,如下:


webp

image.png

在javascript没有像协议这样的语法,不过原理还是一致,改良好的双向数据绑定模型如下代码。

  //第三部分
    function Watcher(vm, node, name, nodeType){
        Dep.target = this;        this.vm = vm;        this.node = node;        this.name = name;        this.nodeType = nodeType;        this.update();
        Dep.target = null;
    }
    Watcher.prototype = {        update: function(){            this.get();            if (this.nodeType === 'text') {                this.node.nodeValue = this.value;
            }            if (this.nodeType === 'input') {                this.node.value = this.value;
            }
        },        get: function(){            this.value = this.vm[this.name];
        }
    }    function Dep(){        this.subs = [];
    }
    Dep.prototype = {        addSub: function(sub){            this.subs.push(sub);
        },        notify: function(){            this.subs.forEach(function(sub){
                sub.update();
            });
        }
    }    //第二部分
    function defineProperty(vm, key, val){        var dep = new Dep();        Object.defineProperty(vm, key, {            get: function (){                if(Dep.target){
                    dep.addSub(Dep.target);
                }                return val;
            },            set: function (newValue){                if(newValue === val){                    return;
                }
                val = newValue;
                dep.notify();
            }
        });
    }    function observe(data, vm){        //Object.keys(data)返回data的key数组
        Object.keys(data).forEach(function(key){
            defineProperty(vm, key, data[key]);
        });
    }    //第一部分
    function compile(node, vm){        if(node.nodeType === 1){            var attr = node.attributes;            for(let i = 0; i<attr.length; i++){                if(attr[i].nodeName === 'v-model'){                    let name = attr[i].nodeValue;
                    node.addEventListener('keyup', function(e){
                        vm[name] = e.target.value;
                    });
                    node.value = vm[name];
                    node.removeAttribute('v-model');                    new Watcher(vm, node, name, "input");
                }
            }            if (child = node.firstChild) {
                compile(child, vm);
            }
        }        if(node.nodeType === 3){            let reg = /\{\{(.*)\}\}/;            if(reg.test(node.nodeValue)){                let name = RegExp.$1;
                name = name.trim();                // node.nodeValue = vm.data[name];
                new Watcher(vm, node, name, "text");
            }
        }
    }    function nodeToFragment(node, vm){        var flag = document.createDocumentFragment();        var child;        while(child = node.firstChild){
            compile(child, vm);
            flag.appendChild(child);
        }        return flag;
    }    function Vue(options){        var id = options.el;        var data = options.data;
        observe(data, this);        var dom = nodeToFragment(document.getElementById(id), this);        document.getElementById(id).appendChild(dom);
    }    var vm = new Vue({        el: 'app',        data: {            input: 'hello'
        }
    });

大体逻辑表现为,首先定义观察者Watcher,并在编译函数compile()中对每个节点添加观察着Watcher,当接收到分发者指令时,调用update方法更新视图。接下来定义消息分发者Dep,Dep维护观察者数组,当值发生变化时,通知各观察者调用update方法。


webp

image.png

四、附上源码

源码的github地址

webp

image.png



作者:破晓霜林
链接:https://www.jianshu.com/p/4cfbeddc5db6


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消