詳解vue2 ,vue3響應(yīng)式原理

響應(yīng)式是什么

簡單解釋就是x有一個初始化的值汉匙,有一段代碼在別的地方使用了,當x發(fā)生變化時,在剛才使用x的地方能夠立馬發(fā)生改變或者重新執(zhí)行

簡單的響應(yīng)式實現(xiàn)

const obj = {
  name: 'jack',
  age: '18',
};
obj.name = 'mask';

在上面的代碼中来累,當name發(fā)生改變時没隘,在其他的地方使用了name的地方也要立馬發(fā)生改變懂扼,這里需要解決兩個問題,怎么知道哪里使用了右蒲,怎么通知更新阀湿,第一個問題我們稱之為收集依賴,第二個問題叫做派發(fā)更新瑰妄。

watchFn(function () {
  console.log(obj.name, 'name收集依賴----------');
});

這里通過watch函數(shù)來收集依賴陷嘴,解決的問題是怎么知道哪里使用了,類似于在vue中使用{{}}使用了name屬性

// 封裝一個響應(yīng)式的函數(shù)
let reactiveFns = []
function watchFn(fn) {
  reactiveFns.push(fn)
}

watch函數(shù)解決的收集依賴的問題间坐,這里將收集到的依賴放在數(shù)組中灾挨,在進行派發(fā)操作的時候只需要遍歷數(shù)組中的函數(shù),進行執(zhí)行即可

reactiveFns.forEach(fn => {
  fn()
})

完整代碼

let reactiveFns = [];
function watchFn(fn) {
  reactiveFns.push(fn);
}

const obj = {
  name: 'jack',
  age: '18',
};

watchFn(function () {
  console.log(obj.name, 'name收集依賴----------');
});

obj.name = 'mask';

reactiveFns.forEach((fn) => {
  fn();
});
結(jié)果

在上面的簡單例子中可以看出在下面修改的name屬性竹宋,上面使用的name地方也發(fā)生改變了劳澄,這就是響應(yīng)式最簡單的原理,如果能夠理解上面的例子蜈七,接下來就可以看完整的響應(yīng)式原理了

封裝收集依賴類

在上面例子中秒拔,收集依賴和派發(fā)更新時可以封裝在一個類中

class Depend {
  constructor() {
    this.reactiveFns = []  //收集依賴的地方
  }

  addDepend(reactiveFn) {
    this.reactiveFns.push(reactiveFn)//將依賴收集
  }

  notify() {
    this.reactiveFns.forEach(fn => {   //遍歷依賴,執(zhí)行依賴
      fn()
    })
  }
}

在使用的時候飒硅,可以直接new一下上面的depend類

const depend = new Depend()
function watchFn(fn) {
  depend.addDepend(fn)  //添加依賴
}

const obj = {
  name: 'jack',
  age: '18',
};

watchFn(function () {
  console.log(obj.name, 'name收集依賴----------');
});

obj.name = 'mask';
depend.notify() //派發(fā)更新操作

這樣在其他地方使用的時候可以簡化大量的代碼砂缩,
現(xiàn)在還有問題就是作谚,在進行派發(fā)更新的時候是手動進行派發(fā)的,如果再其他地方也需要進行派發(fā)更新的時候就會造成大量的代碼冗余庵芭,最好是能給一個能自動收集依賴的操作妹懒,畢竟能自動絕不手動,接下來就要實現(xiàn)一個自動派發(fā)更新的操作

自動監(jiān)聽對象的改變

在js中監(jiān)聽對象的改變可以通過set和get來監(jiān)聽對象的屬性變化双吆,在vue2中用的是Object.defineProperty,但是這個屬性只能監(jiān)聽對象屬性的修改和訪問眨唬,在進行修改和其他操作就不行了,vue3中用Proxy類來實現(xiàn)對象的監(jiān)聽伊诵,接下來先用Proxy來實現(xiàn)一下单绑,下面會用到Proxy和define.Property對比還有Reflectweakmap曹宴,和set在我的其他文章中有解釋搂橙,可以先去看一下

const objproxy = new Proxy(obj, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
  set(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    depend.notify();
  },
});

Proxy監(jiān)聽的整個對象,set在屬性改變時進行監(jiān)聽對象的屬性改變笛坦,這里直接將派發(fā)更新的操作放在set函數(shù)中就可以實現(xiàn)自動派發(fā)更新的操作了区转。基本上整個功能實現(xiàn)了一大步版扩,但是還有幾個問題沒有解決废离,這里的收集依賴都是放在一整個的數(shù)組中,只要有一個屬性的值發(fā)生了改變其他的依賴也會相應(yīng)的進行執(zhí)行礁芦,

class Depend {
  constructor() {
    this.reactiveFns = []; //收集依賴的地方
  }

  addDepend(reactiveFn) {
    this.reactiveFns.push(reactiveFn); //將依賴收集
  }

  notify() {
    this.reactiveFns.forEach((fn) => {
      //遍歷依賴蜻韭,執(zhí)行依賴
      fn();
    });
  }
}

// 封裝一個響應(yīng)式的函數(shù)
const depend = new Depend();
function watchFn(fn) {
  depend.addDepend(fn);
}

const obj = {
  name: 'jack',
  age: '18',
};

const objproxy = new Proxy(obj, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
  set(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver);
    depend.notify();
  },
});

watchFn(function () {
  console.log(objproxy.name, '   name收集依賴----------');
});
watchFn(function () {
  console.log(objproxy.age, '   age收集依賴----------');
});

objproxy.name = 'mask';

image.png

類似于這樣的在這樣的情況下,只改變name的值柿扣,但是age的依賴也發(fā)生變化了肖方,這不是想要的結(jié)果,想要的結(jié)果是在當前屬性發(fā)生變化的時候未状,收集的依賴只能是當前屬性的依賴俯画,派發(fā)的更新也是派發(fā)當前屬性的更新,所以這里需要解決這個問題司草,需要通過一個結(jié)構(gòu)來收集正確的依賴艰垂,派發(fā)正確的更新


依賴圖

這里用上面的結(jié)構(gòu)來收集正確的依賴,派發(fā)正確的更新
將所有的對象放在WeakMap中埋虹,單個對象放在Map中猜憎,每一個屬性的依賴放在一個dep對象中,最后在Map中實現(xiàn) obj:{key:[dep對象]}搔课,這樣的數(shù)據(jù)結(jié)構(gòu),用這種數(shù)據(jù)結(jié)構(gòu)的話拉宗,又有新的問題了,需要實現(xiàn)一個函數(shù)找到正確的依賴辣辫,并且返回Dep對象
接下來就是要先實現(xiàn)這個方法

//封裝一個收集依賴的getdepend
//收集依賴
//1.封裝一個weakmap收集所有的對象
//2.在從weakmap中獲取到需要的對象
//3.在用map收集所有的對象屬性添加依賴
const targetMap = new WeakMap();//第一步
function getDepend(target, key) {  //傳入的對象和key的值
  let map = targetMap.get(target); //第二步
  if (!map) {   //沒有這個對象的map類的話旦事,先創(chuàng)建一個
    map = new Map();
    targetMap.set(target, map);  //根據(jù)target和key設(shè)置對象的map
  }
  //根據(jù)key去取出相對應(yīng)的依賴
  let depend = map.get(key);   
  if (!depend) {    //沒有depend類先創(chuàng)建dep類,在設(shè)置(key, depend);
    depend = new Depend();
    map.set(key, depend);
  }
  return depend;  //返回找到的key值的dep類
}

現(xiàn)在就可以在進行派發(fā)更新之前的先獲取到depend急灭,再更據(jù)depend進行派發(fā)正確的更新了姐浮,現(xiàn)在這里完成了正確的派發(fā)更新操作,還有一個問題是怎么收集正確的依賴
首先這里實現(xiàn)的watch函數(shù)

const depend = new Depend();
function watchFn(fn) {
  depend.addDepend(fn);
}

在watch函數(shù)中是傳入了相應(yīng)的依賴,現(xiàn)在的問題是怎么把正確的依賴放到相應(yīng)的key值上葬馋,解決的辦法就是在獲取值的時候即get操作的時候卖鲤,先找到正確的dep類,在將依賴放入到依賴數(shù)組中,但是又出現(xiàn)了一個新的問題畴嘶,在Proxy怎么添加watch函數(shù)中傳入的依賴蛋逾,這里需要對watch函數(shù)進行重構(gòu)

// 封裝一個響應(yīng)式的函數(shù)
let activeReactiveFn = null;  //用一個全局變量表示將要收集的依賴
function watchFn(fn) {
 //將watch函數(shù)中傳入的依賴賦值給全局變量,這樣的話在Proxy類中就可以訪問到要添加的依賴了
  activeReactiveFn = fn; 
  fn();  //執(zhí)行依賴
  activeReactiveFn = null; //用完之后拋棄窗悯,防止對下一個依賴造成干擾
}
const objproxy = new Proxy(obj, {
  get(target, key, receiver) {
    const depend = getDepend(target, key); //找到當前屬性的dep類
    depend.addDepend(activeReactiveFn);  //收集依賴
    return Reflect.get(target, key, receiver);
  },
  set(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver);
    const depend = getDepend(target, key); //找到當前屬性的dep類
    depend.notify();  //派發(fā)正確的依賴
  },
});

測試收集正確的依賴区匣,派發(fā)正確的依賴

watchFn(function () {
  console.log(objproxy.name, '   name收集依賴----------');
});
watchFn(function () {
  console.log(objproxy.age, '   age收集依賴----------');
});
objproxy.name = 'mask';

結(jié)果

在name屬性改變之后只有name的依賴進行了執(zhí)行,前面兩句執(zhí)行了是因為在watch函數(shù)執(zhí)行了收集的依賴蒋院。 上面在收集正確的依賴和派發(fā)正確的依賴這里會有一點難理解亏钩,總的來說就是在get時找到正確的對象屬性,即name收集自己的依賴欺旧,age收集自己的依賴姑丑,然后在set的時候先找到正確的屬性,再派發(fā)正確的更新辞友,建議自己動手實踐一下栅哀,

class Depend {
  constructor() {
    this.reactiveFns = []; //收集依賴的地方
  }

  addDepend(reactiveFn) {
    this.reactiveFns.push(reactiveFn); //將依賴收集
  }

  notify() {
    this.reactiveFns.forEach((fn) => {
      //遍歷依賴,執(zhí)行依賴
      fn();
    });
  }
}

// 封裝一個響應(yīng)式的函數(shù)
//const depend = new Depend();
let activeReactiveFn = null;
function watchFn(fn) {
  //depend.addDepend(fn);
  activeReactiveFn = fn;
  fn();
  activeReactiveFn = null;
}

const obj = {
  name: 'jack',
  age: '18',
};

const targetMap = new WeakMap();
function getDepend(target, key) {
  let map = targetMap.get(target);
  if (!map) {
    map = new Map();
    targetMap.set(target, map);
  }

  //根據(jù)key去取出相對應(yīng)的依賴
  let depend = map.get(key);
  if (!depend) {
    depend = new Depend();
    map.set(key, depend);
  }
  return depend;
}

const objproxy = new Proxy(obj, {
  get(target, key, receiver) {
    const depend = getDepend(target, key);
    depend.addDepend(activeReactiveFn); //收集依賴
    return Reflect.get(target, key, receiver);
  },
  set(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver);
    const depend = getDepend(target, key); //找到對應(yīng)的dep類
    depend.notify();
  },
});

watchFn(function () {
  console.log(objproxy.name, '   name收集依賴----------');
});
watchFn(function () {
  console.log(objproxy.age, '   age收集依賴----------');
});
objproxy.name = 'mask';

優(yōu)化

如果上面的沒有看懂的話称龙,先把上面的理解好在來看接下來的代碼
上面的代碼存在的問題

  • 在一個依賴中重復(fù)的使用屬性會有重復(fù)的更新操作
  • 在set中添加依賴的時候可以不關(guān)心依賴(選擇重構(gòu))
  • 只能監(jiān)聽單個的對象
    1.解決這一問題其實是非常簡單的留拾,首先這里是將所有的依賴放入到數(shù)組中的,數(shù)組是可以允許重復(fù)的茵瀑,現(xiàn)在只要將數(shù)組中的數(shù)據(jù)進行去重既可,或者將數(shù)據(jù)放入到集合中间驮,這里用集合解決這個問題
 this.reactiveFns = new Set()

2.第二個問題是選擇優(yōu)化的,如果不想將依賴放入到Proxy對象中马昨,可以在dep類中收集依賴的時候?qū)⒁蕾嚪胚M去,重構(gòu)收集依賴的操作

 depend() {
    if (activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn)
    }
  }
  1. 在vue3中使用reactive將對象的轉(zhuǎn)化為相應(yīng)式竞帽,這里也用這種方法
function reactive(obj) {
  return new Proxy(obj, {
    get: function(target, key, receiver) {
      // 根據(jù)target.key獲取對應(yīng)的depend
      const depend = getDepend(target, key)
      // 給depend對象中添加響應(yīng)函數(shù)
      // depend.addDepend(activeReactiveFn)
      depend.depend()
  
      return Reflect.get(target, key, receiver)
    },
    set: function(target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)
      const depend = getDepend(target, key)
      depend.notify()
    }
  })
}

其實很簡單,傳入過來一個對象鸿捧,處理完之后在返回這個對象
最終代碼

let activeReactiveFn = null; //全局變量提升
class Depend {
  constructor() {
    //優(yōu)化一:將數(shù)組改為集合
    this.reactiveFns = new Set(); //  收集依賴的地方
  }

  // addDepend(reactiveFn) {
  //   this.reactiveFns.add(reactiveFn); //將依賴收集
  // }
  //優(yōu)化二:重構(gòu)收集依賴
  depend() {
    if (activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn);
    }
  }

  notify() {
    this.reactiveFns.forEach((fn) => {
      //遍歷依賴屹篓,執(zhí)行依賴
      fn();
    });
  }
}

// 封裝一個響應(yīng)式的函數(shù)
//const depend = new Depend();

function watchFn(fn) {
  //depend.addDepend(fn);
  activeReactiveFn = fn;
  fn();
  activeReactiveFn = null;
}

const targetMap = new WeakMap();
function getDepend(target, key) {
  let map = targetMap.get(target);
  if (!map) {
    map = new Map();
    targetMap.set(target, map);
  }

  //根據(jù)key去取出相對應(yīng)的依賴
  let depend = map.get(key);
  if (!depend) {
    depend = new Depend();
    map.set(key, depend);
  }
  return depend;
}

//優(yōu)化三:reactive函數(shù)
function reactive(obj) {
  return new Proxy(obj, {
    get: function (target, key, receiver) {
      // 根據(jù)target.key獲取對應(yīng)的depend
      const depend = getDepend(target, key);
      // 給depend對象中添加響應(yīng)函數(shù)
      // depend.addDepend(activeReactiveFn)
      //優(yōu)化后
      depend.depend();

      return Reflect.get(target, key, receiver);
    },
    set: function (target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver);
      const depend = getDepend(target, key);
      depend.notify();
    },
  });
}

let obj = {
  name: 'jack',
  age: '18',
};

obj = reactive(obj);

watchFn(function () {
  console.log(obj.name, '   name收集依賴----------');
});
watchFn(function () {
  console.log(obj.age, '   age收集依賴----------');
});
obj.name = 'mask';

上面解釋了vue3的響應(yīng)式原理,在vue2也是一樣的匙奴,唯一的區(qū)別就是Proxy換成Object.defineProperty,由于Proxy對對象的操作性更高堆巧,Object.defineProperty只能監(jiān)聽對象屬性的改變,在刪除或者其他的操作都需要進行其他的特殊處理,比較麻煩谍肤,所以vue3才升級為Proxy

function reactive(obj) {
  // ES6之前, 使用Object.defineProperty
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get: function() {
        const depend = getDepend(obj, key)
        depend.depend()
        return value
      },
      set: function(newValue) {
        value = newValue
        const depend = getDepend(obj, key)
        depend.notify()
      }
    })
  })
  return obj
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末啦租,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子荒揣,更是在濱河造成了極大的恐慌篷角,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件系任,死亡現(xiàn)場離奇詭異恳蹲,居然都是意外死亡,警方通過查閱死者的電腦和手機俩滥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門嘉蕾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人霜旧,你說我怎么就攤上這事错忱。” “怎么了颁糟?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵航背,是天一觀的道長。 經(jīng)常有香客問我棱貌,道長玖媚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任婚脱,我火速辦了婚禮今魔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘障贸。我一直安慰自己错森,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布篮洁。 她就那樣靜靜地躺著涩维,像睡著了一般。 火紅的嫁衣襯著肌膚如雪袁波。 梳的紋絲不亂的頭發(fā)上瓦阐,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音篷牌,去河邊找鬼睡蟋。 笑死,一個胖子當著我的面吹牛枷颊,可吹牛的內(nèi)容都是我干的戳杀。 我是一名探鬼主播该面,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼信卡!你這毒婦竟也來了隔缀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤坐求,失蹤者是張志新(化名)和其女友劉穎蚕泽,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體桥嗤,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年仔蝌,在試婚紗的時候發(fā)現(xiàn)自己被綠了泛领。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡敛惊,死狀恐怖渊鞋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瞧挤,我是刑警寧澤锡宋,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站特恬,受9級特大地震影響执俩,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜癌刽,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一役首、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧显拜,春花似錦衡奥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至譬淳,卻和暖如春档址,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瘦赫。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工辰晕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人确虱。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓含友,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子窘问,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

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