美文网首页
一个简单的vue双向绑定实现案例

一个简单的vue双向绑定实现案例

作者: Mr丶Sunny | 来源:发表于2018-09-05 15:27 被阅读0次

最近面试经常会问这个问题,你知道vue的双向绑定是通过什么实现的吗?了解的同学应该知道vue是使用Object.defineProperty属性,重写data的get和set方法来实现的。先引用网上的一张图,那么接下来我们就按照这张图的步骤去用代码实现这个功能。


image

先看DOM结构部分,结构部分很简单

<div id="app">
    <input type="text" v-model="number">
    <input type="button" value="增加" v-click="increment"/>
    <input type="button" value="减少" v-click="subtract">
    <h3 v-bind="number"></h3>
</div>

然后就是js部分了,按照上图的步骤我们定义一个构造函数,并且init这个构造函数

    //初始化构造函数
    function Vm(options) {
        this._init(options);
    }

    Vm.prototype._init = function (options) {
        this.$options = options;    //options为上面使用时传入的结构体,包括el、data、methods
        this.$el = document.querySelector(options.el);     //el是#app,this.$el是id为app的Element元素
        this.$data = options.data;      //this.$data = {number: 0 ...}
        this.$methods = options.methods;        //this.$methods = {increment: function(){} ...}

        this._binding = {};     //_binding保存着model与view的映射关系,也就是我们前面定义的Watcher的实例。当model改变时,会触发其中的指令类更新,保证view也能实时更新
    }

这个时候我们还需要给实例添加一个observer、compile方法,observer方法实现对数据的劫持,compile方法负责编译解析指令并且初始化视图

    //实现_observer函数,对data进行处理,重写data的set和get函数
    Vm.prototype._observer = function (data) {
        //如果data数据为空
        if (!data) {
            return;
        }
        if (typeof data !== "object") {
            throw new Error("data必须是一个对象");
        }

        var self = this;
        /*
        * 遍历data中所有属性
        * Object.keys(obj)函数返回一个由一个给定对象的自身可枚举属性组成的数组
        * */
        Object.keys(data).forEach(function (key) {
            //当前属性的值
            var oldValue = data[key];

            /*
            *   按照前面的数据
            *   _binding = {
            *       number:{
            *           _directives: [...watch实例]
            *       }
            *   }
            *   这里是先声明一个空数组,以后所有和data中某值有关系的都会追加到相应的_directives中。
            *   为何要在这里声明?
            *   因为这里要根据_binding的key必须为data中的属性(键)
            *   那这个数组何时才有东西呢?
            *   解析指令(比如v-bind)的时候push进去的,因为解析指令的时候,会解析每一个dom,然后解析出你的是点击事件还是修改文本内容。
            *   然后在生成watcher追加进去这个数组-以后所有和data中某值发生改变只要循环执行这个数组中watcher的update更新方法就可以更新所有和这个data中的某值相关联的dom
            * */
            self._binding[key] = {
                _directives: []
            }
            //获取本data某属性对应的_directives
            var binding = self._binding[key];

            /**
             * 语法:Object.defineProperty(obj, key, descriptor)
             *      @param: obj:需要定义属性的对象;
             *              key:需要定义或修改的属性;
             *              descriptor:将被定义或修改属性的描述符
             */
            Object.defineProperty(data, key, {    //实现双向绑定的关键代码
                enumerable: true, // 可枚举--可被for-in和Object.keys()枚举。
                configurable: true, //当且仅当值为true时,该属性描述符才能够被改变,也能被删除
                //value: undefined,   //该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined
                //writable: false,    //当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false
                get: function () {  //一个给属性提供getter的方法,当访问该属性时方法会被执行,执行时没有参数传入,但会传入this对象
                    //发现这个oldValue如果替换成data[key]会造成堆栈溢出。
                    return oldValue;
                },
                set: function (newValue) {  //一个给属性提供setter的方法,当属性值修改时触发该方法,该方法将接受唯一参数,即该属性新的参数值。
                    if (oldValue == newValue) return;
                    console.log("监听到值变化了");
                    //发现这个oldValue如果替换成data[key]会造成堆栈溢出。
                    oldValue = newValue;
                    // 当data中某属性改变时,触发_binding['某值']._directives 中的绑定的Watcher类的更新--这样实现一个data中的值发生改变,和它相关联的dom更新。
                    binding._directives.forEach(function (item) {
                        item.update();
                    })
                }
            });
        })
    }

    //定义complie函数,用来解析指令(v-bind,v-model,v-click)等,并在这个过程中对view与model进行绑定。
    Vm.prototype._complie = function (root) {    //root为id为app的ELement元素,也就是vue的根元素
        var _this = this;
        var nodes = root.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")) { //如果有v-click属性,我们监听onclick事件,触发increment、subtract方法
                node.onclick = (function () {
                    var attrVal = nodes[i].getAttribute("v-click");
                    return _this.$methods[attrVal].bind(_this.$data); //bind是使data的作用域与method函数的作用域保持一致
                })();
            }

            if (node.hasAttribute("v-model") && (node.tagName == "INPUT" || node.tagName == "TEXTAREA")) {//如果有v-model属性,并且元素为input或者textarea,我们监听它的input事件
                node.addEventListener("input", (function (key) {
                    var attrVal = node.getAttribute("v-model");
                    /**
                     * _this._binding["number"]._directives = [一个Watcher实例]
                     * 其中Watcher.prototype.update = function() {
                     *     node["value"] = _this.$data["number"]; 这就将node的值保持与number一致
                     * }
                     */
                    _this._binding[attrVal]._directives.push(new Watcher(
                        "input",
                        node,
                        _this,
                        attrVal,
                        "value",
                    ))

                    return function () {
                        _this.$data[attrVal] = nodes[key].value; //使number的值与node的value保持一致,实现双向绑定
                    }
                })(i));
            }

            if (node.hasAttribute("v-bind")) {  //如果有v-bind属性,只要使node的值及时更新为data中number的值即可
                var attrVal = node.getAttribute("v-bind");
                _this._binding[attrVal]._directives.push(new Watcher(
                    'text',
                    node,
                    _this,
                    attrVal,
                    "innerHTML"
                ))
            }
        }
    }

方法定义完了当然得调用,回到init方法

Vm.prototype._init = function (options) {
        this.$options = options;    //options为上面使用时传入的结构体,包括el、data、methods
        this.$el = document.querySelector(options.el);     //el是#app,this.$el是id为app的Element元素
        this.$data = options.data;      //this.$data = {number: 0}
        this.$methods = options.methods;        //this.$methods = {increment: function(){}}

        this._binding = {};     //_binding保存着model与view的映射关系,也就是我们前面定义的Watcher的实例。当model改变时,会触发其中的指令类更新,保证view也能实时更新
        this._observer(this.$data);
        this._complie(this.$el);
    }

接下来实现一个Watcher,用来绑定update方法,实现对DOM的更新

    //实现一个指令类Watcher,用来绑定更新函数,实现对DOM元素的更新
    function Watcher(name, el, vm, exp, attr) {
        this.name = name;       //指令名称,例如文本节点,该值设置为"text"
        this.el = el;           //指令对应的DOM元素
        this.vm = vm;           //指令所属的实例
        this.exp = exp;         //指令对应的值,本例为:"number"
        this.attr = attr;       //指令绑定的属性值,本例为:"innerHTML"

        this.update();
    }

    Watcher.prototype.update = function () {
        this.el[this.attr] = this.vm.$data[this.exp]; //比如H3.innerHtml = this.data.number;当number改变时会触发update函数,保证对应的DOM内容进行更新
    }

调用也很简单,也是vue最熟悉的调用方式

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

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

完整代码如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">
    <input type="text" v-model="number">
    <input type="button" value="增加" v-click="increment"/>
    <input type="button" value="减少" v-click="subtract">
    <h3 v-bind="number"></h3>
</div>
</body>
</html>
<script>
    //初始化构造函数
    function Vm(options) {
        this._init(options);
    }

    Vm.prototype._init = function (options) {
        this.$options = options;    //options为上面使用时传入的结构体,包括el、data、methods
        this.$el = document.querySelector(options.el);     //el是#app,this.$el是id为app的Element元素
        this.$data = options.data;      //this.$data = {number: 0}
        this.$methods = options.methods;        //this.$methods = {increment: function(){}}

        this._binding = {};     //_binding保存着model与view的映射关系,也就是我们前面定义的Watcher的实例。当model改变时,会触发其中的指令类更新,保证view也能实时更新
        this._observer(this.$data);
        this._complie(this.$el);
    }

    //实现_observer函数,对data进行处理,重写data的set和get函数
    Vm.prototype._observer = function (data) {
        //如果data数据为空
        if (!data) {
            return;
        }
        if (typeof data !== "object") {
            throw new Error("data必须是一个对象");
        }

        var self = this;
        /*
        * 遍历data中所有属性
        * Object.keys(obj)函数返回一个由一个给定对象的自身可枚举属性组成的数组
        * */
        Object.keys(data).forEach(function (key) {
            //当前属性的值
            var oldValue = data[key];

            /*
            *   按照前面的数据
            *   _binding = {
            *       number:{
            *           _directives: [...watch实例]
            *       }
            *   }
            *   这里是先声明一个空数组,以后所有和data中某值有关系的都会追加到相应的_directives中。
            *   为何要在这里声明?
            *   因为这里要根据_binding的key必须为data中的属性(键)
            *   那这个数组何时才有东西呢?
            *   解析指令(比如v-bind)的时候push进去的,因为解析指令的时候,会解析每一个dom,然后解析出你的是点击事件还是修改文本内容。
            *   然后在生成watcher追加进去这个数组-以后所有和data中某值发生改变只要循环执行这个数组中watcher的update更新方法就可以更新所有和这个data中的某值相关联的dom
            * */
            self._binding[key] = {
                _directives: []
            }
            //获取本data某属性对应的_directives
            var binding = self._binding[key];

            /**
             * 语法:Object.defineProperty(obj, key, descriptor)
             *      @param: obj:需要定义属性的对象;
             *              key:需要定义或修改的属性;
             *              descriptor:将被定义或修改属性的描述符
             */
            Object.defineProperty(data, key, {    //实现双向绑定的关键代码
                enumerable: true, // 可枚举--可被for-in和Object.keys()枚举。
                configurable: true, //当且仅当值为true时,该属性描述符才能够被改变,也能被删除
                //value: undefined,   //该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined
                //writable: false,    //当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false
                get: function () {  //一个给属性提供getter的方法,当访问该属性时方法会被执行,执行时没有参数传入,但会传入this对象
                    //发现这个oldValue如果替换成data[key]会造成堆栈溢出。
                    return oldValue;
                },
                set: function (newValue) {  //一个给属性提供setter的方法,当属性值修改时触发该方法,该方法将接受唯一参数,即该属性新的参数值。
                    if (oldValue == newValue) return;
                    console.log("监听到值变化了");
                    //发现这个oldValue如果替换成data[key]会造成堆栈溢出。
                    oldValue = newValue;
                    // 当data中某属性改变时,触发_binding['某值']._directives 中的绑定的Watcher类的更新--这样实现一个data中的值发生改变,和它相关联的dom更新。
                    binding._directives.forEach(function (item) {
                        item.update();
                    })
                }
            });
        })
    }

    //定义complie函数,用来解析指令(v-bind,v-model,v-click)等,并在这个过程中对view与model进行绑定。
    Vm.prototype._complie = function (root) {    //root为id为app的ELement元素,也就是vue的根元素
        var _this = this;
        var nodes = root.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")) { //如果有v-click属性,我们监听onclick事件,触发increment、subtract方法
                node.onclick = (function () {
                    var attrVal = nodes[i].getAttribute("v-click");
                    return _this.$methods[attrVal].bind(_this.$data); //bind是使data的作用域与method函数的作用域保持一致
                })();
            }

            if (node.hasAttribute("v-model") && (node.tagName == "INPUT" || node.tagName == "TEXTAREA")) {//如果有v-model属性,并且元素为input或者textarea,我们监听它的input事件
                node.addEventListener("input", (function (key) {
                    var attrVal = node.getAttribute("v-model");
                    /**
                     * _this._binding["number"]._directives = [一个Watcher实例]
                     * 其中Watcher.prototype.update = function() {
                     *     node["value"] = _this.$data["number"]; 这就将node的值保持与number一致
                     * }
                     */
                    _this._binding[attrVal]._directives.push(new Watcher(
                        "input",
                        node,
                        _this,
                        attrVal,
                        "value",
                    ))

                    return function () {
                        _this.$data[attrVal] = nodes[key].value; //使number的值与node的value保持一致,实现双向绑定
                    }
                })(i));
            }

            if (node.hasAttribute("v-bind")) {  //如果有v-bind属性,只要使node的值及时更新为data中number的值即可
                var attrVal = node.getAttribute("v-bind");
                _this._binding[attrVal]._directives.push(new Watcher(
                    'text',
                    node,
                    _this,
                    attrVal,
                    "innerHTML"
                ))
            }
        }
    }

    //实现一个指令类Watcher,用来绑定更新函数,实现对DOM元素的更新
    function Watcher(name, el, vm, exp, attr) {
        this.name = name;       //指令名称,例如文本节点,该值设置为"text"
        this.el = el;           //指令对应的DOM元素
        this.vm = vm;           //指令所属的实例
        this.exp = exp;         //指令对应的值,本例为:"number"
        this.attr = attr;       //指令绑定的属性值,本例为:"innerHTML"

        this.update();
    }

    Watcher.prototype.update = function () {
        this.el[this.attr] = this.vm.$data[this.exp]; //比如H3.innerHtml = this.data.number;当number改变时会触发update函数,保证对应的DOM内容进行更新
    }

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

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

</script>

相关文章

  • 深入Vue响应式原理

    1.Vue的双向数据绑定 参考 vue的双向绑定原理及实现Vue双向绑定的实现原理Object.definepro...

  • Vue框架基础

    原生js与Vue框架的区别 用原生实现双向数据绑定 用Vue实现双向数据绑定 Vue是一个javaScript框架...

  • Vue实现数据双向绑定的原理

    Vue实现数据双向绑定的原理:Object.defineProperty() vue实现数据双向绑定主要是:采用数...

  • vue 双向数据绑定

    Vue实现数据双向绑定的原理:Object.defineProperty()vue实现数据双向绑定主要是:采用数据...

  • js实现双向数据绑定

    js双向绑定几种方法的介绍 使用Object.defineProperty实现简单的js双向绑定剖析Vue原理&实...

  • Vue2.0原理与MVVM的实现

    剖析Vue原理&实现双向绑定MVVM vue源码 双向绑定 -- MVVM 目前几种主流的MVC框架都实现了单向数...

  • 【转】JavaScript的观察者模式(Vue双向绑定原理)

    关于Vue实现数据双向绑定的原理,请点击:Vue实现数据双向绑定的原理原文链接:JavaScript设计模式之观察...

  • 前端理论面试--VUE

    vue双向绑定的原理(详细链接) VUE实现双向数据绑定的原理就是利用了 Object.definePropert...

  • Vue双向数据绑定原理

    剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模...

  • 关于双向绑定的问题

    剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模...

网友评论

      本文标题:一个简单的vue双向绑定实现案例

      本文链接:https://www.haomeiwen.com/subject/rfyawftx.html