用原生JS從零到一實現Redux架構

来源:https://www.cnblogs.com/peerless1029/archive/2019/04/28/10781391.html
-Advertisement-
Play Games

前言 最近利用業餘時間閱讀了鬍子大哈寫的《React小書》,從基本的原理講解了React,Redux等等受益頗豐。眼過千遍不如手寫一遍,跟著作者的思路以及參考代碼可以實現基本的Demo,下麵根據自己的理解和參考一些資料,用原生JS從零開始實現一個Redux架構。 一.Redux基本概念 經常用Rea ...


前言

  最近利用業餘時間閱讀了鬍子大哈寫的《React小書》,從基本的原理講解了React,Redux等等受益頗豐。眼過千遍不如手寫一遍,跟著作者的思路以及參考代碼可以實現基本的Demo,下麵根據自己的理解和參考一些資料,用原生JS從零開始實現一個Redux架構。

一.Redux基本概念

  經常用React開發的朋友可能很熟悉Redux,React-Redux,這裡告訴大家的是,Redux和React-Redux並不是一個東西,Redux是一種架構模式,2015年,Redux出現,將 Flux 與函數式編程結合一起,很短時間內就成為了最熱門的前端架構。它不關心你使用什麼庫,可以把它和React,Vue或者JQuery結合。

二.由一個簡單的例子開始

  我們從一個簡單的例子開始推演,新建一個html頁面,代碼如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Make-Redux</title>
</head>
<body>
<div id="app">
  <div id="title"></div>
  <div id="content"></div>
</div>
<script>
  // 應用的狀態
  const appState = {
    title: {
      text: '這是一段標題',
      color: 'Red'
    },
    content: {
      text: '這是一段內容',
      color: 'blue'
    }
  };

  // 渲染函數
  function renderApp(appState) {
    renderTitle(appState.title);
    renderContent(appState.content);
  }

  function renderTitle(title) {
    const titleDOM = document.getElementById('title');
    titleDOM.innerHTML = title.text;
    titleDOM.style.color = title.color;
  }

  function renderContent(content) {
    const contentDOM = document.getElementById('content');
    contentDOM.innerHTML = content.text;
    contentDOM.style.color = content.color;
  }

  // 渲染數據到頁面上
  renderApp(appState);
</script>
</body>
</html>

 HTML內容很簡單,我們定義了一個appState數據對象,包括title和content屬性,各自都有text和color,然後定義了renderApp,renderTitle,renderContent渲染方法,最後執行renderApp(appState),打開頁面:

這些寫雖然沒有什麼問題,但是存在一個比較大的隱患,每個人都可以修改共用狀態appState,在平時的業務開發中也很常見的一個問題是,定義了一個全局變數,其他同事在不知情的情況下可能會被覆蓋修改刪除掉,帶來的問題是函數執行的結果往往是不可預料的,出現問題的時候調試起來非常困難。

那我們如何解決這個問題呢,我們可以提高修改共用數據的門檻,但是不能直接修改,只能修改我允許的某些修改。於是,定義一個dispatch方法,專門負責數據的修改。

function dispatch (action) {
    switch (action.type) {
      case 'UPDATE_TITLE_TEXT':
        appState.title.text = action.text;
        break;
      case 'UPDATE_TITLE_COLOR':
        appState.title.color = action.color;
        break;
      default:
        break;
    }
  }

這樣我們規定,所有歲數據的操作必須通過dispatch方法。它接受一個對象暫且叫它action,規定只能修改title的文字與顏色。這樣要想知道哪個函數修改了數據,我們直接在dispatch方法裡面斷點調試就可以了。大大的提高瞭解決問題的效率。

三.抽離store和實現監控數據變化

  上面我們的appStore和dispatch分開的,為了使這種模式更加通用化,我們把他們集中一個地方構建一個函數createStore,用它來生產一個store對象,包含state和dispatch。

function createStore (state, stateChanger) {
    const getState = () => state;
    const dispatch = (action) => stateChanger(state, action);
    return { getState, dispatch }
  }

 我們修改之前的代碼如下:

let appState = {
    title: {
      text: '這是一段標題',
      color: 'red',
    },
    content: {
      text: '這是一段內容',
      color: 'blue'
    }
  }

  function stateChanger (state, action) {
    switch (action.type) {
      case 'UPDATE_TITLE_TEXT':
        state.title.text = action.text
        break
      case 'UPDATE_TITLE_COLOR':
        state.title.color = action.color
        break
      default:
        break
    }
  }

  const store = createStore(appState, stateChanger)
  // 首次渲染頁面
  renderApp(store.getState());
  // 修改標題文本
  store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '換一個標題' });
  // 修改標題顏色
  store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'grey' });
   // 再次把修改後的數據渲染到頁面上
  renderApp(store.getState());

 上面代碼不難理解:我們用createStore生成了一個store,可以發現,第一個參數state就是我們之前聲明的共用數據,第二個stateChanger方法就是之前聲明的dispatch用於修改數據的方法。

然後我們調用了來兩次store.dispatch方法,最後又重新調用了renderApp再重新獲取新數據渲染了頁面,如下:可以發現title的文字和標題都改變了。

那麼問題來了,我們每次dispatch修改數據的時候,都要手動的調用renderApp方法才能使頁面得以改變。我們可以把renderApp放到dispatch方法最後,這樣的話,我們的createStore不夠通用,因為其他的App不一定要執行renderApp方法,這裡我們通過一種監聽數據變化,然後再重新渲染頁面,術語上講叫做觀察者模式。

我們修改createStore如下。

function createStore (state, stateChanger) {
    const listeners = []; // 空的方法數組
    // store調用一次subscribe就把傳入的listener方法push到方法數組中
    const subscribe = (listener) => listeners.push(listener); 
    const getState = () => state;
    // 當store調用dispatch的改變數據的時候遍歷listeners數組,執行其中每一個方法,到達監聽數據重新渲染頁面的效果
    const dispatch = (action) => {
      stateChanger(state, action);
      listeners.forEach((listener) => listener())
    };
    return { getState, dispatch, subscribe }
  }

 

再次修改上一部分的代碼如下:

// 首次渲染頁面
  renderApp(store.getState());
  // 監聽數據變化重新渲染頁面
  store.subscribe(()=>{
    renderApp(store.getState());
  });
  // 修改標題文本
  store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '換一個標題' });
  // 修改標題顏色
  store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'grey' });

 我們在首次渲染頁面後只需要subscribe一次,後面dispatch修改數據,renderApp方法會被重新調用,實現了監聽數據自動渲染數據的效果。

三.生成一個共用結構的對象來提高頁面的性能

上一節我們每次調用renderApp方法的時候實際上是執行了renderTitle和renderContent方法,我們兩次都是dispatch修改的是title數據,可是renderContent方法也都被一起執行了,這樣執行了不必要的函數,有嚴重的性能問題,我們可以在幾個渲染函數上加上一些Log看看實際上是不是這樣的

 

function renderApp (appState) {
  console.log('render app...')
  ...
}
function renderTitle (title) {
  console.log('render title...')
  ...
}
function renderContent (content) {
  console.log('render content...')
 ...
}

 瀏覽器控制台列印如下:

  

 

解決方案是:我們在每個渲染函數執行之前對其傳入的數據進行一個判斷,判斷傳入的新數據和舊數據是否相同,相同就return不渲染,否則就渲染。

  // 渲染函數
  function renderApp (newAppState, oldAppState = {}) { // 防止 oldAppState 沒有傳入,所以加了預設參數 oldAppState = {}
    if (newAppState === oldAppState) return; // 數據沒有變化就不渲染了
    console.log('render app...');
    renderTitle(newAppState.title, oldAppState.title);
    renderContent(newAppState.content, oldAppState.content);
  }
  function renderTitle (newTitle, oldTitle = {}) {
    if (newTitle === oldTitle) return; // 數據沒有變化就不渲染了
    console.log('render title...');
    const titleDOM = document.getElementById('title');
    titleDOM.innerHTML = newTitle.text;
    titleDOM.style.color = newTitle.color;
  }
  function renderContent (newContent, oldContent = {}) {
    if (newContent === oldContent) return; // 數據沒有變化就不渲染了
    console.log('render content...');
    const contentDOM = document.getElementById('content')
    contentDOM.innerHTML = newContent.text;
    contentDOM.style.color = newContent.color;
  }
  ...
  let oldState = store.getState(); // 緩存舊的 state
  store.subscribe(() => {
    const newState = store.getState(); // 數據可能變化,獲取新的 state
    renderApp(newState, oldState); // 把新舊的 state 傳進去渲染
    oldState = newState // 渲染完以後,新的 newState 變成了舊的 oldState,等待下一次數據變化重新渲染
  })
...

以上代碼我們在subscribe的時候先用oldState緩存舊的state,在dispatch之後執行裡面的方法再次獲取新的state然後oldState和newState傳入到renderApp中,之後再用oldState保存newState。

好,我們打開瀏覽器看下效果:

 

控制台只列印了首次渲染的幾行日誌,後面兩次dispatch數據之後渲染函數都沒有執行。這說明oldState和newState相等了。

 

通過斷點調試,發現newAppState和oldAppState是相等的。

究其原因,因為對象和數組是引用類型,newState,oldState指向同一個state對象地址,在每個渲染函數判斷始終相等,就return了。

解決方法:appState和newState其實是兩個不同的對象,我們利用ES6語法來淺複製appState對象,當執行dispatch方法的時候,用一個新對象覆蓋原來title裡面內容,其餘的屬性值保持不變。形成一個共用數據對象,可以參考以下一個demo:

我們修改stateChanger讓它修改數據的時候,並不會直接修改原來的數據 state,而是產生上述的共用結構的對象:

function stateChanger (state, action) {
    switch (action.type) {
      case 'UPDATE_TITLE_TEXT':
        return { // 構建新的對象並且返回
          ...state,
          title: {
            ...state.title,
            text: action.text
          }
        }
      case 'UPDATE_TITLE_COLOR':
        return { // 構建新的對象並且返回
          ...state,
          title: {
            ...state.title,
            color: action.color
          }
        }
      default:
        return state // 沒有修改,返回原來的對象
    }
  }

因為stateChanger不會修改原來的對象了,而是返回一個對象,所以修改createStore裡面的dispatch方法,執行stateChanger(state,action)的返回值來覆蓋原來的state,這樣在subscribe執行傳入的方法在dispatch調用時,newState就是stateChanger()返回的結果。

function createStore (state, stateChanger) {
    ...
    const dispatch = (action) => {
      state=stateChanger(state, action);
      listeners.forEach((listener) => listener())
    };
    return { getState, dispatch, subscribe }
  }

 再次運行代碼打開瀏覽器:

發現後兩次store.dispatch導致的content重新渲染不存在了,優化了性能。

 四.通用化Reducer

appState是可以合併到一起的

function stateChanger (state, action) {
    if(state){
      return {
        title: {
          text: '這是一個標題',
          color: 'Red'
        },
        content: {
          text: '這是一段內容',
          color: 'blue'
        }
      }
    }
    switch (action.type) {
      case 'UPDATE_TITLE_TEXT':
        return { // 構建新的對象並且返回
          ...state,
          title: {
            ...state.title,
            text: action.text
          }
        }
      case 'UPDATE_TITLE_COLOR':
        return { // 構建新的對象並且返回
          ...state,
          title: {
            ...state.title,
            color: action.color
          }
        }
      default:
        return state // 沒有修改,返回原來的對象
    }
  }

 再修改createStore方法:

function createStore (stateChanger) {
    let state = null;
    const listeners = []; // 空的方法數組
    // store調用一次subscribe就把傳入的listener方法push到方法數組中
    const subscribe = (listener) => listeners.push(listener);
    const getState = () => state;
    // 當store調用dispatch的改變數據的時候遍歷listeners數組,執行其中每一個方法,到達監聽數據重新渲染頁面的效果
    const dispatch = (action) => {
      state=stateChanger(state, action);
      listeners.forEach((listener) => listener())
    };
    dispatch({}); //初始化state
    return { getState, dispatch, subscribe }
  }

初始化一個局部變數state=null,最後手動調用一次dispatch({})來初始化數據。

stateChanger這個函數也可以叫通用的名字:reducer。為什麼叫reducer? 參考阮一峰的《redux基本用法》裡面對reducder的講解;

五:Redux總結

以上是根據閱讀《React.js小書》再次復盤,通過以上我們由一個簡單的例子引入用原生JS能大概的從零到一完成了Redux,具體的使用步驟如下:

// 定一個 reducer
function reducer (state, action) {
/* 初始化 state 和 switch case */
}
// 生成 store
const store = createStore(reducer)
// 監聽數據變化重新渲染頁面
store.subscribe(() => renderApp(store.getState()))
// 首次渲染頁面
renderApp(store.getState())
// 後面可以隨意 dispatch 了,頁面自動更新
store.dispatch(...)

按照定義reducer->生成store->監聽數據變化->dispatch頁面自動更新。

下麵兩幅圖也能很好表達出Redux的工作流程

使用Redux遵循的三大原則:

1.唯一的數據源store

2.保持狀態的store只讀,不能直接修改應用狀態

3.應用狀態的修改通過純函數Reducer完成

當然不是每個項目都要使用Redux,一些小心共用數據較少的沒必要使用Redux,視項目大小複雜度而定,具體什麼時候使用?引用一句話:當你不確定是否使用Redux的時候,那就不要用Redux。

項目完整代碼地址make-redux

六.寫在最後

  每一個工具或框架都是在一定的條件下為瞭解決某種問題產生的,在閱讀幾遍《React.js》小書之後,終於對React,Redux等一些基本原理有了一些瞭解,深感作為一個coder,不能只CV,記憶一些框架API會用就行,知其然不可,更要知其所以然,這樣我們在完成項目才能更好的優化又能,是代碼寫的更加優雅。有什麼錯誤的地方,敬請指正,技術想要有質的飛躍,就要多學習,多思考,多實踐,與君共勉。

 


 

參考資料:

1.《React.js小書》-鬍子大哈

2.React進階之路-徐超

3.Redux 入門教程(一):基本用法-阮一峰

 

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 閱讀目錄 一、Hive內部表和外部表 1、Hive的create創建表的時候,選擇的創建方式: - create table - create external table 2、特點: ● 在導入數據到外部表,數據並沒有移動到自己的數據倉庫目錄下,也就是說外部表中的數據並不是由它自己來管理的!而表則 ...
  • Student: Course: SC: 1.資料庫的定義、刪除 1.1資料庫的定義(創建) 1 CREATE DATABASE student; 1.2資料庫的刪除 1 DROP DATABASE student; 2.表的定義、修改與刪除 2.1表的定義 建表語句: 1 CREATE TABLE ...
  • 原理: MySQL主從複製涉及到三個線程,一個運行在主節點(log dump thread),其餘兩個(I/O thread, SQL thread)運行在從節點,如下圖所示: l 主節點 binary log dump 線程 當從節點連接主節點時,主節點會創建一個log dump 線程,用於發送b ...
  • 首先百度搜索阿裡雲 如果是學生可以學生認證 然後註冊賬號->個人認證->學生認證 然後你會發現 伺服器一年只要114,114你買不了上當,買不了吃虧,買下麵的ECS伺服器,系統可以選擇window也可以選擇linux,編者用的centos7。 當然輕量級的應用伺服器也是可以的,這些輕量級伺服器會預裝 ...
  • Mysql子查詢 概念分析: 根據相關性分: (1)不相關子查詢:一條Sql語句中含有多條SELECT語句,先執行子查詢,再執行外查詢,子查詢可對立運行 關鍵字:(1)先子查詢,再外查詢 (2)可以對立運行,即可以單獨運行子查詢,對外查詢不幹擾 (2)相關子查詢:子查詢不能獨立運行,並且先運行外查詢 ...
  • 初學者都會接觸到三種表:emp、dept、salgrade表,進行練習各種語句操作再合適不過 但是,網上大多數的操作語句都是用oracle進行操作的,小編在學習mysql的時候,參考網上的書寫遇到了不少問題 都是由於oracle語句和mysql語句的不相容的引起的。 寫多行sql語句的時候或者嵌套查 ...
  • 第一題 代碼生成表格如: 根據以上代碼生成的表寫出一條查詢語句,查詢結果如下: 第二題 第三題 第四題(這道題難度相對較高) ...
  • 版權聲明:本文為HaiyuKing原創文章,轉載請註明出處! 前言 使用Poi實現android中根據模板文件生成Word文檔的功能。這裡的模板文件是doc文件。如果模板文件是docx文件的話,請閱讀下一篇文章《PoiDocxDemo【Android將表單數據生成Word文檔的方案之二(基於Poi4 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...