从零写最简单代码实现类似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压入更新器。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

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