特性:
React(一):React的特征及优势
ReactDOM.render(
<div>
<h1>{1+1}</h1> //JavaScript表达式由{}表示
</div> ,
document.getElementById('example')
);
虚拟DOM,高效速度快,跨浏览器兼容
React之所以速度快,是因为其独特的特征——虚拟DOM(Document Object Model)。为什么直接操作DOM很慢,而虚拟DOM很快,和浏览器的渲染机制有关。
组件
组件是react的核心,一个完整的react应用是由若干个组件搭建起来的,每个组件有自己的数据和方法,组件具体如何划分,需要根据不同的项目来确定。
组件的特征是可复用,可维护性高。
虚拟DOM可以看做一棵模拟了DOM树的JavaScript对象树。
既然原来 DOM 树的信息都可以用 JavaScript 对象来表示,反过来,你就可以根据这个用 JavaScript 对象表示的树结构来构建一棵真正的DOM树。
状态变更->重新渲染整个视图的方式可以稍微修改一下:用 JavaScript 对象表示 DOM 信息和结构,当状态变更的时候,重新渲染这个 JavaScript 的对象结构。当然这样做其实没什么卵用,因为真正的页面其实没有改变。
但是可以用新渲染的对象树去和旧的树进行对比,记录这两棵树差异。记录下来的不同就是我们需要对页面真正的 DOM 操作,然后把它们应用在真正的 DOM 树上,页面就变更了。
这样就可以做到:视图的结构确实是整个全新渲染了,但是最后操作DOM的时候确实只变更有不同的地方。
这就是所谓的 Virtual DOM 算法。包括几个步骤:
它打开了函数式的UI编程的大门,即UI = f(data)这种构建UI的方式。 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
可以将JS对象渲染到浏览器DOM以外的环境中,也就是支持了跨平台开发,比如ReactNative。
保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
真实DOM的属性很多,创建DOM节点开销很大
虚拟DOM只是普通JavaScript对象,描述属性并不需要很多,创建开销很小
复杂视图情况下提升渲染性能(操作dom性能消耗大,减少操作dom的范围可以提升性能)
缺点:
无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。
(1)纯js(一般不用)
React.createElement('h1', {id:'myTitle'}, title)
(2)JSX
页面结构:
<h1 id='myTitle'>{title}h1>
<div id='example1'>div>
<div id='example2'>div>
JS:
ReactDOM.render()
即为react项目中index.js中的:
ReactDOM.render(<App />, document.querySelector('#root'));
而app.js一般汇总了各个模块,组成一棵完整的DOM树。
多点diff更新的JSX对象 newChildren为数组形式,但是和newChildren中每个组件进行比较的是current fiber,同级的Fiber节点是由sibling指针链接形成的单链表,即不支持双指针遍历。
即 newChildren[0]与fiber比较,newChildren[1]与fiber.sibling比较。
当Node节点的更新,虚拟DOM会比较两棵DOM树的区别,保证最小化的DOM操作,使得执行效率得到保证。
diff过程整体策略:深度优先,同层比较。
计算两棵树的常规算法是O(n^3)级别,所以需要优化深度遍历(先序) 的算法。React diff算法的时间复杂度为O(n)。
diff 策略
Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
React 分别对 tree diff
、component diff
以及 element diff
进行算法优化。
在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记:
在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比。如果有差异的话就记录到一个对象里面。
DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,
React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。
当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。
这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
React 是基于组件构建应用的,对于组件间的比较所采取的策略也是简洁高效。
如下图,当 component D 改变为 component G 时,即使这两个 component 结构相似,
一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点。
虽然当两个 component 是不同类型但结构相似时,React diff 会影响性能,但正如 React 官方博客所言:不同类型的 component 是很少存在相似 DOM tree 的机会,因此这种极端因素很难在实现开发过程中造成重大影响的。
当节点处于同一层级时,React diff 提供了三种节点操作,分别为:
INSERT_MARKUP
(插入)、MOVE_EXISTING
(移动)和 REMOVE_NODE
(删除)。
INSERT_MARKUP
,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。
MOVE_EXISTING
,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。
REMOVE_NODE
,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。
React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;
React 通过分层求异的策略,对 tree diff 进行算法优化;
React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;
React 通过设置唯一 key的策略,对 element diff 进行算法优化;
React 在v0.14之前是没有 ReactDOM 的,所有功能都包含在 React 里。从v0.14(2015-10)开始,React 才被拆分成React 和 ReactDOM。为什么要把 React 和 ReactDOM 分开呢?因为有了 ReactNative。React 只包含了 Web 和 Mobile 通用的核心部分,负责 Dom 操作的分到 ReactDOM 中,负责 Mobile 的包含在 ReactNative 中。
强烈建议使用 JSX 来编写你的 UI 组件。因为每个 JSX 元素都是调用 React.createElement() 的语法糖。
React 提供了几个用于操作元素的 API:
React 还提供了用于减少不必要嵌套的组件。
react-dom 的 package 提供了可在应用顶层使用的 DOM(DOM-specific)方法,如果有需要,你可以把这些方法用于 React 模型以外的地方。不过一般情况下,大部分组件都不需要使用这个模块。
React的生命周期从广义上分为三个阶段:挂载、渲染、卸载
因此可以把React的生命周期分为两类:挂载卸载过程和更新过程。
React的生命周期
在源码里Mount时和Update时都会触发,并且执行时机是同步的,在源码里就是简单的值的修改,所以也不会发起新的更新。
Hooks 与 React 生命周期的关系
Redux 入门教程(一):基本用法
Redux 入门教程(二):中间件与异步操作
dispatch
方法 被分派或“发送”到 store 中。Action 对应用程序状态的影响是通过使用一个 reducer 来定义的。 实际上,reducer 是一个函数,它以当前状态和 action 为参数。 它返回一个新的状态。
多个reducer可以被结合到一起,在结合的时候为每个reducer创建一个标识符,在action中可以使用该标识符获取对应reducer的state(即数据)。
redux是为了解决react组件间通信和组件间状态共享而提出的一种解决方案
1、组件间通信
由于connect后,各connect组件是共享store的,所以各组件可以通过store来进行数据通信,当然这里必须遵守redux的一些规范,比如遵守 view -> aciton -> reducer的改变state的路径
2、通过对象驱动组件进入生命周期
对于一个react组件来说,只能对自己的state改变驱动自己的生命周期,或者通过外部传入的props进行驱动。通过redux,可以通过store中改变的state,来驱动组件进行update
3、方便进行数据管理和切片
redux通过对store的管理和控制,可以很方便的实现页面状态的管理和切片。通过切片的操作,可以轻松的实现redo之类的操作
store.dispatch(action)
),Store 调用 Reducer 计算出新的 state ,若 state 产生变化,则调用监听函数重新渲染 View (store.subscribe(render)
)store.dispatch()
是 View 发出 Action 的唯一途径State 一旦有变化,Store 就会调用监听函数。
// 设置监听函数
store.subscribe(listener);
listener可以通过store.getState()
得到当前状态。如果使用的是 React,这时可以通过component.setState()
触发重新渲染 View。
function listerner() {
let newState = store.getState();
component.setState(newState);
}
它允许我们创建asynchronous actions。Redux-thunk 是所谓的redux-中间件,它必须在store的初始化过程中初始化。
import { createStore, combineReducers, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import noteReducer from './reducers/noteReducer'
import filterReducer from './reducers/filterReducer'
const reducer = combineReducers({
notes: noteReducer,
filter: filterReducer,
})
const store = createStore(
reducer,
composeWithDevTools(
applyMiddleware(thunk)
)
)
export default store
redux-thunk主要的功能就是可以让我们dispatch一个函数,而不只是普通的 Object。
通过 redux-thunk,可以定义action creators,这样它们就可以返回一个函数,其参数是 redux-store 的dispatch-method。 因此,可以创建异步action创建器,它们首先等待某个action完成,然后分派真正的action。
export const initializeNotes = () => {
return async dispatch => {
const notes = await noteService.getAll()
dispatch({
type: 'INIT_NOTES',
data: notes,
})
}
}
首先执行一个异步操作,然后调度改变store态的action。
一个 saga 就像是应用程序中一个单独的线程,它独自负责处理副作用。 redux-saga 是一个 redux 中间件,意味着这个线程可以通过正常的 redux action 从主应用程序启动,暂停和取消,它能访问完整的 redux state,也可以 dispatch redux action。
使用流程:
在sagas中定义saga:
import { delay } from 'redux-saga'
import { put, takeEvery, all, call } from 'redux-saga/effects'
export function* helloSaga() {
console.log('Hello Sagas!');
}
// Our worker Saga: 将执行异步的 increment 任务
// Sagas 被实现为 Generator functions,它会 yield 对象到 redux-saga middleware。
// 被 yield 的对象都是一类指令,指令可被 middleware 解释执行。
// 当 middleware 取得一个 yield 后的 Promise,middleware 会暂停 Saga,直到 Promise 完成。
//在上面的例子中,incrementAsync 这个 Saga 会暂停直到 delay 返回的 Promise 被 resolve,这个 Promise 将在 1 秒后 resolve。
// 一旦 Promise 被 resolve,middleware 会恢复 Saga 接着执行,直到遇到下一个 yield。
export function* incrementAsync() {
// yield delay(1000)
yield call(delay, 1000)
yield put({ type: 'INCREMENT' })
}
// Our watcher Saga: 在每个 INCREMENT_ASYNC action spawn 一个新的 incrementAsync 任务
// 用于监听所有的 INCREMENT_ASYNC action,并在 action 被匹配时执行 incrementAsync 任务
export function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
// 现在我们有了 2 个 Sagas,我们需要同时启动它们。为了做到这一点,我们将添加一个 rootSaga,负责启动其他的 Sagas。
// 这个 Saga yield 了一个数组,值是调用 helloSaga 和 watchIncrementAsync 两个 Saga 的结果。
// 意思是说这两个 Generators 将会同时启动。
export default function* rootSaga() {
yield all([
helloSaga(),
watchIncrementAsync()
])
}
在main.js中引入sagas,创建saga中间件:
'INCREMENT_ASYNC'
这个action为前一步用takeEvery
创建的action,用于监听所有的 INCREMENT_ASYNC action,并在 action 被匹配时执行 incrementAsync 任务
import "babel-polyfill"
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import Counter from './Counter'
import reducer from './reducers'
import rootSaga from './sagas'
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)
const action = type => store.dispatch({type})
function render() {
ReactDOM.render(
<Counter
value={store.getState()}
onIncrement={() => action('INCREMENT')}
onDecrement={() => action('DECREMENT')}
onIncrementAsync = {() => action('INCREMENT_ASYNC')} />,
document.getElementById('root')
)
}
render()
store.subscribe(render)
take:yield take([‘LOGOUT’, ‘LOGIN_ERROR’])。意思是监听 2 个并发的 action
redux-saga中如果在非阻塞调用下(fork),那么遵循的是worker/watcher模式,通过take可以监听某个action是否被发起,此外通过take结合fork,可以实现takeEvery和takeLatest的效果。
takeEvery 允许多个 saga 实例同时启动。在某个特定时刻,尽管之前还有一个或多个saga 尚未结束,我们还是可以启动一个新的 saga 任务,
如果我们只想得到最新那个请求的响应(例如,始终显示最新版本的数据)。我们可以使用 takeLatest 辅助函数。
和 takeEvery 不同,在任何时刻 takeLatest 只允许一个 fetchData 任务在执行。并且这个任务是最后被启动的那个。 如果已经有一个任务在执行的时候启动另一个 fetchData ,那之前的这个任务会被自动取消。
工具函数 delay,这个函数返回一个延迟 1 秒再 resolve 的 Promise 我们将使用这个函数去 block(阻塞) Generator。
put对应的是middleware中的dispatch方法,参数是一个plain object,一般在异步调用返回结果后,接着执行put。select相当于getState,用于获取store中的相应部分的state。
function* authorize(user, password) {
try {
const token = yield call(Api.authorize, user, password)
yield put({type: 'LOGIN_SUCCESS', token})
yield call(Api.storeItem, {token})
return token
} catch(error) {
yield put({type: 'LOGIN_ERROR', error})
} finally {
if (yield cancelled()) {
// ... put special cancellation handling code here
}
}
}
优点:
(1)异步解耦:集中处理了所有的异步操作,异步接口部分一目了然
(2)dispatch 的参数依然是⼀个纯粹的 action (FSA),action是普通对象,这跟redux同步的action一模一样
(3)通过Effect,方便异步接口的测试,提供了各种case的测试⽅案,包括mock task,分⽀覆盖等等
(4)异常处理: 受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理
(5) 异步操作的流程是可以控制的,可以随时取消相应的异步操作。
缺点:
Connect 函数接受所谓的mapStateToProps函数作为它的第一个参数。 这个函数可以用来定义基于 Redux 存储状态的连接组件 的props。
const mapStateToProps = (state) => {
return {
notes: state.notes,
filter: state.filter,
}
}
const ConnectedNotes = connect(mapStateToProps)(Notes)
export default ConnectedNotes
Connect 函数的第二个参数可用于定义mapDispatchToProps ,它是一组作为props传递给连接组件的 action creator 函数。
const mapDispatchToProps = {
toggleImportanceOf,
}
const ConnectedNotes = connect(
mapStateToProps,
mapDispatchToProps
)(Notes)
现在这个组件可以通过它的props调用函数直接调用toggleImportanceOf action creator 定义的action:
当使用 connect 时,我们可以简单地这样做:
props.toggleImportanceOf(note.id)
不需要单独调用 dispatch 函数,因为 connect 已经将 toggleImportanceOf action creator 修改为包含 dispatch 的形式。
答案是,react是根据useState出现的顺序来定的。
鉴于此,react规定我们必须把hooks写在函数的最外层,不能写在ifelse等条件语句当中,来确保hooks的执行顺序一致。
https://zh-hans.reactjs.org/docs/hooks-rules.html
// hook1: const [count, setCount] = useState(0) — 拿到state1
{
memorizedState: 0
next : {
// hook2: const [name, setName] = useState('Star') - 拿到state2
memorizedState: 'Star'
next : {
null
}
}
}
// hook1 => Fiber.memoizedState
// state1 === hook1.memoizedState
// hook1.next => hook2
// state2 === hook2.memoizedState
所以如果把hook1放到一个if语句中,当这个没有执行时,hook2拿到的state其实是上一次hook1执行后的state(而不是上一次hook2执行后的)。这样显然会发生错误。
在react中通过currentRenderingFiber
来标识当前渲染节点,每个组件都有一个对应的fiber节点,用来保存组件的相关数据信息。
每次函数组件渲染时,currentRenderingFiber
就被赋值为当前组件对应的fiber,所以实际上hook是通过currentRenderingFiber
来获取状态信息的。
react hook允许我们在一个组件中多次使用hook。如下:
function App(){
const [num, setNum] = useState(0);
const [name, setName] = useState('ashen');
const [age, setAge] = useState(21);
}
currentRenderingFiber.memorizedState
中保存一条hook对应数据的单向链表。
const hookNum = {
memorizedState: null,
next: hookName
}
hookName.next = hookAge;
currentRenderingFiber.memorizedState.next = hookNum;
当函数组件渲染时,每执行到一个hook,就会将currentRenderingFiber.memorizedState
的指针向后移一下。这也是hook的调用顺序不能改变的原因(不能在条件语句中使用hook)
// ReactFiberHooks.js
export type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update | null,
queue: UpdateQueue | null,
next: Hook | null,
};
重点关注memoizedState和next
更新时:
使用一个数组来保存不同的state,和一个index指针指向当前的state。通过调用的顺序来指明中间值 x 保存在哪里。
深究useState的原理
let x = [];
let index = 0;
const myUseState = initial => {
let currentIndex = index;
x[currentIndex] = x[currentIndex] === undefined ? initial : x[currentIndex];
const setState = value=>{
x[currentIndex] = value;
render();
}
index += 1;
return [x[currentIndex],setState]
}
useState异步回调的问题:
当使用usestate对数据进行更新,并不能立刻获取到最新的数据。
React.PureComponent使用useState钩子更新状态是异步的。
同样,这里的问题不仅是异步的性质,而且函数更新状态的当前闭包和状态更新使用状态值这一事实将放映在下一次重新渲染中,现有的闭包不会受到影响,而是会创建新的闭包。 现在,在当前状态下,挂钩中的值是由现有的闭包获得的,并且在重新渲染时,将根据是否再次创建函数来更新闭包。
即使添加了一个setTimeout函数,尽管超时将在重新渲染的一段时间后运行,但setTimeout仍将使用其先去关闭而不是更新后的值。
解决方法:
对setState的解释:
对useState 的解释:
对象/数组:
在 Hook 中直接修改 state 的一个对象(或数组)属性的某个子属性或值,然后直接进行 set,不会触发重新渲染
在 class component ,setState 之后无论传给方法的是个什么值,都会触发重新渲染
Hook 使用了 JavaScript 的闭包机制,useEffect 会在每次渲染后都,默认情况下,它在第一次渲染之后和每次更新之后都会执行,但是你可以选择只在某些值发生变化时才调用。
useEffect有两个参数
useLayoutEffect调用时机和componentDidMount相同:
了解了 useEffect 可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,所以需要返回一个函数:
深入理解 React useLayoutEffect 和 useEffect 的执行时机
一句话总结:useLayoutEffect是在commit阶段的mutation完成后,在浏览器的layout阶段同步执行;而useEffect则是在commit阶段结束后,即浏览器layout完成后异步调用。
即useLayoutEffect比useEffect先执行。
useEffect(create, deps):
该 Hook 接收一个包含命令式、且可能有副作用代码的函数。在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
useLayoutEffect(create, deps):
其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
useEffect和useLayoutEffect的区别 带demo
useEffect 和 useLayoutEffect 的区别?
useEffect 在渲染时是异步执行,并且要等到浏览器将所有变化渲染到屏幕后才会被执行。
useLayoutEffect 在渲染时是同步执行,其执行时机与 componentDidMount,componentDidUpdate 一致
对于 useEffect 和 useLayoutEffect 哪一个与 componentDidMount,componentDidUpdate 的是等价的?
useLayoutEffect,因为从源码中调用的位置来看,useLayoutEffect的 create 函数的调用位置、时机都和 componentDidMount,componentDidUpdate 一致,且都是被 React 同步调用,都会阻塞浏览器渲染。
useEffect 和 useLayoutEffect 哪一个与 componentWillUnmount 的是等价的?
同上,useLayoutEffect 的 detroy 函数的调用位置、时机与 componentWillUnmount 一致,且都是同步调用。useEffect 的 detroy 函数从调用时机上来看,更像是 componentDidUnmount (注意React 中并没有这个生命周期函数)。
为什么建议将修改 DOM 的操作里放到 useLayoutEffect 里,而不是 useEffect?
可以看到在流程9/10期间,DOM 已经被修改,但但浏览器渲染线程依旧处于被阻塞阶段,所以还没有发生回流、重绘过程。由于内存中的 DOM 已经被修改,通过 useLayoutEffect 可以拿到最新的 DOM 节点,并且在此时对 DOM 进行样式上的修改,假设修改了元素的 height,这些修改会在步骤 11 和 react 做出的更改一起被一次性渲染到屏幕上,依旧只有一次回流、重绘的代价。
如果放在 useEffect 里,useEffect 的函数会在组件渲染到屏幕之后执行,此时对 DOM 进行修改,会触发浏览器再次进行回流、重绘,增加了性能上的损耗。
需要注意的是useContext和redux的作用是不同的!
useContext:解决的是组件之间值传递的问题
redux:是应用中统一管理状态的问题
但通过和useReducer的配合使用,可以实现类似Redux的作用。
在组建外部建立一个Context ->
import React, { createContext, useContext } from "react";
import ReactDOM from "react-dom";
// 全局创建Context
const TestContext= createContext({});
const Navbar = () => {
const { username } = useContext(TestContext)
return (
<div className="navbar">
<p>{username}</p>
</div>
)
}
const Messages = () => {
const { username } = useContext(TestContext)
return (
<div className="messages">
<p>1 message for {username}</p>
</div>
)
}
function App() {
return (
<TestContext.Provider
value={{
username: 'superawesome',
}}
>
<div className="test">
<Navbar />
<Messages />
</div>
<TestContext.Provider/>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
TestContext.Provider
提供了一个Context对象,这个对象是可以被子组件共享的;
在子组件内,使用 useContext() 钩子函数用来引入Context对象,从中获取username属性(解构赋值)。
2.Context的作用就是对它所包含的组件树提供全局共享数据的一种技术。
Context.Provider重新渲染的时候,它所有的子组件都被重新渲染了,比如上面例子中子组件有Header和Content,Content作为Consumer之一重画没问题,但是Header不是Consumer,也不依赖于Context的value,根本没有必要重画;Context.Provider说到底还是组件,也按照组件基本法来办事,当value发生变化时,它也可以不引发子组件的渲染,前提是,子组件作为一个属性(this.props.children)也要保持不变才行
const [state, dispatch] = useReducer(reducer, initState);
第一个参数:reducer函数。第二个参数:初始化的state。返回值为最新的state和dispatch函数(用来触发reducer函数,计算对应的state)。按照官方的说法:对于复杂的state操作逻辑,嵌套的state的对象,推荐使用useReducer。
好处:
使用场景:
官方文档
这一次彻底搞定 useReducer - useContext使用
useMemo与useCallback使用指南
在hooks出来之后,我们能够使用function的形式来创建包含内部state的组件。但是,使用function的形式,失去了shouldComponentUpdate,我们无法通过判断前后状态来决定是否更新。而且,在函数组件中,react不再区分mount和update两个状态,这意味着函数组件的每一次调用都会执行其内部的所有逻辑,那么会带来较大的性能损耗。因此useMemo 和useCallback就是解决性能问题的杀手锏。
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;
function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;
useCallback和useMemo的参数跟useEffect一致,他们之间最大的区别有是useEffect会用于处理副作用,而前两个hooks不能。
useMemo和useCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数。
共同作用:
1.仅 依赖数据 发生变化, 才会重新计算结果,也就是起到缓存的作用。
两者区别:
1.useMemo
计算结果是 return 回来的值, 主要用于 缓存计算结果的值 ,应用场景如: 需要 计算的状态
2.useCallback
计算结果是 函数, 主要用于 缓存函数,应用场景如: 需要缓存的函数;因为函数式组件在每次任何一个 state 的变化时,整个组件 都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。
注意: 不要滥用,会造成性能浪费,react中减少render就能提高性能,所以这个仅仅只针对缓存能减少重复渲染时使用和缓存计算结果。
React.memo
React.memo 和 React.PureComponent 类似, React.PureComponent 在类组件中使用,而React.memo 在函数组件中使用
memo针对一个(函数)组件的渲染是否重复执行:
在子组件不需要父组件的值和函数的情况下,使用memo函数包裹子组件即可。
默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。
https://react-redux.js.org/api/hooks
这就允许所有组件对 redux-store 的状态进行更改。
useHistory
useRouteMatch
useRef官方文档
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。
ref.current 不可以作为其他 hooks(useMemo, useCallback, useEffect)依赖项;ref.current 的值发生变更并不会造成 re-render, Reactjs 并不会跟踪 ref.current 的变化。
和普通变量的区别:
普通变量在每次渲染后都会重新赋值,而useRef会保留上一次的返回值
ref转发
在下面的示例中,FancyButton 使用 React.forwardRef 来获取传递给它的 ref,然后转发到它渲染的 DOM button:
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
这样,使用 FancyButton 的组件可以获取底层 DOM 节点 button 的 ref ,并在必要时访问,就像其直接使用 DOM button 一样。
以下是对上述示例发生情况的逐步解释:
。
,将其指定为 JSX 属性。例子:
使用useRef hook创建博客表单的ref;
然后传递给Togglable wrapper组件;
在Togglable组件中使用forwardRef
获取ref,并使用useImperativeHandle
将ref绑定到toggleVisibility
方法上;
这样就可以在Togglable的父组件BlogForm中使用blogFormRef.current.toggleVisibility()
来访问Togglable组件中的toggleVisibility
方法,在表单提交的时候改变表单的显隐状态。
组件随着 count 的值的改变重新渲染,每次 count 改变,DOM 重新渲染之后,createRef 就会重新创建,生成一个新的引用地址,新的 ref 初始值为 null,页面渲染时被赋予当前 count 的值。
但是 useRef 一旦被创建,在组件的整个生命周期中,react 都会给它保持同一个引用,useRef 返回的 {current: xxx} 对象也将在整个生命周期中保持同一个值,除非手动改变 current 的值。
所以 refByCreateRef count 会随着 count 改变,但是 refByUseRef count 将始终保持在第一次渲染时的值。
createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。
当 useRef 的 current 发生变化时,页面不会收到通知,也就是说更改 .current 属性不会导致页面重新渲染,因为引用地址始终是同一个。
function Example () {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count)
console.log(prevCount, count, '之前的状态和现在的状态')
return (
<div>
<div>{count}</div>
<button onClick={() => {setCount(count+1)}}>+</button>
</div>
)
}
function usePrevious (value) {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
在状态中保存先前属性,紧接着比对先前属性和当前属性。如果不一样就更新状态,实现属性和状态的同步。在设置完新的状态后,react不会继续当前的渲染过程,而是会重新调度一次渲染,所以开销并不是很昂贵。
function ScrollView({row}) {
const [isScrollingDown, setIsScrollingDown] = useState(false);
const [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row changed since last render. Update isScrollingDown.
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
没有hooks之前的问题:
hooks带来的好处:
简洁: React Hooks解决了HOC和Render Props的嵌套问题,更加简洁
解耦: React Hooks可以更⽅便地把 UI 和状态分离,做到更彻底的解耦
组合: Hooks 中可以引⽤另外的 Hooks形成新的Hooks,组合变化万千
函数友好: React Hooks为函数组件⽽⽣,从⽽解决了类组件的⼏⼤问题:
状态不同步:不好用的useEffect,函数的运行是独立的,每个函数都有一份独立的作用域。函数的变量是保存在运行时的作用域里面,当我们有异步操作的时候,经常会碰到异步回调的变量引用是之前的,也就是旧的(这里也可以理解成闭包)。
代码从主动式变成响应式:必须把深入理解useEffect和useCallback这些api的第二个参数的作用;useEffect依赖某个函数的不可变性,这个函数的不可变性又依赖于另一个函数的不可变性,这样便形成了一条依赖链。一旦这条依赖链的某个节点意外地被改变了,你的useEffect就被意外地触发了
额外的学习成本(Functional Component 与Class Component之间的困惑)
写法上有限制(不能出现在条件、循环中),并且写法限制增加了重构成本
破坏了PureComponent、React.memo浅⽐较的性能优化效果(为了取最新的props和state,每次render()都要重新创建事件处函数)
在闭包场景可能会引⽤到旧的state、props值
内部实现上不直观(依赖⼀份可变的全局状态,不再那么“纯”)
React.memo并不能完全替代shouldComponentUpdate(因为拿不到state change,只针对 props change)
React-router路由基本原理
通过在父组件引入的子组件中传递一个函数并传参,子组件去触发这个函数更改参数完成数据更新
const Blogs = ( { user } ) => {
const blogs = useSelector(state => state.blogs)
const dispatch = useDispatch()
const addLikes = (id) => {
// ...
}
const deleteBlog = (id) => {
// ...
}
return (
<div id='showblogs'>
{blogs.sort((a, b) => parseFloat(b.likes) - parseFloat(a.likes)).map(blog =>
<Blog key={blog.id} blog={blog} user={user.username}
addLikes={() => addLikes(blog.id)} deleteBlog={() => deleteBlog(blog.id)}/>
)}
</div>
)
}
Refs适用于父子组件的通信,Refs提供了一种方式,允许我们访问DOM节点或在render方法中创建的React元素,在某些情况下,需要在典型数据流之外强制修改子组件,被修改的子组件可能是一个React组件的实例,也可能是一个DOM元素,渲染组件时返回的是组件实例,而渲染DOM元素时返回是具体的DOM节点,React提供的这个ref属性,表示为对组件真正实例的引用,其实就是ReactDOM.render()返回的组件实例。
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以
然而,useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。
这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: …} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。
请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
pubsub-js
// event-bus.js
var PubSub = function() {
this.handlers = {};
}
PubSub.prototype = {
constructor: PubSub,
on: function(key, handler) { // 订阅
if(!(key in this.handlers)) this.handlers[key] = [];
if(!this.handlers[key].includes(handler)) {
this.handlers[key].push(handler);
return true;
}
return false;
},
once: function(key, handler) { // 一次性订阅
if(!(key in this.handlers)) this.handlers[key] = [];
if(this.handlers[key].includes(handler)) return false;
const onceHandler = (...args) => {
handler.apply(this, args);
this.off(key, onceHandler);
}
this.handlers[key].push(onceHandler);
return true;
},
off: function(key, handler) { // 卸载
const index = this.handlers[key].findIndex(item => item === handler);
if (index < 0) return false;
if (this.handlers[key].length === 1) delete this.handlers[key];
else this.handlers[key].splice(index, 1);
return true;
},
commit: function(key, ...args) { // 触发
if (!this.handlers[key]) return false;
console.log(key, "Execute");
this.handlers[key].forEach(handler => handler.apply(this, args));
return true;
},
}
export default new PubSub();
Copy
<!-- 子组件 -->
import React from "react";
import eventBus from "./event-bus";
class Child extends React.PureComponent{
render() {
return (
<>
<div>接收父组件的值: {this.props.msg}</div>
<button onClick={() => eventBus.commit("ChangeMsg", "Changed Msg")}>修改父组件的值</button>
</>
)
}
}
export default Child;
Copy
<!-- 父组件 -->
import React from "react";
import Child from "./child";
import eventBus from "./event-bus";
class Parent extends React.PureComponent{
constructor(props){
super(props);
this.state = { msg: "Parent Msg" };
this.child = React.createRef();
}
changeMsg = (msg) => {
this.setState({ msg });
}
componentDidMount(){
eventBus.on("ChangeMsg", this.changeMsg);
}
componentWillUnmount(){
eventBus.off("ChangeMsg", this.changeMsg);
}
render() {
return (
<div>
<Child msg={this.state.msg} ref={this.child} />
</div>
)
}
}
export default Parent;
如果需要在组件之间共享状态,可以使用useContext()。
接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的
当组件上层最近的
import React, { useContext } from "react";
import ReactDOM from "react-dom";
const TestContext= React.createContext({});
const Navbar = () => {
const { username } = useContext(TestContext)
return (
<div className="navbar">
<p>{username}</p>
</div>
)
}
const Messages = () => {
const { username } = useContext(TestContext)
return (
<div className="messages">
<p>1 message for {username}</p>
</div>
)
}
function App() {
return (
<TestContext.Provider
value={{
username: 'superawesome',
}}
>
<div className="test">
<Navbar />
<Messages />
</div>
<TestContext.Provider/>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
只要父组件的render了,那么默认情况下就会触发子组件的render过程,子组件的render过程又会触发它的子组件的render过程,一直到React元素(即jsx中的
React不能检测到你是否给子组件传了属性,所以它必须进行这个重渲染过程(术语叫做reconciliation)。
React提供了shouldComponentUpdate回调函数,让程序员根据情况决定是否决定是否要重render本组件。如果某个组件的shouldComponentUpdate总是返回false, 那么当它的父组件render了,会触发该组件的render过程,但是进行到shouldComponentUpdate判断时会被阻止掉,从而就不调用它的render方法了,它自己下面的组件的render过程压根也不会触发了。
PureComponent 是优化 React 应用程序最重要的方法之一,易于实施,只要把继承类从 Component 换成 PureComponent 即可,可以减少不必要的 render操作的次数,从而提高性能,而且可以少写 shouldComponentUpdate 函数,节省了点代码。
React.PureComponent 与 React.Component 几乎完全相同,但 React.PureComponent 通过props和state的浅对比来实现 shouldComponentUpate()。
当组件更新时,如果组件的 props 和 state 都没发生改变, render 方法就不会触发,省去 Virtual DOM 的生成和比对过程,达到提升性能的目的。具体就是 React 自动帮我们做了一层浅比较:
if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate = !shallowEqual(prevProps, nextProps) || !shallowEqual(inst.state, nextState);
}
shallowEqual 会比较 Object.keys(state | props) 的长度是否一致,每一个 key 是否两者都有,并且是否是一个引用,也就是只比较了第一层的值,确实很浅,所以深层的嵌套数据是对比不出来的。
不需要开发者自己实现shouldComponentUpdate,就可以进行简单的判断来提升性能。
可能会因深层的数据不一致而产生错误的否定判断,从而shouldComponentUpdate结果返回false,界面得不到更新。
例如:
在MainComponent中去修改arr时,ChildComponent并没有得到刷新。原因在于js使用的是引用赋值,新的对象简单引用了原始对象,改变新对象虽然影响了原始对象,但对象的地址还是一样,使用===比较的方式相等。而在PureComponent中,会被判定prop相等而不触发render()。
React 性能优化技巧总结
从render/渲染上入手:
什么时候会执行「render」?
render 函数会在两种场景下被调用:
- 状态更新时:继承了 React.Component 的 class 组件,即使状态没变化,只要调用了setState 就会触发 render;对函数式组件来说,状态值改变时才会触发 render 函数的调用。
- 父容器重新渲染时:无论组件是继承自 React.Component 的 class 组件还是函数式组件,一旦父容器重新 render,组件的 render 都会再次被调用。
在「render」过程中会发生什么?
Diffing
在此步骤中,React 将新调用的 render 函数返回的树与旧版本的树进行比较,这一步是 React 决定如何更新 DOM 的必要步骤。虽然 React 使用高度优化的算法执行此步骤,但仍然有一定的性能开销。
Reconciliation
基于 diffing 的结果,React 更新 DOM 树。这一步因为需要卸载和挂载 DOM 节点同样存在许多性能开销。
React.memo/useMemo/useCallback/pureComponent
避免组件不必要的渲染的方法有:React.memo 包裹的函数式组件,继承自 React.PureComponent 的 class 组件。
按需传递props
只传递组件用到的props
(?)
(?)
(?)
(?)
(?)
(?)
(?)
(?)
(?)
我们写的纯函数组件只负责处理展示,很多时候会发现,由于业务需求,组件需要被“增强”,例如响应浏览器事件等。如果只有一两个组件我们大可以全部重写为class形式,但如果有许多组件需要进行相似或相同的处理(例如都响应浏览器窗口改变这个事件)时,考虑到代码的复用性,很容易想到用函数处理,HOC也正是为了解决这样的问题而出现的。
一个高阶组件只是一个包装了另外一个 React 组件的 React 组件。返回的新组件拥有了输入组件所不具备的功能。这里提到的组件并不是组件实例,而是组件类,也可以是一个无状态组件的函数。
如果一个函数 接受一个或多个函数作为参数或者返回一个函数 就可称之为 高阶函数。
定义中的『包装』一词故意被定义的比较模糊,因为它可以指两件事情:
其中,渲染劫持可以:
概括的讲,高阶组件允许你做:
高阶组件有如下好处:
基本原理
HOC的基本原理可以写成这样:
const HOCFactory = (Component) => {
return class HOC extends React.Component {
render(){
return <Component {...this.props} />
}
}
}
很明显HOC最大的特点就是:接受一个组件作为参数,返回一个新的组件。
(1)重用代码。有时候很多React组件都需要公用同样一个逻辑,比如说React-Redux中容器组件的部分,没有必要让每个组件都实现一遍shouldComponentUpdate这些生命周期函数,把这部分逻辑提取出来,利用高阶组件的方式应用出去,就可以减少很多组件的重复代码。
(2)修改现有React组件的行为。有些现成的React组件并不是开发者自己开发的,来自于第三方,或者即便是我们自己开发的,但是我们不想去触碰这些组件的内部逻辑,这时候可以用高阶组件。通过一个独立于原有组件的函数,可以产生新的组件,对原有组件没有任何侵害。
React 中的高阶组件及其应用场景
什么是装饰者模式:在不改变对象自身的前提下在程序运行期间动态的给对象添加一些额外的属性或行为。
使用装饰者模式实现 AOP:
面向切面编程(AOP)和面向对象编程(OOP)一样,只是一种编程范式,并没有规定说要用什么方式去实现 AOP。
// 在需要执行的函数之前执行某个新添加的功能函数
Function.prototype.before = function(before = () => {}) {
return () => {
before.apply(this, arguments);
return this.apply(this, arguments);
};
}
// 在需要执行的函数之后执行某个新添加的功能函数
Function.prototype.after = function(after = () => {}) {
return () => {
const result = after.apply(this, arguments);
this.apply(this, arguments);
return result;
};
}
可以发现其实 before 和 after 就是一个 高阶函数,和高阶组件非常类似。
面向切面编程(AOP)主要应用在 与核心业务无关但又在多个模块使用的功能比如权限控制、日志记录、数据校验、异常处理、统计上报等等领域。
高阶组件存在的问题
React.forwardRef
回调的第二个参数ref
,可以将其作为常规 prop 属性传递给 HOC,例如 “forwardedRef
”,然后它就可以被挂载到被 HOC包裹的子组件上。function logProps(Component) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
const {forwardedRef, ...rest} = this.props;
// 将自定义的 prop 属性 “forwardedRef” 定义为 ref
return <Component ref={forwardedRef} {...rest} />;
}
}
// 注意 React.forwardRef 回调的第二个参数 “ref”。
// 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
// 然后它就可以被挂载到被 LogPros 包裹的子组件上。
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
反向继承不能保证完整的子组件树被解析
如果渲染 elements tree 中包含了 function 类型的组件的话,这时候就不能操作组件的子组件了。
无法清晰地标识数据的来源:当有多个HOC一同使用时,无法直接判断子组件的props是哪个HOC负责传递的。
重复命名的问题(固定的 props 可能会被覆盖):若父子组件有同样名称的props,或使用的多个HOC中存在相同名称的props,则存在覆盖问题,而且react并不会报错。当然可以通过规范命名空间的方式避免。
在react开发者工具中观察HOC返回的结构,可以发现HOC产生了许多无用的组件,加深了组件层级。
同时,HOC使用了静态构建,即当AppWithMouse被创建时,调用了一次withMouse中的静态构建。而在render中调用构建方法才是react所倡导的动态构建。与此同时,在render中构建可以更好的利用react的生命周期。
高阶组件带给我们极大方便的同时,我们也要遵循一些 约定:
props 保持一致
高阶组件在为子组件添加特性的同时,要尽量保持原有组件的 props 不受影响,也就是说传入的组件和返回的组件在 props 上尽量保持一致。
你不能在函数式(无状态)组件上使用 ref 属性,因为它没有实例
不要以任何方式改变原始组件 WrappedComponent
在高阶组件的内部对 WrappedComponent 进行了修改,一旦对原组件进行了修改,那么就失去了组件复用的意义,所以请通过 纯函数(相同的输入总有相同的输出) 返回新的组件
透传不相关 props 属性给被包裹的组件 WrappedComponent
不要在 render() 方法中使用高阶组件
因为 hoc 的作用是返回一个新的组件,如果直接在 render 中调用 hoc 函数,每次 render 都会生成新的组件。对于复用的目的来说,这毫无帮助,之前此外生成的旧组件因此被不断卸载。
使用 compose 组合高阶组件
使用 compose 组合高阶组件使用,可以显著提高代码的可读性和逻辑的清晰度。
包装显示名字以便于调试
高阶组件创建的容器组件在 React Developer Tools 中的表现和其它的普通组件是一样的。为了便于调试,可以选择一个显示名字,传达它是一个高阶组件的结果。
渲染属性(Render Props)
术语 “render prop” 是指一种简单的技术,用于使用一个值为函数的 prop 在 React 组件之间的代码共享。
通过函数回调的方式提供一个接口,用以渲染外部组件。
Render Props 的核心思想是,通过一个函数将class组件的state作为props传递给纯函数组件。
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
class Mouse extends React.Component {
static propTypes = {
render: PropTypes.func.isRequired
}
state = { x: 0, y: 0 }
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
})
}
render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
)
}
}
const App = () => (
<div style={{ height: '100%' }}>
<Mouse render={({ x, y }) => (
<h1>The mouse position is ({x}, {y})</h1>
)}/>
</div>
)
ReactDOM.render(<App/>, document.getElementById('root'))
分析:
从demo中很容易看到,新建的Mouse组件的render方法中返回了{this.props.render(this.state)}
这个函数,将其state(Mouse组件的state)作为参数传入其props.render
方法中,调用时直接取组件所需要的state即可。
React Fiber
可以理解为:
React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。
其中每个任务更新单元为React Element对应的Fiber节点。
在React15及以前,Reconciler采用递归的方式创建虚拟DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,造成卡顿。
为了解决这个问题,React16 将递归的无法中断的更新重构为异步的可中断更新, 由于曾经用于递归的虚拟DOM数据结构已经无法满足需要。于是,全新的Fiber架构应运而生。
将recocilation(递归diff),拆分成⽆数个⼩任务;它随时能够停⽌,恢复。停⽌恢复的时机取决于当前的⼀帧(16ms)内,还有没有⾜够的时间允许计算。
时间分片指的是一种将多个粒度小的任务放入一个时间切片(一帧)中执行的一种方案,在 React Fiber 中就是将多个任务放在了一个时间片中去执行。
Fiber包含三层含义:
- 作为架构来说,之前React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler。React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler。
- 作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件…)、对应的DOM节点等信息。
- 作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新…)。
每个Fiber节点有个对应的React element,多个Fiber节点是如何连接形成树呢?靠如下三个属性:
// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;
// Fiber对应组件的类型 Function/Class/Host...
this.tag = tag;
// key属性
this.key = key;
// 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
this.elementType = null;
// 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
this.type = null;
// Fiber对应的真实DOM节点
this.stateNode = null;
作为动态的工作单元,Fiber中如下参数保存了本次更新相关的信息
// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// 保存本次更新会造成的DOM操作
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
react如何刷新当前页面_前端工程师的自我修养:React Fiber 是如何实现更新过程可控的…
Fiber节点可以保存对应的DOM节点。相应的,Fiber节点构成的Fiber树就对应DOM树。
React使用 “双缓存” 来完成Fiber树的构建与替换——对应着DOM树的创建与更新。
在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。
current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
mount和update过程:
https://react.iamkasong.com/process/doubleBuffer.html#%E5%8F%8C%E7%BC%93%E5%AD%98fiber%E6%A0%91
fiberRootNode
(源码中叫fiberRoot
)和rootFiber
。其中fiberRootNode是整个应用的根节点,rootFiber是
所在组件树的根节点。 fiberRootNode
的current
会指向当前页面上已渲染内容对应Fiber树,即current Fiber树。此时没有挂载dom,current Fiber树为空;render阶段,根据组件返回的JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber
树。构建完后在commit阶段渲染到页面。fiberRootNode的current指针指向workInProgress Fiber树使其变为current Fiber 树。workInProgress Fiber
树。和mount时一样,workInProgress fiber
的创建可以复用current Fiber
树对应的节点数据(这个决定是否复用的过程就是Diff算法)。workInProgress Fiber
树在render阶段完成构建后进入commit阶段渲染到页面上。渲染完毕后,workInProgress Fiber
树变为current Fiber
树。
- 在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。
- 在update时,Reconciler将JSX与Fiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记。
当第一个小任务完成后,先判断这一帧是否还有空闲时间,没有就挂起下一个任务的执行,记住当前挂起的节点,让出控制权给浏览器执行更高优先级的任务。
在浏览器渲染完一帧后,判断当前帧是否有剩余时间,如果有就恢复执行之前挂起的任务。如果没有任务需要处理,代表调和阶段完成,可以开始进入渲染阶段。 这样完美的解决了调和过程一直占用主线程的问题。
通过上面这张图可以清楚的知道,浏览器一帧会经过下面这几个过程:
如何判断一帧是否有空闲时间的呢?
答案就是我们前面提到的 RIC (RequestIdleCallback) 浏览器原生 API,React 源码中为了兼容低版本的浏览器,对该方法进行了 Polyfill。
RIC 事件不是每一帧结束都会执行,只有在一帧的 16ms 中做完了前面 6 件事儿且还有剩余时间,才会执行。这里提一下,如果一帧执行结束后还有时间执行 RIC 事件,那么下一帧需要在事件执行结束才能继续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时。
当恢复执行的时候又是如何知道下一个任务是什么呢?
答案在前面提到的链表。在 React Fiber 中每个任务其实就是在处理一个 FiberNode 对象,然后又生成下一个任务需要处理的 FiberNode。
其实并不是每次更新都会走到提交(commit)阶段。当在调和过程中触发了新的更新,在执行下一个任务的时候,判断是否有优先级更高的执行任务,如果有就终止原来将要执行的任务,开始新的 workInProgressFiber 树构建过程,开始新的更新流程。这样可以避免重复更新操作。这也是在 React 16 以后生命周期函数 componentWillMount 有可能会执行多次的原因。
React合成事件一套机制:React并不是将click事件直接绑定在dom上面,而是采用事件冒泡的形式冒泡到document上面,然后React将事件封装给正式的函数处理运行和处理。
如果DOM上绑定了过多的事件处理函数,整个页面响应以及内存占用可能都会受到影响。React为了避免这类DOM事件滥用,同时屏蔽底层不同浏览器之间的事件系统差异,实现了一个中间层——SyntheticEvent。
原生绑定快于合成事件绑定。
1.React16新的生命周期弃用了componentWillMount、componentWillReceivePorps,componentWillUpdate
2.新增了getDerivedStateFromProps、getSnapshotBeforeUpdate来代替弃用的三个钩子函数(componentWillMount、componentWillReceivePorps,componentWillUpdate)
3.React16并没有删除这三个钩子函数,但是不能和新增的两个钩子函数(getDerivedStateFromProps、getSnapshotBeforeUpdate)混用。
注意:React17将会删除componentWillMount、componentWillReceivePorps,componentWillUpdate
4.React16新增了对错误处理的钩子函数(componentDidCatch)
见上方fiber起源/含义
属性 props 是外界传递过来的,状态 state 是组件本身的,状态可以在组件中任意修改