React总结(持续更新)

316 阅读10分钟

React相比原生的好处

  • 组件化这其中以react贯彻最为彻底,甚至可以到函数级别的原子组件,高度的组件化可以让我们的组件易于维护、易于组合扩展。
  • 天然分层JQuery时代的代码大部分情况下是面条代码,耦合严重,现代框架不管是MVC、MVP或者是MVVM模式都可以帮助我们进行分层,代码解耦更易于读写操作。
  • 生态现在主流框架都自带生态,不管是数据流管理架构还是UI库都有成熟的解决方案。
  • 开发效率现代前端框架都默认自动更新DOM,而不在是需要我们手动进行操作,解放了开发者的手动DOM成本,间接提高了开发效率,从根本上解决了UI与状态同步的问题。

React和Vue的对比

  • 相同

    1. 虚拟DOM -- 映射真实DOM,通过新旧DOM的diff对比,更好的跟踪渲染页面。
    2. 组件化 -- 将应用拆分成一个个功能明确的模块,每个模块之间可以通过合适的方式互相联系。
    3. 构建工具 -- 都有自己的构建工具,通过webpack + babel去搭脚手架工具。
    4. Chrome开发工具 -- 两者都有很好的Chrome扩展去帮助查找Bug
    5. 配套框架 -- Vue有Vue-router和Vuex,React有React-router和React-Redux
  • 不同

    1. 模块 VS JSX -- Vue推荐编写近似常规HTML的模板进行渲染,而React推荐JSX的书写方式。
    2. 监听数据变化的不同 -- Vue使用的是可变数据,而React更强调数据的不可变。在Vue中通过v-model绑定的数据,用户改变输入值后对应的值也会做出相应的改变。而React则是需要通过setState进行设置的变化
    3. Diff不同 -- Vue通过双向链表实现边对比边更新DOM,而React通过Diff队列保存需要更新的DOM,得到patch树,在统一批量更新DOM
    4. 开发团队 -- Vue开始的核心是Evan You,后面再招了其他人组成团队;React则是一开始就是Facebook团队搞的。所以网上对比源码情况的话,Vue的比React的简单易懂。

setState

  • 调用setState之后发生了什么?
    • 在代码中调用setState之后,React会将传入的参数对象与组件当前的状态合并,触发所谓的调和过程
    • 经过调和过程,React会以相对高效的方式根据新的状态构建React元素树并且着手重新渲染整个UI界面
    • 在React得到元素树之后,React会自动计算新树和老树之间的节点差异,然后根据差异对界面进行最小化重新渲染 在差异计算算法(Diff)中,React能够相对精准的知道哪些位置发生了改变,以及应该如何改变,保证了按需更新而不是全部重新渲染
  1. 合并参数对象,触发调和过程
  2. 计算新树和老树差异(Diff)
  3. 根据差异进行最小化重新渲染

setState是同步

  • 有时候同步,有时候异步
    1. setState在合成事件和钩子函数中是异步的,在原生事件和setTimeout是异步的。
    2. setState的异步,并不是说内部由异步代码实现,它本身执行的过程和代码是同步的,只是合成事件和钩子函数和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,从而形成了所谓的异步,
    3. setState可以通过第二个参数,在回调方法中拿到更新后的结果

React this问题

import React, { Component } from 'react'

class App extends Component {
  constructor (props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick () {
    console.log('jsliang 2020');
  }
  handleClick2 = () => {
    console.log('jsliang 2021');
  }
  render () {
    // 四种绑定方法
    return (
      <div className='App'>
        {/* 方法一:通过 constructor 中进行 bind 绑定 */}
        <button onClick={this.handleClick}>btn 1</button>

        {/* 方法二:在里边绑定 this */}
        <button onClick={this.handleClick.bind(this)}>btn 2</button>

        {/* 方法三:通过箭头函数返回事件 */}
        <button onClick={() => this.handleClick()}>btn 3</button>
        
        {/* 方法四:让方法变成箭头函数 */}
        <button onClick={this.handleClick2}>btn 4</button>
        
        {/* 额外:直接调用不需要绑定 this */}
        {this.handleClick()}
      </div>
    )
  }
}

export default App;
  • 使用bind和箭头函数有什么区别
    • 箭头函数除了代码少,与普通函数最大的不同就是:this是由声明该函数时候定义的,一般是隐性定义为声明该函数时的作用域this。
    • 通过bind的话,相当于:Foo.prototype.a = function() {},是通过原型链的一个指针绑定的

Redux的工作流程

  • redux的核心概念:
    • Store:保存数据的地方,可以把它当成一个容器,整个应用只能有一个Store
    • State:Store对象包含所有数据,如果想得到某个时点的数据,就要对Store生成快照,这种时点的数据集合就叫State
    • Action:State的变化,会导致View的变化。但是,用户接触不到Stae,只能接触到View。所以,State的变化必须是View导致的。Action就是View发出的通知,表示State应该要发生变化了。
    • Action Creator:View要发送多少种消息,就会有多少种Action。如果都手写,会很麻烦,所以我们定义一个函数来生成Action
    • Reducer:Store收到Action之后,必须给出一个新的State,这样View才会发生变化。这种State的计算过程就叫Reducer。Reducer是一个函数,它接受Action和当前State作为参数,返回一个新的State。
    • dispatch:是View发出Action的唯一方法
  • 完成的工作流程
    1. 首先,用户(通过View)发出Action,发出方式就用到了dispatch方法
    2. Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State
    3. State一旦有变化,Store就会调用监听函数,来更新View

react-redux是如何工作的?

  • Provider:Provider的作用是从最外部封装了整个应用,并向connect模块传递store
  • connect:负责连接React和Redux
    • 获取state:connect通过context获取Provider中的store,通过store.getState()获取整个store tree上所有state
    • 包装原组件:将state和action通过props的方式传入到原组件内部wrapWithConnect返回一个ReactComponent对象Connect, Connect重新render外部传入的原组件WrappedComponent,并把Connect中传入的mapStateToProps,mapDispatchToProps与组件上原有的props合并后,通过属性的方式传给WrappedComponent
    • 监听store tree变化:connect缓存了store tree中state的状态,通过当前的state状态和变更前state状态进行比较,从而确定是否调用this.setState()方法触发Connect及其子组件的重新渲染

虚拟DOM

浏览器渲染过程

  • 创建DOM树。用HTML解析器分析HTML元素,创建一棵DOM树
  • 创建CSS规则书(CSS rule tree)。用CSS解析器解析CSS文件和inline样式,生成页面的样式表
  • 创建Render树。将DOM树和CSS规则树关联起来,构建Render树
  • 布局Layout。根据Render树,浏览器开始布局,为每个Render树上的节点确定一个在显示器上出现的精确坐标
  • 绘制Painting。在Render树和节点显示坐标的基础上,调用每个节点的paint方法,将它们绘制出来

DOM操作昂贵

由于在浏览器中操作DOM是很昂贵的

  • 用原生JS或者JQuery操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程

拓展要点:回流和重绘

频繁地操作DOM,会产生一定的性能问题,因此我们需要在这一层抽象,在patch过程中尽可能地一次性将差异更新到DOM中,这样保证了DOM不会出现性能很差的情况。

但是这样并不能解决问题,所以就有了虚拟DOM;

虚拟DOM本质就是用一个原生的JavaScript对象去描述一个DOM节点,是对真实DOM的一层抽象

真实DOM节点

<div id="container">
  <ul>
    <li></li>
  </ul>
</div>

JS模拟虚拟DOM

const tree = Element('div', { id: 'container' }, {
  Element('ul', {}, [
    Element('li', {}, ['新节点值'])
  ]),
});

const root = tree.render();
document.querySelector('#container').appendChild(root);

可以看到虚拟DOM对象最基本的三个属性

  • 标签类型
  • 标签元素的属性
  • 标签元素的子节点

Diff算法

两棵树完全对比的时间复杂度是0(n^3),而React的Diff算法的时间复杂度是0(n)

要实现这么低的时间复杂度,意味着在比较差异时只会对同一层级的节点进行比较,因为如果进行完全的比较,算法实际复杂度会过高,所以舍弃了这种完全的比较方式,采用了同层比较的方式

Diff算法的核心就是对虚拟DOM节点进行深度优先遍历,并对每一个虚拟节点进行编号,在遍历的过程中对同一个层级的节点进行比较,最终得到比较后的差异

Diff的实现,最粗暴的方法就是遍历每个新虚拟DOM节点,和旧虚拟DOM节点比对。在旧DOM中是否存在,不同就卸载原来的改成新的

React或Vue中的key:我们在写业务的时候,经常被告知key值不能是索引值index,why???

假如有四个元素,旧的是1、2、3、4,新的元素是1、3、2、4,这个时候我们如果用了索引值index,那么它们始终只会是0-3。这样的话React或Vue就没法比较好的监听它的一个变动

所以一般来说,我们将key值定位成数组的id或者其他的值,方便它变动的时候去更好的监听它

这样子通过Diff比较完毕之后,我们就可以获取需要变动的内容,最终去更新真实的DOM节点

虚拟DOM实现原理

  • 虚拟DOM本质上是JavaScript对象,是对真实DOM的抽象
  • 状态变更的时候,记录新树和旧树的差异
  • 最后把差异更新到真正的DOM中

虚拟DOM和真实DOM比对

  • 优点
    • 保证性能下限: 虚拟DOM可以经过Diff找出最小差异,然后批量进行patch,这种操作虽然比不上手动优化,但是比起粗暴的DOM操作性能要好很多,因此虚拟DOM可以保证性能下限
    • 无需手动操作DOM:虚拟DOM的Diff和patch都是在一次更新中自动进行的,我们无需手动操作DOM,极大提高了开发效率
    • 跨平台:虚拟DOM本质上是JavaScript对象,而DOM与平台强相关,相比之下虚拟DOM可以进行更方便地跨平台操作,例如服务器渲染、移动端开发等
  • 缺点
    • 无法进行极致优化:在一些性能要求极高的应用中虚拟DOM无法进行针对性的细致优化,例如vsCode采用直接手动操作DOM的方式进行极端的性能优化。

Diff算法

比较原生虚拟DOM和新的虚拟DOM的区别,使用Diff(Different)算法

如上图,在React中,对于setState,它采用异步操作,统一对state中的数据进行更改


  1. 比对第一层的DOM节点,如果它相同,则往下继续对比;如果它不同,则停止对比,更新第一层及以下的DOM节点
  2. 比对第二次的DOM节点......
  3. 形成一种比对算法

总结

  • 把树形结构按照层级分解,只比较同级元素
  • 给列表结构的每个单元添加唯一的key属性,方便比较
  • React只会匹配相同class的component(这里面的class指的是组件的名字)
  • 合并操作,调用component的setState方法的时候,React将其标记为dirty。到每一个事件循环结束,React检查所有标记dirty的component重新绘制
  • 选择性子树渲染。开发人员可以重写shouldComponentUpdate提高Diff的性能