Vue雙向數(shù)據(jù)綁定篇

一姜盈、Vue簡介

1.1 Vue是什么

Vue (讀音 /vju?/迹辐,類似于 view) 是一套用于構(gòu)建用戶界面的漸進式框架呵萨。與其它大型框架不同的是撒犀,Vue 被設(shè)計為可以自底向上逐層應(yīng)用某宪。Vue 的核心庫只關(guān)注視圖層仿村,不僅易于上手,還便于與第三方庫或既有項目整合兴喂。另一方面蔼囊,當與現(xiàn)代化的工具鏈以及各種支持類庫結(jié)合使用時,Vue 也完全能夠為復(fù)雜的單頁應(yīng)用提供驅(qū)動衣迷。
由于筆者水平有限畏鼓,如有不足和不正確的地方,請評論指出壶谒。

1.2 Vue解決了什么問題

  • 數(shù)據(jù)的雙向綁定
  • 組件化管理

1.3 怎么學(xué)習(xí)Vue

官網(wǎng)是最好的資料

二云矫、 MVVM

2.1 順便摘要下廖雪峰JavaScript教程的一段前端的發(fā)展史

在上個世紀的1989年,歐洲核子研究中心的物理學(xué)家Tim Berners-Lee發(fā)明了超文本標記語言(HyperText Markup Language)汗菜,簡稱HTML让禀,并在1993年成為互聯(lián)網(wǎng)草案挑社。從此,互聯(lián)網(wǎng)開始迅速商業(yè)化巡揍,誕生了一大批商業(yè)網(wǎng)站痛阻。
最早的HTML頁面是完全靜態(tài)的網(wǎng)頁,它們是預(yù)先編寫好的存放在Web服務(wù)器上的html文件腮敌。瀏覽器請求某個URL時录平,Web服務(wù)器把對應(yīng)的html文件扔給瀏覽器,就可以顯示html文件的內(nèi)容了缀皱。

如果要針對不同的用戶顯示不同的頁面斗这,顯然不可能給成千上萬的用戶準備好成千上萬的不同的html文件,所以啤斗,服務(wù)器就需要針對不同的用戶表箭,動態(tài)生成不同的html文件。一個最直接的想法就是利用C钮莲、C++這些編程語言免钻,直接向瀏覽器輸出拼接后的字符串。這種技術(shù)被稱為CGI:Common Gateway Interface崔拥。

很顯然极舔,像新浪首頁這樣的復(fù)雜的HTML是不可能通過拼字符串得到的。于是链瓦,人們又發(fā)現(xiàn)拆魏,其實拼字符串的時候,大多數(shù)字符串都是HTML片段慈俯,是不變的渤刃,變化的只有少數(shù)和用戶相關(guān)的數(shù)據(jù),所以贴膘,又出現(xiàn)了新的創(chuàng)建動態(tài)HTML的方式:ASP卖子、JSP和PHP——分別由微軟、SUN和開源社區(qū)開發(fā)刑峡。
在ASP中洋闽,一個asp文件就是一個HTML,但是突梦,需要替換的變量用特殊的<%=var%>標記出來了诫舅,再配合循環(huán)、條件判斷阳似,創(chuàng)建動態(tài)HTML就比CGI要容易得多骚勘。

但是,一旦瀏覽器顯示了一個HTML頁面,要更新頁面內(nèi)容俏讹,唯一的方法就是重新向服務(wù)器獲取一份新的HTML內(nèi)容当宴。如果瀏覽器想要自己修改HTML頁面的內(nèi)容,就需要等到1995年年底泽疆,JavaScript被引入到瀏覽器户矢。

有了JavaScript后,瀏覽器就可以運行JavaScript殉疼,然后梯浪,對頁面進行一些修改。JavaScript還可以通過修改HTML的DOM結(jié)構(gòu)和CSS來實現(xiàn)一些動畫效果瓢娜,而這些功能沒法通過服務(wù)器完成挂洛,必須在瀏覽器實現(xiàn)。

第一階段眠砾,直接用JavaScript操作DOM節(jié)點虏劲,使用瀏覽器提供的原生API:

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

第二階段,由于原生API不好用褒颈,還要考慮瀏覽器兼容性柒巫,jQuery橫空出世,以簡潔的API迅速俘獲了前端開發(fā)者的芳心

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

第三階段谷丸,MVC模式堡掏,需要服務(wù)器端配合,JavaScript可以在前端修改服務(wù)器渲染后的數(shù)據(jù)刨疼。
現(xiàn)在泉唁,隨著前端頁面越來越復(fù)雜,用戶對于交互性要求也越來越高币狠,想要寫出Gmail這樣的頁面游两,僅僅用jQuery是遠遠不夠的砾层。MVVM模型應(yīng)運而生漩绵。

MVVM最早由微軟提出來,它借鑒了桌面應(yīng)用程序的MVC思想肛炮,在前端頁面中止吐,把Model用純JavaScript對象表示,View負責(zé)顯示侨糟,兩者做到了最大限度的分離碍扔。

把Model和View關(guān)聯(lián)起來的就是ViewModel。ViewModel負責(zé)把Model的數(shù)據(jù)同步到View顯示出來秕重,還負責(zé)把View的修改同步回Model不同。

其實從jq語法的引入操作DOM結(jié)構(gòu),變的容易的多了。但是如果能直接該表javaScript對象就能導(dǎo)致DOM結(jié)構(gòu)做出對應(yīng)的變化二拐,那該多好呀服鹅,而MVVM就把開發(fā)者從DOM的繁瑣步驟中解脫出來了,而更加關(guān)注Mode的變化百新。

三企软、步步為營

3.1 主流雙向綁定的做法

手動綁定
臟值檢查(angular.js)
數(shù)據(jù)劫持

具體的做法可以參考javascript實現(xiàn)數(shù)據(jù)雙向綁定的三種方式

3.2 簡要概述以上做法:

雙向綁定從本質(zhì)上來說無非兩部分 Model->View 與 View->Model

3.2.1 首先是Model->View的思路

model無非是個Object,或者是如Vue里面是個全局的vm.data
view 在html上無疑是個樹形的標簽結(jié)構(gòu)饭望,所以也就是node這樣結(jié)構(gòu)
最直接的做法遍歷仗哨。
先看下最基本的vue代碼
html

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

可以看到Vue里面綁定數(shù)據(jù)無非兩種,<input type="text" v-model="input" id="input"> 其中 v-model加載<>中铅辞,也就是給標簽增加新的屬性厌漂,和data-的方式增加屬性一般無二,(PS:順便提及小程序中函數(shù)傳參斟珊,運用就是這樣的方法)桩卵。
So, a:for也罷,v-model也罷倍宾,或者其他各種種種無非是標識符不同而已雏节,萬變不離其中。第二部分就是關(guān)于'{{}}'高职,因為其實在標簽內(nèi)部钩乍,比如<p>{{input}}</p>可以看到, {{input}}并不作為app的子節(jié)點怔锌,所以當為元素節(jié)點的是寥粹,判斷是否有子節(jié)點,有則再次調(diào)用scan函數(shù)埃元。
所以有了涝涤,第一簡單的方法就是每次改變data數(shù)值的時候,直接再次調(diào)用scan函數(shù)(PS:因為scan方法因為 先遍歷node列表岛杀,再遍歷該節(jié)點的屬性阔拳,所以會是雙層遍歷)
也就是簡單綁定的方法

        /**
         * 設(shè)置數(shù)據(jù)后掃描
         */
        function mvSet(key, value){
            data[key] = value;
            scan();
        }

第二種臟值檢查
直接封裝和執(zhí)行$digest()$apply()

/**
     * 臟循環(huán)檢測
     * @param  {[type]} elems [description]
     * @return {[type]}       [description]
     */
    var digest = function(elems) {
        /**
         * 掃描帶指令的節(jié)點屬性
         */
        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) {
                    /**
                     * 調(diào)用屬性指令
                     */
                    var dataKey = elem.getAttribute('ng-bind') || undefined;

                    /**
                     * 進行臟數(shù)據(jù)檢測,如果數(shù)據(jù)改變类嗤,則重新執(zhí)行指令糊肠,否則跳過
                     */
                    if(elem.command[attr.nodeValue] !== data[dataKey]){

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

第三種方式 采用Object.defineProperty對數(shù)據(jù)對象做屬性get和set的監(jiān)聽,但是需要注意的是為了保存?zhèn)鬟M來的數(shù)值遗锣,并且避免無效循環(huán)货裹,采用如下方法用于獨立的函數(shù),value來存儲對應(yīng)的對應(yīng)的數(shù)值精偿。

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 關(guān)于設(shè)計模式

從Model->View以及后面的從 View->Model相信大家也能看到毡惜,其實這三種綁定方式,最大區(qū)別體現(xiàn)在Model->層斯撮。雖然我們可以通過遍歷的方式對應(yīng)地修改對應(yīng)的標簽的屬性经伙。也能通過我們自己指定的標識符比如’v-model‘, 'ng-text','{{}}',甚至比如采用自己的名稱的前綴比如筆者的'sl-text'等等來采用需要雙向綁定的標簽元素采用列表的統(tǒng)一管理,這樣能減少遍歷次數(shù)勿锅,也可以對于v-model綁定的屬性帕膜,通過列表添加到該標簽,作為其的一個屬性溢十,但是是否還能進一步優(yōu)化垮刹。
引用一張''Header First"設(shè)計模式上的觀察者模式一圖,如下:


image.png

在javascript沒有像協(xié)議這樣的語法张弛,不過原理還是一致荒典,改良好的雙向數(shù)據(jù)綁定模型如下代碼。

  //第三部分
    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數(shù)組
        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'
        }
    });

大體邏輯表現(xiàn)為,首先定義觀察者Watcher吞鸭,并在編譯函數(shù)compile()中對每個節(jié)點添加觀察著Watcher寺董,當接收到分發(fā)者指令時,調(diào)用update方法更新視圖刻剥。接下來定義消息分發(fā)者Dep遮咖,Dep維護觀察者數(shù)組,當值發(fā)生變化時造虏,通知各觀察者調(diào)用update方法御吞。


image.png

四、附上源碼

源碼的github地址

image.png

參考文獻和鏈接

官網(wǎng)
剖析Vue原理&實現(xiàn)雙向綁定MVVM
廖雪峰MVVM
談?wù)凧avaScript中的雙向數(shù)據(jù)綁定
【JavaScript學(xué)習(xí)筆記】自己實現(xiàn)雙向綁定
剖析Vue原理&實現(xiàn)雙向綁定MVVM

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末漓藕,一起剝皮案震驚了整個濱河市陶珠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌享钞,老刑警劉巖揍诽,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異嫩与,居然都是意外死亡寝姿,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門划滋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人埃篓,你說我怎么就攤上這事处坪。” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵同窘,是天一觀的道長玄帕。 經(jīng)常有香客問我,道長想邦,這世上最難降的妖魔是什么裤纹? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮丧没,結(jié)果婚禮上鹰椒,老公的妹妹穿的比我還像新娘。我一直安慰自己呕童,他們只是感情好漆际,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著夺饲,像睡著了一般奸汇。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上往声,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天擂找,我揣著相機與錄音,去河邊找鬼浩销。 笑死婴洼,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的撼嗓。 我是一名探鬼主播柬采,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼且警!你這毒婦竟也來了粉捻?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤斑芜,失蹤者是張志新(化名)和其女友劉穎肩刃,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體杏头,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡盈包,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了醇王。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呢燥。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖寓娩,靈堂內(nèi)的尸體忽然破棺而出叛氨,到底是詐尸還是另有隱情呼渣,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布寞埠,位于F島的核電站屁置,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏仁连。R本人自食惡果不足惜蓝角,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望饭冬。 院中可真熱鬧使鹅,春花似錦、人聲如沸伍伤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽扰魂。三九已至麦乞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間劝评,已是汗流浹背姐直。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蒋畜,地道東北人声畏。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像姻成,于是被迫代替她去往敵國和親插龄。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 一:什么是閉包科展?閉包的用處均牢? (1)閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。在本質(zhì)上才睹,閉包就 是將函數(shù)內(nèi)部和函數(shù)外...
    xuguibin閱讀 9,593評論 1 52
  • 1徘跪、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明先生_X自主閱讀 15,979評論 3 119
  • 今天看了《明亮的星》,男主居然是演《香水》的琅攘,好出戲啊垮庐。看了《If only》坞琴,編劇是完全的女性視角啊哨查,愛情中又不...
    行走ing閱讀 160評論 0 0
  • 昨天上午倒沒什么可干的,自己也沒有去找事做置济,也就渾渾噩噩的過去了解恰,中午吃過飯以后锋八,和舍友一起研究去哪里玩浙于,我們都在...
    堅志閱讀 140評論 0 0
  • 今晚有個人對我說护盈,我喜歡你,讓我照顧你吧羞酗,我明知道我們沒有結(jié)果腐宋,而我還是自私的想留住那稀缺的溫暖,一而再檀轨,再而三的...
    南北無恙閱讀 194評論 0 0