前端追梦人React技术栈教程(基础知识部分)

本文详细介绍了React的核心概念,包括JSX语法、组件、状态管理、事件处理、表单控制及非受控组件等内容,并探讨了状态提升、代码分割、Context API等高级特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一. react基础知识部分

我们在使用react开发网页时,会下载两个包,一个是react,一个是react-dom,其中:

react包是react的核心代码,

react-dom则是React剥离出的涉及DOM操作的部分

最简单的HelloWorld程序如下:

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
);

1.1 重要概念

1.1.1 JSX

JavaScript语法糖,可以很好地描述 UI 应该呈现出它应有交互的本质形式。

Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。

以下代码等效:

const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);
// 等效
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

JSX支持DOM标签和用户自定义的组件, 小写字母开头视为原生DOM标签,大小字母开头视为用户自定义组件

1.1.2 函数组件与class组件以及Props

定义组件最简单的方式就是编写 JavaScript 函数

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

也可以使用ES6的class来定义组件

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。

1.1.3 State

State用来存储组件内可变的数据,不要直接修改State,要通过setState函数来进行修改

this.setState({comment: 'Hello'});

异步更新

出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。因为 this.propsthis.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。如果要依赖this.props更新下一个状态可以传递给setState函数一个回调函数

this.setState((state, props) => ({
  counter: state.counter + props.increment  // 这里的props为更新被应用时的最新值
}));

更新合并

当你调用 setState() 的时候,React 会把你提供的对象合并到当前的 state,这里的合并是浅合并

数据单向流动,自顶向下

组件可以选择把它的 state 作为 props 向下传递到它的子组件中:

<FormattedDate date={this.state.date} />

任何的 state 总是所属于特定的组件,而且从该 state 派生的任何数据或 UI 只能影响树中“低于”它们的组件。

1.1.4 事件处理

  • 事件名小驼峰形式: 如onClick
  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串
  • 不能通过返回 false 的方式阻止默认行为。你必须显式的使用 preventDefault
function ActionLink() {
  function handleClick(e) {
    /*
       e是一个合成事件(SyntheticEvent),并不是原生事件
    */
    e.preventDefault();
    console.log('The link was clicked.');
  }

  return (
    <a href="#" onClick={handleClick}>
      Click me
    </a>
  );
}
  • this的处理
    在 JavaScript 中,class 的方法默认不会绑定 this。如果你忘记绑定 this.handleClick 并把它传入了 onClick,当你调用这个函数的时候 this 的值为 undefined
    三种解决方法:

    1.  constructor(props) {
          super(props);
          this.state = {isToggleOn: true};
         
          // 为了在回调中使用 `this`,这个绑定是必不可少的
          this.handleClick = this.handleClick.bind(this);
        }
      
    2.  handleClick = () => {
          console.log('this is:', this);
        }
      
    3.    <button onClick={() => this.handleClick()}>
              Click me
            </button>
      
  • 向事件处理函数传递参数
    2中方式:

    1. <button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
      
    2. <button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
      

    在这两种情况下,React 的事件对象 e 会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递

1.1.5 条件渲染

  • 使用if/else

    function Greeting(props) {
      const isLoggedIn = props.isLoggedIn;
      if (isLoggedIn) {    
        return <UserGreeting />;  
      }  
      return <GuestGreeting />;
    }
    
  • 使用元素变量

     render() {
        const isLoggedIn = this.state.isLoggedIn;
        let button;
        if (isLoggedIn) {
          button = <LogoutButton onClick={this.handleLogoutClick} />;
        } else {
          button = <LoginButton onClick={this.handleLoginClick} />;
        }
    
        return (
          <div>
            <Greeting isLoggedIn={isLoggedIn} />
            {button}
          </div>
        );
      }
    
  • && 运算符, 三目运算符的使用
    通过花括号包裹代码,你可以在 JSX 中嵌入任何表达式。这也包括 JavaScript 中的逻辑与 (&&) 运算符和三目运算符

    function Mailbox(props) {
      const unreadMessages = props.unreadMessages;
      return (
        <div>
          <h1>Hello!</h1>
          {unreadMessages.length > 0 &&
            <h2>
              You have {unreadMessages.length} unread messages.
            </h2>
          }
        </div>
      );
    }
    
    // 三目运算符渲染不同的用户自定义组件
    render() {
      const isLoggedIn = this.state.isLoggedIn;
      return (
        <div>
          {isLoggedIn
            ? <LogoutButton onClick={this.handleLogoutClick} />
            : <LoginButton onClick={this.handleLoginClick} />
          }
        </div>
      );
    }
    
  • 阻止组件渲染
    render 方法直接返回 null

1.1.6 列表和key

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number,index) =>
	// key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。应该是在列表元素属性中独一无二的值,实在找不到可以用index
    <li key={number.toString()}>
      {number}
    </li>
  );
  return (
    <ul>{listItems}</ul>
  );
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);

// 元素的 key 只有放在就近的数组上下文中才有意义。
比方说,如果你提取出一个 ListItem 组件,你应该把 key 保留在数组中的这个 <ListItem /> 元素上,而不是放在 ListItem 组件中的 <li> 元素上。

// 也可以直接将map直接嵌入到JSX中
function NumberList(props) {
  const numbers = props.numbers;
  return (
    <ul>
      {numbers.map((number) =>
        <ListItem key={number.toString()}
                  value={number} />
      )}
    </ul>
  );
}

1.1.7 表单

受控组件

html中, input等表单元素维护自己的值,并根据用户输入进行更新,在React中可变状态通常用state来存储,并通过setState函数来进行更新,我们结合它们使得state成为表单唯一数据源,并且在用户输入过程中及时更新state的值, 以这种方式来控制取值的表单输入元素就叫做**受控组件 **

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('提交的名字: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          名字:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="提交" />
      </form>
    );
  }
}
非受控组件

受控组件每个状态更新都需要编写数据处理函数,非常麻烦,可以使用ref来直接从DOM节点中获取表单数据

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.input = React.createRef();
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.input.current.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={this.input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}
默认值

在 React 渲染生命周期时,表单元素上的 value 将会覆盖 DOM 节点中的值,在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 defaultValue 属性,而不是 value

render() {
  return (
    <form onSubmit={this.handleSubmit}>
      <label>
        Name:
        <input
          defaultValue="Bob"
          type="text"
          ref={this.input} />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}
// 同样,<input type="checkbox"> 和 <input type="radio"> 支持 defaultChecked,<select> 和 <textarea> 支持 defaultValue。
文件上传

在 React 中,<input type="file" /> 始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制。

应该使用File API与文件进行交互

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')
);

1.1.8 状态提升

React的状态提升就是用户对子组件操作,子组件不改变自己的状态,通过自己的props把这个操作改变的数据传递给父组件,改变父组件的状态,从而改变受父组件控制的所有子组件的状态,这也是React单项数据流的特性决定的。官方的原话是:共享 state(状态) 是通过将其移动到需要它的组件的最接近的共同祖先组件来实现的。 这被称为“状态提升(Lifting State Up)”。

1.1.9 代码分割

  1. 使用ES6 import(当使用 Babel 时,你要确保 Babel 能够解析动态 import 语法而不是将其进行转换。对于这一要求你需要 babel-plugin-syntax-dynamic-import 插件)

  2. React.lazy和Suspense(不支持SSR)

    import React, { Suspense } from 'react';
    
    const OtherComponent = React.lazy(() => import('./OtherComponent'));
    
    function MyComponent() {
      return (
        <div>
          <Suspense fallback={<div>Loading...</div>}>
            <OtherComponent />
          </Suspense>
        </div>
      );
    }
    
  3. 使用loadable-components

  4. 基于路由的代码分割

    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>
    );
    

1.1.10 Context

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言,通过这种方式不用显示地通过组件树逐层传递props, 而是可以将数据向组件树下所有的组件进行广播

  • 使用Context需要谨慎, 因为这会使得组件复用性变差

  • 使用 context 的通用的场景包括管理当前的 locale,theme,或者一些缓存数据

  • 如果只是想避免层层传递一些属性,应该使用组件组合(component composition)
    即将组件作为属性进行传递,这样只有最顶层的组件需要知道某些属性的使用用途

    const { PureComponent, Fragment } = require("react");
    
    function PageLayout(props){
        return (
            <Fragment>
                <header>
                    {props.header}
                </header>
                <main>
                    {props.main}
                </main>
                <footer>
                    {props.footer}
                </footer>
            </Fragment>
        );
    }
    
    function Page(props){
        return (
            <PageLayout header={<h1>this is header</h1>} main={<p>this is main text</p>} footer={<h3>this is footer</h3>}/>
        );
    }
    
    class PageComponent extends PureComponent {
        render(){
            return (
                <Fragment>
                    <h1>Home Page</h1>
                    <Page />
                </Fragment>
            );
        }
    }
    
    export default PageComponent;
    
  • 主要API

    • React.createContext
      用来创建一个Context对象,在组件树中能够对其包裹的组件提供值, 只有当组件所处的树中没有匹配到Provider时候, Context对象的defaultValue才会生效

    • Context.Provider
      每个Context对象都会返回一个Provider React组件, 它允许消费组件订阅context的变化, Provider接收一个value属性,传递给消费组件,一个Provider可以和多个消费组件有对应关系,多个Provider也可以嵌套使用,里层的会覆盖外层的数据, 当Provider的值发生变化时候,它内部的所有消费组件都会重新渲染(不受shouldComponentUpdate函数影响)

    • Class.contextType
      挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。

    • Context.Consumer

      <MyContext.Consumer>
        {value => /* 基于 context 值进行渲染*/}
      </MyContext.Consumer>
      // 这能在函数式组件中完成context的订阅
      
    • Context.displayName
      context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容

    • 在嵌套组件中更新Context
      可以通过 context 传递一个函数,使得 consumers 组件更新 context

      // context.js
      export const ThemeContext = React.createContext({
        theme: 'light',
        toggleTheme: () => {},
      });
      
      import { PureComponent } from "react";
      import loadable from "@loadable/component";
      import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
      import { ThemeContext } from "./context";
      const Login = loadable(() => import("./Login"));
      const Home = loadable(() => import("./Home"));
      
      // App.js
      class App extends PureComponent {
        constructor(props) {
          super(props);
          this.toggleTheme = () => {
            this.setState(state => ({
              themeName: state.themeName === 'light'? 'dark' : 'light'
            }))
          }
          this.state = {
            themeName: 'light',
            toggleTheme: this.toggleTheme
          };
        }
      
        render() {
          return (
            <ThemeContext.Provider value={this.state}>
              <Router>
                <Switch>
                  <Route exact path="/" component={Login} />
                  <Route path="/home" component={Home} />
                </Switch>
              </Router>
            </ThemeContext.Provider>
          );
        }
      }
      
      export default App;
      
      //login.js
      import { ThemeContext } from './context';
      const { PureComponent, Fragment } = require("react");
      
      class Login extends PureComponent {
          static contextType = ThemeContext;
      
          componentDidMount(){
              console.log(this.context)
          }
      
          toggleTheme = () => {
              this.context.toggleTheme();
          }
      
          render(){
              return (
                  <Fragment>
                       <h1 style={{color: this.context.themeName === 'dark'? '#000': '#ddd'}}>Login Page</h1>
                       <ThemeContext.Consumer>
                           {
                               ({themeName, toggleTheme}) => 
                                  <div>
                                      <h3>目前的主题是: {themeName}</h3>
                                      <button onClick={toggleTheme}>login页面切换主题</button>
                                  </div>
                           }
                       </ThemeContext.Consumer>
                  </Fragment>
              );
          }
      }
      
      export default Login;
      
      

      优化

      以下的代码会重渲染所有下面的 consumers 组件,因为 value 属性总是被赋值为新的对象:

      class App extends React.Component {
        render() {
          return (
            <MyContext.Provider value={{something: 'something'}}>
              <Toolbar />
            </MyContext.Provider>
          );
        }
      }
      // 为了防止这种情况,将 value 状态提升到父节点的 state 里:
      class App extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            value: {something: 'something'},
          };
        }
      
        render() {
          return (
            <Provider value={this.state.value}>
              <Toolbar />
            </Provider>
          );
        }
      }
      

1.1.11 错误边界(Error Boundaries)

  • 部分UI的JavaScript错误不应该导致整个应用崩溃
  • 错误边界是React中的一种组件,可以捕获并打印发生在其子组件树任何位置的JavaScript错误,并且,它会渲染出备用UI
  • 错误边界无法捕获错误的情况: 事件处理(需要使用try {} catch(e){}), 异步代码, 服务端渲染,它自身产生的错误

错误边界的工作方式类似于 JavaScript 的 catch {},不同的地方在于错误边界只针对 React 组件。只有 class 组件才可以成为错误边界组件大多数情况下, 你只需要声明一次错误边界组件, 并在整个应用中使用它。

import { PureComponent } from 'react';

/**
如果一个 class 组件中定义了 static getDerivedStateFromError() 或 componentDidCatch() 这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。
**/
class ErrorBoundary extends PureComponent {
    constructor(props){
        super(props);
        this.state  = { hasError: false };
    }

    static getDerivedStateFromError(error) {
        return { hasError: true };
    }

    componentDidCatch(error, errorInfo) {
        console.error(error, errorInfo);
    }
    
    render(){
        if(this.state.hasError){
            return <h1>Something went wrong</h1>;
        }
        return this.props.children;
    }
}

export default ErrorBoundary;

注意

开发模式下页面会崩溃提醒我们异常出现,生产模式下错误会被错误边界捕获, 在控制台进行打印, 但是保证系统不因为局部错误而崩溃。

1.1.12 refs转发

refs转发是将某些组件赋予ref, 并将此引用传递到其子组件中

转发refs到DOM组件
const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

/**
我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
我们通过指定 ref 为 JSX 属性,将其向下传递给 <FancyButton ref={ref}>。
React 传递 ref 给 forwardRef 内函数 (props, ref) => ...,作为其第二个参数。
我们向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性。
当 ref 挂载完成,ref.current 将指向 <button> DOM 节点。
**/

/**
第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref。

Ref 转发不仅限于 DOM 组件,你也可以转发 refs 到 class 组件实例中。
**/
在高阶组件HOC中转发refs

下面是一个HOC的实例

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return LogProps;
  // ref因为不是props因此不会透传(pass through)到被包裹的组件
  
  // 要像下面这样处理
  function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      const {forwardedRef, ...rest} = this.props;

      // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  // 注意 React.forwardRef 回调的第二个参数 “ref”。
  // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
  // 然后它就可以被挂载到被 LogProps 包裹的子组件上。
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

1.1.13 Fragments

用于在不额外增加DOM元素的前提下载一个组件中返回多个元素

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

简写

 render() {
    return (
      <>
        <td>Hello</td>
        <td>World</td>
      </>
    );
  }

将一个集合映射到一个 Fragments 数组

  <dl>
      {props.items.map(item => (
        // 没有`key`,React 会发出一个关键警告
        <React.Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
        </React.Fragment>
      ))}
    </dl>

1.1.14 高阶组件HOC

是一种React中的组件复用高级技巧, 高阶组件是参数为组件,返回值为新组件的函数

HOC 在 React 的第三方库中很常见,例如 Redux 的 connect 和 Relay 的 createFragmentContainer

HOC是用来解决mixins 引入隐式依赖关系导致依赖关系不明确, mixin命名冲突, 滚雪球的复杂性问题

下面是一个简单的HOC例子:

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('Current props: ', this.props);
      console.log('Previous props: ', prevProps);
    }
    render() {
      // 将 input 组件包装在容器中,而不对其进行修改。Good!
      return <WrappedComponent {...this.props} />;
    }
  }
}

1.1.15 深入JSX

JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖

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

会编译为:

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

运行时动态决定渲染组件

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

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

function Story(props) {
  // 正确!JSX 类型可以是大写字母开头的变量。  
  const SpecificStory = components[props.storyType]; 
  return <SpecificStory story={props.story} />;
}

1.1.16 性能优化(Optimizing Performance)

  • 上线后要使用生产版本react.js

  • 如果你知道在什么情况下你的组件不需要更新,你可以在 shouldComponentUpdate 中返回 false 来跳过整个渲染过程。其包括该组件的 render 调用以及之后的操作

  • 在大部分情况下,你可以继承 React.PureComponent 以代替手写 shouldComponentUpdate()

  • PureComponent只会进行浅比较, 因此如果是渲染的数组的话, 向数组中push元素, 浅比较并不能保证正确更新

    解决这个问题使用不可变数据, 如concat, ES6展开运算符以生成新的数组引用

    推荐使用immutable.js

1.1.17 Portals

是一种将子节点渲染到存在于父组件以外的DOM节点的方案

ReactDOM.createPortal(child, container)

第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。

一个 portal 的典型用例是当父组件有 overflow: hiddenz-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框:

一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树 中的祖先

// 在 DOM 中有两个容器是兄弟级 (siblings)
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');

class Modal extends React.Component {
  constructor(props) {
    super(props);
    this.el = document.createElement('div');
  }

  componentDidMount() {
    // 在 Modal 的所有子元素被挂载后,
    // 这个 portal 元素会被嵌入到 DOM 树中,
    // 这意味着子元素将被挂载到一个分离的 DOM 节点中。
    // 如果要求子组件在挂载时可以立刻接入 DOM 树,
    // 例如衡量一个 DOM 节点,
    // 或者在后代节点中使用 ‘autoFocus’,
    // 则需添加 state 到 Modal 中,
    // 仅当 Modal 被插入 DOM 树中才能渲染子元素。
    modalRoot.appendChild(this.el);
  }

  componentWillUnmount() {
    modalRoot.removeChild(this.el);
  }

  render() {
    return ReactDOM.createPortal(
      this.props.children,
      this.el
    );
  }
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {clicks: 0};
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 当子元素里的按钮被点击时,
    // 这个将会被触发更新父元素的 state,
    // 即使这个按钮在 DOM 中不是直接关联的后代
    this.setState(state => ({
      clicks: state.clicks + 1
    }));
  }

  render() {
    return (
      <div onClick={this.handleClick}>
        <p>Number of clicks: {this.state.clicks}</p>
        <p>
          Open up the browser DevTools
          to observe that the button
          is not a child of the div
          with the onClick handler.
        </p>
        <Modal>
          <Child />
        </Modal>
      </div>
    );
  }
}

function Child() {
  // 这个按钮的点击事件会冒泡到父元素
  // 因为这里没有定义 'onClick' 属性
  return (
    <div className="modal">
      <button>Click</button>
    </div>
  );
}

ReactDOM.render(<Parent />, appRoot);

1.1.18 Profiler

Profiler 能添加在 React 树中的任何地方来测量树中这部分渲染所带来的开销。 它需要两个 prop :一个是 id(string),一个是当组件树中的组件“提交”更新的时候被React调用的回调函数 onRender(function)。

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

Profiler必须指定id prop, 可以嵌套测试组件树种不同部分的性能开销

Profiler 需要一个 onRender 函数作为参数。 React 会在 profile 包含的组件树中任何组件 “提交” 一个更新的时候调用这个函数。 它的参数描述了渲染了什么和花费了多久。

function onRenderCallback(
  id, // 发生提交的 Profiler 树的 “id”
  phase, // "mount" (如果组件树刚加载) 或者 "update" (如果它重渲染了)之一
  actualDuration, // 本次更新 committed 花费的渲染时间
  baseDuration, // 估计不使用 memoization 的情况下渲染整颗子树需要的时间
  startTime, // 本次更新中 React 开始渲染的时间
  commitTime, // 本次更新中 React committed 的时间
  interactions // 属于本次更新的 interactions 的集合
) {
  // 合计或记录渲染时间。。。
}

1.1.19静态类型检查

小型项目使用prop-types

大型项目使用Flow或者TypeScript

import PropTypes from 'prop-types';

MyComponent.propTypes = {
  // 你可以将属性声明为 JS 原生类型,默认情况下
  // 这些属性都是可选的。
  optionalArray: PropTypes.array,
  optionalBool: PropTypes.bool,
  optionalFunc: PropTypes.func,
  optionalNumber: PropTypes.number,
  optionalObject: PropTypes.object,
  optionalString: PropTypes.string,
  optionalSymbol: PropTypes.symbol,

  // 任何可被渲染的元素(包括数字、字符串、元素或数组)
  // (或 Fragment) 也包含这些类型。
  optionalNode: PropTypes.node,

  // 一个 React 元素。
  optionalElement: PropTypes.element,

  // 一个 React 元素类型(即,MyComponent)。
  optionalElementType: PropTypes.elementType,

  // 你也可以声明 prop 为类的实例,这里使用
  // JS 的 instanceof 操作符。
  optionalMessage: PropTypes.instanceOf(Message),

  // 你可以让你的 prop 只能是特定的值,指定它为
  // 枚举类型。
  optionalEnum: PropTypes.oneOf(['News', 'Photos']),

  // 一个对象可以是几种类型中的任意一个类型
  optionalUnion: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.number,
    PropTypes.instanceOf(Message)
  ]),

  // 可以指定一个数组由某一类型的元素组成
  optionalArrayOf: PropTypes.arrayOf(PropTypes.number),

  // 可以指定一个对象由某一类型的值组成
  optionalObjectOf: PropTypes.objectOf(PropTypes.number),

  // 可以指定一个对象由特定的类型值组成
  optionalObjectWithShape: PropTypes.shape({
    color: PropTypes.string,
    fontSize: PropTypes.number
  }),
  
  // An object with warnings on extra properties
  optionalObjectWithStrictShape: PropTypes.exact({
    name: PropTypes.string,
    quantity: PropTypes.number
  }),   

  // 你可以在任何 PropTypes 属性后面加上 `isRequired` ,确保
  // 这个 prop 没有被提供时,会打印警告信息。
  requiredFunc: PropTypes.func.isRequired,

  // 任意类型的数据
  requiredAny: PropTypes.any.isRequired,

  // 你可以指定一个自定义验证器。它在验证失败时应返回一个 Error 对象。
  // 请不要使用 `console.warn` 或抛出异常,因为这在 `onOfType` 中不会起作用。
  customProp: function(props, propName, componentName) {
    if (!/matchme/.test(props[propName])) {
      return new Error(
        'Invalid prop `' + propName + '` supplied to' +
        ' `' + componentName + '`. Validation failed.'
      );
    }
  },

  // 你也可以提供一个自定义的 `arrayOf` 或 `objectOf` 验证器。
  // 它应该在验证失败时返回一个 Error 对象。
  // 验证器将验证数组或对象中的每个值。验证器的前两个参数
  // 第一个是数组或对象本身
  // 第二个是他们当前的键。
  customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
    if (!/matchme/.test(propValue[key])) {
      return new Error(
        'Invalid prop `' + propFullName + '` supplied to' +
        ' `' + componentName + '`. Validation failed.'
      );
    }
  })
};

默认Prop值

// 指定 props 的默认值:
Greeting.defaultProps = {
  name: 'Stranger'
};

如果你正在使用像 transform-class-properties 的 Babel 转换工具,你也可以在 React 组件类中声明 defaultProps 作为静态属性

1.1.20 Hook

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

使用State Hook

import React, { useState } from 'react';

function Example() {
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useState参数为初始state值, 返回值为当前 state 以及更新 state 的函数

使用Effect Hook

Effect Hook 可以让你在函数组件中执行副作用(数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用)操作

import React, { useState, useEffect } from 'react';
function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {    document.title = `You clicked ${count} times`;  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

是的,默认情况下,它在第一次渲染之后每次更新之后都会执行,

你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

Hook规则

  • 只在最顶层使用Hook, 不要在循环,条件或者嵌套函数中调用Hook
  • 只在React函数中调用Hook, 不要在普通的JavaScript函数中调用Hook

自定义Hook

自定义 Hook 是一个函数,其名称以 必须以“use” 开头(React才能检查其内部是否包含对内部Hook的调用和对是否违反Hook规则进行检查),函数内部可以调用其他的 Hook

下面是一个订阅好友在线状态的自定义Hook

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

使用上面的自定义Hook

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

自定义Hook是一种重用状态逻辑的机制,因此每次使用自定义Hook时,其中所有的state和副作用都是完全隔离的。

React内部提供的一些自定义Hook

1.1.21 学习网站

create-react-app

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值