(PS:本文尚未更新完成。本文仅为个人观点或整理笔记,如有问题,望请指正)
设计理念
用javascript构建快速响应的大型web应用程序的首选方式。关键是快速响应。
为了解决快速响应的问题,面临两个问题:
? (1)I/O瓶颈
? 网络延迟前端无法解决,在网络延迟客观存在的情况下,React通过将人机交互研究的成果整合到真实UI中来降低用户对网络延迟的感知程度。
? (2)CPU瓶颈
? 由于js是单线程的,js同步执行会阻塞渲染,因此在遇到CPU密集型计算时,会导致页面卡顿,因此,为突破CPU瓶颈问题,react的做法是使用concurrent模式,启用时间切片,可以把长任务拆分到每一帧中——这个做法就是从React15更新到React16的最核心的思想的体现,也就是将同步的更新变为可中断的异步更新。
为什么用react框架(框架比原生环境有什么好处?<为什么要重构,除了学习原因>)
(1)最大限度避免直接操作DOM带来的性能问题,以及React的虚拟DOM可以在每次更新时对真实dom作最少的修改
(2)可以复用组件
(3)组件是数据驱动的,意味着组件维护自身对数据的响应就行了,容易维护,也就是说考虑把数据放在哪,而不是怎么更新dom结构
(4)组件化和模块化开发(组件化是UI层面上的拆分,模块化是逻辑层面上的拆分)
vDOM详述(包括diff原理)
? 简单来说就是用来映射真实DOM的一个js对象。虚拟DOM的核心思想是让开发者可以避免直接操作DOM,以及每次更新时只进行最小化的dom操作,以此来降低操作真实DOM带来的页面性能问题,比如重绘重排。
? 详细来说,React使用双缓存来完成VDOM树,也就是Fiber树的构建与替换,视图中对应的是current Fiber树,内存中正在构建的是workInProgress Fiber树,在内存中进行新旧Fiber树的比较,找出差异,复用current Fiber树的节点,只修改需要修改的节点。这个比较新旧Fiber树差异的算法就是diff算法。
? 比较两颗树的节点的差异,最优解也是O(n3)的算法,这在性能上是不能接受的。但是由于我们一般只在同层级DOM进行更新,所以在diff算法中,通过DFS遍历树的节点,但是只对同级元素进行比较,如果一个DOM节点在前后两次状态中跨越了层级,React不会尝试复用它,这是diff算法的第一个特点;第二个特点是如果节点类型不同,比如从div更新成了p,那么React会销毁该节点所在的子树,重新构建新状态对应的子树;第三个特点是,开发者可以为子元素指定key这个属性,用来表明哪些节点在更新中能够保持稳定,比如只是将两个元素位置对调,但是元素包含key属性,react在比较时会复用这两个节点,只是调整顺序,而不是销毁重建。
对Fiber数据结构的了解
-
背景:对DOM的操作非常影响性能,于是有了diff算法。尽管有了diff算法,但是在面对复杂的大型应用时,diff算法消耗的时间仍然很长。在React15中,使用diff算法对比差异的过程是stack reconciler,它是递归更新且不能中断的,这在渲染一个消耗性能的操作时仍然会导致卡顿(递归对比所需时间超过一帧的时间就会卡顿)。 -
Fiber是什么? Fiber的含义需要从多个角度解释:
- react的理念是构建快速响应的应用,因此,在react16中,抛弃了stack reconciler,而采用fiber reconciler,并在react17中加入了lane模型,这么做的目标就是实现异步的可中断的更新,Fiber就是Fiber reconciler架构中的核心数据结构。
- 作为静态的数据结构:每个Fiber对应一个React Element,保存了该组件的类型(函数组件/类组件/原生组件),对应的DOM节点信息等。
- 作为动态的工作单元:每个Fiber保存了本次更新中该组件改变的状态、要执行的工作(被删除/插入/更新…)
Fiber数据结构中核心的属性:
- 作为树结构节点的属性:return(指向父节点)、sibling(指向兄弟节点)、child(指向子节点)
- 作为双缓存更新策略中两棵Fiber树转换的桥梁:alternate
- 作为静态的数据结构的属性(没有全列出来):key、elementType(指向函数或类或DOM节点tagName)、stateNode(对应的真实DOM节点)
- 作为动态的工作单元:state/props状态改变相关信息,本次更新会造成的副作用(DOM操作)
- 保存调度优先级的属性:lanes和childLanes
setState
-
原理 -
多次setState -
异步setState
组件间消息传递最佳实践
- 父子:props;通过子组件的refs直接使用子组件实例上的方法
- 子父:props传递能够修改父状态的回调函数;利用事件冒泡,把子组件的事件委托给父组件(比如获取子组件的点击事件,进而修改父状态)
- 父子(深层):context
- 兄弟:父节点为桥梁
- 不相关组件:pubsub,redux|react-redux|全局变量
props改变后组件内部变化顺序
-
类组件 执行生命周期函数,顺序是 ① static getDerivedStateFromProps 这个方法是挂在类上的,this是undefined,这个函数的作用是从props中派生组件的状态 ② 然后执行类实例上的生命周期函数,shouldComponentUpdate return false的话中断更新,return true的话继续执行 ③ 执行render方法 ④ 在渲染前,执行getSnapshotBeforeUpdate,该函数返回值将传入componentDidUpdate ⑤ 进入虚拟DOM的diff算法,然后修改相应节点 ⑥ 触发componentDidUpdate ⑦ 渲染到页面中 -
函数式组件 执行函数,对于useEffect,useMemo,useCallback等钩子,如果依赖改变,就执行回调函数,否则跳过。return后进入虚拟DOM的diff算法,重新渲染到页面上。
生命周期
- 有哪些(见上图)
- 新增了哪些,删除了哪些,为什么删除
redux原理及横向库了解
redux的官方定义是一个可预测的Javascript应用状态管理容器。
核心原理是唯一数据源,数据源只读,只能通过纯函数修改数据源。相应的三个核心的概念是store,action,reducer。
(1)store可以理解为整个redux应用的所有状态的存储,或者说是全局的state,但是它没有setter,不能直接更改。它身上有三个重要的api:
store.getState()
store.dispatch(actionObj)
store.subscribe(fn)
(2)action,本质是一个包含type键的对象,实际使用过程中一般使用action creator函数,作用是返回一个action对象,比如
const Add = data=>({type:'ADD',data})
(3)通过action改变store维护的状态的桥梁就是reducer。它是一个纯函数,形式是根据之前的状态和action,进行无副作用的计算,返回一个新的状态。
(4)在redux中的数据流是,UI发出动作(比如click)->action描述发生了什么->reducer执行无副作用的计算->store更新->UI响应store的更新。(也可以独立于UI)
我在自己的项目中,使用的是react-redux的库。
-
这个库提供了一个Provider组件,作用是将state和dispatch(action)通过props传入Provider包含的所有组件,Provider的唯一属性是store -
react-redux的核心是connect函数,它接收mapStateToProps和mapDispatchToProps,返回一个函数,返回的函数接收一个组件,返回包装后的组件。作用是将state和dispatch(action)通过props传入组件。其中, 1)mapStateToProps的作用是将store维护的state根据组件的需要,传入部分state作为组件的props 2)mapDispatchToProps是包含组件需要用到的action对象的集合,可以是返回action的函数,也可以是一个object,它是action creator的集合,用于在组件内部向store dispatch action触发store的更新
- connect函数会在传入组件的componentDidMount中注册store状态变化的监听,在componentWillUnmount中注销
redux优缺点
(1)缺点:
? ① 复杂,冗余,一个功能要在reducer、action、actionType多个文件中写,加上js是弱类型语言,更加容易出bug
? ② 需要中间件来支持副作用操作
? ③ store的设计,在状态层级比较深的时候,修改起来比较麻烦和容易出问题
(2)优点:
? redux标榜的最大的优势是pure,也就是predictable,可预测。这在一些场景下有优势,比如,触发bug的时候,可以方便开发者根据action序列重现bug。再比如需要全局的状态,数据不是单方向流动等情况。
useState等钩子原理
(可以考虑从这个角度回答为什么useHooks只能放在函数的顶层作用域)
(1)[state,setState] = useState(initState)
var _state=[]
var _index=0
function useState(initState){
const current_index = _index
_state[current_index] = _state[current_index]===undefined?initState:_state[current_index]
const setState=(newValue)=>{
_state[current_index] = newValue
render()
}
_index++
return [state[current_index],setState]
}
function render(){
_index=0
ReactDOM.render(<App/>,document.getElementById('root'))
}
(2)useEffect(callback,deps)
useEffect 在依赖变化时,执行回调函数。这个变化,是「本次 render 和上次 render 时的依赖比较」。
我们需要:
- 存储依赖,上一次 render 的依赖
- 兼容多次调用,也是收纳盒的思路
- 比较依赖,执行回调函数
- (如果有)执行回调函数中return的回调函数,清除上一次的副作用
const lastDepsBox = []
const lastClearCallbackBox = []
var _index = 0
const useEffect = (callback,deps)=>{
const lastDeps = lastDepsBox[_index]
const changed =
!lastDeps
|| !deps
|| deps.some((dep,i)=>dep!==lastDeps[i])
if(changed){
lastDepsBox[_index] = deps
const lastClearCallback = lastClearCallbackBox[_index]
if(typeof lastClearCallback==='function'){
lastClearCallback()
}
lastClearCallbackBox[_index] = callback()
}
_index++
}
(3)[state,dispatch] = useReducer(reducer,initArgs[,initFn])
(ps<useReducer三个优势>:在某些场景下,useReducer 会比 useState 更适用,例如
? ① state 逻辑较复杂且包含多个子值
? ② 下一个 state 依赖于之前的 state 等。
? ③ 并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数
)
var memorizedState
const useReducer = (reducer,initArgs,initFn)=>{
var initState
if(typeof initFn === 'function'){
initState = initFn(initArgs)
}else{
initState = initArgs
}
function dispatch(action){
memorizedState = reducer(memorizedState,action)
render()
}
memorizedState = memorizedState || initState
return [memorizedState,dispatch]
}
用useReducer实现useState(我只能写出在一个组件中只能用一次的useState)
function useState(initState){
return useReducer((_,newState)=>newState,initState)
}
函数式组件与类组件的区别,优缺点
(1)性能上差异不大
(2)区别:
? 编写形式、状态管理、生命周期、调用方式、获取渲染的值、复用
? ① 状态管理和生命周期:类组件有自己的状态,函数组件也可以通过useState维护状态。类组件的生命周期是继承自React.Component的,因此函数组件没有生命周期,但是可以通过useEffect模拟。类组件和函数组件状态管理的方式不同,但是可以实现的功能大致相同。
? ② 调用方式:React内部调用方式,函数组件是直接执行,类组件是先实例化,再调用实例的生命周期方法。
? ③ 获取渲染的值:在react中,props和state都是不可变的,但是this永远是可变的,这会导致类组件可能渲染一个“过于新”的this.state或者this.props,比如在组件中注册了异步请求的回调函数,但是在回调执行前组件重新渲染了,那么回调函数是从当前组件的this上取得props和state(解决方法是箭头函数和bind)。但是函数组件的props不是从this上读取的,因为它根本没有this,它的props是保存在当前作用域的闭包中的。
? ④ 为了解决组件逻辑复用的问题,类组件应用高阶组件和render props的设计模式,相比之下函数组件逻辑复用更加清晰和优雅,可以用自定义的useXXX钩子来抽离和封装事务逻辑(比如可以写一个请求最新新闻的钩子)。
https://blog.csdn.net/yiyueqinghui/article/details/121278395(组件/逻辑复用的区别,有例子)
组件逻辑复用 HOC render props useDIY
用HOC、renderProps、useHooks复用鼠标移动逻辑:
//HOC export default withMouseMove(OriComponent)
/*
HOC的缺点
(1)难以溯源。如果原始组件A通过好几个HOC的构造,最终生成了组件B,这个就不知道哪个属性来自于哪个HOC,需要翻看每个HOC才知道各自做了什么事情,使用了什么属性
(2)props属性名的冲突。某个属性可能被多个HOC重复使用。
(3)静态构建。新的组件是在页面构建之前生成,先有组件,后生成页面
*/
import React, { Component } from 'react'
function withMouseMove(Comp){
return class extends Component{
state = {
x:0,y:0
}
handleMouseMove = (event)=>{
this.setState({
x:event.clientX,
y:event.clientY
})
}
render(){
const {x,y} = this.state
return (
<div style={{height:'100%',width:'100%'}} onMouseMove={this.handleMouseMove}>
<Comp
{...this.props}
mouse={{x,y}}
/>
</div>
)
}
}
}
function App({mouse:{x,y}}){
return (
<div style={{height:'100vh'}}>
{x},{y}
</div>
)
}
export default withMouseMove(App)
//render props 把OriComponent放在MouseMove组件的this.props.render函数中返回,ori使用Mouse传入的state作为自己的props //进行渲染
import React, { Component } from 'react'
class Mouse extends Component{
state = {x:0,y:0}
handleMouseMove = (event)=>{
this.setState({
x:event.clientX,
y:event.clientY
})
}
render(){
const {x,y} = this.state
return (
<div style={{height:'100vh',width:'100%'}} onMouseMove={this.handleMouseMove}>
{this.props.render({x,y})}
</div>
)
}
}
export default function App(){
return (
<Mouse render={({x,y})=>{
return (<>({x},{y})</>)
}}
/>
)
}
//usehooks 抽离和封装handleMouseMove逻辑,组件直接使用封装好的handleMouseMove和state
import {useState} from 'react'
function useMouse(){
const [{x,y},setPosition] = useState({x:0,y:0})
const handleMouseMove = (event)=>{
setPosition({
x: event.clientX,
y: event.clientY
})
}
return [{x,y},handleMouseMove]
}
export default function App(){
const [{x,y},handleMouseMove] = useMouse()
return (
<div style={{height:'100vh',width:'100%'}} onMouseMove={handleMouseMove}>
({x},{y})
</div>
)
}
错误控制
(0)错误边界、try…catch、window.onerror
(1)类组件:
static getDerivedStateFromError(error)
componentDidCatch(error,info)
? error:后代组件引发的错误,info:存储哪个组件引发了此错误的componentStack跟踪
(2)通用:
try…catch
window.onerror
(3)示例:
-
模拟一个Error组件 import {Component} from 'react'
export default class TestError extends Component{
componentDidMount(){
setTimeout(()=>{
throw new Error('something went wrong')
},2000)
}
render(){
return (
<div>error will happen soon</div>
)
}
}
-
HOC import React, { Component } from 'react'
export default function withErrorBoundary(Com){
return class extends Component{
state = {
hasError:false
}
static getDerivedStateFromError(error){
return {hasError:error}
}
componentDidCatch(error,errorInfo){
console.log('发送错误到服务器',error,errorInfo)
}
render(){
return this.state.hasError ?
<div>Error occured</div> :
<>
<Com {...this.props}/>
</>
}
}
}
//使用示例
const TestErrorWithErrorBoundary = withErrorBoundary(TestError)
-
RenderProps import React, { Component } from 'react'
export default class ErrorRenderProps extends Component {
state = {
hasError:false
}
static getDerivedStateFromError(error){
return {hasError:error}
}
componentDidCatch(error,errorInfo){
console.log('发送错误到服务器',error,errorInfo)
}
render() {
return this.state.hasError ?
<div>ErrorRenderProps</div> :
this.props.render()
}
}
//使用示例
<ErrorRenderProps render={()=><TestError/>}/>
-
try…catch import { useState,useEffect } from "react";
export default function ErrorTryCatch(){
const [hasError,setHasError] = useState(false)
useEffect(()=>{
setTimeout(()=>{
setHasError(true)
},2000)
},[])
try{
if(hasError){
throw new Error('error occurs')
}
}catch(error){
console.log(error,'sending to server...')
return (<div>something went wrong</div>)
}
return (<div>Error will occur soon</div>)
}
//使用示例 直接用
-
window.onerror //index.js 即在全局环境下设置window.onerror函数用于捕获全局的错误
window.onerror = function(error){
console.log('sending to server...',error)
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
ReactDOM.render(
<div>something went wrong</div>,
document.getElementById('root')
)
}
ReactDOM.render(
<TestError/>,
document.getElementById('root')
);
路由及路由传参
-
params传参 //路由配置:
<Route path='/Demo/:id' component={Demo} />
//导航:
<NavLink to={'/Demo/123456'} />
/*或*/this.props.history.push('/Demo/123456')
//取值:
this.props.match.params.id
/*
优势:刷新,参数依然存在
缺点:只能传字符串,并且,如果传的值太多的话,url会变得长而丑陋。
*/
-
query传参 //路由配置:
<Route path='/Demo' component={Demo} />
//导航:
<Link to={{pathname:'/Demo',query:{id:123456}}} />
/*或*/ this.props.history.push({pathname:'/Demo',query:{id:123456}})
//取值:
this.props.location.query.id
/*
优势:传参优雅,传递参数可传对象;
缺点:刷新地址栏,参数丢失(不管是hash方式,还是Browser模式都会丢失参数)
*/
-
state传参 //同query差不多,只是属性不一样,而且state传的参数不在url显示,query传的参数是公开的
//路由配置:
<Router path='/Demo' component={Demo} />
//导航:
<Link to={{pathname:'/Demo',state:{id:123456}}} />
/*或*/this.props.history.push({pathname:'/Demo',state:{id:123456}})
//取值:
this.props.location.state.id
/*
优势:传参优雅,传递参数可传对象
缺点:刷新地址栏,hash方式会丢失参数,Browser模式不会丢失参数
*/
-
search传参 //路由配置:
<Router path='/Demo' component={Demo} />
//导航:
<Link to='/Demo?id=123456&name=zy' />
/*或*/this.props.history.push({pathname:'/Demo',search:'?id=123456&name=zy')
//取值:
this.props.location.search...//从字符串上取出值 注意:search字符串的第一个字符为?
immutable是什么,原理,使用场景,优缺点
- 原理
是react 不可变数据结构的实现。它实现了完全的持久化数据结构,使用结构共享,所有的更新会返回新的值,但是内部结构是共享的,意义在于减少内存占用,以及在useMemo、useEffect等包含依赖项的钩子中,能够避免传递无效依赖,比如dep参数是一个动态生成的对象,尽管值一样,但是由于比较的是引用地址,所以依赖无效。
immutable data的更新机制:
(1)immutable data是一旦创建就不能更改的数据,对immutable对象的任何更新都会返回一个新的immutable对象
(2)immutable实现原理是持久化数据结构,也就是使用旧数据创建新数据时要保证旧数据不变且可用
(3)为保证深拷贝把所有节点复制一遍带来的性能问题,Immutable data使用共享数据结构,即如果对象树中一个节点变化,新生成的immutable对象树只修改该节点以及受它影响的父节点(逐层往上直到根节点) ,而其它节点与旧数据共享。
- 优点
(1)降低了mutable(可变)带来的复杂度,因为mutable意味着time和value是耦合的,尤其是在js这种弱类型语言中,这(mutable)可能会导致变量在过程中发生不可预计的变化,比如
function touchAndLog(touchFn) {
let data = { key: 'value' };
touchFn(data);
console.log(data.key);
}
(2)节省内存:immutable data是结构共享的,能够尽量复用内存
(3)由于每次数据一旦产生就不会再变化,因此可以保存在一个数组中,利于撤销/重做等功能的开发
(4)并发安全:由于数据天生不可变,因此不需要并发锁来保证数据在并发过程中不被更改
(5)利于函数式编程,因为纯函数就是只要输入一致,输出必然一致
- 缺点
新的api,增加了资源大小,容易与原生对象混淆
- 其他
Object.freeze和const为什么不能做不可变?因为它们只是浅比较
seamless-immutable.js 库的大小相对小很多
服务端渲染
React各种设计的具体使用场景
-
可中断的异步更新 -
高优先级任务 -
shouldComponentUpdate -
getSnapshot
|