自己寫一個vue雙向綁定

        在目前的前端面試中,vue的雙向數據綁定已經成爲了一個非常容易考到的點,即使不能當場寫出來,至少也要能說出原理。本篇文章中我將會仿照vue寫一個雙向數據綁定的實例,名字就叫myVue吧。

1、原理

Vue的雙向數據綁定的原理相信大家也都十分了解了,主要是通過 Object對象的defineProperty屬性,重寫datasetget函數來實現的,這裏對原理不做過多描述,主要還是來實現一個實例。爲了使代碼更加的清晰,這裏只會實現最基本的內容,主要實現v-model,v-bind 和v-click三個命令,其他命令也可以自行補充。下面這個是雙向綁定的原理圖。

2、實現

頁面結構很簡單,如下:

包含:

  1. 一個input,使用v-model指令

  2. 一個button,使用v-click指令

  3. 一個h3,使用v-bind指令。

我們最後會通過類似於vue的方式來使用我們的雙向數據綁定,結合我們的數據結構添加註釋:

var app = new myVue({
    el: "#app",
    data: {
        number: 0
    },
    methods: {
        increment: function () {
            this.number++;
        }
    }
});

首先我們需要定義一個myVue構造函數:

function myVue(options) {
    this._init(options);
};

爲了初始化這個構造函數,給它添加一個 _init 屬性:

function myVue(options) {
    this._init(options);
};
myVue.prototype._init=function (options) {
    this.$options=options;
    this.$el=document.querySelector(options.el);
    this.$data=options.data;
    this.$methods=options.methods;
};

接下來實現 _obverse 函數,對data進行處理,重寫data的set和get函數:

並改造_init函數

myVue.prototype._observer=function(obj){
    var value;
    for(key in obj){
        if(obj.hasOwnProperty(key)){
            value=obj[key];
            if(typeof(vlaue)){
                this._observer(value);
            }

            Object.defineProperty($this.data,key,{
                get:function () {
                    return value;

                },
                set:function (newVal) {
                    if(value !== newVal){
                        value=newVal;
                    }
                }
            })
        }
    }
}
myVue.prototype._init=function (options) {
    this.$options=options;
    this.$el=document.querySelector(options.el);
    this.$data=options.data;
    this.$methods=options.methods;

    this._observer(this.$data);
};

接下來我們寫一個指令類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 idappElement元素,也就是我們的根元素

  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.  }

全部代碼如下:

  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>


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章