偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

淺析 Preact Signals 及實(shí)現(xiàn)原理

開發(fā) 前端
Preact Signals 本身在狀態(tài)管理上區(qū)別于 React Hooks 上的一個點(diǎn)在于: Signals 本身是基于應(yīng)用的狀態(tài)圖去做數(shù)據(jù)更新,而 Hooks 本身則是依附于 React 的組件樹去進(jìn)行更新。

介紹

Preact Signals 是 Preact 團(tuán)隊(duì)在22年9月引入的一個特性。我們可以將它理解為一種細(xì)粒度響應(yīng)式數(shù)據(jù)管理的方式,這個在很多前端框架中都會有類似的概念,例如 SolidJS、Vue3 的 Reactivity、Svelte 等等。

Preact Signals 在命名上參考了 SolidJS 的 Signals 的概念,不過兩個框架的實(shí)現(xiàn)方式和行為都有一些區(qū)別。在 Preact Signals 中,一個 signal 本質(zhì)上是個擁有 .value 屬性的對象,你可以在一個 React 組件中按照如下方式使用:

import { signal } from '@preact/signals';

const count = signal(0);

function Counter() {
  const value = count.value;
  
  return (
    <div>
      <p>Count: {value}</p>
      <button onClick={() => count.value ++}>Click</button>
    </div>
  )
}

通過這個例子,我們可以看到 Signal 不同于 React Hooks 的地方: 它是可以直接在組件外部調(diào)用的。

同時這里我們也可以看到,在組件中聲明了一個叫 count 的 signal 對象,但組件在消費(fèi)對應(yīng)的 signal 值的時候,只用訪問對應(yīng) signal 對象的 .value 值即可。

在開始具體的介紹之前,筆者先從 Preact 官方文檔中貼幾個關(guān)于 Signal API 的介紹,讓讀者對 Preact Signals 這套數(shù)據(jù)管理方式有個基本的了解。

API

以下為 Preact Signals 提供的一些 Common API:

signal(initialValue)

這個 API 表示的就是個最普通的 Signals 對象,它算是 Preact Signals 整個響應(yīng)式系統(tǒng)最基礎(chǔ)的地方。

當(dāng)然,在不同的響應(yīng)式庫中,這個最基礎(chǔ)的原語對象也會有不同的名稱,例如 Mobx、RxJS 的 Observers,Vue 的 Refs。而 Preact 這里參考了和 SolidJS 一樣的術(shù)語 signal。

Signal 可以表示包裝在響應(yīng)式里層的任意 JS 值類型,你可以創(chuàng)建一個帶有初始值的 signal,然后可以隨意讀和更新它:

import { signal } from '@preact/signals-core';

const s = signal(0);
console.log(s.value); // Console: 0

s.value = 1;
console.log(s.value); // Console: 1

computed(fn)

Computed Signals 通過 computed(fn) 函數(shù)從其它 signals 中派生出新的 signals 對象:

import { signal, computed } from '@preact/signals-core';

const s1 = signal('hello');
const s2 = signal('world');

const c = computed(() => {
  return s1.value + " " + s2.value
})

不過需要注意的是,computed 這個函數(shù)在這里并不會立即執(zhí)行,因?yàn)榘凑?Preact 的設(shè)計(jì)原則,computed signals 被規(guī)定為懶執(zhí)行的(這個后面會介紹),它只有在本身值被讀取的時候才會觸發(fā)執(zhí)行,同時它本身也是只可讀的:

console.log(c.value) // hello world

同時 computed signals 的值是會被緩存的。一般而言,computed(fn) 運(yùn)行開銷會比較大, Preact 只會在真正需要的時候去重新更新它。一個正在執(zhí)行的 computed(fn) 會追蹤它運(yùn)行期間讀取到的那些 signals 值,如果這些值都沒變化,那么是會跳過重新計(jì)算的步驟的。

因此在上面的示例中,只要 s1.value 和 s2.value 的值不變化,那么 c.value 的值永遠(yuǎn)不會重新計(jì)算。

同樣,一個 computed signal 也可以被其它的 computed signal 消費(fèi):

const count = signal(1);
const double = computed(() => count.value * 2);
const quadruple = computed(() => double.value * 2);

console.log(quadruple.value); // Console: 4
count.value = 20;
console.log(quadruple.value); // Console: 80

同時 computed 依賴的 signals 也并不需要是靜態(tài)的,它只會對最新的依賴變更發(fā)生重新執(zhí)行:

const choice = signal(true);
const funk = signal("Uptown");
const purple = signal("Haze"); 

const c = computed( 
  () => {
    if (choice.value) {
      console.log(funk.value, "Funk");
    } else {
      console.log("Purple", purple.value);
    }
}); 
  
c.value; // Console: Uptown Funk

purple.value = "Rain"; // purple is not a dependency, so 
c.value; // effect doesn't run

choice.value = false; 
c.value; // Console: Purple Rain 

funk.value = "Da"; // funk not a dependency anymore, so 
c.value; // effect doesn't run

我們可以通過這個 Demo 看到,c 這個 computed signal 只會在它最新依賴的 signal 對象值發(fā)生變化的時候去觸發(fā)重新執(zhí)行。

effect(fn)

上一節(jié)中介紹的 Computed Signals 一般都是一些不帶副作用的純函數(shù)(所以它們可以在初次懶執(zhí)行)。這節(jié)要介紹的 Effect Signals 則是用來處理一些響應(yīng)式中的副作用使用。

和 Computed Signals 一樣的是,Effect Signals 同樣也會對依賴進(jìn)行追蹤。但 Effect 則不會懶執(zhí)行,與之相反,它會在創(chuàng)建的時候立即執(zhí)行,然后當(dāng)它追蹤的依賴值發(fā)生變化的時候,它會隨著變化而更新:

import { signal, computed, effect } from '@preact/signals-core';

const count = signal(1);
const double = computed(() => count.value * 2);
const quadrple = computed(() => double.value * 2);

effect(() => {
  // is now 4
  console.log('quadruple is now', quadruple.value);
})

count.value = 20; // is now 80

這里的 effect 執(zhí)行是由 Preact Signals 內(nèi)部的通知機(jī)制觸發(fā)的。當(dāng)一個普通的 signal 發(fā)生變化的時候,它會通知它的直接依賴項(xiàng),這些依賴項(xiàng)同樣也會去通知它們自己對應(yīng)的直接依賴項(xiàng),依此類推。

在 Preact 的內(nèi)部實(shí)現(xiàn)中,通知路徑中的 Computed Signals 會被標(biāo)記為 OUTDATED 的狀態(tài),然后再去做重新執(zhí)行計(jì)算操作。如果一個依賴變更通知一直傳播到一個 effect 上面,那么這個 effect 會被安排到當(dāng)其自身前面的 effect 函數(shù)執(zhí)行完之后再執(zhí)行。

如果你只想調(diào)用一次 effect 函數(shù),那么可以把它賦值為一個函數(shù)調(diào)用,等到這個函數(shù)執(zhí)行完,這個 effect 也會一起結(jié)束:

const count = signal(1);
const double = computed(() => count.value * 2);
const quadruple = computed(() => double.value * 2);
const dispose = effect(() => {
  console.log('quadruple is now', quadruple.value);
});

// Console: quadruple is now 4
dispose();
count.value = 20;

batch(fn)

用于將多個值的更新在回調(diào)結(jié)束時合成為一個。batch 的處理可以被嵌套,并且只有當(dāng)最外層的處理回調(diào)完成后,更新才會刷新:

const name = signal('Dong');
const surname = signal('Zoom');

// Combine both writes into one
batch(() => {
  name.value = 'Haha';
  surname.value = 'Nana';
})

實(shí)現(xiàn)方式

在開始介紹之前,我們結(jié)合前面的 API 介紹,來強(qiáng)調(diào)一些 Preact Signals 本身的設(shè)計(jì)性原則:

  • 依賴追蹤: 跟蹤使用到的 signals(不管是 signals 還是 computed)。依賴項(xiàng)可能會動態(tài)改變
  • 懶執(zhí)行的 computed: computed 值在被需要的時候運(yùn)行
  • 緩存: computed 值只在依賴項(xiàng)可能改變的情況下才會重新計(jì)算
  • 立即執(zhí)行的 effect: 當(dāng)依賴中的某個內(nèi)容變化時,effect 應(yīng)該盡快運(yùn)行。

關(guān)于 Signals 的具體實(shí)現(xiàn)方式具體可以參考: https://github.com/preactjs/signals 。

依賴追蹤

不管什么時候評估實(shí)現(xiàn) compute / effect 這兩個函數(shù),它們都需要一種在其運(yùn)行時期捕獲他們會讀取到的 signal 的方式。Preact Signals 給 Compute 和 Effect 這兩個 Signals 都設(shè)置了其自身對應(yīng)的 context 。

當(dāng)讀取 Signal 的 .value 屬性時,它會調(diào)用一次 getter ,getter 會將 signal 當(dāng)成當(dāng)前 context 依賴項(xiàng)源頭給添加進(jìn)來。這個 context 也會被這個 signal 添加為其依賴項(xiàng)目標(biāo)。

到最后,signal 和 effects 對其自身的依賴關(guān)系以及依賴者都會有個最新的試圖。每個 signal 都可以在其 .value 值發(fā)生改變的時候通知到它的依賴者。例如在一個 effect 執(zhí)行完成之后釋放掉了,effect 和 computed signals 都是可以通知他們依賴集去取消訂閱這些通知的。

圖片圖片

同一個 signals 可能在一個 context 里面被讀取多次。在這種情況下,進(jìn)行依賴項(xiàng)的去重會很方便。然后我們還需要一種處理 發(fā)生變化依賴項(xiàng)集合 的方式: 要么在每次重新觸發(fā)運(yùn)行時 時再重建依賴項(xiàng)集合,要么遞增地添加/刪除依賴項(xiàng) / 依賴者。

Preact Signals 在早期版本中使用到了 JS 的 Set 對象去處理這種情況(Set 本身的性能比較不錯,能在 O(1) 時間內(nèi)去添加 / 刪除子項(xiàng),同時能在 O(N) 的時間里面遍歷當(dāng)前集合,對于重復(fù)的依賴項(xiàng),Set 也會自動去重)。

但創(chuàng)建 Sets 的開銷可能相對 Array 要更昂貴(從空間上看),因?yàn)?Signals 至少需要創(chuàng)建兩個單獨(dú)的 Sets : 存儲依賴項(xiàng)和依賴者。

圖片圖片

同時 Sets 中也有個屬性,它們是按照插入順序來進(jìn)行迭代。這對于 Signals 中處理緩存的情況會很方便,但也有些情況下,Signals 插入的順序并不是總保持不變的,例如以下情況:

const s1 = signal(0)
const s2 = signal(0)
const s3 = signal(0)

const c = computed(() => {
  if (s1.value) {
    s2.value;
    s3.value
  } else {
    s3.value 
    s2.value
  }
})

可以看到,這這次代碼中,依賴項(xiàng)的順序取決于 s1 這個 signal,順序要么是 s1、s2、s3,要么是 s1、s3、s2。按照這種情況,就必須采取一些其他的步驟來保證 Sets 中的內(nèi)容順序是正常的: 刪除然后再添加項(xiàng)目,清空函數(shù)運(yùn)行前的集合,或者為每次運(yùn)行創(chuàng)建一個新的集合。每種方法都有可能導(dǎo)致內(nèi)存抖動。而所有這些只是為了處理理論上可能,但可能很少出現(xiàn)的,依賴關(guān)系順序改變的情況。

而 Preact Signals 則采用了一種類似雙向鏈表的數(shù)據(jù)結(jié)構(gòu)去存儲解決了這個問題。

鏈表

鏈表是一種比較原始的存儲結(jié)構(gòu),但對于實(shí)現(xiàn) Preact Signals 的一些特點(diǎn)來說,它具備一些非常好的屬性,例如在雙向鏈表節(jié)點(diǎn)中,以下操作會非常節(jié)省:

  • 在 O(1) 時間內(nèi),將一個 signals 值插到鏈表的某一端
  • 在 O(1) 時間內(nèi),刪除鏈表任何位置的一個節(jié)點(diǎn)(假設(shè)存在對應(yīng)指針的情況下)
  • 在 O(n) 時間內(nèi),遍歷鏈表中的節(jié)點(diǎn)

以上這些操作,都可以用于管理 Signals 中的依賴 / 依賴列表。

Preact 會首先給每個依賴關(guān)系都創(chuàng)建一個 source Node 。而對應(yīng) Node 的 source 屬性會指向目前正在被依賴的 Signal。同時每個 Node 都有 nextSource 和 prevSource 屬性,分別指向依賴列表中的下一個和前一個 source Nodes 。Effect 和 Computed Signals 獲得一個指向鏈表第一個 Node 的 sources 屬性,然后我們可以去遍歷這里面的一些依賴關(guān)系,或者去插入 / 刪除新的依賴關(guān)系。

圖片圖片

然后處理完上面的依賴項(xiàng)步驟后,我們再反過來去做同樣的事情: 給每個依賴者創(chuàng)建一個 Target Node 。Node 的 target 屬性則會指向它們依賴的 Effect 或 Computed Signals。nextTarget 和 prevTarget 構(gòu)建一個雙項(xiàng)鏈表。普通和 computed Signals Node 節(jié)點(diǎn)中會有個targets 屬性用于指向他們依賴列表中的第一個 Target Node:

圖片圖片

但一般依賴項(xiàng)和依賴者都是成對出現(xiàn)的。對于每個 source Node 都會有一個對應(yīng)的 target Node 。本質(zhì)上我們可以將 source Nodes 和 target Nodes 統(tǒng)一合并為 Nodes 。這樣每個 Node 本質(zhì)上會有四條鏈節(jié),依賴者可以作為它依賴列表的一部分使用,如下圖所示:

圖片圖片

在每個 computed / effect 函數(shù)執(zhí)行之前,Preact 會迭代以前的依賴關(guān)系,并設(shè)置每個 Node 為 unused 的標(biāo)志位。同時還會臨時把 Node 存儲到它的 .source.node 屬性中用于以后使用。

在函數(shù)執(zhí)行期間,每次讀取依賴項(xiàng)時,我們可以使用節(jié)點(diǎn)以前記錄的值(上次的值)來發(fā)現(xiàn)該依賴項(xiàng)是否在這次或者上次運(yùn)行時已經(jīng)被記錄下來,如果記錄下來了,我們就可以回收它之前的 Node(具體方式就是將這個節(jié)點(diǎn)的位置重新排序)。如果是沒見過的依賴項(xiàng),我們會創(chuàng)建一個新的 Node 節(jié)點(diǎn),然后將剩下的節(jié)點(diǎn)按照使用的時期進(jìn)行逆序排序。

函數(shù)運(yùn)行結(jié)束后,Preact Signals 會遍歷依賴列表,將打上了 unused 標(biāo)志的 Nodes 節(jié)點(diǎn)給刪除掉。然后整理一下剩余的鏈表節(jié)點(diǎn)。

這種鏈表結(jié)構(gòu)可以讓每次只用給每個依賴項(xiàng) - 依賴者的關(guān)系對分配一個 Node,然后只要依賴關(guān)系是存在的,這個節(jié)點(diǎn)是可以一直用的(不過需要更新下節(jié)點(diǎn)的順序而已)。如果項(xiàng)目的 Signals 依賴樹是穩(wěn)定的,內(nèi)存也會在構(gòu)建完成后一直保持穩(wěn)定。

立即執(zhí)行的 effect

有了上面依賴追蹤的處理,通過變更通知實(shí)現(xiàn)的立即執(zhí)行的 effect 會很容易。Signals 通知其依賴者們,自己的值發(fā)生了變化。如果依賴者本身是個有依賴者的 computed signals,那么它會繼續(xù)往前傳遞通知。依此類推,接到通知的 effect 會自己安排自己運(yùn)行。

如果通知的接收端,已經(jīng)被提前通知了,但還沒機(jī)會執(zhí)行,那它就不會向前傳遞通知了。這會減輕當(dāng)前依賴樹擴(kuò)散出去或者進(jìn)來時形成的通知踩踏。如果 signals 本身的值實(shí)際上沒發(fā)生變化,例如 s.value = s.value。普通的 signal 也不會去通知它的依賴者。

Effect 如果想調(diào)度它自身,需要有個排序好的調(diào)度表。Preact 給每個 Effect 實(shí)例都添加了專門的 .nextBatchedEffect 屬性,讓 Effect 實(shí)例作為單向調(diào)度列表中的節(jié)點(diǎn)進(jìn)行雙重作用,這減少了內(nèi)存抖動,因?yàn)榉磸?fù)調(diào)度同一個效果不需要額外的內(nèi)存分配或釋放。

通知訂閱和垃圾回收

computed signals 實(shí)際上并不總是從他們的依賴關(guān)系中獲取通知的。只有當(dāng)有像 effect 這樣的東西在監(jiān)聽 signals 本身時,compute signals 才會訂閱依賴通知。這避免了下面的一些情況:

const s = signal(0);

{
  const c = computed(() => s.value)
}
// c 并不在同一個作用域下

如果 c 總是訂閱來自 s 的通知,那么 c 無法被垃圾回收,直到 s 也去它這個 scope 上面去。主要因?yàn)?s 會繼續(xù)掛在一個對 c 的引用上。

在 Preact Signals 中,鏈表提供了一種比較好的辦法去動態(tài)訂閱和取消訂閱依賴通知。

在那些 computed signal 已經(jīng)訂閱了通知的情況下,我們可以利用這個做一些額外的優(yōu)化。后面會介紹 computed 懶執(zhí)行和緩存。

Computed signals 的懶執(zhí)行 & 緩存

實(shí)現(xiàn)懶執(zhí)行 computed Signals 的最簡單方法是每次讀取其值時都重新計(jì)算。不過,這不是很高效。這就是緩存和依賴跟蹤需要幫助優(yōu)化的地方。

每個普通和 Computed Signals 都有它們自己的版本號。每次當(dāng)其值變化時,它們會增加版本號。當(dāng)運(yùn)行一個 compute fn 時,它會在 Node 中存儲上次看到的依賴項(xiàng)的版本號。我們原本可以選擇在節(jié)點(diǎn)中存儲先前的依賴值而不是版本號。然而,由于 computed signals 是懶執(zhí)行的,這些依賴值可能會永遠(yuǎn)掛在一些過期或者無限循環(huán)執(zhí)行的 Node 節(jié)點(diǎn)上。因此,我們認(rèn)為版本編號是一種安全的折中方法。

我們得出了以下算法,用于確定當(dāng) computed signals 可以懶執(zhí)行和復(fù)用它的緩存:

  1. 如果自上次運(yùn)行以來,任何地方的 signal 的值都沒有改變,那么退出 & 返回緩存值。

每次當(dāng)普通 signal 改變時,它也會遞增一個全局版本號,這個版本號在所有的普通信號之間共享。每個計(jì)算信號都跟蹤他們看到的最后一個全局版本號。如果全局版本自上次計(jì)算以來沒有改變,那么可以早點(diǎn)跳過重新計(jì)算。無論如何,在這種情況下,都不可能對任何計(jì)算值進(jìn)行任何更改。

  1. 如果 computed signals 正在監(jiān)聽通知,并且自上次運(yùn)行以來沒有被通知,那么退出 & 返回緩存值。

當(dāng) compute signals 從其依賴項(xiàng)中得到通知時,它標(biāo)記緩存值已經(jīng)過時。如前所述,compute signals 并不總是得到通知。但是當(dāng)他們得到通知時,我們可以利用它。

  1. 按順序重新評估依賴項(xiàng)。檢查它們的版本號。如果沒有依賴項(xiàng)改變過它的版本號,即使在重新評估后,也退出 & 返回緩存值。

這個步驟是我們特別關(guān)心保持依賴項(xiàng)按使用順序排列的原因。如果一個依賴項(xiàng)發(fā)生改變,那么我們不希望重更新 compute list 中后來的依賴項(xiàng),因?yàn)槟强赡苤皇遣槐匾墓ぷ?。誰知道,也許那個第一個依賴項(xiàng)的改變導(dǎo)致下次 compute function 運(yùn)行時丟棄了后面的依賴項(xiàng)。

  1. 運(yùn)行 compute function。如果返回的值與緩存值不同,那么遞增計(jì)算信號的版本號。緩存并返回新值。

這是最后的手段!但如果新值等于緩存的值,那么版本號不會改變,而線路下方的依賴項(xiàng)可以利用這一點(diǎn)來優(yōu)化他們自己的緩存。

最后兩個步驟經(jīng)常遞歸到依賴項(xiàng)中。這就是為什么早期的步驟被設(shè)計(jì)為嘗試短路遞歸的原因。

一些思考

JSX 渲染

Signal 在 Preact JSX 語法進(jìn)行傳值的時候,可以直接傳對應(yīng)的 Signal 對象而不是具體的值,這樣在 Signal 對象的值發(fā)生變化的時候,可以在組件不經(jīng)過重新渲染的情況下觸發(fā)值的變化(本質(zhì)上是把 Signal 值綁定到 DOM 值上)。

例如以下組件:

import { render } from 'preact'
import { signal } from '@preact/signals'

const count = signal(1);

// Component 跳過流程是怎么處理
// 可能對 state less 的組件跳過 render(function component)
funciton Counter() {
  console.log('render')
  return (
    <>
     <p>Count: {count}</p>
     <button notallow={() => count.value ++}>Add Count</button>
    </>
  )
}

render(<TodoList />, document.getElement('app'))

這個地方如果傳的是個 count 的 signal 對象,那么在點(diǎn)擊 button 的時候,這里的 Counter 組件并不會觸發(fā) re-render ,如果是個 signal 值,那么它會觸發(fā)更新。

關(guān)于把 Signals 在 JSX 中渲染成文本值,可以直接參考: https://github.com/preactjs/signals/pull/147

這里渲染的原理是 Preact Signal 本身會去劫持原有的 Diff 執(zhí)行算法:

圖片圖片

把對應(yīng)的 signal value 存到 vnode.__np 這個節(jié)點(diǎn)屬性上面去,并且這里會跳過原有的 diff 算法執(zhí)行邏輯(這里的 old(value) 執(zhí)行函數(shù))。

然后在 diff 完之后的更新的時候,直接去把對應(yīng)的 signals 值更新到真實(shí)的 dom 節(jié)點(diǎn)上面去即可:

圖片圖片

Preact signals 和 hooks 之間關(guān)系

兩者并不互斥,可以一起使用,因?yàn)閮烧咚蕾嚨母碌倪壿嫴灰粯印?/p>

Preact Signals 對比 Hooks 帶來收益

Preact Signals 本身在狀態(tài)管理上區(qū)別于 React Hooks 上的一個點(diǎn)在于: Signals 本身是基于應(yīng)用的狀態(tài)圖去做數(shù)據(jù)更新,而 Hooks 本身則是依附于 React 的組件樹去進(jìn)行更新。

本質(zhì)上,一個應(yīng)用的狀態(tài)圖比組件樹要淺很多,更新狀態(tài)圖造成的組件渲染遠(yuǎn)遠(yuǎn)低于更新狀態(tài)樹所產(chǎn)生的渲染性能損耗,具體差異可以參考分別使用 Hooks 和 Signals 的 Devtools Profile 分析:

圖片圖片

參考資料

  • Why Signals Are Better than Preact: https://www.youtube.com/watch?v=SO8lBVWF2Y8
  • https://preactjs.com/guide/v10/signals/

責(zé)任編輯:武曉燕 來源: zoomdong
相關(guān)推薦

2009-07-06 09:23:51

Servlet定義

2022-09-04 21:08:50

響應(yīng)式設(shè)計(jì)Resize

2009-09-04 10:05:16

C#調(diào)用瀏覽器瀏覽器的原理

2014-08-26 09:40:54

圖數(shù)據(jù)數(shù)據(jù)挖掘

2020-08-05 08:21:41

Webpack

2009-09-07 05:24:22

C#窗體繼承

2009-08-27 14:29:28

顯式實(shí)現(xiàn)接口

2017-07-19 11:11:40

CTS漏洞檢測原理淺析

2020-03-31 08:05:23

分布式開發(fā)技術(shù)

2023-05-11 07:25:57

ReduxMiddleware函數(shù)

2009-07-16 10:23:30

iBATIS工作原理

2018-10-25 15:13:23

APP脫殼工具

2020-11-05 11:14:29

Docker底層原理

2009-08-04 14:18:49

ASP.NET郵件列表

2009-08-24 10:37:27

C# 泛型

2009-08-13 18:36:36

C#繼承構(gòu)造函數(shù)

2015-12-02 14:10:56

HTTP網(wǎng)絡(luò)協(xié)議代理原理

2015-12-02 15:29:32

HTTP網(wǎng)絡(luò)協(xié)議代理原理

2022-03-17 08:55:43

本地線程變量共享全局變量

2025-05-27 01:00:00

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號