查看原文
其他

面试题:你能写一个 Vue 的双向数据绑定吗?

前端大全 2019-11-12

(点击上方公众号,可快速关注)


作者:呆头呆脑丶

segmentfault.com/a/1190000014274840


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

1、原理

Vue的双向数据绑定的原理相信大家也都十分了解了,主要是通过 Object对象的defineProperty属性,重写datasetget函数来实现的,这里对原理不做过多描述,主要还是来实现一个实例。为了使代码更加的清晰,这里只会实现最基本的内容,主要实现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. }

为了初始化这个构造函数,给它添加一个 _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. myVue.prototype._init = function (options) {

  27.    this.$options = options;

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

  29.    this.$data = options.data;

  30.    this.$methods = options.methods;

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

  32.  }

接下来我们写一个指令类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.    this.update();

  8.  }

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

  10.    this.el[this.attr] = this.vm.$data[this.exp]; //比如 H3.innerHTML = this.data.number; 当number改变时,会触发这个update函数,保证对应的DOM内容进行了更新。

  11.  }

更新 _init 函数以及 \_obverse 函数:

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

  2.    //...

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

  4.    //...

  5.  }

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

  7.    //...

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

  9.        this._binding[key] = {    // 按照前面的数据,_binding = {number: _directives: []}                                                                                                                                                  

  10.          _directives: []

  11.        };

  12.        //...

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

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

  15.          //...

  16.          set: function (newVal) {

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

  18.            if (value !== newVal) {

  19.              value = newVal;

  20.              binding._directives.forEach(function (item) {  // 当number改变时,触发_binding[number]._directives 中的绑定的Watcher类的更新

  21.                item.update();

  22.              })

  23.            }

  24.          }

  25.        })

  26.      }

  27.    }

  28.  }

那么如何将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. myVue.prototype._complie = function (root) { root 为 id为app的Element元素,也就是我们的根元素

  6.    var _this = this;

  7.    var nodes = root.children;

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

  9.      var node = nodes[i];

  10.      if (node.children.length) {  // 对所有元素进行遍历,并进行处理

  11.        this._complie(node);

  12.      }

  13.      if (node.hasAttribute('v-click')) {  // 如果有v-click属性,我们监听它的onclick事件,触发increment事件,即number++

  14.        node.onclick = (function () {

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

  16.          return _this.$methods[attrVal].bind(_this.$data);  //bind是使data的作用域与method函数的作用域保持一致

  17.        })();

  18.      }

  19.      if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) { // 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件

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

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

  22.           //_this._binding['number']._directives = [一个Watcher实例]

  23.           // 其中Watcher.prototype.update = function () {

  24.           //    node['vaule'] = _this.$data['number'];  这就将node的值保持与number一致

  25.           // }

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

  27.            'input',

  28.            node,

  29.            _this,

  30.            attrVal,

  31.            'value'

  32.          ))

  33.          return function() {

  34.            _this.$data[attrVal] =  nodes[key].value; // 使number 的值与 node的value保持一致,已经实现了双向绑定

  35.          }

  36.        })(i));

  37.      }

  38.      if (node.hasAttribute('v-bind')) { // 如果有v-bind属性,我们只要使node的值及时更新为data中number的值即可

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

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

  41.          'text',

  42.          node,

  43.          _this,

  44.          attrVal,

  45.          'innerHTML'

  46.        ))

  47.      }

  48.    }

  49.  }

至此,我们已经实现了一个简单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. <script>

  20.  function myVue(options) {

  21.    this._init(options);

  22.  }

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

  24.    this.$options = options;

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

  26.    this.$data = options.data;

  27.    this.$methods = options.methods;

  28.    this._binding = {};

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

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

  31.  }

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

  33.    var value;

  34.    for (key in obj) {

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

  36.        this._binding[key] = {                                                                                                                                                          

  37.          _directives: []

  38.        };

  39.        value = obj[key];

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

  41.          this._obverse(value);

  42.        }

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

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

  45.          enumerable: true,

  46.          configurable: true,

  47.          get: function () {

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

  49.            return value;

  50.          },

  51.          set: function (newVal) {

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

  53.            if (value !== newVal) {

  54.              value = newVal;

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

  56.                item.update();

  57.              })

  58.            }

  59.          }

  60.        })

  61.      }

  62.    }

  63.  }

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

  65.    var _this = this;

  66.    var nodes = root.children;

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

  68.      var node = nodes[i];

  69.      if (node.children.length) {

  70.        this._complie(node);

  71.      }

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

  73.        node.onclick = (function () {

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

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

  76.        })();

  77.      }

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

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

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

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

  82.            'input',

  83.            node,

  84.            _this,

  85.            attrVal,

  86.            'value'

  87.          ))

  88.          return function() {

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

  90.          }

  91.        })(i));

  92.      }

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

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

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

  96.          'text',

  97.          node,

  98.          _this,

  99.          attrVal,

  100.          'innerHTML'

  101.        ))

  102.      }

  103.    }

  104.  }

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

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

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

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

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

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

  111.    this.update();

  112.  }

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

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

  115.  }

  116.  window.onload = function() {

  117.    var app = new myVue({

  118.      el:'#app',

  119.      data: {

  120.        number: 0

  121.      },

  122.      methods: {

  123.        increment: function() {

  124.          this.number ++;

  125.        },

  126.      }

  127.    })

  128.  }

  129. </script>

如果喜欢请关注我的Github(https://github.com/louzhedong/blog),给个Star吧,我会定期分享一些JS中的知识,\^_^


觉得本文对你有帮助?请分享给更多人

关注「前端大全」,提升前端技能

淘口令复制以下红色内容,再打开手淘即可购买

范品社,使用¥极客T恤¥抢先预览(长按复制整段文案,打开手机淘宝即可进入活动内容)

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存