构建利用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行,全部源代码能够在这里下载。这固然不能算做一个框架了,不过能够学习学习,这学期有时间的话,还要继续完善,也欢迎你们一块儿探讨。

一块儿学习,一块儿提升,作技术应当是直接的,有问题欢迎指出~


最后说的第三点:是本身仍是一个学生,作这些内容也仅仅是出于兴趣。

相关文章
相关标签/搜索