React 知识点
React 基础知识点综合
React 和 Vue 的区别
相同点:
- 数据驱动视图:从直接操作DOM到数据驱动视图
- 组件化:将页面分成细碎的组件,组件之间的组合嵌套就形成最后的页面
- 虚拟DOM
不同点:
响应式原理
Vue 提供的是反应式的数据,Vue 会遍历 data 数据对象,为每一个属性都设置 getter(访问属性时会调用该函数) 和 setter(修改属性时会调用该函数),这样就能清楚的知道哪些属性在变动。当属性值发生变动的时候,寻找所有使用到该属性的组件,重新渲染该组件。
而 React 则是需要给系统一个明确的信号(setState)来告诉系统重新渲染界面,系统在接受到指令后会使用 Diff 算法对虚拟 DOM 进行比较,计算出需要更新的部分,然后进行部分渲染。
写法差异:React all in js,Vue推荐使用 template 单文件组件格式
Diff算法不同
数据流
单向数据流:数据从父组件流向子组件,子组件的数据更新的请求必须发送到父组件,让父组件作出相应的更改,再将数据传递给子组件,即子组件不能自行修改父组件传递过来的数据。
双向数据绑定:就是 UI 行为改变 Modal 层的数据,Modal 层的数据能够即时反映到 UI 上,同时在 Modal 层修改的数据(即不通过 UI 行为触发)也能够反应到 UI 上。
JSX
JSX 语法实际上是 React.createElement(component,props,...children)
的语法糖,所有的 JSX 语法都会被转换成这个方法调用,举个例子:
const element = <div className='foo'>Hello, React</div>
// 等价于
const elemnt = React.createElement( 'div', { className:'foo' }, 'Hello,React' );
在编译时 Babel 读取代码并解析,生成 AST,再将 AST 传入插件层进行转换,在转换时就可以将 JSX 的结构转换为 React.createElement
的函数。
Render
组件是 React 的核心,将组件挂载到页面的 DOM 节点中需要使用 react-dom 库中的ReactDOM.render()
方法:
// index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
render 方法可以返回单个元素、数组和字符串(16.0+),这样就不需要在多个元素外再包裹一个 div 了;
数组:
class PostList extends Component {
render() {
return [
<li key="A">First item</li>,
<li key="B">Second item</li>,
<li key="C">Third item</li>
];
}
}
字符串:
class StringComponent extends Component {
render() {
return "Just a strings";
}
}
生命周期
react 的生命周期有:
constructor
用于初始化 state
render
负责返回组件
componentDidMount
在组件被挂载之后触发,且只会触发一次,推荐在这里调用 Ajax 请求获取服务端数据
需要注意的是:当所有子元素都被加载完成之后,才会触发父元素的 componentDidMount 事件
shouldComponentUpdate
组件更新之前触发,常用与减少组件不必要的更新,举个例子:
shouldComponentUpdate(nextProps, nextState){
if(nextState.Number === this.state.Number){
return false // 如果state中前后number的值不变则不更新组件
}
}
componentDidUpdate
组件更新之后触发
componentWillUnmount
组件被卸载之前触发
完整生命周期(16.4)图示:
componentDidCatch
错误边界的作用:在 react 16 之后,任何未被错误边界包裹的错误都会导致整个React树被卸载。
如果一个 class 组件设置了static getDerivedStateFromError()
或 componentDidCatch(error,errorInfo)
中的一个或者两个,那么这个组件就会成为一个错误边界组件,当子组件错误发生时,作为父级的错误边界组件就能够获取到错误并作出对应的操作。
getDerivedStateFromError 用于在发生错误的时候修改 state 以显示错误信息或降级内容
componentDidCatch 用于在发生错误的时候对错误作出相应的操作
举个例子:
// 创建一个 ErrorBoundary 组件
class ErrorBoundary extends Component{
constructor(props){
super(props);
this.state={
hasError:false
}
}
static getDerivedStateFromError(){
return { hasError:true }
}
componentDidCatch(error,errorInfo){
logErrorToMyService(error, errorInfo);
}
render() {
return this.state.hasError ? (
// 当出现错误的时候显示提示
<h1>Oops, something went wrong!</h1>
) : (
this.props.children
);
}
}
用错误边界将可能发生错误的组件包裹起来,这样就能捕获到子组件的错误信息了:
class App extends Component{
render(){
return (
<ErrorBoundary>
<Something></Something>
</ErrorBoundary>
);
}
}
不过错误边界能够捕获到的错误是有限的,只能捕获渲染期间、生命周期内的和组件构造函数内的错误,但是事件处理器(例如 onClick)内的、定时器内的、错误边界组件自身的错误无法被捕获。这些无法被捕获的错误可以通过try/catch
来捕获。
组件分类
类组件(Class Component):
传统的组件,继承于 React.Component
class App extends React.Component{
...
}
纯函数组件(PureComponent):
由 React 自行管理 shouldComponentUpdate 的 Component。
对于传统组件来说,一旦 state 或 props 发生了变化,组件默认会被重新渲染,当然你也可以通过 shouldComponentUpdate 来对 state 和 props 进行对比,手动控制组件的更新。
pureComponent 简化了这个过程,由 React 自行管理,对比没有设置 shouldComponentUpdate 的传统组件性能上有一定程度上的提升。
函数组件(Function component):
函数式组件,指只需要处理 props 不需要拥有 state 的组件,举个例子:
function App(){
return <div>...</div>
}
Function component 的终极形式:React Hook
有状态组件和无状态组件
组件可以根据 state 的有无分为有状态组件和无状态组件,无状态组件一般使用函数组件,常用于展示数据和通知父元素,举个例子:
function PostItem(props){
const { handleClick,postContent } = props;
return <div onClick={ handleClick }>{ postContent.title }</div>
}
受控组件与非受控组件
受控组件:将表单元素的值交给 react 管理,通过 state 和 onChange 事件来修改 value 值;
非受控组件:通过操作表单元素的 DOM 来修改 value 值。
非受控组件因为值不由 react 管理,当我们需要获取该表单元素值的时候,就需要用到 ref,React.createRef()
方法能够创建 Refs 并通过 ref 属性联系到 react 组件或 DOM 元素。
举个例子:
import React,{ Component, createRef } from 'react';
class Login extends Component{
constructor(props){
super(props)
this.state = {
account:''
}
this.account = createRef();
}
handleSubmit=()=>{
console.log(this.account.value)
}
render(){
// 将input元素赋给 this.account,之后便可在其他地方通过 this.account 调用该元素
return <input type='text' ref={this.account}/>
}
}
注意:
- 如果要给非受控组件赋予初始值,需要使用
defaultValue
或者defaultChecked
属性,不能设置value
属性; - 只能为类组件定义 ref 属性,函数组件不支持 ref 属性(函数组件内部可以使用 ref 属性);
类组件的 this
在组件中调用方法时,this 默认不指向当前类,此时函数内获取到的 this 为 undefined,举个例子:
class PostList extends Component {
handleClick() {
console.log(this); // undefined
}
render() {
return <div onClick={this.handleClick}>Click Me</div>;
}
}
绑定 this 主要有以下三种方法:
在回调中绑定:
<button onClick={this.deleteRow.bind(this)}>Delete Row</button>
在构造函数中绑定:
constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } render(){ return( <button onClick={this.handleClick}>Delete Row</button> ) }
回调使用箭头函数:
<button onClick={()=>{this.deleteRow()}}>Delete Row</button>
使用箭头函数进行属性初始化:
class PostList extends Component { handleClick=()=>{ console.log(this); } render() { return <div onClick={this.handleClick}>Click Me</div>; } }
组件间的通信
父子之间
父组件通过 props 向子组件传递数据,子组件通过调用父组件传递过来的 props 里面的回调函数向父组件传递数据。
兄弟之间
通过一父多子(多个父子间通信结合)的方式实现,将共同的父组件作为中转来实现通信。
多层级组件之间
使用 Context 来处理不同层级之间的数据传递问题,避免复杂的 props 传递,常用于用户信息、主题或者首选语言。
Context 有两个主要的 API:createContext 和 Provider,createContext 用来创建 Context 对象,Provider 用来传递数据给组件,举个例子:
import React,{createContext} from 'react';
class App extends Component{
const MyContext = createContext('light'); // 创建 Context 对象并设定默认值 light
render(){
return (
<MyContext.Provider value={'dark'}> /* Context 传递的值为 dark */
<childComponent></childComponent>
</MyContext.Provider>
)
}
}
Context 在 class 中可以使用.contextType
来设置目标 Context,挂载后允许使用this.context
的形式获取 Context 的值:
class Child extends Component{
componentDidMount(){
console.log(this.context)
}
}
Child.contextType = MyContext;
对于函数组件,可以使用.Consumer
方法订阅到 Context,举个例子:
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>
在 Hook 中。只需要使用 useContext 方法即可订阅 Context:
const myContext = useContext(MyContext);
Context 的替代方式
利用插槽直接将元素传递下去,这样就可以在父元素直接赋值:
// Contacts 组件是被传递的元素
function App() {
return <SplitPane left={<Contacts />} />;
}
function SplitPane(props) {
return <div>{props.left}</div>; // 接收并渲染传递过来的元素
}
向 props.children 传值
子组件可能通过插槽(slot)的方式引入,此时父组件内子组件用props.children
代替,父组件无法直接操作子组件,也无法直接传值,React.cloneElement()
很好的解决了这个问题,他可以克隆出子组件,并传递参数:
// parent component
// old
{ props.children }
// new
{props.children &&
React.cloneElement(props.children, {...props})}
上面的方法在只有一个子组件的时候是能够正常运行的,但是在拥有多个子组件时会发生错误,这时候就需要用到另外一个方法:React.Children.map()
,他能够对子组件数组进行循环:
{props.children &&
React.Children.map(props.children, child =>
React.cloneElement(child, {...props})
)}
上面的代码实现了多个 React 组件的正常传值与渲染,但如果子组件不是 React 组件,例如字符串,如果依然使用React.cloneElement()
就会发生错误,需要使用React.isValidElement()
方法判断子组件是否为 React 组件,如果不是,则直接返回即可:
{props.children &&
React.Children.map(props.children, child =>
React.isValidElement(child)
? React.cloneElement(child, {...props})
: props.children
)}
父组件获取子组件Ref
当我们需要获取某个子组件内部的 DOM 元素的时候,可以这样将 childRef 属性通过 props 传递给子组件,子组件再将 childRef 赋给 ref,这样父组件就能取到子组件的 DOM 节点了:
import React,{ Component, createRef } from 'react';
class Parent extends Component{
constructor(props){
super(props)
this.childRef = createRef();
}
render(){
return <Child exportRef={this.childRef}></Child>
}
}
class Child extends Component{
constructor(props){
super(props);
}
render(){
return <input ref={this.props.exportRef} />
}
}
这里使用了 childRef 作为变量名是因为 ref 不存在与 props 内,不能通过 props 进行传递,但是换一个变量名就可以了,如果要使用变量 ref 进行传递,就需要使用到 React.forwardRef(props,ref)
来实现,ref 作为第二个参数传入,那么上面的 Child 组件就应该如下改造:
const Child = React.forwardRef((props,ref)=>{
render(){
return <input ref={ref} />
}
})
Ref
Ref 常用于获取获取 DOM 元素或 React 组件实例,当 ref 附加在 DOM 元素上时,可以通过ref.current
获取到对该节点的引用,当 ref 附加在 Class 组件上时(函数组件不可以,因为其没有实例),ref.current
获取到的是组件的挂载实例。
React 在组件挂载时给current
属性传入 DOM 元素(或实例),并在组件卸载时传入 null 值,ref 会在 componentDidMount 或 componentDidUpdate 生命周期钩子触发前更新。
竞态问题
异步获取数据最常见的方法仍是使用 useEffect,但是useEffect会报以下错误:Effect callbacks are synchronous to prevent race conditions
,问题的原因是请求结果返回的顺序不能保证一致。
举个例子,现在有一个组件当接收关键词不同时,会重新渲染组件。那么先请求关键词为 react 的文章,然后请求关键词为 vue 的文章,但关键词为 vue 的请求更先返回。请求更早但返回更晚的情况会错误地覆盖状态值 data(此时关键词是vue,但显示的是后返回react的结果),这就叫做竞态(race conditions),查看例子
解决方法:增加一个 didCancel 的变量,来判断当前操作是否已经被取消,查看例子
React-Router
<a> 和 Link 组件有何区别?
使用 a 标签进行渲染相当于新开了一个页面,页面需要重新被渲染且有可能会有白屏闪烁,而 Link 根据 React-router 的 Diff 算法,能够实现对页面的部分刷新,避免了不必要的渲染,有更高的效率。
React 的路由是如何实现的
路由需要解决两个核心问题:
- 如何改变URL且不刷新页面
- 如何监听URL的变化
Hash 常用于锚点定位,所以本身就不会引起页面的刷新,History 是一个对象,保存了当前窗口访问过的所有网址,并提供一系列的方法来进行无刷新的前进和后退。
不论是 Hash 路由还是 History 路由,都是通过监听浏览器事件来监听URL变化的,Hash 路由监听的是hashchange
事件,History 路由监听的是popstate
事件。
React 在侦听到路由变化后,页面不进行刷新而是进行页面的部分替换来实现路由效果
React18 新特性
React 18 默认执行批处理,原本在 promise、定时器和原生事件处理函数内的 setState 是同步的,无法被批处理,但是在 React 18 中都会被批量合并成一次处理。如果需要退出批量更新,可以使用
flushSync
API<div onClick={() => { flushSync(() => { setCount1(count => count + 1); }); }} />
增加了新的 API:useId、useDeferredValue、useTransition
并发模式,当你使用 startTransition 或者 useDeferredValue 时就会开启并发模式,并发模式通过 fiber 实现,能将线权的控制权交还给浏览器。