重学React之组件化开发(下)

简介: 重学React之组件化开发

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,那么就不能使用短语法。


严格模式


具体请访问react官网


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>


网络异常,图片无法展示
|



相关文章
|
1月前
|
前端开发 JavaScript API
React开发需要了解的10个库
本文首发于微信公众号“前端徐徐”,介绍了React及其常用库。React是由Meta开发的JavaScript库,用于构建动态用户界面,广泛应用于Facebook、Instagram等知名网站。文章详细讲解了Axios、Formik、React Helmet、React-Redux、React Router DOM、Dotenv、ESLint、Storybook、Framer Motion和React Bootstrap等库的使用方法和应用场景,帮助开发者提升开发效率和代码质量。
119 4
React开发需要了解的10个库
|
3月前
|
设计模式 存储 前端开发
React开发设计模式及原则概念问题之自定义Hooks的作用是什么,自定义Hooks设计时要遵循什么原则呢
React开发设计模式及原则概念问题之自定义Hooks的作用是什么,自定义Hooks设计时要遵循什么原则呢
|
1月前
|
前端开发 JavaScript 开发者
React 组件化开发最佳实践
【10月更文挑战第4天】React 组件化开发最佳实践
49 4
|
2月前
|
XML 移动开发 前端开发
使用duxapp开发 React Native App 事半功倍
对于Taro的壳子,或者原生React Native,都会存在 `android` `ios`这两个文件夹,而在duxapp中,这些文件夹的内容是自动生成的,那么对于需要在这些文件夹中修改的配置内容,例如包名、版本号、新架构开关等,都通过配置文件的方式配置了,而不需要需修改具体的文件
|
2月前
|
资源调度 JavaScript 前端开发
使用vite+react+ts+Ant Design开发后台管理项目(二)
使用vite+react+ts+Ant Design开发后台管理项目(二)
|
2月前
|
存储 前端开发 JavaScript
react 组件化
【9月更文挑战第2天】react 组件化
35 5
|
3月前
|
JavaScript 前端开发 安全
[译] 使用 TypeScript 开发 React Hooks
[译] 使用 TypeScript 开发 React Hooks
|
3月前
|
前端开发 JavaScript
React Server Component 使用问题之添加jsx的组件化能力,如何操作
React Server Component 使用问题之添加jsx的组件化能力,如何操作
|
3月前
|
开发者 自然语言处理 存储
语言不再是壁垒:掌握 JSF 国际化技巧,轻松构建多语言支持的 Web 应用
【8月更文挑战第31天】JavaServer Faces (JSF) 框架提供了强大的国际化 (I18N) 和本地化 (L10N) 支持,使开发者能轻松添加多语言功能。本文通过具体案例展示如何在 JSF 应用中实现多语言支持,包括创建项目、配置语言资源文件 (`messages_xx.properties`)、设置 `web.xml`、编写 Managed Bean (`LanguageBean`) 处理语言选择,以及使用 Facelets 页面 (`index.xhtml`) 显示多语言消息。通过这些步骤,你将学会如何配置 JSF 环境、编写语言资源文件,并实现动态语言切换。
42 0
|
3月前
|
开发者 Java
JSF EL 表达式:乘技术潮流之风,筑简洁开发之梦,触动开发者心弦的强大语言
【8月更文挑战第31天】JavaServer Faces (JSF) 的表达式语言 (EL) 是一种强大的工具,允许开发者在 JSF 页面和后台 bean 间进行简洁高效的数据绑定。本文介绍了 JSF EL 的基本概念及使用技巧,包括访问 bean 属性和方法、数据绑定、内置对象使用、条件判断和循环等,并分享了最佳实践建议,帮助提升开发效率和代码质量。
47 0
下一篇
无影云桌面