从零写最简单代码实现类似vue.js的双向绑定

前言

我们知道vue.js用v-model实现了数据双向绑定,原理大约是:vue使用Object.defineProperty属性,重写data的get和set方法来实现,但如果让你再具体解释一下,可能你就不清楚了,网上有一张图片给了比较详细的解释,但你可能依然看不懂,没关系,今天我们从0写代码,实现一个简单的双向绑定。

双向绑定原理

注意

  1. 下方代码的一些指令模仿了vue,但是并不是说代码要引入vue,因为我们是从0开始写。
  2. 本文假设你了解:面向对象编程、set和get、Object.keys()、Object.defineProperty(),尤其是Object.defineProperty(),可以提前学习一下:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

HTML

  • 先有2个按钮,负责增加和减少数字。
  • 一个输入框可以显示数字也可以修改数字。
  • 一个h3标签只用来显示数字。
<div id="app">
    <input type="button" value="增加" v-click="increment"/>
    <input type="button" value="减少" v-click="subtract">
    <input type="text" v-model="number">
    <h3 v-bind="number"></h3>
</div>

先定义一个Updater构造函数

这个构造函数的唯一作用就是更新DOM某个元素。

  • el:将操作的DOM元素
  • vm:所属的实例。
  • attr:将操作的DOM元素的属性名,比如innerHTMLvalue
  • data:将操作的DOM元素的属性值在data中的映射,在本例中就是number,也就是说该元素的该属性的值就等于data中的number的值。
  • update原型方法的作用就是更新,this.el[this.attr] = this.vm.$data[this.dataKey];就类似于H3.innerHtml = this.data.number,每当number改变时,就应当new一个Updater,保证对应的DOM内容进行更新。
    function Updater(el, vm, attr, dataKey) {
        this.el = el;
        this.vm = vm;
        this.attr = attr;
        this.dataKey = dataKey;
        this.update();
    }

    Updater.prototype.update = function () {
        this.el[this.attr] = this.vm.$data[this.dataKey];
    }

定义Vue构造函数

我们模仿vue定义一个构造函数,为了更好地对照理解,构造函数就叫Vue。当new这个构造函数的时候,就会执行_init原型方法。

    function Vue(options) {
        this._init(options);
    }

定义_init原型方法

  • $options保存传入的参数。
  • $el$data$methods顾名思义,分别保存DOM容器、data、methods。
  • _binding是一个对象,它保存着model与view的映射关系,也就是我们前面定义的Updater的实例。当model改变时,会触发其中的指令类更新,保证view也能实时更新。它不好理解,可以先往下看。
  • _observer是负责监听的实例方法,具体见下文。
  • _complie是负责编译的实例方法,具体见下文。
    Vue.prototype._init = function (options) {
        this.$options = options;
        this.$el = document.querySelector(options.el);
        this.$data = options.data;
        this.$methods = options.methods;
        this._binding = {};
        this._observer(this.$data);
        this._complie(this.$el);
    }

定义_observer原型方法

observer方法是双向绑定的核心,它用来重写data的set和get函数。它做的事包括:

  1. Object.keys()遍历data中所有属性,返回data的键名数组。
  2. 遍历键名数组,给上面提到的_binding压入键值对,键名就是data的键名,键值就是{_updaterList: []}。为什么要这样?因为data的一个属性可能要更新到多个DOM位置,所以我们要把需要更新的位置存下来。
  3. 关键核心来了,用Object.defineProperty给data的添加get和set属性。
    • enumerable: true表示可枚举,也就是可被for-in和Object.keys()枚举;
    • configurable: true表示值为true时,该属性才能够被改变,也能被删除;
    • get就不说了,就是个return;
    • set要说一下,forEach就是要依次执行更新DOM。
    Vue.prototype._observer = function (data) {
        var self = this;
        Object.keys(data).forEach(function (key) {
            var oldValue = data[key];
            self._binding[key] = {
                _updaterList: []
            }
            Object.defineProperty(data, key, {
                enumerable: true,
                configurable: true,
                get: function () {
                    return oldValue;
                },
                set: function (newValue) {
                    if (oldValue === newValue) return;
                    oldValue = newValue;
                    self._binding[key]._updaterList.forEach(function (updater) {
                        updater.update();
                    })
                }
            });
        })
    }

定义_complie原型方法

complie原型方法用来解析指令(v-bind,v-model,v-click),并在解析过程中对view与model进行绑定,也就是push一些Updater实例。

  1. 递归遍历子元素,如果元素上有v-click属性,就监听onclick事件,触发methods里面的increment、subtract方法。这里注意bind,如果没有bind,那么increment中的this指向的是调用increment的对象,也就是input节点,而我们的本意是this指向Vue实例的$data,所以使用bind,修改this指向为_this.$data。
  2. 如果元素上有v-model属性,并且元素为input或者textarea,就监听它的input事件。这里注意几点:
    1. 在本案例中,attrVal是什么?从HTML可以看到,就是字符串'number',而你的data的一个属性也必然是number,这就形成了data属性跟自定义指令的映射。
    2. _updaterList.push(new Updater(...))我们大致可以看出_updaterList是干什么的,它是保存更新器的。
    3. 读到return function () {}返回一个对象,你才恍然发现,原来input绑定的函数是一个自执行函数,自执行函数返回的函数才是input会触发的函数。_this.$data[attrVal] = nodes[key].value这句就是使number的值与input节点的value保持一致,也就是实现了双向绑定。
  3. 如果元素上有v-bind属性,就简单了,只要这个节点的innerHTML及时更新为data中number的值即可。
    Vue.prototype._complie = function (el) {
        var _this = this;
        var nodes = el.children;
        for (var i = 0; i < nodes.length; i++) {
            var node = nodes[i];
            if (node.children.length) {
                this._complie(node);
            }

            if (node.hasAttribute("v-click")) {
                node.onclick = _this.$methods[nodes[i].getAttribute("v-click")].bind(_this.$data)
            }

            if (node.hasAttribute("v-model") && (node.tagName == "INPUT" || node.tagName == "TEXTAREA")) {
                node.addEventListener("input", (function (key) {
                    var attrVal = node.getAttribute("v-model");
                    _this._binding[attrVal]._updaterList.push(new Updater(
                        node,
                        _this,
                        "value",
                        attrVal,
                    ));

                    return function () {
                        _this.$data[attrVal] = nodes[key].value;
                    }
                })(i));
            }

            if (node.hasAttribute("v-bind")) {
                var attrVal = node.getAttribute("v-bind");
                _this._binding[attrVal]._updaterList.push(new Updater(
                    node,
                    _this,
                    "innerHTML",
                    attrVal
                ))
            }
        }
    }

测试

跟vue.js一样,new一个实例看看。可以看到,双向绑定已经实现。

    var vm = new Vue({
        el: "#app",
        data: {
            number: 10,
            age: 18
        },
        methods: {
            increment: function () {
                this.number++;
            },

            subtract: function () {
                this.number--;
            }
        }
    })

总结

第一步,我们要脱离任何复杂概念,写一个更新器,专门用来更新DOM。
第二步,创建构造函数,准备接收options。
第三步,为data的每一项设置get和set,这是ES5就实现的内置方法。其中set方法内除了修改data每一项的值,还要触发更新器,这样才能做到:data也更新,DOM也更新。
第四步,解析指令,将指令翻译为addEventListener,这样才能做到,DOM的输入框有更新,则data也跟着更新。同时给_updaterList压入更新器。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • ## 框架和库的区别?> 框架(framework):一套完整的软件设计架构和**解决方案**。> > 库(lib...
    Rui_bdad阅读 3,158评论 1 4
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 136,695评论 19 139
  • 一桌子的美味,图的都是方便,天天享受着你的手艺,家中的味道要比这午餐纯正,幸福的陪伴,三十一年的风雨相随,无论是在...
    卖菜诗人阅读 268评论 2 6
  • html部分: css部分: js部分:
    林中欢歌要找笑语阅读 773评论 0 0
  • 二十岁 盛开的花季 满身浓郁的香味 灿烂如花的年纪
    小君诺阅读 365评论 1 4

友情链接更多精彩内容