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

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

代碼下載地址:這裏下載html5

綜述

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

實現雙向數據綁定的方法有不少,也能夠參考本專欄以前的其餘實現,我之因此選擇用Proxy和Reflect,一方面是由於能夠大量節約代碼,而且簡化邏輯,可讓我把更多的經歷放在其餘內容的構建上面,另一方面本項目直接基於ES6,用這些內容也符合面向將來的JS編程規範,第三點最後說。git

因爲這個小框架是本身在PolarBear這個咖啡館在一個安靜的午後開始寫成,暫且起名Polar,往後但願我能繼續完善這個小框架,給添加上更多有趣的功能。es6

首先咱們能夠看總體功能演示:
一個gif動圖,若是不能看,請點擊[這裏的連接]github

代碼分析

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

個人思路是這樣的:數組

  • 將全部的數據信息放在一個屬性對象中(this._data),以後給這個屬性對象用Proxy包裝set,在代理函數中咱們更新屬性對象的具體內容,同時通知全部監聽者,以後返回新的代理對象(this.data),咱們以後操做的都是新的代理對象。app

  • 對於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行,全部源代碼能夠在這裏下載。這固然不能算做一個框架了,不過能夠學習學習,這學期有時間的話,還要繼續完善,也歡迎你們一塊兒探討。

一塊兒學習,一塊兒提升,作技術應當是直接的,有問題歡迎指出~


最後說的第三點:是本身仍是一個學生,作這些內容也僅僅是出於興趣。

相關文章
相關標籤/搜索