您好, 欢迎来到 !    登录 | 注册 | | 设为首页 | 收藏本站

js实现数据双向绑定(访问器监听)-起名:板

wiki 2022/7/28 20:25:54 nodejs 字数 18480 阅读 1569

双向绑定基于MVVM模型:model-view-viewModel js实现数据双向绑定(访问器监听)-起名:板

双向绑定基于MVVM模型:model-view-viewModel

model: 模型层,负责业务逻辑以及与数据库的交互
view:视图层,负责将数据模型与UI结合,展示到页面中
viewModel:视图模型层,作为model和view的通信桥梁

双向绑定的含义:当model数据发生变化的时候,会通知到view层,当用户修改了view层的数据的时候,会反映到模型层。

而双向数据绑定的好处在于:只关注于数据操作,DOM操作减少

Vue.js实现的原理就是采用的访问器监听,所以这里也采用访问器监听的方式实现简单的数据双向绑定。

访问器监听的实现,主要采用了javascript中原生方法:Object.defineProperty,该方法可以为某对象添加访问器属性,当访问或者给该对象属性赋值的时候,会触发访问器属性,因此利用此思路,可以在访问器属性中添加处理程序。



/*
 * 作者:lity 
 *日期:2022-07-23
 *版本:1.0.2
 *名称:板(ban)
 *描述:原生js双向绑定,比vue的写法过于让人抛弃底层,只会写组件ui,配置化的东西太多,而ban的双向绑定:遵从原生js规则,使用起来更灵活
 *版本更新:双向绑定1.0.2版本-->更新绑定
 *版权:by lity
 */
function Ban(options) {
    this.$options = options || {};
    this.$el = options.el
    this.$data = options.data
    let computed = options.computed;
    let methods = options.methods;
    let self = this;
    // _binding保存着model与view的映射关系,也就是Wachter的实例,当model更新的时候,更新对应的view
    self._binding = {};
    // 1. 编译模板
    new Compile(this.$el || document.body, this);
    //代理绑定
    self.ProxyDefine(this.$data, [], self);


}
Ban.prototype = {
    //代理递归循坏绑定
    ProxyDefine: (data, objname, self) => {
        //  var self = this;
        var dd = data;
        if (objname == undefined)
            objname = [];
        for (var key in data) {
            if (dd.hasOwnProperty(key)) {
                var t1 = dd[key];
                if (Array.isArray(dd)) {//给数组添加主键
                    var vv = t1[":index"];
                    if (vv) objname.push(":index-" + vv + "");
                    else objname.push(":index-" +key);
                } else {
                    objname.push(key);
                }
                if (typeof t1 == "object") {
                    self.ProxyDefine(t1, objname, self);
                } else {
                    self.Define(dd, key, objname, self);
                    dd[key] = t1;
                }
                objname.pop();

            }

        }
    },

    //声明绑定
    Define: (data, key, objname, self) => {
        let val = data[key];
        let objnamestr = objname.join('.');
        // 建立需要监测属性的关联
        if (!self._binding[objnamestr]) {
            self._binding[objnamestr] = {
                _directives: [] // 存储所有使用该数据的地方
            };
        }
        Object.defineProperty(data, key, {
            configurable: true,
            enumerable: true,
            get: function proxyGetter() {
                return val;
            },
            set: function proxySetter(v) { 
                if (val != v) {
                    val = v;
                    // 如果数据被修改,则需要通知每一个使用该数据的地方进行更新数据, 
                    self.UpdateView(objnamestr, v, self);
                }
            }
        })

    },
    //更新视图:通知观察者 更新view
    UpdateView: (key, val, self) => {
        var binding = self._binding[key];
        binding._directives.forEach(function (item) {
            item.update(val);
        });
    },

}

/**
 * @class 指令解析类 Compile
 * @param {[type]} el [element节点]
 * @param {[type]} vm [mvvm实例]
 */
function Compile(el, vm) {
    this.$vm = vm;
    this.$el = compileUtil.isElementNode(el) ? el : document.querySelector(el);

    if (this.$el) {
        this.$fragment = this.nodeFragment(this.$el);
        this.compileElement(this.$fragment, "");
        // 将文档碎片放回真实dom
        this.$el.appendChild(this.$fragment)
    }
}
Compile.prototype = {
    compileElement: function (el, preexp) {
        let self = this;
        let childNodes = el.childNodes;
        [].slice.call(childNodes).forEach(node => {
            self.parseNode(node, preexp);
        });
    },
    // 文档碎片,遍历过程中会有多次的dom操作,为提高性能我们会将el节点转化为fragment文档碎片进行解析操作
    // 解析操作完成,将其添加回真实dom节点中
    nodeFragment: function (el) {
        let fragment = document.createDocumentFragment();
        let child;

        while (child = el.firstChild) {
            fragment.appendChild(child);
        }
        return fragment;
    },
    //解析节点
    parseNode: function (node, preexp) {
        let text = node.textContent;

        let self = this;
        // 如果是element节点
        if (compileUtil.isElementNode(node)) {
            self.compile(node, preexp);
        }
        // 如果是text节点
        else if (compileUtil.isTextNode(node) && compileUtil.$reg().test(text)) {
            var exparr = [];
            text.match(compileUtil.$reg()).forEach((item, i) => {
                var exp = item.replace("{{", "").replace("}}", "");//info.msg
                if (preexp) {
                    exp = preexp + "." + exp.split(".")[1];//lst.0.n
                    node.textContent = node.textContent.replace(item, "{{" + exp+"}}");
                }
               
                exparr.push(exp);
            });
            compileUtil.BindText(node, this.$vm, exparr, preexp);
        }
        // 解析子节点包含的指令
        if (node.childNodes && node.childNodes.length) {
            self.compileElement(node, preexp);
        }
    },
    // 指令解析
    compile: function (node, preexp) {
        let nodeAttrs = node.attributes;
        let self = this;
        //node.parseNode().appendChild(node);
        [].slice.call(nodeAttrs).forEach(attr => {
            let attrName = attr.name;
            let exp = attr.value;
            //绑定事件集
            if (compileUtil.isEventDirective(attrName)) {
                compileUtil.eventHandler(node, self.$vm, exp, attrName);
                node.removeAttribute(attrName);
            } else {
                var dir = compileUtil.getDir(attrName);
                if (dir) {
                    if (dir == "for") {//for循环
                        self.compileFor(node, attrName, exp, preexp);
                    } else {
                        compileUtil.BindByAttrName(node, self.$vm, exp, dir, "");
                    }
                    node.removeAttribute(attrName);
                }
            }
        });
    },
    //编译for 列表数据
    compileFor: function (node, attrName, exp, preexp) {
        let self = this;//(it,a) in lst
        var lstvar = exp.split('in')[1].trim();//lst
        var itemvar = exp.split('in')[0].trim().replace("(", "").replace(")", "").split(",");//item
        self.$vm.$data[lstvar].forEach((sub, i) => {
            var vv = sub[itemvar[1]];
            sub[":index"] = vv;
            preexp = lstvar + ".:index-" + vv ;
            if (i > 0) {
                var p1 = node.cloneNode(true);
                p1.removeAttribute(attrName);
                p1.setAttribute("b-key", preexp);
                self.compileElement(p1, preexp);
                node.parentNode.appendChild(p1);
            }
        });
        self.$vm.$data[lstvar].forEach((sub, i) => {
            var vv = sub[itemvar[1]];
            sub[":index"] = vv;
            preexp = lstvar + ".:index-" + vv ;
            if (i == 0) {
                // node.setAttribute("b-key", preexp);
                let childNodes = node.childNodes;
                childNodes.forEach(node => {
                    self.compileElement(node, preexp);
                });
            }
        });
   
    }
}
// 定义$elm,缓存当前执行input事件的input dom对象
let $elm;
let timer = null;
// 指令处理集合
const compileUtil = {
    $reg: () => { return /\{\{((?:.|\n)+?)\}\}/g; },
    //通过属性名获取对应的指令
    getDir: function (attrName) {
        var dir = "";
        switch (attrName) {
            case "b-m": { dir = "model"; break; }
            case "b-h": { dir = "html"; break; }
            case "b-t": { dir = "text"; break; }
            case "b-c": { dir = "class"; break; }
            case "b-e": { dir = "event"; break; }
            case "b-for": { dir = "for"; break; }
            default: dir = "";
        }
        return dir;
    },
    isElementNode: function (node) {
        return node.nodeType === 1;
    },
    // text纯文本
    isTextNode: function (node) {
        return node.nodeType === 3
    },
    // x-XXX指令判定
    isDirective: function (attr) {
        return attr.indexOf('b') >= 0;
    },
    // 事件指令判定
    isEventDirective: function (dir) {
        return dir.indexOf('b-e') >= 0;
    },
    //绑定的文本节点
    BindText: function (node, vm, exp, preexp) {
        let initcontent = node.textContent;
        exp.forEach((sub, i) => {//exp:info.msg
            var key = compileUtil.guid();
            // console.log("BindText", sub);
            // node.setAttribute("b-key", key);
            let watcher = this.AddWatch(node, vm, key, sub, "text", initcontent);
            var val = this._getVmVal(vm.$data, sub);
            watcher && watcher.update(val);
        });
    },
    //通过属性名称更新节点并添加观察者
    BindByAttrName: function (node, vm, exp, dir) {
        let initcontent = node.textContent;
        var val = this._getVmVal(vm.$data, exp);
        var key = compileUtil.guid(); 
        //通过指令渲染节点值
        compileUtil.ReaderNodeByDir(node, dir, val, initcontent);
        // 给当前element对象添加model到view层的映射
        let watcher = this.AddWatch(node, vm, key, exp, dir, initcontent);
        // watcher && watcher.update(val);
        if (dir == "model") {
            let self = this;
            // 监听input事件
            node.addEventListener('input', function (e) {
                let newVal = e.target.value;
                $elm = e.target;
                if (val === newVal) {
                    return;
                }
                // 设置定时器  完成ui js的异步渲染
                clearTimeout(timer);
                timer = setTimeout(function () {
                    self._setVmVal(vm.$data, exp, newVal);
                    vm.UpdateView(exp, newVal, vm);
                    val = newVal;
                })
            });
        }
    },

    /**
     * node: 指令对应的DOM元素
     * eb: 指令对应的实例
     * key:对象唯一key 同时会给node节点增加唯一属性
     * exp:对象属性名称:info.msg   lst.0.age 等
     * initnode:初始节点
     * dir:html,text,class等
     **/
    AddWatch: function (node, vm, key, exp, dir, initcontent) {
        // 给当前element对象添加model到view层的映射 
        if (node) {
            if (!vm._binding[exp]) {
                vm._binding[exp] = {
                    _directives: [] // 存储所有使用该数据的地方
                };
            }
            var watcher = new Watcher({
                node: node,//节点元素
                eb: vm,//所挂载的实例 
                key: key,  //value ,txt//暂时未用 
                exp: exp,
                dir: dir,
                initcontent: initcontent
            });
            vm._binding[exp]._directives.push(watcher);
            return watcher; 
        }
        return;
    },

    // 事件处理
    eventHandler: function (node, vm, attrVal, attrName) {
        let eventType = attrName.split(':')[1];
        if (!eventType) eventType = "click";
        if (eventType) {
            let fn = vm.$options.methods && vm.$options.methods[attrVal];
            if (fn) {
                node.addEventListener(eventType, fn.bind(vm), false);
            } else {
                node.addEventListener(eventType, function () {
                    eval(attrVal);
                }, false);
            }
        }
    },
    /**
     * [获取挂载在vm实例上的value]
     * @param  {[type]} vm  [mvvm实例]
     * @param  {[type]} exp [expression]
     */
    _getVmVal: function (vm, exp) {
        var dd = vm;
        var val; var keys = exp.split('.');
        keys.forEach(function (key) {
            if (/:index-((?:.|\n)+?)/g.test(key)) {//lst.[:index-val].a
                var st = key.replace("[", "").replace("]", "").split("-");
                dd.forEach((item, i) => {
                    if (item[":index"] == st[1]) {
                        dd = item;
                    }
                });
            } else {
                if (dd.hasOwnProperty(key)) {
                    var t1 = dd[key];
                    if (typeof t1 != "object") {
                        val = t1;
                    } else {
                        dd = t1;
                    }

                }
            }
        });
        if (val == undefined) val = "";
        return val;
    },
    /**
     * [设置挂载在vm实例上的value值]
     * @param  {[type]} vm    [mvvm实例]
     * @param  {[type]} exp   [expression]
     * @param  {[type]} value [新值]
     */
    _setVmVal: function (vm, exp, value) {
        var dd = vm;
        var keys = exp.split('.');
        keys.forEach(function (key) {
            if (dd.hasOwnProperty(key)) {
                var t1 = dd[key];
                if (typeof t1 != "object") {
                    dd[key] = value;
                } else {
                    dd = t1;
                }
            }
        });
    },
    // 指令渲染集合
    ReaderNodeByDir: function (option, value) {
        //node, dir, value
        var node = option.node;
        switch (option.dir) {
            case "html": { node.innerHTML = typeof value === 'undefined' ? '' : value; break; }
            case "class": { break; }
            case "model": {
                if (node.value != value) {
                    node.value = typeof value === 'undefined' ? '' : value;
                }
                break;
            }
        }

    },
    //渲染文本节点的值
    ReaderTextNode: function (option) {
        var node = option.node;
        var data = option.eb.$data;
        let self = this;
        var text = option.initcontent;//{{info.title}},{info.msg},{{lst.[n-9].n}},{{lst.[a-8].a}}
        let arr = text.match(compileUtil.$reg());
        if (arr) {
            arr.forEach((item, i) => {
                var exp = item.replace("{{", "").replace("}}", "");
                var val = self._getVmVal(data, exp);
                var reg = new RegExp(item, 'g');//g 全局匹配 
                text = text.replace(reg, val);
            });
            node.textContent = text;
        }
    },
    def: function (obj, key, val, enumerable) {
        Object.defineProperty(obj, key, {
            value: val,
            enumerable: !!enumerable,
            configurable: true,
            writable: true
        })
    },
    guid: function () {
        return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
            var r = Math.random() * 16 | 0,
                v = c == 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    }
}

 

/**
 * options 属性:
 * node: 指令对应的DOM元素
 * eb: 指令对应的实例 
 * key:对象唯一key 同时会给node节点增加唯一属性
 * group:属性组
 * type:1对象{} 2数组[]
 * initnode:初始节点
 * dir:html,text,class等
 */
function Watcher(options) {
    this.$options = options;
    this.update();
}

Watcher.prototype = {
    update: function (val) {//更新视图 
        if (val) {
            // 获取到被绑定的对象 
            var dir = this.$options.dir;
            if (dir == "text") {
                compileUtil.ReaderTextNode(this.$options, val);
            } else {
                compileUtil.ReaderNodeByDir(this.$options, val);
            }

        }
    }

}



/**
 * observeArr的部分
 **/
// 生成新的原型
let newPrototype = Object.create(Array.prototype);

let methods = ["push", "pop", "shift", "unshift", "reverse", "sort", "splice"];
// 在新原型上面添加以上方法,实现劫持
methods.forEach(method => {
    newPrototype[method] = function (...args) {
        console.log(`使用了${method}`);
        let inserted;
        switch (method) {
            case "push":
            case "unshift":
                inserted = args;
                break;
            case "splice":
                inserted = args.slice(2);
                break;
            default:
                break;
        }
        inserted && observeArr(inserted);
        return Array.prototype[method].call(this, ...args);
    };
});

function observeArr(arr) {
    // 新加!!!是对象的话,需要用对象
    if (Object.prototype.toString.call(arr) === "[object Object]") {
        observeObj(arr);
        return;
    }

    if (Array.isArray(arr)) {
        // 整个数组指向新的原型
        arr.__proto__ = newPrototype;
        // 数组的每项,如果是数组,也指向新的原型。
        arr.forEach(observeArr);
    }

    // 不是对象或者数组的,什么都不做
}

/**
 * observeObj的部分
 **/
function observeObj(obj) {
    // 加上参数限制,必须是对象才有劫持,也是递归的终止条件
    if (typeof obj !== "object" || obj == null) {
        return;
    }
    // 新加!!!数组交给数组处理
    if (Array.isArray(obj)) {
        observeArr(obj);
        return;
    }
    // 是对象的话 才开始递归
    for (let key in obj) {
        // 直接使用 obj.hasOwnProperty会提示不规范
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            observeKey(obj, key);
            // 这里劫持该属性的属性值,如果不是对象直接返回,不影响
            observeObj(obj[key]);
        }
    }
    return obj;
}
function observeKey(obj, key) {
    let value = obj[key];
    Object.defineProperty(obj, key, {
        get() {
            console.log("读取属性", value);
            return value;
        },
        set(newValue) {
            console.log("设置属性", newValue);
            value = newValue;
        }
    });
}

/**
 * demo试试
 **/
let data = { a: 1, b: [1, 2, { c: 2 }] };
observeObj(data);
data.a = 2;
data.b.push([2, 3]);

let arr = [{ a: "数组里的对象" }, 3, 4];
observeArr(arr);
arr[0].a = 3;


如果您也喜欢它,动动您的小指点个赞吧

除非注明,文章均由 laddyq.com 整理发布,欢迎转载。

转载请注明:
链接:http://laddyq.com
来源:laddyq.com
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


联系我
置顶