深入React顶层对象下的常用api

707 阅读6分钟

React顶层对象下的常用api

  • isValidElement()
  • createElement()
  • cloneElement()
  • React.Children下的方法
  • React.createRef()
  • React.forwardRef()
  • React.memo()

isValidElement()

React.isValidElement(object) 判断object是否为 React 元素,返回值为 true 或 false

举个例子

<div>
    <h1 id='aa' onClick={handle}>
        {name}
    </h1>
    <div>
        {
            React.Children.map(children, (child) => {
                const Ele = React.cloneElement(child, {
                    onClick: handle,
                    style: {...child.props.style, height: '100px', width: '100px'}
                })
                return Ele;
            })
        }
    </div>
</div>
     const handle = (e) => {
        console.log(React.isValidElement(document.getElementById('aa')));//false
        console.log(React.isValidElement(children)); // true;
    }

源码剖析

image.png

image.png

只有同时满足以下三个条件才会返回true:

1、必须是对象

2、不能为null

3、对象中要有$$typeof 属性,且值必须为 REACT_ELEMENT_TYPE这样的一个常量值。它是一个Symbol值或者16进制的数值。

createElement()

react@16.x版本之前我们必须在react项目中的每一个组件中主动引入 import React from 'react' 否则会出现以下错误。'React' must be in scope when using JSX 这是因为我们在使用jsx语法时无法直接将其转换为js代码,而是通过 @babel/preset-react将其转换成js代码,这里用到了React.createElement所以我们需要显示的引入React顶层对象。

react@17.x版本后,如果仅仅使用jsx语法,可以不用主动引入React顶层对象。

React.createElement(type, config,...children) 的参数

  • type:要创建的 React 元素类型,可以是标签名称字符串,也可以是React组件类型,也可以是Reactfragment类型
  • config:写在标签上的属性的集合,js 对象格式,若标签上未添加任何属性则为 null。
  • children: 为创建的React元素添加子节点。可以嵌套React.createElement创建的元素。

举个例子

//仅仅为了举例,代码并不严谨
React.useEffect(() => {
    const bot = React.createElement('h1', {
        style: { color: 'red' },
    },
        'hello Yu',
        React.createElement('div', {style:{color: 'blue'}}, 'hello one!'),,
        React.createElement('div', {}, 'hello two!')
    )
    // ReactDOM.render(bot, document.getElementById('bottom'))
    const container = document.getElementById('bottom');
    const box = ReactDOM.createRoot(container)
    box.render(bot);
}, []);

效果如下: image.png

cloneElement()

cloneElement(ReactElement, config, ...children) 参数与createElement()类似

  • ReactElement:要克隆的 React 元素类型,可以是标签名称字符串,也可以是React组件类型,也可以是Reactfragment类型
  • config:为被克隆元素新添加的属性的集合,js 对象格式,若不需要添加任何属性则为 null。
  • children: 为被克隆的React元素添加子节点。会覆盖原元素的子节点。
  React.useEffect(() => {
    const ret = React.cloneElement(<List/>, null, React.createElement('div', null, '12312'));
    const root = ReactDOM.createRoot(document.getElementById('bottom'));
    root.render(ret);
  }, []);

总结

对于React.cloneElement我的理解是,这个api在封装一些公用组件的时候是非常好用的。

我们常常书写组件的时候的格式大多数是这种

<Conatiner>
    <InnerBox/>
</Container>

对于Container和InnerBox两个组件我们都可以根据需求书写为公用组件。

如果我的Container在很多页面上都会添加类似的html dom结构和css样式结构,并需要对内部的组件进行进一步的封装例如为其中每个元素添加 click事件、pointer样式,根据不同需求设置不同的background背景色。(虽然可以通过事件代理或者是分别添加样式可以解决,但使用React.cloneElement足够优雅)

React.Children下的方法

通常props.children 对于我们来说是一个黑盒的存在,我们无法确认他的具体类型是什么

  • 如果当前组件没有任何子节点,它就是 undefined
  • 如果有一个子节点,数据类型是 object
  • 如果有多个子节点,数据类型就是 array

所以为了防止意外的错误情况,React.Children提供了以下几种方法来解决这个问题。

这几种方法对于传入上方的值的类型,都会进行对应的处理,规避了因props.children类型的不同而产生的意外情况。

  • React.Children.map(children, function(child) {}) 同数组下的map方法,返回一个新的数组。常用渲染props.children时候。

  • React.Children.forEach(children, function(child) {}) 同数组下的forEach方法,单纯遍历不返回结果。

  • React.Children.count(children) 返回子结点的个数。

  • React.Children.only(children) 验证 children 是否只有一个子节点(一个 React 元素),如果有则返回它,否则此方法会抛出错误。不接受React.Children.map的返回值,因为其返回值是数组不是React元素。

    React.Children.only expected to receive a single React element child.
    
  • React.Children.toArray(children) 将 children 这个复杂的数据结构以数组的方式扁平展开并返回,并为每个子节点分配一个 key。当你想要在渲染函数中操作子节点的集合时,它会非常实用,特别是当你想要在向下传递 this.props.children 之前对内容重新排序或获取子集时。(官方介绍)

    简单说就是,不管是上面所说的三种类型的哪一种,最终都返回一个数组。

React.createRef()

React.createRef 创建一个能够通过 ref 属性附加到 React 元素的 ref

ref: 在React典型数据流以外,你可能需要强制的去修子组件。被修改的子组件可能是React组件的实例, 也可能是一个DOM元素。

何时使用ref呢?

  • 在引入第三方DOM库时,我们常常需要在某个元素下去展示,一般DOM库会需要我们将该元素传递给某个方法,这时候我们往往通过ref来绑定指定的元素,并将其传给该方法。
  • 在一些无法通过声明式方式完成的效果时候,我们可以使用ref。例如跳转某个页面时候,我们需要将焦点放在输入框中,这是无法使用声明式来完成的效果,这时候我们就可以通过ref获取input框,在进入该页面后,通过ref调用该元素的focus方法来获取焦点。
  • 触发强制动画。

ref之间的不同

  • 当创建的ref应用于HTML元素时,构造函数中使用React.createRef创建的ref接受DOM元素作为气current属性。
  • 当创建的ref应用于class声明的组件时,ref对象接收组建的挂载实例作为其current属性。
  • 不能使用ref在function函数时组件上,因为函数时组件没有实例的概念。但我们可以通过React.forwardRef进行转发,转发给子组件的真实DOM元素或者类式声明的组件。

React.forwardRef()

在开发中我会遇到在父元素中使用创建ref获取该组件下某个子组件下的dom元素来完成一些效果。这时候我们就可以使用这个方法。

效果

image.png

代码

const Fun = React.forwardRef((props, ref) => {
  return (
    <div style={{ color: 'red' }}>
      <input ref={ref}></input>
    </div>
  )
})
export default Fun;


export default function List() {
    const newRef = React.createRef(null);
    
    React.useEffect(() => {
        newRef.current.value = '12341234123'
        newRef.current.focus();
        console.log(newRef);
    }, []);

    return (
        <div>
            <h1>hello world!</h1>
            <Fun ref={newRef}>
            </Fun>
        </div>
    )
}

React.memo()

React.memo() 是一种使用缓存来提升性能的一种手段。

如果你的某个组件在相同的props的情况下渲染相同的结果,那么你可以通过包装在React.memo 中调用。这意味着在这种情况下,React将跳过渲染组件的操作并直接复用最近一次的渲染结果。

其只会检查传入的props的变化,如果该组件被React.memo包裹,但其内部使用了useState、useReducer,useContext的hook,当state和context发生变化,组件仍会重新渲染。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

代码

当点击时只会更新父组件,而子组件并不会卸载。

export default function List() {
    const value = [1, 2, 3, 4, 5];
    const [state, setState] = React.useState(0);

    console.log('father');
    return (
        <div>
            <div onClick={() => setState(pre => pre + 1)}>{state}</div>
            <Fun value={value}></Fun>
        </div>
    )
}


export default React.memo(function Fun(props) {
  const { value } = props;
  console.log('son');
  return (
    <div style={{ color: 'red' }}>
      {value.map(item => <div key={item}>{item}</div>)}
    </div>
  )
}, (pre, cur) => {
  return JSON.stringify(pre.value.sort()) === JSON.stringify(cur.value.sort());
})