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

前言

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

从零写最简单代码实现类似vue.js的双向绑定_第1张图片
双向绑定原理

注意

  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标签只用来显示数字。

先定义一个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压入更新器。

你可能感兴趣的:(从零写最简单代码实现类似vue.js的双向绑定)