React进阶 一些API使用说明

585 阅读4分钟

目录:
====== 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(纯组件)的区别是更新时只对 propstates 进行浅比较来决定是否重新渲染

比如 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>
      </>
    );
  }
}

点击按钮并不会出现变化

微信截图_20211029160606.png

但改成 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 }} />
  )
}

控制台输出:

微信截图_20211026170952.png

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>
}

控制台输出:

微信截图_20211027111726.png

lazy / Suspense

lazy 可以懒加载组件,在回调函数中调用 import() 动态导入组件,Suspense 可以在组件未被渲染时 渲染一个loading组件,可以用作骨架屏。不支持服务端渲染

lazySuspense 一般结合起来用,比如将路由代码进行分割懒并加载,或动态加载组件

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

微信截图_20211110151516.png

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>
  )
}

打印结果:

微信截图_20211130103810.png

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>
  )
}

打印结果:

微信截图_20211130104428.png

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>
}

微信截图_20211130110337.png

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>
  )
}

打印结果:

微信截图_20211130135625.png

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个子元素,结果就抛出了错误

微信截图_20211130140206.png

isValidElement

验证是对象否为react元素,返回 truefalse 不常用,如果项目使用 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
  )
}

微信截图_20211108140810.png

官方文档: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次,组件并没有被重新渲染

微信截图_20211118142529.png

useLayoutEffect(不建议用)

在所有 DOM 更新后会被调用,等效于 componentDidMount 和 componentDidUpdate,官方建议尽量使用 useEffect 替代,它只是备用方案

其他

项目中会遇到将富文本上传的 html 字符串插入视图节点,普通 js 中可以使用 el.innerHtml='' 由于 React 使用 JSX 语法,有一个 dangerouslySetInnerHTMl 属性可以插入 html 字符串

const htmlStr = `<p>developing...</p>`

<div dangerouslySetInnerHTML={{ __html: htmlStr }} />

====== 将持续更新~ ======