关于javascript:使用访问器属性编写一个双向数据绑定

3次阅读

共计 4200 个字符,预计需要花费 11 分钟才能阅读完成。

一、原理

  1. 双向数据绑定:数据和视图同步,数据发生变化,视图跟着变动,视图变动,数据也随之产生扭转;
  2. 实现办法:通过 Object.defineProperty()办法来实现;

Object.defineProperty()办法 (JavaScript 高级程序设计第三版 P139):
Object.defineProperty() 这个办法承受三个参数 属性所在的对象,属性的名字以及一个描述符对象.

let book = {
    _year: 2004,
    edition: 1
}
Object.defineProperty(book, 'year', {get: function() {console.log('调用了 get 办法');
        return this._year;
    },
    set: function(newvalue) {if (this._year !== newvalue) {console.log('调用了 set 办法');
            this._year = newvalue;
            this.edition += 1;
        }
    }
});
book.year; // 调用了 get 办法
book.year = 2021; // 调用了 set 办法

二、双向数据绑定 1.0

function Bind(options) {
    this.el = options.el;
    this.data = options.data;
    getNode(this.el, this.data);
}

function getNode(el, data) {let inputs = document.querySelector(el);
    let children = inputs.children;
    for (let i = 0; i < children.length; i++) {compile(data, children[i]);
    }
}

function compile(obj, data) {let key = node.getAttribute('v-model');
    define(data, key, data[key]);
}

function define(obj, key, val) {
    Object.defineProperty(obj, key, {get: function() {return val;},
        set: function(newvalue) {if (val !== newvalue) {
                val = newvalue;
                document.getElementsByClassName('insert')[0].innerHTML = newvalue;
            }
        }
    })
}

let inputs = document.getElementsByTagName('input');

let vm = new Bind({
    el: '#app',
    data: {
        name: '',
        age: ''
    }
});

function input1() {vm.data.name = inputs[0].value;
}

function input2() {vm.data.age = inputs[1].value;
}

这段代码尽管实现了实现了双向数据绑定,但也仅仅只是实现了这个性能,它还存在许多 bug
比方:

  1. 代码整体看上去很凌乱,并且复用性不强;
  2. 应用的是函数的办法来定义 Bind,所以编写代码的时候就须要时时思考 this 的指向问题;
  3. 在 Bind()函数外面,如果 options 外面没有 el 这个属性,那么 this.el 就为 null,前面在获取标签的时候也获取不了,前面也会跟着出错;compile()函数外面也没有判断 key 是否存在等等;
  4. 在 input 标签外面增加 oninput 事件来获取输出的数据;

……

三、双向数据绑定 2.0

改良:

  1. 应用 class 来定义 Bind;
  2. 应用 || 来避免参数 options 外面不存在 el,data 这些数据;
  3. 通过应用 addEventListener 来增加事件,并且思考了兼容问题;
  4. 为 Bind 增加了 errorList 数组来记录绑定时和绑定之后的谬误;
  5. 通过创立一个 BindError 对象,并采纳轮询的办法,来对 Bind 进行监听;


index.js

class Bind {constructor(options) {
        this.el = options.el || "";
        this.data = options.data || {};
        this.errorList = [];
        this.getNode();}

    getNode() {this.rootNode = document.querySelector(this.el);
        const children = this.rootNode ? Array.prototype.slice.call(this.rootNode.children) : [];
        children.forEach((child) => {this.complie(child);
        })
    }

    complie(node) {const key = node.getAttribute('v-model');
        if (key) {this.addEvent(node, 'input', (e) => {const { value = 'error'} = e.target;
                this.data[key] = value;
            });
            this.define(this.data, key, this.data[key]);
        } else {var div = document.createElement('div');
            div.appendChild(node)
            this.errorList.push(`${div.innerHTML} doesn't have attribute v-model! `);
        }
    }

    addEvent(node, type, handler) {if (node.addEventListener) {node.addEventListener(type, handler)
        } else if (node.attachEvent) {node.attachEvent('on' + type, handler)
        } else {node[type] = null;
        }
    }

    define(obj, key, val) {
        Object.defineProperty(obj, key, {get: function () {return val;},
            set: function (newvalue) {if (newvalue !== val) {
                    val = newvalue;
                    document.getElementsByClassName('insert')[0].innerHTML = val;
                }
            }
        });
    }
}
var error = new BindError();
var vm = new Bind({
    el: '#app',
    data: {
        name: '',
        age: ''
    }
})
error.addListenerList(vm);

error.js

class BindError {constructor() {this.listenerList = [];
    }

    addListenerList(obj) {this.listenerList.push(obj);
    }

    interval = setInterval(()=>{this.listenerList.forEach((obj)=>{var { errorList} = obj;
            var error = errorList.pop();
            if(error){throw new Error(error);
            }
        })
    },1000);
}

有余:应用轮询,容易造成资源节约

双向数据绑定 3.0

改良:不实用轮询,应用 es6 的 Proxy 来对 Bind 外面的 errorList 进行一个拦挡;

class Bind {constructor(options) {
        this.el = options.el || "";
        this.data = options.data || {};
        this.errorList = new Proxy([], {set(target, prop, value) {Reflect.set(target, prop, value);
                throw new Error(value);
            }
        });
        this.getNode();}

    getNode() {this.rootNode = document.querySelector(this.el);
        const children = this.rootNode ? Array.prototype.slice.call(this.rootNode.children) : [];
        children.forEach((child) => {this.complie(child);
        })
    }

    complie(node) {const key = node.getAttribute('v-model');
        if (key) {this.addEvent(node, 'input', (e) => {const { value = 'error'} = e.target;
                this.data[key] = value;
            });
            this.define(this.data, key, this.data[key]);
        } else {var div = document.createElement('div');
            div.appendChild(node)
            this.errorList.push(`${div.innerHTML} doesn't have attribute v-model! `);
        }
    }

    addEvent(node, type, handler) {if (node.addEventListener) {node.addEventListener(type, handler)
        } else if (node.attachEvent) {node.attachEvent('on' + type, handler)
        } else {node[type] = null;
        }
    }

    define(obj, key, val) {
        Object.defineProperty(obj, key, {get: function () {return val;},
            set: function (newvalue) {if (newvalue !== val) {
                    val = newvalue;
                    document.getElementsByClassName('insert')[0].innerHTML = val;
                }
            }
        });
    }
}

var vm = new Bind({
    el: '#app',
    data: {
        name: '',
        age: ''
    }
})
正文完
 0