整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          JavaScript遍歷:掌握for,forEach、for in、for of和map等方法

          .for循環

          for循環是一種常用的遍歷方法,特別適用于已知遍歷次數的情況。它由三個部分組成:初始化表達式、循環條件和循環迭代器。這三個表達式用分號分隔。可以使用臨時變量將數組的長度緩存起來,避免重復獲取數組長度,當數組較大時優化效果會比較明顯。

          const array = [1,2,3,4,5];
          for(let i = 0, len = array.length; i < len; i++ ){ //(初始化表達式; 循環條件; 循環迭代器)
              console.log(array[i]);
          }

          for循環非常靈活,你可以根據需要自定義循環變量的初始值、循環條件和迭代方式。它適用于各種遍歷需求,包括遍歷數組、對象的屬性等。 2.forEach方法 forEach方法是JavaScript數組對象的一個內置方法,用于遍歷數組的每個元素并執行指定的回調函數。以下是forEach方法的基本語法:

          array.forEach(function(element, index, array) {});

          forEach方法中,我們傳入一個回調函數作為參數。該回調函數接受三個參數:當前元素的值element、當前元素的索引index和正在遍歷的數組array

          下面是一個使用forEach方法遍歷數組的示例:

          array.forEach(function(element, index) {// 對每個元素執行操作
              console.log(element);
          });

          需要注意的是:

          • forEach方法不會改變原數組,也沒有返回值;
          • forEach方法無法在遍歷過程中中止或跳出循環。如果你需要在遍歷過程中進行條件判斷或中斷循環,可以考慮使用其他遍歷方法,如for循環或for...of循環,使用 return 時,效果和在 for 循環中使用 continue 一致;
          • forEach方法無法遍歷對象,僅適用于數組的遍歷。

          3.for...of循環(適用于數組和可迭代對象) for...of循環是一種用于遍歷可迭代對象(如數組、字符串、Set、Map等)的循環結構。它提供了一種簡潔的語法來遍歷對象的每個元素,而無需使用索引或迭代器。 以下是for...of循環的基本語法:

          for (let element of iterable){// 對每個元素執行操作 }

          for...of循環中,我們使用of關鍵字來指定要遍歷的可迭代對象,并將每個元素賦值給一個變量(這里是element)。循環體內的代碼將針對每個元素執行操作。下面是一個使用for...of循環遍歷數組的示例:

          for (let element of array) { // 對每個元素執行操作
              console.log(element);
          }

          需要注意的是:

          • for...of方法只會遍歷當前對象的屬性,不會遍歷其原型鏈上的屬性;
          • for...of循環不能用于遍歷普通對象(Plain Object),因為普通對象不是可迭代對象。如果你需要遍歷普通對象的屬性,可以考慮使用for...in循環。
          • 可以使用break、continue、return來中斷循環遍歷。

          4.for...in循環(適用于對象) for...in循環是一種用于遍歷對象的屬性的循環結構。它可以用于遍歷對象的可枚舉屬性(包括自身屬性和繼承的屬性)。以下是for...in循環的基本語法:

          for (let key in object) { // 對每個屬性執行操作 }

          for...in循環中,我們使用in關鍵字來指定要遍歷的對象,并將每個屬性的鍵賦值給一個變量(這里是key)。循環體內的代碼將針對每個屬性執行操作。

          下面是一個使用for...in循環遍歷對象的示例:

          for (let key in object) { // 對每個屬性執行操作 
              console.log(key + ": " + object[key]);
          }

          在上述示例中,我們使用for...in循環遍歷對象object的每個屬性,并在循環體內打印每個屬性的鍵和對應的值。 需要注意的是:

          • for...in循環遍歷的是對象的屬性,而不是值。
          • 它會遍歷對象的可枚舉屬性,包括自身屬性和其原型鏈上的屬性。如果只需要遍歷對象自身的屬性,可以使用Object.hasOwnProperty()方法進行過濾。
          • for...in循環不保證屬性的遍歷順序,因此在遍歷過程中不要依賴屬性的順序。

          5.map方法 map方法用于對數組的每個元素執行指定的操作,并返回一個新的數組,新數組的元素是原數組經過操作后的結果。 以下是map方法的基本語法:

          const newArray = array.map(function(element, index, array) { // 對每個元素執行操作,并返回新的值
              return modifiedElement;
          });

          map方法中,我們傳入一個回調函數作為參數。該回調函數接受三個參數:當前元素的值element、當前元素的索引index和正在遍歷的數組array。 在回調函數中,我們對每個元素執行操作,并返回經過操作后的新值modifiedElementmap方法會遍歷數組的每個元素,并將每個元素經過回調函數處理后的結果組成一個新的數組。 下面是一個使用map方法的示例:

          const newArray = array.map(function(element) { // 對每個元素執行操作,并返回新的值
              return element * 2;
          });

          在上述示例中,我們使用map方法對數組array的每個元素進行操作,將每個元素乘以2,并將操作后的結果組成一個新的數組newArray。 map方法是一種非常有用的方法,它可以方便地對數組的每個元素進行操作,并生成一個新的數組。 需要注意的是:

          • map方法不會修改原始數組,而是返回一個新的數組。
          • map方法無法遍歷對象,僅適用于數組的遍歷。
          • map方法不會對空數組進行檢測;

          6.reduce方法 reduce方法用于對數組的每個元素進行累積操作,并返回一個最終的累積結果。 以下是reduce方法的基本語法:

          // 對每個元素執行累積操作,并返回累積結果
          const result = array.reduce(function(accumulator, element, index, array) {
              return accumulatedValue;
          }, initialValue);

          reduce方法中,我們傳入一個回調函數作為參數。該回調函數接受四個參數:累積值accumulator、當前元素的值element、當前元素的索引index和正在遍歷的數組array。 在回調函數中,我們對每個元素執行累積操作,并將累積結果返回。reduce方法會遍歷數組的每個元素,并將每個元素經過回調函數處理后的累積結果作為下一次迭代的累積值。 下面是一個使用reduce方法的示例:

          const array = [1, 2, 3, 4, 5];
          const result = array.reduce(function(accumulator, element) { // 對每個元素執行累積操作,并返回累積結果
              return accumulator + element;
          }, 0);
          console.log(result); // 輸出: 15

          在上述示例中,我們使用reduce方法對數組array的每個元素進行累積操作,將所有元素相加得到最終的累積結果。 需要注意的是:

          • reduce方法不會改變原數組。
          • reduce方法可以接受一個可選的初始值initialValue作為第二個參數。如果提供了初始值,累積值accumulator的初始值將為該值;如果未提供初始值,則累積值將為數組的第一個元素,且從數組的第二個元素開始進行累積操作。
          • 如果數組為空,且未提供初始值,則reduce方法會拋出一個TypeError。在處理可能為空的數組時,要確保提供了合適的初始值或進行適當的錯誤處理。

          7.filter方法 filter方法用于篩選數組中滿足指定條件的元素,并返回一個新的數組。 以下是filter方法的基本語法:

          const newArray = array.filter(function(element, index, array) { // 返回一個布爾值,表示是否保留該元素 },thisArg);

          filter方法中,我們傳入一個回調函數作為參數。該回調函數接受三個參數:當前元素的值element、當前元素的索引index和正在遍歷的數組array。thisArg是可選的參數,用于指定回調函數中的this值。如果省略了thisArg參數,回調函數中的this將指向全局對象(在瀏覽器中為window對象)。 在回調函數中,我們根據指定的條件判斷是否保留該元素。如果回調函數返回true,則該元素將被保留在新的數組中;如果返回false,則該元素將被過濾掉。 下面是一個使用filter方法的示例:

          const array = [1, 2, 3, 4, 5];
          const newArray = array.filter(function(element) { // 返回一個布爾值,表示是否保留該元素
              return element % 2 === 0; // 保留偶數元素
          });
          console.log(newArray); // 輸出: [2, 4]

          在上述示例中,我們使用filter方法篩選數組array中的偶數元素,并將滿足條件的元素組成一個新的數組newArray。 filter方法非常靈活,可以根據不同的條件篩選數組中的元素?;卣{函數應該返回一個布爾值,表示是否保留該元素。返回true表示保留,返回false表示過濾掉。 需要注意的是:

          • filter方法會返回一個新的數組,該數組包含滿足指定條件的元素。請確保在回調函數中返回一個布爾值,表示是否保留該元素。
          • filter方法不會對空數組進行檢測。

          8.some方法 some方法用于檢測數組中是否至少有一個元素滿足指定條件。 以下是some方法的基本語法:

          const result = array.some(function(element, index, array) { // 返回一個布爾值,表示是否滿足條件 });

          some方法中,我們傳入一個回調函數作為參數。該回調函數接受三個參數:當前元素的值element、當前元素的索引index和正在遍歷的數組array。 在回調函數中,我們根據指定的條件判斷是否滿足條件。如果回調函數返回true,則表示至少有一個元素滿足條件;如果所有元素都不滿足條件,回調函數返回false。 下面是一個使用some方法的示例:

          const array = [1, 2, 3, 4, 5];
          const result = array.some(function(element) { // 返回一個布爾值,表示是否滿足條件
              return element > 3; // 判斷是否存在大于3的元素
          });
          console.log(result); // 輸出: true

          在上述示例中,我們使用some方法檢測數組array中是否存在大于3的元素。由于數組中存在元素4和5滿足條件,所以some方法返回true。 some方法可以用于檢測數組中是否滿足某個條件的元素。它提供了一種簡潔的方式來進行條件判斷。 需要注意的是:

          • some方法在找到滿足條件的元素后會立即停止遍歷,不會繼續遍歷剩余的元素。
          • some方法不會改變原數組,會返回一個布爾值。

          獲取指定站點的所有圖片,您可以使用C#中的HttpClient和HTML解析庫(如HtmlAgilityPack)來實現。

          下面是一個簡單的示例:

          using System;
          using System.Collections.Generic;
          using System.Net.Http;
          using HtmlAgilityPack;
          
          public class Program
          {
              public static async Task Main()
              {
                  string url = "https://example.com"; // 指定站點的URL
          
                  // 創建HttpClient實例
                  using (HttpClient client = new HttpClient())
                  {
                      // 發送GET請求并獲取響應內容
                      HttpResponseMessage response = await client.GetAsync(url);
                      string htmlContent = await response.Content.ReadAsStringAsync();
          
                      // 使用HtmlAgilityPack解析HTML內容
                      HtmlDocument htmlDocument = new HtmlDocument();
                      htmlDocument.LoadHtml(htmlContent);
          
                      // 獲取所有img標簽
                      List<string> imageUrls = new List<string>();
                      foreach (HtmlNode imgNode in htmlDocument.DocumentNode.Descendants("img"))
                      {
                          string imageUrl = imgNode.GetAttributeValue("src", "");
                          if (!string.IsNullOrEmpty(imageUrl))
                          {
                              // 如果圖片URL是相對路徑,則拼接成絕對路徑
                              if (!Uri.IsWellFormedUriString(imageUrl, UriKind.Absolute))
                              {
                                  imageUrl = new Uri(new Uri(url), imageUrl).AbsoluteUri;
                              }
                              imageUrls.Add(imageUrl);
                          }
                      }
          
                      // 打印所有圖片URL
                      foreach (string imageUrl in imageUrls)
                      {
                          Console.WriteLine(imageUrl);
                      }
                  }
              }
          }
          

          在上面的示例中,我們使用HttpClient發送GET請求來獲取指定站點的HTML內容。

          然后,使用HtmlAgilityPack解析HTML內容,并找到所有的img標簽。

          最后,我們將圖片的URL打印出來。

          請注意,這只是一個簡單的示例,僅用于演示如何獲取指定站點的所有圖片。

          在實際應用中,您可能需要處理更復雜的HTML結構和處理異常情況。

          希望這個示例對您有所幫助!

          聽說過 CRDT 并想知道它們是什么嗎?也許你對它們進行過一些研究,但卻遇到了一大堆學術論文和數學術語?在我開始我的Recurse Center批次之前,我就是這樣的。但我花了大約一個月的時間進行研究和編寫代碼,事實證明,只需幾件簡單的事情,你就可以構建很多東西!

          在本系列中,我們將了解什么是 CRDT。然后我們將編寫一個原始 CRDT,將其組合成更復雜的數據結構,最后使用我們學到的知識構建一個協作像素藝術編輯器。所有這些都假設您沒有關于 CRDT 的先驗知識,并且只具有 TypeScript 的基本知識。

          為了激起你的好奇心,我們最終會得到以下結果:

          Network

          Latency

          使用鼠標單擊并拖動即可繪制。使用左下角的顏色輸入更改顏料顏色。您可以在任一畫布上繪制,您的更改將顯示在另一個畫布上,就像它們在同一張圖片上協作一樣。

          單擊網絡按鈕可防止更改到達另一個畫布(盡管當它們重新“在線”時,它們會再次同步)。延遲滑塊會在一個畫布上的更改顯示在另一個畫布上之前添加延遲。

          我們將在下一篇文章中構建它。首先,我們需要了解 CRDT!

          什么是 CRDT?

          好的,讓我們從頭開始。CRDT 代表“無沖突復制數據類型”。這是一個很長的首字母縮略詞,但概念并不太復雜。它是一種可以存儲在不同計算機(對等點)上的數據結構。每個對等點都可以立即更新自己的狀態,而無需通過網絡請求與其他對等點進行檢查。對等點在不同時間點可能具有不同的狀態,但最終保證會收斂到一個商定的狀態。這使得 CRDT 非常適合構建豐富的協作應用程序,例如 Google Docs 和 Figma — 而無需中央服務器來同步更改。

          廣義上,CRDT 有兩種類型:基于狀態的和基于操作的。1基于狀態的 CRDT 在對等體之間傳輸其完整狀態,并通過將所有狀態合并在一起來獲得新狀態。基于操作的 CRDT 僅傳輸用戶采取的操作,這些操作可用于計算新狀態。

          它們也分別被稱為 CvRDT(“Cv”代表“收斂”)和 CmRDT(“Cm”代表“交換”),盡管我認為“基于狀態”和“基于操作”是首選術語。

          這可能會讓基于操作的 CRDT 聽起來更好。例如,如果用戶更新列表中的一項,則基于操作的 CRDT 可以僅發送該更新的描述,而基于狀態的 CRDT 必須發送整個列表!缺點是基于操作的 CRDT 對通信渠道施加了限制:消息必須按因果順序準確地傳遞給每個對等點一次。2

          還有增量 CRDT,即混合 CRDT,允許對等體協商需要相互發送的狀態子集。這是混合基于操作和基于狀態的 CRDT 的一個例子。但根本的權衡仍然存在:對等體之間發送的數據越少,通信的限制就越多。

          這篇文章將專門關注基于狀態的 CRDT。為簡潔起見,從現在開始我只說“CRDT”,但要知道我指的是基于狀態的 CRDT。

          我一直在談論 CRDT 的作用,但什么CRDT?讓我們具體一點:CRDT 是實現此接口的任何數據結構:3

          從技術上講,只要遵循下述合并規則,CRDT 可以是任何東西。這是一個工作定義;實際上,面向對象語言中的實現最終會看起來像這樣。

          interface CRDT<T, S> {
            value: T;
            state: S;
            merge(state: S): void;
          }

          也就是說,一個 CRDT 至少包含三樣東西:

          • 一個值,T。這是我們程序其余部分關心的部分。CRDT 的全部目的是在對等點之間可靠地同步該值。
          • 狀態,S。這是對等體就同一值達成一致所需的元數據。為了更新其他對等體,整個狀態將被序列化并發送給它們。
          • 合并函數。該函數接收一些狀態(可能是從另一個對等點接收的)并將其與本地狀態合并。

          合并函數必須滿足三個屬性,以確保所有對等點都得出相同的結果(我將使用符號A ∨ B來表示將狀態合并A到狀態中B):

          • 交換性:狀態可以按任何順序合并;A ∨ B = B ∨ A。如果 Alice 和 Bob 交換狀態,他們可以將對方的狀態合并到自己的狀態中并得出相同的結果。
          • 結合性:當合并三個(或更多)狀態時,先合并哪個狀態并不重要;(A ∨ B) ∨ C = A ∨ (B ∨ C)如果 Alice 同時收到 Bob 和 Carol 的狀態,她可以按任意順序將它們合并到自己的狀態中,結果都是一樣的。4
          • 冪等性:將一個狀態與自身合并不會改變狀態;A ∨ A = A。如果 Alice 將自己的狀態與自身合并,結果將與她開始時的狀態相同。

          交換律和結合律聽起來可能一樣,而且實際上大多數交換運算也是結合律。但有一些數學運算只是其中之一。例如,矩陣乘法是結合律但不是交換律。令人驚訝的是,浮點運算(即 JavaScript 中的任何數學運算符)是交換律但不是結合律!

          從數學上證明合并函數具有所有這些屬性可能聽起來很難。但幸運的是,我們不必這么做!相反,我們可以結合已經存在的 CRDT,依靠有人已經為我們證明了這些事實。

          說到已經存在的 CRDT:讓我們來了解一下!

          最后寫入有效寄存器

          寄存器是保存單個值的 CRDT。有幾種類型的寄存器,但最簡單的是最后寫入獲勝寄存器(或 LWW 寄存器)。

          顧名思義,LWW 寄存器只是用最后寫入的值覆蓋其當前值。它們使用時間戳來確定最后發生的寫入,這里用整數表示,每當值更新時,整數就會遞增。5算法如下:

          你可能會問:為什么不使用實際時間?不幸的是,準確同步兩臺計算機之間的時鐘是一個極其困難的問題。使用像這樣的遞增整數是邏輯時鐘的一個簡單版本,它捕獲事件相對于彼此而不是“掛鐘”的順序。

          • 如果收到的時間戳小于本地時間戳,則寄存器不會改變其狀態。
          • 如果收到的時間戳大于本地時間戳,則寄存器將用收到的值覆蓋其本地值。它還會存儲收到的時間戳和某種特定于最后寫入該值的對等方的標識符(對等方 ID)。
          • 通過將本地對等 ID 與接收狀態下的對等 ID 進行比較來打破平局。

          使用下面的游樂場嘗試一下。

          Network

          Latency

          ID: alice

          alice1

          ID: bob

          bob1

          您是否了解了 LWW 寄存器的工作原理?以下是一些可供嘗試的特定場景:

          • 關閉網絡,對 進行大量更新bob,然后重新打開。當您從 發送更新時alice,它們將被拒絕,直到時間戳超過bob的時間戳。
          • 執行相同的設置,但一旦您重新打開網絡,就將更新從bob發送到alice。請注意,時間戳現在已同步,并且alice可以再次寫入bob!
          • 增加延遲并同時從兩個對等點發送更新。alice將接受bob的更新,但bob將拒絕 的alice。由于bob的對等點 ID 更大,因此它打破了時間戳綁定。

          以下是 LWW 寄存器的代碼:

          class LWWRegister<T> {
            readonly id: string;
            state: [peer: string, timestamp: number, value: T];
          
            get value() {
              return this.state[2];
            }
          
            constructor(id: string, state: [string, number, T]) {
              this.id = id;
              this.state = state;
            }
          
            set(value: T) {
              // set the peer ID to the local ID, increment the local timestamp by 1 and set the value
              this.state = [this.id, this.state[1] + 1, value];
            }
          
            merge(state: [peer: string, timestamp: number, value: T]) {
              const [remotePeer, remoteTimestamp] = state;
              const [localPeer, localTimestamp] = this.state;
          
              // if the local timestamp is greater than the remote timestamp, discard the incoming value
              if (localTimestamp > remoteTimestamp) return;
          
              // if the timestamps are the same but the local peer ID is greater than the remote peer ID, discard the incoming value
              if (localTimestamp === remoteTimestamp && localPeer > remotePeer) return;
          
              // otherwise, overwrite the local state with the remote state
              this.state = state;
            }
          }

          讓我們看看這與 CRDT 接口相比如何:

          • state是最后寫入寄存器的對等方 ID、最后寫入的時間戳和存儲在寄存器中的值的三元組。
          • value只是state元組的最后一個元素。
          • merge是一種實現上述算法的方法。

          LWW 寄存器還有一個名為 的方法set,該方法在本地調用以設置寄存器的值。它還會更新本地元數據,將本地對等 ID 記錄為最后一個寫入者,并將本地時間戳加一。

          就是這樣!雖然看起來很簡單,但不起眼的 LWW 寄存器是一個強大的構建塊,我們可以使用它來創建實際的應用程序。

          最后寫入獲勝地圖

          大多數程序涉及多個值,6這意味著我們需要一個比 LWW 寄存器更復雜的 CRDT。我們今天要學習的是 Last Write Wins Map(或 LWW Map)。

          [需要引用]

          讓我們先定義幾個類型。首先,我們的值類型:

          type Value<T> = {
            [key: string]: T;
          };

          如果每個單獨的映射值都保留類型T,則整個 LWW 映射的值就是字符串鍵到T值的映射。

          這是我們的狀態類型:

          type State<T> = {
            [key: string]: LWWRegister<T | null>["state"];
          };

          你看出其中的竅門了嗎?從我們的應用程序的角度來看,LWW 映射只保存普通值 — — 但實際上它保存的是 LWW 寄存器。當我們查看完整狀態時,每個鍵的狀態就是該鍵的 LWW 寄存器的狀態。7

          如果值類型是T,為什么狀態類型是 的聯合T | null?稍后會詳細介紹!

          我想暫停一下,因為它很重要。組合讓我們將原始 CRDT 組合成更復雜的 CRDT。當需要合并時,父級所做的就是將傳入狀態的片段傳遞給相應子級的合并函數。我們可以根據需要多次嵌套此過程;每個復雜 CRDT 將越來越小的狀態片段傳遞到下一級,直到我們最終找到執行實際合并的原始 CRDT。

          從這個角度來看,LWW Map 合并功能很簡單:遍歷每個鍵并將該鍵的傳入狀態交給相應的 LWW 寄存器進行合并。在下面的操場上嘗試一下:

          Network

          Latency

          ID: alice

          • barbob1
          • fooalice1

          ID: bob

          • barbob1
          • fooalice1

          這里發生的事情有點難以追蹤,所以我們將每個鍵的狀態分開。不過請注意,這只是一種可視化輔助手段;完整狀態仍作為單個單元進行傳輸。

          嘗試增加延遲,然后在每個對等點上更新不同的密鑰。您將看到每個對等點都接受具有較高時間戳的更新值,同時拒絕具有較低時間戳的值。

          Network

          Latency

          ID: alice

          • barbob1
          • fooalice1

          ID: bob

          • barbob1
          • fooalice1

          完整的 LWW Map 類相當強大,因此讓我們逐一介紹每個屬性。以下是它的開頭:

          class LWWMap<T> {
            readonly id = "";
            #data = new Map<string, LWWRegister<T | null>>();
          
            constructor(id: string, state: State<T>) {
              this.id = id;
          
              // create a new register for each key in the initial state
              for (const [key, register] of Object.entries(state)) {
                this.#data.set(key, new LWWRegister(this.id, register));
              }
            }
          }

          #data是一個私有屬性,包含 LWW Register 實例的鍵映射。要實例化具有預先存在狀態的 LWW Map,我們需要遍歷狀態并實例化每個 LWW Register。

          請記住,CRDT 需要三個屬性:value和state。merge我們首先看一下value:

            get value() {
              const value: Value<T> = {};
          
              // build up an object where each value is set to the value of the register at the corresponding key
              for (const [key, register] of this.#data.entries()) {
                if (register.value !== null) value[key] = register.value;
              }
          
              return value;
            }

          它是一個遍歷鍵并獲取每個寄存器的 getter value。對于應用程序的其余部分而言,它只是普通的地圖!

          現在讓我們看看state:

            get state() {
              const state: State<T> = {};
          
              // build up an object where each value is set to the full state of the register at the corresponding key
              for (const [key, register] of this.#data.entries()) {
                if (register) state[key] = register.state;
              }
          
              return state;
            }

          與 類似value,它是一個從每個寄存器的 構建映射的 getter state。

          這里有一個明顯的趨勢:遍歷輸入的鍵#data并將內容傳遞給存儲在該鍵處的寄存器。您可能認為merge會以相同的方式工作,但它更復雜一些:

            merge(state: State<T>) {
              // recursively merge each key's register with the incoming state for that key
              for (const [key, remote] of Object.entries(state)) {
                const local = this.#data.get(key);
          
                // if the register already exists, merge it with the incoming state
                if (local) local.merge(remote);
                // otherwise, instantiate a new `LWWRegister` with the incoming state
                else this.#data.set(key, new LWWRegister(this.id, remote));
              }
            }

          首先,我們遍歷傳入state參數而不是本地參數#data。這是因為如果傳入狀態缺少一個鍵#data,我們知道我們不需要觸摸該鍵。8

          你可能會想:如果另一個對等點從他們的地圖中刪除了一個鍵,我們是否也應該從本地地圖中刪除它?這與狀態保持的原因相同T | null。我們快到了!

          對于傳入狀態中的每個鍵,我們都會獲取該鍵處的本地寄存器。如果我們找到一個,則說明對等方正在更新我們已經知道的現有鍵,因此我們會使用該merge鍵處的傳入狀態調用該寄存器的方法。否則,說明對等方已將新鍵添加到映射中,因此我們會使用該鍵處的傳入狀態實例化一個新的 LWW 寄存器。

          除了 CRDT 方法之外,我們還需要實現在地圖上更常見的方法:set、get和。deletehas

          讓我們從以下開始set:

            set(key: string, value: T) {
              // get the register at the given key
              const register = this.#data.get(key);
          
              // if the register already exists, set the value
              if (register) register.set(value);
              // otherwise, instantiate a new `LWWRegister` with the value
              else this.#data.set(key, new LWWRegister(this.id, [this.id, 1, value]));
            }

          就像在合并方法中一樣,我們要么調用寄存器set來更新現有密鑰,要么實例化新的 LWW 寄存器來添加新密鑰。初始狀態使用本地對等 ID、時間戳 1 和傳遞給 的值set。

          get更簡單:

            get(key: string) {
              return this.#data.get(key)?.value ?? undefined;
            }

          從本地映射中獲取寄存器,如果有則返回其值。

          為什么要合并到undefined?因為每個寄存器都保存T | null。有了該delete方法,我們就可以解釋為什么了:

            delete(key: string) {
              // set the register to null, if it exists
              this.#data.get(key)?.set(null);
            }

          我們不會將鍵從映射中完全移除,而是將寄存器值設置為null。元數據會保留下來,這樣我們就可以消除尚無鍵的狀態的刪除歧義。這些被稱為墓碑——CRDT 過去的幽靈。

          考慮一下如果我們真的從映射中刪除了鍵,而不是留下墓碑,會發生什么。這是一個操場,同行可以添加鍵,但不能刪除它們。你能想出如何讓同行刪除鍵嗎?

          Network

          Latency

          ID: alice

          Key

          Value

          • fooalice1

          ID: bob

          Key

          Value

          • fooalice1

          關閉網絡,向alice的地圖添加一個密鑰,然后重新打開網絡。最后,對bob的地圖進行更改。由于alice發現 的傳入狀態bob缺少該密鑰,因此她將其從自己的狀態中刪除 — — 盡管她bob從一開始就不知道該密鑰。哎呀!

          這是具有正確行為的操場。您還可以看到刪除鍵時會發生什么。

          Network

          Latency

          ID: alice

          Key

          Value

          • fooalice1

          ID: bob

          Key

          Value

          • fooalice1

          請注意,我們永遠不會從映射中刪除已刪除的鍵。這是 CRDT 的一個缺點——我們只能添加信息,而不能刪除信息。雖然從應用程序的角度來看,該鍵已被完全刪除,但底層狀態仍然記錄該鍵曾經存在。從技術術語上講,我們說 CRDT 是單調遞增的數據結構。9

          有幾種方法可以緩解這一缺陷,但這兩種方法都超出了本文的討論范圍。一種方法是“垃圾收集”:從 CRDT 中修剪墓碑,這可以防止您將狀態與刪除墓碑之前所做的任何更改合并。另一種方法是創建一種有效的格式來編碼數據。您也可以結合使用這些方法。研究表明,與“普通”數據相比,這可以只產生 50% 的開銷。如果您想跳過前面的內容并查看其中的一些優化效果,請查看本系列的最后一部分:使 CRDT 效率提高 98%。

          最后一個 LWW Map 方法是has,它返回一個布爾值,指示映射是否包含給定的鍵。

            has(key: string) {
              // if a register doesn't exist or its value is null, the map doesn't contain the key
              return this.#data.get(key)?.value !== null;
            }

          這里有一個特殊情況:如果映射在給定鍵處包含一個寄存器,但該寄存器包含null,則該映射被視為不包含該鍵。

          為了方便后人參考,下面是完整的 LWW 地圖代碼:

          class LWWMap<T> {
            readonly id: string;
            #data = new Map<string, LWWRegister<T | null>>();
          
            constructor(id: string, state: State<T>) {
              this.id = id;
          
              // create a new register for each key in the initial state
              for (const [key, register] of Object.entries(state)) {
                this.#data.set(key, new LWWRegister(this.id, register));
              }
            }
          
            get value() {
              const value: Value<T> = {};
          
              // build up an object where each value is set to the value of the register at the corresponding key
              for (const [key, register] of this.#data.entries()) {
                if (register.value !== null) value[key] = register.value;
              }
          
              return value;
            }
          
            get state() {
              const state: State<T> = {};
          
              // build up an object where each value is set to the full state of the register at the corresponding key
              for (const [key, register] of this.#data.entries()) {
                if (register) state[key] = register.state;
              }
          
              return state;
            }
          
            has(key: string) {
              return this.#data.get(key)?.value !== null;
            }
          
            get(key: string) {
              return this.#data.get(key)?.value;
            }
          
            set(key: string, value: T) {
              // get the register at the given key
              const register = this.#data.get(key);
          
              // if the register already exists, set the value
              if (register) register.set(value);
              // otherwise, instantiate a new `LWWRegister` with the value
              else this.#data.set(key, new LWWRegister(this.id, [this.id, 1, value]));
            }
          
            delete(key: string) {
              // set the register to null, if it exists
              this.#data.get(key)?.set(null);
            }
          
            merge(state: State<T>) {
              // recursively merge each key's register with the incoming state for that key
              for (const [key, remote] of Object.entries(state)) {
                const local = this.#data.get(key);
          
                // if the register already exists, merge it with the incoming state
                if (local) local.merge(remote);
                // otherwise, instantiate a new `LWWRegister` with the incoming state
                else this.#data.set(key, new LWWRegister(this.id, remote));
              }
            }
          }




          作者:Jake

          出處:https://jakelazaroff.com/words/an-interactive-intro-to-crdts/


          主站蜘蛛池模板: 中文字幕久久久久一区| 毛片一区二区三区| 国产品无码一区二区三区在线蜜桃| 无码一区二区三区免费| 久久精品黄AA片一区二区三区| 国产激情视频一区二区三区| 国产一区二区三区免费在线观看| 3d动漫精品啪啪一区二区中| 亚洲性日韩精品国产一区二区| 国产香蕉一区二区在线网站| 色精品一区二区三区| 在线观看国产一区亚洲bd| 精品视频一区二区三区四区| 在线视频一区二区| 性色A码一区二区三区天美传媒| 一区二区手机视频| 国产伦精品一区二区三区在线观看 | 国产精品免费一区二区三区 | 国产在线视频一区| 亚洲一区二区三区免费| 一区二区国产精品| 一区二区不卡久久精品| 无码精品人妻一区| 肥臀熟女一区二区三区| 久久国产三级无码一区二区| 中文字幕一区二区三区在线播放| 波多野结衣高清一区二区三区 | 亚洲天堂一区在线| 中文字幕一区二区三区人妻少妇| 国产AV一区二区三区传媒| 日韩国产精品无码一区二区三区 | 亚洲日韩中文字幕无码一区| 亚洲爆乳精品无码一区二区 | 精品一区二区三区波多野结衣| 亚洲一区二区在线视频| 中文字幕精品一区二区| 日本一区二区三区不卡在线视频 | 国产精品日本一区二区不卡视频 | 免费无码A片一区二三区| 日本精品高清一区二区2021| 波多野结衣电影区一区二区三区|