Vue
與 React
兩個架構的粗略差別對比
Vue
React
Vue 的優勢包括:
- 模闆和渲染函數的彈性選擇
- 簡單的文法及項目建立
- 更快的渲染速度和更小的體積
React 的優勢包括:
- 更适用于大型應用和更好的可測試性
- 同時适用于 Web 端和原生 App
- 更大的生态圈帶來的更多支援和工具
相似之處
React 與 Vue 有很多相似之處,React 和 Vue 都是非常優秀的架構,它們之間的相似之處多過不同之處,并且它們大部分最棒的功能是相通的:如他們都是 JavaScript 的 UI 架構,專注于創造前端的富應用。不同于早期的 JavaScript 架構“功能齊全”,Reat 與 Vue 隻有架構的骨架,其他的功能如路由、狀态管理等是架構分離的元件。
- 兩者都是用于建立 UI 的 JavaScript 庫;
- 兩者都快速輕便;
- 都有基于元件的架構;
- 都是用虛拟 DOM;
- 都可放入單個 HTML 檔案中,或者成為更複雜 webpack 設定中的子產品;
- 都有獨立但常用的路由器和狀态管理庫;
- 它們之間的最大差別是 Vue 通常使用 HTML 模闆檔案,而 React 則完全是 JavaScript。Vue 有雙向綁定文法糖。
不同點
- Vue 元件分為全局注冊和局部注冊,在 react 中都是通過 import 相應元件,然後模版中引用;
- props 是可以動态變化的,子元件也實時更新,在 react 中官方建議 props 要像純函數那樣,輸入輸出一緻對應,而且不太建議通過 props 來更改視圖;
- 子元件一般要顯示地調用 props 選項來聲明它期待獲得的資料。而在 react 中不必需,另兩者都有 props 校驗機制;
- 每個 Vue 執行個體都實作了事件接口,友善父子元件通信,小型項目中不需要引入狀态管理機制,而 react 必需自己實作;
- 使用插槽分發内容,使得可以混合父元件的内容與子元件自己的模闆;
- 多了指令系統,讓模版可以實作更豐富的功能,而 React 隻能使用 JSX 文法;
- Vue 增加的文法糖 computed 和 watch,而在 React 中需要自己寫一套邏輯來實作;
- react 的思路是 all in js,通過 js 來生成 html,是以設計了 jsx,還有通過 js 來操作 css,社群的 styled-component、jss 等;而 vue 是把 html,css,js 組合到一起,用各自的處理方式,vue 有單檔案元件,可以把 html、css、js 寫到一個檔案中,html 提供了模闆引擎來處理。
- react 做的事情很少,很多都交給社群去做,vue 很多東西都是内置的,寫起來确實友善一些, 比如 redux 的 combineReducer 就對應 vuex 的 modules, 比如 reselect 就對應 vuex 的 getter 和 vue 元件的 computed, vuex 的 mutation 是直接改變的原始資料,而 redux 的 reducer 是傳回一個全新的 state,是以 redux 結合 immutable 來優化性能,vue 不需要。
- react 是整體的思路的就是函數式,是以推崇純元件,資料不可變,單向資料流,當然需要雙向的地方也可以做到,比如結合 redux-form,元件的橫向拆分一般是通過高階元件。而 vue 是資料可變的,雙向綁定,聲明式的寫法,vue 元件的橫向拆分很多情況下用 mixin。
社群活躍度
從兩者的 github 表現來看(資料取于 2019-09-16)

可以看出 vue 的 star 數量已經是前端架構中最火爆的。從維護上來看,react 是 facebook 在維護,而 vue 現階段雖然也有了團隊,但主要還是尤雨溪在維護貢獻代碼,并且阿裡巴巴開源的混合式架構 weex 也是基于 vue 的,是以我們相信 vue 未來将會得到更多的人和團隊維護。
根據不完全統計,包括餓了麼、簡書、高德、稀土掘金、蘇甯易購、美團、天貓、荔枝 FM、房多多、Laravel、htmlBurger 等國内外知名大公司都在使用 vue 進行新項目的開發和舊項目的前端重構工作。
使用 React 的公司 facebook、Twitter、INS、Airbnb、Yahoo、ThoughtWorks、螞蟻金服、阿裡巴巴、騰訊、百度、口碑、美團、滴滴出行、餓了麼、京東、網易等。
UI 生态
vue | react | |
---|---|---|
pc 端 | iview、element 等 | Ant Design、Materal-UI 等 |
h5 端 | 有贊 vant、mintui 等 | Ant Design Mobile、weui |
混合開發 | weexui、bui-weex | teaset、react-native-elements |
微信小程式 | iview、Weapp、zanui | iView Weapp、Taro UI |
無論您選擇React.js還是Vue.js,兩個架構都沒有相當大的差異,根據您的要求,這個決定是非常主觀的。如果您想将前端JavaScript架構內建到現有應用程式中,Vue.js是更好的選擇,如果您想使用JavaScript建構移動應用程式,React絕對是您的選擇。
react16
版本常見 api
react16
api
先來看一下 react 暴露出來的 API
const React = {
Children: {
map,
forEach,
count,
toArray,
only
},
createRef,
Component,
PureComponent,
createContext,
forwardRef,
Fragment: REACT_FRAGMENT_TYPE,
StrictMode: REACT_STRICT_MODE_TYPE,
unstable_AsyncMode: REACT_ASYNC_MODE_TYPE,
unstable_Profiler: REACT_PROFILER_TYPE,
createElement: __DEV__ ? createElementWithValidation : createElement,
cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
isValidElement: isValidElement,
version: ReactVersion,
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals
}
複制
Children
Children
這個對象提供了一堆幫你處理 props.children 的方法,因為 children 是一個類似數組但是不是數組的資料結構,如果你要對其進行處理可以用 React.Children 外挂的方法。
createRef
createRef
新的 ref 用法,React 即将抛棄
<divref="myDiv"/>
這種 string ref 的用法,将來你隻能使用兩種方式來使用 ref
class App extends React.Component {
constructor() {
this.ref = React.createRef()
}
render() {
return <div ref={this.ref} />
// or
return <div ref={node => (this.funRef = node)} />
}
}
複制
createContext
createContext
createContext
是官方定稿的 context 方案,在這之前我們一直在用的老的 context API 都是 React 不推薦的 API,現在新的 API 釋出,官方也已經确定在 17 大版本會把老 API 去除(老 API 的性能不是一般的差)。
新 API 的使用方法:
const { Provider, Consumer } = React.createContext('defaultValue')
const ProviderComp = (props) => (
<Provider value={'realValue'}>
{props.children}
</Provider>
)
const ConsumerComp = () => (
<Consumer>
{(value) => <p>{value}</p>}
</Consumber>
)
複制
react 生命周期
目前 react 16.8 +的生命周期分為三個階段,分别是挂載階段、更新階段、解除安裝階段
- 挂載階段:
: 執行個體化。constructor(props)
從staticgetDerivedStateFromProps
中擷取props
。state
渲染。render
: 完成挂載。componentDidMount
- 更新階段:
從 props 中擷取 state。staticgetDerivedStateFromProps
判斷是否需要重繪。shouldComponentUpdate
渲染。render
擷取快照。getSnapshotBeforeUpdate
渲染完成後回調。componentDidUpdate
- 解除安裝階段:
即将解除安裝。componentWillUnmount
- 錯誤處理:
從錯誤中擷取staticgetDerivedStateFromError
。state
捕獲錯誤并進行處理。componentDidCatch
class ExampleComponent extends react.Component {
// 構造函數,最先被執行,我們通常在構造函數裡初始化state對象或者給自定義方法綁定this
constructor() {}
//getDerivedStateFromProps(nextProps, prevState)用于替換 `componentWillReceiveProps` ,該函數會在初始化和 `update` 時被調用
// 這是個靜态方法,當我們接收到新的屬性想去修改我們state,可以使用getDerivedStateFromProps
static getDerivedStateFromProps(nextProps, prevState) {
// 新的鈎子 getDerivedStateFromProps() 更加純粹, 它做的事情是将新傳進來的屬性和目前的狀态值進行對比, 若不一緻則更新目前的狀态。
if (nextProps.riderId !== prevState.riderId) {
return {
riderId: nextProps.riderId
}
}
// 傳回 null 則表示 state 不用作更新
return null
}
// shouldComponentUpdate(nextProps, nextState),有兩個參數nextProps和nextState,表示新的屬性和變化之後的state,傳回一個布爾值,true表示會觸發重新渲染,false表示不會觸發重新渲染,預設傳回true,我們通常利用此生命周期來優化react程式性能
shouldComponentUpdate(nextProps, nextState) {
return nextProps.id !== this.props.id
}
// 元件挂載後調用
// 可以在該函數中進行請求或者訂閱
componentDidMount() {}
// getSnapshotBeforeUpdate(prevProps, prevState):這個方法在render之後,componentDidUpdate之前調用,有兩個參數prevProps和prevState,表示之前的屬性和之前的state,這個函數有一個傳回值,會作為第三個參數傳給componentDidUpdate,如果你不想要傳回值,可以傳回null,此生命周期必須與componentDidUpdate搭配使用
getSnapshotBeforeUpdate() {}
// 元件即将銷毀
// 可以在此處移除訂閱,定時器等等
componentWillUnmount() {}
// 元件銷毀後調用
componentDidUnMount() {}
// componentDidUpdate(prevProps, prevState, snapshot):該方法在getSnapshotBeforeUpdate方法之後被調用,有三個參數prevProps,prevState,snapshot,表示之前的props,之前的state,和snapshot。第三個參數是getSnapshotBeforeUpdate傳回的,如果觸發某些回調函數時需要用到 DOM 元素的狀态,則将對比或計算的過程遷移至 getSnapshotBeforeUpdate,然後在 componentDidUpdate 中統一觸發回調或更新狀态。
componentDidUpdate() {}
// 渲染元件函數
render() {}
// 以下函數不建議使用
UNSAFE_componentWillMount() {}
UNSAFE_componentWillUpdate(nextProps, nextState) {}
UNSAFE_componentWillReceiveProps(nextProps) {}
}
複制
react 版本 17 将棄用幾個類元件 API 生命周期:
componentWillMount
,
componentWillReceiveProps
和
componentWillUpdate
。
react 事件機制
簡單的了解 react 如何處理事件的,React 在元件加載(mount)和更新(update)時,将事件通過 addEventListener 統一注冊到 document 上,然後會有一個事件池存儲了所有的事件,當事件觸發的時候,通過 dispatchEvent 進行事件分發。
引用新手學習 react 迷惑的點(二)
- react 裡面綁定事件的方式和在 HTML 中綁定事件類似,使用駝峰式命名指定要綁定的 onClick 屬性為元件定義的一個方法{this.handleClick.bind(this)}。
- 由于類的方法預設不會綁定 this,是以在調用的時候如果忘記綁定,this 的值将會是 undefined。通常如果不是直接調用,應該為方法綁定 this,将事件函數上下文綁定要元件執行個體上。
綁定事件的四種方式
class Button extends react.Component {
constructor(props) {
super(props)
this.handleClick1 = this.handleClick1.bind(this)
}
//方式1:在構造函數中使用bind綁定this,官方推薦的綁定方式,也是性能最好的方式
handleClick1() {
console.log('this is:', this)
}
//方式2:在調用的時候使用bind綁定this
handleClick2() {
console.log('this is:', this)
}
//方式3:在調用的時候使用箭頭函數綁定this
// 方式2和方式3會有性能影響并且當方法作為屬性傳遞給子元件的時候會引起重渲問題
handleClick3() {
console.log('this is:', this)
}
//方式4:使用屬性初始化器文法綁定this,需要babel轉義
handleClick4 = () => {
console.log('this is:', this)
}
render() {
return (
<div>
<button onClick={this.handleClick1}>Click me</button>
<button onClick={this.handleClick2.bind(this)}>Click me</button>
<button onClick={() => this.handleClick3}>Click me</button>
<button onClick={this.handleClick4}>Click me</button>
</div>
)
}
}
複制
為什麼直接調用方法會報錯
class Foo extends React.Component {
handleClick() {
this.setState({ xxx: aaa })
}
render() {
return <button onClick={this.handleClick.bind(this)}>Click me</button>
}
}
複制
會被 babel 轉化成
React.createElement(
'button',
{
onClick: this.handleClick
},
'Click me'
)
複制
“合成事件”和“原生事件”
react 實作了一個“合成事件”層(
syntheticeventsystem
),這抹平了各個浏覽器的事件相容性問題。所有事件均注冊到了元素的最頂層-document 上,“合成事件”會以事件委托(
eventdelegation
)的方式綁定到元件最上層,并且在元件解除安裝(
unmount
)的時候自動銷毀綁定的事件。
react 元件開發
react 元件化思想
一個 UI 元件的完整模闆
import classNames from 'classnames'
class Button extends react.Component {
//參數傳參與校驗
static propTypes = {
type: PropTypes.oneOf(['success', 'normal']),
onClick: PropTypes.func
}
static defaultProps = {
type: 'normal'
}
handleClick() {}
render() {
let { className, type, children, ...other } = this.props
const classes = classNames(
className,
'prefix-button',
'prefix-button-' + type
)
return (
<span className={classes} {...other} onClick={() => this.handleClick}>
{children}
</span>
)
}
}
複制
函數定義元件(Function Component)
純展示型的,不需要維護 state 和生命周期,則優先使用
FunctionComponent
- 代碼更簡潔,一看就知道是純展示型的,沒有複雜的業務邏輯
- 更好的複用性。隻要傳入相同結構的 props,就能展示相同的界面,不需要考慮副作用。
- 打包體積小,執行效率高
import react from 'react'
function MyComponent(props) {
let { firstName, lastName } = props
return (
<div>
<img src="avatar.png" className="profile" />
<h3>{[firstName, lastName].join(' ')}</h3>
</div>
)
}
複制
會被 babel 轉義成
return React.createElement(
'div',
null,
React.createElement('img', { src: 'avatar.png', className: 'profile' }),
React.createElement('h3', null, [firstName, lastName].join(' '))
)
複制
createElement
函數對 key 和 ref 等特殊的 props 進行處理,并擷取
defaultProps
對預設 props 進行指派,并且對傳入的孩子節點進行處理,最終構造成一個
reactElement
對象(所謂的虛拟 DOM)。
reactDOM.render
将生成好的虛拟 DOM 渲染到指定容器上,其中采用了批處理、事務等機制并且對特定浏覽器進行了性能優化,最終轉換為真實 DOM。
那麼,
React.createElement
是在做什麼?看下相關部分代碼:
var ReactElement = function(type, key, ref, self, source, owner, props) {
var element = {
// This tag allow us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner
}
// ...
return element
}
ReactElement.createElement = function(type, config, children) {
// ...
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props
)
}
複制
ES6class
定義一個純元件( PureComponent
)
ES6class
PureComponent
元件需要維護 state 或使用生命周期方法,則優先使用
PureComponent
class MyComponent extends react.Component {
render() {
let { name } = this.props
return <h1>Hello, {name}</h1>
}
}
複制
PureComponent
PureComponent
Component
&
PureComponent
這兩個類基本相同,唯一的差別是
PureComponent
的原型上多了一個辨別,
shallowEqual
(淺比較),來決定是否更新元件,淺比較類似于淺複制,隻會比較第一層。使用
PureComponent
相當于省去了寫
shouldComponentUpdate
函數
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
}
複制
這是檢查元件是否需要更新的一個判斷,ctor 就是你聲明的繼承自
Component
or
PureComponent
的類,他會判斷你是否繼承自
PureComponent,如果是的話就
shallowEqual
比較 state 和 props。
React 中對比一個
ClassComponent
是否需要更新,隻有兩個地方。一是看有沒有
shouldComponentUpdate
方法,二就是這裡的
PureComponent
判斷
使用不可變資料結構 Immutablejs
Immutablejs
Immutable.js
是 Facebook 在 2014 年出的持久性資料結構的庫,持久性指的是資料一旦建立,就不能再被更改,任何修改或添加删除操作都會傳回一個新的
Immutable
對象。可以讓我們更容易的去處理緩存、回退、資料變化檢測等問題,簡化開發。并且提供了大量的類似原生 JS 的方法,還有
LazyOperation
的特性,完全的函數式程式設計。
import { Map } from 'immutable'
const map1 = Map({ a: { aa: 1 }, b: 2, c: 3 })
const map2 = map1.set('b', 50)
map1 !== map2 // true
map1.get('b') // 2
map2.get('b') // 50
map1.get('a') === map2.get('a') // true
複制
可以看到,修改 map1 的屬性傳回 map2,他們并不是指向同一存儲空間,map1 聲明了隻有,所有的操作都不會改變它。
ImmutableJS
提供了大量的方法去更新、删除、添加資料,極大的友善了我們操縱資料。除此之外,還提供了原生類型與
ImmutableJS
類型判斷與轉換方法:
import { fromJS, isImmutable } from 'immutable'
const obj = fromJS({
a: 'test',
b: [1, 2, 4]
}) // 支援混合類型
isImmutable(obj) // true
obj.size() // 2
const obj1 = obj.toJS() // 轉換成原生 `js` 類型
複制
ImmutableJS
最大的兩個特性就是:
immutable data structures
(持久性資料結構)與
structural sharing
(結構共享),持久性資料結構保證資料一旦建立就不能修改,使用舊資料建立新資料時,舊資料也不會改變,不會像原生 js 那樣新資料的操作會影響舊資料。而結構共享是指沒有改變的資料共用一個引用,這樣既減少了深拷貝的性能消耗,也減少了記憶體。
比如下圖:
左邊是舊值,右邊是新值,我需要改變左邊紅色節點的值,生成的新值改變了紅色節點到根節點路徑之間的所有節點,也就是所有青色節點的值,舊值沒有任何改變,其他使用它的地方并不會受影響,而超過一大半的藍色節點還是和舊值共享的。在
ImmutableJS
内部,構造了一種特殊的資料結構,把原生的值結合一系列的私有屬性,建立成
ImmutableJS
類型,每次改變值,先會通過私有屬性的輔助檢測,然後改變對應的需要改變的私有屬性和真實值,最後生成一個新的值,中間會有很多的優化,是以性能會很高。
高階元件( higher order component
)
higher order component
高階元件是一個以元件為參數并傳回一個新元件的函數。HOC 運作你重用代碼、邏輯和引導抽象。
function visible(WrappedComponent) {
return class extends Component {
render() {
const { visible, ...props } = this.props
if (visible === false) return null
return <WrappedComponent {...props} />
}
}
}
複制
上面的代碼就是一個 HOC 的簡單應用,函數接收一個元件作為參數,并傳回一個新元件,新組建可以接收一個 visible props,根據 visible 的值來判斷是否渲染 Visible。 最常見的還有 Redux 的 connect 函數。除了簡單分享工具庫和簡單的組合,HOC 最好的方式是共享 react 元件之間的行為。如果你發現你在不同的地方寫了大量代碼來做同一件事時,就應該考慮将代碼重構為可重用的 HOC。下面就是一個簡化版的 connect 實作:
export const connect = (
mapStateToProps,
mapDispatchToProps
) => WrappedComponent => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor() {
super()
this.state = {
allProps: {}
}
}
componentWillMount() {
const { store } = this.context
this._updateProps()
store.subscribe(() => this._updateProps())
}
_updateProps() {
const { store } = this.context
let stateProps = mapStateToProps
? mapStateToProps(store.getState(), this.props)
: {}
let dispatchProps = mapDispatchToProps
? mapDispatchToProps(store.dispatch, this.props)
: {}
this.setState({
allProps: {
...stateProps,
...dispatchProps,
...this.props
}
})
}
render() {
return <WrappedComponent {...this.state.allProps} />
}
}
return Connect
}
複制
代碼非常清晰,connect 函數其實就做了一件事,将
mapStateToProps
和
mapDispatchToProps
分别解構後傳給原元件,這樣我們在原元件内就可以直接用
props
擷取
state
以及
dispatch
函數了。
高階元件的應用
某些頁面需要記錄使用者行為,性能名額等等,通過高階元件做這些事情可以省去很多重複代碼。
日志打點
function logHoc(WrappedComponent) {
return class extends Component {
componentWillMount() {
this.start = Date.now()
}
componentDidMount() {
this.end = Date.now()
console.log(
`${WrappedComponent.dispalyName} 渲染時間:${this.end - this.start} ms`
)
console.log(`${user}進入${WrappedComponent.dispalyName}`)
}
componentWillUnmount() {
console.log(`${user}退出${WrappedComponent.dispalyName}`)
}
render() {
return <WrappedComponent {...this.props} />
}
}
}
複制
可用、權限控制
function auth(WrappedComponent) {
return class extends Component {
render() {
const { visible, auth, display = null, ...props } = this.props
if (visible === false || (auth && authList.indexOf(auth) === -1)) {
return display
}
return <WrappedComponent {...props} />
}
}
}
複制
表單校驗
基于上面的雙向綁定的例子,我們再來一個表單驗證器,表單驗證器可以包含驗證函數以及提示資訊,當驗證不通過時,展示錯誤資訊:
function validateHoc(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props)
this.state = { error: '' }
}
onChange = event => {
const { validator } = this.props
if (validator && typeof validator.func === 'function') {
if (!validator.func(event.target.value)) {
this.setState({ error: validator.msg })
} else {
this.setState({ error: '' })
}
}
}
render() {
return (
<div>
<WrappedComponent onChange={this.onChange} {...this.props} />
<div>{this.state.error || ''}</div>
</div>
)
}
}
}
複制
const validatorName = {
func: (val) => val && !isNaN(val),
msg: '請輸入數字'
}
const validatorPwd = {
func: (val) => val && val.length > 6,
msg: '密碼必須大于6位'
}
<HOCInput validator={validatorName} v_model="name"></HOCInput>
<HOCInput validator={validatorPwd} v_model="pwd"></HOCInput>
複制
HOC 的缺陷
- HOC 需要在原元件上進行包裹或者嵌套,如果大量使用 HOC,将會産生非常多的嵌套,這讓調試變得非常困難。
- HOC 可以劫持 props,在不遵守約定的情況下也可能造成沖突。
render props
render props
一種在 React 元件之間使用一個值為函數的 prop 共享代碼的簡單技術 具有 render prop 的元件接受一個函數,該函數傳回一個 React 元素并調用它而不是實作自己的渲染邏輯。
<DataProvider render={data => <h1>Hello {data.target}</h1>} />
複制
setState
資料管理
setState
不要直接更新狀态
// Wrong 此代碼不會重新渲染元件,構造函數是唯一能夠初始化 this.state 的地方。
this.state.comment = 'Hello'
// Correct 應當使用 setState():
this.setState({ comment: 'Hello' })
複制
元件生命周期中或者 react 事件綁定中,setState 是通過異步更新的,在延時的回調或者原生事件綁定的回調中調用 setState 不一定是異步的。
- 多個 setState() 調用合并成一個調用來提高性能。
- this.props 和 this.state 可能是異步更新的,不應該依靠它們的值來計算下一個狀态。
// Wrong
this.setState({
counter: this.state.counter + this.props.increment
})
// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}))
複制
原生事件綁定不會通過合成事件的方式處理,會進入更新事務的處理流程。
setTimeout
也一樣,在
setTimeout
回調執行時已經完成了原更新元件流程,不會放入
dirtyComponent
進行異步更新,其結果自然是同步的。
setState
原理
setState
setState 并沒有直接操作去渲染,而是執行了一個
updateQueue
(異步 updater 隊列),
setState( stateChange ) {
Object.assign( this.state, stateChange );
//合并接收到的state||stateChange改變的state(setState接收到的參數)
renderComponent( this );//調用render渲染元件
}
複制
這種實作,每次調用
setState
都會更新 state 并馬上渲染一次(不符合其更新優化機制),是以我們要合并
setState
。
具體可以閱讀源碼
ReactUpdateQueue.js
ErrorBoundary
、 Suspense
和 Fragment
ErrorBoundary
Suspense
Fragment
ErrorBoundaries
ErrorBoundaries
react 16 提供了一個新的錯誤捕獲鈎子
componentDidCatch(error,errorInfo)
, 它能将子元件生命周期裡所抛出的錯誤捕獲, 防止頁面全局崩潰。demo
componentDidCatch
并不會捕獲以下幾種錯誤
- 事件機制抛出的錯誤(事件裡的錯誤并不會影響渲染)
- Error Boundaries 自身抛出的錯誤
- 異步産生的錯誤
- 服務端渲染
lazy、Suspence
延遲加載元件
lazy、Suspence
lazy
需要跟
Suspence
配合使用,否則會報錯。
lazy
實際上是幫助我們實作代碼分割 ,類似 webpack 的
splitchunk
的功能。
Suspense
意思是能暫停目前元件的渲染, 當完成某件事以後再繼續渲染。簡單來說就是減少首屏代碼的體積,提升性能。
import react, { lazy, Suspense } from 'react'
const OtherComponent = lazy(() => import('./OtherComponent'))
function MyComponent() {
return (
<Suspense fallback={<div>loading...</div>}>
<OtherComponent />
</Suspense>
)
}
複制
一種簡單的預加載思路, 可參考 preload
const OtherComponentPromise = import('./OtherComponent')
const OtherComponent = react.lazy(() => OtherComponentPromise)
複制
Fragments(v16.2.0)
Fragments(v16.2.0)
Fragments 允許你将子清單分組,避免向 DOM 添加額外的節點。
render() {
return (
<>
<ChildA />
<ChildB />
<ChildC />
</>
);
}
複制
reactFiber
架構分析
reactFiber
react-fiber
是為了增強動畫、布局、移動端手勢領域的适用性,最重要的特性是對頁面渲染的優化: 允許将渲染方面的工作拆分為多段進行。
reactFiber
架構解決了什麼問題
reactFiber
react-fiber
可以為我們提供如下幾個功能:
- 設定渲染任務的優先
- 采用新的 Diff 算法
- 采用虛拟棧設計允許當優先級更高的渲染任務和較低優先的任務之間來回切換
Fiber
如何做到異步渲染 VirtualDom
和 Diff
算法
Fiber
VirtualDom
Diff
衆所周知,畫面每秒鐘更新 60 次,頁面在人眼中顯得流暢,無明顯示卡頓。每秒 60 次,即 16ms 要更新一次頁面,如果更新頁面消耗的時間不到 16ms,那麼在下一次更新時機來到之前會剩下一點時間執行其他的任務,隻要保證及時在 16ms 的間隔下更新界面就完全不會影響到頁面的流暢程度。fiber 的核心正是利用了 60 幀原則,實作了一個基于優先級和 requestIdleCallback 的循環任務排程算法。
function fiber(剩餘時間) {
if (剩餘時間 > 任務所需時間) {
做任務
} else {
requestIdleCallback(fiber)
// requestIdleCallback 是浏覽器提供的一個 api,可以讓浏覽器在空閑的時候執行回調,
// 在回調參數中可以擷取到目前幀剩餘的時間,fiber 利用了這個參數,
// 判斷目前剩下的時間是否足夠繼續執行任務,
// 如果足夠則繼續執行,否則暫停任務,
// 并調用 requestIdleCallback 通知浏覽器空閑的時候繼續執行目前的任務
}
}
複制
react hooks
react hooks
在 react 16.7 之前, react 有兩種形式的元件, 有狀态元件(類)和無狀态元件(函數)。官方解釋:hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。個人了解:讓傳統的函數元件 function component 有内部狀态 state 的函數 function,簡單來說就是 hooks 讓函數元件有了狀态,可以完全替代 class。
接下來梳理 Hooks 中最核心的 2 個 api,
useState
和
useEffect
useState
useState
useState 是一個鈎子,他可以為函數式元件增加一些狀态,并且提供改變這些狀态的函數,同時它接收一個參數,這個參數作為狀态的預設值。
const [count, setCount] = useState(initialState)
複制
使用 Hooks 相比之前用 class 的寫法最直覺的感受是更為簡潔
function App() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
複制
useEffect(fn)
useEffect(fn)
在每次 render 後都會執行這個鈎子。可以将它當成是
componentDidMount
、
componentDidUpdate``、componentWillUnmount
的合集。是以使用
useEffect
比之前優越的地方在于:
可以避免在
componentDidMount
、
componentDidUpdate
書寫重複的代碼; 可以将關聯邏輯寫進一個
useEffect
(在以前得寫進不同生命周期裡);
深入了解 react 原理
react 虛拟 dom 原理剖析
react 元件的渲染流程
使用
react.createElement
或 JSX 編寫 react 元件,實際上所有的 JSX 代碼最後都會轉換成
react.createElement(...)
,Babel 幫助我們完成了這個轉換的過程。
createElement 函數對 key 和 ref 等特殊的 props 進行處理,并擷取
defaultProps
對預設 props 進行指派,并且對傳入的孩子節點進行處理,最終構造成一個
reactElement
對象(所謂的虛拟 DOM)。
reactDOM.render
将生成好的虛拟 DOM 渲染到指定容器上,其中采用了批處理、事務等機制并且對特定浏覽器進行了性能優化,最終轉換為真實 DOM。
虛拟 DOM 的組成
即
reactElementelement
對象,我們的元件最終會被渲染成下面的結構:
`type`:元素的類型,可以是原生 html 類型(字元串),或者自定義元件(函數或 class)
`key`:元件的唯一辨別,用于 Diff 算法,下面會詳細介紹
`ref`:用于通路原生 dom 節點
`props`:傳入元件的 props,chidren 是 props 中的一個屬性,它存儲了目前元件的孩子節點,可以是數組(多個孩子節點)或對象(隻有一個孩子節點)
`owner`:目前正在建構的 Component 所屬的 Component
`self`:(非生産環境)指定目前位于哪個元件執行個體
`_source`:(非生産環境)指定調試代碼來自的檔案(fileName)和代碼行數(lineNumber)
複制
當元件狀态 state 有更改的時候,react 會自動調用元件的 render 方法重新渲染整個元件的 UI。當然如果真的這樣大面積的操作 DOM,性能會是一個很大的問題,是以 react 實作了一個
VirtualDOM
,元件 DOM 結構就是映射到這個
VirtualDOM
上,react 在這個
VirtualDOM
上實作了一個 diff 算法,當要重新渲染元件的時候,會通過 diff 尋找到要變更的 DOM 節點,再把這個修改更新到浏覽器實際的 DOM 節點上,是以實際上不是真的渲染整個 DOM 樹。這個
VirtualDOM
是一個純粹的 JS 資料結構,是以性能會比原生 DOM 快很多。
react
是如何防止 XSS
的
react
XSS
reactElement
對象還有一個
$$typeof
屬性,它是一個 Symbol 類型的變量
Symbol.for('react.element')
,當環境不支援 Symbol 時,
$$typeof
被指派為
0xeac7
。這個變量可以防止 XSS。如果你的伺服器有一個漏洞,允許使用者存儲任意 JSON 對象, 而用戶端代碼需要一個字元串,這可能為你的應用程式帶來風險。JSON 中不能存儲
Symbol
類型的變量,而 react 渲染時會把沒有
\$\$typeof
辨別的元件過濾掉。
diff
算法
diff
傳統的
diff
算法通過循環遞歸對節點一次對比,效率很低,算法複雜度達到 O(n^3),其中 n 是樹中節點的總數,React 通過制定大膽的政策,将 O(n^3) 複雜度的問題轉換成 O(n) 複雜度的問題。
diff
政策:
- web ui 中 Dom 節點跨層級的移動操作很少,
算法比較新舊節點的時候,比較隻會在同層級比較,不會跨層級比較diff
- 擁有相同類的兩個元件将會生成相似的樹形結構,擁有不同類的兩個元件将會生成不同的樹形結構。
- 對于同一層級的一組子節點,他們可以通過唯一 key 進行區分
基于以上三個前提政策,React 分别對
tree diff
、
component diff
以及
element diff
進行算法優化,事實也證明這三個前提政策是合理且準确的,它保證了整體界面建構的性能。簡單的講就是:
具體可以參考React 源碼剖析系列 - 不可思議的 react diff
- React 通過制定大膽的 diff 政策,将 O(n3) 複雜度的問題轉換成 O(n) 複雜度的問題;
- React 通過分層求異的政策,對
進行算法優化;tree diff
- React 通過相同類生成相似樹形結構,不同類生成不同樹形結構的政策,對
進行算法優化;component diff
- React 通過設定唯一 key 的政策,對
進行算法優化;element diff
建議,在開發元件時,保持穩定的 DOM 結構會有助于性能的提升;建議,在開發過程中,盡量減少類似将最後一個節點移動到清單首部的操作,當節點數量過大或更新操作過于頻繁時,在一定程度上會影響 React 的渲染性能。
react 性能分析與優化
減少不必要的渲染
在使用
classComponent
進行開發的時候,我們可以使用
shouldComponentUpdate
來減少不必要的渲染,那麼在使用
react hooks
後,我們如何實作這樣的功能呢?
解決方案:
React.memo
和
useMemo
對于這種情況,react 當然也給出了官方的解決方案,就是使用 React.memo 和 useMemo。
React.memo
React.memo
React.momo 其實并不是一個 hook,它其實等價于 PureComponent,但是它隻會對比 props。使用方式如下(用上面的例子):
import React, { useState } from 'react'
export const Count = React.memo(props => {
const [data, setData] = useState({
count: 0,
name: 'cjg',
age: 18
})
const handleClick = () => {
const { count } = data
setData({
...data,
count: count + 1
})
}
return <button onClick={handleClick}>count:{data.count}</button>
})
複制
useMemo
useMemo 它的用法其實跟 useEffects 有點像,我們直接看官方給的例子
function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => <Child1 a={a} />, [a])
// Only re-rendered if `b` changes:
const child2 = useMemo(() => <Child2 b={b} />, [b])
return (
<>
{child1}
{child2}
</>
)
}
複制
從例子可以看出來,它的第二個參數和 useEffect 的第二個參數是一樣的,隻有在第二個參數數組的值發生變化時,才會觸發子元件的更新。
引用React hooks 實踐
使用 shouldComponentUpdate() 防止不必要的重新渲染
當一個元件的 props 或 state 變更,React 會将最新傳回的元素與之前渲染的元素進行對比,以此決定是否有必要更新真實的 DOM,當它們不相同時 React 會更新該 DOM。
即使 React 隻更新改變了的 DOM 節點,重新渲染仍然花費了一些時間。在大部分情況下它并不是問題,但是如果渲染的元件非常多時,就會浮現性能上的問題,我們可以通過覆寫生命周期方法 shouldComponentUpdate 來進行提速。
shouldComponentUpdate 方法會在重新渲染前被觸發。其預設實作總是傳回 true,如果元件不需要更新,可以在 shouldComponentUpdate 中傳回 false 來跳過整個渲染過程。其包括該元件的 render 調用以及之後的操作。
shouldComponentUpdate(nextProps, nextState) {
return nextProps.next !== this.props.next
}
複制
React 性能分析器
React 16.5 增加了對新的開發者工具 DevTools 性能分析插件的支援。此插件使用 React 實驗性的 Profiler API 來收集有關每個元件渲染的用時資訊,以便識别 React 應用程式中的性能瓶頸。它将與我們即将推出的 time slicing(時間分片) 和 suspense(懸停) 功能完全相容。
redux
Store
:儲存資料的地方,你可以把它看成一個容器,整個應用隻能有一個
Store
。
State
:
Store
對象包含所有資料,如果想得到某個時點的資料,就要對
Store
生成快照,這種時點的資料集合,就叫做
State
。
Action
:
State
的變化,會導緻 View 的變化。但是,使用者接觸不到 State,隻能接觸到 View。是以,State 的變化必須是 View 導緻的。Action 就是 View 發出的通知,表示 State 應該要發生變化了。
ActionCreator
:View 要發送多少種消息,就會有多少種
Action
。如果都手寫,會很麻煩,是以我們定義一個函數來生成 Action,這個函數就叫
ActionCreator
。
Reducer
:
Store
收到
Action
以後,必須給出一個新的
State
,這樣 View 才會發生變化。這種
State
的計算過程就叫做
Reducer
。
Reducer
是一個函數,它接受 Action 和目前
State
作為參數,傳回一個新的
State
。
dispatch
:是
View
發出
Action
的唯一方法。
redux 的基本原理
然後我們過下整個工作流程:
首先,使用者(通過
View
)發出
Action
,發出方式就用到了
dispatch
方法。
然後,
Store
自動調用
Reducer
,并且傳入兩個參數:目前
State
和收到的
Action
,
Reducer
會傳回新的
State
State
一旦有變化,
Store
就會調用監聽函數,來更新
View
。
到這兒為止,一次使用者互動流程結束。可以看到,在整個流程中資料都是單向流動的,這種方式保證了流程的清晰。
redux
中間件
redux
Redux 的中間件提供的是位于 action 被發起之後,到達 reducer 之前的擴充點,換而言之,原本 view -> action -> reducer -> store 的資料流加上中間件後變成了 view -> action -> middleware -> reducer -> store ,在這一環節我們可以做一些 “副作用” 的操作,如 異步請求、列印日志等。
redux 中間件通過改寫 store.dispatch 方法實作了 action -> reducer 的攔截,從上面的描述中可以更加清晰地了解 redux 中間件的洋蔥圈模型:
中間件A -> 中間件B-> 中間件C-> 原始 dispatch -> 中間件C -> 中間件B -> 中間件A
複制
這也就提醒我們使用中間件時需要注意這個中間件是在什麼時候 “搞事情” 的,比如 redux-thunk 在執行 next(action) 前就攔截了類型為 function 的 action,而 redux-saga 就在 next(action) 才會觸發監聽 sagaEmitter.emit(action), 并不會攔截已有 action 到達 reducer。
參考:
- 深入分析虛拟 DOM 的渲染原理和特性
- react 事件機制
- 從 Mixin 到 HOC 再到 Hook
- 美團技術團隊-Redux 從設計到源碼
- react 源碼解析
- Vue 與 React 兩個架構的粗略差別對比