React 学习笔记(1)—— 核心概念

257 阅读17分钟

什么是 React

React 的特点

React 是一个用于构建用户界面的 JavaScript 库,具有以下特点:

  1. 声明式

   React 为应用的每个状态设计简洁的视图,当数据变动时 React 能高效更新并渲染合适的组件。

  1. 组件化

   构建管理自身状态的组件,然后组合成复杂的 UI。

   由于组件逻辑使用 JavaScript 编写而非模板,因此可以方便地在应用中传递数据,并保持状态和 DOM 分离。

  1. 跨平台

   React 可以使用 Node 进行服务端渲染,使用 React Native 开发原生移动应用。

  1. 渐进式

   无需重写现有代码,引入 React 即可开发新功能。React 也允许和其它框架或库结合起来使用。使用者可以按需引入或多或少的 React 特性。

React 组件

  1. 简单组件

   React 组件使用 render() 方法,接收输入的数据并返回需要展示的内容,被传入的数据可以在组件中通过 this.props 来访问。

  1. 有状态组件

   除了通过 this.props 使用外部数据之外,组件还可以维护内部的状态数据,并通过 this.state 来访问。当组件的状态数据改变时,组件会再次调用 render() 方法重新渲染。

JSX 简介

JSX 是一个 JavaScript 扩展语法,可以很好地描述 UI 应呈现出的交互形式。

为什么使用 JSX

React 认为渲染逻辑和 UI 逻辑本质上是耦合的,React 并没有人为地将标签(markup)和逻辑分离到不同文件,而是将两者共同存放在“组件”这一松散耦合单元中。

JSX 中嵌入表达式

JSX 语法中,任何有效的 JavaScript 表达式都可以放到大括号内。

const element = (
  <h1>
    Hello, {'World'}!
  </h1>
);

如果 JSX 拆为多行,最好使用括号包裹。

JSX 也是一个表达式

JSX 最终会被编译成 JavaScript 函数调用,且返回值是一个对象。也就是说,JSX 可以直接作为值来使用。

function greeting(user) {
  if (user !== '') {
    return <h1>Hello {user}!</h1>;
  } else {
    return <h1>Hello React!</h1>;
  }
}

JSX 中指定属性

JSX 中有两种方式指定属性:

  1. 使用双引号,指定属性值为字符串字面量;
  2. 使用大括号插入表达式作为属性值。

两种方式不能同时使用,JSX 更接近 JavaScript 而不是 HTML,因此 React DOM 使用 camelCase 定义属性名。

const element = <a href="https://zh-hans.reactjs.org/" className="link" />;

JSX 子元素

const element = <img src="chrome://branding/content/about-logo.png" />
const elementWithChildren = (
  <div>
    <h1>Hello</h1>
    <h1>World</h1>
  </div>
);

JSX 防止注入攻击

React DOM 在渲染所有输入内容之前,默认会进行转义,所有内容在渲染之前都被转成字符串,以防止 XSS 攻击。

const element = <h1>{title}</h1>;

JSX 表示对象

const element = (
  <h1 className="greeting">
    Hello, World!
  </h1>
);

JSX 最终会编译成 React.createElement() 函数调用。

const element = React.createElement(
  'h1',
  { className: 'greeting'},
  'Hello, World!'
);

React.createElement() 函数的返回值是如下形式的对象:

const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, World!',
  },
};

这些对象被称为“React 元素”。

元素渲染

元素描述了用户想要看到的内容,是构成 React 应用的最小砖块。

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

和 DOM 不同,React 元素是一种开销极小的普通对象。React DOM 会负责根据 React 元素更新 DOM。

将元素渲染为 DOM

通常会指定 HTML 中的某个节点为“根” DOM 节点,该节点的内容由 React DOM 来管理。仅用 React 构建的应用通常只有一个根 DOM 节点,如果把 React 集成到一个已有的应用中,那么该应用中可能包含多个根 DOM 节点。

const element = <h1>Hello</h1>;
ReactDOM.render(element, document.getElementById('root'));

更新已渲染的元素

React 元素是不可变对象,一旦创建,无法改变它的子元素和属性。因此,更新 UI 的一种方式是创建一个全新的元素,并使用 ReactDOM.render() 来渲染。但在实践中,大多数 React 应用都只会调用一次 ReactDOM.render(),然后通过有状态的组件来实现自动更新。

React 按需更新

React 会将元素及其子元素与它们之前的状态进行比较,并只进行必要的更新。

组件和 Props

函数组件和 class 组件

函数组件,接收带有数据的 props 对象,返回 React 元素。

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

class 组件,通过自身实例上的 props 获取数据。

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

渲染组件

React 元素可以是用户自定义的组件,此时,JSX 所接收的属性(attributes)和子组件(children)将会转换为 props 对象传递给组件。

function Welcome(props) {
  return <h1>Hello {props.name}!</h1>;
}
const element = <Welcome name="Tom" />;
ReactDOM.render(element, document.getElementById('root'));

为了区分于原生 DOM 标签,组件名称必须以大写字母开头,并且只能在声明组件的作用域内使用。

组合组件

组件中可以使用其它组件。通常,每个 React 应用的顶层组件都是 App 组件。但是如果是把 React 集成到现有应用中,可能需要一些小组件,并自下而上地逐步应用到视图层中。

function Welcome(props) {
  return <h1>Hello {props.name}!</h1>;
}
function App() {
  return (
    <div>
      <Welcome name="Tom" />
      <Welcome name="Jerry" />
    </div>
  );
};
ReactDOM.render(<App />, document.getElementById('root'));

提取组件

如果 UI 中有一部分被多次使用,或者组件本身足够复杂,可以考虑对组件进行抽离,以提高组件的可维护性和复用性。

应该根据组件自身的角度命名 props,而不是根据调用组件的上下文命名。

Props 的只读性

拥有下面两条性质的函数称为纯函数:

  1. 不管何时何地调用该函数,只要入参相同,函数返回值就相同(不会随局部静态变量、非局部变量、可变引用实参或输入流的变化而变化);
  2. 没有副作用(不改变局部静态变量、非局部变量、可变引用实参或输入输出流)。

这样的函数可作为数学函数的类比。

副作用:一个操作、函数或表达式,如果它修改了当前局部环境之外的状态变量,则称为具有副作用。如果存在副作用,一个程序的行为可能依赖于历史。

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

State 和生命周期

除了调用 ReactDOM.render() 更新 UI 之外,还可以通过 state 来实现。state 和 props 类似,但是 state 是私有的,完全受控于当前组件。

对于 class 组件,每次组件更新时,render 方法都会被调用。但只要在相同的 DOM 节点中渲染组件,就仅有一个组件实例被创建使用。这就使得可以使用如 state 或生命周期方法等特性。

class 组件中添加 state 和生命周期方法

class 组件可以在 constructor 中初始化 state。在 constructor 中,class 组件应始终使用 props 参数调用父类构造函数,以初始化 this。

当应用拥有许多组件时,需要在组件销毁时释放所占用的资源。

React 为组件定义了一些生命周期:

  1. 挂载(mount):组件第一次被渲染到 DOM 中时;
  2. 卸载(unmount):组件从 DOM 中删除时;

以及相应的生命周期方法,当组件进行到该生命周期时,就会执行相应的方法:

  1. componentDidMount 方法在组件被渲染到 DOM 中后执行;
  2. componentWillUnmount 方法在组件从 DOM 中删除时执行。
class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      date: new Date(),
    };
  }
  componentDidMount() {
    this.timerID = setInterval(() => {
      this.tick();
    }, 1000);
  }
  componentWillUnmount() {
    clearInterval(this.timerID);
  }
  tick() {
    this.setState({
      date: new Date(),
    });
  }
  render() {
    return (
      <div>
        <h1>Hello React!</h1>
        <h2>It is {this.state.date}</h2>
      </div>
    );
  }
}

React 允许向 class 中添加任何不参与数据流(即,非 props/state)的额外字段。

setState

关于 statesetState,有以下注意事项:

  1. 不要直接给 state 赋值

   直接赋值无法重新渲染组件,只有使用 setState 方法才可以,构造函数(constructor)是唯一可以给 state 直接赋值的地方。

  1. state 的更新可能是异步的

   出于性能的考虑,React 可能会把 setState 方法的多次调用合并成一次调用。由于 this.propsthis.state 可能会异步更新,因此不要依赖它们的值来更新下一个状态。为解决这一问题,可以传给 setState 一个函数而不是对象,这个函数的第一个参数是上一个 state,第二个参数是当前更新被应用时的 props,React 将以该函数返回值更新 state

   JavaScript    this.setState((state, props) => ({      counter: state.counter + props.increment,    }));    

  1. state 的更新会被合并

   当调用 setState() 时,React 会把所提供的对象合并到当前的 state 中。但这种合并是浅合并:未提供的状态直接保留,提供的状态完全替换。

   class App extends React.Component {
     constructor(props) {
       super(props);
       this.state = {
         posts: [],
         comments: [],
       };
     }
     ...
   }
   this.setState({
     posts: [1],
   });

数据流动自上而下

不管是父组件还是子组件都无法知道某个组件有无状态,它们也并不关心该组件是函数组件还是 class 组件。

除了拥有并设置 state 的组件之外,其它组件都无法访问它,这也是 state 被称为局部的或是封装的原因。

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

这通常被称为“自上而下”或是“单向”的数据流,任何 state 总是属于某个特定组件,而且从该 state 派生的任何数据或 UI 都只能影响组件树中“低于”它的组件。

事件处理

React 元素的事件处理和 DOM 元素的很相似,但也有些不同:

  1. React 事件名采用 camelCase 命名,而不是纯小写;
  2. 使用 JSX 语法时必须传入一个函数作为事件处理函数,而不能是字符串;
  3. React 中必须显示调用 preventDefault 来阻止事件默认行为,而不能通过返回 false 的方式。
  4. React 中的事件对象是一个合成事件,React 事件和原生事件不完全相同。React 中,一般不必使用 addEventListener 监听 DOM 元素,而只需在元素初始渲染时添加监听器即可。
function Form() {
  function handleSubmit(e) {
    e.preventDefault();
  }
  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Submit</button>
    </form>
  );
}

class 组件中监听事件时,通常将事件处理函数声明为 class 中的方法,但是在绑定该函数之前,必须确定该函数的 this 指向,有三种方式可以实现这一点:

  1. 手动调用 bind 绑定 this 的指向
   class Welcome extends React.Component {
     constructor(props) {
       super(props);
       this.state = {
         user: 'React',
       };
       this.handleClick = this.handleClick.bind(this);
     }
     handleClick() {
       this.setState({
         user: 'Tom',
       });
     }
     render() {
       return (
         <div onClick={this.handleClick}>
           <h1>Hello, {this.state.user}!</h1>
         </div>
       );
     }
   }
  1. class 字段实验性语法
   class Welcome extends React.Component {
     // some code
     handleClick = () => {
       console.log(this);
     }
     constructor(props) {
       super(props);
       this.state = {
         user: 'React',
       };
     }
     render() {
       return (
         <div onClick={this.handleClick}>
           <h1>Hello, {this.state.user}!</h1>
         </div>
       );
     }
   }
  1. 箭头函数
   class Welcome extends React.Component {
     constructor(props) {
       super(props);
       this.state = {
         user: 'React',
       };
     }
     handleClick() {
       this.setState({
         user: 'Tom',
       });
     }
     render() {
       return (
         <div onClick={() => this.handleClick()}>
           <h1>Hello, {this.state.user}!</h1>
         </div>
       );
     }
   }

   如果使用箭头函数,那么每次渲染组件时都会创建不同的回调函数,这些回调函数如果作为 props 传入子组件,这些组件可能会进行额外的重新渲染。因此,建议使用方式 1 和 方式 2 来解决 this 指向的问题。

传递参数给事件处理程序

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

条件渲染

React 允许根据条件来决定渲染结果,一般有两种条件渲染的方式:

  1. 根据条件决定渲染出哪个 JSX
   function Greeting(props) {
     const isBlock = props.isBlock;
     if (isBlock) {
       return <div />
     } else {
       return <span />
     }
   }
  1. 根据条件表达式决定 JSX 的渲染内容

   元素表达式

   function Greeting(props) {
     const element = props.isBlock ? <div /> : <span />;
     return (
       <div>
         <h1>Hello</h1>
         {element}
       <div />
     );
   }

   逻辑运算符

   function Greeting(props) {
     return (
       <div>
         <h1>Hello</h1>
         {props.isBlock && <div />}
       <div />
     );
   }

   三元表达式

   function Greeting(props) {
     return (
       <div>
         <h1>Hello</h1>
         <div>{props.isTom ? 'Tom' : 'Jerry'}</div>
       <div />
     );
   }

如果条件过于复杂,应考虑抽离出组件。

阻止条件渲染

让组件的 render 方法返回 null

function WarningBanner(props) {
  if (!props.warn) {
    return null;
  }
  return (
    <div>
      Warning!
    </div>
  );
}

组件的 render 方法返回 null 不会影响组件的生命周期。

列表和 key

JSX 中的子元素可以是由 React 元素所构成的数组,从而渲染出列表。列表中的每一个表项都必须有一个属性 key。

function NumberList(props) {
  const ListItems = props.numbers.map((number, index) => {
    return <li key={index}>number</li>;
  });
  return (
    <ul>{ListItems}</ul>
  );
}
ReactDOM.render(
  <NumberList numbers={[1, 2, 3]}>,
  document.getElementById('root'),
);

key

key 帮助 React 识别哪个元素发生了变化,比如添加或删除。

  1. 一个元素的 key 最好是所在列表中独一无二的字符串,一般使用数据中的 id 作为元素的 key。如果没有显式指明 key,那么 React 将默认使用索引作为列表项的 key。
  2. 如果列表项可能会发生变化,建议不要使用索引作为 key,这样做会导致性能变差,还可能会引起组件状态的问题。
  3. 元素的 key 只有放在就近的数组上下文中才有意义。换句话说,key 设置在列表项上面,而不是列表项的子元素上。
  4. key 值在兄弟节点中必须是唯一的,但不需要它们全局唯一。
  5. key 会传递给 React,但不会传递给组件,组件中要想获得 key 值,则需添加其它属性,如:id。

JSX 内部渲染列表

除了事先生成 React 元素数组之外,还可以直接在 JSX 中生成 React 元素数组。

function NumberList(props) {
  return (
    <ul>
      {props.numbers.map((number, index) => <li key={index}>number</li>)}
    </ul>
  );
}
ReactDOM.render(
  <NumberList numbers={[1, 2, 3]}>,
  document.getElementById('root'),
);

但这种风格可能会被滥用,何时需要为了可读性来抽离组件,完全取决于使用者。

表单

受控组件

HTML 中,表单元素的状态由自身维护,并根据用户输入自动更新。

React 中的可变状态通常保存在组件的 state 里面,并且通过 setState 更新,这使得 React 的 state 成为唯一数据源。渲染表单的 React 组件还控制着用户输入时表单发生的操作,被 React 以这种方式控制取值的表单输入元素称为受控组件

class Form extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: '',
    };
    this.handleChange = this.handleChange.bind(this);
  }
  handleChange(event) {
    this.setState({
      value: event.target.value,
    });
    console.log('user input');
  }
  render() {
    return (
      <form>
        <input value={this.state.value} onChange={this.handleChange} />
      </form>
    );
  }
}

textarea 标签

HTML 中,<textarea> 元素由其子元素定义其文本。

React 中,<textarea> 的文本内容由 value 属性提供。

class Form extends React.Component {
  // some code ...
  render() {
    return (
      <form>
        <textarea value={this.state.value} onChange={this.handleChange} />
      </form>
    );
  }
}

select 标签

HTML 中,<select> 标签创建下拉列表,选项标签 <option>selected 属性用于表示该选项是否选中。

React 中,根 <select> 标签上的 value 属性指明了哪些选项 <option> 被选中。

  1. 单选
   class Form extends React.Component {
     constructor(props) {
       super(props);
       this.state = {
         value: 'apple',
       };
       // some code ...
     }
     // some code ...
     render() {
       return (
         <form>
           <select value={this.state.value} onChange={this.handleChange}>
             <option value="apple" />
             <option value="orange" />
           </select>
         </form>
       );
     }
   }
  1. 多选
   class Form extends React.Component {
     constructor(props) {
       super(props);
       this.state = {
         value: [],
       };
       // some code ...
     }
     // some code ...
     render() {
       return (
         <form>
           <select multiple={true} value={this.state.value} onChange={this.handleChange}>
             <option value="apple" />
             <option value="orange" />
           </select>
         </form>
       );
     }
   }

文件 input 标签

在 HTML 中,<input type="file" /> 允许用户从存储设备中选择一个或多个文件,上传到服务器。因为它的 value 只读,所以它是 React 中的非受控组件

在一个回调函数中处理多个表单输入

为每个元素添加 name 属性,可以对表单元素进行区分。

class Form extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      age: 0,
      gender: 'male',
    };
    this.handleChange = this.handleChange.bind(this);
  }
  handleChange(event) {
    this.setState({
      [event.target.name]: event.target.value,
    });
  }
  render() {
    return (
      <form>
        <input name="age" value={this.state.age} onChange={this.handleChange} />
        <input name="gender" value={this.state.gender} onChange={this.handleChange} />
      </form>
    );
  }
}

受控输入空值

在受控组件中指定 value 属性的值,会阻止用户更改输入,但如果 value 值为 null/undefined,那么用户可以手动更改。

状态提升

如果多个组件需要反映共同的状态变化,那么最好把共享状态提升到最近的公共父组件中,这被称为状态提升

状态提升之后,子组件失去了数据控制权,因而变成了受控组件,通过为父组件提供两个相应的属性 {valueName}/on{ValueName} 来实现数据控制权的向上转移。

React 应用中,任何可变数据应只有唯一数据源。组件间的数据共享应当依靠自上而下的数据流,而不是尝试在不同组件间同步状态。

提升 state 的方式比双向绑定方式需要编写很多的样板代码,但是排查和隔离 bug 所需的工作量将会变少。由于存在于组件中的任何 state,都只有组件自己才能修改,因此 bug 的排查范围被大大缩小。此外,还可以使用自定义逻辑来拒绝或转换用户输入。

如果某些数据可以由 propsstate 推导出,那就不应该存在于 state 中。

组合与继承

React 具有十分强大的组合模式,推荐使用组合而非继承来实现组件代码重用。

包含关系

有些组件无法提前知道它们子组件的具体内容,比如:通用展示容器之类的组件。有两种方案可以解决这一问题:

  1. 使用 children prop,适用于子组件较多的情形
   function DisplayBox(props) {
     return (
       <div>
         {props.children}
       </div>
     );
   }
   function Welcome(props) {
     return (
       <DisplayBox>
         <h1>Welcome</h1>
         <p>Thanks</p>
       </DisplayBox>
     );
   }
  1. 使用一般的 props,适用于子组件较少且具有明确含义的情形

   React 元素本质上也是对象,可以作为 props 进行传递。

   function DisplayBox(props) {
     return (
       <div>
         <div className="left">
           {props.left}
         </div>
         <div className="right">
           {props.right}
         </div>
       </div>
     );
   }
   function App() {
     return (
       <DisplayBox
         left={<LeftBox />}
         right={<RightBox />}
       />
     );
   }

   这种用法类似于插槽(slot),但是 React 中没有插槽的概念。

特例关系

有些组件可以看作是其它组件的特殊实例,比如:WelcomeDialog 是 Dialog 的特殊实例。React 中,可以通过为“一般”组件定制 props 而得到“特殊”组件。

function Dialog(props) {
  return (
    <div>
      <h1>
        {props.title}
      </h1>
      <p>
        {props.message}
      </p>
    </div>
  );
}
function WelcomeDialog() {
  return (
    <Dialog
      title="Welcome"
      message="Thanks"
    />
  );
}

继承

目前没有发现必须使用继承才能构建组件层次的情况。

props 和组合可以清晰而安全地定制组件外观和行为,组件可以接收任意 props,包括基本数据类型、React 元素以及函数。

如果要复用非 UI 的功能,可以提取出一个单独的 JavaScript 模块,如函数、对象或类。组件可以直接导入它们而无需继承。

React 哲学

如何构建一个应用:

1. 将 UI 划分为组件层级

UI 设计师可能已经完成了这一工作,图层名称可能就是最终组件名。

应该由单一功能原则确定组件的范围,也就是说,一个组件原则上只能负责一个功能。

如果模型设计得恰当,UI 和数据模型应该是一一对应的,这是因为 UI 和数据模型会倾向于遵守相同的信息结构。将 UI 分离成组件,其中每个组件都会和数据模型的某部分相对应。

2. 创建静态版本

渲染 UI 和添加交互两个过程最好分开,编写静态版本通常需要大量代码,添加交互则要考虑大量细节。

创建静态版本时,完全不应该使用 state,应该使用 props 传入所需数据。state 代表随时间变化的数据,应只在实现交互时使用。

对于比较简单的应用,自上而下的方式更方便;对于较大型项目,自下而上的构建,并同时为底层组件编写测试是更加简单的方式。

单向数据流(也叫单向绑定)的思想使得组件模块化,易快速开发。

3. 确定 UI state 的最小完整表示

UI 交互需要有改变基础数据模型的能力,React 通过 state 来实现这一点。

为了正确地构建应用,需要找出应用所需的 state 的最小表示,其它数据均由它们计算产生。

一个 state 数据应该满足以下三点:

  1. 该数据无法由父组件通过 props 传递而来;
  2. 该数据随时间而变化;
  3. 该数据不能根据其它 stateprops 计算出来。

4. 确定 state 的位置

确定哪个组件能够改变这些 state,或者说拥有这些 state

根据以下步骤判断 state 的位置:

  1. 找到根据这个 state 进行渲染的所有组件;
  2. 找到它们的共同所有者(common owner)组件;
  3. 该共同所有者组件或者比它层级更高的组件应该拥有该 state
  4. 如果没有合适的位置存放该 state,就直接创建一个新组件存放该 state,并将这一新组件置于高于共同所有者组件层级的位置。

5. 添加反向数据流

数据反向传递:处于较低层级的组件更新较高层级中的 state

较高层级的组件传给较低层级组件一个回调函数,以改变高层级组件的 state

结束

比起写,代码更多是给人看的。

当构建更大的组件库时,代码的模块化和清晰度非常重要。并且随着代码复用程度逐渐加深,代码行数会显著减少。