目录:
====== React ======
Component / PureComponent
forwardRef
lazy / Suspense
Fragment
cloneElement
Children
isValidElement
====== ReactDOM ======
render / hydrate
createPortal
unmountComponentAtNode
flushSync(不建议用)
findDOMNode(不建议用)
====== Hooks ======
useMemo
useCallback
useLayoutEffect(不建议用)
====== 其他 ======
插入html字符串
此为常用并容易被忽略的api,一些特别基础的就不说了~
React
Component / PureComponent
这两个都用于创建 React class 组件所继承的基类,Component
官网文档介绍的很详细就不说了,PureComponent
(纯组件)的区别是更新时只对 prop
和 states
进行浅比较来决定是否重新渲染
比如 PureComponent
中只修改对象的属性 视图则不会更新渲染:
class TestIndex extends React.PureComponent {
constructor(props) {
super(props)
this.state = {
userInfo: { name: '张二', age: 18 }
}
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
const { userInfo } = this.state
userInfo.name = '张大三'
this.setState({ userInfo })
}
render() {
return (
<>
<p>{this.state.userInfo.name}</p>
<button onClick={this.handleClick}>点我</button>
</>
);
}
}
点击按钮并不会出现变化
但改成 userInfo
赋值为浅拷贝对象的方式就可以了
this.setState({ userInfo: { ...userInfo } })
官网相关文档:
React.Component
React.PureComponent
forwardRef
增加一些对 官网forwardRef 用法的补充理解,下面举具体栗子:
1.获取子孙组件内部 dom 的 ref
react 不能通过 props
透传 ref
,引用 必须借助 forwardRef
import React, { useRef, useEffect } from 'react'
const Son = (props) => {
const { grandRef } = props
return <div ref={grandRef}>大孙子</div>
}
const Father = (props) => {
const { grandRef } = props
return <Son grandRef={grandRef} />
}
// 使用forwardRef进行包裹
const WrappedFather = React.forwardRef((props, ref) => (
<Father grandRef={ref} {...props} />
))
const GrandFather = () => {
const domRef = useRef(null)
useEffect(() => {
console.log(domRef.current)
}, [])
return (
<WrappedFather ref={(node) => { domRef.current = node }} />
)
}
控制台输出:
2.高阶组件中转发 refs
如果在 HOC
上添加 ref
属性,该 ref
引用的是最外层的容器组件 而不是被包裹的那个组件,需要用 forwardRef
解决这个问题。下面的栗子通过 forwardRef
使被 HOC
包裹的类组件拿到它的实例:
import React, { useRef, useEffect } from 'react'
function testHoc(Component) {
class TestHoc extends React.Component {
componentDidMount() {}
render() {
const { forwardedRef, ...rest } = this.props
// 将自定义的 prop 属性 “forwardedRef” 定义为 ref
return <Component ref={forwardedRef} {...rest} />
}
}
return React.forwardRef((props, ref) => (
<TestHoc {...props} forwardedRef={ref} />
))
}
class SpecBtn extends React.Component{
componentDidMount(){
console.log('SpecBtn componentDidMount!')
}
render(){
return <button>按钮</button>
}
}
const HocSpecBtn = testHoc(SpecBtn)
const testPage = () => {
const node = useRef(null)
useEffect(() => {
// 打印被包裹组件的生命周期函数componentDidMount
console.log(node.current.componentDidMount)
}, [])
return <div><HocSpecBtn ref={node} /></div>
}
控制台输出:
lazy / Suspense
lazy
可以懒加载组件,在回调函数中调用 import()
动态导入组件,Suspense
可以在组件未被渲染时 渲染一个loading组件,可以用作骨架屏。不支持服务端渲染
lazy
和 Suspense
一般结合起来用,比如将路由代码进行分割懒并加载,或动态加载组件
import React, { Suspense, lazy } from 'react'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import Loader from './components/Loading'
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
const App = () => (
<Router>
<Suspense fallback={<Loader />}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Switch>
</Suspense>
</Router>
)
export default App
也可以使用 @loadable/component,它也支持服务端渲染!
import loadable from '@loadable/component'
const Home = loadable(() => import('./pages/Home'))
官方文档:suspense
Fragment
React 不允许组件直接返回多个元素 需要一个根节点包裹,Fragment
能在不额外创建 DOM 元素的情况下,让 render()
方法中返回多个元素,可以减少不必要的元素嵌套
import { Fragment } from 'react'
const TestComp = () => (
<Fragment>
<div>元素1</div>
<div>元素2</div>
</Fragment>
)
也可以直接使用空标签(推荐使用)
const TestComp = () => (
<>
<div>元素1</div>
<div>元素2</div>
</>
)
cloneElement
以传入的元素为样板克隆并返回新的 React 元素,可以额外传入一些 prosp
,克隆出的新元素会将原始的 props 和传入的 props 进行浅层合并,这个api不常用 但特定情况下也许很有用
import React, { useEffect } from 'react'
const ParentComp = ({ children }) => {
return <div className="parent">{React.cloneElement(children, { remarks: '我挺特别' })}</div>
}
const ChildComp = (props) => {
const { userName, remarks } = props
useEffect(() => { console.log(props) }, [])
return (
<div className="child">
用户名:{userName}<br/>
{remarks ? `备注:${remarks}` : ''}
</div>
)
}
const TestIndex = () => (
<ParentComp>
<ChildComp userName="橘子" />
</ParentComp>
)
export default TestIndex
Children
React.Children
上有5个方法用来遍历 props.children
不透明的数据结构
官方文档:React.Children
下面是 透明的数据结构,子元素是数组可以直接遍历
const Item = (props) => <div className="item">{props.text}</div>
const List = (props) => {
useEffect(() => {
console.log('List children:', props.children)
}, [])
return <div className="list">{props.children}</div>
}
const TestPage = () => {
return (
<div>
<List>
<Item text="项1" />
<Item text="项2" />
<Item text="项3" />
<Item text="项4" />
<div>最后一项</div>
</List>
</div>
)
}
打印结果:
将 TestPage
组件中的 Item
修改成 不透明的数据结构,使用动态数组的方式
const dataFromReq = ['项1', '项2', '项3', '项4'] // 假装是请求来的动态数据
const TestPage = () => {
return (
<div>
<List>
{dataFromReq.map((text, i) => <Item text={text} key={i} />)}
<div>最后一项</div>
</List>
</div>
)
}
打印结果:
children 变成嵌套数组不能正常遍历了,此时就需要 React.Children
的方法来遍历
React.Children.map
修改下 List
组件,使用 React.Children.map
遍历 props.children
数组中每一个子元素
const List = (props) => {
useEffect(() => {
const childrenArr = React.Children.map(props.children,(item) => item)
console.log('children:', childrenArr)
console.log('length:', childrenArr.length)
})
return <div className="list">{props.children}</div>
}
React.Children.forEach
和 React.Children.map
类似,但不会返回数组
React.Children.count
返回 props.children
中所有子元素的总数量
React.Children.only
验证 children
是否只有一个子节点(一个 React 元素),如果有则返回它,否则此方法会抛出错误
React.Children.toArray
将 props.children
中复杂的数据结构以扁平化展开的数组的返回
const List: React.FC = (props) => {
useEffect(() => {
const newChildren = React.Children.toArray(props.children)
console.log('children:', newChildren)
console.log('length:', newChildren?.length)
})
return <div className="list">{props.children}</div>
}
const TestPage = () => {
return (
<div>
<List>
{[
<Item text="项1" key={1} />,
<Item text="项2" key={2} />,
[
<Item text="项3" key={3} />,
[
<Item text="项4" key={4} />,
<Item text="项5" key={5} />
]
]
]}
<div>最后一项</div>
</List>
</div>
)
}
打印结果:
React.Children.only
验证 props.children
是否只有一个子元素,成功就返回它,否则就抛出错误
const List = (props) => {
useEffect(() => {
console.log(React.Children.only(props.children))
})
return <div className="list">{props.children}</div>
}
const TestPage = () => {
return (
<div>
<List>
<Item text="第一项" />
<div>最后一项</div>
</List>
</div>
)
}
上面children有2个子元素,结果就抛出了错误
isValidElement
验证是对象否为react元素,返回 true
或 false
不常用,如果项目使用 tsx
可以直接声明类型来进行静态检查
const obj = () => <div>我是React元素</div>
console.log(React.isValidElement(obj)) // true
function createEl(el: React.ReactElement): React.ReactNode {
return <div>{el}</div>
}
ReactDOM
render / hydrate
render
不必说,hydrate
用做服务端渲染,用于在 ReactDOMServer
渲染的容器中对 HTML 的内容进行 hydrate
操作
ReactDOM.hydrate(element, container[, callback])
createPortal
这个方法可以将子节点渲染到父节点以外的地方,比如用来将模态框渲染到 document.boy
下,ReactDOM.createPortal
的第一个参数为子节点 children
或 子组件,第二个参数为dom元素引用
import { createPortal } from 'react-dom'
const Modal = () => {
return createPortal(
<div className="modal">我是弹框</div>,
document.body
)
}
官方文档:Portals
unmountComponentAtNode
从 DOM 中卸载组件,会将其事件处理器 和 state 一起清除,如果指定容器上没有对应已挂载的组件,这个函数就什么都不做。如果组件被移除就会返回 true
,没有就返回 false
(demo有待验证)
flushSync(不建议用)
将回调函数中的 setState
进行优先渲染,下面 age
会优先更新,不建议使用
const [ num, setNum ] = useState(0)
const [ age, setAge ] = useState(0)
const handleClick = () => {
setNum(1)
ReactDOM.flushSync(() => { setAge(18) })
}
findDOMNode(不建议用)
findDOMNode
是一个访问底层 DOM 节点的应急方案,不推荐使用该方法 React 推荐使用 ref
模式,它会破坏组件的抽象结构
Hooks
useMemo
官网useMemo 只适合用在需要复杂逻辑和多次响应的计算属性,但不要在其中做和渲染无关的操作
const expensive = useMemo(() => {
let sum = 0
for (let i = 0; i < count * 50; i++) {
sum += i
}
return sum
}, [count]) // count变化时才计算属性
useCallback
useMemo
会缓存计算数据的值,而 useCallback
缓存函数的引用 它通常作为性能优化方案和 React.memo
一起使用
比如下面例子 不想因为父组件更新而导致子组件做不必须要的渲染
在简单不传属性的情况下,可以用 memo
包裹解决,这样无论点击多少次按钮,Child
都不会被重复渲染
import React, { useEffect } from 'react'
const Child = memo(() => {
useEffect(() => {
console.log(`Child被渲染了 ${Date.now()}`)
})
return <p>我是子组件</p>
})
const Parent = () => {
const [ num, setNum ] = useState(0)
const handleBtnClick = () => { setNum(num + 1) }
return (
<div>
<button onClick={handleBtnClick}>{num}</button>
<Child />
</div>
)
}
但将父组件中的函数传给子组件时,还是会触发子组件渲染,因为父组件重新渲染时会创建新的 changeStatus
函数,可以使用 useCallback
包裹 changeStatus
将函数引用缓存起来就可以解决这个问题:
const Child = memo(({ status, onChangeStatus }) => {
// ...
return (
<div style={{ backgroundColor: '#e0e0e0' }}>
<button onClick={() => { onChangeStatus('success') }}>点击改变状态</button>
<p>我是子组件,状态:{status}</p>
</div>
)
})
const Parent = () => {
const [ num, setNum ] = useState(0)
const [ status, setStatus ] = useState('wait')
const handleBtnClick = () => { setNum(num + 1) }
const changeStatus = useCallback((newVal) => { setStatus(newVal) }, [])
return (
<div style={{ padding: 15, backgroundColor: '#f4f4f4' }}>
<button onClick={handleClick}>{num}</button>
<Child status={status} onChangeStatus={changeStatus} />
</div>
)
}
父组件的 <button>
被点击了5次,组件并没有被重新渲染
useLayoutEffect(不建议用)
在所有 DOM
更新后会被调用,等效于 componentDidMount
和 componentDidUpdate
,官方建议尽量使用 useEffect
替代,它只是备用方案
其他
项目中会遇到将富文本上传的 html 字符串插入视图节点,普通 js 中可以使用 el.innerHtml=''
由于 React 使用 JSX 语法,有一个 dangerouslySetInnerHTMl
属性可以插入 html 字符串
const htmlStr = `<p>developing...</p>`
<div dangerouslySetInnerHTML={{ __html: htmlStr }} />
====== 将持续更新~ ======