Vue—關(guān)于響應(yīng)式(一滥朱、依賴收集原理分析)

一根暑、什么是響應(yīng)式?

在了解什么是響應(yīng)式之前我們現(xiàn)來看一段代碼演示

let x;
let y;
let f = n => n * 100

x = 1;
y = f(x);
console.log(y); // 100

x = 2;
y = f(x);
console.log(y); // 200

x = 3;
y = f(x);
console.log(y); // 300

代碼示例中徙邻,變量y依賴變量x進(jìn)行求值排嫌,但是我們會(huì)發(fā)現(xiàn)每一次變量x重新賦值時(shí)都要手動(dòng)對(duì)y進(jìn)行求值,存在大量的重復(fù)模板缰犁,因此淳地,指導(dǎo)我們進(jìn)行程序設(shè)計(jì)的DRY原則就發(fā)揮價(jià)值了

DRY 全稱:Don't Repeat Yourself (摘自wikipedia)怖糊,是指編程過程中不寫重復(fù)代碼,將能夠公共的部分抽象出來颇象,封裝成工具類或者用“abstraction”類來抽象公有的東西蓬抄,降低代碼的耦合性,這樣不僅提高代碼的靈活性夯到、健壯性以及可讀性嚷缭,也方便后期的維護(hù)或者修改。

那么我們需要有一個(gè)方法耍贾,實(shí)現(xiàn)自動(dòng)監(jiān)聽x的變化并且自動(dòng)對(duì)y進(jìn)行求值阅爽,以減少重復(fù)代碼

假設(shè)我們有一個(gè)onXChange函數(shù),使得每次x重新賦值時(shí)都會(huì)觸發(fā)onXChange中的回調(diào)函數(shù)荐开,你的代碼看起來應(yīng)該像下面這樣:

let x;
let y;
let onXChange = function(cb) {
  // ...
}

onXChange(() => {
  y = f(x);
  console.log(y);
})

x = 1; // 100
x = 2; // 200
x = 3; // 300

如果將y換成dom模板付翁,根據(jù)x的變化自動(dòng)渲染不同的模板也是同理。

現(xiàn)在我們可以來解釋什么是響應(yīng)式了(其實(shí)都不用我解釋晃听,看到這你自己也有答案了)百侧,響應(yīng)式只是一種編程方式,它的目的是為了簡(jiǎn)化編程能扒,特點(diǎn)是自動(dòng)對(duì)變化進(jìn)行響應(yīng)佣渴。

以下是摘自wikipedia的解釋:

響應(yīng)式(Reactive Programming)

是一種面向數(shù)據(jù)流和變化傳播的編程范式。這意味著可以在編程語(yǔ)言中很方便的表達(dá)靜態(tài)或動(dòng)態(tài)的數(shù)據(jù)流初斑,而相關(guān)的計(jì)算模型會(huì)自動(dòng)將變化的值通過數(shù)據(jù)流進(jìn)行傳播辛润。

二、Vue中的響應(yīng)式分析

我們來看看Vue中是怎么實(shí)現(xiàn)響應(yīng)式的

首先第一步需要監(jiān)聽數(shù)據(jù)變化见秤,知道變量什么時(shí)候進(jìn)行了修改砂竖,JS提供的能夠監(jiān)聽數(shù)據(jù)變化的API有Object.defineProperty以及ES6新增的Proxy,本節(jié)我們只探討Object.defineProperty

關(guān)于Object.defineProperty的使用如果你還不了解的話請(qǐng)閱讀如下文檔:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

Vue2.0中使用了Object.defineProperty來遍歷data中的數(shù)據(jù)鹃答,在getter中將使用到這個(gè)數(shù)據(jù)的上下文進(jìn)行收集乎澄,這個(gè)過程稱之為【依賴收集】,在setter中修改這個(gè)數(shù)據(jù)時(shí)則會(huì)觸發(fā)【通知依賴更新】的操作测摔,如下圖所示:

1628664097.png

什么是依賴收集置济?

所謂依賴收集,就是把一個(gè)數(shù)據(jù)用到的地方收集起來避咆,在這個(gè)數(shù)據(jù)發(fā)生改變的時(shí)候舟肉,統(tǒng)一去通知各個(gè)地方做對(duì)應(yīng)的操作。

為什么這里需要依賴收集查库?

考慮到一個(gè)變量的修改可能會(huì)引起多處變化路媚,因此需要將依賴這個(gè)變量的所有地方都收集起來,等到變量更新時(shí)再進(jìn)行批量操作樊销。

Vue關(guān)于響應(yīng)式原理的介紹官網(wǎng)已經(jīng)說的很清楚整慎,這里貼出鏈接不再贅述:

https://cn.vuejs.org/v2/guide/reactivity.html#ad

三脏款、實(shí)現(xiàn)一個(gè)簡(jiǎn)單的數(shù)據(jù)響應(yīng)

以上面的代碼為例,我們需要通過onXChange函數(shù)來監(jiān)聽x的修改裤园,由于上面代碼中x的值是基礎(chǔ)類型撤师,我們需要將x更改為引用類型才可以使用Object.defineProperty,因此我們可以創(chuàng)建一個(gè)函數(shù)來做這件事情拧揽,假設(shè)這個(gè)函數(shù)為ref

ref函數(shù)接收一個(gè)初始值剃盾,函數(shù)內(nèi)閉包一個(gè)value變量賦值為傳入的初始值,通過Object.defineProperty返回一個(gè)帶有value屬性的對(duì)象淤袜,在get中返回value痒谴,并在set中將value賦值,這樣在我們修改了x.value之后會(huì)自動(dòng)觸發(fā)set來更新閉包的value變量

let ref = initValue => {
  let value = initValue;

  return Object.defineProperty({}, 'value', {
    get() {
      return value;
    },
    set(newValue) {
      value = newValue;
    }
  })
}

對(duì)應(yīng)的代碼也需要修改一下:

  • 修改x為ref函數(shù)調(diào)用的返回值
  • 將對(duì)應(yīng)的x賦值更改為x.value賦值
let x;
let y;
let f = n => n * 100

let onXChange = function(cb) {
  // ...
}

let ref = initValue => {
  let value = initValue;

  return Object.defineProperty({}, 'value', {
    get() {
      return value;
    },
    set(newValue) {
      value = newValue;
    }
  })
}

// 創(chuàng)建x對(duì)象铡羡,初始value傳入1
x = ref(1);

// 監(jiān)聽x
onXChange(() => {
  y = f(x.value);
  console.log(y);
})

x.value = 2;
x.value = 3;

到這一步已經(jīng)可以自動(dòng)獲取到x.value改變后的值了积蔚,我們可以在set方法中打印newValue

set(newValue) {
  console.log('x: ', newValue)
  value = newValue;
}
image.png

既然已經(jīng)監(jiān)聽到x.value的修改了,接下來我們只需要拿到onXChange中的回調(diào)函數(shù)烦周,在set方法中調(diào)用它就可以同步修改y的值

怎么拿這個(gè)回調(diào)函數(shù)尽爆?

我們可以創(chuàng)建一個(gè)變量將這個(gè)回調(diào)函數(shù)存起來,假設(shè)變量名為active读慎,然后在set方法中調(diào)用active函數(shù)即可

// 省略部分代碼...

// 創(chuàng)建active變量
let active;
let onXChange = function(cb) {
  // 將回調(diào)賦值給active
  active = cb;
}

let ref = initValue => {
  let value = initValue;

  return Object.defineProperty({}, 'value', {
    get() {
      return value;
    },
    set(newValue) {
      value = newValue;
      active(); // 調(diào)用active函數(shù)
    }
  })
}
image.png

可以看到y(tǒng)的打印結(jié)果出來了漱贱,但少了x.value初始為1時(shí)的結(jié)果,我們還需要在初始的時(shí)候調(diào)用一次active

// 執(zhí)行onXChange時(shí)就調(diào)用一次回調(diào)函數(shù)
let onXChange = function(cb) {
  active = cb;
  active();
  active = null; // 銷毀active贪壳,避免修改x.value時(shí)重復(fù)添加依賴
}
image.png

四饱亿、結(jié)合Vue源碼來看響應(yīng)式

到這里一個(gè)簡(jiǎn)單的響應(yīng)式其實(shí)已經(jīng)完成了,但還不夠闰靴,如果我們不僅有onXChange,還有onYChange钻注、onZChange呢蚂且,這些函數(shù)都依賴了x變量怎么辦?

Vue的解決辦法是通過一個(gè)Dep對(duì)象將這些依賴都收集起來幅恋,在變量發(fā)生改變時(shí)進(jìn)行批量通知更新杏死。

那么Dep對(duì)象至少應(yīng)該具有一個(gè)存儲(chǔ)依賴的列表、一個(gè)添加依賴的方法和一個(gè)通知依賴更新的方法

我們先來簡(jiǎn)單實(shí)現(xiàn)一下捆交,再結(jié)合Vue源碼驗(yàn)證

Dep代碼如下:

class Dep {
  deps = new Set();

  // 收集依賴
  depend() {
    if (active) {
      this.deps.add(active);
    }
  }

  // 批量更新
  // 將所有的依賴都執(zhí)行一遍
  notify() {
    this.deps.forEach(dep => dep());
  }
}

然后我們需要在ref函數(shù)中獲取dep實(shí)例淑翼,在get時(shí)調(diào)用dep.depend()添加依賴,在set時(shí)調(diào)用dep.notify來通知依賴更新

let ref = (initValue) => {
  let value = initValue;
  // 獲取dep實(shí)例
  let dep = new Dep();

  return Object.defineProperty({}, "value", {
    get() {
      // 添加依賴
      dep.depend();
      return value;
    },
    set(newValue) {
      value = newValue;
      // 通知依賴更新
      dep.notify();
    },
  });
};

現(xiàn)在來驗(yàn)證一下品追,我們添加任意個(gè)依賴x變量的函數(shù):

let onYChange = function(cb) {
  active = cb;
  active();
}

onYChange(() => {
  console.log('onYChange', f(x.value));
})

// ......
image.png

可以看到所有依賴x變量的地方都打印了結(jié)果玄括,一切都沒有問題。

那么Vue的源碼是不是這么實(shí)現(xiàn)的呢肉瓦?

以vue2.6.11版本為例:

defineReactive$$1函數(shù)的作用就是通過Object.defineProperty來將普通的數(shù)據(jù)處理成響應(yīng)式數(shù)據(jù)遭京,完整代碼如下:

function defineReactive$$1 (
    obj,
    key,
    val,
    customSetter,
    shallow
  ) {
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
      return
    }

    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
      val = obj[key];
    }

    var childOb = !shallow && observe(val);
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
    });
  }

去除源碼中影響閱讀的代碼:

function defineReactive$$1 (
    obj,
    key,
    val,
  ) {
    var dep = new Dep();
    
    // 省略部分代碼...
    
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        if (Dep.target) {
          dep.depend();
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        val = newVal;
        dep.notify();
      }
    });
  }

與我們自己實(shí)現(xiàn)的ref函數(shù)對(duì)比一下胃惜,你Get到了嗎?

再看源碼中的Dep是怎么實(shí)現(xiàn)的:

  var Dep = function Dep () {
    this.id = uid++;
    this.subs = [];
  };

  Dep.prototype.addSub = function addSub (sub) {
    this.subs.push(sub);
  };

  Dep.prototype.removeSub = function removeSub (sub) {
    remove(this.subs, sub);
  };

  Dep.prototype.depend = function depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  };

  Dep.prototype.notify = function notify () {
    // stabilize the subscriber list first
    var subs = this.subs.slice();
    if (!config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort(function (a, b) { return a.id - b.id; });
    }
    for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  };

先不用管別的代碼哪雕,至少我們?cè)贒ep源碼中找到了一個(gè)存儲(chǔ)依賴的列表subs船殉、添加依賴的方法depend、通知依賴更新的方法notify斯嚎,看到這里我想你對(duì)Vue的響應(yīng)式原理已經(jīng)有自己的理解了利虫。

那么我們?cè)賮砜偨Y(jié)一下Vue的響應(yīng)式原理:

  1. 將data中的數(shù)據(jù)通過Object.defineProperty處理成響應(yīng)式數(shù)據(jù)
  2. 數(shù)據(jù)被【讀】的時(shí)候會(huì)觸發(fā)getter,將使用到這個(gè)數(shù)據(jù)的上下文進(jìn)行依賴收集堡僻,存放到Dep類中
  3. 數(shù)據(jù)被【寫】的時(shí)候會(huì)觸發(fā)setter糠惫,調(diào)用Dep.notify方法通知依賴更新

不對(duì)的地方請(qǐng)指正,但不要批評(píng)我苦始,不聽哈哈哈寞钥!以上演示代碼已上傳github:

https://github.com/Mr-Jemp/VueStudy/blob/main/vue-reactive-demo/src/assets/js/demo2.js

后面要學(xué)習(xí)的內(nèi)容在這里:

Vue—關(guān)于響應(yīng)式(二、異步更新隊(duì)列原理分析)

Vue—關(guān)于響應(yīng)式(三陌选、Diff Patch原理分析)

Vue—關(guān)于響應(yīng)式(四理郑、深入學(xué)習(xí)Vue響應(yīng)式源碼)

本文由博客一文多發(fā)平臺(tái) OpenWrite 發(fā)布!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末咨油,一起剝皮案震驚了整個(gè)濱河市您炉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌役电,老刑警劉巖赚爵,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異法瑟,居然都是意外死亡冀膝,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門霎挟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來窝剖,“玉大人,你說我怎么就攤上這事酥夭〈蜕矗” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵熬北,是天一觀的道長(zhǎng)疙描。 經(jīng)常有香客問我,道長(zhǎng)讶隐,這世上最難降的妖魔是什么起胰? 我笑而不...
    開封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮整份,結(jié)果婚禮上待错,老公的妹妹穿的比我還像新娘籽孙。我一直安慰自己,他們只是感情好火俄,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開白布犯建。 她就那樣靜靜地躺著,像睡著了一般瓜客。 火紅的嫁衣襯著肌膚如雪适瓦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天谱仪,我揣著相機(jī)與錄音玻熙,去河邊找鬼。 笑死疯攒,一個(gè)胖子當(dāng)著我的面吹牛嗦随,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播敬尺,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼枚尼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了砂吞?” 一聲冷哼從身側(cè)響起署恍,我...
    開封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蜻直,沒想到半個(gè)月后盯质,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡概而,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年呼巷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赎瑰。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡朵逝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出乡范,到底是詐尸還是另有隱情,我是刑警寧澤啤咽,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布晋辆,位于F島的核電站,受9級(jí)特大地震影響宇整,放射性物質(zhì)發(fā)生泄漏瓶佳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一鳞青、第九天 我趴在偏房一處隱蔽的房頂上張望霸饲。 院中可真熱鬧为朋,春花似錦、人聲如沸厚脉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)傻工。三九已至霞溪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間中捆,已是汗流浹背鸯匹。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留泄伪,地道東北人殴蓬。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像蟋滴,于是被迫代替她去往敵國(guó)和親染厅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

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