構(gòu)建利用Proxy和Reflect實(shí)現(xiàn)雙向數(shù)據(jù)綁定的微框架(基于ES6)

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

代碼下載地址:這里下載

綜述

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

實(shí)現(xiàn)雙向數(shù)據(jù)綁定的方法有很多,也可以參考本專欄之前的其他實(shí)現(xiàn),我之所以選擇用Proxy和Reflect,一方面是因?yàn)榭梢源罅抗?jié)約代碼,并且簡化邏輯,可以讓我把更多的經(jīng)歷放在其他內(nèi)容的構(gòu)建上面,另外一方面本項(xiàng)目直接基于ES6,用這些內(nèi)容也符合面向未來的JS編程規(guī)范,第三點(diǎn)最后說。

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

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

代碼分析

我們要做這樣一個小框架,核心是要監(jiān)聽數(shù)據(jù)的改變,并且在數(shù)據(jù)的改變的時候進(jìn)行一些操作,從而維持?jǐn)?shù)據(jù)的一致。

我的思路是這樣的:

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

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

<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神似吧,所以這種調(diào)用方式應(yīng)當(dāng)為我們所熟悉。

我們需要建立一個Polar類,這個類的構(gòu)造函數(shù)應(yīng)該進(jìn)行一些初始化操作:

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

        this._compile(this.root);
}

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

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

我們還是先不能看我們代理的set該怎么寫,因?yàn)檫@個時候我們還要先繼續(xù)梳理思路:

假設(shè)我們這樣<div>name:{{name}}</div>將數(shù)據(jù)綁定到dom節(jié)點(diǎn),這個時候我們需要做什么呢,或者說,我們通過什么方式讓dom節(jié)點(diǎn)和數(shù)據(jù)對應(yīng)起來,隨著數(shù)據(jù)改變而改變。

看上文的__bindings。這個對象用來存儲所有綁定的dom節(jié)點(diǎn)信息,__bindings本身是一個對象,每一個有對應(yīng)dom節(jié)點(diǎn)綁定的數(shù)據(jù)名稱都是它的屬性,對應(yīng)一個數(shù)組,數(shù)組中的每一個內(nèi)容都是一個綁定信息,這樣,我們在自己寫的set代理函數(shù)中,我們一個個調(diào)用過去,就可以更新內(nèi)容了:

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

我這里創(chuàng)建了一個用于構(gòu)造調(diào)用的函數(shù),這個函數(shù)用于創(chuàng)建存儲綁定信息的對象:

function Directive(el,polar,attr,elementValue){
    this.el=el;//元素本身dom節(jié)點(diǎn)
    this.polar = polar;//對應(yīng)的polar實(shí)例
    this.attr = attr;//元素的被綁定的屬性值,比如如果是文本節(jié)點(diǎn)就可以是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}}實(shí)際上只是節(jié)點(diǎn)的一部分,這并不是節(jié)點(diǎn)啊,另外我們是不是還可以這么寫:<div>name:{{name}} age:{{age}}</div>?

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

所以我們在_compile函數(shù)首先:

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字節(jié)點(diǎn),則遞歸
            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]));
                    }
                //對數(shù)據(jù)和dom進(jìn)行綁定
                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]]))
                }
            }

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

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

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
                    }
                })());
}

請注意,上面調(diào)用了一個IIFE,實(shí)際綁定的函數(shù)只有返回的函數(shù)那一小部分。

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

實(shí)際上這比處理p-model還簡單,但是我們?yōu)榱酥С趾瘮?shù)參數(shù)的情況,處理了一下傳入?yún)?shù),另外我實(shí)際上將event始終作為一個參數(shù)傳遞,這也許并不是好的實(shí)踐,因?yàn)槭褂玫臅r候還要多注意。

if(node.hasAttribute("p-click")) {
                node.addEventListener("click",function(){
                    var attributeValue=node.getAttribute("p-click");
                    var args=/\(.*\)/.exec(attributeValue);
                    //允許參數(shù)
                    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]);
                    }
                }());
}

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

一起學(xué)習(xí),一起提高,做技術(shù)應(yīng)當(dāng)是直接的,有問題歡迎指出~


最后說的第三點(diǎn):是自己還是一個學(xué)生,做這些內(nèi)容也僅僅是出于興趣。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,534評論 19 139
  • 工廠模式類似于現(xiàn)實(shí)生活中的工廠可以產(chǎn)生大量相似的商品,去做同樣的事情,實(shí)現(xiàn)同樣的效果;這時候需要使用工廠模式。簡單...
    舟漁行舟閱讀 8,116評論 2 17
  • 北方有佳人。絕世而獨(dú)立。 一顧傾人城。再顧傾人國。 寧不知傾城與傾國。佳人難再得。
    葉子很忙閱讀 249評論 0 0
  • 作者 沈姜 10 犟頭倔耳朵 小姨對塔斯精說:“奧白相呢,要吃點(diǎn)心了呀?!彼咕呀?jīng)跑的滿頭大汗了。 他跑到廂...
    姜蘇閱讀 412評論 0 0
  • 消失了許久,自己。 也許是期盼吧。 我懶得和這個世界打招呼,雖然很熱愛它。 鏡子中的影子,捕捉不到, 但清晰如見。...
    冶玉春閱讀 306評論 0 0

友情鏈接更多精彩內(nèi)容