setState两种参数的区别
下面通过一个小案例,来介绍一下他两的区别。点击按钮,多次调用setState方法,然后,看其num增加几。
- 传入一个对象
import React from 'react' export default class BtnTest extends React.Component { constructor(props) { super(props) this.state = { num: 0 } } handleClick = () => { this.setState({ num: this.state.num + 1 }) this.setState({ num: this.state.num + 1 }) this.setState({ num: this.state.num + 1 }) } render() { return ( <div> <h1>{this.state.num}</h1> <button onClick={this.handleClick}>按钮</button> </div> ) } }
由上面可以看出,每次点击只会增加1。
- 传入一个updater函数。
import React from 'react' export default class BtnTest extends React.Component { constructor(props) { super(props) this.state = { num: 0 } } handleClick = () => { console.log('点击按钮') this.setState((state, props) => { return { num: state.num + 1 } }) this.setState((state, props) => { return { num: state.num + 1 } }) this.setState((state, props) => { return { num: state.num + 1 } }) } render() { return ( <div> <h1>{this.state.num}</h1> <button onClick={this.handleClick}>按钮</button> </div> ) } }
从上面的结果可以看出,点击一次按钮,你会增加3。
所以传入对象作为参数,他的更新不会依据上一次的结果。但是传入一个函数作为参数,他会依据上一次结果计算。
React更新流程
React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树。然后会对比旧的DOM树。
对比不同类型的元素
当节点为不同的元素,React会拆卸原有的树,并且建立起新的树。
- 当一个元素从
<a>
变成<img>
,从<Article>
变成<Comment>
都会触发一个完整的重建流程。
- 当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执componentWillUnmount() 方法。
- 建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行 componentWillMount() 方法,紧接着 componentDidMount() 方法。
对比同一类型的元素
- 当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对更新有改变的属性。
对子节点进行递归
- 在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个mutation。
- 当然如果自在最后插入一个元素,不会影响性能。但是如果在中间或者开始,或者修改元素,那么将严重影响性能。 对于上面的性能问题,我们就需要通过设置key值来避免。
render函数何时被调用
默认情况下只要修改了props,state数据,他都会被调用。如何才能有条件的使render被调用呢?
- 可以通过
shouldComponentUpdate
生命周期函数返回false。并且也可以根据最新的newProps,newState的值来做出对应的需求。
shouldComponentUpdate(newProps, newState) { // 这里都是new值。 console.log(newProps, newState) return false }
- 可以让class继承PureComponent,而不是Component。他可以避免我们每次手动在class中处理大量的props,state比对。PureComponent内部是通过浅比较比较新旧props, state来决定是否重新执行render函数。
- 如果想要优化函数组件,我们可以将函数组件传入memo函数。而且memo还可以传入第二个参数,表示根据什么来保证是否再次渲染该组件,如果没有传递,他依旧调用PureComponent中调用的浅层比较函数。
浅层比较函数
- 先比较新旧props或者state是否是同一个对象。
- 比较新旧props或者state其中之一是否为null。
- 比较新旧props或者state第一层属性个数是否相同。
- 比较新旧props或者state第一层属性是否相同。
所以说,我们一定不要直接修改state中的数据,如果类组件是继承PureComponent或者函数组件被memo包裹,那么如果传入的state中的数据是引用类型,将会出现不会更新界面的bug。
class ListOfWords extends React.PureComponent { render() { console.log('子组件') return <div>{this.props.words.join(',')}</div> } } // 如果继承Component,只要props,state改变,render就会重新渲染。 export default class PureComTest extends React.PureComponent { constructor(props) { super(props) this.state = { words: ['marklar'] } this.handleClick = this.handleClick.bind(this) } handleClick() { // 这部分代码很糟,而且还有 bug // 这里的一个bug是使用了同一个数组。 const words = this.state.words words.push('marklar') this.setState({ words: words }) } render() { console.log('父组件') return ( <div> <button onClick={this.handleClick}>按钮</button> <ListOfWords words={this.state.words} /> </div> ) } }
上面这个例子是不会渲染界面的。
事件总线
第三方库events。 安装
npm install events
使用
import React from 'react' import EventEmitter from 'events' const eventEmitter = new EventEmitter() class Com extends React.Component { getName(...args) { console.log(args) } componentDidMount() { // eventEmitter.addListener('name', this.getName) eventEmitter.on('name', this.getName) } componentWillUnmount() {} eventEmitter.removeListener('name') } render() { return <div>子元素</div> } } export default class EventBusTest extends React.Component { handleEmit() { eventEmitter.emit('name', 'zh', 'llm') } render() { return ( <div> <Com /> <button onClick={this.handleEmit}>传递事件</button> </div> ) } }
主要的作用是实现非父子组件的通信。
ref
通过createRef来创建一个ref实例
class MyComponent extends React.Component { constructor(props) { super(props); //这里创建一个ref实例 this.domRef = React.createRef(); } componentDidMount() { console.log('======', this.refs.stringRef) // console.log('======', this.domRef.current) // <div>createRef获取dom</div> } render() { //使ref实例绑定该元素 return <div ref={this.domRef}>createRef获取dom</div> } }
直接获取到该组件this.refs["ref的属性值"]
componentDidMount() { console.log('======', this.refs.stringRef) //<div>字符串获取dom</div> } render() { return ( <div ref="stringRef">字符串获取dom</div> ) }
直接给ref传递一个函数
constructor(props) { this.fnDom = null } componentDidMount() { console.log('======', this.fnDom) // <div>通过函数获取dom</div> } render() { return ( <div> <div ref={(dom) => { this.fnDom = dom }} > 通过函数获取dom </div> </div> ) }
注意我们可以在类组件中定义ref属性(上述三种方法都可以),不能在函数式组件中定义ref属性。这样会报错误。
// 当Com是类组件时,将不会出现错误。但是当Com是函数式组件时,将会出现错误。 <Com ref={this.componentDom} /> <Com ref="componentDom" /> <Com ref={(el) => { this.c = el }} />
上面我们可以看出,想要在函数式组件中使用ref属性,我们可以使用React.forwardRef()
。
ref转发
一般我们在自定义组件中定义ref属性,那么我们也可以指定自定义组件中html元素。
- 可以将ref对象当做一个props属性传递到子组件(需要自定义ref传递的props名),然后将该props属性子组件中赋值到ref属性。注意类组件和函数组件都可以。
import React from 'react' class Son extends React.Component { render() { return <div ref={this.props.refDom}>子组件</div> } } // function Son(props) { // return <div ref={props.refDom}>子组件</div> // } export default class Parent extends React.Component { constructor(props) { super(props) this.refDom = React.createRef() } componentDidMount() { console.log('==========', this.refDom.current) // <div>子组件</div> } render() { return ( <div> <Son refDom={this.refDom}></Son> </div> ) } }
- 通过内置的forward函数来转发ref。这里我们可以直接设置ref作为自定义组件的props,不需要自定义ref属性名。
const ConvertComponent = React.forwardRef((props, ref) => ( <div ref={ref}>子组件</div> )) export default class Parent extends React.Component { constructor(props) { super(props) this.refDom = React.createRef() } componentDidMount() { console.log('==========', this.refDom.current) // <div>子组件</div> } render() { return ( <div> <ConvertComponent ref={this.refDom} /> </div> ) } }
受控组件
组件的状态通过React 的状态值 state 或者 props 控制。
HTML 中,表单元素(如、 和 )之类的表单元素通常自己维护 state,并根据用户输入进行更新。
而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。
- 将两者结合起来,使React的state成为“唯一数据源”。
- 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。
- 被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
select标签
单选
- value属性可以设置option的默认值。绑定state中对应的值。
- option的value属性是设置触发事件的targe.value。并更新到对应的state中。 多选
- value属性可以设置option的默认值。绑定state中对应的值。为一个数组。
- option的value属性是设置触发事件的targe.value。并更新到对应的state中。
input:text, textarea
- value属性将被作为target.value值。并更新到对应的state中。
input:checkbox, input:radio
- checked属性可以设置input的默认值,绑定state中对应的值。
- checked属性是设置触发事件的target.checked,并更新到对应的state中。注意当使用多个input标签时,我们可以给input标签设置name属性,让我们可以通过e.target.name来复用事件处理。
handleInputChange(event) { const target = event.target; const value = target.type === 'checkbox' ? target.checked : target.value; const name = target.name; this.setState({ [name]: value }); }
非受控组件
组件不被 React的状态值控制。通过 dom 的特性或者React 的ref 来控制。
export default class AppTest extends React.Component { constructor(props) { super(props) this.inputDom = React.createRef() } componentDidMount() { this.inputDom.current.addEventListener('change', (e) => { console.log('========', e.target.value) }) } render() { return ( <div> <form action="#"> <input type="text" ref={this.inputDom} /> </form> </div> ) } }
高阶组件
高阶组件是参数为组件,返回值为新组件的函数。
- 我们可以通过displayName来给组件命名,以便React developer tools区分各个组件。
- 他可以返回函数式组件或者类组件。
- 它主要的目的是将传入的组件统一处理,然后返回。简化单一的枯燥的操作。
- 了解过express, koa等框架,可以给他理解为一个中间件。
增强props
当我们一个组件使用多次,或者多个组件都需要增加相同的props,那么我们就可以设置一个高阶组件统一做处理。
下面一个例子就是给App, App2增加age这个props。
import React from 'react' function enhanceComponent(WrapeComponent) { class newComponent extends React.Component { render() { return <WrapeComponent {...this.props} age="30" /> } } // newComponent.displayName = 'pp' return newComponent } class App extends React.Component { render() { return ( <div> App1: {this.props.name} - {this.props.age} </div> ) } } function App2(props) { return ( <div> App2: {props.name} - {props.age} </div> ) } const EnhanceApp = enhanceComponent(App) const EnhanceApp2 = enhanceComponent(App2) class ParentCom extends React.Component { render() { return ( <div> <EnhanceApp name="zh" /> <EnhanceApp2 name="gl" /> </div> ) } }
通过context增强props
通过context包裹高阶组件,共享context数据,来增强props。
import React from 'react' const UserContext = React.createContext({ name: 'zh', age: 20 }) function enhanceComponent(WrapeComponent) { class newComponent extends React.Component { render() { return ( <UserContext.Consumer> {(user) => { return <WrapeComponent {...this.props} {...user} /> }} </UserContext.Consumer> ) } } // newComponent.displayName = 'pp' return newComponent } class App extends React.Component { render() { return ( <div> App1: {this.props.name} - {this.props.age} </div> ) } } function App2(props) { return ( <div> App2: {props.name} - {props.age} </div> ) } const EnhanceApp = enhanceComponent(App) const EnhanceApp2 = enhanceComponent(App2) class ParentCom extends React.Component { render() { return ( <div> <UserContext.Provider value={{ name: '===', age: 30 }}> <EnhanceApp /> <EnhanceApp2 /> </UserContext.Provider> </div> ) } } export default ParentCom
渲染判断鉴权
就是根据props,来控制显示不同的组件。
生命周期劫持
就是把生命周期处理相同事情的内容,当在高阶组件中统一处理。
高阶组件的意义
利用高阶组件可以针对某些React代码进行更加优雅的处理。
HOC也有自己的一些缺陷:
- HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难。
- HOC可以劫持props,在不遵守约定的情况下也可能造成冲突。
Portals转移节点
某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM元素上的)。
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:
- 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。
- 第二个参数(container)是一个 DOM 元素。参数一挂载的根节点。
import React from 'react' import ReactDOM from 'react-dom' function Modal(props) { // 这里将modal组件中传入的内容拿过来。 return ReactDOM.createPortal(props.children, document.getElementById('modal')) } export default class Parent extends React.Component { constructor(props) { super(props) this.refDom = React.createRef() } render() { return ( <div> <Modal> <h1>modal标题</h1> </Modal> </div> ) } }
fragment 空标签
由于render函数或者函数组件必须只能放回单标签组件,所以我们必须用一个标签包裹众多内容,如果不想渲染这个标签,我们可以使用Fragment标签。
- Fragment 允许你将子列表分组,而无需向 DOM 添加额外节点;
- React还提供了Fragment的短语法:
<> </>
。 但是,如果我们需要在Fragment中添加key,那么就不能使用短语法。
严格模式
StrictMode
是一个用来突出显示应用程序中潜在问题的工具。与 Fragment
一样,StrictMode
不会渲染任何可见的 UI。严格模式检查仅在开发模式下运行;它们不会影响生产构建。
通过React.StrictMode
标签包括的react元素及其子元素都会被检查。
那么严格模式到底检查什么呢?
- 识别不安全的生命周期。
- 使用过时的ref API。
- 使用废弃的findDOMNode方法
- 在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用了。
- 检查意外的副作用
- 这个组件的constructor会被调用两次。
- 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用。
- 在生产环境中,是不会被调用两次的。
- 检测过时的context API
- 早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context的。
添加className的方式
- 字符串拼接注意: 需要在每个class后面或者前面加一个空格。
<p className={'pp ' + (true ? 'active' : '')}>iiii</p>
- 数组拼接
{/* 这里的class会用,链接 */} <p className={['title', 'active']}>oooo</p> <p className={['title', 'active'].join(' ')}>ppppp</p>
- 通过第三方库classnames 我们知道vue中添加class属性是非常方便的。所以这个库可以让我们很方便的给dom元素添加class属性。注意undefined, null, 0, false, NaN, true传入classNames函数不会被加入className中。
<div className={classNames( { active: true }, 'title', undefined, null, 0, NaN, '', true )} > classnams格式 </div>