實(shí)現(xiàn) Vue 的響應(yīng)式系統(tǒng)

前言

Vue 最獨(dú)特的特性之一,是其非侵入性的響應(yīng)式系統(tǒng)净蚤。比如我們修改了數(shù)據(jù)锥咸,那么依賴這些數(shù)據(jù)的視圖都會(huì)進(jìn)行更新,大大提高了我們的"搬磚"效率柬采,回想一下初學(xué) JS 的時(shí)候海量的 Dom操作.......欢唾,Vue 通過數(shù)據(jù)驅(qū)動(dòng)視圖,極大的將我們從繁瑣的DOM操作中解放出來粉捻。

如下圖礁遣,我們改變了 msg 的值,視圖也響應(yīng)式的進(jìn)行了更新

Vue 響應(yīng)式原理

我們先看 vue 官網(wǎng)的圖肩刃,其實(shí)不太清晰祟霍,我初看的時(shí)候也是一臉懵逼的.

再看下面這張圖,響應(yīng)式原理涵蓋在里面了(圖片來源于網(wǎng)絡(luò)):

梳理一下流程:

    1. Vue 初始化 => 劫持 data 設(shè)置 get盈包、set (攔截?cái)?shù)據(jù)讀寫)
    1. Compile 解析模板 => 生成 watcher => 讀取 data沸呐,觸發(fā) get 方法 => Dep 收集依賴(watcher)
    1. 數(shù)據(jù)變化 => 觸發(fā) set方法 => 通知 Dep 中的所有 watcher => 視圖更新

對(duì)于 Observer、Dep 和 Watcher 這三大金剛 呢燥,我初學(xué)的時(shí)候也是傻傻的分不清楚很懵崭添,我的理解是:

Dep(dependence) 即依賴收集器,收集 Watcher 即觀察者叛氨。

Watcher 即觀察者呼渣,觀察數(shù)據(jù),數(shù)據(jù)變化時(shí)更新對(duì)應(yīng)的視圖(dom)寞埠。

Observer 即劫持者屁置,通過 Object.defineProperty() 給數(shù)據(jù)設(shè)置 get 和 set 方法:

  • get: 當(dāng)某個(gè)地方用到數(shù)據(jù)時(shí),如下 h1畸裳、h2 標(biāo)簽都用到了 msg 數(shù)據(jù)缰犁,即觀察 msg 數(shù)據(jù) 的兩個(gè) watcher 將被放入 msg 數(shù)據(jù)的依賴收集器 Dep 中淳地。
data() {
  return {
    msg: 'hello vue',
  }
},

<h1>{{msg}}</h1>
<h2>{{msg}}</h2>
  • set:當(dāng) msg 數(shù)據(jù)改變的時(shí)候怖糊,遍歷 Dep 依賴收集器帅容,通知所有 Watcher 更新視圖,即更新 h1伍伤、h2 標(biāo)簽內(nèi)的文本內(nèi)容

實(shí)現(xiàn) Vue 的響應(yīng)式系統(tǒng)

通過上面分析并徘,可知每一個(gè)數(shù)據(jù)有一個(gè)依賴收集器 Dep,Dep 里面存放用到該數(shù)據(jù)的 Watcher扰魂,如下圖所示(圖片來源于網(wǎng)絡(luò)):

1. Dep

我們先實(shí)現(xiàn) Dep麦乞,Dep 我們可以用數(shù)組來模擬,它應(yīng)該有兩個(gè)方法:

  • add劝评,收集 Watcher
  • notify姐直,數(shù)據(jù)變化的時(shí)候通知 Watcher 更新視圖
# 依賴收集器
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(watcher) {
    # 添加觀察者
    this.subs.push(watcher);
  }
  notify() {
    # 通知每一個(gè)觀察者更新視圖
    this.subs.forEach(watcher => watcher.update());
  }
}

2. Watcher

Watcher 實(shí)現(xiàn)如下,其中 cb 是更新視圖的方法蒋畜,關(guān)鍵點(diǎn)在于 oldVal声畏,它有兩個(gè)用處:

  • Dep 觸發(fā) update 方法時(shí),比對(duì)新舊值姻成,若有變化才更新插龄,避免不必要的視圖更新
  • 初始化的時(shí)候,會(huì)獲取舊值科展,會(huì)觸發(fā)數(shù)據(jù)的 get 方法均牢,在此時(shí)可以把依賴注入到 Dep 中(即依賴收集)
# 觀察者,用于更新視圖
class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    # 視圖更新函數(shù)
    this.cb = cb;
    # 舊值
    this.oldVal = this.getOldVal();
  }
  getOldVal() {
    # 傳遞watch自己
    Dep.target = this;
    # 獲取值的時(shí)候會(huì)觸發(fā) get 方法才睹,把自己 push 進(jìn) deps[] 里
    const oldVal = compileUtils.getVal(this.expr, this.vm);
    Dep.target = null;
    return oldVal;
  }
  update() {
    # 獲取新值
    const newVal = compileUtils.getVal(this.expr, this.vm);
    if (newVal !== this.oldVal) {
      this.cb(newVal);
    }
  }
}

Dep.target = this 的用處是相當(dāng)于設(shè)置了一個(gè)全局變量讓 Dep 能收集到 watcher 自己徘跪,后面 Dep.target = null 用處是銷毀全局變量

3. Observer

Observer 實(shí)現(xiàn)如下,通過 Object.defineProperty 攔截?cái)?shù)據(jù)的讀寫操作:

  • get 收集依賴琅攘,注意判斷 Dep.target 是否有值真椿,因?yàn)槟0褰馕龅臅r(shí)候也會(huì)讀取數(shù)據(jù)觸發(fā) get 方法
  • set 通知依賴收集器,更新視圖
// 數(shù)據(jù)劫持
class Observer {
  constructor(data) {
    this.observer(data, key, data[key]);
  }
  observer(obj, key, value) {
    const dep = new Dep();
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: false,
      get() {
        # 防止視圖初始化的時(shí)候也被收集到Dep中
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      set: newVal => {
        this.observer(newVal);
        if (newVal !== value) {
          value = newVal;
          # 通知依賴收集器乎澄,有變化
          dep.notify();
        }
      },
    });
  }
}

4. Compile

到這里我們已經(jīng)實(shí)現(xiàn)了 Observer突硝、Dep 和 Watcher,實(shí)現(xiàn)了數(shù)據(jù)的響應(yīng)式追蹤置济,可是還有一個(gè)點(diǎn)沒打通解恰,那就是依賴收集,那么依賴什么時(shí)候收集呢浙于?換言之我們?cè)趺粗滥男?shù)據(jù)依賴了哪些視圖呢护盈?

在 Vue 解析模板的時(shí)候,實(shí)際上我們已經(jīng)知道了哪些 Dom 依賴了哪些數(shù)據(jù)羞酗,所以是在 compile 的時(shí)候完成了模板解析并完成了依賴收集腐宋。

Compile 實(shí)現(xiàn)如下,省略大部分 dom 操作相關(guān)代碼,可以用 DocumentFragment 文檔碎片提升性能胸竞,邏輯比較簡(jiǎn)單欺嗤,我們?cè)?dom 解析數(shù)據(jù)的時(shí)候生成了對(duì)應(yīng)的 watcher,并完成了依賴收集:

# 編譯類卫枝,輸出真實(shí)Dom
class Compile {
  constructor(el, vm) {
    this.el = this.isElementNode(el) ? el : document.querySelector(el);
    this.vm = vm;
    # 獲取文檔對(duì)象
    const fragment = this.nodeFragment(this.el);
    # 編譯
    this.compile(fragment);
    # 掛載回app
    this.el.appendChild(fragment);
  }
  # 是否元素節(jié)點(diǎn)
  isElementNode(node) {
    return node.nodeType === 1;
  }
  # 獲取文檔碎片
  nodeFragment(el) {
    # do something
  }
  compile(fragment) {
    const childNodes = fragment.childNodes;
    [...childNodes].forEach(node => {
      if (this.isElementNode(node)) {
        # 元素節(jié)點(diǎn)
        # do something
      } else {
        # 文本節(jié)點(diǎn)
        # do something
      }
    })
  }
}

# 根據(jù)不同指令 執(zhí)行不同的編譯操作
const compileUtils = {
  # v-text
  text(node, expr, vm) {
    const value = vm.$data[expr];
    # 創(chuàng)建觀察者 完成依賴收集
    new Watcher(vm, expr, newVal => {
      node.textContent = value;
    });
    node.textContent = value;
  },
};

至此一個(gè)響應(yīng)式的系統(tǒng)就已經(jīng)完了

雙向數(shù)據(jù)綁定

什么是雙向數(shù)據(jù)綁定

上面我們實(shí)現(xiàn)了響應(yīng)式的系統(tǒng)煎饼,但只是單向的,即數(shù)據(jù)驅(qū)動(dòng)視圖校赤,什么是雙向數(shù)據(jù)綁定呢吆玖?如下圖:


我們常見的 v-model, 就是雙向數(shù)據(jù)綁定马篮,其實(shí)它是一個(gè)語法糖:

<input v-model="msg" />

等價(jià)于 =>

<input :value="msg" @input="msg = $event.target.value" />

實(shí)現(xiàn)

雙向數(shù)據(jù)綁定即:

  • 數(shù)據(jù)改變 => 視圖更新
  • 視圖改變 => 數(shù)據(jù)改變 => 視圖更新

比如最簡(jiǎn)單的 input沾乘,我們只需要監(jiān)聽 input 事件,文本發(fā)生變化時(shí)更新數(shù)據(jù)浑测,觸發(fā)數(shù)據(jù)的 set 方法意鲸,通知所有的 watcher 更新視圖

我們?cè)谀0寰幾g的時(shí)候,給 dom 元素綁定相應(yīng)的事件尽爆,如 input 標(biāo)簽綁定 input 事件并指定更新數(shù)據(jù)的回調(diào)函數(shù):

const compileUtils = {
  # v-model
  model(node, expr, vm) {
    const value = vm.$data[expr];
    # 創(chuàng)建觀察者 完成依賴收集
    new Watcher(vm, expr, newVal => {
      node.value = value;
    });
    node.addEventListener('input', (e) => {
      # 更新數(shù)據(jù)怎顾,觸發(fā)數(shù)據(jù)的 set 方法
      vm.$data[expr] = newVal;
    });
    node.value = value;
  },
};

至此大功告成

源碼

源碼


END

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市漱贱,隨后出現(xiàn)的幾起案子槐雾,更是在濱河造成了極大的恐慌,老刑警劉巖幅狮,帶你破解...
    沈念sama閱讀 212,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件募强,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡崇摄,警方通過查閱死者的電腦和手機(jī)擎值,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逐抑,“玉大人鸠儿,你說我怎么就攤上這事〔薨保” “怎么了进每?”我有些...
    開封第一講書人閱讀 158,369評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)命斧。 經(jīng)常有香客問我田晚,道長(zhǎng),這世上最難降的妖魔是什么国葬? 我笑而不...
    開封第一講書人閱讀 56,799評(píng)論 1 285
  • 正文 為了忘掉前任贤徒,我火速辦了婚禮芹壕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘接奈。我一直安慰自己踢涌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,910評(píng)論 6 386
  • 文/花漫 我一把揭開白布鲫趁。 她就那樣靜靜地躺著斯嚎,像睡著了一般利虫。 火紅的嫁衣襯著肌膚如雪挨厚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,096評(píng)論 1 291
  • 那天糠惫,我揣著相機(jī)與錄音疫剃,去河邊找鬼。 笑死硼讽,一個(gè)胖子當(dāng)著我的面吹牛巢价,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播固阁,決...
    沈念sama閱讀 39,159評(píng)論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼壤躲,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了备燃?” 一聲冷哼從身側(cè)響起碉克,我...
    開封第一講書人閱讀 37,917評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎并齐,沒想到半個(gè)月后漏麦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,360評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡况褪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,673評(píng)論 2 327
  • 正文 我和宋清朗相戀三年撕贞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片测垛。...
    茶點(diǎn)故事閱讀 38,814評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡捏膨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出食侮,到底是詐尸還是另有隱情脊奋,我是刑警寧澤,帶...
    沈念sama閱讀 34,509評(píng)論 4 334
  • 正文 年R本政府宣布疙描,位于F島的核電站诚隙,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏起胰。R本人自食惡果不足惜久又,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,156評(píng)論 3 317
  • 文/蒙蒙 一巫延、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧地消,春花似錦炉峰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至半夷,卻和暖如春婆廊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背巫橄。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評(píng)論 1 267
  • 我被黑心中介騙來泰國(guó)打工淘邻, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人湘换。 一個(gè)月前我還...
    沈念sama閱讀 46,641評(píng)論 2 362
  • 正文 我出身青樓宾舅,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親彩倚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子筹我,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,728評(píng)論 2 351

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