小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

一探 Vue 數(shù)據(jù)響應(yīng)式原理

 行者花雕 2021-09-28

本文寫(xiě)于 2020 年 8 月 5 日

相信在很多新人第一次使用 Vue 這種框架的時(shí)候,就會(huì)被其修改數(shù)據(jù)便自動(dòng)更新視圖的操作所震撼。

Vue 的文檔中也這么寫(xiě)道:

Vue 最獨(dú)特的特性之一,是其非侵入性的響應(yīng)式系統(tǒng)。數(shù)據(jù)模型僅僅是普通的 JavaScript 對(duì)象。而當(dāng)你修改它們時(shí),視圖會(huì)進(jìn)行更新。

單看這句話,像我這種菜鳥(niǎo)程序員必然是看不懂的。我只知道,在 new Vue() 時(shí)傳入的 data 屬性一旦產(chǎn)生變化,那么在視圖里的變量也會(huì)隨之而變。

但這個(gè)變化是如何實(shí)現(xiàn)的呢?接下來(lái)讓我們,一探究竟。

1 偷偷變化的 data

我們先來(lái)新建一個(gè)變量:let data = { msg: 'hello world' }。

接著我們將這個(gè) data 傳給 Vue 的 data:

let data = { msg: 'hello world' }

/*****留空處*****/

new Vue({
  data,
  methods: {
    showData() {
      console.log(data)
    }
  }
})

這看似是非常平常的操作,但是我們?cè)谟|發(fā) showData 的時(shí)候,會(huì)發(fā)現(xiàn)打出來(lái) data 不太對(duì)勁:

msg: (...)
__ob__: Observer {value: {…}, dep: Dep, vmCount: 1}
get msg: ? reactiveGetter()
set msg: ? reactiveSetter(newVal)
__proto__: Object

它不僅多了很多沒(méi)見(jiàn)過(guò)的屬性,還把里面的 msg: hello world 變成了 msg: (...)。

接下來(lái)我們嘗試在留空處打印出 data,即在定義完 data 之后立即將其打印。

但是很不幸,打印出來(lái)依然是上面這個(gè)不對(duì)勁的值。

可是很明顯,當(dāng)我們不去 new Vue(),并且傳入 data 的時(shí)候,data 的打印結(jié)果絕對(duì)不是這樣。

所以我們可以嘗試?yán)?setTimeout()new Vue() 延遲 3 秒執(zhí)行。

這個(gè)時(shí)候我們就會(huì)驚訝的發(fā)現(xiàn):

  1. 當(dāng)我們?cè)?3s 內(nèi)點(diǎn)開(kāi) console 的結(jié)果時(shí),data 是普通的形式;

  2. 當(dāng)我們?cè)?3s 后點(diǎn)開(kāi) console 的結(jié)果時(shí),data 又變成了奇怪的形式。

這說(shuō)明就是 new Vue() 的過(guò)程中,Vue 偷偷的對(duì) data 進(jìn)行了修改!正是這個(gè)修改,讓 data 的數(shù)據(jù),變成了響應(yīng)式數(shù)據(jù)。

2 (...) 的由來(lái)

為什么好好的一個(gè) msg 屬性會(huì)變成 (...) 呢?

這就涉及到了 ES6 中的 getter 和 setter。(如果理解 getter/setter,可跳至下一節(jié))

一般我們?nèi)绻枰?jì)算后的值,會(huì)定義一個(gè)函數(shù),例如:

const obj = {
  number: 5,
  double() {
    return this.number * 2;
  }
};

在使用的時(shí)候,我們寫(xiě)上 obj.double(obj.number) 即可。

但是函數(shù)是需要加括號(hào)的,我太懶了,以至于括號(hào)都不想要了。

于是就有了 getter 方法:

const obj = {
  number: 5,
  get double() {
    return this.number * 2;
  }
};

const newNumber = obj.double;

這樣一來(lái),就能夠不需要括號(hào),就可以得到 return 的值。

setter 同理:

const obj = {
  number: 5,
  set double(value) {
    if(this.number * 2 != value;)
    this.number = value;
  }
};

obj.double = obj.number * 2;

由此我們可以看出:通過(guò) setter,我們可以達(dá)到給賦值設(shè)限的效果,例如這里我就要求新值必須是原值的兩倍才可以。

但經(jīng)常的,我們會(huì)用 getter/setter 來(lái)隱藏一個(gè)變量。

比如:

const obj = {
  _number: 5,
  get number() {
    return this._number;
  },
  set number(value) {
    this._number = value;
  }
};

這個(gè)時(shí)候我們打印出 obj,就會(huì)驚訝的發(fā)現(xiàn) (...) 出現(xiàn)了:

number: (...)
_number: 5

現(xiàn)在我們明白了,Vue 偷偷做的事情,就是把 data 里面的數(shù)據(jù)全變成了 getter/setter。

3 利用 Object.defineProperty() 實(shí)現(xiàn)代理

這個(gè)時(shí)候我們想一個(gè)問(wèn)題,原來(lái)我們可以通過(guò) obj.c = 'c'; 來(lái)定義 c 的值——即使 c 本身不在 obj 中。

但如何定義一個(gè) getter/setter 呢?答:使用 Object.defineProperty()。

Object.defineProperty() 方法會(huì)直接在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)對(duì)象的現(xiàn)有屬性,并返回此對(duì)象。

例如我們上面寫(xiě)的 obj.c = 'c';,就可以通過(guò)

const obj = {
  a: 'a',
  b: 'b'
}
Object.defineProperty(obj, 'c', {
  value: 'c'
})

Object.defineProperty() 接收三個(gè)參數(shù):第一個(gè)是要定義屬性的對(duì)象;第二個(gè)是要定義或修改的屬性的名稱或 Symbol;第三個(gè)則是要定義或修改的屬性描述符。

在第三個(gè)參數(shù)中,可以接收多個(gè)屬性,value 代表「值」,除此之外還有 configurable, enumerable, writable, get, set 一共六個(gè)屬性。

這里我們只看 getset。

之前我們說(shuō)了,通過(guò) getter/setter 我們可以把不想讓別人直接操作的數(shù)據(jù)“藏起來(lái)”。

可是本質(zhì)上,我們只是在前面加了一個(gè) _ 而已,直接訪問(wèn)是可以繞過(guò)我們的 getter/setter 的!

那么我們?cè)趺崔k呢?

利用代理。這個(gè)代理不是 ES6 新增的 Proxy,而是設(shè)計(jì)模式的一種。

我們剛剛為什么可以去修改我們“藏起來(lái)”的屬性值?

因?yàn)槲覀冎浪拿盅?!如果?strong>不給他名字,自然別人就不可能修改了。

例如我們寫(xiě)一個(gè)函數(shù),然后把數(shù)據(jù)傳進(jìn)去:

proxy({ a: 'a' })

這樣一來(lái)我們的 { a: 'a' } 就根本沒(méi)有名字了,無(wú)從改起!

接下來(lái)我們?cè)诙x proxy 函數(shù)時(shí),可以新建一個(gè)空對(duì)象,然后遍歷傳入的值,分別進(jìn)行 Object.defineProperty()將傳入的對(duì)象的 keys 作為 getter/setter 賦給新建的空對(duì)象。

最后,我們 return 這個(gè)對(duì)象即可。

let data = proxy({
  a: 'a',
  b: 'b'
});

function proxy(data) {
  const obj = {};
  const keys = Object.keys(data);
  for (let i = 0; i < keys.length; i++) {
    Object.defineProperty(obj, keys[i], {
      get() {
        return data[keys[i]];
      },
      set(value) {
        if (value < 0) return;
        data[keys[i]] = value;
      }
    });
  }
  return obj;
}

這樣一來(lái),我們一開(kāi)始聲明的 data,就是我們 return 的對(duì)象了。在這個(gè)對(duì)象里,沒(méi)有原始的數(shù)據(jù),別人無(wú)法繞過(guò) getter/setter 進(jìn)行操作!

但是往往并沒(méi)有這么簡(jiǎn)單,如果我一定需要一個(gè)變量名呢?

const sourceData = {
  a: 'a',
  b: 'b'
};

let data = proxy(sourceData);

如此一來(lái),通過(guò)直接操作 sourceData.a,時(shí)可以直接繞過(guò)我們?cè)?proxy 中設(shè)置的 set a 進(jìn)行賦值的。這個(gè)時(shí)候我們?cè)趺刺幚恚?/p>

很簡(jiǎn)單嘛,當(dāng)我們遍歷傳入的數(shù)據(jù)時(shí),我們可以對(duì)傳入的數(shù)據(jù)新增 getter/setter,此后原始的數(shù)據(jù)就會(huì)被 getter/setter 所替代。

在剛剛的代碼中,我們?cè)谘h(huán)的剛開(kāi)始添加這樣一段代碼:

for(/*......*/) {
  const value = data[keys[i]];
  Object.defineProperty(data, keys[i], {
    get() {
      return value;
    },
    set(newValue) {
      if (newValue < 0) return;
      value = newValue;
    }
  });
  /*......*/
}

這是什么意思呢?

我們利用了閉包,將原始值單獨(dú)拎出來(lái),每一次對(duì)原始屬性進(jìn)行讀寫(xiě),其實(shí)都是 get 和 set 在讀取閉包時(shí)被拎出來(lái)的值。

那么不管別人是操作我們的 let data = proxy(sourceData); 的 data,還是操作 sourceData,都會(huì)被我們的 getter/setter 所攔截。

4 回到 Vue

我們剛剛寫(xiě)的代碼是這樣的:

let data = proxy({
  a: 'a'
})

function proxy(data) {

}

那如果我改成這樣呢:

let data = proxy({
  data: {
    a: 'a'
  }
})

function proxy({ data }) {
  // 結(jié)構(gòu)賦值
}

是不是和 Vue 就非常非常像了!

const vm = new Vue({ data: {} }) 也是讓 vm 成為 data 的代理,并且就算你從外部將數(shù)據(jù)傳給 data,也會(huì)被 Vue 所捕捉。

而在每一次捕獲到你操作數(shù)據(jù)之后,就會(huì)對(duì)需要改變的 UI 進(jìn)行重新渲染。

同理,Vue 對(duì) computed 和 watch 也存在著各種偷偷的處理。

5 Vue 數(shù)據(jù)響應(yīng)式的 Bug

如果我們的數(shù)據(jù)是這樣:

data: {
  obj: {
    a: 'a'
  }
}

我們?cè)?Vue 的 template 里卻寫(xiě)了 <div>{{ obj.b }}<div> 會(huì)怎樣?

Vue 對(duì)于不存在或者為 undefined 和 null 的數(shù)據(jù)是不予以顯示的。但是當(dāng)我們往 obj 中新增 b 的時(shí)候,他會(huì)顯示嗎?

寫(xiě)法一:

const vm = new Vue({
  data: {
    obj: {
      a: 'a'
    }
  },
  methods: {
    changeObj() {
      this.obj.b = 'b';
    }
  }
})

我們可以給一個(gè)按鈕綁定 changeObj 事件,但是很遺憾,這樣并不能使視圖中的 obj.b 顯示出來(lái)。

回想一下剛剛我們對(duì)于數(shù)據(jù)的處理,是不是只遍歷了外層?這就是因?yàn)?Vue 并沒(méi)有對(duì) b 進(jìn)行監(jiān)聽(tīng),他根本不知道你的 b 是如何變化的,自然也就不會(huì)去更新視圖層了。

寫(xiě)法 2:

const vm = new Vue({
  data: {
    obj: {
      a: 'a'
    }
  },
  methods: {
    changeObj() {
      this.obj.a = 'a2'
      this.obj.b = 'b';
    }
  }
})

我們僅僅只是新增了一行代碼,在改變 b 之前先改變了 a,居然就讓 b 實(shí)現(xiàn)了更新!

這是為什么?

因?yàn)橐晥D更新其實(shí)是異步的。

當(dāng)我們讓 a'a' 變成 'a2' 時(shí),Vue 會(huì)監(jiān)聽(tīng)到這個(gè)變化,但是 Vue 并不能馬上更新視圖,因?yàn)?Vue 是使用 Object.defineProperty() 這樣的方式來(lái)監(jiān)聽(tīng)變化的,監(jiān)聽(tīng)到變化后會(huì)創(chuàng)建一個(gè)視圖更新任務(wù)到任務(wù)隊(duì)列里。

所以在視圖更新之前,要先把余下的代碼運(yùn)行完才行,也就是會(huì)運(yùn)行 b = 'b'。

最后等到視圖更新的時(shí)候,由于 Vue 會(huì)去做 diff 算法,于是 Vue 就會(huì)發(fā)現(xiàn) a 和 b 都變了,自然會(huì)去更新相對(duì)應(yīng)的視圖。

但是這并不是我們解決問(wèn)題的辦法,寫(xiě)法 2 充其量只能算是“副作用”。

Vue 其實(shí)提供了方法讓我們來(lái)新增以前沒(méi)有生命的屬性:Vue.set() 或者 this.$set()。

Vue.set(this.obj, 'b', 'b'); 會(huì)代替我們進(jìn)行 obj.b = 'b';,然后監(jiān)聽(tīng) b 的變化,觸發(fā)視圖更新。

那數(shù)組怎么響應(yīng)呢?

每當(dāng)我們往數(shù)組里新增元素的時(shí)候,數(shù)組就在不斷的變長(zhǎng)。對(duì)于沒(méi)有聲明的數(shù)組下標(biāo),很明顯 Vue 不會(huì)給予監(jiān)聽(tīng)呀。

比如 a: [1, 2, 3],當(dāng)我新增一個(gè)元素,讓 a === [1, 2, 3, 4] 的時(shí)候,a[3] 是不會(huì)被監(jiān)聽(tīng)的。

總不能每次 push 數(shù)組,都要手寫(xiě)剛剛說(shuō)的 Vue.set 方法吧。

可實(shí)際操作中,我們發(fā)現(xiàn)并沒(méi)有呀,Vue 監(jiān)聽(tīng)了新增的數(shù)據(jù)。

這是因?yàn)?Vue 又偷偷的干了一件事兒,它把你原本的數(shù)組方法給改了一些。

  • push()

  • pop()

  • shift()

  • unshift()

  • splice()

  • sort()

  • reverse()

在 Vue 中的數(shù)組所帶的這七個(gè)方法都不是原生的方法了。Vue 考慮到這些操作極為常用,所在中間為我們添加了監(jiān)聽(tīng)。

講到這里,相信大家對(duì) Vue 的響應(yīng)式原理應(yīng)該有了更深的認(rèn)識(shí)了。

(完)

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多