# 初识React你应该知道的一些点

# 组件

组件允许你将UI拆分为独立可复用的代码片段,每个片段进行独立构思。

组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。

  • 函数组件

    function Welcome (props) {
    	return <h2>Hello, {props.name}</h2>;
    }
    
  • class组件

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

以上两个组件是等效的。

注意: 组件名称必须以大写字母开头。

React 会将以小写字母开头的组件视为原生 DOM 标签。例如,代表 HTML 的 div 标签,而 则代表一个组件,并且需在作用域内使用 Welcome

# State

正确的使用setState()

  • 不要直接修改State

    // 错误
    this.state.msg = 'LinDaiDai'
    
    // 正确
    this.setState({ msg: 'LinDaiDai' })
    
  • State的更新可能是异步的,如果更新的state中有依赖到state或者props则可以用函数:

    // 错误
    this.setState({
    	counter: this.state.counter + this.props.increment
    });
    
    // 正确
    this.setState((state, props) => ({
    	counter: state.counter + props.increment
    }));
    
    // 使用普通函数也可以
    this.setState(function(state, props) {
      return {
        counter: state.counter + props.increment
      };
    });
    
  • State的更新可能会被合并

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

    例如,你的 state 包含几个独立的变量:

    constructor(props) {
      super(props);
      this.state = {
        posts: [],
        comments: []
      };
    }
    

    然后你可以分别调用 setState() 来单独地更新它们:

      componentDidMount() {
        fetchPosts().then(response => {
          this.setState({
            posts: response.posts
          });
        });
    
        fetchComments().then(response => {
          this.setState({
            comments: response.comments
          });
        });
      }
    

    这里的合并是浅合并,所以 this.setState({comments}) 完整保留了 this.state.posts, 但是完全替换了 this.state.comments

    https://react.docschina.org/docs/state-and-lifecycle.html#state-updates-are-merged

# 事件

解决事件中this绑定的问题,如果不想用bind显式绑定的话,可以使用下面两种方式:

  • class fields语法:

    class LoggingButton extends React.Component {
      // 此语法确保 `handleClick` 内的 `this` 已被绑定。
      // 注意: 这是 *实验性* 语法。
      handleClick = () => {
        console.log('this is:', this);
      }
    
      render() {
        return (
          <button onClick={this.handleClick}>
            Click me
          </button>
        );
      }
    }
    
  • 在回调中使用箭头函数

    class LoggingButton extends React.Component {
      handleClick() {
        console.log('this is:', this);
      }
    
      render() {
        // 此语法确保 `handleClick` 内的 `this` 已被绑定。
        return (
          <button onClick={() => this.handleClick()}>
            Click me
          </button>
        );
      }
    }
    

    此语法问题在于每次渲染 LoggingButton 时都会创建不同的回调函数。在大多数情况下,这没什么问题,但如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染。我们通常建议在构造器中绑定或使用 class fields 语法来避免这类性能问题。

# 组合VS继承

React中并没有类似于Vue中的slot,不过可以靠prop来实现:

function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

function App() {
  return (
    <SplitPane
      left={
        <Contacts />
      }
      right={
        <Chat />
      } />
  );
}

# 语法需要注意的点

# 1. 数字0依旧会被渲染

false, null, undefined是合法的子元素,但它们并不会渲染,因此可以作为判断JSX表达式渲染结果:

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

但是数字0,仍然会被 React 渲染。例如,以下代码并不会像你预期那样工作,因为当 props.messages 是空数组时,0 仍然会被渲染:

<div>
  {props.messages.length &&
    <MessageList messages={props.messages} />
  }
</div>

解决方式:确保 && 之前的表达式总是布尔值:

<div>
  {props.messages.length > 0 &&
    <MessageList messages={props.messages} />
  }
</div>

# 2. 元素和组件

元素:React 元素是构成 React 应用的基础砖块。元素描述了你在屏幕上想看到的内容。React 元素是不可变对象。例如:

const element = <h1>Hello, world</h1>;

组件:React 组件是可复用的小的代码片段,它们返回要在页面中渲染的 React 元素。例如:

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

# 如何阻止组件不更新

以下生命周期默认是返回true,只要将其改为false即不更新。

shouldComponentUpdate(nextProps, nextState) {
    return false;
  }

# 其它API

# 在组件上能调用的方法

不同于上述生命周期方法(React 主动调用),以下方法是你可以在组件中调用的方法。

只有两个方法:setState()forceUpdate()

# forceUpdate()强制组件重新渲染

默认情况下,当组件的 state 或 props 发生变化时,组件将重新渲染。如果 render() 方法依赖于其他数据,则可以调用 forceUpdate() 强制让组件重新渲染。

调用 forceUpdate() 将致使组件调用 render() 方法,此操作会跳过该组件的 shouldComponentUpdate()

但其子组件会触发正常的生命周期方法,包括 shouldComponentUpdate() 方法。如果标记发生变化,React 仍将只更新 DOM。

注意⚠️:

通常你应该避免使用 forceUpdate(),尽量在 render() 中使用 this.propsthis.state

# Class属性

# defaultProps

defaultProps 可以为 Class 组件添加默认 props。这一般用于 props 未赋值,但又不能为 null 的情况:

class CustomButton extends React.Component {
  // ...
}

CustomButton.defaultProps = {
  color: 'blue'
};

上述CustomButton被调用时如果没有提供props.color,则默认为blue

此时如果传递了null的话,则就会是null

render() {
  return <CustomButton color={null} /> ; // props.color 将保持是 null
}

# displayName

displayName 字符串多用于调试消息。通常,你不需要设置它,因为它可以根据函数组件或 class 组件的名称推断出来。

用处

调试时需要显示不同的名称或创建高阶组件。displayName可以方便我们在使用Chrome浏览器插件的情况下通过审查元素看到具体是哪个组件。

例如你要审查的这个元素是一个HOC产物,如下一个简单的案例🌰:

LoginHOC.tsx:

import React, { Component } from 'react';

export default function LoginHoc (WrappedComponent) {
  class InnerComponent extends Component {
    render () {
      return (
        <WrappedComponent {...this.props}></WrappedComponent>
      )
    }
  }
  return InnerComponent;
}

Blog.tsx:

import React, { Component } from 'react';
import LoginHOC from './LoginHOC';

class Blog extends Component {
  render () {
    return (
      <div className='blog'>
        <button>我是按钮</button>
      </div>
    )
  }
}

export default LoginHOC(Blog);

App.js中引用:

import Blog from './components/HOC-example/Blog';

function App () {
  return (
    <div className='App'>
      <Blog detail={1}></Blog>
    </div>
  )
}
export default App;

这时候使用Chrome插件React Developer Tools调试时,会发现页面中的元素为这样:

此时如果你给HOC设置了displayName的话:

import React, { Component } from 'react';

export default function LoginHoc (WrappedComponent) {
  class InnerComponent extends Component {
    render () {
      return (
        <WrappedComponent {...this.props}></WrappedComponent>
      )
    }
  }
+ InnerComponent.displayName = `LoginHoc(${getDisplayName(WrappedComponent)})`;
  return InnerComponent;
}
+ function getDisplayName (WrappedComponent) {
+  return WrappedComponent.displayName || WrappedComponent.name || 'Component'
+}

效果如下:

能更加清楚的让我们看到包装显示名称。

# ReactDOM

react-dom 的 package 提供了可在应用顶层使用的 DOM(DOM-specific)方法,如果有需要,你可以把这些方法用于 React 模型以外的地方。不过一般情况下,大部分组件都不需要使用这个模块。

  • render:
    • React.render(element, container[, callback])
    • 在提供的 container 里渲染一个 React 元素,并返回对该组件的引用(或者针对无状态组件返回 null)。
  • hydrate:
    • ReactDOM.hydrate(element, container[, callback])
    • render() 相同,但它用于在 ReactDOMServer 渲染的容器中对 HTML 的内容进行 hydrate 操作。
  • unmountComponentAtNode()
    • ReactDOM.unmountComponentAtNode(container)
    • 从 DOM 中卸载组件,会将其事件处理器(event handlers)和 state 一并清除。
  • findDOMNode()
    • ReactDOM.findDOMNode(component)
    • findDOMNode 是一个访问底层 DOM 节点的应急方案,在大多数情况下,不推荐使用该方法,因为它会破坏组件的抽象结构。严格模式下该方法已弃用。
    • 大多数情况下,你可以绑定一个 ref 到 DOM 节点上,可以完全避免使用 findDOMNode。
    • findDOMNode 不能用于函数组件。
  • createPortal()
    • ReactDOM.createPortal(child, container)
    • 创建 portal。Portal 将提供一种将子节点渲染到 DOM 节点中的方式,该节点存在于 DOM 组件的层次结构之外。

# DOM相关

# className

className 属性用于指定 CSS 的 class,此特性适用于所有常规 DOM 节点和 SVG 元素,如 及其它标签。

如果你在 React 中使用 Web Components(这是一种不常见的使用方式),请使用 class 属性代替。

# dangerouslySetInnerHTML

dangerouslySetInnerHTML 是 React 为浏览器 DOM 提供 innerHTML 的替换方案。通常来讲,使用代码直接设置 HTML 存在风险,因为很容易无意中使用户暴露于跨站脚本(XSS)的攻击。

使用:

设置一个带有属性名为key的对象:

function createMarkup() {
  return {__html: 'First &middot; Second'};
}

function MyComponent() {
  return <div dangerouslySetInnerHTML={createMarkup()} />;
}
  
// 使用
<MyComponent></MyComponent>

最终会被渲染为:

First · Second

# style

style属性接收的是一个对象,这点和DOMstyleJavaScript属性一样,同时会更高效的,且能预防跨站脚本(XSS)的安全漏洞。例如:

const divStyle = {
  color: 'blue',
  backgroundImage: 'url(' + imgUrl + ')',
};

function HelloWorldComponent() {
  return <div style={divStyle}>Hello World!</div>;
}

需要注意的点:

  • 样式不会自动补齐前缀。如需支持旧版浏览器,需要手动补充对应的样式属性:

    const divStyle = {
      WebkitTransition: 'all', // note the capital 'W' here
      msTransition: 'all' // 'ms' is the only lowercase vendor prefix
    };
    

    浏览器引擎前缀都应以大写字母开头,除了 ms。因此,WebkitTransition 首字母为 ”W”。

  • React 会自动添加 ”px” 后缀到内联样式为数字的属性后。如需使用 ”px” 以外的单位,请将此值设为数字与所需单位组成的字符串。例如:

    // Result style: '10px'
    <div style={{ height: 10 }}>
      Hello World!
    </div>
    
    // Result style: '10%'
    <div style={{ height: '10%' }}>
      Hello World!
    </div>
    

# 合成事件

我们知道在普通的DOM元素中有一个event对象来获取事件对象。

而在React中,获取到的这个事件对象被称为合成事件SyntheticEvent

它是浏览器的原生事件的跨浏览器包装器。除兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation()preventDefault()

每个 SyntheticEvent 对象都包含以下属性:

boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
void persist()
DOMEventTarget target
number timeStamp
string type

# 事件池

如上所述,SyntheticEvent是一个合成事件,如果我们在事件的回调函数中直接打印它出来,获取到的属性值全都是null或者undefined

例如这段代码:

import React, { Component } from 'react';

export default class SyntheticEvent extends Component {
  clickBtn (event) {
    console.log(event); // nullified object
    console.log(event.type); // "click"
  }
  render () {
    return (
      <>
        <button onClick={this.clickBtn.bind(this)}>
          点击我
        </button>
      </>
    )
  }
}

event拿到的对象:

{
 detail: null,
 type: null,
 nativeEvent: null,
 isTrusted: null,
 ...
}

所以有了官网的这句话:

SyntheticEvent 是合并而来。这意味着 SyntheticEvent 对象可能会被重用,而且在事件回调函数被调用后,所有的属性都会无效。出于性能考虑,你不能通过异步访问事件。

再如下:

function clickBtn(event) {
  console.log(event); // => nullified object.
  console.log(event.type); // => "click"
  const eventType = event.type; // => "click"

  setTimeout(function() {
    console.log(event.type); // => null
    console.log(eventType); // => "click"
  }, 0);

  // 不起作用,this.state.clickEvent 的值将会只包含 null
  this.setState({clickEvent: event});

  // 你仍然可以导出事件属性
  this.setState({eventType: event.type});
}

如果你想异步访问事件属性,你需在事件上调用 event.persist(),此方法会从池中移除合成事件,允许用户代码保留对事件的引用。

clickBtn (event) {
  event.persist()
  console.log(event);
  console.log(event.type);
}

现在的event

{
 detail: 1,
 type: 'click',
 nativeEvent: {...},
 isTrusted: true,
 ...
}

# 处理捕获阶段的点击事件

处理捕获阶段的点击事件请使用 onClickCapture,而不是 onClick

# 控制组件与非控制组件

React 有两种不同的方式来处理表单输入。

如果一个 input 表单元素的值是由 React 控制,就其称为受控组件。当用户将数据输入到受控组件时,会触发修改状态的事件处理器,这时由你的代码来决定此输入是否有效(如果有效就使用更新后的值重新渲染)。如果不重新渲染,则表单元素将保持不变。

一个非受控组件,就像是运行在 React 体系之外的表单元素。当用户将数据输入到表单字段(例如 input,dropdown 等)时,React 不需要做任何事情就可以映射更新后的信息。然而,这也意味着,你无法强制给这个表单字段设置一个特定值。

在大多数情况下,你应该使用受控组件。

阅读全文