目录
六,如何实现多个useState, useEffect(原理)
七,Hooks性能优化(项目性能优化,老生常谈,做一下补充)
春节过后一直在工作之余零散的学习react,怎么说呢,来了,看过,走了,仿佛什么都没有留下,正赶上小组内需要做技术分享,nice,领了 React Hooks 技术分享题目,开始准备。因为之前是一直在用vue,所以开始接触的是react的类组件模式,相对来说便于理解(跟着b站大佬学习,141节课,20年视频),后面开始接触学习函数式组件,才发现函数式组件已经一统江山了(离了个大谱,前面白学了,在公司接手项目都是函数式写法),目前持续学习中…
一,什么是Hooks
hooks: 钩子, React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来 。
react hooks的诞生是为了解决react开发中遇到的问题,this的指向问题,生命周期,给函数组件扩展功能。
二,为什么要使用Hooks
要解释这个原因,首先得了解react 中两种组件模式,类式组件,函数式组件
类式组件:
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user)
};
handleClick = () => {
setTimeout(this.showMessage, 3000)
}
render() {
return <button onClick={this.handleClick}>Follow</button>
}
}
函数式组件:
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
}
const handleClick = () => {
setTimeout(showMessage, 3000)
}
return (
<button onClick={handleClick}>Follow</button>
)
}
ps:通常情况下默认这两种写法相同,但是上述例子有个隐藏问题,props绑定的值不会一直更新,而this是一直是最新的,这也是class写法的弊端
react在v16.8.0版本推出hooks,彻底改变了react组件生态,推出hooks之前大家都写class,v16.8.0之后,函数式组件逐步一统江山。
为什么函数式组件比类式组件好呢,为什么是在推出hooks之后呢?
- 函数式组件更加声明式,hooks使得生命周期更加底层化,与开发者无关
- 函数式组件更加函数式,hooks拥抱了函数
- 类式组件消耗性能比较大,因为需要创建组件的实例,而且不能销毁
- 函数式组件消耗性能小,不需要创建实例,得到返回的react元素后就把中间量销毁
- 函数式组件是没有状态,没有生命周期的,hooks出现解决了这一痛点
React 的本质是能够将声明式的代码映射成命令式的DOM操作,将数据映射成可描述的UI对象。hooks更符合这一理念,因此有更广阔的发展空间。
三,React hooks
名称及作用:
- useState 返回有状态值,以及更新这个状态值的函数
- useEffect 接受包含命令式,可能有副作用代码的函数
- useContext 接受上下文对象(从react.createContext返回的值)并返回当前上下文值
- useReducer useState的代替方案,接受类型为(state,action)=> newState的reducer,并返回与dispatch方法配对的当前状态
- useCallback 返回一个回忆的memoized版本,该版本仅在其中一个输入发生更改时才会更改
- useMemo 纯的一个记忆函数
- useRef 返回一个可变的ref对象,其.current属性被初始化为传递的参数
- useImperativeMethods 自定义使用ref时公开给父组件的实例值
- useMutationEffect 更新兄弟组件之前,它在react执行其DOM改变的同一阶段同步触发
- useLayoutEffect DOM改变后同步触发,使用它来从DOM读取布局并同步重新渲染
特性:
1,只能在顶层调用Hooks,不要在循环,条件或嵌套函数中调用Hook
2,不要在普通的JavaScript中使用Hooks
3,除了useRef,其他hooks 均存在Capture Value 特性
初学者掌握 useState,useEffect,useRef 这三个hooks就可以上手开发了,下面我也围绕着这几个hooks逐一展开(useRef 与 vue ref 大致相同,故忽略,有需要的小伙伴可查找官网API)
四, useState 使用及实现
使用方法:
- 让函数组件可以有state状态,并进行状态的读写操作
- 语法: const [xxx, setXxx] = useState(initValue)
- useState() 说明:
- 参数:第一次初始化指定的值在内部作缓存
- 返回值: 包括两个元素的数组,第一个为内部当前状态值,第二个为更新状态值的函数
- setXxx()两种写法:
- setXxx(newValue) : 参数为非函数值,直接指定新的状态值,内部用其覆盖原来的状态值
- setXxx(value => newValue): 参数为函数,接受原来的状态值,返回新的状态值,内部用其覆盖原来的状态值
eg:
import { Component, useState } from 'react';
// 类式组件
// class App extends Component {
// state = { count: 0 }
// add = () => {
// this.setState(state => ({count: state.count + 1}))
// }
// render() {
// return (
// <div>
// <h2>当前和为{this.state.count}</h2>
// <button onClick={this.add}>点我+1</button>
// </div>
// )
// }
// }
// 函数式组件
function App () {
const [count, setCount] = useState(0)
function add () {
// setCount(count +1)
setCount(count => count + 1) // 函数式写法
}
return (
<div>
<h2>当前和为{count}</h2>
<button onClick={add}>点我+1</button>
</div>
)
}
export default App;
手动实现一个简单useState
useState实现功能并不复杂,初始化赋值,返回一个函数改变状态
import { render } from 'react-dom'
let _state // 把 state 存储在外面
function useMyState(initialValue) {
_state = _state || initialValue // 如果没有 _state,说明是第一次执行,把 initialValue 复制给它
function setState(newState) {
_state = newState
render()
}
return [_state, setState]
}
export default useMyState
这样模拟了一个简单的useState,并不能使用它,可以思考一下,当有多个状态需要初始化的时候该怎么处理,这个下面再探讨
五,useEffect 使用及实现
使用方法:
- 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)
- React中的副作用操作
- 发ajax请求获取数据
- 设置订阅 / 启动定时器
- 手动更改真实DOM
- 语法说明
useEffect(() => {
// 在此可以执行任何带副作用操作
return () => {
// 在此做一下收尾工作,比如清除定时器,取消订阅等
}
}, [stateValue]) // 如果指定的是 [ ] ,回调函数只会在第一次render()后执行
- 可以把 useEffect 看做如下三个函数的组合
- componentDidMount()
- componentDidUpdate()
- componentWillmount()
eg:
import { Component, useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
// 类式组件
// class App extends Component {
// state = { count: 0 }
// add = () => {
// this.setState(state => ({count: state.count + 1}))
// }
// unmounted = () => {
// ReactDOM.unmountComponentAtNode(document.getElementById('root'))
// }
// componentDidMount () {
// this.timer = setInterval(() => {
// this.setState(state => ({count: state.count + 1}))
// }, 1000)
// }
// componentWillUnmount () {
// clearInterval(this.timer)
// }
// render() {
// return (
// <div>
// <h2>当前和为{this.state.count}</h2>
// <button onClick={this.add}>点我+1</button>
// <button onClick={this.unmounted}>卸载组件</button>
// </div>
// )
// }
// }
// 函数式组件
function App () {
const [count, setCount] = useState(0)
useEffect(() => {
let timer = setInterval(() => {
setCount(count => count +1)
}, 1000)
return () => {
clearInterval(timer)
}
},[])
function add () {
setCount(count => count +1)
}
function unmounted () {
ReactDOM.unmountComponentAtNode(document.getElementById('root'))
}
return (
<div>
<h2>当前和为{count}</h2>
<button onClick={add}>点我+1</button>
<button onClick={unmounted}>卸载组件</button>
</div>
)
}
export default App;
同理实现一个简单useEffect
let _deps // _deps 记录 useEffect 上一次的 依赖
function useMyEffect(callback, depArray) {
const hasNoDeps = !depArray // 如果 dependencies 不存在
const hasChangedDeps = _deps
? !depArray.every((el, i) => el === _deps[i]) // 两次的dependencies 是否完全相等
: true
/* 如果 dependencies 不存在,或者 dependencies 有变化*/
if (hasNoDeps || hasChangedDeps) {
callback()
_deps = depArray
}
}
export default useMyEffect
六,如何实现多个useState, useEffect(原理)
上面我们已经简单实现了useState,useEffect 这两个hooks,但是只能使用一次,如果声明多个,_state, _deps会被覆盖,React 底层是通过单链表来实现的,这也导致了 hooks的一些特性,如只能在函数最外层调用hooks,不能在循环、条件判断、子函数中调用,Capture Value等等
模拟底层实现:
let memoizedState = []; // hooks 存放在这个数组
let cursor = 0; // 当前 memoizedState 下标
function useState(initialValue) {
memoizedState[cursor] = memoizedState[cursor]
|| initialValue;
const currentCursor = cursor;
function setState(newState) {
memoizedState[currentCursor] = newState;
render();
}
return [memoizedState[cursor++], setState];
// 返回当前 state,并把 cursor 加 1
}
function useEffect(callback, depArray) {
const hasNoDeps = !depArray;
const deps = memoizedState[cursor];
const hasChangedDeps = deps
? !depArray.every((el, i) => el === deps[i])
: true;
if (hasNoDeps || hasChangedDeps) {
callback();
memoizedState[cursor] = depArray;
}
cursor++;
}
模拟实现图例说明
1,初始化
2,初次渲染
3,事件触发
4,re-render
hooks流程小结:
Q:为什么只能在函数最外层调用 Hook?为什么不要在循环、条件判断或者子函数中调用?
A:memoizedState 数组是按hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。
Q:自定义的 Hook 是如何影响使用它的函数组件的?
A:共享同一个 memoizedState,共享同一个顺序。
Q:"Capture Value" 特性是如何产生的?
A:每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。(本质上是形成了闭包,useRef是一个普通对象,所以不存在Capture Value,可以通过这个useRef绕开这个特性)
七,Hooks性能优化(项目性能优化,老生常谈,做一下补充)
性能优化是前端项目,网站,框架等等绕不开的坎,正是我们一直追求的高性能不断推进着前端技术的发展。在react中我们知道,当父组件发生改变,子组件一定会重新渲染,即使所依赖的prop值未发生变化。
在类组件中,我们可以通过shouldComponentUpdate增加逻辑来判断是否更新,但是函数式组件没有生命周期,这意味着函数式组件每一次调用都会执行其中所有逻辑,这样会带来非常大的性能损耗,因此hooks给我们提供了这两个api:useMemo、 useCallback
老规矩,使用方法:接收两个参数,第一个是回调,第二个为依赖数据
// useMemo
const memoizedValue = useMemo(
() => computeExpensiveValue(a, b), [a, b])
// useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b)
},
[a, b],
)
// useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
相同作用:仅仅是依赖发生改变,才会重新计算结果
两者区别:
- useMemo:计算结果是reture回来的值,主要用于缓存计算结果的值
- useCallback: 计算结果是函数,主要用于缓存函数
参考下面例子便于理解概念
import React from 'react'
export default function TestMemo() {
const [count, setCount] = useState(1)
const [val, setValue] = useState('')
function expensive() {
console.log('compute')
let sum = 0
for (let i = 0; i < count * 100; i++) {
sum += i
}
return sum
}
// const expensive = useMemo(() => {
// console.log('compute')
// let sum = 0
// for (let i = 0; i < count * 100; i++) {
// sum += i
// }
// return sum
// }, [count])
return <div>
<h4>{count}-{val}-{expensive()}</h4>
{/* <h4>{count}-{val}-{expensive}</h4> */}
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
<input value={val} onChange={event => setValue(event.target.value)}/>
</div>
</div>
}
useMemo简单实现,useCallback同理可得,感兴趣可以在网上找找
let hookStates = [] // 保存状态的数组
let hookIndex = 0 // 索引
function useMemo(factory, dependencies) {
if (hookStates[hookIndex]) { // 说明不是第一次
let [lastMemo, lastDependencies] = hookStates[hookIndex]
// 判断一下新的依赖数组中的每一项是否跟上次完全相等
let same = dependencies.every((item, index) => item === lastDependencies[index])
// 没变则用老的
if (same) {
hookIndex++
return lastMemo
} else { // 只要有一个依赖变量不一样的话
let newMemo = factory()
hookStates[hookIndex++] = [newMemo, dependencies]
return newMemo
}
} else { // 说明是第一次渲染
let newMemo = factory()
hookStates[hookIndex++] = [newMemo, dependencies]
return newMemo
}
}
简单介绍了一下关于react 官方针对hooks优化提供的api,可以作为我们优化项目的工具,而工作中大部分的性能优化还是对于代码结构的优化,从设计的合理性,组件的提取拆分从而配合hooks 特性,api去完成优化,不可同一而论。
比如,开发一个大型页面,需要初始化十几个甚至更多的状态,我们每多写一个useState,组件需要多执行一次render(函数式组件相比于类组件),这时候可读性就会很差,需要通过逻辑为导向,抽离在不同的文件中,再通过useMemo来屏蔽来自其他的state改变导致的Re-render等等,来降低代码的耦合性和复杂性,相信谁也不愿看到一个文件2000+行的恐怖代码。
八,总结
在写这篇分享之前,断断续续了解react,对于 react hooks 的概念是,很强很难很酷,是react高手进阶之法,通过这段时间的学习,遇到问题,解决问题,去查找实现原理,再回过头来看,自己所掌握的还是略显浅薄,就当做自己的一篇学习笔记,给初学者一点建议,共同学习,共同成长
以上是本次分享内容,由于是初学,查了很多技术前辈博客,借鉴学习了很多,按照自己学习思路整理在一起,因为是技术分享,主要还是现场讲解,文档只是辅助,因为时间有限,所以直接把分享文档放上来了,如果读起来不是很通畅,大家勿怪,2022,一起冲!!!
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=224k7uaw4hqi