面試題:你能寫一個(gè)Vue的雙向數(shù)據(jù)綁定嗎耕腾?

在目前的前端面試中见剩,vue的雙向數(shù)據(jù)綁定已經(jīng)成為了一個(gè)非常容易考到的點(diǎn),即時(shí)不能當(dāng)場(chǎng)寫出來扫俺,至少也要能說出原理苍苞。本篇文章中我將會(huì)仿照vue寫一個(gè)雙向數(shù)據(jù)綁定的實(shí)例,名字就叫myVue吧狼纬。結(jié)合注釋羹呵,希望能讓大家有所收獲。

1疗琉、原理

Vue的雙向數(shù)據(jù)綁定的原理相信大家也都十分了解了冈欢,主要是通過Object對(duì)象的defineProperty屬性,重寫data的set和get函數(shù)來實(shí)現(xiàn)的,這里對(duì)原理不做過多描述盈简,只要還是來實(shí)現(xiàn)一個(gè)實(shí)例凑耻。為了使代碼更加的清晰,這里只會(huì)實(shí)現(xiàn)最基本的內(nèi)容柠贤,主要實(shí)現(xiàn)v-model拳话,v-bind 和v-click三個(gè)命令,其他命令也可以自行補(bǔ)充种吸。

添加網(wǎng)上的一張圖

image
2、實(shí)現(xiàn)

頁面結(jié)構(gòu)很簡單呀非,如下

<div id="app">
    <form>
      <input type="text"  v-model="number">
      <button type="button" v-click="increment">增加</button>
    </form>
    <h3 v-bind="number"></h3>
  </div>

包含:

1. 一個(gè)input坚俗,使用v-model指令
2. 一個(gè)button,使用v-click指令
3. 一個(gè)h3岸裙,使用v-bind指令猖败。

我們最后會(huì)通過類似于vue的方式來使用我們的雙向數(shù)據(jù)綁定,結(jié)合我們的數(shù)據(jù)結(jié)構(gòu)添加注釋

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

首先我們需要定義一個(gè)myVue構(gòu)造函數(shù):

function myVue(options) {
  
}

為了初始化這個(gè)構(gòu)造函數(shù)降允,給它添加一 個(gè)_init屬性

function myVue(options) {
  this._init(options);
}
myVue.prototype._init = function (options) {
    this.$options = options;  // options 為上面使用時(shí)傳入的結(jié)構(gòu)體恩闻,包括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(){}}
  }

接下來實(shí)現(xiàn)_obverse函數(shù),對(duì)data進(jìn)行處理剧董,重寫data的set和get函數(shù)

并改造_init函數(shù)

 myVue.prototype._obverse = function (obj) { // obj = {number: 0}
    var value;
    for (key in obj) {  //遍歷obj對(duì)象
      if (obj.hasOwnProperty(key)) {
        value = obj[key]; 
        if (typeof value === 'object') {  //如果值還是對(duì)象幢尚,則遍歷處理
          this._obverse(value);
        }
        Object.defineProperty(this.$data, key, {  //關(guān)鍵
          enumerable: true,
          configurable: true,
          get: function () {
            console.log(`獲取${value}`);
            return value;
          },
          set: function (newVal) {
            console.log(`更新${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._obverse(this.$data);
  }

接下來我們寫一個(gè)指令類Watcher破停,用來綁定更新函數(shù),實(shí)現(xiàn)對(duì)DOM元素的更新

function Watcher(name, el, vm, exp, attr) {
    this.name = name;         //指令名稱尉剩,例如文本節(jié)點(diǎn)真慢,該值設(shè)為"text"
    this.el = el;             //指令對(duì)應(yīng)的DOM元素
    this.vm = vm;             //指令所屬myVue實(shí)例
    this.exp = exp;           //指令對(duì)應(yīng)的值,本例如"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; 當(dāng)number改變時(shí)黑界,會(huì)觸發(fā)這個(gè)update函數(shù),保證對(duì)應(yīng)的DOM內(nèi)容進(jìn)行了更新皂林。
  }

更新_init函數(shù)以及_obverse函數(shù)

myVue.prototype._init = function (options) {
    //...
    this._binding = {};   //_binding保存著model與view的映射關(guān)系朗鸠,也就是我們前面定義的Watcher的實(shí)例。當(dāng)model改變時(shí)础倍,我們會(huì)觸發(fā)其中的指令類更新烛占,保證view也能實(shí)時(shí)更新
    //...
  }
 
  myVue.prototype._obverse = function (obj) {
    //...
      if (obj.hasOwnProperty(key)) {
        this._binding[key] = {    // 按照前面的數(shù)據(jù),_binding = {number: _directives: []}                                                                                                                                                  
          _directives: []
        };
        //...
        var binding = this._binding[key];
        Object.defineProperty(this.$data, key, {
          //...
          set: function (newVal) {
            console.log(`更新${newVal}`);
            if (value !== newVal) {
              value = newVal;
              binding._directives.forEach(function (item) {  // 當(dāng)number改變時(shí)著隆,觸發(fā)_binding[number]._directives 中的綁定的Watcher類的更新
                item.update();
              })
            }
          }
        })
      }
    }
  }

那么如何將view與model進(jìn)行綁定呢扰楼?接下來我們定義一個(gè)_compile函數(shù),用來解析我們的指令(v-bind,v-model,v-clickde)等美浦,并在這個(gè)過程中對(duì)view與model進(jìn)行綁定弦赖。

 myVue.prototype._init = function (options) {
   //...
    this._complie(this.$el);
  }
 
myVue.prototype._complie = function (root) { root 為 id為app的Element元素,也就是我們的根元素
    var _this = this;
    var nodes = root.children;
    for (var i = 0; i < nodes.length; i++) {
      var node = nodes[i];
      if (node.children.length) {  // 對(duì)所有元素進(jìn)行遍歷浦辨,并進(jìn)行處理
        this._complie(node);
      }

      if (node.hasAttribute('v-click')) {  // 如果有v-click屬性蹬竖,我們監(jiān)聽它的onclick事件,觸發(fā)increment事件流酬,即number++
        node.onclick = (function () {
          var attrVal = nodes[i].getAttribute('v-click');
          return _this.$methods[attrVal].bind(_this.$data);  //bind是使data的作用域與method函數(shù)的作用域保持一致
        })();
      }

      if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) { // 如果有v-model屬性币厕,并且元素是INPUT或者TEXTAREA,我們監(jiān)聽它的input事件
        node.addEventListener('input', (function(key) {  
          var attrVal = node.getAttribute('v-model');
           //_this._binding['number']._directives = [一個(gè)Watcher實(shí)例]
           // 其中Watcher.prototype.update = function () {
           //   node['vaule'] = _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保持一致芽腾,已經(jīng)實(shí)現(xiàn)了雙向綁定
          }
        })(i));
      } 

      if (node.hasAttribute('v-bind')) { // 如果有v-bind屬性旦装,我們只要使node的值及時(shí)更新為data中number的值即可
        var attrVal = node.getAttribute('v-bind');
        _this._binding[attrVal]._directives.push(new Watcher(
          'text',
          node,
          _this,
          attrVal,
          'innerHTML'
        ))
      }
    }
  }

至此,我們已經(jīng)實(shí)現(xiàn)了一個(gè)簡單vue的雙向綁定功能摊滔,包括v-bind, v-model, v-click三個(gè)指令阴绢。效果如下圖

image

附上全部代碼,不到150行

<!DOCTYPE html>
<head>
  <title>myVue</title>
</head>
<style>
  #app {
    text-align: center;
  }
</style>
<body>
  <div id="app">
    <form>
      <input type="text"  v-model="number">
      <button type="button" v-click="increment">增加</button>
    </form>
    <h3 v-bind="number"></h3>
  </div>
</body>

<script>
  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;

    this._binding = {};
    this._obverse(this.$data);
    this._complie(this.$el);
  }
 
  myVue.prototype._obverse = function (obj) {
    var value;
    for (key in obj) {
      if (obj.hasOwnProperty(key)) {
        this._binding[key] = {                                                                                                                                                          
          _directives: []
        };
        value = obj[key];
        if (typeof value === 'object') {
          this._obverse(value);
        }
        var binding = this._binding[key];
        Object.defineProperty(this.$data, key, {
          enumerable: true,
          configurable: true,
          get: function () {
            console.log(`獲取${value}`);
            return value;
          },
          set: function (newVal) {
            console.log(`更新${newVal}`);
            if (value !== newVal) {
              value = newVal;
              binding._directives.forEach(function (item) {
                item.update();
              })
            }
          }
        })
      }
    }
  }

  myVue.prototype._complie = function (root) {
    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')) {
        node.onclick = (function () {
          var attrVal = nodes[i].getAttribute('v-click');
          return _this.$methods[attrVal].bind(_this.$data);
        })();
      }

      if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) {
        node.addEventListener('input', (function(key) {
          var attrVal = node.getAttribute('v-model');
          _this._binding[attrVal]._directives.push(new Watcher(
            'input',
            node,
            _this,
            attrVal,
            'value'
          ))

          return function() {
            _this.$data[attrVal] =  nodes[key].value;
          }
        })(i));
      } 

      if (node.hasAttribute('v-bind')) {
        var attrVal = node.getAttribute('v-bind');
        _this._binding[attrVal]._directives.push(new Watcher(
          'text',
          node,
          _this,
          attrVal,
          'innerHTML'
        ))
      }
    }
  }

  function Watcher(name, el, vm, exp, attr) {
    this.name = name;         //指令名稱艰躺,例如文本節(jié)點(diǎn)呻袭,該值設(shè)為"text"
    this.el = el;             //指令對(duì)應(yīng)的DOM元素
    this.vm = vm;             //指令所屬myVue實(shí)例
    this.exp = exp;           //指令對(duì)應(yīng)的值,本例如"number"
    this.attr = attr;         //綁定的屬性值腺兴,本例為"innerHTML"

    this.update();
  }

  Watcher.prototype.update = function () {
    this.el[this.attr] = this.vm.$data[this.exp];
  }

  window.onload = function() {
    var app = new myVue({
      el:'#app',
      data: {
        number: 0
      },
      methods: {
        increment: function() {
          this.number ++;
        },
      }
    })
  }
</script>

如果喜歡請(qǐng)關(guān)注我的Github左电,給個(gè)Star吧,我會(huì)定期分享一些JS中的知識(shí),^_^

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末篓足,一起剝皮案震驚了整個(gè)濱河市段誊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌纷纫,老刑警劉巖枕扫,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異辱魁,居然都是意外死亡烟瞧,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門染簇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來参滴,“玉大人,你說我怎么就攤上這事锻弓±猓” “怎么了?”我有些...
    開封第一講書人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵青灼,是天一觀的道長暴心。 經(jīng)常有香客問我,道長杂拨,這世上最難降的妖魔是什么专普? 我笑而不...
    開封第一講書人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮弹沽,結(jié)果婚禮上檀夹,老公的妹妹穿的比我還像新娘。我一直安慰自己策橘,他們只是感情好炸渡,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著丽已,像睡著了一般蚌堵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上沛婴,一...
    開封第一講書人閱讀 52,736評(píng)論 1 312
  • 那天辰斋,我揣著相機(jī)與錄音,去河邊找鬼瘸味。 笑死,一個(gè)胖子當(dāng)著我的面吹牛够挂,可吹牛的內(nèi)容都是我干的旁仿。 我是一名探鬼主播,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼枯冈!你這毒婦竟也來了毅贮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤尘奏,失蹤者是張志新(化名)和其女友劉穎滩褥,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體炫加,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瑰煎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了俗孝。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片酒甸。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖赋铝,靈堂內(nèi)的尸體忽然破棺而出插勤,到底是詐尸還是另有隱情,我是刑警寧澤革骨,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布农尖,位于F島的核電站,受9級(jí)特大地震影響良哲,放射性物質(zhì)發(fā)生泄漏盛卡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一臂外、第九天 我趴在偏房一處隱蔽的房頂上張望窟扑。 院中可真熱鬧,春花似錦漏健、人聲如沸嚎货。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽殖属。三九已至,卻和暖如春瓦盛,著一層夾襖步出監(jiān)牢的瞬間洗显,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來泰國打工原环, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留挠唆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓嘱吗,卻偏偏與公主長得像玄组,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內(nèi)容俄讹,還有我對(duì)于 Vue 1.0 印象不深的內(nèi)容哆致。關(guān)于...
    云之外閱讀 5,052評(píng)論 0 29
  • 1.安裝 可以簡單地在頁面引入Vue.js作為獨(dú)立版本,Vue即被注冊(cè)為全局變量患膛,可以在頁面使用了摊阀。 如果希望搭建...
    Awey閱讀 11,039評(píng)論 4 129
  • Vue 實(shí)例 屬性和方法 每個(gè) Vue 實(shí)例都會(huì)代理其 data 對(duì)象里所有的屬性:var data = { a:...
    云之外閱讀 2,221評(píng)論 0 6
  • 更新了Xcode8之后,簡直被它"強(qiáng)大"的報(bào)錯(cuò)能力所折服踪蹬,各種莫名其妙的錯(cuò)誤胞此,下面是我的一些錯(cuò)誤整理及相應(yīng)的解決辦...
    小唐羽鋒閱讀 1,160評(píng)論 3 51
  • 作為“樂器之后”的小提琴,雖然有著高貴典雅的樂曲延曙,但在眾多樂器的學(xué)習(xí)中小提琴學(xué)習(xí)者并不是最多豌鹤,因其有一定的“最難...
    VictoriaCai閱讀 709評(píng)論 0 5