zoukankan      html  css  js  c++  java
  • 面试题:你能写一个Vue的双向数据绑定吗?

    在目前的前端面试中,vue的双向数据绑定已经成为了一个非常容易考到的点,即使不能当场写出来,至少也要能说出原理。本篇文章中我将会仿照vue写一个双向数据绑定的实例,名字就叫myVue吧。结合注释,希望能让大家有所收获。

    1、原理

    Vue的双向数据绑定的原理相信大家也都十分了解了,主要是通过Object对象的defineProperty属性,重写data的set和get函数来实现的,这里对原理不做过多描述,主要还是来实现一个实例。为了使代码更加的清晰,这里只会实现最基本的内容,主要实现v-model,v-bind 和v-click三个命令,其他命令也可以自行补充。

    添加网上的一张图

     

    2、实现

    页面结构很简单,如下

    1 <div id="app">
    2     <form>
    3       <input type="text"  v-model="number">
    4       <button type="button" v-click="increment">增加</button>
    5     </form>
    6     <h3 v-bind="number"></h3>
    7   </div>

    包含:

    1. 一个input,使用v-model指令
    2. 一个button,使用v-click指令
    3. 一个h3,使用v-bind指令。

    我们最后会通过类似于vue的方式来使用我们的双向数据绑定,结合我们的数据结构添加注释

     1 var app = new myVue({
     2       el:'#app',
     3       data: {
     4         number: 0
     5       },
     6       methods: {
     7         increment: function() {
     8           this.number ++;
     9         },
    10       }
    11     })

    首先我们需要定义一个myVue构造函数:

    1 function myVue(options) {
    2   
    3 }

    为了初始化这个构造函数,给它添加一 个_init属性

    1 function myVue(options) {
    2   this._init(options);
    3 }
    4 myVue.prototype._init = function (options) {
    5     this.$options = options;  // options 为上面使用时传入的结构体,包括el,data,methods
    6     this.$el = document.querySelector(options.el); // el是 #app, this.$el是id为app的Element元素
    7     this.$data = options.data; // this.$data = {number: 0}
    8     this.$methods = options.methods;  // this.$methods = {increment: function(){}}
    9   }

    接下来实现_obverse函数,对data进行处理,重写data的set和get函数

    并改造_init函数

     1  myVue.prototype._obverse = function (obj) { // obj = {number: 0}
     2     var value;
     3     for (key in obj) {  //遍历obj对象
     4       if (obj.hasOwnProperty(key)) {
     5         value = obj[key]; 
     6         if (typeof value === 'object') {  //如果值还是对象,则遍历处理
     7           this._obverse(value);
     8         }
     9         Object.defineProperty(this.$data, key, {  //关键
    10           enumerable: true,
    11           configurable: true,
    12           get: function () {
    13             console.log(`获取${value}`);
    14             return value;
    15           },
    16           set: function (newVal) {
    17             console.log(`更新${newVal}`);
    18             if (value !== newVal) {
    19               value = newVal;
    20             }
    21           }
    22         })
    23       }
    24     }
    25   }
    26  
    27  myVue.prototype._init = function (options) {
    28     this.$options = options;
    29     this.$el = document.querySelector(options.el);
    30     this.$data = options.data;
    31     this.$methods = options.methods;
    32    
    33     this._obverse(this.$data);
    34   }

    接下来我们写一个指令类Watcher,用来绑定更新函数,实现对DOM元素的更新

     1 function Watcher(name, el, vm, exp, attr) {
     2     this.name = name;         //指令名称,例如文本节点,该值设为"text"
     3     this.el = el;             //指令对应的DOM元素
     4     this.vm = vm;             //指令所属myVue实例
     5     this.exp = exp;           //指令对应的值,本例如"number"
     6     this.attr = attr;         //绑定的属性值,本例为"innerHTML"
     7 
     8     this.update();
     9   }
    10 
    11   Watcher.prototype.update = function () {
    12     this.el[this.attr] = this.vm.$data[this.exp]; //比如 H3.innerHTML = this.data.number; 当number改变时,会触发这个update函数,保证对应的DOM内容进行了更新。
    13   }

    更新_init函数以及_obverse函数

     1 myVue.prototype._init = function (options) {
     2     //...
     3     this._binding = {};   //_binding保存着model与view的映射关系,也就是我们前面定义的Watcher的实例。当model改变时,我们会触发其中的指令类更新,保证view也能实时更新
     4     //...
     5   }
     6  
     7   myVue.prototype._obverse = function (obj) {
     8     //...
     9       if (obj.hasOwnProperty(key)) {
    10         this._binding[key] = {    // 按照前面的数据,_binding = {number: _directives: []}                                                                                                                                                  
    11           _directives: []
    12         };
    13         //...
    14         var binding = this._binding[key];
    15         Object.defineProperty(this.$data, key, {
    16           //...
    17           set: function (newVal) {
    18             console.log(`更新${newVal}`);
    19             if (value !== newVal) {
    20               value = newVal;
    21               binding._directives.forEach(function (item) {  // 当number改变时,触发_binding[number]._directives 中的绑定的Watcher类的更新
    22                 item.update();
    23               })
    24             }
    25           }
    26         })
    27       }
    28     }
    29   }

    那么如何将view与model进行绑定呢?接下来我们定义一个_compile函数,用来解析我们的指令(v-bind,v-model,v-clickde)等,并在这个过程中对view与model进行绑定。

     1  myVue.prototype._init = function (options) {
     2    //...
     3     this._complie(this.$el);
     4   }
     5  
     6 myVue.prototype._complie = function (root) { root 为 id为app的Element元素,也就是我们的根元素
     7     var _this = this;
     8     var nodes = root.children;
     9     for (var i = 0; i < nodes.length; i++) {
    10       var node = nodes[i];
    11       if (node.children.length) {  // 对所有元素进行遍历,并进行处理
    12         this._complie(node);
    13       }
    14 
    15       if (node.hasAttribute('v-click')) {  // 如果有v-click属性,我们监听它的onclick事件,触发increment事件,即number++
    16         node.onclick = (function () {
    17           var attrVal = nodes[i].getAttribute('v-click');
    18           return _this.$methods[attrVal].bind(_this.$data);  //bind是使data的作用域与method函数的作用域保持一致
    19         })();
    20       }
    21 
    22       if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) { // 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件
    23         node.addEventListener('input', (function(key) {  
    24           var attrVal = node.getAttribute('v-model');
    25            //_this._binding['number']._directives = [一个Watcher实例]
    26            // 其中Watcher.prototype.update = function () {
    27            //    node['vaule'] = _this.$data['number'];  这就将node的值保持与number一致
    28            // }
    29           _this._binding[attrVal]._directives.push(new Watcher(  
    30             'input',
    31             node,
    32             _this,
    33             attrVal,
    34             'value'
    35           ))
    36 
    37           return function() {
    38             _this.$data[attrVal] =  nodes[key].value; // 使number 的值与 node的value保持一致,已经实现了双向绑定
    39           }
    40         })(i));
    41       } 
    42 
    43       if (node.hasAttribute('v-bind')) { // 如果有v-bind属性,我们只要使node的值及时更新为data中number的值即可
    44         var attrVal = node.getAttribute('v-bind');
    45         _this._binding[attrVal]._directives.push(new Watcher(
    46           'text',
    47           node,
    48           _this,
    49           attrVal,
    50           'innerHTML'
    51         ))
    52       }
    53     }
    54   }

    至此,我们已经实现了一个简单vue的双向绑定功能,包括v-bind, v-model, v-click三个指令。效果如下图

     

    附上全部代码,不到150行

      1 <!DOCTYPE html>
      2 <head>
      3   <title>myVue</title>
      4 </head>
      5 <style>
      6   #app {
      7     text-align: center;
      8   }
      9 </style>
     10 <body>
     11   <div id="app">
     12     <form>
     13       <input type="text"  v-model="number">
     14       <button type="button" v-click="increment">增加</button>
     15     </form>
     16     <h3 v-bind="number"></h3>
     17     <form>
     18       <input type="text"  v-model="count">
     19       <button type="button" v-click="incre">增加</button>
     20     </form>
     21     <h3 v-bind="count"></h3>
     22   </div>
     23 </body>
     24 
     25 <script>
     26   function myVue(options) {
     27     this._init(options);
     28   }
     29 
     30   myVue.prototype._init = function (options) {
     31     this.$options = options;
     32     this.$el = document.querySelector(options.el);
     33     this.$data = options.data;
     34     this.$methods = options.methods;
     35 
     36     this._binding = {};
     37     this._obverse(this.$data);
     38     this._complie(this.$el);
     39   }
     40  
     41   myVue.prototype._obverse = function (obj) {
     42     var _this = this;
     43     Object.keys(obj).forEach(function (key) {
     44       if (obj.hasOwnProperty(key)) {
     45         _this._binding[key] = {                                                                                                                                                          
     46           _directives: []
     47         };
     48         console.log(_this._binding[key])
     49         var value = obj[key];
     50         if (typeof value === 'object') {
     51           _this._obverse(value);
     52         }
     53         var binding = _this._binding[key];
     54         Object.defineProperty(_this.$data, key, {
     55           enumerable: true,
     56           configurable: true,
     57           get: function () {
     58             console.log(`${key}获取${value}`);
     59             return value;
     60           },
     61           set: function (newVal) {
     62             console.log(`${key}更新${newVal}`);
     63             if (value !== newVal) {
     64               value = newVal;
     65               binding._directives.forEach(function (item) {
     66                 item.update();
     67               })
     68             }
     69           }
     70         })
     71       }
     72     })
     73   }
     74 
     75   myVue.prototype._complie = function (root) {
     76     var _this = this;
     77     var nodes = root.children;
     78     for (var i = 0; i < nodes.length; i++) {
     79       var node = nodes[i];
     80       if (node.children.length) {
     81         this._complie(node);
     82       }
     83 
     84       if (node.hasAttribute('v-click')) {
     85         node.onclick = (function () {
     86           var attrVal = nodes[i].getAttribute('v-click');
     87           return _this.$methods[attrVal].bind(_this.$data);
     88         })();
     89       }
     90 
     91       if (node.hasAttribute('v-model') && (node.tagName = 'INPUT' || node.tagName == 'TEXTAREA')) {
     92         node.addEventListener('input', (function(key) {
     93           var attrVal = node.getAttribute('v-model');
     94           _this._binding[attrVal]._directives.push(new Watcher(
     95             'input',
     96             node,
     97             _this,
     98             attrVal,
     99             'value'
    100           ))
    101 
    102           return function() {
    103             _this.$data[attrVal] =  nodes[key].value;
    104           }
    105         })(i));
    106       } 
    107 
    108       if (node.hasAttribute('v-bind')) {
    109         var attrVal = node.getAttribute('v-bind');
    110         _this._binding[attrVal]._directives.push(new Watcher(
    111           'text',
    112           node,
    113           _this,
    114           attrVal,
    115           'innerHTML'
    116         ))
    117       }
    118     }
    119   }
    120 
    121   function Watcher(name, el, vm, exp, attr) {
    122     this.name = name;         //指令名称,例如文本节点,该值设为"text"
    123     this.el = el;             //指令对应的DOM元素
    124     this.vm = vm;             //指令所属myVue实例
    125     this.exp = exp;           //指令对应的值,本例如"number"
    126     this.attr = attr;         //绑定的属性值,本例为"innerHTML"
    127 
    128     this.update();
    129   }
    130 
    131   Watcher.prototype.update = function () {
    132     this.el[this.attr] = this.vm.$data[this.exp];
    133   }
    134 
    135   window.onload = function() {
    136     var app = new myVue({
    137       el:'#app',
    138       data: {
    139         number: 0,
    140         count: 0,
    141       },
    142       methods: {
    143         increment: function() {
    144           this.number ++;
    145         },
    146         incre: function() {
    147           this.count ++;
    148         }
    149       }
    150     })
    151   }
    152 </script>

    附上原文地址

    
    
  • 相关阅读:
    linux-网卡故障
    css hack
    IE7的overflow失效的解决方法
    Js中 关于top、clientTop、scrollTop、offsetTop的用法
    javascript作用域(Scope),简述上下文(context)和作用域的定义
    统计代码行数的小技巧
    sql复制表、拷贝表、临时表
    string.format
    手机号正则验证
    getBoundingClientRect() 来获取页面元素的位置
  • 原文地址:https://www.cnblogs.com/Object-L/p/12529510.html
Copyright © 2011-2022 走看看