Vue双向数据绑定篇

一、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"设计模式上的观察者模式一图,如下:


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方法。


image.png

四、附上源码

源码的github地址

image.png

参考文献和链接

官网
剖析Vue原理&实现双向绑定MVVM
廖雪峰MVVM
谈谈JavaScript中的双向数据绑定
【JavaScript学习笔记】自己实现双向绑定
剖析Vue原理&实现双向绑定MVVM

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,884评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,212评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,351评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,412评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,438评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,127评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,714评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,636评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,173评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,264评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,402评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,073评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,763评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,253评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,382评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,749评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,403评论 2 358

推荐阅读更多精彩内容

  • 一:什么是闭包?闭包的用处? (1)闭包就是能够读取其他函数内部变量的函数。在本质上,闭包就 是将函数内部和函数外...
    xuguibin阅读 9,629评论 1 52
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_X自主阅读 15,985评论 3 119
  • 今天看了《明亮的星》,男主居然是演《香水》的,好出戏啊。看了《If only》,编剧是完全的女性视角啊,爱情中又不...
    行走ing阅读 161评论 0 0
  • 昨天上午倒没什么可干的,自己也没有去找事做,也就浑浑噩噩的过去了,中午吃过饭以后,和舍友一起研究去哪里玩,我们都在...
    坚志阅读 141评论 0 0
  • 今晚有个人对我说,我喜欢你,让我照顾你吧,我明知道我们没有结果,而我还是自私的想留住那稀缺的温暖,一而再,再而三的...
    南北无恙阅读 195评论 0 0