溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

怎么實(shí)現(xiàn)Vue的響應(yīng)式

發(fā)布時(shí)間:2021-07-02 10:57:08 來(lái)源:億速云 閱讀:101 作者:小新 欄目:web開(kāi)發(fā)

這篇文章主要介紹了怎么實(shí)現(xiàn)Vue的響應(yīng)式,具有一定借鑒價(jià)值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

簡(jiǎn)易版

以watch為切入點(diǎn)

watch是平時(shí)開(kāi)發(fā)中使用率非常高的功能,其目的是觀測(cè)一個(gè)數(shù)據(jù),當(dāng)數(shù)據(jù)變化時(shí)執(zhí)行我們預(yù)先定義的回調(diào)。使用方式如下:

{
 watch: {
  obj(val, oldVal) {
   console.log(val, oldVal);
  }
 }
}

上面觀測(cè)了Vue實(shí)例的obj屬性,當(dāng)其值發(fā)生變化時(shí),打印出新值與舊值。

因此,我們定義一個(gè)watch函數(shù):

function watch (data, key, cb) {
 // do something
}
  1. watch函數(shù)接收3個(gè)屬性,分別是

  2. data: 被觀測(cè)對(duì)象 key: 被觀測(cè)的屬性

  3. cb: 數(shù)據(jù)變化后要執(zhí)行的回調(diào)

Object.defineProperty

既然要在數(shù)據(jù)變化后再執(zhí)行回調(diào),所以需要知道數(shù)據(jù)是什么時(shí)候被修改的,這就是Object.defineProperty的作用,其為數(shù)據(jù)定義了訪問(wèn)器屬性。在數(shù)據(jù)被讀取時(shí)會(huì)觸發(fā)get,在數(shù)據(jù)被修改時(shí)會(huì)觸發(fā)set。

我們定義一個(gè)defineReactive函數(shù),其用來(lái)將一個(gè)數(shù)據(jù)變成響應(yīng)式的:

function defineReactive(data, key) {
 let val = data[key];
 
 Object.defineProperty(data, key, {
  configurable: true,
  enumerable: true,
  get: function() {
   return val;
  },
  set: function(newVal) {
   if (newVal === val) {
    return;
   }
   
   val = newVal;
  }
 });
}

defineReactive函數(shù)為data對(duì)象的key屬性定義了get、set,get返回屬性key的值val,set中修改key的值為新值newVal。到目前為止,key屬性還是沒(méi)有什么特殊之處。

數(shù)據(jù)被修改會(huì)觸發(fā)set,那cb一定是在set中被執(zhí)行。但set與cb之間好像并沒(méi)有什么聯(lián)系,所以我們來(lái)搭建一座橋梁,來(lái)構(gòu)建兩者的聯(lián)系:

let target = null;

我們?cè)谌侄x了一個(gè)target變量,它用來(lái)保存cb的值,然后在set中調(diào)用。所以,cb什么時(shí)候被保存在target中?回到出發(fā)點(diǎn),我們要調(diào)用watch函數(shù)來(lái)觀測(cè)data的key屬性,當(dāng)值被修改時(shí)執(zhí)行我們定義的回調(diào)cb,這就是cb被保存在target中的時(shí)機(jī)了:

function watch(data, key, cb) {
 target = cb;
}

watch函數(shù)中target被修改了,但我要是再想調(diào)用watch函數(shù)一次,也就是說(shuō)我想在data[key]被修改時(shí),執(zhí)行兩個(gè)不同的回調(diào),又或者說(shuō),我想再觀測(cè)data的其它屬性,那該怎么辦?必須得在target被再次修改前,將其值保存到別處。因?yàn)?,target是同個(gè)屬性的不同回調(diào)或不同屬性的回調(diào)所共有的。

我們有必要為key屬性建立一個(gè)私有的倉(cāng)庫(kù),來(lái)保存回調(diào)。其實(shí)defineReactive函數(shù)有一點(diǎn)特殊地方:函數(shù)內(nèi)部定義了一個(gè)val變量,然后在get和set函數(shù)都使用了val變量,這形成一個(gè)閉包,defineReactive函數(shù)的作用域是key屬性私有的,這就是天然的私有倉(cāng)庫(kù)了:

function defineReactive(data, key) {
 let val = data[key];
 const dep = [];
 
 
 Object.defineProperty(data, key, {
  configurable: true,
  enumerable: true,
  get: function() {
   target && dep.push(target);
   
   return val;
  },
  set: function(newVal) {
   if (newVal === val) {
    return;
   }
   
   dep.forEach(fn => fn(newVal, val));
   
   val = newVal;
  }
 });
}

我們?cè)赿efineReactive函數(shù)內(nèi)定義了一個(gè)數(shù)組dep,其保存著每個(gè)屬性key的回調(diào)集合,也稱(chēng)為依賴(lài)集合。在get函數(shù)中將依賴(lài)收集到dep中,在set函數(shù)中循環(huán)dep執(zhí)行每一個(gè)依賴(lài)。總結(jié)起來(lái)就是:在get中收集依賴(lài),set中觸發(fā)依賴(lài)。

既然是在get中收集依賴(lài),那就要想辦法在tatget被修改時(shí)候觸發(fā)get,所以我們?cè)趙atch函數(shù)中讀取一下屬性key的值:

function watch(data, key, cb) {
 target = cb;
 data[key];
 target = null;
}

接下來(lái)我們測(cè)試下代碼:

怎么實(shí)現(xiàn)Vue的響應(yīng)式

完全ok!

依賴(lài)

回想簡(jiǎn)易版中,我們一共提到3個(gè)角色:defineReactive、dep、watch,三者其實(shí)各司其職,但我們把三者代碼耦合在了一起,不方便接下來(lái)擴(kuò)展與理解,所以我們來(lái)做一下歸類(lèi)。

Watcher

觀察者,也稱(chēng)為依賴(lài),它的職責(zé)就是訂閱一個(gè)數(shù)據(jù),當(dāng)數(shù)據(jù)發(fā)生變化時(shí),做些什么:

class Watcher {
 constructor(data, key, cb) {
  this.vm = data;
  this.key = key;
  this.cb = cb;
  this.value = this.get();
 }
 
 get() {
  Dep.target = this;
  const value = this.vm[this.key];
  Dep.target = null;
  
  return value;
 }
 
 update() {
  const oldValue = this.value;
  this.value = this.vm[this.key];
  
  this.cb.call(this.vm, this.value, oldVal);
 }
}

首先在構(gòu)造函數(shù)中讀取了屬性key的值,這會(huì)觸發(fā)屬性key的set,然后將自己作為依賴(lài)存入其dep數(shù)組中。當(dāng)然,在讀取屬性值之前,需要將自己賦值給橋梁Dep.target,這是get方法所做的事。最后是update方法,這是當(dāng)訂閱的數(shù)據(jù)發(fā)生變化后,需要被執(zhí)行的,其主要目的就是要執(zhí)行cb,因?yàn)閏d需要變化后的新值作為參數(shù),所以要再一次讀取屬性值。

Dep

Dep的職責(zé)就是構(gòu)建屬性key與依賴(lài)Watcher之間的聯(lián)系,其實(shí)例一定有一個(gè)獨(dú)一無(wú)二的屬于屬性key的依賴(lài)收集框:

class Dep {
 constructor() {
  this.subs = [];
 }
 
 addSub(sub) {
  this.subs.push(sub);
 }
 
 depend() {
  Dep.taget && this.addSub(Dep.target);
 }
 
 notify() {
  for (let sub of subs) {
   sub.update();
  }
 }
}

subs就是依賴(lài)收集框,當(dāng)屬性值被讀取時(shí),在depend方法中將依賴(lài)收入到框內(nèi);當(dāng)屬性值被修改時(shí),在notify方法中將依賴(lài)收集框遍歷,每一個(gè)依賴(lài)的update方法都將被執(zhí)行。

Observer

defineReactive函數(shù)只做了一件事,將數(shù)據(jù)轉(zhuǎn)換成響應(yīng)式的,我們定義一個(gè)Observer類(lèi)來(lái)聚合其功能:

class Observer {
 constructor(data, key) {
  this.value = data;
  
  defineReactive(data, key);
 }
}

function defineReactive(data, key) {
 let val = data[key];
 const dep = new Dep();
 
 Object.defineProperty(data, key, {
  configurable: true,
  enumerable: true,
  get: function() {
   dep.depend();
   
   return val;
  },
  set: function(newVal) {
   if (newVal === val) {
    return;
   }
   
   dep.notify();
   
   val = newVal;
  }
 });
}

dep不再是一個(gè)純粹的數(shù)組,而是一個(gè)Dep類(lèi)的實(shí)例。get函數(shù)中的依賴(lài)收集、set函數(shù)中的依賴(lài)觸發(fā)的邏輯,分別用dep.depend、dep.update替代,這讓defineReactive函數(shù)邏輯變得變得更加清晰。但是Observer類(lèi)只是在構(gòu)造函數(shù)中調(diào)用defineReactive函數(shù),沒(méi)起什么作用?這當(dāng)然都是為后面做鋪墊的!

測(cè)試一下代碼:

怎么實(shí)現(xiàn)Vue的響應(yīng)式

觀測(cè)所有屬性

到目前為止我們都只在針對(duì)一個(gè)屬性,而一個(gè)對(duì)象可能有n多個(gè)屬性,因此我們要對(duì)做下調(diào)整。

觀測(cè)一個(gè)對(duì)象的所有屬性

觀測(cè)一個(gè)屬性主要是要定義其訪問(wèn)器屬性,對(duì)于我們的代碼來(lái)說(shuō),就是要執(zhí)行defineReactive函數(shù),所以對(duì)Observer類(lèi)做下修改:

class Observer {
 constructor(data) {
  this.value = data;
  
  if (isPlainObject(data)) {
   this.walk(data);
  }
 }
 
 walk(value) {
  const keys = Object.keys(value);
  
  for (let key of keys) {
   defineReactive(value, key);
  }
 }
}

function isPlainObject(obj) {
 return ({}).toString.call(obj) === '[object Object]';
}

我們?cè)贠bserver類(lèi)中定義一個(gè)walk方法,其作用就是遍歷對(duì)象的所有屬性,然后在構(gòu)造函數(shù)中調(diào)用。調(diào)用的前提是對(duì)象是一個(gè)純對(duì)象,即對(duì)象是通過(guò)字面量或new Object()初始化的,因?yàn)橄馎rray、Function等也都是對(duì)象。

測(cè)試一下代碼:

怎么實(shí)現(xiàn)Vue的響應(yīng)式

深度觀測(cè)

我們只要對(duì)象是可以嵌套的,即一個(gè)對(duì)象的某個(gè)屬性值也可以是對(duì)象,我們的代碼目前還做不到這一點(diǎn)。其實(shí)也很簡(jiǎn)單,做一下遞歸遍歷的就好了:

class Observer {
 constructor(data) {
  this.value = data;
  
  if (isPlainObject(data)) {
   this.walk(data);
  }
 }
 
 walk(value) {
  const keys = Object.keys(value);
  
  for (let key of keys) {
   const val = value[key];
   
   if (isPlainObject(val)) {
    this.walk(val);
   }
   else {
    defineReactive(value, key);
   }
  }
 }
}

我們?cè)趙alk方法中做了判斷,如果key的屬性值val是個(gè)純對(duì)象,那就調(diào)用walk方法去遍歷其屬性值。既然是深度觀測(cè),那watcher類(lèi)中的key的用法也發(fā)生了變化,比如說(shuō):'a.b.c',那我們就要兼容這種嵌套key的寫(xiě)法:

class Watcher {
 constructor(data, path, cb) {
  this.vm = data;
  this.cb = cb;
  this.getter = parsePath(path);
  this.value = this.get();
 }
 
 get() {
  Dep.target = this;
  const value = this.getter.call(this.vm);
  Dep.target = null;
  
  return value;
 }
 
 update() {
  const oldValue = this.value;
  this.value = this.getter.call(this.vm, this.vm);

  this.cb.call(this.vm, this.value, oldValue);
 }
}

function parsePath(path) {
 if (/.$_/.test(path)) {
  return;
 }

 const segments = path.split('.');

 return function(obj) {
  for (let segment of segments) {
   obj = obj[segment]
  }

  return obj;
 }
}

Watcher類(lèi)實(shí)例新增了getter屬性,其值為parsePath函數(shù)的返回值,在parsePath函數(shù)中,返回的是一個(gè)匿名函數(shù),匿名函數(shù)接收一個(gè)參數(shù)obj,最后又將obj作為返回值返回,那么這里的重點(diǎn)是匿名函數(shù)對(duì)obj做了什么處理。

匿名函數(shù)內(nèi)只有一個(gè)for...of迭代,迭代對(duì)象為segments,segments是通過(guò)path對(duì)'.'分割得到的一個(gè)數(shù)組,比如path為'a.b.c',那么segments就為['a', 'b', 'c']。迭代內(nèi)只有一個(gè)語(yǔ)句,obj被賦值為obj的屬性值,這相當(dāng)于一層一層去讀取,比如說(shuō),obj初始值為:

obj = {
 a: {
  b: {
   c: 1
  }
 }
}

那么最后的結(jié)果為:

obj = 1

讀取屬性值的目的就是為了收集依賴(lài),比如我們要觀測(cè)obj.a.b.c,那么目的就達(dá)到了。 既然知道了getter是一個(gè)函數(shù),那么在get方法中執(zhí)行g(shù)etter,就可以獲取值了。

測(cè)試下代碼:

怎么實(shí)現(xiàn)Vue的響應(yīng)式

這里有個(gè)細(xì)節(jié),我們看Watcher類(lèi)的get方法:

get() {
 Dep.target = this;
 const value = this.getter.call(this.vm);
 Dep.target = null;
  
 return value;
}

在執(zhí)行this.getter函數(shù)的時(shí)候,Dep.target的值一直都是當(dāng)前依賴(lài),而this.getter函數(shù)中一層一層讀取屬性值,在這路徑之中的所有屬性其實(shí)都收集了當(dāng)前依賴(lài)。比如上面的例子來(lái)說(shuō),屬性'a.b.c'的依賴(lài),被收集到obj.a、obj.a.b、obj.a.b.c的dep中,那么修改obj.a或obj.b都是會(huì)觸發(fā)當(dāng)前依賴(lài)的:

怎么實(shí)現(xiàn)Vue的響應(yīng)式

避免重復(fù)收集依賴(lài)

觀測(cè)表達(dá)式

在Vue中,$watch方法的第一個(gè)參數(shù)是可以傳函數(shù)的:

this.$watch(() => {
 return this.a + this.b;
}, (val, oldVal) => {
 console.log(val, oldVal);
});

這種寫(xiě)法相當(dāng)于觀測(cè)一個(gè)表達(dá)式,類(lèi)似與Vue中computed,依賴(lài)會(huì)被收集到屬性a與屬性b的dep中,無(wú)論修改其中任一,只要表達(dá)式的值發(fā)生變化,依賴(lài)都將會(huì)觸發(fā)。

為了兼容函數(shù)的傳入,我們稍微修改下Watcher類(lèi):

class Watcher {
 constructor(data, pathOrFn, cb) {
  this.vm = data;
  this.cb = cb;
  this.getter = typeof pathOrFn === 'function' ? pathOrFn : parsePath(pathOrFn);
  this.value = this.get();
 }
 
 ...
 
 update() {
  const oldValue = this.value;
  this.value = this.get();

  this.cb.call(this.vm, this.value, oldValue);
 }
}

對(duì)于第二個(gè)參數(shù)pathOrFn,我們優(yōu)先判斷其本身是否已經(jīng)是函數(shù),是則直接賦值給this.getter,否則調(diào)用parsePath函數(shù)解析。在update方法中,再次調(diào)用了get方法來(lái)獲取被修改后的值。

測(cè)試下代碼:

怎么實(shí)現(xiàn)Vue的響應(yīng)式

結(jié)果好像有點(diǎn)不對(duì)?輸出了1949次!而且還在增加之中,一定是某個(gè)陷入無(wú)限循環(huán)了。仔細(xì)回看我們修改的點(diǎn),在update方法中,我們?cè)俅握{(diào)用了get方法,這又會(huì)觸發(fā)一次依賴(lài)的收集。然后我們?cè)贒ep類(lèi)的notify方法中遍歷依賴(lài)集合,每次觸發(fā)依賴(lài)都會(huì)導(dǎo)致依賴(lài)的再次收集,這就是個(gè)無(wú)限循環(huán)了!

發(fā)現(xiàn)了問(wèn)題,就來(lái)解決問(wèn)題。我們要對(duì)依賴(lài)做唯一性校驗(yàn):

let uid = 1;

class Watcher {
 constructor(data, pathOrFn) {
  this.id = uid++;
  ...
 }
}

class Dep() {
 construct() {
  this.subs = [];
  this.subIds = new Set();
 }
 ...
 addSub(sub) {
  const id = sub.id;
  
  if (!this.subIds.has(id)) {
   this.subs.push(sub);
   this.subIds.add(id);
  }
 }
 ...
}

既然要做唯一性校驗(yàn),我們給Watcher類(lèi)實(shí)例增加了獨(dú)一無(wú)二的id。在Dep類(lèi)中,我們給構(gòu)造函數(shù)里增加了屬性subIds,其初始值為空Set,作用是存儲(chǔ)依賴(lài)的id。然后在addSub方法中,在將依賴(lài)添加到subs之前,先判斷這個(gè)依賴(lài)的id是否已經(jīng)存在。

測(cè)試下代碼:

怎么實(shí)現(xiàn)Vue的響應(yīng)式

只輸出了一次,完全ok。

在Vue中的意義

防止依賴(lài)的重復(fù)收集,除了防止上面提到的陷入無(wú)限循環(huán),在Vue中還有更重要的意義,比如一下模板:

<template>
 <div>
  <p>{{ a }}</p>
  <p>{{ a }}</p>
  <p>{{ a }}</p>
 </div>
</template>

在Vue中,除了watch選項(xiàng)的依賴(lài),還有一個(gè)特殊依賴(lài)叫渲染函數(shù)的依賴(lài),其作用就是當(dāng)模板中的變量發(fā)生變化時(shí),更新VNode,重新生成DOM。在我們上面定義的模板中,一共使用a變量3次,當(dāng)a變量被修改,如果沒(méi)有防止重復(fù)依賴(lài)的收集,渲染函數(shù)就會(huì)被執(zhí)行3次!這是完全必要的!并且3次只是個(gè)例子,實(shí)際可能會(huì)更多!

感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“怎么實(shí)現(xiàn)Vue的響應(yīng)式”這篇文章對(duì)大家有幫助,同時(shí)也希望大家多多支持億速云,關(guān)注億速云行業(yè)資訊頻道,更多相關(guān)知識(shí)等著你來(lái)學(xué)習(xí)!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

vue
AI