美文网首页
一个简单的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双向绑定实现案例

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