構建利用Proxy和Reflect實現雙向數據綁定的微框架(基于ES6)

寫在前面:這篇文章講述了如何利用Proxy和Reflect實現雙向數據綁定,個人系Vue早期玩家,寫這個小框架的時候也沒有參考Vue等源代碼,之前了解過其他實現,但沒有直接參考其他代碼,如有雷同,純屬巧合。

代碼下載地址:這里下載

綜述

關于Proxy和Reflect的資料推薦阮老師的教程:http://es6.ruanyifeng.com/ 這里不做過多介紹。

實現雙向數據綁定的方法有很多,也可以參考本專欄之前的其他實現,我之所以選擇用Proxy和Reflect,一方面是因為可以大量節約代碼,并且簡化邏輯,可以讓我把更多的經歷放在其他內容的構建上面,另外一方面本項目直接基于ES6,用這些內容也符合面向未來的JS編程規范,第三點最后說。

由于這個小框架是自己在PolarBear這個咖啡館在一個安靜的午后開始寫成,暫且起名Polar,日后希望我能繼續完善這個小框架,給添加上更多有趣的功能。

首先我們可以看整體功能演示:
[一個gif動圖,如果不能看,請點擊這里的鏈接]

代碼分析

我們要做這樣一個小框架,核心是要監聽數據的改變,并且在數據的改變的時候進行一些操作,從而維持數據的一致。

我的思路是這樣的:

  • 將所有的數據信息放在一個屬性對象中(this._data),之后給這個屬性對象用Proxy包裝set,在代理函數中我們更新屬性對象的具體內容,同時通知所有監聽者,之后返回新的代理對象(this.data),我們之后操作的都是新的代理對象。
  • 對于input等表單,我們需要監聽input事件,在回調函數中直接設置我們代理好的數據對象,從而觸發我們的代理函數。
  • 我們同時也應該支持事件機制,這里我們以最常用的click方法作為例子實現。

下面開始第一部分,我們希望我們之后使用這個庫的時候可以這樣調用:

<div id="app">
    <form>
        <label>name:</label>
        <input p-model = "name" />
    </form>
    <div>name:{{name}} age:{{age}}</div>
    <i>note:{{note}}</i><br/>
    <button p-click="test(2)">button1</button>
</div>
<script>
 var myPolar = new Polar({
        el:"#app",
        data: {
            name: "niexiaotao",
            age:16,
            note:"Student of Zhejiang University"
        },
        methods:{
            test:function(e,addNumber){
                console.log("e:",e);
                this.data.age+=Number(addNumber);
            }
        }
});
</script>

沒錯,和Vue神似吧,所以這種調用方式應當為我們所熟悉。

我們需要建立一個Polar類,這個類的構造函數應該進行一些初始化操作:

 constructor(configs){
        this.root = this.el = document.querySelector(configs.el);
        this._data = configs.data;
        this._data.__bindings = {};
        //創建代理對象
        this.data = new Proxy(this._data, {set});
        this.methods = configs.methods;

        this._compile(this.root);
}

這里面的一部份內容是直接將我們傳入的configs按照屬性分別賦值,另外就是我們創建代理對象的過程,最后的_compile方法可以理解為一個私有的初始化方法。

實際上我把剩下的內容幾乎都放在_compile方法里面了,這樣理解起來方便,但是之后可能要改動。

我們還是先不能看我們代理的set該怎么寫,因為這個時候我們還要先繼續梳理思路:

假設我們這樣<div>name:{{name}}</div>將數據綁定到dom節點,這個時候我們需要做什么呢,或者說,我們通過什么方式讓dom節點和數據對應起來,隨著數據改變而改變。

看上文的__bindings。這個對象用來存儲所有綁定的dom節點信息,__bindings本身是一個對象,每一個有對應dom節點綁定的數據名稱都是它的屬性,對應一個數組,數組中的每一個內容都是一個綁定信息,這樣,我們在自己寫的set代理函數中,我們一個個調用過去,就可以更新內容了:

dataSet.__bindings[key].forEach(function(item){
       //do something to update...
});

我這里創建了一個用于構造調用的函數,這個函數用于創建存儲綁定信息的對象:

function Directive(el,polar,attr,elementValue){
    this.el=el;//元素本身dom節點
    this.polar = polar;//對應的polar實例
    this.attr = attr;//元素的被綁定的屬性值,比如如果是文本節點就可以是nodeValue
    this.el[this.attr] = this.elementValue = elementValue;//初始化
}

這樣,我們的set可以這樣寫:

function set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver);
    var dataSet = receiver || target;
    dataSet.__bindings[key].forEach(function(item){
        item.el[item.attr] = item.elementValue = value;
    });
    return result;
}

接下來可能還有一個問題:我們的{{name}}實際上只是節點的一部分,這并不是節點啊,另外我們是不是還可以這么寫:<div>name:{{name}} age:{{age}}</div>

關于這兩個問題,前者的答案是我們將{{name}}替換成一個文本節點,而為了應對后者的情況,我們需要將兩個被綁定數據中間和前后的內容,都變成新的文本節點,然后這些文本節點組成文本節點串。(這里多說一句,html5的normalize方法可以將多個文本節點合并成一個,如果不小心調用了它,那我們的程序就要GG了)

所以我們在_compile函數首先:

var _this = this;

        var nodes = root.children;

        var bindDataTester = new RegExp("{{(.*?)}}","ig");

        for(let i=0;i<nodes.length;i++){
            var node=nodes[i];

            //如果還有html字節點,則遞歸
            if(node.children.length){
                this._compile(node);
            }

            var matches = node.innerHTML.match(bindDataTester);
            if(matches){
                var newMatches = matches.map(function (item) {
                    return  item.replace(/{{(.*?)}}/,"$1")
                });
                var splitTextNodes  = node.innerHTML.split(/{{.*?}}/);
                node.innerHTML=null;
                //更新DOM,處理同一個textnode里面多次綁定情況
                if(splitTextNodes[0]){
                    node.append(document.createTextNode(splitTextNodes[0]));
                }
                for(let ii=0;ii<newMatches.length;ii++){
                    var el = document.createTextNode('');
                    node.appendChild(el);
                    if(splitTextNodes[ii+1]){
                        node.append(document.createTextNode(splitTextNodes[ii+1]));
                    }
                //對數據和dom進行綁定
                let returnCode = !this._data.__bindings[newMatches[ii]]?
                    this._data.__bindings[newMatches[ii]] = [new Directive(el,this,"nodeValue",this.data[newMatches[ii]])]
                    :this._data.__bindings[newMatches[ii]].push(new Directive(el,this,"nodeValue",this.data[newMatches[ii]]))
                }
            }

這樣,我們的數據綁定階段就寫好了,接下來,我們處理<input p-model = "name" />這樣的情況。

這實際上是一個指令,我們只需要當識別到這一個指令的時候,做一些處理,即可:

if(node.hasAttribute(("p-model"))
                && node.tagName.toLocaleUpperCase()=="INPUT" || node.tagName.toLocaleUpperCase()=="TEXTAREA"){
                node.addEventListener("input", (function () {

                    var attributeValue = node.getAttribute("p-model");

                    if(_this._data.__bindings[attributeValue]) _this._data.__bindings[attributeValue].push(new Directive(node,_this,"value",_this.data[attributeValue])) ;
                    else _this._data.__bindings[attributeValue] = [new Directive(node,_this,"value",_this.data[attributeValue])];

                    return function (event) {
                        _this.data[attributeValue]=event.target.value
                    }
                })());
}

請注意,上面調用了一個IIFE,實際綁定的函數只有返回的函數那一小部分。

最后我們處理事件的情況:<button p-click="test(2)">button1</button>

實際上這比處理p-model還簡單,但是我們為了支持函數參數的情況,處理了一下傳入參數,另外我實際上將event始終作為一個參數傳遞,這也許并不是好的實踐,因為使用的時候還要多注意。

if(node.hasAttribute("p-click")) {
                node.addEventListener("click",function(){
                    var attributeValue=node.getAttribute("p-click");
                    var args=/\(.*\)/.exec(attributeValue);
                    //允許參數
                    if(args) {
                        args=args[0];
                        attributeValue=attributeValue.replace(args,"");
                        args=args.replace(/[\(\)\'\"]/g,'').split(",");
                    }
                    else args=[];
                    return function (event) {
                        _this.methods[attributeValue].apply(_this,[event,...args]);
                    }
                }());
}

現在我們已經將所有的代碼分析完了,是不是很清爽?代碼除去注釋約100行,所有源代碼可以在這里下載。這當然不能算作一個框架了,不過可以學習學習,這學期有時間的話,還要繼續完善,也歡迎大家一起探討。

一起學習,一起提高,做技術應當是直接的,有問題歡迎指出~


最后說的第三點:是自己還是一個學生,做這些內容也僅僅是出于興趣。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,781評論 18 139
  • 工廠模式類似于現實生活中的工廠可以產生大量相似的商品,去做同樣的事情,實現同樣的效果;這時候需要使用工廠模式。簡單...
    舟漁行舟閱讀 7,796評論 2 17
  • 北方有佳人。絕世而獨立。 一顧傾人城。再顧傾人國。 寧不知傾城與傾國。佳人難再得。
    葉子很忙閱讀 227評論 0 0
  • 作者 沈姜 10 犟頭倔耳朵 小姨對塔斯精說:“奧白相呢,要吃點心了呀。”塔斯精已經跑的滿頭大汗了。 他跑到廂...
    姜蘇閱讀 371評論 0 0
  • 消失了許久,自己。 也許是期盼吧。 我懶得和這個世界打招呼,雖然很熱愛它。 鏡子中的影子,捕捉不到, 但清晰如見。...
    冶玉春閱讀 266評論 0 0