基础概念
JSX
可以是 html 标签,也可以是自定义组件
在 JSX 中嵌入表达式
{}
export default function App() {
  const name = "Word";
  return (
    <div className="App">
      <h1>Hello {name}</h1>
    </div>
  );
}
我们可以在{}中书写任意的 JS 表达式,也可以调用函数
function getName(user) {
  return user.firstName + " " + user.lastName;
}
const user = {
  firstName: "Wang",
  lastName: "Hua"
};
export default function App() {
  return (
    <div className="App">
      <h1>Hello {getName(user)}</h1>
    </div>
  );
}
JSX 也是一个表达式
可以在条件/循环语句代码快中使用 JSX,也可以将 JSX 赋值为一个变量,做为函数的返回值/参数
JSX 中指定属性
为标签或者组件的某一个属性赋值时,如果值是一个字符串,则直接经字符串赋值给属性;如果值是一个变量或者是数字,则使用{}
在属性中嵌入 JavaScript 表达式时,不要在大括号外面加上引号。你应该仅使用引号(对于字符串值)或大括号(对于表达式)中的一个,对于同一属性不能同时使用这两种符号。
JSX 语法上更接近于 JS 而不是 HTML,所以对于属性名使用小驼峰来定义,而不是 html 的属性名。例如说在 JSX 中 class -> className
JSX 防注入攻击
可以在 JSX 中安全的插入用户输入的内容(使用{}的方式插入)
React DOM 在渲染所有输入内容之前,默认会进行转义。可以确保所有的内容在渲染之前都会被转换为字符串
JSX 表示对象
Babel 会将 JSX 转换为 React.createElement 的函数调用
const element = <h1 className="greeting">Hello, world!</h1>;
等价于
const element = React.createElement(
  "h1",
  { className: "greeting" },
  "Hello, world!"
);
// 最后生成的对象,简化版
const element = {
  type: "h1",
  props: {
    className: "greeting",
    children: "Hello, world!"
  }
};
这些对象就是 React 元素
元素渲染
React 元素是构成 React 应用的最小模块。
与 DOM 元素不同,React 元素是开销极小的对象。ReactDOM 负责为更新 DOM 来与 React 元素保持一致
React 只会更新它需要更新的部分,即使我们每次都传递一个新的元素给 ReactDOM.render
组件 & props
组件的作用: 代码复用,可以接受一些 props
组件分为函数式组件和 class 组件。在 React Hooks 出现之前,函数式组件是没有状态的,数据来源于 props
函数式组件
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
类组件
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
当 React 元素为用户自定义组件时,它会将 JSX 所接收的属性(attributes)以及子组件(children)转换为单个对象传递给组件,这个对象被称之为 “props”。
需要注意的是,自定义组件需要以大写字母开头。React 会将以小写字母开头的都视为原生的 DOM 元素
props 的只读性
在 React 中 props 是只读的,不要在组件中修改它的值/它上面的属性,如果你需要一个可以改变的值,那么可以将它定义为 state
state 和生命周期
有时候我们希望可以更新组件的数据,此时我们就需要借助 state。state 是每个组件自己私有的,完全受控于组件本身
定义一个 class 组件来使用 state
class Clock extends Component<object, { time: Date }> {
  timer: null | number;
  constructor(props) {
    super(props);
    this.timer = null;
    this.state = {
      time: new Date()
    };
  }
  componentDidMount() {
    this.timer = setInterval(() => {
      this.setState({ time: new Date() });
    }, 1000);
  }
  componentWillUnmount() {
    clearInterval(this.timer);
  }
  render() {
    return <div>{this.state.time.toLocaleString()} </div>;
  }
}
知识点
- 在构造函数中初始化 state 
- 始终使用 props 参数来调用父类构造函数 
- 在构造函数中使用 this 之前必须调用 super 函数 
- componentDidMount 方法会在组件渲染到 DOM 中后运行,渲染到 DOM 中 && 渲染到页面之前,它中的操作会阻塞页面的渲染 
- componentWillUnmount 会在组件卸载之前执行,可以在这里执行一些清除的操作 
- 尽管 this.props 和 this.state 是 React 本身设置的,且都拥有特殊的含义,但是其实你可以向 class 中随意添加不参与数据流的额外字段。 
- render 方法的返回值定义组件如何展示 
- setState() 的调用,React 能够知道 state 已经改变了,然后会重新调用 render() 方法 
useEffect(fn, []) 和 componentDidMount 的区别
- useEffect(fn, [])会在 commit 阶段执行完成之后异步的调用 fn 函数
- componentDidMount 会在 commit 的 mutation 阶段完成组件更新之后的 layout 阶段同步调用,此时组件还没有被渲染在页面上
可见他们的调用时机完全不同
而 useLayoutEffect(fn,[])它的调用时机和 componentDidMount 完全一致也会在 layout 阶段同步调用
commit 阶段
commit 阶段的作用就是将状态变化渲染到视图中也就是将 effect 渲染到视图中,将 commit 阶段分为三个子阶段
- 渲染视图前 before mutation 阶段
- 渲染视图 mutation 阶段 --- placement effect 会在该阶段执行 DOM 的插入操作,在 layout 阶段调用 componentDidMount
- 渲染视图后 layout 阶段
正确的使用 state
不要直接修改 state
this.state.xxx = 123;
tis.state 上的 xxx 属性的值被改变了,但是 React 并不会重新渲染页面。应该使用 this.setState 来修改 state
构造函数唯一可以对 this.state 赋值的地方
state 的更新可能是异步的
无论是使用 react hook 的useState的setState还是使用 class 组件的this.setState的方式来更新 state,我们都无法在更新 state 之后立即拿到最新的 state
出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。
因为 this.props 和 this.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。
要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:
this.setState((state, props) => ({
  c: state.a + props.b
}));
State 的更新会被合并(仅限 this.setState)
当你调用 setState() 的时候,React 会把你提供的对象合并到当前的 state。
useState hook 不会对传入的对象和当前的 state 进行合并,所以我们每次更新是需要保证传入了完整的 state
数据是向下流动的
React 采用的是“自上而下”或是“单向”的数据流。任何的 state 总是所属于特定的组件,而且从该 state 派生的任何数据或 UI 只能影响树中“低于”它们的组件。
事件处理
React 中的事件和 DOM 事件很类似,只有几点不同:
- 事件名称采用小驼峰命名法(onclick(DOM) -> onClick(React))
- 在 JSX 中需要传入一个函数作为事件处理函数,而不是一个字符串
- 在 React 中不能通过返回 false 的方式来阻止默认事件,需要显示的调用 e.preventDefault()
例如
- 事件处理函数的绑定
<button onclick="activateLasers()">
  Activate Lasers
</button>
<button onClick={activateLasers}>Activate Lasers</button>
- 阻止默认行为
<a href="#" onclick="console.log('The link was clicked.'); return false">
  Click me
</a>
function ActionLink() {
  function handleClick(e) {
    e.preventDefault();
    console.log("The link was clicked.");
  }
  return (
    <a href="#" onClick={handleClick}>
      Click me
    </a>
  );
}
tip
- 在这里,e 是一个合成事件。React 替我们处理了事件对象 event 对于不同浏览器的兼容性问题。 
- 在 React 中,我们一般不需要使用 addEventListener 来为 DOM 添加事件处理函数 
在类组件中绑定事件处理函数
class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isToggleOn: true };
    // 为了在回调中使用 `this`,这个绑定是必不可少的
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    this.setState(state => ({
      isToggleOn: !state.isToggleOn
    }));
  }
  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? "ON" : "OFF"}
      </button>
    );
  }
}
ReactDOM.render(<Toggle />, document.getElementById("root"));
tip
在类组件中,为事件处理函数绑定 this 是必不可少的,否则事件处理函数中的 this 指向的就不是组件实例,而是 undefined
这个其实是 JS 函数的特性:
我们都知道 JavaScript 函数中的 this 不是在函数声明的时候定义的,而是在函数调用(即运行)的时候定义的
onClick 调用 handleClick 函数的时候,默认情况下,this 是指向全局的。但是,在 class 中默认使用严格模式,不会默认绑定,所以打印出来的 this 就是 undefined
class Foo {
  constructor(name) {
    this.name = name;
  }
  display() {
    console.log(this.name);
  }
}
var foo = new Foo("coco");
foo.display(); // coco
// 下面例子类似于在 React Component 中 handle 方法当作为回调函数传参
var display = foo.display;
display(); // TypeError: this is undefined
我们在实际 React 组件例子中,假设 handleClick 方法没有通过 bind 绑定,this 的值为 undefined, 它和上面例子类似 handleClick 也是作为回调函数传参形式。 但是我们代码不是在 strict 模式下, 为什么 this 的值不是全局对象,就像前面的 default binding,而是 undefined? 因为 class 内部默认是严格模式。
对于事件处理函数的 this 我们有三种处理方式:
- 在 constructor 中为事件处理函数绑定 this ✅ 
- 事件处理函数使用箭头函数定义 ✅ 
- 在为事件传递事件处理函数的时候绑定 this(不推荐,因为每次组件重新渲染的时候都会创建一个新函数)❌ 
向事件处理函数传递参数
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
- 箭头函数 
- bind 方法 
在这两种情况下,React 的事件对象 e 会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。(通过 bind 传递的参数,事件对象是最后一个)
条件渲染
React 中的条件渲染和 JavaScript 中的条件判断相同。可以使用 if 语句/三元表达式/逻辑与来实现
if 语句
function ShowInfo(props) {
  if (props.showName) {
    return "Lily";
  } else {
    return "-";
  }
}
class App extends Component<AppProps, AppState> {
  constructor(props) {
    super(props);
    this.state = {
      name: "React"
    };
  }
  render() {
    return <ShowInfo showName={false} />;
  }
}
与运算符 &&
通过花括号包裹代码,你可以在 JSX 中嵌入任何表达式。这也包括 JavaScript 中的逻辑与 (&&) 运算符。它可以很方便地进行元素的条件渲染。(对于 undefined/null/true/false 可以实现理想的为真时展示组件,为假时什么都不渲染。但是并不是所有的假值都可以达到理想效果)
例如
0 && <div>a</div>;
此时页面上就会展示 0
在 JavaScript 中,true && expression 总是会返回 expression, 而 false && expression 总是会返回 false。
因此,如果条件是 true,&& 右侧的元素就会被渲染,如果是 false,React 会忽略并跳过它。
三目运算符
另一种内联条件渲染的方法是使用 JavaScript 中的三目运算符 condition ? true : false。
render() {
  const isLoggedIn = this.state.isLoggedIn;
  return (
    <div>
      The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.
    </div>
  );
}
当然也可以渲染组件,有一个分支为 null 时就可以不在页面上渲染任何东西
阻止条件渲染
null
在极少数情况下,你可能希望能隐藏组件,即使它已经被其他组件渲染。若要完成此操作,你可以让 render 方法直接返回 null,而不进行任何渲染。
在组件的 render 方法中返回 null 并不会影响组件的生命周期。
列表 & key
渲染多个组件
{} + JSX
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map(number => <li>{number}</li>);
ReactDOM.render(<ul>{listItems}</ul>, document.getElementById("root"));
但是,当我们运行这段代码,将会看到一个警告 a key should be provided for list items,意思是当你创建一个元素时,必须包括一个特殊的 key 属性。
key
key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。
key 的标准:
- 在列表中独一无二的字符串,一般我们将 id 作为 key(只需要保证在兄弟元素中唯一,不需要全局唯一) 
- 当元素没有一个确定的 id 时,万不得已的情况下,使用 index 作为 key 
如果你选择不指定显式的 key 值,那么 React 将默认使用索引用作为列表项目的 key 值。
元素的 key 只有放在就近的数组上下文中才有意义。
function ListItem(props) {
  // 正确!这里不需要指定 key:
  return <li>{props.value}</li>;
}
function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map(number => (
    // 正确!key 应该在数组的上下文中被指定
    <ListItem key={number.toString()} value={number} />
  ));
  return <ul>{listItems}</ul>;
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById("root")
);
一个很好的经验法则:在 map 方法中的元素需要设置 key
key 会传递信息给 React ,但不会传递给你的组件。如果你的组件中需要使用 key 属性的值,请用其他属性名显式传递这个值
const content = posts.map(post => (
  <Post key={post.id} id={post.id} title={post.title} />
));
上面例子中,Post 组件可以读出 props.id,但是不能读出 props.key。
如果一个 map() 嵌套了太多层级,那可能就是你提取组件的一个好时机
表单
在 React 中,表单通常都是受控组件,即需要定义 value={state} + onChange(改变 value 绑定的 state 值)
受控组件
在 HTML 中,表单元素(如<input>、 <textarea> 和 <select>)通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。把两者结合起来,使 React 的 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>
    );
  }
}
由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。由于 handlechange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。
对于受控组件来说,输入的值始终由 React 的 state 驱动。
textarea 标签 === input 标签
select 标签
在 HTML 中,<select> 创建下拉列表标签。
<select>
  <option value="grapefruit">葡萄柚</option>
  <option value="lime">酸橙</option>
  <option selected value="coconut">椰子</option>
  <option value="mango">芒果</option>
</select>
由于 selected 属性的缘故,椰子选项默认被选中。React 并不会使用 selected 属性,而是在根 select 标签上使用 value 属性。
class FlavorForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = { value: "coconut" };
    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>
          选择你喜欢的风味:
          <select value={this.state.value} onChange={this.handleChange}>
            <option value="grapefruit">葡萄柚</option>
            <option value="lime">酸橙</option>
            <option value="coconut">椰子</option>
            <option value="mango">芒果</option>
          </select>
        </label>
        <input type="submit" value="提交" />
      </form>
    );
  }
}
select 多选
你可以将数组传递到 value 属性中,以支持在 select 标签中选择多个选项:
<select multiple={true} value={['B', 'C']}>
处理多个输入
当需要处理多个 input 元素时,我们可以给每个元素添加 name 属性,并让处理函数根据 event.target.name 的值选择要执行的操作。
需要这样做的前提:
- 每个表单组件都使用同一个 change 函数处理 
- state 的属性名称需要和把 name 值保持一致 
受控组件输入空值
在受控组件上指定 value 属性会阻止用户更改输入(没有使用 onChange 去更改 value 绑定的值时)。如果你指定了 value,但输入仍可编辑,则可能是你意外地将 value 设置为 undefined 或 null。
我们项目中使用的表单
- antd-form 
- 自定义表单组件:只要这个表单有 value 属性(接受外界 form 的赋值)和 onChange 方法(将表单组件的输出给到外部的 form),name 它就是一个表单组件 
import { Form, Radio } from 'antd';
import type { FormItemProps } from 'antd/lib/form';
import type { FC } from 'react';
import type { RadioChangeEvent } from 'antd/lib/radio/interface';
export interface FormRadioProps extends FormItemProps {
  disabled?: boolean;
  list: any[];
  val2Label: Record<any, string>;
  onChange?: (e: RadioChangeEvent) => void;
}
export const FormRadio: FC<FormRadioProps> = (props) => {
  const { disabled = false, list, val2Label, onChange, ...formItemProps } = props;
  return (
    <Form.Item
      // eslint-disable-next-line
      {...formItemProps}
    >
      <Radio.Group disabled={disabled} options={list.map((val) => ({ label: val2Label[val], value: val }))} onChange={onChange} />
    </Form.Item>
  );
};
状态提升
当多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。应当依靠自上而下的数据流,而不是尝试在不同组件间同步 state。
组合 vs 继承
在 React 中,推荐使用组合而非继承的方式来实现代码的重用
包含关系
有些组件无法提前知道它的子组件的内容,此时推荐使用一个特殊的 children prop 来将他们的子组件传递到渲染结果中
function FancyBorder(props) {
  return (
    <div className={"FancyBorder FancyBorder-" + props.color}>
      {props.children}
    </div>
  );
}
少数情况下,你可能需要在一个组件中预留出几个“洞”。这种情况下,我们可以不使用 children,而是自行约定:将所需内容传入 props,并使用相应的 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 />} />;
}
在 React 中我们可以将任意内容作为 props 传入
留一个问题
在 angular 中怎么实现这种方式渲染组件