(TS转译.tsx成为.jsx或.js,)Babel转译.jsx成为.js。
JSX是一个表达式
if
、for
不是表达式。
return 组件
,若多行,则(
)
包裹组件<组件名称>
(<组件名称>
)必须以大写字母开头。
大写字母表示用户定义组件,小写字母表示HTML标签。
.
<对象.属性名>
(不要求大写字母开头)
e.g.
```jsx import React from "react"; const myComponents = { datePicker: function datePicker (props) { return传值data:{props.data}; } }; export default (props) => { return <myComponents.datePicker data={props.data} />; } ```
不能是表达式
e.g.
```jsx // 错误!不能是一个表达式。 return <components[props.storyType] story={props.story} />; // 正确!大写字母开头的变量(不能是小写字母开头) const SpecificStory = components[props.storyType]; return <SpecificStory story={props.story} />; ```
Props
采用小驼峰式(camelCase)定义标签的属性名称
类似DOM对象的
properties
名。
任何标准的或自定义的DOM属性都是完全支持。
(class
是保留字,)className
代替class
若在React中使用Web Components(这是一种不常见的使用方式),则使用
class
属性代替。
htmlFor
代替for
value
、onChange
、selected
、checked
defaultValue
、defaultChecked
style
特殊情况用-
短横线隔开式(kebab-case)的属性名:
aria-*
data-*
特殊属性
dangerouslySetInnerHTML=
为浏览器DOM提供innerHTML的替换方案。不会进行HTML的字符实体(character entity)转义,有XSS风险。
suppressContentEditableWarning
suppressHydrationWarning
赋值
{}
包裹JS表达式赋值给Props:
某prop={JS表达式}
字符串字面量:
某prop="某字符串"
若没有给Props赋值,则默认值是true
:
某prop
{...某对象}
展开元素
很容易将不必要的props传递给不相关的组件,或者将无效的HTML属性传递给DOM。需要谨慎的使用该语法,如:const {children, xx, otherProps} = props; return <组件名 {...otherProps} />
。
e.g.
<组件名 {...某对象} />
包含在开始和结束标签之间的表达式内容将作为特定属性:props.children
可插入内容:
{JS表达式}
&&
、||
condition ? true : false
Array
方法
任何数据类型(传递给组件的props.children
使用)
e.g. 函数、引用数据类型
HTML标签间可返回:
组件、HTML标签、String类型、Number类型、数组
其他数据类型可能导致报错。
false
、true
、null
、undefined
、Symbol类型、BigInt类型对空白的处理:
key
)注释
{/* 单行注释 */}
{/*
多行注释
*/}
由
ReactDOM.render
对根DOM组件开始初始化-渲染,随着引入的子组件再以树状结构对子组件进行初始化-渲染。
ReactDOM.render
渲染到根DOM(可多次调用)。
React DOM
React元素(React elements)
组件渲染
组件渲染完成后返回React元素。
render
方法(或函数组件)返回null
,组件会正常运行和执行生命周期函数,只是不渲染出任何DOM(因为渲染空内容)。API
React.cloneElement(element[, props[, ...children]])
(已废弃:)React.addons.cloneWithProps
克隆并返回新的React元素。
几乎等同于:
<element.type {...element.props} {...props}>{children}</element.type>
+ 原element上的key
和ref
。添加的props
的属性会覆盖element
原本的属性。
e.g.
把接受到的参数传递给`props.children`渲染: ```tsx const { children, ...otherProps } = props; return ( <> {React.Children.map(children, (child) => { return React.cloneElement(child, {...otherProps, 新增参数: '值'}); })} </> ) ```
React.isValidElement(对象)
验证对象是否为React元素。返回:true/false
。
React.Children
+ .map/.forEach/.count/.only/.toArray
处理this.props.children
组件:它接受任意的入参(Props),并返回用于描述页面展示内容的React元素。
组件类型:
class组件(class components)
e.g.
```jsx class 组件名 extends React.Component { // 或 React.PureComponent render() { return ( <div className="m-div" onClick={this.props.onClick}> {this.props.value} </div> ); } } ```
函数组件(function components,FC)
不能包含State(Hook弥补),没有生命周期(Hook弥补),没有this。
e.g.
```jsx function 组件名 (props) { // 没有this function handleClick (e) { console.log(e, e.preventDefault); } return ( <div onClick={props.onClick} onClick={(e) => {props.onClick(e);}} // 匿名函数 onClick={handleClick} onClick={(e) => {handleClick(e);}} // 匿名函数 > {props.value} </div> ); } ```
限制
style
、className
等,需要在组件内接受并处理)。default
方式导出。State
State是私有的,并且完全受控于当前组件,其父、子组件均不可见。
初始化:
class 组件名 extends React.Component {
constructor(props) { // 覆盖class的实例属性
super(props)
this.state = {
value1: 1,
value2: 2
}
}
// 或
state = {
value2: 22,
value3: 3
}
}
// 结果:初始化的State为`{value1: 1, value2: 2}`
setState
(或forceUpdate
)修改State的更新是异步的(setState
是异步的,并且在同一周期内会对多个setState
进行批处理,浅合并)
原因讨论。
this.props
和this.state
可能会异步更新。
给setState
传递一个函数,而不是一个对象,就可以确保每次的调用都是使用最新版的state
和props
。
e.g.
```jsx // 错误 this.setState({ counter: this.state.counter + this.props.increment, }); // 正确 this.setState((state, props) => ({ // 第一个参数是上一个State,第二个参数是此函数运行时的Props counter: state.counter + props.increment })); ``` ```jsx incrementCount() { // 错误:调用一次 handleSomething 只能导致 + 1 this.setState({count: this.state.count + 1}); // 正确:调用一次 handleSomething 导致 + 3 this.setState((state) => { return { count: state.count + 1 } }); } handleSomething() { this.incrementCount(); this.incrementCount(); this.incrementCount(); } ```
若要确保更新State之后执行方法,则使用componentDidUpdate
或setState
的第二个回调函数参数。
setState
设置相同的值:
根据shouldComponentUpdate
返回的值决定是否更新真实DOM(是否触发render
)。
React.Component
的shouldComponentUpdate
默认返回true
:总会更新DOM。React.PureComponent
的shouldComponentUpdate
默认实现(修改实现会Warning提示):浅比较props
和state
,若相同则返回false
不更新,若不相同则返回true
更新。
属性值改变的策略
模板中渲染相关的属性(如:要在模板内展示的属性 或 Props传值、style
取值等),需要放到State中被观测(或放到store中被观测,如:redux、mobx),才能在这些值改变时通知视图重新渲染(this.setState
)。
使用函数组件的话,放在
useState
等里效果相同。
this.setState(对象或函数, (/* 无参数 */) => {/* 更新后的回调 */})
函数是唯一能够更新this.state.属性
的方式。
不可变性(引用数据类型)
state
、props
、store、等,都建议遵循不可变性。
可以用immer处理(与react组件配合方式:React & Immer)。
e.g. ```javascript import produce from "immer" const baseState = [ { willDo: "Learn typescript", done: true }, { willDo: "Try immer", done: false } ] const nextState = produce(baseState, draftState => { draftState.push({willDo: "Tweet about it"}) draftState[1].done = true }) ``` 上面的示例中,对`draftState`的修改都会反映到`nextState`上,并且不会修改`baseState`。而immer使用的结构是共享的,`nextState`在结构上与`currentState`共享未修改的部分。
不直接修改数据(或改变底层数据),而是用新值替换旧值。(对需要修改的State内容浅复制一层,对新值进行修改后覆盖原State)
优势:
- 简化复杂的功能,方便切换历史数、time-travel。
- 跟踪数据的改变,方便确定数据是否发生了变化。
不在模板内展示的值(如:仅作为事件触发修改的状态值 或 仅在JS逻辑中改变的值),直接放在class中的实例属性中。
使用函数组件的话,放在
useRef
里效果相同。
componentWillUnmount
中处理)。所有这个组件的实例全部共用同一个值,不响应式更新,放在组件外部。
e.g.
```jsx let value = 0 // 该组件的所有实例共享 class MyComponent extends React.Component { constructor (props) { super(props) this.state = { cur: 0 // 需要放到模板内展示 } } componentDidMount () { this.timerID = setInterval( () => {}, 1000 ); } componentWillUnmount () { clearInterval(this.timerID); } isLoading = false // 不需要在模板内展示 render () { return ( <View onClick={() => { this.setState({ // setState修改 cur: this.state.cur + 1 }) this.isLoading = !this.isLoading // 直接赋值修改 value = value + 1 // 直接赋值修改 }} p1={this.state.cur} // 能够响应式传递进去 p2={this.isLoading} // 不能响应式传递进去 p3={value} // 不能响应式传递进去 > {this.state.cur} {/* 值改变,能够响应式触发更新 */} {this.isLoading} {/* 值改变,不能够响应式触发更新 */} {value} {/* 值改变,不能够响应式触发更新 */} </View> ) } } ```
forceUpdate
跳过 ,直接触发shouldComponentUpdate
render
。
Props
可以把任何东西当做Props传递,如:组件、函数、JS的任意数据类型。只读的,不应以任何方式修改。
默认值
class组件的静态属性添加defaultProps
(用于props未赋值,但又不能为null
的情况)。
e.g.
```jsx class 组件名 extends React.Component { static defaultProps = { 属性1: "默认值1", 属性2: "默认值2" }; } // 或 组件名.defaultProps = { // 覆盖class内部的静态属性 属性2: "默认值2_", 属性3: "默认值3" } // 结果:默认的Props为`{属性2: "默认值2_", 属性3: "默认值3"}` ```
特殊属性
key
的取值和逻辑,与Vue中key
的注意点(17.i)一致
key
不是Props(经过React特殊处理),无法传递给子节点。
因为JSX的灵活,只要是能够切换或条件判断
的都需要考虑key
。包括:Array
方法、数组、条件判断(switch
、if
)等。
ref
ref
不是Props(经过React特殊处理),无法传递给子节点。
不能在函数组件上使用
ref
属性,因为他们没有实例。e.g.。<函数组件 ref={React.createRef()} />
由React.createRef()
创建,.current
获取DOM或子组件实例。
createRef
主要用在class组件(函数组件内基本没有使用,都可以用createRef
的场景useRef
替代);useRef
只能用在函数组件。
可以从父级传递React.createRef()
实例到子组件:
不能在函数组件中传入
ref
(因为函数组件没有实例)。
ref=React.createRef()
,父组件获取子组件(不是子组件内部节点)。参数=React.createRef()
,父组件获得子组件内部节点的ref
。引用子组件ref=React.createRef()
,子组件是Refs转发(React.forwardRef((props, ref) => {})
),父组件获取子组件内部节点的ref
ref
回调Refs
传递函数给ref
属性。组件挂载时,会调用ref
回调函数,并传入DOM元素,当卸载时调用它并传入null
。
componentDidMount
或componentDidUpdate
触发前,React会保证回调Refs
一定是最新的;若回调Refs的函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数null
,然后第二次会传入参数DOM元素。
这是因为在每次渲染时会创建一个新的函数实例,所以React清空旧的
ref
并且设置新的。通过将ref
的回调函数定义成class
的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。
(已废弃)this.refs.值
e.g.
```jsx // father import React from "react"; import Son1 from "./Son1"; import Son2 from "./Son2"; import Son3 from "./Son3"; import Son4 from "./Son4"; export default class Father extends React.Component { constructor(props) { super(props); this.divRef = React.createRef(); this.sonRef = React.createRef(); this.sonDomRef = React.createRef(); this.sonForwardRef = React.createRef(); this.funcRef1 = null; this.funcRef2 = null; } render() { return ( <div onClick={() => { console.log(this.divRef, this.sonDomRef, this.sonRef, this.sonForwardRef, this.funcRef1, this.funcRef2); }}> <div className="m-ref" ref={this.divRef} // 本组件内使用,获取DOM > this.divRef </div> <Son1 ref={this.sonRef} // 获取子组件(不是子组件内部DOM) /> <Son2 refData={this.sonDomRef} // 获取子组件内部节点的`ref` /> <Son3 ref={this.sonForwardRef} // 获取子组件内部节点的`ref`,要求子组件是Refs转发 /> <div className="m-ref-func" ref={(dom) => { // 节点更新则触发2次,一次null一次dom console.log(dom, "m-ref-func"); this.funcRef1 = dom; }} > this.funcRef1 </div> <Son4 funcRef={(dom) => { // 节点更新则触发2次,一次null一次dom console.log(dom, "Son4"); this.funcRef2 = dom; }} /> </div> ); } } // Son1Son1// Son2<div className="m-son-2-ref" ref={this.props.refData}> Son2</div> // Son3 import React from "react"; export default React.forwardRef((props, ref) => { return (<div className="m-son-3-ref" ref={ref}> Son3</div> ); }); // Son4<div className="m-son-4-ref" ref={this.props.funcRef}> Son4</div> ```
适用场景
避免使用ref来做任何可以通过声明式实现来完成的事情。
把DOM或React元素传入子组件
children
传入子组件内的内容(开始标签和结束标签之间的内容),都会在子组件的props.children
中。
传参
e.g.
```jsx export default function Father () { return ( <Son1 aa={} // 传参 bb={ from Father 2
} // 传参 > {/*children*/}from Father 3
</Son1> ); } function Son1 (props) { return ( {props.children} {props.aa} {props.bb}); } function Son2 (props) { return (Son2); } ```
插槽(类似Vue)
若父子组件的结构有耦合,则需要用插槽。
e.g.
```tsx // Father.tsx import Son from "./Son"; export default function SlotCompoment() { return ( <Son Slot={({ children }) => { return (②father's Slot 父组件数据 {children}); }} children={({ children }) => { return (④father's children 父组件数据 {children}); }} /> ); } ``` ```tsx // Son.tsx interface Props { Slot: React.FC<{ children: React.ReactNode }>; children: React.FC<{ children: React.ReactNode }>; } export default function Son(props: Props) { // children需要换个变量名才可以<组件名> const { Slot, children: Children } = props; return ( <> ①slot son 子组件数据 ③slot son's Slot 子组件数据</> ); } ``` ```text -> ①slot son 子组件数据 ②father's Slot 父组件数据 ③slot son's Slot 子组件数据 ④father's children 父组件数据 ⑤slot son's children 子组件数据 ``` </details> 组件名> ⑤slot son's children 子组件数据
组合、继承
用组合(import
和Props)的方式、而不是 继承( 的方式构建新组件。extends
)
特例关系(specialization)
一个组件是其他组件的特殊实例(special case)。
e.g.
```jsx function Dialog(props) { return (); } // WelcomeDialog 可以说是 Dialog 的特殊实例 function WelcomeDialog() { return ( ); } ``` {props.title}
{props.message}
书写方式
render
函数中事件触发组件方法时传递this
的绑定事件方式:
非React提供的事件监听回调函数的this绑定也同理。
仅一次声明(推荐)
class 组件名 extends React.Component {
constructor (props) {
super(props);
// 关键代码:
this.handleClick = this.handleClick.bind(this);
}
handleClick () {
console.log(this, arguments) // 组件实例this, [事件回调参数]
}
render () {
return (
<button onClick={this.handleClick} />
);
}
}
class 组件 extends React.Component {
// 关键代码:
handleClick = (..._arguments) => { // 注意: 这是 *实验性* 语法(默认启用此语法)
console.log(this, _arguments); // 组件实例this, [事件回调参数]
};
render () {
return (
<button onClick={this.handleClick} />
);
}
}
组件render时会创建不同回调函数(不推荐)
若这种写法作为Props传入子组件,则父级每次render都会导致传入子组件的这个Props变化,导致子组件重新渲染(性能问题)。
e.g.
```jsx class 组件 extends React.Component { handleClick () { console.log(this, arguments); // 组件实例this, [事件回调参数] }; render () { return ( <button // handleClick的this指向组件实例 onClick={this.handleClick.bind(this)} onClick={(e) => {this.handleClick(e);}} // onClick={this.handleClick} // handleClick的this指向undefined /> ); } } ```
this.
会取到最新值,注意异步操作后的取值
可以用解构的方式在方法顶部先固定this.
当前的值,之后异步时使用。
通信
父子组件间的通信(单向数据流/单向绑定)
父级(Props) -> 子级。其中Props包括:数据(可以是父级的State) + 父级的方法。
e.g.
```jsx // 父级 <子组件 必要属性1='你好' 可选方法1={this.父级方法} // 父级方法的参数:子级调用时传入 /> ``` ```jsx // 子级 import React from "react"; import { View } from "react-native"; // 可选Props interface 组件PropsOptional { 可选属性1?: boolean, 可选属性2?: number, 可选方法1?: () => void, } // +必要Props interface 组件Props extends 组件PropsOptional { 必要属性1: string } // State interface 组件State { 本地属性1: number } export default class 组件 extends React.Component<组件Props, 组件State> { public static defaultProps: 组件PropsOptional = { 可选属性1: false, 可选属性2: 123, }; public readonly state: 组件State = { 本地属性1: this.props.可选属性2 || 0 }; public constructor (props: 组件Props) { super(props); } public render () { const { 本地属性1 } = this.state; const { 必要属性1, 可选属性1, 可选方法1 } = this.props; return( <View onClick={() => { this.setState({ 本地属性1: 本地属性1 + 1 }); 可选方法1 && 可选方法1(子级参数); }} >{本地属性1} {必要属性1} {可选属性1 ? '可选属性1: true' : '可选属性1: false'} </View> ); } } ```
父 -> 子:触发子组件的方法
父级引用子级的方法
子组件是class组件
父级ref引用子级实例,直接调用实例方法。
e.g.
<Son ref={a}>
,a(或a.current)为子级实例,.实例属性方法
。
子组件是函数组件
子级利用useImperativeHandle
(可配合forwardRef
等)导出方法,父级ref引用子级暴露的方法。
e.g.
<Son ref={a}>
,a(或a.current)为对象,包含子级主动暴露的方法,.主动暴露的方法
。
子级通过观察父级修改的数据后触发(利用props或redux等)
非父子组件通信
祖孙组件间的通信
从祖辈向所有孙辈传递
兄弟节点间通信,可以通过在公共的祖先组件设置
Provider
并且value
传递能够修改value
值的方法,兄弟节点设置Consumer
,进行通信。
const MyContext = React.createContext(默认值)
返回{ Provider, Consumer }
。
祖辈组件设置:<MyContext.Provider value={/* 某个值 */}>
可以嵌套不同context的Provider。
当Provider
的value
值发生变化时,它内部所有孙辈节点的this.context
和Consumer组件
都会重新渲染。
孙辈组件使用的2种方式:
从组件树中离自身最近匹配Provider
中读取到当前的context
值。
contextType
class组件的静态属性添加contextType
。
e.g.
```jsx class 组件名 extends React.Component { static contextType = MyContext; } // 或 组件名.contextType = MyContext // 覆盖class内部的静态属性 ```
孙辈组件中通过this.context
使用。
<MyContext.Consumer>{(data) => { return 组件 }}</MyContext.Consumer>
孙辈组件中通过props.children
传入函数使用。
Provider
及其内部Consumer组件
都不受制于shouldComponentUpdate
函数,因此当Consumer组件
在其祖辈组件退出更新的情况下也能更新。
e.g.
```tsx // 使用 Provider、Consumer import React, { useState } from "react"; import { MyContext1 } from "../../context"; import TheContextSon from "./TheContextSon"; export default function TheContext() { const [data, setData] = useState({ a: 10 }); return ( <> <MyContext1.Provider value=> {/* 子组件1、兄弟组件,正常工作 */}{/* 子组件2、兄弟组件,正常工作 */} </MyContext1.Provider> {/* 非子组件,无效,不变化 */} </> ); } // ../TheContextSon.tsx import React from "react"; import { MyContext1 } from "../../context"; export default function TheContextSon() { return ( {({ state, dispatch }) => { return ( <div onClick={() => { dispatch({ a: state.a - 1 }); }} > {JSON.stringify(state)} </div> ); }} ); } // ../../context.ts import React from "react"; export const MyContext1 = React.createContext({ state: { a: 1 }, dispatch(data: { a: number }) {}, }); ```
通用
若多个组件需要反映相同的变化数据,则将共享状态提升到最近的共同父组件中去。
应当依靠自上而下的数据流,而不是尝试在不同组件间同步State。
若某些数据可以由Props或State推导得出,则它就不应该存在于State中
样式
style
style={[ 对象1, 判断 ? 对象2 : ''或null或undefined ]}
,空不能用{}
。
style
在React应用中多用于在渲染过程中添加动态计算的样式。
通常不推荐将style
属性作为设置元素样式的主要方式。在多数情况下,应使用className
属性来引用外部CSS样式表中定义的class。
样式不会自动补齐前缀。若需要,则需要手动补充。
e.g.
```javascript const divStyle = { transition: 'all', MozTransition: 'all', OTransition: 'all', WebkitTransition: 'all', msTransition: 'all' // 'ms' is the only lowercase vendor prefix }; ```
React会自动添加px
后缀到内联样式为数字的属性后。
px
以外的单位,请将此值设为数字与所需单位组成的字符串。className
在其他地方全局引入.css
文件(import './x.css'
或 html的css引入),className="类名"
或className={变量或类名字符串}
除了引入.css文件(或CSS预处理器文件)之外,其他支持:
CSS in JS方案:styled-components(运行时生成<style>
类样式,供给生成的样式组件的className使用)
CSS in JS/CSS-in-JS:CSS由JavaScript生成而不是
在外部文件(.css文件或CSS预处理器文件)中定义。
事件处理
事件处理函数的参数是一个合成事件(e
)。
e.preventDefault()
、e.stopPropagation()
。debounce/throttle事件处理
class组件
// 不随渲染而改变的函数
debounceFunc = debounce((e, otherValue) => {
// do something...
}, 500);
<input onChange={(e) => debounceFunc(e, 其他响应式数据)}>
函数组件
// 不随渲染而改变的函数(随依赖项而改变)
const debounceFunc = useCallback( // 或:useRef
debounce((e, otherValue) => {
// do something...
}, 500),
[依赖项]
);
<input onChange={(e) => debounceFunc(e, 其他响应式数据)}>
特殊组件
高阶组件(higher order component,HOC)
是一种设计模式。高阶函数 (Higher-Order Function,HOF):以函数为参数或/和返回值的函数。
参数为组件,返回值为新组件的函数。
约定:
将不相关的Props传递给被包裹的组件。
特殊属性无法传递:ref
(可以利用Refs转发传递)、key
。
最大化可组合性。
注意事项:
不要在render
方法中使用
e.g.
```jsx render() { // 每次调用 render 函数都会创建一个新的 EnhancedComponent // EnhancedComponent1 !== EnhancedComponent2 const EnhancedComponent = enhance(MyComponent); // 应该在非render处仅创建一次 // 这将导致子树每次渲染都会进行卸载,和重新挂载的操作! return; } ```
务必复制静态方法
e.g.
```jsx import hoistNonReactStatic from 'hoist-non-react-statics'; function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} hoistNonReactStatic(Enhance, WrappedComponent); // 把所有WrappedComponent的静态方法(不包括react原生静态方法)拷贝到Enhance return Enhance; } ```
ref
(和key
一样)不会被传递,需要使用React.forwardRef
e.g.
```jsx // 高阶组件 HOCComponent.js import React from "react"; export default (WrappedComponent) => class WrapperComponent extends React.Component { render () { console.log(this.props, 'HOCComponent') return <WrappedComponent {...this.props} />; } } // 利用高阶组件生成的新组件 WrappedComponent.js import React from "react"; import HOC from "./HOCComponent"; export default HOC( class WrappedComponent extends React.Component { render () { returnWrappedComponent} } ); // 使用组件```
<React.Fragment>
key
属性。<>子节点</>
(不支持所有属性,包括不支持key
属性)。表单
受控组件(controlled components)
事件:
onChange
。每当表单字段变化时,该事件都会被触发。
表单数据由React组件来管理。渲染表单的React组件还控制着用户输入过程中表单发生的操作(表单的value
、onChange
、selected
、checked
受组件控制)。React的State成为表单“唯一数据源”。
<input type="text">
、<textarea type="text">
e.g.
```jsx constructor (props) { super(props); this.state = { value: "初始化值" }; this.handleChange = this.handleChange.bind(this); } handleChange (event) { this.setState({ value: event.target.value }); } render () { return ( <input或textarea type="text" value={this.state.value} onChange={this.handleChange} /> ); } ```
<input type="checkbox">
e.g.
```jsx function Checkbox() { const [checked, setChecked] = React.useState(true); return ( ); } ```
<input type="radio">
e.g.
```jsx function Radio() { const [radioValue, setRadioValue] = useState<"chooseA" | "chooseB">("chooseB"); return <> </> } ```
<select>
e.g.
```jsx // 单选 constructor (props) { super(props); this.state = { value: "coconut" }; this.handleChange = this.handleChange.bind(this); } handleChange (event) { this.setState({ value: event.target.value }); } render () { return ( <select value={this.state.value} onChange={this.handleChange}> </select> ); } ``` ```jsx // 多选 constructor (props) { super(props); this.state = { value: ["coconut", "mango"] }; this.handleClick = this.handleClick.bind(this); } handleClick (event) { const value = event.target.value; this.setState((state) => { const oldValue = state.value.slice(); if (oldValue.includes(value)) { oldValue.splice(oldValue.indexOf(value), 1); } else { oldValue.push(value); } return { value: oldValue }; }); } render () { return ( <select multiple={true} value={this.state.value} onChange={this.handleClick}> </select> ); } ```
在受控组件上写死value
会阻止用户输入(除了 )。null/undefined
e.g.
- 无法输入改变表单:
<input value="hi" />
。- 可以输入改变表单:
<input value={undefined} />
、<input value={null} />
、<input />
。
非受控组件(uncontrolled components)
表单数据将交由DOM节点来处理。
默认值defaultValue
、defaultChecked
e.g.
<input defaultValue="默认值" type="text" ref={this.input} />
<input type="file" />
始终是一个非受控组件
<input type="checkbox">
e.g.
```jsx function Checkbox() { const [checked, setChecked] = React.useState(true); return ( ); } ```
e.g.
```jsx 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}>
</form> ); } ```
错误边界(error boundaries)
若一个class组件中定义了static getDerivedStateFromError()
(渲染备用UI)或componentDidCatch()
(打印错误信息)这两个生命周期方法中的任意一个(或两个)时,则它就变成一个错误边界。可以当做常规组件去使用。
任何未被错误边界捕获的错误将会导致整个React组件树被卸载。
无法捕获错误的场景:
事件处理
利用
try-catch
弥补。
它自身抛出的错误
若一个错误边界无法渲染错误信息,则错误会冒泡至最近的上层错误边界。
严格模式<React.StrictMode>
仅在开发模式下运行;它们不会影响生产构建。
在开发模式中,对所有孙辈组件运行严格模式检查。
this.refs.值
)findDOMNode
方法的警告检测意外的副作用
故意重复调用以下函数来实现检测:
constructor
、render
、shouldComponentUpdate
getDerivedStateFromProps
setState
的第一个参数)useState
、useMemo
或useReducer
React.memo
类似
React.Component
实现浅比较shouldComponentUpdate
或React.PureComponent
。
若组件在相同props的情况下渲染相同的结果,则可以通过将其包装在React.memo
中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React将跳过渲染组件的操作并直接复用最近一次渲染的结果。
e.g.
```jsx function MyComponent(props) { /* 函数组件,使用 props 渲染 */ } function areEqual(prevProps, nextProps) { /* 默认是浅比较props。 若把 nextProps 传入 render 方法的返回结果 与 将 prevProps 传入 render 方法的返回结果一致则返回 true, 否则返回 false。 (与shouldComponentUpdate返回值相反) */ } export default React.memo(MyComponent[, areEqual]); ```
ReactDOM.createPortal(组件, 挂载去的DOM节点)
节点渲染挂载到存在于父组件以外的DOM节点。
除了挂载到其他DOM之外,其他特性均没有改变,视作其父组件引用的子组件。
事件冒泡、context等所有行为,也在组件的父级-子级间进行,不会与挂载到的DOM节点有联系。
<Profiler id="id值" onRender={回调函数}>
生产构建默认会被禁用。
测量渲染一个React应用多久渲染一次以及渲染一次的“代价”。它的目的是识别出应用中渲染较慢的部分。
<React.Suspense>
、React.lazy
代码分割(动态加载)。
命名规范
on[Event]
;事件监听处理函数命名为:handle[Event]
。displayName
React DevTools使用该字符串来确定要显示的名字。
class组件的静态属性displayName
或 函数组件名.displayName
赋值 新组件名
e.g.
```jsx class 组件名 extends React.Component { static displayName = '新组件名' } // 或 组件名.displayName = '新组件名' // 覆盖class内部的静态属性 // 结果:显示为:`新组件名` ```
默认显示:class组件名或函数组件名。
React.createContext()的对象.displayName = 「context名」
显示为:「context名」.Provider
和「context名」.Consumer
。
默认显示:
Context.Provider
和Context.Consumer
。
React.forwardRef的匿名函数.displayName = 「Refs转发组件名」
显示为:「Refs转发组件名」 ForwardRef
默认显示:
Anonymous ForwardRef
或「匿名函数名」 ForwardRef
。
constructor(props)
super(props)
。作用
this.state
赋值对象来初始化内部state
(不能用 this.setState
this
。props
的值复制给state
。避免引入任何副作用或订阅。
static getDerivedStateFromProps(props, state)
this
指向类名,不指向实例。
render
方法之前调用。唯一作用:让组件在props
变化时更新state
、state
的值在任何时候都取决于props
(需要开发者自行判断)。
返回一个对象来更新state
,若返回null
则不更新state
。
e.g.
```jsx class ExampleComponent extends React.Component { // 在构造函数中初始化state 或 使用属性初始化器 state = { isScrollingDown: false, lastRow: null, }; static getDerivedStateFromProps(props, state) { if (props.currentRow !== state.lastRow) { return { isScrollingDown: props.currentRow > state.lastRow, lastRow: props.currentRow, }; } // 返回 null 表示无需更新 state return null; } } ```
此方法无权访问组件实例(
this
返回undefined
)。
shouldComponentUpdate(nextProps, nextState)
this.props
和this.state
都是当前值(未改变的,不是、nextProps
)。调用组件方法nextState
this.func
,组件方法用当前值,若需要用next值,要用传参的方式this.func(nextProps, nextState)
或其他方法(nextProps, nextState)
。
重新渲染前被触发。
true
,更新真实DOM(触发componentWillUpdate+render+componentDidUpdate
);false
,(可能)跳过本次更新DOM(不触发 componentWillUpdate+render+componentDidUpdate
forceUpdate
时不执行。JSON.stringify
。这样非常影响效率,且会损害性能。应该仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生bug。应该考虑使用内置的
PureComponent
组件。
render()
不可省略。应该为纯函数。
getSnapshotBeforeUpdate(prevProps, prevState)
render
触发之后、DOM真实渲染之前触发。componentDidUpdate
的第三个参数。componentDidMount()
组件已经装载。
执行有副作用的操作。依赖DOM的初始化操作、网络请求、订阅。
componentDidUpdate(prevProps, prevState, snapshot)
更新后会被立即调用。首次渲染时不执行。
getSnapshotBeforeUpdate
返回的值作为第三个参数。
componentWillUnmount()
addEventListener
)、订阅、计时器、http连接、以及任何需要手动关闭的内容,需要在componentWillUnmount
手动清除。
render
返回null
时不会触发componentWillUnmount
,只有组件被卸载才会触发。不应该调用,将永远不会重新渲染。this.setState
UNSAFE_componentWillReceiveProps(nextProps)/componentWillReceiveProps
props
发生变化时执行;若父组件导致组件重新渲染,即使props
没有更改,也会执行。this.setState()
来更新组件状态。UNSAFE_componentWillUpdate(nextProps, nextState)/componentWillUpdate
props
或state
时,会在渲染之前执行。不可调用
this.setState
;在componentWillUpdate
返回之前,你也不应该执行任何其他操作(例如,redux的store.dispatch(某个action)
)触发对React组件的更新。
UNSAFE_componentWillMount()/componentWillMount
唯一会在服务端渲染时调用的生命周期。避免引入任何副作用或订阅。
错误边界
仅捕获子组件树中的错误,但它本身组件的错误无法捕获。捕获子组件树在渲染期间、生命周期方法、其整个树的构造函数中发生的错误。
static getDerivedStateFromError(error)
若后代组件抛出错误则被调用。它将抛出的错误作为参数,并返回一个对象以更新state
。
必须是纯函数。
componentDidCatch(error, info)
允许执行副作用。
无法捕获错误的场景:
事件处理
利用
try-catch
弥补。- 异步代码
- 服务端渲染
它自身抛出的错误
若一个错误边界无法渲染错误信息,则错误会冒泡至最近的上层错误边界。
Hook是一些可以在函数组件里“钩入”React state及生命周期等特性的函数。
使用规则:
只能在函数最外层调用Hook。不要在循环、条件判断或嵌套函数中调用。
在条件语句中有
return
的语句之后调用也不可以。
只能在React的函数组件 或 自定义Hook 中调用Hook。
不能在class组件中使用;不要在其他JS函数中调用。
运行规则:函数组件的函数体先执行,按顺序执行语句(包括hook);hook的回调函数在函数组件的函数体执行完毕之后,按照类型和顺序执行。
useState
const [变量名, 修改变量的方法名] = useState(初始值 或 初始方法)
变量名
值:一开始是初始值(或初始方法返回的值),后续是传入修改变量的方法名
的新值(或函数返回的新值)。
修改变量的方法名
(旧值) => 新值
)若修改变量的方法名
的返回值和当前的值完全相同,则不会产生渲染、effect不会执行、也不会运行整个函数组件(子组件也跳过)。
使用
Object.is
进行比较。
this.setState
不同点:不会把新的state和旧的state进行合并,而是用新的state整个替换旧的state。初始值可以是:值 或 函数(仅在初始渲染时调用)
useReducer
useState
的替代方案。
const [变量名, 修改变量的方法名] = useReducer(reducer函数, 初始值或传入初始函数的值[, 初始函数])
若修改变量的方法名
的返回值和当前的值完全相同,则不会产生渲染、effect不会执行、也不会运行整个函数组件(子组件也跳过)。
使用
Object.is
进行比较。
e.g.
```jsx function init(initialCount) { return { count: initialCount?.count || initialCount || 0 }; } function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; case 'reset': return init(action.payload); default: throw new Error(); } } function Counter({initialCount}) { // props传入作为初始化数据 // <Counter initialCount={数字}/> const [state, dispatch] = useReducer(reducer, initialCount, init); // 若有最后一个初始化方法,则init(initialCount)为初始state //// const [state, dispatch] = useReducer(reducer, initialCount); // 若没有最后一个初始化方法,则第二个参数直接为初始state return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); } ```
useEffect
类似生命周期的效果:
componentDidMount
+componentDidUpdate
+componentWillUnmount
。接收一个包含命令式、且可能有副作用代码的函数。
useEffect(() => {
/* 副作用逻辑 */
return () => {
/* 清除副作用逻辑 */
}
}[, 依赖项数组])
React会在每次渲染完成后调用副作用函数,包括第一次渲染时。
虽然useEffect会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。
依赖项
,仅当依赖项内的值都变化才触发执行。
依赖项的每一项进行浅比较
Object.is()
判断是否变化。
确保数组中包含了所有外部作用域中会发生变化
且在effect中使用
的变量(包括要用到的state
和props
),否则代码会引用到先前渲染中的旧变量。
变量一般是其他hook方法产生的变量,也可以是任意变量。
若想执行只运行一次的effect(仅在组件挂载和卸载时执行),可以传递一个空数组([]
)作为第二个参数。
这就告诉React你的effect不依赖于props或state中的任何值,所以它永远都不需要重复执行。
此时,effect内部的props
和state
就会一直保持其初始值。
使用async-await
useEffect
应该返回一个方法(有其特别的意义:清除方法)。async方法返回的是Promise实例。
// 不可以
useEffect(async () => {},)
// 可以
useEffect(() => {
async function func1 () {}
func()
const func2 = async function () {}
func2()
(async function(){}())
},)
执行时机
1. `useEffect` 1. 首次渲染: 函数组件执行 -> React渲染 -> DOM修改 -> useEffect执行 -> 浏览器渲染 2. 非首次渲染: 函数组件执行 -> React渲染 -> DOM修改 -> 浏览器渲染 -> 上一个useEffect清除执行(使用上次useState产生值) -> useEffect执行 3. 卸载组件: 函数组件执行 -> React渲染 -> DOM修改 -> 浏览器渲染 -> 上一个useEffect清除执行(使用上次useState产生值) 2. `useLayoutEffect` 1. 首次渲染: 函数组件执行 -> React渲染 -> DOM修改 -> useLayoutEffect执行 -> 浏览器渲染 2. 非首次渲染: 1. 函数组件执行 -> React渲染 -> DOM修改 -> 上一个useLayoutEffect清除执行(使用上次useState产生值) -> useLayoutEffect执行 2. -> 若导致函数组件需要再次执行,则再次 a. 执行一遍逻辑,直到没有JS代码执行 3. -> 浏览器渲染 3. 卸载组件: 函数组件执行 -> React渲染 -> DOM修改 -> 上一个useLayoutEffect清除执行(使用上次useState产生值) -> 浏览器渲染 - useEffect + useLayoutEffect,同时执行,会按照useLayoutEffect微调执行useEffect时机
useLayoutEffect
与useEffect
函数签名一致。
区别:
useEffect
DOM更新完、浏览器渲染完毕之后,异步调用useEffect函数。
useLayoutEffect
DOM变更之后同步执行useLayoutEffect函数,阻塞浏览器渲染(useLayoutEffect执行完毕之后线程才会去执行浏览器绘制)。
自定义Hook
通过自定义Hook,可以将组件逻辑提取到可重用的函数中。和使用官方hook一样的使用方式。
use
开头。若函数的名字以use
开头并紧跟一个大写字母 且 调用其他Hook,则我们就说这是一个:自定义Hook。
useSomething
的命名约定可以让我们的linter插件在使用Hook的代码中找到bug。
可参考使用:react-use。
useContext
const 变量名 = useContext(React.createContext的返回值)
,返回该context
的当前值
1. 因此,使用`useContext`之后,就不需要使用`
useContext(MyContext)
相当于class组件中的static contextType = MyContext
或<MyContext.Consumer>
。`或`static contextType = MyContext`。 2. `useContext(MyContext)`只是让你能够读取`context`的值以及订阅`context`的变化。你仍然需要在上层组件树中使用` `来为下层组件提供`context`。 </details>
<MyContext.Provider>
的value
属性决定。调用了useContext
的组件总会在context
值变化时重新渲染
当组件上层最近的<MyContext.Provider>
更新时,该Hook会触发重渲染,并使用最新传递给<MyContext.Provider>
的value属性值。
即使祖先使用
React.memo
或shouldComponentUpdate
,也会在组件本身使用useContext时重新渲染。
e.g.
```tsx // 祖先 import React, { useState } from "react"; import { MyContext2 } from "../../context"; import TheContextHook from "./TheContextHook"; export default function TheHooks() { const [data, setData] = useState({ b: 10 }); return ( <> <MyContext2.Provider value=>{/* 子组件1、兄弟组件,正常工作 */} {/* 子组件2、兄弟组件,正常工作 */} </MyContext2.Provider> {/* 非子组件,无效,不变化 */} </> ); } // ./TheContextHook.tsx import React, { useContext } from "react"; import { MyContext2 } from "../../context"; export default function TheContextHook() { const { state, dispatch } = useContext(MyContext2); return ( <div onClick={() => { dispatch({ b: state.b + 1 }); }} > {JSON.stringify(state)} </div> ); } // ../../context.ts import React from "react"; export const MyContext2 = React.createContext({ state: { b: 1 }, dispatch(data: { b: number }) {}, }); ```
useRef
const 带current属性的变量 = useRef(current属性的初始值);
useRef
创建的是一个普通JS对象({ current: 值 }
),它和自建一个{ current: 值 }
对象的唯一区别是:useRef
会在每次渲染时返回同一个ref
对象。- 当
ref
对象内容发生变化时,useRef
并不会发起通知。变更.current
属性不会引发组件重新渲染。
DOM或子组件实例
e.g.
```jsx export default function UseRefDemo() { const inputEl = useRef(null); const onButtonClick = () => { // `current` 指向已挂载到 DOM 上的文本输入元素 inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); } ```
方便地保存任何可变值
refs
(可能导致意外的行为),通常应在事件处理器
或useEffect
中修改。e.g.
1. 事件处理器 ```jsx export default function UseRefDemo() { const xx = useRef(123); return ( <> <button onClick={() => { xx.current = xx.current + 1; }}>修改xx</button> <button onClick={() => { console.log(xx); }}>打印xx</button> </> ); } ``` 2. `useEffect` ```jsx function Timer() { const intervalRef = useRef(0); useEffect(() => { const id = setInterval(() => { // ... }); intervalRef.current = id; return () => { clearInterval(intervalRef.current); }; }); // ... } ```
1. `useState` 1. 建议写法: ```tsx const res0 = useRef
useState
、useRef
等,与Promise数据配合要注意在初始化时处理Promise实例可能导致的问题(有触发条件,暂不明确),建议初始化和赋值分开来(() => {}); const [pro0, setPro0] = useState<Promise |null>(null); useEffect(() => { setPro0( new Promise((resolve) => { res0.current = resolve; }), ); }, []); useEffect(() => { pro0 && pro0.then((data)=>{ // do sth. }) }, [pro0]) // 执行:res0.current() ``` 2. 可能出问题写法: ```tsx const res1 = useRef (() => {}); const [pro1] = useState( new Promise((resolve) => { res1.current = resolve; }) ); useEffect(() => { pro1.then((data)=>{ // do sth. }) }, []) // 执行:res1.current() ``` 2. `useRef` 1. 建议写法: ```tsx const resolveFunc1 = useRef (() => {}); const promiseAll = useRef<Promise |null>(null); useEffect(() => { // 不能把这个赋值,直接放在 useRef 初始化里面,可能导致问题 promiseAll.current = Promise.all([ new Promise((resolve1) => { resolveFunc1.current = resolve1; }), ]); promiseAll.current.then((data) => { // do sth. }); }, []); // 执行:resolveFunc1.current() ``` 2. 可能出问题写法: ```tsx const resolveFunc1 = useRef (() => {}); // 直接初始化,里面赋值的resolveFunc1可能无法达到预期 const promiseAll = useRef<Promise >( Promise.all([ new Promise((resolve1) => { resolveFunc1.current = resolve1; }), ]) ); useEffect(() => { promiseAll.current.then((data) => { // do sth. }); }, []); // 执行:resolveFunc1.current() ``` - 上面的2种写法可以简化为: ```tsx const resolveFunc1 = useRef (() => {}); useEffect(() => { Promise.all([ new Promise((resolve1) => { resolveFunc1.current = resolve1; }), ]).then((data) => { // do sth. }); }, []); // 执行:resolveFunc1.current() ``` </details>
useMemo
const memoizedValue = useMemo(
() => computeExpensiveValue(a, b),
[a, b] // 依赖项数组
);
返回一个memoized
值。
若没有提供依赖项数组(第二个参数为undefined
),则useMemo
在每次渲染时都会计算新的值。
若提供空数组[]
,则仅计算一次,之后渲染不会再改变值。
传入useMemo
的函数会在渲染期间执行。
不要在这个函数内部执行与渲染无关的操作。诸如副作用这类的操作属于useEffect
的适用范畴,而不是 。useMemo
若useMemo
第一个参数返回的computeExpensiveValue
返回的是Promise实例,则useMemo
返回的是Promise实例
。
函数组件可以直接返回
useMemo(() => (组件内容), [依赖项])
,这样仅依赖项变化才会重新整个组件。
useMemo(() => fn, deps)
相当于useCallback(fn, deps)
,均返回fn
。
useCallback
const memoizedCallback = useCallback(
(props) => { // 传入 memoizedCallback 的参数会到达 props
doSomething(a, b, props);
// 方法内调用 memoizedCallback() 是旧的自己(不随依赖项改变)
},
[a, b] // 依赖项数组
);
返回一个memoized
回调函数。
useMemo
。若useCallback
第一个参数方法返回Promise实例,则useCallback
返回的是返回Promise实例的方法
。
引用自己会引用旧的自己(没有随依赖项变化)
当想要
useEffect
限制执行次数,但依赖一些不变化的props或state时,很有用:const a = useCallback(方法,[依赖1]); useEffect(()=>{执行},[a])
。
useMemo
、useCallback
辩证看待:
useCallback
设计初衷并非useMemo
主要作用是 避免重复进行复杂耗时的计算,而不是 子组件不会重新渲染(re-render)需要满足的条件:
React.memo
或useMemo
等)e.g.
`<Child prop1={data1} prop2={data2}>`,此子组件不重新渲染需要:``被缓存,`data1`、`data2`被缓存。 ```jsx import React from "react"; const CountButton = React.memo(function ({ onClick, count }) { console.log("子级渲染", count); return <button onClick={onClick}>{count}</button>; }); export default function Demo() { const [count1, setCount1] = React.useState(0); const increment1 = React.useCallback(() => setCount1((c) => c + 1), []); const [count2, setCount2] = React.useState(0); const increment2 = React.useCallback(() => setCount2((c) => c + 1), []); console.log("父级渲染"); return ( <> <CountButton count={count1} onClick={increment1} /> <CountButton count={count2} onClick={increment2} /> </> ); } ``` </details> 10. `useImperativeHandle`
useImperativeHandle(ref, createHandle, [deps])
在使用ref
时自定义暴露给父组件的实例值,与forwardRef
一起使用。
e.g.
```tsx // 子 import { useImperativeHandle, forwardRef } from "react"; function _Son(props: any, ref: any) { function _func() { console.log("son3 func"); } useImperativeHandle(ref, () => ({ func: () => { _func(); }, })); returnson; } const Son = forwardRef(_Son); export default Son; // 父 <Son ref={a} /> a.current.func() ```
若指定了一个
依赖项数组
作为useEffect
、useLayoutEffect
、useMemo
、useCallback
、useImperativeHandle
的最后一个参数,它必须包含回调中的所有值,并参与React数据流。这就包括props
、state
,以及任何由它们衍生而来的东西。
useDebugValue
useDebugValue(value[, 格式化函数])
,在React开发者工具中显示自定义Hook
的标签
useDeferredValue
useTransition
useId
从class迁移到Hook
1. `constructor`: 函数组件不需要构造函数。你可以通过调用`useState`来初始化state。若计算的代价比较昂贵,则可以传一个函数给`useState`。 2. `getDerivedStateFromProps`: 改为 在渲染时 安排一次更新。 3. `shouldComponentUpdate`: `React.memo`。 4. `render`: 这是函数组件体本身。 5. `componentDidMount`、`componentDidUpdate`、`componentWillUnmount`: `useEffect`可以表达所有这些(包括 不那么 常见 的场景)的组合。 6. `getSnapshotBeforeUpdate`、`componentDidCatch`、`getDerivedStateFromError`: 目前还没有这些方法的Hook等价写法,但很快会被添加。
例子
强制刷新函数组件(也可改成强制防抖动)
const [show, setShow] = useState(true);
useEffect(() => {
!show && setTimeout(() => setShow(true))
}, [show])
// 重新渲染触发。或其他时机触发。注意可能会导致子组件的`useEffect`等有依赖项的钩子多次执行
const doRefreshShow = () => setShow(false)
React不会理会React自身之外的DOM操作。它根据内部虚拟DOM来决定是否需要更新,而且若同一个DOM被另一个库操作了,则React会觉得困惑而且没有办法恢复。
协同而避免冲突的最简单方式就是防止React组件更新。
<div>
。componentDidMount
和componentWillUnmount
对React不会更新的React元素进行挂载和清理。当一个组件的props
或state
变更,React会将最新返回的元素与之前渲染的元素进行对比(diff),以此决定是否有必要更新真实的DOM。当它们不相同时,React会最小化更新该DOM。这个过程被称为“协调”。
React16新增的协调引擎:Fiber(介绍文章:react-fiber-architecture),将整个更新划分为多个原子性的任务,这就保证了原本完整的组件的更新流程可以被中断与恢复,在浏览器的空闲期执行这些任务并且区别高优先级与低优先级的任务。
虚拟DOM系统的辩证思考:理解 Virtual DOM
Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。 1. Virtual DOM在牺牲部分性能的前提下,增加了可维护性,这也是很多框架的通性。 2. 实现了对DOM的集中化操作,在数据改变时先对虚拟DOM进行修改,再反映到真实的DOM中,用最小的代价来更新DOM,提高效率。 3. 打开了函数式UI编程的大门。 4. 可以渲染到DOM以外的端,如:ReactNative、SSR。
虚拟化长列表
如:react-window。
shouldComponentUpdate
跳过添加一个动态引入,就会新增一个chunk
、不会把动态引入的代码加入。策略:基于路由进行代码分割。bundle
import()
webpack提供的支持。require
和非动态import
不会进行代码分割。
e.g.
import("./math").then(math => { console.log(math.add(16, 26)); });
<React.Suspense>
渲染React.lazy
不支持服务端渲染。
e.g.
```jsx import React from 'react'; const OtherComponent = React.lazy(() => import('./OtherComponent')); function MyComponent() { return ( <React.Suspense fallback={懒加载前展示的组件}> 多个懒加载组件</React.Suspense> ); } ```
PropTypes
利用:prop-types。
类名.propTypes = { props名: 类型 }
TypeScript
当出现.js使用正常(说明功能正常),但是换成.tsx就类型报错的情况,除了检查自己是否写错之外,也可能是框架bug,只能用设置为
any
来绕过。
模板:
创建:npx create-react-app 「文件夹名」 --template 「模板名」
e.g.
npx create-react-app my-app --template typescript
必要文件
public/index.html
:页面模板
public/
内的文件。public/
内的文件。引用public/
路径:
public/
内使用%PUBLIC_URL%
src/
内使用process.env.PUBLIC_URL
src/index.js
:入口文件
src/
。src/
内的文件。scripts
模块的导出引入
import()
export default 表达式
。环境变量
若要改变环境变量,则需要重启应用。
内容:
NODE_ENV
REACT_APP_
开头的环境变量(不是以REACT_APP_
开头的均无法获得)使用:
src/
:process.env.「名字」
public/index.html
:%「名字」%
只有
public/index.html
可以使用环境变量,public/
其他文件都不编译解析、直接拷贝到根目录供引用。
设置:
.env
等文件REACT_APP_「变量」=「值」 npm start
(macOS)export 「变量」=「值」
小部分可以在代码中使用,大部分只是配置、无法在代码中输出。
支持:前端、服务端、客户端。
Web应用是一个状态机,视图与状态是一一对应的。让state的变化变得可预测。
核心概念
store
通过传入reducer
(作为root reducer)来创建。
若root reducer包含多个键-值(利用
combineReducers
组合),则把root reducer的任意一个键-值区域称为一个’切片’(slice),同时我们使用”切片 reducer”(slice reducer)这个术语,去形容负责更新该切片状态的reducer函数。
import { createStore } from 'redux'
import reducers对象 from './reducers' // reducers对象 === combineReducers({ reduce1: reduce函数1, reduce2: reduce函数2 })。root reducer
// store 是通过传入一个reducer(root reducer)来创建的
let store = createStore(reducers对象[, state初始状态]) // store === { getState, dispatch, subscribe }
// react中的使用
import { Provider } from 'react-redux'
<Provider store={store}>
子节点通过connect或useStore、useDispatch等来使用
</Provider>
提供store.getState()
获取当前最新state(全局)
若想要非组件的地方获得最新state(又不愿意通过thunk或传递dispatch的方式),则可以导出store或仅导出store.getState。
store.dispatch(某个action)
更新state
dispatch一个action可以形象的理解为“触发一个事件”。
触发后,store将执行所有reducer函数(root reducer)并计算出更新后的state,之后调用getState()可以获取当前最新state(全局)。
更新state的唯一方式。
返回值
store.dispatch(某个action)
返回:某个action
(若使用thunk,则)store.dispatch(thunk函数)
返回:thunk函数()
(可返回Promise实例)
thunk函数:
(dispatch, getState) => {/* 具体实现,可返回Promise实例 */}
通过store.subscribe(监听函数)
注册监听器,并返回注销监听器方法,store变化后会触发监听函数
在react中使用react-redux代替手动书写
store.subscribe
监听逻辑。
store.replaceReducer(nextReducer)
设置store会使用的下一个reducer(替换store当前用来计算state的reducer)。
state
来自服务端的state可以在无需编写更多代码的情况下被序列化并注入到前端(
JSON.stringify/parse
)。
普通对象。store的当前值,不能被其他代码修改。
Selector函数
可以从store状态树中提取指定的片段。随着应用变得越来越大,会遇到应用程序的不同部分需要读取相同的数据,selector可以避免重复这样的读取逻辑。
e.g.
```javascript const selectXx = state => state.Xx const currentXx = selectXx(store.getState()) ``` ```javascript import { connect } from 'react-redux' `connect`的第一个参数 ``` ```javascript import { useSelector } from 'react-redux' useSelector(Selector函数) ```
action
普通对象。{ type: 「字符串」[, 多个参数(payload): 值 ]}
,描述已发生事件,store数据的唯一来源。
可以将action视为描述应用程序中发生了什么的事件。
type
描述性的名字,会被定义成字符串常量
通常把type
写成域/事件名称
,其中第一部分是这个action所属的特征或类别,第二部分是发生的具体事情。
当应用规模越来越大时,建议使用单独的模块或文件来存放action的type。e.g.
import { 常量1, 常量2 } from '../actionTypes'
action的创建函数(action creator):
返回action的函数,作用是让你不必每次都手动编写action对象。
(若使用thunk,则)thunk函数的创建函数(thunk creator): 返回thunk函数的函数,作用是让你不必每次都手动编写thunk函数。
store.dispatch(某个action)
将执行所有reducer函数(root reducer)并计算出更新后的statereducer
函数。接受当前state和发送来的action,(仅基于state和action)返回新的state。
可以将reducer视为事件侦听器,该事件侦听器根据接收到的事件类型(action的type)来处理事件,当收到关注的action后,更新state。
不要进行:
执行有副作用的操作。e.g. API请求或路由跳转、异步逻辑
reducer是纯函数:当传入参数相同时(state、action),返回的state也相同,没有副作用。
Data.now()
或Math.random
默认情况 或 遇到未知的action时,返回旧的state
e.g.
```javascript switch (action.type) { case XXX: return Object.assign({}, state, action.data) default: // 默认情况 或 遇到未知的action时,返回旧的state return state } ```
拆分reducer再合成:combineReducers
(生成root reducer)
e.g.
```typescript import { combineReducers } from 'redux' import reduce函数1 from '../xxSlice' const rootReducer = combineReducers({ 属性名1: reduce函数1, // 只能一级对象,不能:属性名1: { 属性名2: reduce函数1, } 属性名2: (state, action) => { switch (action.type) { case xxx: return Object.assign({}, state, action.data) default: return state } } }) export default rootReducer; // state的类型 export type RootState = ReturnType; ``` </details>
一个reducer函数的一般流程:
检查reducer是否关心这个action
三大原则
单一数据源
整个应用的state被储存在一棵object tree中,并且这个object tree只存在于唯一一个store中:store -> object tree -> 众多state。
state是只读的,不可变的
更新state的唯一方式:store.dispatch(某个action)
。
reducer是纯函数
api
createStore(reducer[, 初始state][, 中间件方法])
combineReducers(reducers)
applyMiddleware(...middlewares)
e.g.
createStore(reducer, applyMiddleware(thunk, 其他中间件))
bindActionCreators(action creators或value是action creator的对象, dispatch)
唯一会使用到的场景是当需要把action creator往下传到一个组件上,却不想让这个组件觉察到Redux的存在,而且不希望把dispatch或Redux store传给它。
若第一个参数是对象,则返回把这个value为不同action creator的对象,转成拥有同名key的对象。若第一个参数是方法,则返回一个方法。同时使用dispatch对每个action creator进行包装,以便可以直接调用它们。
compose(...functions)
e.g.
createStore(reducer, compose(applyMiddleware(thunk), DevTools.instrument()))
与React配合使用:react-redux
为什么应该使用React-Redux?
Redux本身是一个独立的库,可以与任何UI层或框架一起使用,包括React、Angular、Vue、Ember和vanilla JS。虽然Redux和React通常一起使用,但它们是相互独立的。 Redux与任何类型的UI框架一起使用时,通常会使用“UI绑定”库将Redux与UI框架绑定在一起,而不是直接在UI代码里与store进行交互。 React-Redux是React的官方Redux UI绑定库。如果想要同时使用Redux和React,你也应该使用React-Redux来绑定这两个库。 虽然可以手动编写Redux store的订阅逻辑,但这样做会有很多重复工作。此外,优化UI性能包含复杂的逻辑。 订阅store、检查更新数据和触发重新渲染的过程可以变得更加通用和可复用。像React-Redux这样的UI绑定库会处理store的交互逻辑,因此你不必手动编写该代码。
访问store
Provider
<Provider>
的所有孙辈的容器组件都可以访问store
,而不必显式地传递它。
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import xx from './reducers'
import App from './components/App'
let store = createStore(xx)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
手动监听store.subscribe
函数组件内使用(hooks)
也可以用
connect
,但是不推荐。以下hooks更推荐。
import { useSelector, useDispatch, useStore } from 'react-redux'
useSelector
const state切片值 = useSelector((state: RootState) => { return state.slice名 }[, 浅比较函数])
浅比较函数
可以直接用import { shallowEqual } from 'react-redux'
,作用:浅比较前后state切片值
是否相同,如果相同则不触发更新。
useDispatch
const dispatch = useDispatch()
dispatch(createSlice实例.actions.方法名(参数));
useStore
// `{store.getState()}`不会随着state更新而触发视图更新
const store = useStore() // store === { dispatch, getState, replaceReducer, subscribe }
class组件内使用
import { connect } from 'react-redux'
export default connect(
(state, ownProps) => { props某属性: state相关内容, }, // mapStateToProps:redux的store 映射到 组件的props。默认方法返回:state: state
(dispatch, ownProps) => { props某方法: dispatch相关内容, } // mapDispatchToProps:redux的dispatch 映射到 组件的props。默认方法返回:dispatch: dispatch
)(class组件)
简化逻辑的最佳实践:redux-toolkit(包含:redux、redux-thunk、reselect、immer、等)
e.g.
```jsx // ./index.js import ReactDOM from 'react-dom'; import store from './store'; import { Provider } from 'react-redux'; import Counter from '../features/counter/Counter'; ReactDOM.render( <Provider store={store}></Provider>, document.getElementById('root') ); ``` ```typescript // ./store.ts import { configureStore, ThunkAction, Action, combineReducers } from '@reduxjs/toolkit'; import counterReducer from '../features/counter/counterSlice'; // 默认启用了 redux-thunk 中间件 const store = configureStore({ reducer: { // 或 reducer: combineReducers({ counter: counterReducer, 其他: 其他Reducer }) counter: counterReducer, 其他: 其他Reducer }, }); export default store; export type AppDispatch = typeof store.dispatch; export type RootState = ReturnType ; export type AppThunk<returnType = void> = ThunkAction< returnType, RootState, unknown, Action >; ``` ```jsx // ../features/counter/counterSlice.js import { createSlice } from '@reduxjs/toolkit'; const counterSlice = createSlice({ name: 'xx', // 被用于生成的action type的前缀(不是切片名) initialState: { // reducer的初始状态值 value: 0, }, reducers: { // 其中的键会成为action type,而函数是当action type被分发时调用的reducer(有时候它们也会被称为"case reducers",因为它们类似于switch语句中的case)。默认开启immer increment: state => { // state仅自己切片的,不是全局的 state.value += 1; }, decrement: state => { state.value -= 1; }, incrementByAmount: (state, action) => { state.value += action.payload; }, // yyy: { reducer (state, action) {}, prepare回调函数 } }, }); // action creator export const { increment, decrement, incrementByAmount } = counterSlice.actions; // createSlice会自动生成与我们编写的reducer函数同名的action creator,参数会成为action的payload,e.g. incrementByAmount(参数) -> {type: 'xx/incrementByAmount', payload: 参数} // thunk creator(action creator) export const incrementAsync = amount => // 用法:dispatch(incrementAsync(数字)) // thunk函数 dispatch => { setTimeout(() => { dispatch(incrementByAmount(amount)); // 也可以继续触发thunk函数 }, 1000); }; export const incrementAsync2 = (amount) => { return async (dispatch, getState) => { // getState()获取当前最新state(全局) try { await sleep(1000); dispatch(incrementByAmount(amount)); } catch (err) { } }; }; // selector export const selectCount = state => state.counter.value; // reducer export default counterSlice.reducer; ``` ```jsx // ../features/counter/Counter.js import { useSelector, useDispatch } from 'react-redux' import { increment, incrementByAmount, incrementAsync, incrementAsync2, selectCount } from './counterSlice' export function Counter() { const count = useSelector(selectCount) const dispatch = useDispatch() return ( <div onClick={() => { // `increment()`和`dispatch(increment())`都返回:`{type: 'xx/increment', payload: undefined}` dispatch(increment()) // `incrementByAmount(1)`和`dispatch(incrementByAmount(1))`都返回:`{type: 'xx/incrementByAmount', payload: 1}` dispatch(incrementByAmount(1)) // thunk creator返回thunk函数的函数,看起来是一个 返回特殊函数 的特殊函数:`(参数) => (dispatch, getState)=> {/* 使用 参数、dispatch、getState,可返回Promise实例 */}` // `dispatch(thunk函数)`和`thunk函数()`返回一致 dispatch(incrementAsync(2)) dispatch(incrementAsync2(3)) }} > {count} </div> ) } ``` </details>
configureStore
包装createStore
以提供简化的配置选项和良好的默认预设。自动组合切片的reducer,添加你提供的任何Redux中间件。
- 默认启用了
redux-thunk
中间件;默认启用redux-devtools
扩展。中间件默认开启序列化检查
>[原因](https://redux.js.org/style-guide#do-not-put-non-serializable-values-in-state-or-actions)。 ```javascript export default configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => { return getDefaultMiddleware({ // 关闭序列化检查 serializableCheck: false, }); }, }); ```
createReducer
生成reducer。
const counter1 = createReducer(0, { // 初始状态值、action type的查找表。创建一个reducer来处理所有这些action type
[INCREMENT]: state => state + 1,
[DECREMENT]: state => state - 1
})
// 等价于:
function counter2(state = 0, action) {
switch (action.type) {
case INCREMENT:
return state + 1
case DECREMENT:
return state - 1
default:
return state
}
}
自动启用
immer
。
createAction
生成action creator、action的type。
createAction(「type值」)(「payload值」)
等于 { type: 「type值」, payload: 「payload值」 }
。createAction(「type值」).toString()
等于 createAction(「type值」).type
等于 「type值」
。createAction(「type值」, prepare回调函数)
createSlice
自动启用
immer
。
createAsyncThunk
接受一个action type和一个返回Promise实例的函数,并生成一个发起基于该Promise实例的pending/fulfilled/rejected
的action类型的thunk函数。
createEntityAdapter
createSelector
记忆化selector。
来自:reselect。
中间件(Middleware)
把原本只能同步的store.dispatch(某个action)
变成异步或更多功能。
可实现
store.dispatch(参数)
时执行额外的逻辑(例如打印action的日志、状态)store.dispatch(thunk函数 或 thunk函数生成器(参数))
,其中thunk函数的参数是dispatch
、getState
,可返回Promise实例。
thunk函数生成器
也是action creator。
基于redux和redux-saga的精简封装数据流方案。然后为了简化开发体验,还额外内置了react-router和fetch,所以也可以理解为一个轻量级的应用框架。
没有样板文件的Redux最佳实践。没有多余的action types,action creators,switch语句或thunks。简单、易用。
computed
(?某些情况下:)用到才去计算。每一次用到都会跑一遍计算的方法(类似vuex的getters返回一个函数的情况)。
路由+antd+dva等阿里系封装框架,类似next.js。
样式
Stack Overflow: flex vs flexGrow vs flexShrink vs flexBasis in React Native?
Dimensions
Dimensions.get('window')
尽管尺寸信息立即就可用,但它可能会在将来被修改(譬如设备的方向改变),所以基于这些常量的渲染逻辑和样式应当每次 render 之后都调用此函数,而不是将对应的值保存下来。
React Native = JavaScriptCore + ReactJS + Bridges
JavaScriptCore负责JS代码解释执行
iOS自带,Android没有,所以RN打包后Android的包比iOS大。
ReactJS负责描述和管理VirtualDom,指挥原生组件进行绘制和更新,同时很多计算逻辑也在js里面进行
ReactJS自身是不直接绘制UI的,UI绘制是非常耗时的操作,原生组件最擅长这事情(,Flutter则自绘渲染)。
Bridges用来翻译ReactJS的绘制指令给原生组件进行绘制,同时把原生组件接收到的用户事件反馈给ReactJS
要在不同的平台实现不同的效果就可以通过定制Bridges来实现。