你能写一个 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.  </div>

  18. </body>

  19.  

  20. <script>

  21.  function myVue(options) {

  22.    this._init(options);

  23.  }

  24.  

  25.  myVue.prototype._init = function (options) {

  26.    this.$options = options;

  27.    this.$el = document.querySelector(options.el);

  28.    this.$data = options.data;

  29.    this.$methods = options.methods;

  30.  

  31.    this._binding = {};

  32.    this._obverse(this.$data);

  33.    this._complie(this.$el);

  34.  }

  35.  

  36.  myVue.prototype._obverse = function (obj) {

  37.    var value;

  38.    for (key in obj) {

  39.      if (obj.hasOwnProperty(key)) {

  40.        this._binding[key] = {                                                                                                                                                          

  41.          _directives: []

  42.        };

  43.        value = obj[key];

  44.        if (typeof value === 'object') {

  45.          this._obverse(value);

  46.        }

  47.        var binding = this._binding[key];

  48.        Object.defineProperty(this.$data, key, {

  49.          enumerable: true,

  50.          configurable: true,

  51.          get: function () {

  52.            console.log(`获取${value}`);

  53.            return value;

  54.          },

  55.          set: function (newVal) {

  56.            console.log(`更新${newVal}`);

  57.            if (value !== newVal) {

  58.              value = newVal;

  59.              binding._directives.forEach(function (item) {

  60.                item.update();

  61.              })

  62.            }

  63.          }

  64.        })

  65.      }

  66.    }

  67.  }

  68.  

  69.  myVue.prototype._complie = function (root) {

  70.    var _this = this;

  71.    var nodes = root.children;

  72.    for (var i = 0; i < nodes.length; i++) {

  73.      var node = nodes[i];

  74.      if (node.children.length) {

  75.        this._complie(node);

  76.      }

  77.  

  78.      if (node.hasAttribute('v-click')) {

  79.        node.onclick = (function () {

  80.          var attrVal = nodes[i].getAttribute('v-click');

  81.          return _this.$methods[attrVal].bind(_this.$data);

  82.        })();

  83.      }

  84.  

  85.      if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) {

  86.        node.addEventListener('input', (function(key) {

  87.          var attrVal = node.getAttribute('v-model');

  88.          _this._binding[attrVal]._directives.push(new Watcher(

  89.            'input',

  90.            node,

  91.            _this,

  92.            attrVal,

  93.            'value'

  94.          ))

  95.  

  96.          return function() {

  97.            _this.$data[attrVal] =  nodes[key].value;

  98.          }

  99.        })(i));

  100.      }

  101.  

  102.      if (node.hasAttribute('v-bind')) {

  103.        var attrVal = node.getAttribute('v-bind');

  104.        _this._binding[attrVal]._directives.push(new Watcher(

  105.          'text',

  106.          node,

  107.          _this,

  108.          attrVal,

  109.          'innerHTML'

  110.        ))

  111.      }

  112.    }

  113.  }

  114.  

  115.  function Watcher(name, el, vm, exp, attr) {

  116.    this.name = name;         //指令名称,例如文本节点,该值设为"text"

  117.    this.el = el;             //指令对应的DOM元素

  118.    this.vm = vm;             //指令所属myVue实例

  119.    this.exp = exp;           //指令对应的值,本例如"number"

  120.    this.attr = attr;         //绑定的属性值,本例为"innerHTML"

  121.  

  122.    this.update();

  123.  }

  124.  

  125.  Watcher.prototype.update = function () {

  126.    this.el[this.attr] = this.vm.$data[this.exp];

  127.  }

  128.  

  129.  window.onload = function() {

  130.    var app = new myVue({

  131.      el:'#app',

  132.      data: {

  133.        number: 0

  134.      },

  135.      methods: {

  136.        increment: function() {

  137.          this.number ++;

  138.        },

  139.      }

  140.    })

  141.  }

  142. </script>

 

链接地址:https://mp.weixin.qq.com/s?__biz=MzAxODE2MjM1MA==&mid=2651554000&idx=1&sn=08219fe9433fef033b17b98d1072367f&chksm=80255711b752de07e163f621ed848096850130a4484e0c81357917fd6e044a76a3cd5cf17926&scene=0#rd

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值