一種JavaScript響應式系統設計與實現

来源:https://www.cnblogs.com/CherishTheYouth/archive/2022/06/28/CherishTheYouth_20220628.html
-Advertisement-
Play Games

一種JavaScript響應式系統實現 根據VueJs核心團隊成員霍春陽《Vue.js設計與實現》第四章前三節整理而成 1. 響應式數據與副作用函數 1.1 副作用函數 會產生副作用的函數。 如下示例所示: function effect () { document.body.innerText = ...


一種JavaScript響應式系統實現

根據VueJs核心團隊成員霍春陽《Vue.js設計與實現》第四章前三節整理而成

1. 響應式數據與副作用函數

1.1 副作用函數

會產生副作用的函數。

如下示例所示:

function effect () {
    document.body.innerText = 'hello vue3!'
}

當effect函數執行時,會設置body的文本內容。但是,除了effect函數之外的任何函數都可以讀取或者設置body的文本內容。也就是說,effect函數的執行,會直接或間接影響其他函數的執行,這時,我們說effect函數產生了副作用。

// 此方法的結果就受effect函數的影響
function getInnerText () {
    return document.body.innerText
}

副作用很容易產生,例如一個函數修改了全局變數,這其實也是一個副作用,如下麵代碼所示:

let globalValue = 1
function effect () {
    globalValue = 3 // 修改全局變數,產生副作用
}

1.2 響應式數據

理解了什麼是副作用函數,再來說一說什麼是響應式數據。

假設在一個副作用函數中讀取了某個對象的屬性:

const obj = {
    text: 'hello vue2'
}
function effect () {
    document.body.innerText = obj.text // effect 函數的執行會讀取obj.text
}

如上面代碼所示,副作用函數effect會設置body元素的innerText屬性,其值為obj.text。當obj.text的值改變時,我們希望副作用函數effect()會重新執行:

obj.text = 'hello vue3' // 修改obj.text的值,同時希望副作用函數重新執行

這句代碼修改了欄位 obj.text 的值,我們希望當值變化後,副作用函數會重新執行,如果能實現這個目標,那麼對象 obj 就是響應式數據。(某數據改變時,依賴該數據的副作用函數會重新執行,該數據即為響應式數據

但是,從上面代碼來看,我們還做不到這一點,因為obj是一個普通的對象,當我們修改它的值時,除了值本身發生變化外,不會有任何其他反應。

2. 響應式數據的基本實現

2.1 如何讓 obj 變為響應式數據?

通過觀察,我們可以發現 2 點線索:

  • 當副作用函數effect()執行時,會觸發欄位 obj.text 的讀取操作;
  • 當修改 obj.text 的值時,會觸發欄位 obj.text設置操作;

​ 如果我們能攔截一個對象的讀取設置操作,事情就變得簡單了。

  • 當讀取obj.text 時,我們可以把副作用函數effect()存儲到一個“桶”里;

image-20220627151018125

  • 當設置obj.text時,再把effect()副作用函數從 “桶” 里取出,並執行;
image-20220627151637370

2.2 如何攔截對象屬性的讀取和設置操作

在 es5 及之前,只能通過 Obj.defineProperty 來實現,這也是vue2中所採用的方式。

es6以後,可以使用代理對象 Proxy 來實現,這是vue3中的所採用的方式。

實現步驟:

  • 創建一個用於存儲副作用函數的容器(或者通俗點說,一個桶,vue3里用了bucket這個詞,以後都稱為桶);
  • 定義一個普通對象obj作為原始數據;
  • 創建 obj 對象的代理作為響應式數據,分別設置 getset 攔截函數,用於攔截讀和寫操作;
  • 當在effect副作用函數中執行響應式數據讀操作時,將effect()副作用函數存到桶里;
  • 當對響應式數據進行寫操作時,先更新原始數據,再從桶中取出依賴了響應式數據的函數,進行執行;

接下來,根據以上思路,使用Proxy 實現一下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <p id="title"></p>
    <button onclick="changeObj()">修改響應式數據</button>
  </div>
</body>
</html>
<script>
  // 1. 創建一個存儲副作用函數的桶      
  const bucket = new Set()
  // 2. 一個普通的對象
  const obj = {
    text: 'hello vue2'
  }
  // 3. 普通對象的代理,一個簡單的響應式數據
  const objProxy = new Proxy(obj, {
    get (target, key) {
      bucket.add(effect)
      return target[key]
    },
    set (target, key, newValue) {
      target[key] = newValue
      bucket.forEach((fn) => {
        fn()
      })
      return true
    }
  })

  // 4. 副作用函數 effect(), 給p標簽設置值
  const effect = function () {
    document.getElementById('title').innerText = objProxy.text
  }

  // 5. 改變代理對象的元素值
  const changeObj = function () {
    objProxy.text = 'hello vue3!!!!!!'
  }

  // 首次進入時,給p標簽設置值
  effect()
</script>

響應式-show-1

2.3 缺陷

  • 副作用函數名字叫effect ,是硬編碼的,假如叫其他的名字,就無法正確執行了;

3. 完善的響應式系統

3.1 如何存儲一個任意命名(甚至匿名)的副作用函數?

需要提供一個用來註冊副作用函數的機制,如以下代碼所示:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <p id="title"></p>
    <button onclick="changeObj()">修改響應式數據</button>
  </div>
</body>
</html>
<script>
let activeEffect
function effectRegister (fn) {
  activeEffect = fn
  fn()
}

const bucket = new Set()
const obj = {
  text: 'hello vue2'
}
const objProxy = new Proxy(obj, {
  get (target, key) {
    if (activeEffect) {
      bucket.add(activeEffect)
    }
    return target[key]
  },
  set (target, key, newValue) {
    target[key] = newValue
    bucket.forEach((fn) => {
      fn()
    })
    return true
  }
})
const changeObj = function () {
  objProxy.text = 'hello vue3!!!!!!'
}

effectRegister(() => {
  document.getElementById('title').innerText = objProxy.text
})
</script>

以上方式,通過定義一個全局變數activeEffect,用來存儲(匿名)副作用函數。並提供一個註冊副作用函數的註冊函數 effectRegister,該函數是一個高階函數,接受一個函數作為參數,保存並執行該函數。將(匿名)副作用函數保存到activeEffect 中,當(匿名)副作用函數執行時,觸發響應式數據的讀操作,此時將activeEffect 存入副作用函數桶中。

缺陷:

以上方案成功解決了匿名的函數的保存問題,但仍存在一個嚴重問題:

響應式對象內的不同屬性和不同副作用函數的對應問題

下麵給響應式數據添加一個屬性,以上代碼微調為如下:

// ... 省略未改變代碼

const setObj = function (key, value) {
  objProxy[key] = value
}

const changeObj = function () {
  setObj('text', 'hello vue3!!!!!!')
  setObj('notExist', 'this key is not exist')
  console.log(1, bucket)
}

effectRegister(() => {
  console.log('執行')
  document.getElementById('title').innerText = objProxy.text
})

執行結果為:

image-20220627190632726

可以看到,此時副作用函數與obj.notExist 屬性並未建立響應關係,但當給 notExist 賦值時,副作用函數也執行了,這顯然不對了。

理想情況應該是,a屬性與aFunc建立響應式關係,b屬性與bFunc建立響應式聯繫,則 a 改變時,僅 aFunc函數觸發執行,b改變時,僅bFunc觸發執行。

3.2 如何解決響應式對象內的不同屬性和不同副作用函數的對應問題?

問題分析:

導致該問題的根本原因是,我們沒有在副作用函數與被操作的目標欄位之間建立明確的聯繫

例如,但讀取屬性值時,無論讀取到哪一個屬性,其實都一樣,都會把副作用函數收集到“桶”里;無論設置的是哪一個屬性,也會把“桶”裡面的副作用函數取出並執行。響應數據屬性和副作用函數之間沒有明確的聯繫。

解決方案很簡單,只需要在副作用函數與被操作的欄位之間建立聯繫即可。

要實現不同屬性值與副作用函數對應,Set類型的數據作為桶已經明顯不合適了。

科普下es6幾大數據類型:

Set:ES6 提供了新的數據結構 Set。它類似於數組,但是成員的值都是唯一的,沒有重覆的值。

WeakSet:WeakSet 結構與 Set 類似,也是不重覆的值的集合。但是,它與 Set 有兩個區別。首先,WeakSet 的成員只能是對象,而不能是其他類型的值。其次,WeakSet 中的對象都是弱引用,即垃圾回收機制不考慮 WeakSet 對該對象的引用,也就是說,如果其他對象都不再引用該對象,那麼垃圾回收機制會自動回收該對象所占用的記憶體,不考慮該對象還存在於 WeakSet 之中。

Map:它類似於對象,也是鍵值對的集合,但是“鍵”的範圍不限於字元串,各種類型的值(包括對象)都可以當作鍵。也就是說,Object 結構提供了“字元串—值”的對應,Map 結構提供了“值—值”的對應,是一種更完善的 Hash 結構實現。

WeakMapWeakMap結構與Map結構類似,也是用於生成鍵值對的集合。WeakMapMap的區別有兩點。

首先,WeakMap只接受對象作為鍵名(null除外),不接受其他類型的值作為鍵名。其次,WeakMap的鍵名所指向的對象,不計入垃圾回收機制。

新的方案:

首先,來看下如下代碼:

  effectRegister(function effectFn () {
    document.getElementById('title').innerText = objProxy.text
  })

這段代碼存在三個角色:

  • 被讀取的響應式數據 objProxy;
  • 被讀取的響應式數據的屬性名 text;
  • 使用effectRegister 函數註冊的副作用函數 effectFn;

如果使用 target 來表示一個代理對象所代理的原始對象,用key 來表示被操作的欄位名,用effectFn 來表示被註冊的副作用函數,那麼可以為這三個角色建立如下關係:

image-20220627213637491

這是一種樹型結構,下麵舉幾個例子進行補充說明:

  1. 如果有2個副作用函數同時讀取同一個對象的屬性值:

    effectRegister(function effectFn1 () {
        obj.text
    })
    
    effectRegister(function effectFn2 () {
        obj.text
    })
    

​ 那麼這三者關係如下:

image-20220627214317647

  1. 如果一個副作用函數中讀取了同一個對象的 2 個不同屬性:

    effectRegister(function effectFn () {
        obj.text1
        obj.text2
    }
    

那麼這三者關係如下:

image-20220627214620629

  1. 如果 2 個不同的副作用函數中讀取了 2 個不同對象的不同屬性:

    effectRegister(function effectFn1 () {
        obj1.text1
    })
    
    effectRegister(function effectFn2 () {
        obj2.text2
    })
    

    那麼這三者關係如下:

    image-20220627215041694

總之,這其實就是一個樹形數據結構。這個聯繫建立起來後,就可以解決前文提到的問題。上文中,如果我們設置了obj2.text2的值,就只會導致 effectFn2 函數重新執行,並不會導致 effectFn1 函數重新執行。

接下來,需要重新設計這個桶。根據對以上幾種情況的分析,我們可以總結一下這種數據結構的模型:

image-20220628103144724

首先,使用WeakMap 代替 Set 來作為桶,將 原始對象 target 作為WeakMap 的key,使用 Map 作為value。在Map 中又以屬性值 作為 key值,使用 Set 存儲key 對應的副作用函數 effectFn,然後修改 get/set 攔截器代碼。

以下,我們將桶命名為 bucket, 每個對象的 屬性-副作用函數 存儲塊 命名為 depsMap,將depsMap中的value部分(Set類型,存儲副作用函數)命名為 deps(與vue3響應式實現源碼命名保持一致),根據數據結構圖,重新編寫get/set 攔截器代碼:

js:

// WeakMap 類型的桶
const bucket = new WeakMap()
// 初始值為undefined
let activeEffect
// 定義2個對象
const obj_1 = {
    text1: 'hello vue',
    text2: 'hello jquery'
}

const obj_2 = {
    text1: 'hello react',
    text2: 'hello angular'
}

const objProxy1 = new Proxy(obj_1, {
    get (target, key) {
        // 如果沒有註冊的副作用函數,直接返回key對應的value值
        if (!activeEffect) {
            return target[key]
        }
        
        // 獲取桶內 target 對象對應的 depsMap
        let depsMap = bucket.get(target)
        // 如果該對象還沒有depsMap,這新建一個
        if (!depsMap) {
            depsMap = new Map()
            bucket.set(target, depsMap)
        }
        
        // 從depsMap中取出屬性對應的副作用函數集合
        let deps = depsMap.get(key)
        // 同理,若不存在,則創建
        if (!deps) {
            deps = new Set()
            depsMap.set(key, deps)
        }
        deps.add(activeEffect)
        return target[key]
    },
    set (target,key, newValue) {
        // 給target的key設置新的value
        target[key] = newValue
        
        // 取出該target對應的depsMap
        let depsMap = bucket.get(target)
        // depsMap 不存在或為空直接返回
        if (!depsMap) {
            return
        }
        
        // 取出deps
        let deps = depsMap.get(key)
        // deps 不存在或為空直接返回
        if (!deps) {
            return
        }
        // 依次執行對應副作用函數
        deps.forEach((fn) => {
            fn()
        })
        return true
    }
})

const objProxy2 = new Proxy(obj_2, {
    ...
})

const effectRegister = function (fn) {
    activeEffect = fn
    fn()
}

演示:

響應式-show-3

3.3 思考:為什麼桶的結構使用WeakMap而不是Map?

看一段代碼:

const map = new Map()
const weakmap = new WeakMap()
;(function () {
    const foo = { foo: 1}
    const bar = { bar: 2}
    map.set(foo, 1)
    weakmap.set(bar, 2)
})();
console.log(map)
console.log(weakmap)

思考,列印結果分別是啥?

註意:在瀏覽器環境下,console列印結果表現出非同步行為。

image-20220628140308855

原因分析:

Map對key是強引用,當立即執行函數結束後,foo仍被map引用,因此map.keys()可以成功列印出key值,而WeakMap對key是弱引用,立即執行函數結束後,bar 失去引用,被垃圾回收器回收掉,因此weakmap.keys()無法取出key值。

結論:

WeakMap經常存儲那些只有當key值所引用的對象存在(沒有被垃圾回收)時才有價值的信息,例如示例中的target,如果target對象沒有任何引用了,說明用戶測不需要它了,這是垃圾回收器會完成回收任務。

如果使用Map來代替WeakMap,那即使用戶側沒有任何對target的引用,這個target也不會被回收,最終可能導致記憶體溢出。

4. 總結

以上,我們實現了一個簡單的響應系統,核心思路是通過代理來攔截響應數據的讀和寫的操作,將與對象屬性相關的副作用函數進行存儲,當對象屬性變化時,同步執行相關聯的副作用函數,達到響應式的效果。

其實現中混合使用了代理模式和觀察者模式。

5. 參考資料

附:程式源碼

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <p id="title_1"></p>
    <p id="title_2"></p>
    <button onclick="setObj_1()">修改響應式數據1</button>
    <button onclick="setObj_2()">修改響應式數據2</button>
  </div>
</body>
</html>
<script>
  let activeEffect
  let count = 3
  let version = 3
  const bucket = new WeakMap()
  const obj_1 = {
    text1: 'hello vue2',
    text2: 'hello jq2'
  }

  const obj_2 = {
    text2: 'hello react16',
    text1: 'hello ng2'
  }

  const getProxy = function (obj) {
    return new Proxy(obj, {
      get (target, key) {
        track(target, key)
        return target[key]
      },
      set (target, key, newValue) {
        trigger(target, key, newValue)
        return true
      }
    })
  }

  const track = function (target, key) {
    // 如果無副作用函數,則直接返回原始對象值
    if (!activeEffect) {
      return
    }

    // 以該代理對象的原始對象作為key值,獲取depsMap  (屬性和副作用函數之間的對應關係 key --> effectFn)
    let depsMap = bucket.get(target)
    // 如果不存在depsMap,則新建一個Map與target關聯起來
    if (!depsMap) {
      depsMap = new Map()
      bucket.set(target, depsMap)
    }
    // 根據key從depsMap中獲取對應的deps,deps是一個Set
    let deps = depsMap.get(key)
    // 如果不存在,則新建Set,並與key關聯
    if (!deps) {
      deps = new Set()
      depsMap.set(key, deps)
    }
    // 最後將當前活躍的副作用函數添加到桶里
    deps.add(activeEffect)
  }

  const trigger = function (target, key, newValue) {
    target[key] = newValue
    let depsMap = bucket.get(target)
    if (!depsMap) {
      return
    }
    let deps = depsMap.get(key)
    if (!deps) {
      return
    }

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

  const objProxy1 = getProxy(obj_1)
  const objProxy2 = getProxy(obj_2)

  const effectRegister = function (fn) {
    activeEffect = fn
    fn()
  }

  const setObj_1 = function () {
    objProxy1.text1 = `hello, vue${count++}`
    console.log('bucket-1', bucket)
  }

  const setObj_2 = function () {
    objProxy2.text2 = `hello, react${version++}`
    console.log('bucket-2', bucket)
  }

  console.log('bucket-init', bucket)
  effectRegister(function effectFn1 () {
    document.getElementById('title_1').innerText = objProxy1.text1
  })

  effectRegister(function effectFn2 () {
    document.getElementById('title_2').innerText = objProxy2.text2
  })
</script>
作者:CherishTheYouth 出處:https://www.cnblogs.com/CherishTheYouth/ 聲明:本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。對於本博客如有任何問題,可發郵件與我溝通,我的QQ郵箱是:[email protected]
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 原文鏈接:數據資產為王,解析企業數字化轉型與數據資產管理的關係 視頻回顧:點擊這裡 課件獲取:點擊這裡 一、數據資產背景介紹 隨著企業數字化轉型的深入,數據體量爆炸式增長,如何控制數據生產成本、發現有價值的數據,提高數據ROI,成了企業數字化轉型中後期的關鍵任務,這也是數據資產管理的終極目標。 而在 ...
  • 介紹 作為人類,我們天生就想要即刻擁有東西。即時滿足感是一種強大的力量,當前的實時經濟反映了這一點。業務方之間的事務在實時的數字化,逐漸自動化(M2M通信、物聯網、人工智慧和機器學習)並完成。 根據Volt Active Data的​​等待心理調查​​,當被問及“實時意味著什麼”時,超過70%的受訪 ...
  • 今天日誌出現異常,一步一步debug發現SQL語句返回值出錯,進一步發現是max()函數返回出錯。點擊跳轉解決辦法,趕時間的朋友可以去獲得答案。當然我還是希望大伙看看原由。 select max(HTMBXH) from biz_mn_contract_temp; 返回值按理應該是10 ,結果返回了 ...
  • 體驗簡介 場景將提供一臺配置了CentOS 8.5操作系統的ECS實例(雲伺服器)。通過本教程的操作帶您體驗如何將PolarDB-X通過Canal與ClickHouse進行互通,搭建一個實時分析系統。點擊前往 實驗準備 1. 創建實驗資源 開始實驗之前,您需要先創建ECS實例資源。 在實驗室頁面,單 ...
  • 你在鍛煉健身時,有沒有遇到這樣的情況?辛辛苦苦鍛煉了幾小時,卻發現App停止了運行,本次運動並沒有被記錄到App上,從而失去了一個查看完整運動數據的機會? 運動類App是通過手機或者穿戴設備的感測器,來識別運動狀態並反饋給用戶的,App能否在手機後臺時刻保持運行是影響運動數據完整性的關鍵因素。為了滿 ...
  • 視頻鏈接: JavaScript var let const的區別 - Web前端工程師面試題講解 參考鏈接: JavaScript 變數 JavaScript Let JavaScript Const 練習網站: codepen.io 初步認識: 功能實現 HTML的部分: <input type ...
  • 1、CSS畫一個三角形:(div寬高為0,border存在且顏色不一) step1: 設置寬度,高度為 0 的一個div盒子; step2: 為了方便理解,將盒子的 4 個邊框分別設置一樣的寬度boder,不同的顏色; step3: transparent將其他三個 邊框隱藏掉,就能看到效果了。 如 ...
  • Vue 框架通過數據雙向綁定和虛擬 DOM 技術,幫我們處理了前端開發中最臟最累的 DOM 操作部分, 我們不再需要去考慮如何操作 DOM 以及如何最高效地操作 DOM;但 Vue 項目中仍然存在項目首屏優化、Webpack 編譯配置優化等問題,所以我們仍然需要去關註 Vue 項目性能方面的優化,使 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...