React 高级三部曲 | 8月更文挑战

277 阅读9分钟

第七节:React原理揭秘

  • 为什么setState() 更新数据是异步的?
  • 知道JSX语法的转化过程吗?
  • 说出React组件的更新机制?
  • 如何对React组件进行性能优化?
  • React的虚拟DOM和Diff算法

1,为什么setState() 更新数据是异步的?

  • setState()更新数据是异步的
  • 后面的setState不依赖于前面的setState
  • 可以多次调用setState, 只会触发一次重新渲染render
state = {
  count: 1
}
handleClick = () => {
   // 注意: setState异步更新数据的
  this.setState({
    count: this.state.count + 1     // 此时:count: 2
  })
  console.log('count', this.state.count)  // 此时:count 1, 因为setState是异步的
  this.setState({
    count: this.state.count + 1    // 此时: count 2
  })
}

如果想实现后面一个setState 依赖前面一个setState,推荐语法:

  • 推荐使用: setState((state, props) => {})

    参数:state: 表示最新的state

    参数: props: 表示最新的props

   handleClick = () => {
       this.setState((state, props) => {
           console.log('我是最新的:', state.count)
           return {
               count: state.count + 1
           }
       })
       this.setState((state, props) => {
        console.log('我是第二次的:', state.count)
        return {
            count: state.count + 1
        }
    })
       console.log(this.state.count)
   }

如果在一个方法里面,两次调用setState(), 推荐使用上面最新的方法,不然会有异步问题,第二次setState的值还是以前的值。

  • setState的第二个参数,是一个回调函数,即:在状态更新后,也就是页面完全渲染后,会立即执行的
setState(updater, [callback])
使用:
this.setState({
   (state, props) => {},
   () => {console.log('这个回调函数会在状态更新后立即执行')}
})
this.setState(
           (state, props) => {
           console.log('我是最新的:', state.count)
           return {
               count: state.count + 1
           }
       },
       () => {
           console.log('状态更新之后', this.state.count)
           document.title = '更新成功之后' + this.state.count
       }
       )

总结一下

  • setState设计为异步,可以显著的提升性能。如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新;
  • 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步。state和props不能保持一致性,会在开发中产生很多的问题;

2, JSX 语法转化的过程

  • JSX仅仅是createElement()方法的语法糖
  • JSX语法是被@babel/preset-react插件编译为createElement()方法
  • React元素:是一个对象,用来描述你希望在屏幕上看到的内容
graph LR
A{JSX语法}
A-->B(createElement)
B-->C{React元素}

-JSX语法
const element1 = <h1 className="greeting">Hello JSX</h1>
- createElement()方法
const element2 = React.createElement(
    'h1',
    {
        className: 'greeting'
    },
    "Hello JSX"
)
-React元素
const element3 = {
   type: 'h1',
   props: {
      className: 'greeting',
      children: 'Hello JSX'
   }
}

3 组件更新机制

  • setState()的两个作用: 1,修改state. 2, 更新组件UI
  • 过程: 父组件重新渲染时, 也会重新渲染子组件,但只会渲染当前组件以及它下面的所有子组件

4, 组件性能优化

  • 减轻state: 只存储跟组件渲染相关的数据

    注意:不做渲染的数据不要方法state中,比如定时器ID,对应这种需要在多个方法中用到的数据,应该放在this中

  • 避免不必要的重新渲染

    注意:组件更新机制里面,父组件更新会引起子组件也被更新,如果子组件没有任何变化时,也会重新渲染,如果避免不必要的重新渲染,使用钩子函数

    shouldComponentUpdate(nextProps, nextState)

    原理:通过返回值决定组件是否重新渲染,返回true表示重新渲染,false表示不重新渲染

    触发时机:更新阶段的钩子函数,组件重新渲染之前执行。

    shouldComponentUpdate —> render

    shouldComponentUpdate(nextProps, nextState) {
         // 根据条件,决定是否重新渲染组件
         // 最新的状态
         console.log('最新的state', nextState)
         // 更新前的状态
         console.log('this.state', this.state)
         return false
     }
    

    随机数案例(1)

import React from 'react'

class RanDom extends React.Component {
    state = {
        number: 0
    }
    handleClick = () => {
       this.setState(() => {
         return {
             number: Math.floor(Math.random() * 3)
         }
       }) 
    }
    // 因为两次生成的随机数可能相同,如果相同,此时,不需要重新渲染
    shouldComponentUpdate(nextProps, nextState) {
      console.log('最新状态:', nextState, ', 当前状态:', this.state.number)
    //   if (nextState.number === this.state.number) {
    //       return false
    //   }
    //   return true
    // 优化写法
      return nextState.number !== this.state.number
    }
    render() {
        console.log('render')
        return (
            <div>
                <h1>随机数:{ this.state.number }</h1>
                <button onClick={this.handleClick}>重新生成</button>
            </div>
        )
    }
}
export default RanDom;

随机案例子组件2

class NumberBox extends React.Component {
    shouldComponentUpdate(nextProps, nextState) {
       console.log('最新Props:', nextProps, ', 上一次Props:', this.props)
       if (nextProps.number === this.props.number) {
           return false
       }
       return true
    }
    render() {
        console.log('子组件render')
        return (
        <h1>随机数:{ this.props.number }</h1>
        )
    }
}
  • React组件复用 1, 思考:如果两个组件中的部分功能相似或相同,如果复用相似的功能

2, 复用相似的功能,注意复用组件的什么 ?

1, state. 2, 操作state的方法(组件状态逻辑)

3,如何复用?

方式1: render props模式。 方式2: 高阶组件(HOC)

注意:这两种方式不是新的API,而是利用React自身特点的编码技巧,演化而成的固定模式

  • 纯组件

    纯组件的内部对比是:shallow compare(浅层对比)

    对于引用类型来说,只比较对象的引用地址是否相同,所以:state或着props中属性值为引用类型时,应该创建新数据,不要直接修改原数据

    // 对象正确做法
    const newObj = { ...state.obj, number: 2}
    setState({obj: newObj})
    ​
    ​
    // 数组正确做法
    // 不要使用数组的push / unshift 等直接修改当前数组的方法
    // 应该用concat 或 slice 等这些返回新数组的方法,或着下面的方法
    this.setState({
      list: [...this.state.list, {新数据}]
    })
    

5, 虚拟DOM和Diff算法

  • react 更新视图的思想是: 只要state变化就重新渲染视图
  • 特点: 思路清晰
  • 问题: 组件中只有一个DOM元素需要更新时,也得把整个组件的内容重新渲染到页面中
  • 理想状态: 部分更新,只要更新变化的地方
  • 问题: React 是如何做到部分更新的? 虚拟DOM配合Diff算法

虚拟DOM: 本质上就是一个JS对象,用了描述你希望在屏幕上看到的内容(UI),

执行的过程中;

1, 初次渲染时, React 会根据初始state(Model),创建一个虚拟DOM对象,

2,React会根据虚拟DOM生成真正的DOM, 渲染到页面中

3,当数据变化后(setState()), 重新根据新的数据,创建新的虚拟DOM对象树

4,与上一次得到的虚拟DOM对象,使用Diff算法对比,找不同点,得到需要更新的内容

5, 最终,React只将变化的内容更新到DOM中, 重新渲染到页面

注意:render方法调用并不意味着浏览器中的重新渲染
// render 方法调用仅仅说明要进行diff算法

6,总结

1, 从工作角度,应用第一,原理第二

2,原理有助于更好地理解React的自身运行机制

3,setState() 异步更新数据

4,父组件更新导致子组件更新,纯组件提升性能

5,思路清晰简单为前提,虚拟DOM和Diff保效率

6,虚拟DOM —> state + JSX 共同构成

7,虚拟DOM的真正价值从来都不是性能,而是让他脱离了浏览器环境限制

第八节:React路由

1, React 基础知识

现代的前端应用大多都是SPA,单页应用程序, 也就是只有一个HTML页面的应用程序,因为它的用户体验更好,对服务器的压力更小,所以更受欢迎。为了有效的使用单页面来管理原来多页面的功能,前端路由应运而生。

  • 前端路由的功能: 让用户从一个视图导航到另一个视图
  • 前端路由是一套映射规则,在React中,是URL路径与组件的对应关系
  • 使用React路由简单来说,就是配置路径和组件

2, 路由使用步骤

1, 安装

npm install react-router-dom
​
yarn add react-router-dom

2, 导入路由的三个核心组件: Router/Route/Link

import {BrowserRouter as Router, Route, Link } from 'react-router-dom'

3, 使用Router 组件包裹整个应用(重要)

<Router>
   <div className="App">
      xxx
   </div>
</Router>

4, 使用Link组件作为导航菜单(路由入口)

<Link to="/first"> 页面一 </Link>

5, 使用Route组件配置路由规则和要展示的组件(路由出口)

const First = () => <p> 页面一的页面内容 </p>
<Router>
  <div>
     <Link to="/first">页面一</Link>
     <Route path="/first" component={First}></Route>
  </div>
</Router>

3, 常用组件说明

  • Router 组件: 包裹整个应用,一个React应用只需要使用一次
  • 两种常用Router: HashRouter 和 BrowserRouter
  • HashRouter: 使用URL的哈希值实现的(localhost:3000/#/first)
  • 推荐:BrowserRouter, 使用H5的history API 实现的(localhost:3000/first)
  • Link组件: 用于指定导航链接(a标签)
  • Route组件, 指定路由展示组件相关信息,Route写在那里,组件就展示在那个位置

4, 路由的执行过程

1, 点击Link组件(a标签),修改了浏览器地址栏中的URL

2,React路由监听到地址栏URL的变化

3,React路由内部遍历所有Route组件, 使用路由规则path与pathname进行匹配

4, 当路由规则path能够匹配地址栏中的pathname时, 就在当前位置上展示该Route组件内容

5, 编程式导航

  • 场景: 点击登陆按钮,登陆成功之后,通过代码跳转到后台首页
  • 编程式导航: 通过JS代码来实现页面跳转
  • history是React路由提供的,用户获取浏览器历史记录的相关信息
  • push(path): 跳转到某个页面,参数path表示要跳转的路径
  • go(n): 前进或后退到某个页面,参数n表示前进或后退页面数量
// 类组件
class Login extends Component {
   handleLogin = () => {
      this.props.history.push('/home')
   }
}
// 函数式组件
const Home = (props) => {
  const handleBack = () => {
    props.history.go(-1)
  }
  return (
    <div>
       <h2>我是后端首页</h2>
      <button onClick={handleBack}></button>
    </div>
  )
}

6, 默认路由

  • 默认路由: 表示进入页面时就会匹配的路由
  • 默认路由path = "/"
<Route path="/" component={Home} />

7, 匹配模式

(1)模糊匹配模式

  • React 默认情况下,路由配置规则是: 模糊匹配模式
  • 模糊匹配规则: 只有pathname以path 开头就会匹配成功

Pathname: 代表Link组件的to属性,也就是:location.pathname

Path: 代表Route组件的path属性

path="/" 配置所有pathname

path="/first" 配置: /first 或 /first/a 或 /first/b/c

(2)精准匹配模式

  • 给Route组件添加exact属性,就让其变成精确匹配模式
  • 精确匹配: 只有当path和pathname完全匹配时,才会展示该路由
// 此时, 该组件只能匹配:pathname="/" 这一种情况
<Route exact path="/" component />

8 嵌套路由

使用步骤:

1, 设置嵌套路由的path, 格式需要以父路由path开头

<Router>
  <div>
    <Route path="/home" component={Home} />
  </div>
</Router>
​
const Home = () => {
  <div>
    <Route path="/home/news" component={News} />
  </div>
}

9, 总结

1, React 路由可以有效的管理多个视图实现SPA

2, Router组件包裹整个应用, 只需要使用一次

3,Link组件是入口, Route组件是出口

4,通过props.history实现编程式导航

5,默认是模糊匹配,添加exact变成精确匹配

6, React 路由的一切都是组件,可以像思考组件一样思考路由