1. 程式人生 > 程式設計 >利用C語言編寫“剪刀石頭布”小遊戲

利用C語言編寫“剪刀石頭布”小遊戲

React

使用 React Hooks 的無狀態元件,在每次渲染時,包括元件內的函式,都會保留所有 state ,props 在執行時刻的值,這一點和 class 版本的元件具有很大的差異,class 版本會取到最新值。

import React, { Component } from 'react';
class OldApp extends Component {


    constructor(props) {
        super(props);
        this.state = {
            count: 1
        }

    }

    testOnclick = () => {
        setTimeout(() => {
            console.log(this.state.count);
        }, 3000);
    }

    render() {
        return (
            <div>
                <div>
                    oldApp
                </div>
                <div>{this.state.count}</div>
                <button onClick={() => { this.setState({ count: this.state.count+1 }) }} >add</button>
                <button onClick={() => { this.testOnclick(); }} >show</button>
            </div>
        )
    }
}

export default OldApp;

多次點選 add ,交錯點選 show,console 裡輸出最新狀態的 count n 次。

function App() {
  const [count, setCount] = useState({ name: 1 });

  function handleAlertClick() {
    setTimeout(() => {
      alert(count.name);

    }, 3000);
  }

  return (
    <div className="App">
      <OldApp />
      <p>{count.name}</p>
      <button onClick={() => { setCount({ name: count.name + 1 }) }} > add</button>
      <button onClick={() => { handleAlertClick() }} >alert</button>
    </div>
  );
}

export default App;

多次點選 add ,交錯點選 alert ,console 多次出現被呼叫當時的值。

class 版本可以使用閉包修復,實際 Hooks 依賴 JavaScript 閉包。如果希望無狀態元件獲取到最新值,想 class 這樣的表現,可以使用useRef

function App() {
  const [count, setCount] = useState({ num: 1 });
  const lastCount=useRef(count);
  function handleAlertClick() {
    lastCount.current = count;
    setTimeout(() => {
      console.log(lastCount.current.num);
    }, 3000);
  }

  return (
    <div className="App">
      <p>{count.num}</p>
      <button onClick={() => { setCount({ num: count.num + 1 }) }} > add</button>
      <button onClick={() => { handleAlertClick() }} >alert</button>
    </div>
  );
}

React Hooks

  • useMemo:只有在某個依賴項改變時才會重新計算

    const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
    
  • useCallback:把行內函數以及依賴陣列作為引數傳入,將返回回撥函式的 memoized 版本。

    const memoizedCallback = useCallback(
      () => {
        doSomething(a, b);
      },
      [a, b],
    );
    

    直接定義的函式,在每次渲染時其實都會變化,這樣的函式無法作為其他 Hook 的依賴存在,通過 useCallback 定義的函式,可作為其他 hooks 的依賴存在。當有多個 useEffect ,我們希望抽象出 useEffect 相同部分的邏輯,這部分邏輯依賴於 props 或者 state 時,可以考慮使用 useCallback。

  • useContext:上下文

    import React from 'react';
    export default React.createContext(null);
    
    import React, { useEffect, useState, useRef, useReducer, useContext } from 'react';
    import './App.css';
    import Test from './Test';
    import ToDoContext from './ToDoContext';
    
    
    function App() {
      const [count, dispatch] = useReducer(reducer, { num: 1 })
    
      function reducer(state, action) {
        switch (action.type) {
          case 'add':
            return {
              ...state,
              num: state.num + 1
            }
        }
      }
    
      return (
        <ToDoContext.Provider value={{count,dispatch}} >
          <Test></Test>
        </ToDoContext.Provider>
      );
    }
    export default App;
    
    import React, { useContext } from 'react';
    import ToDoContext from './ToDoContext';
    
    function Test() {
        const { count, dispatch } = useContext(ToDoContext);
        return (
            <div>{count.num}</div>
        )
    }
    export default Test;
    

    上下文用於解決 React 層層傳遞資料的問題,被包裹的子元件可以獲取到全域性資料,通常全域性狀態樹會使用到上下文。

  • useEffect:在 render 之後執行的的方法,可以理解為 componentDidMount 或者 componentDidUpdate生命週期經常完成的操作,但是不一樣的是,我們可以通過傳遞依賴的形式,確保程式碼僅在依賴變化時執行,這點我們之前使用shouldComponentUpdate進行props的對比類似。

  • useRef:使用 useRef 建立的物件和之間建立的物件的不同之處在於,useRef 返回的物件,在每次使用時會拿到最新值,而不是當次渲染值。

  • useReducer:當改變狀態的邏輯很複雜時,我們通常使用 useReducer 來實現,而其他地方只需要 dispatch 相應的 type 不需要關心如何改變。同時結合useContext 我們可以做到統一的狀態樹管理

useEffect 詳解

useEffect 用來處理會有副作用的操作,比如之前我們在生命週期函式中常常使用的獲取資料操作。

 useEffect(async () => {
    const result = await axios(
      'https://hn.algolia.com/api/v1/search?query=redux',
    );
 
    setData(result.data);
  });

但是這樣並不理想,因為不經在元件載入時會執行,在元件更新時也會執行,因此,對於只需要在載入階段執行的操作,我們通常給予一個空依賴。

  useEffect(async () => {
    const result = await axios(
      'https://hn.algolia.com/api/v1/search?query=redux',
    );
 
    setData(result.data);
  }, []);

如此在第一次執行之後,useEffect 不會再執行,因為依賴未變化(為空)。

但是這樣依然不完美,useEffect 並不希望函式有返回,而非同步函式實際上會返回一個AsyncFunction ,會報警告,因此我們可以這樣優化。

useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'https://hn.algolia.com/api/v1/search?query=redux',
      );
 
      setData(result.data);
    };
 
    fetchData();
  }, []);

以上操作實際使用 effects 模擬了傳統的生命週期函式ComponentDidMount。而實際上我們應該用不同的眼光來看待useEffect

例如常見的查詢獲取資料操作

  const [search, setSearch] = useState('redux');
 
  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${search}`,
      );
 
      setData(result.data);
    };
 
    fetchData();
  }, [search]);
 

頁面操作改變 search ,依賴於 search 的 effect 重新執行。

當返回一個方法時,會在元件清除階段執行。useEffect 是可選清除方式,不返回方法,預設不需要清除

 useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

錯誤處理

 const [isError, setIsError] = useState(false);
 
  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);
 
      try {
        const result = await axios(url);
 
        setData(result.data);
      } catch (error) {
        setIsError(true);
      }
 
      setIsLoading(false);
    };
 
    fetchData();
  }, [url]);

使用一個 state 存放錯誤,並顯示在頁面

自定義 Hook

我們將獲取資料,錯誤判斷,載入等從 APP 元件抽離,形成一個自定義的 Hooks。

const useHackerNewsApi = () => {
  const [data, setData] = useState({ hits: [] });
  const [url, setUrl] = useState(
    'https://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);
 
  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);
 
      try {
        const result = await axios(url);
 
        setData(result.data);
      } catch (error) {
        setIsError(true);
      }
 
      setIsLoading(false);
    };
 
    fetchData();
  }, [url]);
 
  return [{ data, isLoading, isError }, setUrl];
}

在 App 中使用

function App() {
  const [query, setQuery] = useState('redux');
  const [{ data, isLoading, isError }, doFetch] = useHackerNewsApi();
 
  return (
    <Fragment>
      <form onSubmit={event => {
        doFetch(`http://hn.algolia.com/api/v1/search?query=${query}`);
        event.preventDefault();
      }}>
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>
 
      ...
    </Fragment>
  );
}

同樣可以抽離初始值

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
 
const useDataApi = (initialUrl, initialData) => {
  const [data, setData] = useState(initialData);
  const [url, setUrl] = useState(initialUrl);
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);
 
  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);
 
      try {
        const result = await axios(url);
 
        setData(result.data);
      } catch (error) {
        setIsError(true);
      }
 
      setIsLoading(false);
    };
 
    fetchData();
  }, [url]);
 
  return [{ data, isLoading, isError }, setUrl];
};
 
function App() {
  const [query, setQuery] = useState('redux');
  const [{ data, isLoading, isError }, doFetch] = useDataApi(
    'https://hn.algolia.com/api/v1/search?query=redux',
    { hits: [] },
  );
 
  return (
    <Fragment>
      <form
        onSubmit={event => {
          doFetch(
            `http://hn.algolia.com/api/v1/search?query=${query}`,
          );
 
          event.preventDefault();
        }}
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>
 
      {isError && <div>Something went wrong ...</div>}
 
      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}
 
export default App;

程式碼分片(code-spliting)

大部分應用會被打包到一個檔案中,但是有很多程式碼不一定是首屏需要用到的。當專案越來越大時,我們可以分開打包,在執行時再載入。Code-Spliting 可以幫助我們實現 lazy-load ,減少在初次載入時下載的程式碼數量。

import()

引入 Code-Spliting 的最佳方式在使用 import 的動態引入語法。

之前

import { add } from './math';

console.log(add(16, 26));

之後

import("./math").then(math => {
  console.log(math.add(16, 26));
});

如果是使用 create-react-app 建立的react 應用,當 webpack 處理時,會自動開始 Code-Spliting ,如果是自己從頭建立的,則需要配置 webpack。

React.lazy

React.lazy 允許我們渲染一個動態的 import 像一個普通的元件一樣。

之前

import OtherComponent from './OtherComponent';

之後

const OtherComponent = React.lazy(()=>import('./OtherComponent'));

當 OtherComponet 被渲染時,才會自動載入包含這個元件的編譯檔案。

React.lazy 必須使用一個呼叫了 import() 的函式做為引數,返回一個 promise ,promise resolve 時是模組。動態載入模組需要在 Suspense 元件內使用。

import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

Fallback 屬性接受一個 react component 當子元件在載入時展示,也可以放多個動態載入元件在內。

import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <section>
          <OtherComponent />
          <AnotherComponent />
        </section>
      </Suspense>
    </div>
  );
}

錯誤處理

如果其他模組載入失敗,例如因為網路原因,會觸發一個錯誤,你可以通過展示一個良好使用者體驗的頁面來處理這種錯誤既,Error Boundaries。一旦建立了Error Boundaries 在任何地方展示,當發生錯誤時展示一個錯誤狀態。

import React, { Suspense } from 'react';
import MyErrorBoundary from './MyErrorBoundary';

const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

const MyComponent = () => (
  <div>
    <MyErrorBoundary>
      <Suspense fallback={<div>Loading...</div>}>
        <section>
          <OtherComponent />
          <AnotherComponent />
        </section>
      </Suspense>
    </MyErrorBoundary>
  </div>
);

基於路由的 Code-Spliting

如何妥善使用 Code Spliting 但是不影響使用者的使用體驗,一個合適的實踐是基於路由來處理,一下是使用 React-Router 元件的示例。

import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/about" component={About}/>
      </Switch>
    </Suspense>
  </Router>
);

名稱匯出

當前 React.lazy 僅支援 default export,如果希望使用 name export 可以如下操作

// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));

Context

Context 提供了資料沿元件樹一路傳遞的方法,而不需要手動賦值給 props 一級級傳遞資料

在典型的 react 應用裡,資料通過 props 上下傳遞,但是對於一些特定的資料,例如全球化引數,主題等,很多元件都需要這些資料,Context 提供了在元件中共享資料的方式,而不是在每一級元件中被顯式的賦值。

何時使用 Context

Context 被設計為共享那些被考慮為 "global" 的資料,例如主題,當前使用者等。

// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // Use a Provider to pass the current theme to the tree below.
    // Any component can read it, no matter how deep it is.
    // In this example, we're passing "dark" as the current value.
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // Assign a contextType to read the current theme context.
  // React will find the closest theme Provider above and use its value.
  // In this example, the current theme is "dark".
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

在使用 Context 之前

Context 主要用於在很多不同層級元件共享資料,需要小心使用,因為它使元件重用變的困難。

如果僅僅是想避免在多級元件中傳遞資料, component composition 提供了一個更簡單的解決方案

API

React.createContext

const MyContext = React.createContext(defaultValue);

建立一個 Context 物件,當訂閱了這個 Context 物件的元件被渲染時,會從元件樹中最靠近的 Provider 讀到實時的 Context 值。

Context.Provider

<MyContext.Provider value={/* some value */}>

每個 Context 物件需要和 Provider 元件配合使用,幫助子元件訂閱 Context 物件的變化。屬性 value 的值會被所有訂閱了這個 context 的子元件使用。一個 Provider 可以被連線到多個子元件。當 Provider 的屬性 value 發生變化時,所有被 Provider 包括的消費元件會被重新 render。這種重新 render 不被 shouldComponentUpdate 限制。新舊值的變化和 Object.is 使用了相同的演算法。

Class.contextType

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* perform a side-effect at mount using the value of MyContext */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* render something based on the value of MyContext */
  }
}
MyClass.contextType = MyContext;

contextType 屬性可以被使用 Context 物件賦值,你可以使用 this.context 來消費上下文資料,this.context 可以在任意宣告週期,甚至是 render 中使用。

這個 api 只可以訂閱單個 Context 物件,如果需要從多箇中獲取,參考: Consuming Multiple Contexts

如果你有使用實驗中的類屬性語句,你可以使用靜態類屬性來初始化 Context。

class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* render something based on the value */
  }
}

Context.Consumer

<MyContext.Consumer>
  {value => /* render something based on the context value */}
</MyContext.Consumer>

這是一個訂閱 Context 變化的 react 元件,可以幫助我們在函式式元件中使用上下文。子元件必須是一個 function,這個 function 接受一個 Context 的當前值為引數而且返回一個 React 元素,這個值會和元件樹中最近的 Provider 值相同。

Context.displayName

Context 物件接受一個 displayName 字串屬性,React DevTools 使用這個字串來顯示 Context。

例如:

const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" in DevTools
<MyContext.Consumer> // "MyDisplayName.Consumer" in DevTools

示例

動態 Context

一個更復雜的動態 Context 的例子如下,主要思路是將 Provider 的屬性 value 放入元件的 state 。

theme-context.js

export const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee',
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222',
  },
};

export const ThemeContext = React.createContext(  themes.dark // default value);

themed-button.js

import {ThemeContext} from './theme-context';

class ThemedButton extends React.Component {
  render() {
    let props = this.props;
    let theme = this.context;    return (
      <button
        {...props}
        style={{backgroundColor: theme.background}}
      />
    );
  }
}
ThemedButton.contextType = ThemeContext;
export default ThemedButton;

app.js

import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };

    // State also contains the updater function so it will
    // be passed down into the context provider
    this.state = {
      theme: themes.light,
      toggleTheme: this.toggleTheme,
    };
  }

  render() {
    // The entire state is passed to the provider
    return (
      <ThemeContext.Provider value={this.state}>
        <Content />
      </ThemeContext.Provider>
    );
  }
}

function Content() {
  return (
    <div>
      <ThemeTogglerButton />
    </div>
  );
}

ReactDOM.render(<App />, document.root);

訂閱多個 Context

為了保持 Context 渲染速度,React 需要保持每個 Context 在樹中是分開的節點。

// Theme context, default to light theme
const ThemeContext = React.createContext('light');

// Signed-in user context
const UserContext = React.createContext({
  name: 'Guest',
});

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;

    // App component that provides initial context values
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

function Layout() {
  return (
    <div>
      <Sidebar />
      <Content />
    </div>
  );
}

// A component may consume multiple contexts
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

Error Boundaries

在過去,元件內的 js 錯誤通常會破壞元件內部的狀態,並且在下一次 render 時報錯,而且 react 沒有提供一種方式從錯誤中恢復。

介紹

部分 UI 程式碼的錯誤不應該破壞掉整個 APP 。為了解決這個問題,React 16 引入了一個新的概念 error boundary

Error Boundaries 是一個 react 元件,用於處理子元件樹中所有的 js 錯誤,記錄這些錯誤,並且顯示一個 fallback 頁面,而不是讓整個元件樹崩潰,Error Boundaries 處理所有在 render,Life Cycle,constructors中

注意:

Error Boundaries 無法捕捉這些錯誤

  1. 事件處理。
  2. 非同步程式碼。
  3. 服務端渲染。
  4. Error Boundaries 自身的異常。

當一個類元件,定義了方法 static getDerivedStateFromError() 或者 componentDidCatch()時會被認為是一個 Error Boundary 元件。使用 static getDerivedStateFromError() 來 render 一個發生異常時的 UI,使用 componentDidCatch() 來記錄錯誤資訊。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

然後你可以在普通元件使用

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

Error Boundaries 和 try catch 相同的工作方式,但是僅類元件可以作為 Error Boundaries 。實踐中,大部分情況下,會定義一個 Error Boundaries 然後在整個應用中使用。

何處使用

可以在整個應用外層使用,提示 something is error 類似服務端處理的方式。也可以在限定範圍使用,防止異常讓整個應用崩潰。

捕捉錯誤的新方式

在引入 Error Boundaries 之前,React 團隊進行過討論,因為在某些場景,白屏可能比顯示錯誤的資訊更合適,例如支付應用,聊天應用等。因此需要根據不同的場景考慮如何使用。

Forwarding Refs

Ref Fowarding 是一個使用者把 ref 轉發繫結到子元素上的技術手段,對大部分應用元件來說不會用到,但是對於一些類庫元件確可能很有用。

Forwarding refs to DOM components

考慮一個FancyButton 元件,渲染一個元素的 dom 元素如下

function FancyButton(props) {
  return (
    <button className="FancyButton">
      {props.children}
    </button>
  );
}

React 元件隱藏了自己的實現細節,其他使用 FancyButton 的元件一般不需要內部 button 元件的 ref。這是好的設計,因為能過阻止其他元件過度依賴內部的 DOM 結構。

但是一些應用層級的元件可能傾向於向使用傳統的DOM一樣使用這些元件,用於管理焦點,選中,動畫等。

ref forward 是一個可選的特性,讓元件可以獲取到自己的 ref 並傳遞給下級子元件。

例如:

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

通過這種方式,使用 FancyButton 的元件,可以直接獲取到內部 Button 的 ref。

  1. 我們使用 React.createRef 建立了一個 React ref ,並將它賦值給 ref 變數。
  2. 通過制定 JSX 的屬性,我們將 ref 傳遞給 FancyButton 元件。
  3. React 傳遞 ref 通過呼叫方法 (props , ref) => ... 。
  4. 我們將 ref 下放繫結到 button。
  5. 當 ref 被指定後,ref.current 會被指到 button DOM 節點。

第二個引數 ref 僅在使用 React.forwardRef 中可以使用,普通的函式元件或者類元件無法獲取到 ref 引數。

ref forward 不限於原生 DOM 元素,react 元素同樣可以。

Fragments

React 中可能會遇到一個元件系統返回多個元素的場景,使用 Framents 幫助我們返回多個元素而不需要給 DOM 新增額外的節點。

render() {
  return (
    <React.Fragment>
      <ChildA />
      <ChildB />
      <ChildC />
    </React.Fragment>
  );
}

動機

一個常見的場景是返回多個元素,例如:

class Table extends React.Component {
  render() {
    return (
      <table>
        <tr>
          <Columns />
        </tr>
      </table>
    );
  }
}

<Colums> 需要返回多個 td 來渲染頁面,程式碼可能這樣寫

class Columns extends React.Component {
  render() {
    return (
      <div>
        <td>Hello</td>
        <td>World</td>
      </div>
    );
  }
}

但是這會像 DOM 中新增 div 導致失效。

用法

class Columns extends React.Component {
  render() {
    return (
      <React.Fragment>
        <td>Hello</td>
        <td>World</td>
      </React.Fragment>
    );
  }
}

於是我們這樣使用。

簡寫

class Columns extends React.Component {
  render() {
    return (
      <>
        <td>Hello</td>
        <td>World</td>
      </>
    );
  }
}

使用 <></> 可以有相同的效果,唯一的區別是不支援 key 屬性。

帶 key 屬性的 Framents

key 是 Frament 唯一支援的屬性。

Higher-Order Components

高階元件是複用元件邏輯的推薦技術,HOCs 並不是 React API 的一部分,他是 React 生態中出現的模式。

具體來說,高階元件是一個方法,接受一個元件作為引數,返回另外一個元件

const EnhancedComponent = higherOrderComponent(WrappedComponent);

一般元件將 props 轉換成 UI,但高階元件是將一個元件轉換成另一個元件。

高階元件在 React 的第三方庫中普遍存在,比如 Redux’s 的 connect 。

使用 HOC 解決橫切關注點問題

Cross-Cutting Concerns :橫切關注點,部分關注點橫切程式中的多個模組,既在多個模組中都有他,他們既被稱為橫切關注點,一個典型的例子就是日誌系統。

我們之前使用 minix 處理這個問題,但是後來意識到帶來的問題比解決的問題更多。

在 react 中元件是主要的重用單位,但是你會發現一些奇怪的情況,傳統元件無法處理。例如下面的 CommentList 元件,訂閱一個外部資料來源,然後渲染一個評論列表。

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // "DataSource" is some global data source
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // Subscribe to changes
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // Clean up listener
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // Update component state whenever the data source changes
    this.setState({
      comments: DataSource.getComments()
    });
  }

  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}

整合其他庫

React 能在任何 web 應用中使用,這個主題集中於 React 和其他類似於 jQuery 的整合,相同的 idea 也可以用於 React 元件和其他已存在程式碼的整合上。

與基於操作 DOM 的其他外掛整合

React 對不由 React 建立的 DOM 元素的變化沒有察覺,React 的更新依賴於它自身的內部抽象,如果相同的 DOM 被其他的庫改變,React 會陷入困惑而且不會恢復。

這不意味著無法或者很難將 React 和以其他方式影響 DOM 的方式相結合,只是你需要留意做的事情。

最簡單的避免衝突的方式是在更新狀態時阻止 React 元件。你可以渲染一個 React 沒有理由去更新的元素,例如空div <div />

如何解決問題

class SomePlugin extends React.Component {
  componentDidMount() {
    this.$el = $(this.el);
    this.$el.somePlugin();
  }

  componentWillUnmount() {
    this.$el.somePlugin('destroy');
  }

  render() {
    return <div ref={el => this.el = el} />;
  }
}

和 Backbone 的整合內容跳過。

深入瞭解 JSX

本質上講, JSX 只是提供了一種語法糖,實際上是 React.createElement(component, props, ...children) 方法。

JSX 程式碼:

<MyButton color="blue" shadowSize={2}>
  Click Me
</MyButton>

編譯成:

React.createElement(
  MyButton,
  {color: 'blue', shadowSize: 2},
  'Click Me'
)

也可以使用自包含,沒有子元素的標籤:

<div className="sidebar" />

編譯成:

React.createElement(
  'div',
  {className: 'sidebar'}
)

指定 React Element 型別

JSX 標籤首字母大寫時,即表示這是一個 React element 型別。

首字母大寫型別的 JSX 標籤指向一個 React 元件,這些標籤會直接編譯成名稱變數,所以如果你使用 JSX <Foo />

表示式,Foo 必須在作用域中。

React 必須在作用域中

因為 JSX 編譯呼叫 React.createElement, React 庫必須在 JSX 程式碼的作用域中,例如,下面程式碼中的兩個 import 都是有必要的,儘管 React 和 CustomButton 沒有在 JavaScript 程式碼中直接使用到。

import React from 'react';
import CustomButton from './CustomButton';

function WarningButton() {
  // return React.createElement(CustomButton, {color: 'red'}, null);
  return <CustomButton color="red" />;
}

如果沒有使用 JavaScript 編譯工具來載入 React ,而是直接使用的 <script> 標籤,那 React 已經在全域性作用域中。

使用 . 符號

同樣可以使用 dot-notaion 符號來引用 React 元件,這樣可以方便與使用單個模組來匯出多個 React 元件,例如如果MyComponents.DatePicker 是一個元件,你可以在 JSX 中直接這樣使用:

import React from 'react';

const MyComponents = {
  DatePicker: function DatePicker(props) {
    return <div>Imagine a {props.color} datepicker here.</div>;
  }
}

function BlueDatePicker() {
  return <MyComponents.DatePicker color="blue" />;
}

使用者定義元件必須大寫首字母

當一個元素型別使用小寫字母開頭時,它指向的時內建元件,例如 <div> 或者<span> ,內建元件在傳遞給 createElement 時是字串的形式,例如'div'或者‘span’,但是首字母大寫的元件傳遞給 createElement 時是React.createElement(Foo) 的形式,所以對應的元件必須要有定義,或者從其他檔案中引入。

推薦使用大寫開頭來命名元件,如果已經有了小寫開頭的元件,建議賦值給大寫開頭的變數後再使用。

在執行時指定型別

不能使用表示式作為 React 元素的型別,如果你想使用表示式作為元素型別,只需要將表示式的值賦給一個大寫字母開頭的變數。例如:

import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
  photo: PhotoStory,
  video: VideoStory
};

function Story(props) {
    // Wrong! JSX type can't be an expression.  
    return <components[props.storyType] story={props.story} />;
}

如下修復

import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
  photo: PhotoStory,
  video: VideoStory
};

function Story(props) {
  // Correct! JSX type can be a capitalized variable.  
    const SpecificStory = components[props.storyType];  
    return <SpecificStory story={props.story} />;
}

JSX 中的屬性

有幾個不同的方式在 JSX 中指定屬性

使用 JavaScript 表示式作為屬性

可以使用 JavaScript 表示式作為屬性,使用 {} 包圍,例如:

<MyComponent foo={1 + 2 + 3 + 4} />

if 語句和 for 迴圈在 JavaScript 中並不是表示式,因此不能再 JSX 中直接使用,取而代之的,你可以這樣使用

function NumberDescriber(props) {
  let description;
  if (props.number % 2 == 0) {    
      description = <strong>even</strong>;  
  } else {    
      description = <i>odd</i>;  
  }  
  return <div>{props.number} is an {description} number</div>;
}

字串

可以直接在屬性中使用字串,以下兩種表達是相同的:

<MyComponent message="hello world" />

<MyComponent message={'hello world'} />

當直接使用字串是,他的值是 HTML-unescaped ,下面兩種表示相同:

<MyComponent message="&lt;3" />

<MyComponent message={'<3'} />

屬性的預設值是 ”True“

如果你沒有給屬性賦予任何值,那它的值將會是 true,以下兩中表達相同

<MyTextBox autocomplete />

<MyTextBox autocomplete={true} />

擴充套件屬性

如果你已經有了一個屬性物件,你想在 JSX 中使用,可以使用 ... 作為擴充套件操作來傳遞整個屬性物件,下面兩個表達相同

function App1() {
  return <Greeting firstName="Ben" lastName="Hector" />;
}

function App2() {
  const props = {firstName: 'Ben', lastName: 'Hector'};
  return <Greeting {...props} />;
}

JSX 子元素

在 JSX 表達方式中,被標籤包圍的內容,會被構造成一個特色的屬性 props.children。有以下幾種不同的方式在處理子元素

字串

可以在標籤中放置一個字串,props.children 也會被指定成這個字串,這在很多內建的 HTML 元素中會用到。

<MyComponent>Hello world!</MyComponent>

這是一種可用的表達形式,MyComponent 元件的 props.children 會簡單的變成字串 Hello world! 字串中的換行會被移除。

JSX 子元素

你可以提供更多的 JSX 元素作為子元件,在展示一些巢狀時很有用:

<MyContainer>
  <MyFirstComponent />
  <MySecondComponent />
</MyContainer>

你也能混合幾種不同型別的子元素,例如 JSX 元素和字串元素,他們像 HTML 一樣組合在一起。

<div>
  Here is a list:
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
  </ul>
</div>

React 元件也可以返回一個元素陣列。

render() {
  // No need to wrap list items in an extra element!
  return [
    // Don't forget the keys :)
    <li key="A">First item</li>,
    <li key="B">Second item</li>,
    <li key="C">Third item</li>,
  ];
}
JavaScript 表示式作為子元素

你可以使用 JavaScript 表示式作為子元素,只需要用 {} 來包含,例如以下兩種表達相同

<MyComponent>foo</MyComponent>

<MyComponent>{'foo'}</MyComponent>

在用於渲染未知長度的列表時經常用到。

function Item(props) {
  return <li>{props.message}</li>;
}

function TodoList() {
  const todos = ['finish doc', 'submit pr', 'nag dan to review'];
  return (
    <ul>
      {todos.map((message) => <Item key={message} message={message} />)}
    </ul>
  );
}

JavaScript 表示式也能和其他型別混合,例如

function Hello(props) {
  return <div>Hello {props.addressee}!</div>;
}

方法作為子元件

通常來說,JSX 表示式會被計算為字串,React 元素,一些元素的列表等,但是 props.children 和其他屬性一樣工作,可以被傳遞任何資料,例如:

function Repeat(props) {
  let items = [];
  for (let i = 0; i < props.numTimes; i++) {
    items.push(props.children(i));
  }
  return <div>{items}</div>;
}

function ListOfTenThings() {
  return (
    <Repeat numTimes={10}>
      {(index) => <div key={index}>This is item {index} in the list</div>}
    </Repeat>
  );
}
Booleans,Null 和 Undefined 會被忽視

falsenullundefinedtrue 是不可用的子元素,他們不會被渲染,這些 Jsx 表示式會有相同的結果:

<div />

<div></div>

<div>{false}</div>

<div>{null}</div>

<div>{undefined}</div>

<div>{true}</div>

這在有條件的渲染 React 元件時經常用到,例如下面的 <Header /> 只有在 showHeadertrue 時渲染:

<div>
  {showHeader && <Header />}
  <Content />
</div>

注意,這對一些可轉化成 false 的表示式不適用,例如數字 0。

如果你想渲染這些型別的資料,可以將他們轉化成 string,例如

<div>
  My JavaScript variable is {String(myVariable)}.
</div>

效能優化

React 使用了很聰明的技術來優化更新 DOM 的次數,對大部分應用來說,使用 React 不用做任何事情,也能有優秀的效能。儘管如此,哦我們一人有提升 React 應用的方法。

使用生產配置來編譯

如果你在衡量或者試圖優化 React app 的效能,首先要確保你在使用最優的生產編譯配置。

在預設設定下,React 包含了很多有用的警告,這些警告在開發中非常有用,但是他們讓 React 變大更加龐大和緩慢,所以在部署應用時,一定要確保使用的是生產版本。

如果你不確定編譯設定是否爭取,你可以通過安裝 React 開發工具外掛來檢查。

Create React App

如果你的應用是使用 Create React App 腳手架建設的,執行:

npm run build

這會幫你編譯應用的生產版本,這是你在釋出前唯一需要執行的操作。

Signle-File Builds

我們提供編譯版本的 React 和 React Dom 作為單個檔案:

<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

webpack

Webpack v4+ 會自動最小化你的程式碼,當設定成 production 模式時。

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  mode: 'production',
  optimization: {
    minimizer: [new TerserPlugin({ /* additional options here */ })],
  },
};

記住僅在 production 編譯模式下需要這樣設定,在開發環境下請不要使用 TerserPlugin ,因為這會隱藏 React 的提醒,而且讓編譯變得緩慢。

使用 Chrome Performace Tab 視覺化元件裝載過程

在開發模式下,使用 Chrome 的效能工具,可以講元件如何載入,更新,解除安裝的過程可視化出來:

使用 React 開發工具來視覺化效能

虛擬化長列表

如果你的應用需要渲染一個長列表(幾百或上千行),我們建議使用”windowing“的技術,這個技術僅會渲染一小部分子集,這能減少 DOM 節點建立時間。

react-windowreact-virtualized 時兩個流行的視窗化庫,他們提供了集中重用元件來展示列表等資料,你也可以建立你自己的視窗化元件,例如 Twitter 的做法,如果你想給你的元件量身定製的話。

避免調和

React 構建和維持了一個針對已渲染 UI 的內部表現,包含了所有你從元件中 return 的元素,這個表現可以讓 React 僅在必要時才建立和操作 DOM 元素。

當一個元件的屬性或者狀態改變時,React 會計算新的 renturn 和之前的映象比較,當不一樣時,React 會更新 Dom。

儘管 React 只更新改變的 DOM 節點,重渲染依然會花費一些時間,在大部分情況下這不會成為一個問題,但是當這種緩慢可以被察覺到時,你可以通過過載生命週期函式 shouldComponentUpdate 來是實現速度優化。

在大多數情況下,你可以通過繼承 React.PureComponent 來替代重寫 shouldComponentUpdate ,這個繼承實際上實現了一個 shouldComponentUpdate 方法,這個方法回淺比較當前和上一個狀態的 props 和 state。

ShouldComponentUpdate 的執行過程

其他

當你處理深層巢狀的物件資料時,更新他們可能會事件繁瑣的事情,這時可檢查 Immerimmutability-helper 庫,這可以在不犧牲效能的同時快速更新到物件。

Portals

提供一種方法講允許子節點渲染到父元件意外的 DOM 節點。

ReactDOM.createPortal(child, container)

第一個引數是任意可被渲染你的 React 元素,第二個引數,是一個 DOM 元素

用法

一般來講,當從元件周昂返回一個元素時,它被掛在與 DOM 上作為一個子元素,但是有些時候,講子元素插入到 DOM 的任意位置也是有用的,一個典型的使用場景便是 Modaltooltips 等。

事件冒泡

儘管 Portal 可以在 DOM 樹的任意地方,但是它依然和普通的 React child 的行為一致,作為一個 portal ,它仍然在 react tree 的原來位置,而無論 DOM 樹是怎樣的。

這包括事件冒泡,一個事件從 portal 內部發出,會沿著 react 樹向祖先元素傳播。

Profiler API

Profiler 可以衡量一個 React 應用的渲染事件,幫助定位應用中的緩慢部分。

注意:Profiling 添加了一些額外的部分,所以它在生產模式編譯時會被禁用。對與生產下進行探查的需求,React 提供了一個特殊的探查版用於開啟探查功能,fb.me/react-profiling

用法

一個 Profiler 可以被加在 React 樹的任意地方,來很亮這部分樹的渲染耗時。他需要兩個屬性:idonRender 回撥。例如下面探查一個 Navigation 元件的方式:

render(
  <App>
    <Profiler id="Navigation" onRender={callback}>      <Navigation {...props} />
    </Profiler>
    <Main {...props} />
  </App>
);

探查元件可以巢狀或者並排

render(
  <App>
    <Profiler id="Navigation" onRender={callback}>      <Navigation {...props} />
    </Profiler>
    <Profiler id="Main" onRender={callback}>      <Main {...props} />
    </Profiler>
  </App>
);
render(
  <App>
    <Profiler id="Panel" onRender={callback}>      <Panel {...props}>
        <Profiler id="Content" onRender={callback}>          <Content {...props} />
        </Profiler>
        <Profiler id="PreviewPane" onRender={callback}>          <PreviewPane {...props} />
        </Profiler>
      </Panel>
    </Profiler>
  </App>
);

注意,Profiler 儘管是一個輕量的元件,它仍然應該盡在必要時使用,因為它會新增 CPU 的開銷和記憶體的負擔。

onRender Callback

Profiler 需要一個 onRender 方法,React 呼叫這個方法,每當元件出發更新時,他會收到描述渲染時間的引數

function onRenderCallback(
  id, // the "id" prop of the Profiler tree that has just committed
  phase, // either "mount" (if the tree just mounted) or "update" (if it re-rendered)
  actualDuration, // time spent rendering the committed update
  baseDuration, // estimated time to render the entire subtree without memoization
  startTime, // when React began rendering this update
  commitTime, // when React committed this update
  interactions // the Set of interactions belonging to this update
) {
  // Aggregate or log render timings...
}

React 的調和過程

React 提供了陳述式的 API ,讓我們不用擔心每次更新實際上的變化,這讓我們編寫一個應用程式變得更容易,但是 React 如何實現的細節可能不是那麼顯而易見,這篇文章解釋了我們在打造 React diffing 演算法中的選擇和決定。

動機

當你使用 React 時,唯一你需要思考的關注點時 render 方法,在下一個狀態或者屬性更新時,render 方法會返回一個不同的React 樹,於是 React 需要解決,如何比較兩個樹,來最有效率的更新 UI。

有一些通用的演算法來處理將一個樹轉換成另一個樹的問題,但是通常擁有 O(n3) 的複雜度,如果我們使用這個演算法,那展示 1000 個元素,將需要對比百萬次,代價過於高昂,取而代之的是,React 實現了一個啟發式的演算法基於以下兩個假設:

  1. 兩個不同型別的元素會產生不同的樹。
  2. 開發者可以通過 key 屬性,表示不同渲染的兩個元素是否相同。

在實踐中,這兩個假設被驗證是可行的。

Diffing 演算法

當 diffing 兩個樹時,React 首先比價兩個 root 的元素。

元素型別不相同

無論什麼時候,當 root 元素有兩個不同的型別時,React 會直接幹掉舊的樹,而且構建一個新樹來代替。當銷燬一箇舊的樹時,元件例項會收到 componentWillUnmount() 方法,當構造一個新樹時,新的 DOM 節點會被插入到 DOM 中,元件會收到 componentWillMount() 然後收到 componentDidMount() ,任何和舊樹相關的狀態會消失掉。

DOM 型別而且型別相同

當比較兩個 React DOM 元素時,React 會關注兩邊的屬性,保留相同的,而僅更新有變化的。

元件型別而且型別相同

當一個元件更新時,例項依然時同一個,所以狀態會在整個渲染過程中保留,React 會更新元件例項的屬性,來匹配新的袁術,然後呼叫例項中的 componentWillReceiveProps 方法和 componentWillUpdate 方法。

接下來,render 方法會被呼叫,diff 演算法遞歸向下呼叫。

子元素列表

預設的,當遞迴一個 DOM 節點的子元素時,React 簡單的列出兩邊的 list ,然後按順序比較。

Keys

為了解決這個問題,React 支援 key 屬性。

權衡

Refs 和 DOM

Refs 提供了一種訪問 DOM 節點或者 React 元素的方式

在典型的 React 資料流中,props 是唯一的父元件和子元件溝通的方式。為了改變子元素,你需要使用新的屬性重新渲染它。然而,仍然有少數的情況你需要直接修改子元素。

何時使用

refs 有以下幾個最佳實踐

  • 管理 focus,文字選擇,或者媒體播放
  • 觸發重要的動畫
  • 和第三方 DOM 庫整合

除非無法實現,否則請儘量避免使用 Ref

不要過度使用

你可能會認為 ref 可以做到任何可能的事請,如果你是這樣想的,不妨審慎思考一下,是否有其他途徑來實現,尤其是元件狀態的層級問題是否有正確的設計。

建立 Ref

可以使用 React.createRef() 來建立 Refs ,通常在建構函式方法內將 React 元素賦值給一個例項的 ref 屬性。一下是一個例項:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return <div ref={this.myRef} />;
  }
}

訪問 Refs

當在 render 方法中繫結 ref 後,node 就可以通過 ref 的 current 屬性來訪問到

const node=this.myRef.current;

ref 的值取決於 node 的類別:

  • 但 ref 屬性在 html 元素中使用時,在建構函式呼叫的 React.createRef() 會接收到 DOM 元素作為他的 current 屬性。
  • 但 ref 屬性在自定義元件中用到時,ref 物件會接收到一個元件例項作為他的 current 屬性
  • 你不可以在函式式元件中使用 ref 屬性,因為他們不具備一個例項。

以下示例展示了他們的區別

給 DOM 元素新增 REF

以下示例使用了 ref 來儲存一個 DOM 節點的引用:

class CustomTextInput extends React.Component{
    constructor(props){
        super(props);
        this.textInput=React.createRef();
        this.focusTextInput=this.focusTextInput.bind(this);
    }
    
    focusTextInput(){
        this.textInput.current.focus();
    }
    
    render(){
        return(
        	<div>
            	<input type='text' ref={this.textInput}  />
                <input 
                    type='button' 
                    value='Focus the text input' 
                    onClick={this.focusTextInput}
                />
            </div>
        )
    }
}

React 會在元件載入時將 DOM 元素賦值給 current 屬性,而且在解除安裝時賦予 null 值,ref 的更新發生在 componentDidMount 或者 componentDidUpdate 生命週期函式之前。

給 Class 型別元件新增 ref

如果在 CustomerTextInput 上層有類似的邏輯,我們可以使用 ref 來獲取到自定義的 input 而且呼叫它的 focusTextInput 方法。

class AutoFocusTextInput extends React.Component{
    constructor(props){
        super(props);
        this.textInput=React.createRef();
    }
    
    componentDidMount(){
        this.textInput.current.focusTextInput();
    }
    
    render(){
        return(
        	<CustomerTextInput ref={this.textInput}  />
        )
    }
}

注意這僅當 CustomTextInput 使用 class 來宣告時才有用

給函式式元件新增 REF

預設的,我們可能沒有辦法在一個函式式元件中使用 ref 屬性,因為他們並沒有一個例項:

function MyFunctionComponent(){
    return <input />;
}

class Parent extends React.Component{
    constructor(props){
        super(props);
        this.textInput=React.createRef();
    }
    
    render(){
        //不會正常工作
        return(
        	<MyFunctionComponent ref={this.textInput}  />
        )
        
    }
}

如果你想允許人們獲取你的函式式元件的 ref,你可能要使用 forwardRef 或者你可以將他轉換成一個類宣告的元件。

然而,你能在函式式元件的內部使用 ref,無論指向一個 DOM 元素,還是一個類宣告的元素。

function CustomTextInput(props){
    const textInput=useRef(null);
    
    function handleClick(){
        textInput.current.focus();
    }
    
    return(
    	<div>
        	<input 
            	type='text'
                ref={textInput}
            />
            <input 
            	type='button'
                value='Focus the text input'
                onClick={handleClick}
            />
        </div>
    )
}

向父元件暴露 DOM Refs

在極其稀有的場景下,你會想要通過父元件訪問其子元素的 DOM ,這通常時不推薦的,因為這打破的元件封裝,但是這有時會很有用,當需要監控 focus 或者計運算元元素的位置和大小時。

你可能會給子元件新增 ref ,但是這不是個好主意,你只能得到一個元件例項,而不是一個 DOM 節點,而且,這在函式式元件中並不可用。

如果你在使用 React 16.3 以上的版本,我們推薦使用 ref forwarding ,Ref forwarding 讓元件選項透傳出任意子元素的 ref 作為他們自己的 ref,這裡是一個例項,in the ref forwarding documentation

如果你使用的是 React 16.2 或者更低,你需要更復雜的方案來實現,你可以使用

this alternative approach 來使用一個命名的屬性顯式傳遞 ref。

在可能的情況下,我們建議不要透傳 DOM 節點。

回撥式 Ref

React 也支援另外一種方式設定 ref 叫做 "callback refs",這種方式給了我們更細粒度控制 refs 的方法

不同於使用 createRef 建立傳遞一個 ref 的屬性,傳遞一個方法,這個方法接受一個 dom 節點或者有一個 react 元件例項作為引數,他們可以被儲存起來,在任意地方被訪問。

下面是使用這種方式的一個例項:

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);

    this.textInput = null;
    this.setTextInputRef = element => {      
        this.textInput = element;   
    };
    this.focusTextInput = () => {      
        // Focus the text input using the raw DOM API      
        if (this.textInput) 
            this.textInput.focus();    
    };  
  }

  componentDidMount() {
    // autofocus the input on mount
    this.focusTextInput();  }

  render() {
    // Use the `ref` callback to store a reference to the text input DOM
    // element in an instance field (for example, this.textInput).
    return (
      <div>
        <input
          type="text"
          ref={this.setTextInputRef}        />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}        />
      </div>
    );
  }
}

在上面的例子中,父元素通過傳遞 CustomTextInput 屬性獲取到子元素 input 的 ref。

建立 REF 的注意事項

如果你使用行內函式來定義的 ref 回撥,他會在 update 期間更新兩次,第一次傳遞 null 再次才傳遞 DOM 元素,這是因為每次 render 都會建立一個新的函式例項,react 需要清除舊的 ref 設定一個新的。你可以通過定義 ref 回撥在一個類上。

渲染屬性

主題 render prop 指的是在 React 元件間共享資料的方法,通過設定方法作為屬性的值

擁有 render 屬性的元件,會使用 render 屬性的方法代理自身的 render 方法,來進行渲染。

<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>

React Router, DownshiftFormik 庫都有使用到 render 屬性。在本文中,我們將進行討論,為什麼 render 屬性是有用的,以及如何編寫你自己的。

使用 Render 屬性處理關注點橫切問題

元件在 react 中是主要的程式碼複用單元,但是如何將一個元件的狀態和行為共享給其他元件並不總是清晰明確的。

例如,下面的元件追蹤了 web app 的滑鼠位置:

class MouseTracker extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
        <h1>Move the mouse around!</h1>
        <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
      </div>
    );
  }
}

隨著滑鼠在螢幕上的移動,元件顯示他的 橫縱座標在 p 元素節點中。

現在問題是,我們如何在其他元件中重用這種行為,換句話說,如果另外的元件需要知道滑鼠位置,我們可以封裝這種行為,以便我們輕鬆的在元件中共享和使用嗎。

使用 render props 的方案

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>

        {/*
          Instead of providing a static representation of what <Mouse> renders,
          use the `render` prop to dynamically determine what to render.
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}

使用其他元件和 render 的區別

很重要的一點是,這種模式叫做 "render props" 並不意味著只能用一個命名為 render 的屬性來實現這種模式,實際上,任意屬性都可以實現這種技術。

我們可以輕鬆的使用 children 屬性來實現它。

注意事項

對於繼承 React.PureComponent 的元件謹慎使用

使用 render 屬性會抹除使用 React.PureComponent 元件的優勢,如果你在 render 屬性內建立函式,那每次對 props 的比較都將不相同。

靜態型別檢查

嚴格模式

嚴格模式是高亮潛在問題的一種方式,像 Fragment ,StrictMode 不會渲染任何可見的 UI ,它激活了潛在的檢查和警告

嚴格模式檢查僅在開發模式下游泳,他們對生產編譯不會有任何影響

你可以在應用的任何部分開啟 strict mode 例如:

import React from 'react';

function ExampleApplication() {
  return (
    <div>
      <Header />
      <React.StrictMode>
        <div>
          <ComponentOne />
          <ComponentTwo />
        </div>
      </React.StrictMode>
      <Footer />
    </div>
  );
}

在上面的例子中,嚴格模式檢查不會在 HeaderFooter 元件中生效,只有 ComponentOneComponentTwo 以及他們的子元素會應用檢查。

嚴格模式會在一下幾點有助於我們:

  • 定位使用不安全什麼周期函式的元件
  • 對遺留 string ref 的使用進行警告
  • 警告棄用的 findDOMNode 方法
  • 檢測意外的副作用
  • 探測將被廢棄的 context api

定位不安全的生命週期

某些生命週期方法在非同步 react 應用中可能是不安全的,然而,如果你的應用使用了第三方庫,很難確保這些庫是否有使用這些到這些生命週期函式,幸運的是,嚴格模式會幫助到我們。

當嚴格模式開啟時,React 會列出所有使用了不安全生命週期的元件,而且在 console 中打印出來

對遺留 string ref 的使用進行警告

我們知道,react 提供了兩種使用和關聯 ref 的方法,legacy string ref api 和 callback api,儘管 string ref 看上去似乎更方便,但是基於 several downsides 我們官方推薦使用 callback ref 代替 string ref

React 16.3 添加了第三個選項來使用 string ref 而沒有任何缺陷

class MyComponent extends React.Component {
  constructor(props) {
    super(props);

    this.inputRef = React.createRef();
  }

  render() {
    return <input type="text" ref={this.inputRef} />;
  }

  componentDidMount() {
    this.inputRef.current.focus();
  }
}

警告棄用的 findDOMNode 方法

React 過去支援 findDOMNOde 方法來打破抽象,直接找到 DOM 元素。但是現在有了 ref 所有實際上我們不再需要該方法。

檢測意外的副作用

概念上講,React 在完成工作可以分為兩個階段:

  • render 階段:確定需要應用到 DOM 的修改,在這個階段,React 呼叫 render 然後和上一次的 render 結果進行比較
  • commit 階段:React 應用變更到 DOM,同時也會呼叫生命週期方法,像 componentDidMountcomponentDidUpdate

探測將被廢棄的 context api

非受控元件

在大多數情況下,我們建議使用受控元件來實現表單,在受控元件中,表單資料被 react 元件處理。另外一個選項是非受控元件,表單資料將由 DOM 自身處理

非受控:

class Form extends Component {
  handleSubmitClick = () => {
    const name = this._name.value;
    // do something with `name`
  }

  render() {
    return (
      <div>
        <input type="text" ref={input => this._name = input} />
        <button onClick={this.handleSubmitClick}>Sign up</button>
      </div>
    );
  }
}

受控:

class Form extends Component {
  constructor() {
    super();
    this.state = {
      name: '',
    };
  }

  handleNameChange = (event) => {
    this.setState({ name: event.target.value });
  };

  render() {
    return (
      <div>
        <input
          type="text"
          value={this.state.name}
          onChange={this.handleNameChange}
        />
      </div>
    );
  }
}

區別在於是否要將 form 的 value 儲存到狀態中。

儘管一個非受控元件會儲存 DOM 的真實資料,在某些時候,這對於整合 react 和非 react 程式碼很有用處,而且這種方式看上去程式碼更少和更高效,但是,我們仍然建議使用受控元件。

預設值

在 react 渲染生命週期中,元素的value 屬性會覆蓋 DOM 的 vaule,但使用一個非受控元件時,你通常希望 react 特別指定預設值,但是在後續的更新中離開。為了處理這種案例,你可以通過傳遞 defaultValue 代替傳遞 value 。元件轉載後改變 defaultValue 將不會更新 dom 的value。

檔案型別的 input

在 html 中,<input type="file"> 讓使用者可以從他們的裝置中選擇一個或多個檔案。在 React 中,<input type="file" /> 總是一個非受控元件,因為他的值無法被使用者設定,而且不可程式設計。

你可以使用 FILE API 來和檔案互動,下面的例子展示瞭如果通過建立一個 DOM 節點的 ref 來在提交前訪問檔案

class FileInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.fileInput = React.createRef();
  }
  handleSubmit(event) {
    event.preventDefault();
    alert(
      `Selected file - ${this.fileInput.current.files[0].name}`
    );
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Upload file:
          <input type="file" ref={this.fileInput} />
        </label>
        <br />
        <button type="submit">Submit</button>
      </form>
    );
  }
}

ReactDOM.render(
  <FileInput />,
  document.getElementById('root')
);

Web Components

react 和 Web Components 為了處理不同的問題而構造,web component 為可複用的元件提供了強封裝,但是 react 提供了一個宣告式的庫來保持 DOM 和資料的同步,這兩個目標是互補的,作為一個開發者,你可以在你的 web component 中自由的使用 react,或者在 React 中使用 web component。

大多數使用 react 的使用者不會使用到 web component,但是你可能會想使用,尤其是當你使用第三方的由 web component 編寫的 UI 元件時。

在 react 中使用 web component

class HelloMessage extends React.Component {
  render() {
    return <div>Hello <x-search>{this.props.name}</x-search>!</div>;
  }
}