react
函数式编程
- 概念:函数式编程是通过编写纯函数,避免共享状态、可变数据、副作用来构建软件的过程。函数式编程是声明式的编程而不是命令式的。
对react的理解?有哪些特性?
react是什么?
React,用于构建用户界面的 JavaScript 库,只提供了 UI 层面的解决方案。遵循组件设计模式、声明式编程范式和函数式编程概念,以使前端应用程序更高效。使用虚拟 DOM
来有效地操作 DOM
,遵循从高阶组件到低阶组件的单向数据流。帮助我们将界面成了各个独立的小块,每一个块就是组件,这些组件之间可以组合、嵌套,构成整体页面。react
类组件使用一个名为 render()
的方法或者函数组件return
,接收输入的数据并返回需要展示的内容。
特性
- JSX 语法:在JS中通过使用XML的方式去直接声明界面的DOM结构
- 单向数据绑定
- 虚拟 DOM
- 声明式编程。声明式编程是一种编程范式,它关注的是你要做什么,而不是如何做。它表达逻辑而不显式地定义步骤。这意味着我们需要根据逻辑的计算来声明要显示的组件。
- 组件。组件的特点:可组合(每个组件易于和其它组件一起使用,或者嵌套在另一个组件内部)、可重用(每个组件都是具有独立功能的,它可以被使用在多个 UI 场景)、可维护(每个小的组件仅仅包含自身的逻辑,更容易被理解和维护)
组件的优点
- 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现
- 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单
- 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级
类组件与函数组件
类组件
通过使用
ES6
类的编写形式去编写组件,该类必须继承React.Component
如果想要访问父组件传递过来的参数,可通过
this.props
的方式去访问在组件中必须实现
render
方法,在return
中返回React
对象
函数组件
- 通过函数编写的形式去实现一个
React
组件,函数第一个参数为props
用于接收父组件传递过来的参数
类组件与函数组件的区别
- 编写形式
- 状态管理:类组件调用setState就能进行数据状态管理,而函数组件需要使用hooks进行状态管理
- 生命周期:函数组件中不存在生命周期,但某些钩子可以替代生命周期的作用。例如useEffect钩子中能完成componentDidMount生命周期的功能,return时,能完成componentWillUnmount生命周期的功能。
- 调用方式:react内部调用函数组件即是执行函数,而react内部调用类组件需要将组件进行实例化,再调用实例对象的render方法。
- this:类组件中this总是可变的,会产生某些与this指向相关的问题。
真实DOM和虚拟DOM
是什么?
真实DOM是文档对象模型,在页面渲染出的每一个节点都是一个真实DOM结构。
虚拟DOM本质是以JS对象形式存在的对DOM的描述。创建虚拟DOM目的是为了更好将虚拟的节点渲染到页面视图中,虚拟DOM对象的节点与真实DOM的属性一一对应。
区别
- 虚拟 DOM 减少了页面的排版与重绘操作,而真实 DOM 会频繁重排与重绘
- 虚拟 DOM 的总损耗是“虚拟 DOM 增删改+真实 DOM 差异增删改+排版与重绘”,真实 DOM 的总损耗是“真实 DOM 完全增删改+排版与重绘”
优缺点
真实DOM优点:
- 易用
真实DOM缺点:
- 效率低,解析速度慢,内存占用量过高
- 性能差:频繁操作真实 DOM,易于导致重绘与回流
虚拟DOM优点:
- 简单方便:如果使用手动操作真实
DOM
来完成页面,繁琐又容易出错,在大规模应用下维护起来也很困难 - 性能方面:使用 Virtual DOM,能够有效避免真实 DOM 树频繁更新,减少多次更新而引起重绘与回流,提高性能
- 跨平台:React 借助虚拟 DOM,带来了跨平台的能力,一套代码多端运行
虚拟DOM缺点:
- 在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化
- 首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,速度比正常稍慢
react diff
是什么?
diff
算法通过对比新旧Virtual DOM
来找出真正的Dom
变化之处。传统diff算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3)。因为每个节点都要去和另一棵树的全部节点对比一次,这就是n,如果找到有变化的节点,执行插入、删除、修改也是n的复杂度。所有节点都需要执行上述过程,再乘以n,就是o(n^3)复杂度。
为降低时间复杂度,前端框架的diff约定了处理原则:只做同层对比,type变了就不再对比子节点。这个处理原则将算法进行了一个优化,时间复杂度为O(n)
。因为只需遍历一遍,对比type就行了,type变了就不用对比子节点。此外由于vdom中记录了关联的dom节点,执行dom的增删改也不需要遍历,是o(1),所以整体算法复杂度是o(n)。
原理
遵循三个层级的策略:
- tree层级
- conponent 层级
- element 层级
tree层级
虚拟节点中,对DOM相同层级的节点进行比较。这部分只涉及删除、创建操作。
component层级
同一类的组件,则会继续往下diff运算,如果不是同一类的组件,那么直接删除这个组件下的所有子节点,创建新的。这部分只涉及删除、创建操作。
element层级
对于比较同一层级的节点,每个节点在对应的层级用唯一的key作为标识,提供了插入、删除和移动。
通过key
可以准确地发现新旧集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置。
key的作用
key是虚拟DOM对象的标识,判断元素是新创建的还是被移动的元素,从而减少不必要的元素渲染。当状态中的数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】,随后react进行【新虚拟DOM】与【旧虚拟DOM】的diffing比较,比较原则如下:
**a.**旧虚拟DOM中找到了与新虚拟DOM相同的key:
1. 若虚拟DOM中内容没变,直接使用之前的真实DOM
2. 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换页面中之前的真实DOM
b. 旧虚拟DOM中未找到与新虚拟DOM相同的key:根据数据创建新的真实DOM,随后渲染到页面
使用index作为key可能引发的问题
若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新==》界面效果没问题,但效率低
若结构中还包含输入类DOM(input\radio): 会产生错误DOM更新==》界面有问题
注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用Index作为key是没有问题的。
如何选择key
- 最好使用每条数据的唯一标识作为key,比如id\手机号等
- 如果只是简单的数据展示,用index也可以
JSX转成真实DOM的过程
是什么?
react
通过将组件编写的JSX
映射到屏幕,以及组件中的状态发生了变化之后 React
会将这些「变化」更新到屏幕上。JSX
通过babel
最终转化成React.createElement
这种形式。
在转化过程中,babel
在编译时会判断 JSX 中组件的首字母:
- 当首字母为小写时,其被认定为原生
DOM
标签,createElement
的第一个变量被编译为字符串 - 当首字母为大写时,其被认定为自定义组件,createElement 的第一个变量被编译为对象
最终都会通过RenderDOM.render(...)
方法进行挂载
过程
在react
中,节点大致可以分成四个类别:
- 原生标签节点
- 文本节点
- 函数组件
- 类组件
这些类别最终都会被转化成React.createElement
这种形式。
React.createElement
其被调用时会传入标签类型type
,标签属性props
及若干子元素children
,作用是生成一个虚拟Dom
对象。
1 | <div> |
1 | function createElement(type, config, ...children) { |
createElement
会根据传入的节点信息进行一个判断:
- 如果是原生标签节点, type 是字符串,如div、span
- 如果是文本节点, type就没有,这里是 TEXT
- 如果是函数组件,type 是函数名
- 如果是类组件,type 是类名
虚拟DOM
会通过ReactDOM.render
进行渲染成真实DOM
,使用方法如下:
1 | ReactDOM.render(element, container[, callback]) |
当首次调用时,容器节点里的所有 DOM
元素都会被替换,后续的调用则会使用 React
的 diff
算法进行高效的更新。
如果提供了可选的回调函数callback
,该回调将在组件被渲染或更新之后被执行。
ReactDOM.render方法大致实现过程:
1 | function render(vnode, container) { |
流程总结
- 使用React.createElement或JSX编写React组件,实际上所有的 JSX 代码最后都会转换成React.createElement(…) ,Babel帮助我们完成了这个转换的过程。
- createElement函数对key和ref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个虚拟DOM对象
- ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM
react生命周期
旧生命周期流程
- 初始化阶段:由ReactDOM.render()触发—初次渲染
- constructor()
- componentWillMount()
- render()
- componentDidMount()
- 更新阶段:由组件内部this.setState()或由父组件重新render触发
- shouldComponentUpdate()
- componentWillUpdate()
- render()
- componentDidUpdate()
- 卸载阶段:由React.unmountComponentAtNode()触发
- componentWillUnmount()
新生命周期流程
- 初始化阶段:由ReactDOM.render()触发—初次渲染
- constructor():在方法内部通过
super
关键字获取来自父组件的props
。在该方法中,通常的操作为初始化state
状态或者在this
上挂载方法。 - getDerivedStateFromProps():静态方法,不能访问到组件的实例。第一个参数为即将更新的
props
,第二个参数为上一个状态的state
,可以比较props
和state
来加一些限制条件,防止无用的state更新。该方法需要返回一个新的对象作为新的state
或者返回null
表示state
状态不需要更新。 - render()
- componentDidMount()
- 更新阶段:由组件内部this.setState()或由父组件重新render触发
- getDerivedStateFromProps():用于更新state
- shouldComponentUpdate(nextProps, nextState):用于告知组件本身基于当前的
props
和state
是否需要重新渲染组件,默认情况返回true
。用于更新组件。 - render()
- getSnapshotBeforeUpdate():此方法的目的在于获取组件更新前的一些信息,比如组件的滚动位置之类的,在组件更新后可以根据这些信息恢复一些UI视觉上的状态。
- componentDidUpdate()
- 卸载阶段:由React.unmountComponentAtNode()触发
- componentWillUnmount()
即将废弃的生命周期函数
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
现在使用会出现警告,下一个大版本需要加上UNSAFE_前缀才能使用,以后可能会被彻底废弃,不建议使用。
为什么要废弃这些生命周期函数
废弃的原因是在React16的Fiber架构中,调和过程会多次执行will周期,不再是一次执行,失去了原有的意义。fiber是异步渲染,很可能因为高优先级任务的出现而打断现有任务导致will执行多次。多次执行will,在周期中如果有setState或dom操作,会触发多次重绘,影响性能,也会导致数据错乱。
state和props
state
一个组件的显示形态可以由数据状态和外部参数所决定,而数据状态就是 state
,一般在 constructor
中初始化。当需要修改里面的值的状态需要通过调用 setState
来改变,从而达到更新组件内部数据的作用,并且重新调用组件 render
方法。
props
props
是外部传入组件内部的数据。react
具有单向数据流的特性,所以他的主要作用是从父组件向子组件中传递数据。
区别
相同点:
- 两者都是 JavaScript 对象
- 两者都是用于保存信息
- props 和 state 都能触发渲染更新
区别:
- props 是外部传递给组件的,而 state 是在组件内被组件自己管理的,一般在 constructor 中初始化
- props 在组件内部是不可修改的,但 state 在组件内部可以进行修改
super()与super(props)
ES6类
super
关键字实现调用父类,super
代替的是父类的构建函数,使用 super(name)
相当于调用 father.prototype.constructor.call(this,name)
如果在子类中不使用 super
关键字,则会引发报错。报错的原因是子类是没有自己的 this
对象的,它只能继承父类的 this
对象,然后对其进行加工。而 super()
就是将父类中的 this
对象继承给子类的,没有 super()
子类就得不到 this
对象。如果先调用 this
,再初始化 super()
,同样是禁止的行为。
super()与super(props)的区别
在 React
中,类组件基于 ES6
,所以在 constructor
中必须使用 super
。在调用 super
过程,无论是否传入 props
,React
内部都会将 props
赋值给组件实例 props
属性中。如果只调用了 super()
,那么 this.props
在 super()
和构造函数结束之间仍是 undefined
。
setState
是什么?
更改数据状态state里面的值需要通过调用setState来改变
写法
- 对象式
- setState(stateChange, [callback])
- stateChange为状态改变对象;callback是可选的回调函数,在状态更新完毕、界面也更新后才被调用
1 | this.setState({count:count + 1}) |
- 函数式
- setState(updater, [callback])
- updater为函数,可以接收state和props
1 | this.setState((state,props) => { |
- 使用原则:如果新状态不依赖于原状态则使用对象式;如果新状态依赖于原状态则使用函数式;如果需要在setState执行后获取最新的状态数据,就需要在回调函数中读取。
更新类型
setState是一个同步的方法,但是setState引起react后续更新状态的动作是异步的。
异步更新:在组件生命周期或React合成事件中,也就是在React能管控的地方
同步更新:在setTimeout或者原生dom事件中
1 | // 异步更新 |
1 | // 同步更新 |
批量更新
1 | handleClick = () => { |
对同一个值进行多次 setState
, setState
的批量更新策略会对其进行覆盖,取最后一次的执行结果。所谓异步批量是指在一次页面更新中如果涉及多次 state 修改时,会合并多次 state 修改的结果得到最终结果从而进行一次页面更新。
如果是下一个state
依赖前一个state
的话,推荐给setState
一个参数传入一个function
,如下:
1 | onClick = () => { |
react事件机制
是什么?
React
基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等。在React
中这套事件机制被称之为合成事件。
合成事件
合成事件是 React
模拟原生 DOM
事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器。根据 W3C
规范来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口。
react事件与原生事件非常相似,但也存在区别:
- 事件名称命名方式不同
1 | // 原生事件绑定方式:小写 |
- 事件处理函数书写不同
1 | // 原生事件 事件处理函数写法:带括号 |
虽然onClick
看似绑定到DOM
元素上,但实际并不会把事件代理函数直接绑定到真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件去监听。
这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象。
当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率也有很大提升。
合成事件与原生事件的执行顺序
1 | import React from 'react'; |
输出顺序为:原生事件–>合成事件–>document挂载事件
1 | 原生事件:子元素 DOM 事件监听! |
可以得出以下结论:
- React 所有事件都挂载在 document 对象上
- 当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件
- 所以会先执行原生事件,然后处理 React 事件
- 最后真正执行 document 上挂载的事件
- React 自身实现了一套事件冒泡机制,想要阻止不同时间段的冒泡行为,对应使用不同的方法
类组件中事件绑定方式
render函数是被组件实例调用的,其中的this指向的就是当前的组件实例,但是如果赋值给点击事件了,this则改变了指向,由react内部直接调用onClick,此时的this则是undefined (class内部开启局部的严格模式this不指向window)
常见的绑定方式
- render方法中使用bind
- render方法中使用箭头函数
- constructor中bind
- 定义阶段使用箭头函数绑定
render方法中使用bind
1 | render() { |
render方法中使用箭头函数
1 | render() { |
constructor中bind
1 | class App extends React.Component { |
函数定义阶段使用箭头函数绑定
1 | class App extends React.Component { |
react中组件的通信方式
分为:
父组件向子组件传递:props
子组件向父组件传递:父组件向子组件传一个函数,然后通过这个函数的回调,拿到子组件传过来的值
兄弟组件之间的通信:父组件作为中间层来实现数据的互通,通过使用父组件传递
父组件向后代组件传递:使用
context
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22//父组件
const PriceContext = React.createContext('price')
//使用Provider创建数据源,通过value属性给后代组件传递数据
<PriceContext.Provider value={100}></PriceContext.Provider>
//子组件
//后代组件可以通过Consumer或者使用contextType属性接收数据
//contextType
class MyClass extends React.Component {
static contextType = PriceContext;
render() {
let price = this.context;
/* 基于这个值进行渲染工作 */
}
}
//Consumer
<PriceContext.Consumer>
{ /*这里是一个函数*/ }
{
price => <div>price:{price}</div>
}
</PriceContext.Consumer>非关系组件传递:使用
redux
refs
是什么?
refs允许我们访问 DOM
节点或在 render
方法中创建的 React
组件。如果是渲染组件则返回的是组件实例,如果渲染dom
则返回的是具体的dom
节点。不能在函数组件上使用ref
属性,因为他们并没有实例。
一般函数式组件都是用React.forwardRef
包装一下然后返回出去的, 函数式组件本来就是一个render
函数,不过在被React.forwardRef
包装后就多了一个ref
属性了。
使用方式
传入字符串,使用时通过 this.refs.传入的字符串的格式获取对应的元素
1
2
3
4
5
6
7
8
9constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref="myref" />;
}
//访问
this.refs.myref.innerHTML = "hello";传入对象,对象是通过 React.createRef() 方式创建出来,使用时获取到创建的对象中存在 current 属性就是对应的元素
1
2
3
4
5
6
7
8
9constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
//访问
const node = this.myRef.current;传入函数,该函数会在 DOM 被挂载时进行回调,这个函数会传入一个元素对象,可以自己保存,使用时,直接拿到之前保存的元素对象即可
1
2
3
4
5
6
7
8
9constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={element => this.myref = element} />;
}
//访问
const node = this.myref传入hook,hook是通过 useRef() 方式创建,使用时通过生成hook对象的 current 属性就是对应的元素
1 | const myref = useRef() |
refs应用场景
- 对Dom元素的焦点控制、内容选择、控制
- 对Dom元素的内容设置及媒体播放
- 对Dom元素的操作和对组件实例的操作
- 集成第三方 DOM 库
受控组件与非受控组件
受控组件
受控制的组件,组件的状态全程响应外部数据。受控组件一般需要初始状态和一个状态更新事件函数。
在HTML中,表单元素通常维护自己的value,并根据用户输入进行更新。而在React中,可变状态通常保存在组件的state属性中,并且只能通过使用setState()来更新。可以把两者结合起来,使React的state成为唯一数据源。渲染表单的React组件还控制着用户输入过程中表单发生的操作。被React以这种方式控制取值的表单输入元素叫做受控组件。如下为示例:
1 | class NameForm extends React.Component{ |
由于在表单元素上设置了value
属性,因此显示的值始终为this.state.value
,这使得React的state成为唯一数据源。对于受控组件来说,输入的值始终由React的state驱动。
非受控组件
不受控制的组件。一般情况是在初始化的时候接受外部数据,然后自己在内部存储其自身状态。当需要时,可以使用ref
查询 DOM
并查找其当前值。
非受控组件的数据将交由DOM节点来处理。要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,使用ref来从DOM节点中获取表单数据。如下所示为示例:
1 | class NameForm extends React.Component{ |
非受控组件将真实数据存储在DOM节点中,所以在使用非受控组件时,有时候更容易同时集成React和非React代码。
应用场景
大部分时候推荐使用受控组件来实现表单,因为在受控组件中,表单数据由React
组件负责处理。如果选择非受控组件的话,控制能力较弱,表单数据就由DOM
本身处理,但更加方便快捷,代码量少。
高阶组件
是什么?
接受一个或多个组件作为参数并且返回一个组件。本质上是一个装饰者设计模式。
编写
1 | import React, { Component } from 'react'; |
把通用的逻辑放在高阶组件中,对组件实现一致的处理,从而实现代码的复用。所以,高阶组件的主要功能是封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用。
如果向一个高阶组件添加ref
引用,那么ref
指向的是最外层容器组件实例的,而不是被包裹的组件,如果需要传递refs
的话,则使用React.forwardRef
。
应用场景
高阶组件能够提高代码的复用性和灵活性,在实际应用中,常常用于与核心业务无关但又在多个模块使用的功能,如权限控制、日志记录、数据校验、异常处理、统计上报等。
如下实例,存在一个组件,需要从缓存中获取数据,然后渲染。
1 | //不使用高阶组件 |
1 | // 使用高阶组件 |
react hooks
是什么?
可以在不编写 class
的情况下使用 state
以及其他的 React
特性。使用hooks重点解决的是状态相关的重用问题。
使用hooks的原因
- 难以重用和共享组件中的与状态相关的逻辑
- 逻辑复杂的组件难以开发与维护,当我们的组件需要处理多个互不相关的 local state 时,每个生命周期函数中可能会包含着各种互不相关的逻辑在里面
- 类组件中的this增加学习成本,类组件在基于现有工具的优化上存在些许问题
- 由于业务变动,函数组件不得不改为类组件等等
常用hooks
- useState:在函数组件中通过
useState
实现函数内部维护state
,参数为state
默认的值,返回值是一个数组,第一个值为当前的state
,第二个值为更新state
的函数。 - useEffect:可以在函数组件中进行一些带有副作用的操作。
useEffect
第一个参数接受一个回调函数,默认情况下,useEffect
会在第一次渲染和更新之后都会执行,相当于在componentDidMount
和componentDidUpdate
两个生命周期函数中执行回调。如果某些特定值在两次重渲染之间没有发生变化,你可以跳过对 effect 的调用,这时候只需要传入第二个参数。回调函数中可以返回一个清除函数,这是effect
可选的清除机制,相当于类组件中componentwillUnmount
生命周期函数。**useEffect
相当于componentDidMount
,componentDidUpdate
和componentWillUnmount
这三个生命周期函数的组合**。 - useContext:父组件与后代组件通信
- useRef:获取DOM结构
- useMemo:类组件中性能优化的方法是shouldComponentUpdate和PureComponent,函数组件中性能优化的方法是useMemo。它接收两个参数,一个是在渲染过程中被调用的函数,另一个是变量。只有当这个变量被调用的时候,函数才会被执行。
1 | useMemo(()=>value, [m ,n]) |
useEffect第二个参数的执行规则
- 不传参数:每次 render 后都执行
- 空数组:传入第二个参数,每次 render 后比较数组的值没变化,不会在执行,等同于类组件中的 componentDidMount。故只会在第一次render的执行一次。
- 一个值的数组:比较该值有变化就执行
- 多个值的数组:会比较每一个值,有一个不相等就执行
- 传入的为数组/函数/对象:第一次渲染以及每次更新渲染后都执行。因为useEffect执行的是浅层比较([…]===[…] false)。
react中引入CSS的方式
- 在组件内直接使用
- 组件中引入 .css 文件
- 组件中引入 .module.css 文件
- CSS in JS
在组件中直接使用
1 | import React, { Component } from "react"; |
优点:
- 内联样式, 样式之间不会有冲突
- 可以动态获取当前state中的状态
缺点:
- 写法上都需要使用驼峰标识
- 某些样式没有提示
- 大量的样式, 代码混乱
- 某些样式无法编写(比如伪类/伪元素)
组件中引入CSS文件
App.css
1 | .title { |
组件中引入
1 | import React, { PureComponent } from 'react'; |
缺点:
- 样式是全局生效,样式之间会互相影响
组件中引入.module.css文件
将css
文件作为一个模块引入,这个模块中的所有css
,只作用于当前组件,不会影响当前组件的后代组件。
1 | import React, { PureComponent } from 'react'; |
缺点:
- 引用的类名,不能使用连接符(.xxx-xx),在 JavaScript 中是不识别的
- 所有的 className 都必须使用 {style.className} 的形式来编写
- 不方便动态来修改某些样式,依然需要使用内联样式的方式;
CSS in JS
CSS-in-JS, 是指一种模式,其中CSS
由 JavaScript
生成而不是在外部文件中定义。此功能并不是 React 的一部分,而是由第三方库提供,例如:
- styled-components
- emotion
- glamorous
styled-components基本使用
本质是通过函数的调用,最终创建出一个组件:
- 这个组件会被自动添加上一个不重复的class
- styled-components会给该class添加相关的样式
基本使用如下:
创建一个style.js
文件用于存放样式组件:
1 | export const SelfLink = styled.div` |
引入
1 | import React, { Component } from "react"; |
react组件间过渡动画
是什么?
当一个组件在显示与消失过程中存在过渡动画,可以很好的增加用户的体验。在react
中实现过渡动画效果会有很多种选择,如react-transition-group
,react-motion
,Animated
,以及原生的CSS
都能完成切换动画。
如何实现?
在react
中,react-transition-group
是一种很好的解决方案,其为元素添加enter
,enter-active
,exit
,exit-active
这一系列勾子。可以帮助我们方便的实现组件的入场和离场动画。
其主要提供了三个主要的组件:
- CSSTransition:在前端开发中,结合 CSS 来完成过渡动画效果
- SwitchTransition:两个组件显示和隐藏切换时,使用该组件
- TransitionGroup:将多个动画组件包裹在其中,一般用于列表中元素的动画
Redux工作原理
是什么?
将所有状态进行集中管理,当需要更新状态的时候,只需要对这个管理集中处理,而不用去关心状态是如何分发到每一个组件内部的。
redux遵循的原则:
- 单一数据源:state以单一对象存储在store对象中
- state 是只读的:只有get,没有set
- 使用纯函数来执行修改:使用reducer执行state更新
redux工作流程
View在redux中会派发action方法;action通过store的dispatch方法会派发给store;store接收action,连同之前的state,一起传递给reducer;reducer返回新的数据给store;store去改变自己的state。
代码
创建store
1 | import { createStore } from 'redux' // 引入一个第三方的方法 |
创建reducer,reducer
本质就是一个函数,接收两个参数state
,action
,返回state
1 | const initialState = { |
通过store.getState()
可以来获取当前state
纯函数
一类特别的函数:只要是同样的输入,必定得到同样的输出
需要遵守的约束
- 不得改写参数数据
- 不会产生任何副作用,例如网络请求,输入和输出设备
- 不能调用Date.now()或Math.random()等不纯的方法
redux的reducer必须是一个纯函数
redux中间件
是什么?
中间件(Middleware)是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的。redux是JS库而不是react组件库。
Redux
整个工作流程,当action
发出之后,reducer
立即算出state
,整个过程是一个同步的操作。如果需要支持异步操作,或者支持错误处理、日志监控,这个过程就可以用上中间件。Redux
中,中间件就是在dispatch
过程,在分发action
进行拦截处理。
中间件本质上是一个函数,对store.dispatch
方法进行了改造,在发出 Action
和执行 Reducer
这两步之间,添加了其他功能。
常见的中间件
中间件需要通过applyMiddlewares
进行注册,作用是将所有的中间件组成一个数组,依次执行,然后作为第二个参数传入到createStore
中。
1 | const store = createStore( |
常用中间件:
- redux-chunk:用于异步操作
- redux-logger:用于日志记录
redux-chunk
异步处理中间件
redux-chunk源码
1 | function createThunkMiddleware(extraArgument) { |
翻译成ES5的写法
1 | function createThunkMiddleware(extraArgument) { |
redux-thunk最重要的思想,就是可以接受一个返回函数的action creator。如果这个action creator 返回的是一个函数,就执行它,如果不是,就按照原来的next(action)执行。
正因为这个action creator可以返回一个函数,那么就可以在这个函数中执行一些异步的操作。例如:
1 | export function addCount() { |
addCountAsync
这个action creator就返回了一个函数,将dispatch作为函数的第一个参数传递进去,在函数内进行异步操作就可以了。
redux-logger
实现日志记录
在react项目中使用redux
react-redux
react-redux将组件分成:
- 容器组件:存在逻辑处理
- UI 组件:只负责现显示和交互,内部不处理逻辑,状态由外部控制
使用
react-redux两大核心:
- Provider
- connection
Provider
在redux
中存在一个store
用于存储state
,如果将这个store
存放在顶层元素中,其他组件都被包裹在顶层元素之上。那么所有的组件都能够受到redux
的控制,都能够获取到redux
中的数据。
1 | <Provider store = {store}> |
connection
connect
方法将store
上的getState
和 dispatch
包装成组件的props
1 | //导入 |
mapStateToProps
把redux
中的数据映射到react
中的props
中去
1 | //用法 |
mapDispatchToProps
将redux
中分发action的函数映射到组件内部的props
中
1 | //用法 |
项目结构
按角色组织(MVC)
角色如下:
- reducers
- actions
- components
- containers
按功能组织
使用redux
使用功能组织项目,也就是把完成同一应用功能的代码放在一个目录下,一个应用功能包含多个角色的代码
React Router
是什么?
用于实现无刷新的条件下切换显示不同的页面。路由的本质就是页面的URL
发生改变时,页面的显示结果可以根据URL
的变化而变化,但是页面不会刷新。
因此,可以通过前端路由可以实现单页(SPA)应用
react-router
主要分成了几个不同的包:
- react-router: 实现了路由的核心功能
- react-router-dom: 基于 react-router,加入了在浏览器运行环境下的一些功能
- react-router-native:基于 react-router,加入了 react-native 运行环境下的一些功能
- react-router-config: 用于配置静态路由的工具库
常用API
- BrowserRouter、HashRouter
- Route
- Link、NavLink
- switch
- redirect
BrowserRouter、HashRouter区别
- 底层原理不一样
BrowserRouter使用的是H5的history API,不兼容IE9及以下版本
HashRouter使用的是URL的哈希值 - path表现形式不一样
BrowserRouter的路径中没有#,e.g. localhost:3000/demo/test
HashRouter的路径中包含#, e.g. localhost:3000/#/demo/test - 刷新后对路由state参数的影响
BrowserRouter没有任何影响,因为state保存在history对象中
HashRouter刷新后会导致路由state参数的丢失,因为其没有使用history - 备注:HashRouter可以用于解决一些路径错误相关的问题,例如样式丢失问题
Route
Route
用于路径的匹配,然后进行组件的渲染,对应的属性如下:
- path 属性:用于设置匹配到的路径
- component 属性:设置匹配到路径后,渲染的组件
- render 属性:设置匹配到路径后,渲染的内容
- exact 属性:开启精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件
1 | <Route path="/" render={() => <h1>Welcome!</h1>} /> |
Link、NavLink
Link和NavLink用于路径跳转,其中NavLink是在Link基础之上增加了一些样式属性,例如当组件被选中时,发生样式变化就可以使用NavLink设置:
- activeStyle:活跃时(匹配时)的样式
- activeClassName:活跃时添加的class
1 | <NavLink to="/" exact activeStyle={{color: "red"}}>首页</NavLink> |
redirect
路由重定向,当这个组件出现时,会执行跳转到对应的to路径中
1 | const About = ({ |
重定向与link的区别在于,link可以用浏览器的回退按钮返回上一级,而重定向不可以
Switch
当匹配到第一个组件的时候,后面的组件就不应该继续匹配
1 | <Switch> |
react-router中的hooks
- useHistory
- useParams
- useLocation
useHistory
useHistory
可以让组件内部直接访问history
,无须通过props
获取
1 | import { useHistory } from "react-router-dom"; |
useParams
1 | const About = () => { |
useLocation
useLocation
会返回当前 URL
的 location
对象
1 | import { useLocation } from "react-router-dom"; |
路由传递参数的形式
- params参数
- search参数
- state参数
params参数
1 | 路由链接(携带参数):<Link to='/demo/test/tom/18'>detail</Link> |
search参数
1 | 路由链接(携带参数):<Link to='/demo/test/?name=tom&age=18'>detail</Link> |
state参数
1 | 路由链接(携带参数):<Link to={{pathname: '/demo/test', state:{name: 'tome', age: 18}}}></Link> |
路由中的对象
- history对象
- match对象
- location对象
history对象
包含了组件可以使用的各种路由系统的方法,常用的有push和replace,两者都是跳转页面,但是replace不会引起页面的刷新,仅仅改变url。
match对象
包括了具体的URL信息,在params字段中可以获取到各个路由参数的值
location对象
相当于URL的对象形式表示,通过search字段可以获取到url中的query信息
HashRouter原理
hash
值改变,触发全局 window
对象上的 hashchange
事件。所以 hash
模式路由就是利用 hashchange
事件监听 URL
的变化,从而进行 DOM
操作来模拟页面跳转。
HashRouter
包裹了整个应用,通过window.addEventListener('hashChange',callback)
监听hash
值的变化,并传递给其嵌套的组件然后通过context
将location
数据往后代组件传递。
单页面应用路由实现原理
原理是切换URL,监听URL变化,从而渲染不同的页面。主要的方式有history模式和hash模式。
history模式
- 改变路由
1 | history.pushState(state, title, path) |
- 监听路由
1 | window.addEventListener('popstate', function(e){ |
hash模式
- 改变路由
1 | window.location.hash |
- 监听路由
1 | window.addEventListener('hashchange', function(e){ |
Router与Route的区别
Router接收location变化,派发更新流。也就是把props.history.location等路由信息传递下去。一个项目应该有一个根Router,来产生切换路由组件之前的更新作用。如果存在多个Router会造成路由切换页面不更新的情况。
Route匹配路径渲染组件。作为路由组件的容器,可以根据URL将实际的组件渲染出来。通过RouterContext.Consumer
取出上一级的location,match等信息,并作为props传递给页面组件,使得可以在页面组件中的props中获取location,match等信息。
Immutable
是什么?
指一旦创建,就不能再被更改的数据。对Immutable对象的任何修改、增加或删除操作都会返回一个新的Immutable对象。
Immutable实现的原理是Persistent Data Structure(持久化数据结构):
- 用一种数据结构来保存数据
- 当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费
也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变,同时为了避免 deepCopy
把所有节点都复制一遍带来的性能损耗,Immutable
使用了 Structural Sharing
(结构共享)。如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。
内部提供了一套完整的 Persistent Data Structure,还有很多易用的数据类型,如Collection
、List
、Map
、Set
、Record
、Seq
,其中:
- List: 有序索引集,类似 JavaScript 中的 Array
- Map: 无序索引集,类似 JavaScript 中的 Object
- Set: 没有重复值的集合
使用
依赖于immutable.js库。
主要方法:
- fromJS:将一个js数据转换为Immutable类型的数据
- toJS:将一个Immutable数据转换为JS类型的数据
- is:对两个对象进行比较
1 | import { Map, is } from 'immutable' |
- get(key):对数据或对象取值
- getIn([]):对嵌套对象或数组取值,传参为数组,表示位置
1 | let abs = Immutable.fromJS({a: {b:2}}); |
- setIn([], value):赋值,数组表示位置,value为所赋的值
1 | import Immutable from 'immutable'; |
Immutable在React中的应用
使用 Immutable
可以给 React
应用带来性能的优化,主要体现在减少渲染的次数。
在做
react
性能优化的时候,为了避免重复渲染,我们会在shouldComponentUpdate()
中做对比,当返回true
执行render
方法。Immutable
通过is
方法则可以完成对比,而无需通过深度比较的方式比较。在使用
redux
过程中也可以结合Immutable
,不使用Immutable
前修改一个数据需要做一个深拷贝。
render触发时机
- setState
- useState
在React
中,类组件只要执行了 setState
方法,就一定会触发 render
函数执行,函数组件使用useState
更改状态不一定导致重新render
。
组件的props
改变了,不一定触发 render
函数的执行,但是如果 props
的值来自于父组件或者祖先组件的 state
。在这种情况下,父组件或者祖先组件的 state
发生了改变,就会导致子组件的重新渲染。
所以,一旦执行了setState
就会执行render
方法,useState
会判断当前值有无发生改变确定是否执行render
方法,一旦父组件发生渲染,子组件也会渲染。
提高组件渲染效率
效率低的两个场景:1. 类组件中只要调用setState方法就会导致渲染,而不论state是否发生变化;2. 父组件只要发生了渲染,不论子组件是否改变,子组件都会执行渲染。
提高效率的方式:
- shouldComponentUpdate
- PureComponent
- React.memo
shouldComponentUpdate(nextProps, nextState)
通过shouldComponentUpdate
生命周期函数来比对 state
和 props
,确定是否要重新渲染。默认情况下返回true
表示重新渲染,如果不希望组件重新渲染,返回 false
即可。
PureComponent
跟shouldComponentUpdate
原理基本一致,通过对 props
和 state
的浅比较结果来实现 shouldComponentUpdate
。当对象包含复杂的数据结构时,对象深层的数据已改变却不会触发 render
。
React.memo
React.memo
用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent
十分类似。但不同的是, React.memo
只能用于函数组件。
1 | import { memo } from 'react'; |
通过给memo第二个参数传递比较函数,可以进行深层次的比较。
1 | function arePropsEqual(prevProps, nextProps) { |
react.memo的缺点:1.在最外层包装整个组件;2.需要手动写一个方法比较那些具体的props不相同才进行重新渲染。这些缺点导致无法实现如下场景:组件不需要整个重新渲染,而是只需要局部渲染。可以使用useMemo实现,只有当依赖项发生改变的时候才会重新触发渲染逻辑。
react性能优化的方法
主要是从代码层面、工程层面和框架机制层面进行优化:
- 避免不必要的render,使用shouldComponentUpdate、PureComponent、React.memo
- 避免使用内联函数
- 使用React Fragments避免额外标记
- 使用Immutable
- 懒加载组件
- 事件绑定方式
- 服务端渲染
避免使用内联函数
若使用内联函数,则每次调用render
函数时都会创建一个新的函数实例
1 | //使用内联函数 |
应该在组件内部创建一个函数,并将事件绑定到该函数本身。这样每次调用 render
时就不会创建单独的函数实例。
1 | import React from "react"; |
使用Fragments避免额外标签
用户创建新组件时,每个组件需要有一个父标签。父级不能有两个标签,所以顶部要有一个公共标签,所以我们经常在组件顶部添加额外标签div
。这个额外标签除了充当父标签之外,并没有其他作用,这时候则可以使用fragement
。其不会向组件引入任何额外标记,但它可以作为父级标签的作用。
事件绑定方式
从性能方面考虑,在render
方法中使用bind
和render
方法中使用箭头函数这两种形式在每次组件render
的时候都会生成新的方法实例,性能欠缺。
而constructor
中bind
事件与定义阶段使用箭头函数绑定这两种形式只会生成一个方法实例,性能方面会有所改善。
使用Immutable
使用Immutable能减少渲染次数。在做react
性能优化的时候,为了避免重复渲染,我们会在shouldComponentUpdate()
中做对比,当返回true
执行render
方法。Immutable
通过is
方法则可以完成对比,而无需像一样通过深度比较的方式比较。
懒加载组件
从工程方面考虑,webpack
存在代码拆分能力,可以为应用创建多个包,并在运行时动态加载,减少初始包的大小。而在react
中使用到了Suspense
和 lazy
组件实现代码拆分功能,基本使用如下。
1 | const johanComponent = React.lazy(() => import(/* webpackChunkName: "johanComponent" */ './myAwesome.component')); |
服务端渲染
采用服务端渲染端方式,可以使用户更快的看到渲染完成的页面。服务端渲染,需要开启一个node
服务,可以使用express
、koa
等,调用react
的renderToString
方法,将根组件渲染成字符串,再输出到响应中。
1 | import { renderToString } from "react-dom/server"; |
1 | import ReactDOM from 'react-dom'; |
react服务端渲染
是什么?
SSR(Server-Side Rendering)服务端渲染。指由服务端完成页面的 HTML
结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程。
能够解决两个问题:
- SEO,搜索引擎爬虫抓取工具可以直接查看完全渲染的页面
- 加速首屏加载,解决首屏白屏问题
实现
- 手动搭建一个SSR框架
- 使用成熟的SSR框架,如Next.js
手动搭建
首先通过express
启动一个app.js
文件,用于监听3000端口的请求,当请求根目录时,返回HTML
,如下。
1 | const express = require('express') |
然后再服务器中编写react
代码,在app.js
中进行应引用
1 | import React from 'react' |
为了让服务器能够识别JSX
,这里需要使用webpack
对项目进行打包转换,创建一个配置文件webpack.server.js
并进行相关配置,如下:
1 | const path = require('path') //node的path模块 |
借助react-dom
提供了服务端渲染的 renderToString
方法,负责把React
组件解析成html
1 | import express from 'express' |
某些事件处理的方法,是无法在服务端完成,因此需要将组件代码在浏览器中再执行一遍,这种服务器端和客户端共用一套代码的方式就称之为同构。重构通俗讲就是一套React代码在服务器上运行一遍,到达浏览器又运行一遍:
- 服务端渲染完成页面结构
- 浏览器端渲染完成事件绑定
浏览器实现事件绑定的方式为让浏览器去拉取JS
文件执行,让JS
代码来控制,因此需要引入script
标签。通过script
标签为页面引入客户端执行的react
代码,并通过express
的static
中间件为js
文件配置路由。
1 | import express from 'express' |
在客户端执行以下react
代码,新建webpack.client.js
作为客户端React代码的webpack
配置文件如下:
1 | const path = require('path') //node的path模块 |
这种方法就能够简单实现首页的react
服务端渲染。
在做完初始渲染的时候,一个应用会存在路由的情况,配置信息如下Routers.js:
1 | import React from 'react' //引入React以支持JSX |
然后可以通过index.js
引用路由信息,如下
1 | import React from 'react' |
这时候控制台会存在报错信息,原因在于每个Route
组件外面包裹着一层div
,但服务端返回的代码中并没有这个div
。解决方法只需要将路由信息在服务端执行一遍,使用StaticRouter
来替代BrowserRouter
,通过context
进行参数传递。
1 | import express from 'express' |
简化原理
node server
接收客户端请求,得到当前的请求url
路径,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 props
、context
或者store
形式传入组件。
然后基于 react
内置的服务端渲染方法 renderToString()
把组件渲染为 html
字符串,再把最终的 html
进行输出前需要将数据注入到浏览器端。
浏览器开始进行渲染和节点对比,然后执行完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html
节点,整个流程结束。
Fiber架构
JS单线程的问题
JS是单线程的,在浏览器的环境中,需要负责页面的JS解析、执行、绘制、事件处理、静态资源加载和处理等。JS引擎和页面渲染引擎处于同一个主线程,两者的执行是互斥的。单线程表明JS同时只能执行一个任务,如果有个任务长期霸占CPU,那么其他任务都无法执行,浏览器就会呈现卡死的状态,这样会降低用户的体验感。
对于前端框架来说,解决这个问题的三种途径:
- 优化每个任务,让他有多快就多快,相当于是在挤压CPU的运算量。vue就是基于这种想法的,使用
模板
来对任务进行优化,并结合响应式机制让vue精确地进行节点更新。 - 快速响应用户,让用户觉得够快,不能阻塞用户的交互。react是基于此种思想的。
- 尝试worker多线程。这种方法需要保持状态和视图的一致性比较麻烦。
1 | // 多线程之间的通信实际上需要借助主线程,子线程A将消息发送给主线程,然后主线程将A线程发送的消息发送给B |
reconcilation调和过程及为什么需要Fiber
react递归比对虚拟树,找出需要变动的节点,然后进行更新的过程称为调和过程。
在调和过程期间,react会霸占浏览器资源,可能导致用户触发的事件得不到响应或者导致掉帧,从而使用户感知到卡顿。调和过程是CPU密集型操作,相当于是长进程,不能让长进程长期霸占资源。
为了使浏览器合理分配CPU资源,提高用户响应速率,同时兼顾任务执行效率。react通过fiber架构,使reconcilation过程变成可中断的,适时让出CPU执行权。
Fiber
React Fiber 在React 16 版本发布
在react
中,主要做了以下的操作:
- 为每个任务增加了优先级,优先级高的任务可以中断低优先级的任务。然后再重新,注意是重新执行优先级低的任务
- 增加了异步任务,调用requestIdleCallback api,浏览器空闲的时候执行
- dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执行
从架构角度来看,Fiber
是对 React
核心算法(即调和过程)的重写
从编码角度来看,Fiber
是 React
内部所定义的一种数据结构,它是 Fiber
树结构的节点单位,也就是 React 16
新架构下的虚拟DOM
一个 fiber
就是一个 JavaScript
对象,包含了元素的信息、该元素的更新操作队列、类型。
Fiber如何解决
首先 React 中的渲染更新任务会被切割为多个子任务,对不同的任务赋予不同的优先级,分批完成。每次只做一小部分,做完看是否还有剩余时间,如果有就继续执行下一个任务;如果没有,挂起当前任务,将控制权交回给浏览器,让浏览器有时间再进行页面的渲染。等浏览器忙完之后有剩余时间,再继续之前 React 未完成的任务,是一种合作式调度。
该实现过程是基于 Fiber
节点实现。作为静态的数据结构来说,每个 Fiber
节点保存了该组件的类型(函数组件/类组件/原生组件等等)、对应的 DOM 节点等信息。作为动态的工作单元来说,每个 Fiber
节点保存了本次更新中该组件改变的状态、要执行的工作。
需要明确的问题
浏览器没有的抢占条件,react只能使用主动让出机制
一是浏览器中没有类似进程的概念,’任务‘之间的界限很模糊,没有上下文,所以不具备中断/恢复的条件。二是没有抢占的机制,无法中断一个正在执行的程序。所以只能进行合作式调度。由浏览器分配执行时间片(通过requestIdleCallback
实现),react按照约定在这个时间内执行任务,时间到则将控制权还给浏览器。
requestIdleCallback API
确定一个合理的运行时长,然后在合适的检查点检测是否超时(比如每执行一个小任务),如果超时就停止执行,将控制权交还给浏览器。
1 | window.requestIdleCallback( |
第一个参数表明,如果浏览器处理完用户输入事件、JS执行、requestAnimation调用、布局Layout、绘制Paint等任务之后,若还有多余时间,就会去调用requestIdleCallback的回调。但是在浏览器繁忙的时候,可能不会有盈余时间,这时候requestIdleCallback
回调可能就不会被执行。 为了避免饿死,可以通过requestIdleCallback的第二个参数指定一个超时时间。
捕获异常
错误边界是什么?
错误边界是一种react组件,这种组件可以捕获发生在其子组件树任何位置的JS错误,并打印这些错误,同时展示降级UI,而不会渲染那些发生崩溃的子组件树。
错误边界在渲染、生命周期方法和整个组件树的构造函数中捕获错误。
形成错误边界的两个条件:
- 使用static getDerivedStateFromError(),抛出错误后,使用此方法渲染备用UI
- 使用componentDidCatch(),抛出错误后,使用此方法打印错误信息
错误边界使用
1 | class ErrorBoundary extends React.Component { |
然后就可以把自身组件作为错误边界的子组件,如下:
1 | <ErrorBoundary> |
try catch
错误边界无法捕获到下面这些情况的异常:
- 事件处理
- 异步代码
- 服务端渲染
- 自身抛出来的错误
对于错误边界无法捕获的异常,是因为其不会在渲染期间触发,并不会导致渲染出现问题,这种情况可以使用js
的try...catch...
语法。
diff
react16以前,react是直接递归渲染vdom的,setState触发重新渲染后,对比渲染出的新旧vdom,对差异部分进行dom更新操作
react16以后,为了优化性能,会先把vdom转换成fiber,也就是从树结构转换成链表,然后再进行渲染。整体的渲染流程分成了两个阶段:
- render阶段:从vdom转换成fiber,并且对需要dom操作的节点打上effectTag标记
- commit阶段:对有effectTag标记的fiber节点进行dom操作,并执行所有的effect副作用函数
从 vdom 转成 fiber 的过程叫做 reconcile(调和),这个过程是可以打断的,由 scheduler 调度执行。
diff 算法作用在 reconcile 阶段:
第一次渲染不需要 diff,直接 vdom 转 fiber。再次渲染的时候,会产生新的 vdom,这时候要和之前的 fiber 做下对比,决定怎么产生新的 fiber,对可复用的节点打上修改的标记,剩余的旧节点打上删除标记,新节点打上新增标记。
diff 算法的目的就是对比两次渲染结果,找到可复用的部分,然后剩下的该删除删除,该新增新增。
diff具体流程:
比如父节点下有 A、B、C、D 四个子节点,那渲染出的 vdom 就是这样的:
经过 reconcile 之后,会变成这样的 fiber 结构:
如果再次渲染的时候,渲染出了 A、C、B、E 的 vdom,这时候怎么处理呢?
再次渲染出 vdom 的时候,也要进行 vdom 转 fiber 的 reconcile 阶段,但是要尽量能复用之前的节点。那怎么复用呢?一一对比下不就行了?先把之前的 fiber 节点放到一个 map 里,key 就是节点的 key:
然后每个新的 vdom 都去这个 map 里查找下有没有可以复用的,找到了的话就移动过来,打上更新的 effectTag:
这样遍历完 vdom 节点之后,map 里剩下一些,这些是不可复用的,那就删掉,打上删除的 effectTag;如果 vdom 中还有一些没找到复用节点的,就直接创建,打上新增的 effectTag。
这样就实现了更新时的 reconcile,也就是上面的 diff 算法。其实核心就是找到可复用的节点,剩下的旧节点删掉,新节点新增。
但有的时候可以再简化一下,比如上次渲染是 A、B、C、D,这次渲染也是 A、B、C、D,那直接顺序对比下就行,没必要建立 map 再找。
diff算法的两次遍历
第一轮遍历**(一一对比)**,一一对比 vdom 和老的 fiber,如果可以复用就处理下一个节点,否则就结束遍历。如果所有的新的 vdom 处理完了,那就把剩下的老 fiber 节点删掉就行。如果还有 vdom 没处理,那就进行第二次遍历:
第二轮遍历**(map)**,把剩下的老 fiber 放到 map 里,遍历剩下的 vdom,从 map 里查找,如果找到了,就移动过来。第二轮遍历完了之后,把剩余的老 fiber 删掉,剩余的 vdom 新增。
找到可复用节点后,如何移动呢?
新的 vnode 数组中记录的顺序就是目标的顺序。把对应的节点按照新 vnode 数组的顺序来移动就好了。
查找新的 vnode 在旧的 vnode 数组中的下标,如果找到了的话,说明对应的 dom 就是可以复用的,先 patch 一下,然后移动。
移动的话判断下标(当前下标)是否在 lastIndex(当前节点在老集合中的下标) 之后,如果本来就在后面,那就不用移动,更新下 lastIndex 就行。
如果下标在 lastIndex 之前,说明需要移动,移动到的位置前面分析过了,就是新 vnode 数组 i-1 的后面。这样,我们就完成了 dom 节点的复用和移动。
双端diff(vue2 和 vue3采用的思想,react是单端diff)
双端 diff 是头尾指针向中间移动的同时,对比头头、尾尾、头尾、尾头是否可以复用,如果可以的话就移动对应的 dom 节点。
如果头尾没找到可复用节点就遍历 vnode 数组来查找,然后移动对应下标的节点到头部。最后还剩下旧的 vnode 就批量删除,剩下新的 vnode 就批量新增。
为什么react不采用双端的思想呢?
因为react的fiber链表不是双向链表,难以进行从后往前的回溯。fiber的单链表使得每个节点容易知道它的父节点、第一个子元素节点和兄弟节点,但不知道它的前一个节点。
react与vue的异同
- 组件化
相同点:
react和vue都推崇组件化,通过将页面拆分成一个一个小的可复用单元来提高代码的复用率和开发效率。在开发时react和vue有相同的套路,比如都有父子组件传参,都有数据状态管理,都有前端路由等。
不同点:
React推荐的做法是JSX + inline style, 也就是把 HTML 和 CSS 全都写进 JavaScript 中,即 all in js;
Vue 推荐的做法是 template 的单文件组件格式(简单易懂,从传统前端转过来易于理解),即 html,css,JS 写在同一个文件(vue也支持JSX写法)
- 虚拟DOM
相同点:
生成VDOM树,每次UI更新时,总会根据render重新生成最新的VNode,然后跟以前缓存起来老的VNode进行比对,再使用Diff算法(框架核心)去真正更新真实DOM。
不同点:
react 会自顶向下全diff。vue会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
react使用单指针比较,vue使用双指针比较。
- 数据驱动视图
不同点:
vue的数据驱动是通过MVVM框架实现的。MVVM框架主要包含3个部分:model、view和 viewModel。Model:指的是数据部分,对应到前端就是javascript对象。View:指的是视图部分,对应前端就是dom。ViewModel:就是连接视图与数据的中间件。ViewModel是实现数据驱动视图的核心,当数据变化的时候,ViewModel能够监听到这种变化,并及时的通知view做出修改。同样的,当页面有事件触发时,ViewModel也能够监听到事件,并通知model进行响应。ViewModel就相当于一个观察者,监控着双方的动作,并及时通知对方进行相应的操作。
React通过setState实现数据驱动视图,通过setState来引发一次组件的更新过程从而实现页面的重新渲染(除非shouldComponentUpdate返回false)。
组件拆分
视图组件
主要功能是用于显示信息,并通过回调发送用户输入。
特点:
- 将属性分发给子元素。
- 拥有将数据从子元素转发到父组件的回调。
- 通常是函数组件,但如果为了性能,它们需要绑定回调,则可能是类。
- 一般不使用生命周期方法,性能优化除外。
- 不直接存储状态,除了以 UI 为中心的状态,例如动画状态。
- 不使用 refs 或直接与 DOM 进行交互(因为 DOM 的改变意味着状态的改变)。
- 不修改环境。它们不应该直接将动作发送给 redux 的 store 或者调用 API 等。
- 不使用 React 上下文。
场景:
- 有 DOM 标记或者样式。
- 有像列表项这样重复的部分。
- 有“看起来”像一个盒子或者区域的内容。
- JSX 的一部分仅依赖于单个对象作为输入数据。
- 有一个具有不同区域的大型展示组件。
控制组件
主要功能是存储与部分输入相关的状态
特点:
- 可以存储状态(当与部分输入相关时)。
- 可以使用 refs 和与 DOM 进行交互。
- 可以使用生命周期方法。
- 通常没有任何样式,也没有 DOM 标记。
场景:
- 将部分输入存储在状态中。
- 通过 refs 与 DOM 进行交互。
- 某些部分看起来像原生控件 —— 按钮,表单域等。
控制器
主要用于存放业务逻辑
特点:
- 存储某个状态。
- 有改变那个状态的动作,并可能引起副作用。
- 可能有一些订阅状态变更的方法,而这些变更不是由动作直接造成的。
- 可以接受类似属性的配置,或者订阅某个全局控制器的状态。
- 不依赖于任何 React API。
- 不与 DOM 进行交互,也没有任何样式。
场景:
- 组件有很多与部分输入无关的状态。
- 状态用于存储从服务器接收到的信息。
- 引用全局状态,如拖放或导航的状态。
容器组件
主要是将控制器连接到视图组件和控制组件的粘合剂
特点:
- 在组件状态中存储控制器实例。
- 通过展示组件和控制组件来渲染状态。
- 使用生命周期方法来订阅控制器状态的更新。
- 不使用 DOM 标记或样式(可能出现的例外是一些无样式的 div)。
- 通常由像 Redux 的
connect
这样的高阶函数生成。 - 可以通过上下文访问全局控制器(例如 Redux 的 store)。
场景:
- 一个
App
组件 - 由 Redux 的
connect
返回的组件。 - 由 MobX 的
observer
返回的组件。 - react-router 的
<Link>
组件(因为它使用上下文并影响环境)。
package.json和package-lock.json的区别
package.json的结构:
1 | { |
- name:项目名,也就是在使用npm init 初始化时取的名字,但是如果使用的是npm init -y 快速初始化的话,那这里的名字就是默认存放这个文件的文件名;
- version:版本号;
- private:希不希望授权别人以任何形式使用私有包或未发布的;
- scripts-serve:项目启动简写配置;
- scripts-build:打包操作简写配置;
- dependencies:指定了项目运行时所依赖的模块;
- devDependencies:指定项目开发时所需要的模块,也就是在项目开发时才用得上,一旦项目打包上线了,就将移除这里的第三方模块;
package-lock.json是在运行“npm install”时生成的一个文件,用于记录当前状态下项目中实际安装的各个package的版本号、模块下载地址、及这个模块又依赖了哪些依赖。
区别:
package.json指定所要下载的库的大版本号,而package-lock.json会在保证下载的库是大版本号的前提下的最新版本。
为什么有package.json还需要package-lock.json呢?
当node_modules文件夹不存在或被删除时,需要用到npm install重新下载全部依赖,通过package-lock.json可以直接标明下载地址和相关依赖,相对下载速度也更快,也不容易报错。
react与react native的区别
- 框架作用的平台不同
RN是react衍生出来的,两种框架都是用JSX语法开发的。但是RN主要是用来开发IOS和Android应用的,而React是将浏览器作为渲染平台的。
- 工作原理的区别
React通过操作DOM来渲染页面,而RN通过调用java api来渲染安卓组件,调用object c api来渲染IOS组件。
- 创建组件
编写react的时候,视图最终会被渲染为普通的HTML元素;而RN中的元素最终会被渲染为安卓或IOS中的组件。
react父子组件的渲染执行顺序
1 | parent constructor |
redux优缺点
优点:
- 写法固定,易于维护
- reducer中每次return的都是不可变对象,操作相对容易
缺点:
- 实现一个小改动需要触碰很多文件才能完成,也就是模板代码太多,使用不太方便。
- reducer是一个纯函数,无法执行异步操作,所以衍生出了例如redux-thunk、redux-sage等中间件,非常散乱。
react官方文档学习
不变性
场景:useState中的state是一个数组,更新的时候,可以用.slice去获取新的数据,然后更新这个新的数组,再用新的数组去setState。这么做的好处是什么呢?(也可以称为不变性的好处)
好处:
- 可以实现撤销重做,因为没有破坏历史数组
- 默认情况下,当父组件的state发生变化时,所有子组件都会自动重新渲染。这甚至包括未受变化影响的子组件。处于性能考虑,我们当然希望跳过重新渲染中不受影响的部分。不变性使得组件比较其数据是否已更改的成本非常低。
TODO:
- 可以在
memo
API 参考 中了解更多关于 React 如何选择何时重新渲染组件的信息。