react-loadable 原始碼解析

Grewer發表於2021-11-01

react-loadable 原始碼解析

簡要的來說, loadable 是一個高階函式, 他同時利用了 react 的渲染 API, webpack 知識點, babel, promise 合併起來的元件

使用

首先我們要知道 react-loadable 他的用法是什麼:

  1. loader
    需要延遲載入的元件, 使用必須要用 () => import('xxx') 語法
  2. loading
    loading 元件, props 接受 error, pastDelay , timedOut, retry引數, 可以自定義
  3. delay
    可以新增延遲
  4. timeout
    超時時間
  5. render
    型別為: (loaded, props)=>ReactNode, 可以新增額外的引數注入

多個元件同時載入

其中接受的引數 loader, render, 型別和上述差不太多

Loadable.Map({
  loader: {
    Bar: () => import('./Bar'),
    i18n: () => fetch('./i18n/bar.json').then(res => res.json()),
  },
  render(loaded, props) {
    let Bar = loaded.Bar.default;
    let i18n = loaded.i18n;
    return <Bar {...props} i18n={i18n}/>;
  },
})

預載入

const LoadableBar = Loadable({
  loader: () => import('./Bar'),
  loading: Loading,
});


觸發:
LoadableBar.preload();

庫裡還涉及到 SSR 相關知識點, 這裡就不涉及了

原始碼

這裡因為不講 SSR 相關, 所以我在這裡刪除了相關程式碼: loadable.jsx

主體

在此高階元件中, 他的主體是: createLoadableComponent

首先我們看他閉包中所作的東西:

function createLoadableComponent(loadFn, options) {
    // loading 的判斷, 忽略

    // 建立配置項, 覆蓋預設值
    // 其中 render 原始碼:  function render(loaded, props) {
    //     return React.createElement(resolve(loaded), props);
    // }
    let opts = Object.assign(
        {
            loader: null,
            loading: null,
            delay: 200,
            timeout: null,
            render: render,
            webpack: null,
            modules: null
        },
        options
    );
    
    // 結果, 用於 呼叫 loader
    let res = null;
    
    // 初始化時呼叫, loadFn 函式後面再講
    function init() {
        if (!res) {
            res = loadFn(opts.loader);
        }
        return res.promise;
    }
    
    return class LoadableComponent extends React.Component{
        // 這裡先忽略
    }
}

返回元件

我們再來看返回的元件:

class LoadableComponent extends React.Component {
        constructor(props) {
            super(props);
            init(); // 在建構函式中啟用初始化函式, 他將 res 賦值為一個 promise
            
            // 定義的 state
            this.state = {
                error: res.error,
                pastDelay: false,
                timedOut: false,
                loading: res.loading,
                loaded: res.loaded
            };
        }
        
        // 靜態函式, 之前介紹用法的時候說過了
        static preload() {
            return init();
        }

        componentWillMount() {
            // 用來設定定時器和 delay 相關
            this._loadModule();
        }

        componentDidMount() {
            // 標記是否mounted
            this._mounted = true;
        }
        
        componentWillUnmount() {
            // 修改標記, 清除定時器
            this._mounted = false;
            this._clearTimeouts();
        }

        render() {
            // 渲染函式, 如果當前是 載入中或者錯誤載入的狀態 , 則使用 loading 渲染, 並且傳遞多種引數
            if (this.state.loading || this.state.error) {
                return React.createElement(opts.loading, {
                    isLoading: this.state.loading,
                    pastDelay: this.state.pastDelay,
                    timedOut: this.state.timedOut,
                    error: this.state.error,
                    retry: this.retry
                });
            } else if (this.state.loaded) {
                // 如果已經載入完畢, 則呼叫 render 函式, 使用 React.createElement 渲染
                return opts.render(this.state.loaded, this.props);
            } else {
                return null;
            }
        }
    }

load

// 這裡的 load 就是 createLoadableComponent(loadFn, options) 中的入參loadFn
function load(loader) {
    // loader 是 options 中的 loader
    // 比如: () => import('./my-component')
    let promise = loader();
    
    // 用來返回結果
    let state = {
        loading: true,
        loaded: null,
        error: null
    };
    
    // 一個 promise 賦值, 未呼叫
    state.promise = promise
        .then(loaded => {
            state.loading = false;
            state.loaded = loaded;
            return loaded;
        })
        .catch(err => {
            state.loading = false;
            state.error = err;
            throw err;
        });

    return state;
}

loadMap

呼叫:

Loadable.Map = LoadableMap;

function LoadableMap(opts) {
    return createLoadableComponent(loadMap, opts);
}

具體程式碼:

function loadMap(obj) {
    let state = {
        loading: false,
        loaded: {},
        error: null
    };

    let promises = [];

    try {
        Object.keys(obj).forEach(key => {
            let result = load(obj[key]);

            if (!result.loading) {
                state.loaded[key] = result.loaded;
                state.error = result.error;
            } else {
                state.loading = true;
            }

            promises.push(result.promise);

            result.promise
                .then(res => {
                    state.loaded[key] = res;
                })
                .catch(err => {
                    state.error = err;
                });
        });
    } catch (err) {
        state.error = err;
    }

    state.promise = Promise.all(promises)
        .then(res => {
            state.loading = false;
            return res;
        })
        .catch(err => {
            state.loading = false;
            throw err;
        });

    return state;
}

總的來說, 和 load 類似, 利用了 Promise.all api 來構建了一個 promise 陣列結果

總結

從元件來上看結構:

Loadable() === createLoadableComponent(load, opts) === class LoadableComponent

從呼叫上來看:

  1. init 呼叫 load 函式, 他用來包裝 元件載入後的引數
  2. init 直接返回元件對應 promise 的結果
  3. 在 render 函式中根據對應結果渲染 loading 元件或者 render 元件
  4. render 元件利用的是 React.createElement 元件渲染

總的來說去除 SSR 相關, 還是一個比較簡單的元件, 主要的利用還是 ()=>import() 語法的支援

參考:

https://github.com/jamiebuild...

相關文章