国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > JavaScript > 正文

一步一步實現Vue的響應式(對象觀測)

2019-11-19 10:55:41
字體:
來源:轉載
供稿:網友

平時開發中,Vue的響應式系統讓我們不再去操作DOM,只需關心數據邏輯的處理,極大地降低了代碼的復雜度。而響應式系統也是Vue的核心,作為開發者有必要了解其實現原理!

簡易版

以watch為切入點

watch是平時開發中使用率非常高的功能,其目的是觀測一個數據,當數據變化時執行我們預先定義的回調。使用方式如下:

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

上面觀測了Vue實例的obj屬性,當其值發生變化時,打印出新值與舊值。

因此,我們定義一個watch函數:

function watch (data, key, cb) { // do something}
  1. watch函數接收3個屬性,分別是
  2. data: 被觀測對象 key: 被觀測的屬性
  3. cb: 數據變化后要執行的回調

Object.defineProperty

既然要在數據變化后再執行回調,所以需要知道數據是什么時候被修改的,這就是Object.defineProperty的作用,其為數據定義了訪問器屬性。在數據被讀取時會觸發get,在數據被修改時會觸發set。

我們定義一個defineReactive函數,其用來將一個數據變成響應式的:

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函數為data對象的key屬性定義了get、set,get返回屬性key的值val,set中修改key的值為新值newVal。到目前為止,key屬性還是沒有什么特殊之處。

數據被修改會觸發set,那cb一定是在set中被執行。但set與cb之間好像并沒有什么聯系,所以我們來搭建一座橋梁,來構建兩者的聯系:

let target = null;

我們在全局定義了一個target變量,它用來保存cb的值,然后在set中調用。所以,cb什么時候被保存在target中?回到出發點,我們要調用watch函數來觀測data的key屬性,當值被修改時執行我們定義的回調cb,這就是cb被保存在target中的時機了:

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

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

我們有必要為key屬性建立一個私有的倉庫,來保存回調。其實defineReactive函數有一點特殊地方:函數內部定義了一個val變量,然后在get和set函數都使用了val變量,這形成一個閉包,defineReactive函數的作用域是key屬性私有的,這就是天然的私有倉庫了:

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;  } });}

我們在defineReactive函數內定義了一個數組dep,其保存著每個屬性key的回調集合,也稱為依賴集合。在get函數中將依賴收集到dep中,在set函數中循環dep執行每一個依賴。總結起來就是:在get中收集依賴,set中觸發依賴。

既然是在get中收集依賴,那就要想辦法在tatget被修改時候觸發get,所以我們在watch函數中讀取一下屬性key的值:

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

接下來我們測試下代碼:

完全ok!

依賴

回想簡易版中,我們一共提到3個角色:defineReactive、dep、watch,三者其實各司其職,但我們把三者代碼耦合在了一起,不方便接下來擴展與理解,所以我們來做一下歸類。

Watcher

觀察者,也稱為依賴,它的職責就是訂閱一個數據,當數據發生變化時,做些什么:

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); }}

首先在構造函數中讀取了屬性key的值,這會觸發屬性key的set,然后將自己作為依賴存入其dep數組中。當然,在讀取屬性值之前,需要將自己賦值給橋梁Dep.target,這是get方法所做的事。最后是update方法,這是當訂閱的數據發生變化后,需要被執行的,其主要目的就是要執行cb,因為cd需要變化后的新值作為參數,所以要再一次讀取屬性值。

Dep

Dep的職責就是構建屬性key與依賴Watcher之間的聯系,其實例一定有一個獨一無二的屬于屬性key的依賴收集框:

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就是依賴收集框,當屬性值被讀取時,在depend方法中將依賴收入到框內;當屬性值被修改時,在notify方法中將依賴收集框遍歷,每一個依賴的update方法都將被執行。

Observer

defineReactive函數只做了一件事,將數據轉換成響應式的,我們定義一個Observer類來聚合其功能:

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不再是一個純粹的數組,而是一個Dep類的實例。get函數中的依賴收集、set函數中的依賴觸發的邏輯,分別用dep.depend、dep.update替代,這讓defineReactive函數邏輯變得變得更加清晰。但是Observer類只是在構造函數中調用defineReactive函數,沒起什么作用?這當然都是為后面做鋪墊的!

測試一下代碼:

觀測所有屬性

到目前為止我們都只在針對一個屬性,而一個對象可能有n多個屬性,因此我們要對做下調整。

觀測一個對象的所有屬性

觀測一個屬性主要是要定義其訪問器屬性,對于我們的代碼來說,就是要執行defineReactive函數,所以對Observer類做下修改:

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]';}

我們在Observer類中定義一個walk方法,其作用就是遍歷對象的所有屬性,然后在構造函數中調用。調用的前提是對象是一個純對象,即對象是通過字面量或new Object()初始化的,因為像Array、Function等也都是對象。

測試一下代碼:

深度觀測

我們只要對象是可以嵌套的,即一個對象的某個屬性值也可以是對象,我們的代碼目前還做不到這一點。其實也很簡單,做一下遞歸遍歷的就好了:

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);   }  } }}

我們在walk方法中做了判斷,如果key的屬性值val是個純對象,那就調用walk方法去遍歷其屬性值。既然是深度觀測,那watcher類中的key的用法也發生了變化,比如說:'a.b.c',那我們就要兼容這種嵌套key的寫法:

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類實例新增了getter屬性,其值為parsePath函數的返回值,在parsePath函數中,返回的是一個匿名函數,匿名函數接收一個參數obj,最后又將obj作為返回值返回,那么這里的重點是匿名函數對obj做了什么處理。

匿名函數內只有一個for...of迭代,迭代對象為segments,segments是通過path對'.'分割得到的一個數組,比如path為'a.b.c',那么segments就為['a', 'b', 'c']。迭代內只有一個語句,obj被賦值為obj的屬性值,這相當于一層一層去讀取,比如說,obj初始值為:

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

那么最后的結果為:

obj = 1

讀取屬性值的目的就是為了收集依賴,比如我們要觀測obj.a.b.c,那么目的就達到了。 既然知道了getter是一個函數,那么在get方法中執行getter,就可以獲取值了。

測試下代碼:

這里有個細節,我們看Watcher類的get方法:

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

在執行this.getter函數的時候,Dep.target的值一直都是當前依賴,而this.getter函數中一層一層讀取屬性值,在這路徑之中的所有屬性其實都收集了當前依賴。比如上面的例子來說,屬性'a.b.c'的依賴,被收集到obj.a、obj.a.b、obj.a.b.c的dep中,那么修改obj.a或obj.b都是會觸發當前依賴的:

避免重復收集依賴

觀測表達式

在Vue中,$watch方法的第一個參數是可以傳函數的:

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

這種寫法相當于觀測一個表達式,類似與Vue中computed,依賴會被收集到屬性a與屬性b的dep中,無論修改其中任一,只要表達式的值發生變化,依賴都將會觸發。

為了兼容函數的傳入,我們稍微修改下Watcher類:

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); }}

對于第二個參數pathOrFn,我們優先判斷其本身是否已經是函數,是則直接賦值給this.getter,否則調用parsePath函數解析。在update方法中,再次調用了get方法來獲取被修改后的值。

測試下代碼:

結果好像有點不對?輸出了1949次!而且還在增加之中,一定是某個陷入無限循環了。仔細回看我們修改的點,在update方法中,我們再次調用了get方法,這又會觸發一次依賴的收集。然后我們在Dep類的notify方法中遍歷依賴集合,每次觸發依賴都會導致依賴的再次收集,這就是個無限循環了!

發現了問題,就來解決問題。我們要對依賴做唯一性校驗:

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);  } } ...}

既然要做唯一性校驗,我們給Watcher類實例增加了獨一無二的id。在Dep類中,我們給構造函數里增加了屬性subIds,其初始值為空Set,作用是存儲依賴的id。然后在addSub方法中,在將依賴添加到subs之前,先判斷這個依賴的id是否已經存在。

測試下代碼:

只輸出了一次,完全ok。

在Vue中的意義

防止依賴的重復收集,除了防止上面提到的陷入無限循環,在Vue中還有更重要的意義,比如一下模板:

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

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

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 招远市| 桦南县| 吉林省| 若尔盖县| 绍兴市| 辽源市| 沧源| 咸宁市| 海原县| 临沂市| 遂溪县| 南城县| 兴业县| 开江县| 德保县| 吕梁市| 扎囊县| 山丹县| 石门县| 开远市| 达拉特旗| 陆良县| 沧州市| 济阳县| 富阳市| 五峰| 东乡县| 巴林左旗| 称多县| 建德市| 肃南| 紫金县| 固始县| 朝阳市| 乌审旗| 龙州县| 芒康县| 贡山| 邛崃市| 陆丰市| 新竹县|