前言
React 推出 react-hooks,为了让开发者更方便的开发应用
其可以不用可以理会 React.Componet的生命周期,对与不关注这些的来说,简直是一个福音,另外他减少了大量的开发代码,尤其是 state相关,尤其是 typescript,可以不用可以维护 state 的 type,每个state状态单独定义,且使用更为方便
React-hooks
react-hooks正常使用为一个函数形式,使用时需要返回 html 节点,实现了在函数中更新节点的效果,正因为此特性,一些在 React.Componet中的操作无法在使用,否则可能出现很多问题,例如:不能直接在内部声明与 组件同生命周期的变量(错误演示:name: string),后面会介绍
里面的一些use特性的方法,可以保存我们的参数,避免了了函数执行特性时导致的重复创建
使用 hooks 声明一个组件,结尾需要 return 组件节点,如下所示
const Counter = (props: CounterPropsType) => {
const [count, setCount] = useState<number>(props.count)
return (
<div>
</div>
)
}
//对外暴露
export default Counter
传递属性
其传递属性和 component 不同,这里直接通过参数的形式传递,如下所示
const Counter = (props: CounterProps) => {
return (
<div>{props.count}</div>
)
}
顺道帮忙回忆一下 component 如何声明 props 属性
//声明 props类型
class Counter extends React.Component<CounterProps>
//调用props的属性
this.props.count
注意 props 属性传递过来的内容,可以直接用于渲染节点,且会随着传递参数的变更而重新渲染,如果属性需要加工,可以声明一个新的 state属性 使用
useState
看名字就知道设置状态的,其声明如下所示,相当于保存了一个节点到 state 中去,可以通过此方法返回的内容进行读写(可以只返回一个读)
const Counter = (props: CounterProps) => {
//更新内容的时候,必须要传递一个新的内容,为堆中指针对比
//例如:list更新一个item,需要创建一个新的list,并将原来list内容传递过去,否则不更新渲染
const [count, setCount] = useState<number>(props.count)
//只读取的或也可以设置一个
const [status] = useState<number>(0)
return (
<div onClick={()=>{ setCount(count + 1) //点击更新count }} >{props.count}</div>
)
}
useEffect
useEffect副作用函数,在函数渲染完成之后执行,且同时可以监听多个属性状态的变化,以回调更新内容
注: 一个hook函数可以添加多个 useEffect
const Counter = (props: CounterProps) => {
const [count, setCount] = useState<number>(props.count)
//可以声明多个 useEffect 副作用函数
useEffect(() => {
//当 props传递新的count的时候,会回调
//相当于监听了某个属性的 componentDidUpdate
//如果直接使用 props.status 参数,那么不用重新定义监听,当外部更新时,内部会自动重新触发渲染
}, [props.status])
useEffect(() => {
//当count重新渲染的时候,会回调,注意这里不可直接 setCount,否则会陷入回调地狱
}, [count])
useEffect(() => {
//不监听任何属性或者状态,相当于 componentDidMount
return () => {
//当组件释放的时候走该回调,相当于componentWillUnmount
}
}, [])
return (
<div>{props.count}</div>
)
}
useMemo
前面提到了,每当页面发生改变时,该hook函数都会重新执行计算,因此可能出现一些额外的计算造成新能问题
useMemo 可以作为渲染端优化的手段,react当渲染内容时,可能会丢失一些渲染过的信息,因此一些无需再次渲染的内容可能会再次渲染,useMemo 当依赖参数(监听的属性)发生变化时,才会重新回调进行计算,当没有依赖参数时,每次都会重新计算(此时还不如不用)
同时 userMemo 也可以保存一些常态参数,避免重复创建
注:过早的优化时万恶之源,当完成版本时,如果发现性能问题,可以尝试优化
const Counter = (props: CounterProps) => {
const [count, setCount] = useState<number>(props.count)
//初始化一个list,避免每次都重新创建,给个监听参数,可以避免无效更新
const list = useMemo(() => {
return [];
}, [xxx]) //某个状态改变时才更新
//用于减少一些计算的重新回调
useMemo(() => {
//根据id做了非常复杂的算法,当id变更时再重新计算
}, [props.id])
//用于减少一些 ui 渲染相关的重新回调(可以针对一些比较复杂的 ui,例如:封装了一个很复杂的3d渲染模块,一个长列表等),后面参数可以控制仅仅部分参数
const get3dView = () => {
return useMemo(() => {
<div3d count={props.count}>啦啦啦啦啦</div3d>
}, [props.count])
}
//此时就和useEffect差不多,不会走第二次了
useMemo(() => {
}, [])
return (
<div>{props.count}</div>
)
}
有时候也会像下面这样写,由于非函数内部不能使用hooks系列,只能使用React.memo等
注:由于不是hook类型,参数和那个就不一样了
//没有的默认进行浅对比
const TestView = React.memo((props: any) => {
console.log(123456)
return <div>标题:</div>
})
//如果想根据参数来指定规则, 第二个可以使用下面闭包,案例为浅比较案例,不填默认就是这个
const TestView = React.memo((props: any) => {
console.log(123456)
return <div>标题:</div>
}, (prevProps: any, nextProps: any) => {
return prevProps !== nextProps
})
useCallback
保存一些函数,避免函数式变成引起的重新创建新函数的行为,与 useMemo类似,可以避免防止做无关的刷新,另外,这个组件需要配合memo,否则非但不提升性能,可能还会降低性能
- 默认父组件节点发生改变,引用参数的子组件一定会重新触发渲染
- 使用了useCallback + useMemo,让组件仅type改变的时候才会触发更新渲染
const Counter = (props: CounterProps) => {
const [count, setCount] = useState<number>(props.count)
//配合useMemo,能提高性能
useCallback(() => {
setCount(props.type === 0 ? count + 1: count + props.type)
}, [props.type])
//该函数当反生改变时会触发,此外我们也可以保存该函数,传递给子组件,子组件可以直接调用该函数
//能避免函数式编程导致的每次都创建新的函数,避免子组件被强制渲染, useMemo 无效问题
//例如:子组件需要时可以直接调用 updateCallback()
const updateCallback = useCallback(() => {
setCount(props.type === 0 ? count + 1: count + props.type)
}, [])
return (
<div>
//默认父组件节点发生改变,引用参数的子组件一定会重新触发渲染
//使用了useCallback + useMemo,让组件仅type改变的时候才会触发更新渲染
<TestMemoView count={props.count} />
</div
)
}
//需要声明
const TestMemoView = React.memo((props: any)=>{
console.log(props)
return <div>{props.count}</div>
})
useRef
使用了 useRef 之后,该属性将会被挂载到 组件component上,组件整个声明周期内都会一直存在
注:除了临时变量外,不要像在 component 中一样声明属性,否则就是一个临时变量,容易出现野指针问题
const Counter = (props: CounterProps) => {
const [count, setCount] = useState<number>(props.count)
//使用 useRef 接收一个变量,可以用来做其他事情,这只是一个案例
//使用 useRef 的变量,将会挂载到组件上,受组件生命周期影响
//不会像state一样触发渲染,因此作为不渲染的状态也很好
const canvasView = useRef<CanvasView>(null)
const isRequest = useRef<boolean>(false)
//错误演示,此时声明相当于声明了一个普通的临时对象
//当此函数执行完成之后,该参数会被释放,会成为野指针
//private canvasView?: CanvasView
useEffect(() => {
//假设有一个网络请求
request.get().then((res) {
//此时函数已经执行完毕,canvasView 如果是上面的错误声明,此时已经被释放,则会出现野指针问题
//如果使用 useRef 此时调用毫无问题
let img = canvasView.getImageBy(res);
})
//isRequest.current = true; //更新状态且不触发渲染
}, [])
return (
<div ref={container}>
//一个用于特殊功能的 CanvasView,例如:截图
<CanvasView ref={canvasView} />
</div>
)
}
useContext
useContext 与 createContext基本上是不可分割的,其主要用于在各个页面共享一些数据
案例是typescript,javascript请自行简化
静态参数使用
一般直接用的比较多的就是当做静态数据配置信息等
//这里将参数设置为String类型,实际上更多的是一个对象
export const ConfigContext = createContext<String>('我是设置的config')
const Config = useContext(ConfigContext) //然后直接使用即可
动态可修改数据
原理就是共享数据时,同时共享一个set方法,可以让其他页面更新数据,初始化时传递 useState 的 渲染和设置渲染的参数即可
开发中可能会存在用户数据,可能需要动态修改,但是会应用到各个页面,也可以使用 useContext
创建用户数据和 Context
export class UserInfo {
name?: string
age?: number
phone?: string
}
//如果你觉得初始值之类的数组不优雅,也可以对象形式,不过现在这样更像hooks是吧
export const UserContext = createContext<[UserInfo, Function]>([
{},
(userInfo: UserInfo) => {}
])
使用前传入用于渲染useState的get、set方法,可以在多个页面获取get、set方法,可用于显示和渲染
注意当父节点触发渲染时,子节点会强制触发渲染,可以使用 useMemo 进行优化,和上面介绍 useCallback 同样的道理, useContext 如果在 useMemo 里面也会强制触发渲染,需要注意 useMemo 的位置,避免无效优化
import { UserContext, UserInfo } from './userContext';
function App() {
//使用 useState 用于触发渲染
const [userInfo, setUserInfo] = useState<UserInfo>({});
return (
//给定初始值
<UserContext.Provider value={[userInfo, setUserInfo]}>
<div className="App">
<header className="App-header">
<UpdateView></UpdateView>
<TestView></TestView>
</header>
</div>
</UserContext.Provider>
);
}
const UpdateView = () => {
//获取 set 方法触发渲染,第一个参数不用的话,就不起名即可
const [, setUserInfo] = useContext(UserContext)
return <div onClick={() => {
setUserInfo({
name: 'marshal',
age: 20
})
}}>点击我更新userInfo内容</div>
}
const TestView = () => {
const {userInfo} = useContext(UserContext)
return <div>内容是:{userInfo.name}</div>
}
感觉是不是有了 redux 的缩影,其实就差一个类型分发了😂
ps:由于 context 强制触发更新,尽量能把 context 分割成多个就分割(就是多嵌套几个 Provider),能优化一些性能
useReducer
useReducer 其就是 redux 的替代方案,或者简化版本
useReducer为 useState的替代方案,看到它,估计很多人会想到 redux,他算是一个 redux的简化版,当我们多个组件之间需要进行交互的时候,或者内部组件嵌套比较深的时候,可以将部分 useState 换成它,以优化代码交互
initialState:保存的 state 状态,用于显示和更新渲染内容,和组件中的 state 类似
reducer:更新组件state的实现,同时也在这里也可以负责分发事件,最终返回新的 state 渲染节点,注意分发事件平衡
reducer-state:useReducer返回的第一个参数,为state状态,可以取内部属性用来渲染节点
reducer-dispatch:useReducer返回的第二个参数,其用来传递参数给 reducer 从而触发渲染
//创建一个默认状态,用来保存各组件交互所用到的 state //切勿将所有组件的 state 都扔进去,这样会变成一些 redux 项目那样 //因为那样 state 的状态管理会变得复杂,且随着 state 增多,性能越来越差 //个人不是很看好 redux 的一点就是这个,感觉 redux 应该主要用于公用部分状态的管理 //需要即添加,当然也可以更加规范化 //当然也可能是自己看到的文章或者一些项目案例使用有问题😂
创建context和数据相关,以用于获取数据
export class ClientInfo {
name?: string
age?: number
}
export const initializeClientInfo: ClientInfo = {}
//创建context,顺道定义我们的 context 的 value 类型和初始值
export const ClientContext = createContext<[ClientInfo, (info: ActionType) => void]>([
initializeClientInfo,
() => {}
])
//声明action的类型,用于限制参数类型的传递
export interface ActionType {
type: 'name' | 'age', //也可以自己定义成枚举
value: any
}
//action可以自己定义类型,结果返回新的state
export const reducer = (state: ClientInfo, action: ActionType) => {
//type如果定义成枚举类型更好了
if (action.type === 'name') {
state.name = action.value as string
}else if (action.type === 'age') {
state.age = action.value as number
}
return {
...state
}
}
使用案例,
function App() {
//通过 useReducer 获取我们的 state 和 dispatch
const [state, dispatch] = useReducer<Reducer<ClientInfo, ActionType>>(reducer, initializeClientInfo)
return (
<ClientContext.Provider value={[state, dispatch]}>
<div className="App">
<header className="App-header">
<UpdateView></UpdateView>
<TestView></TestView>
</header>
</div>
</ClientContext.Provider>
);
}
//更新我们的值
const UpdateView = () => {
const [, dispatch] = useContext(ClientContext)
return <div onClick={() => {
dispatch({
type: 'name',
value: 'reducer'
})
}}>点击我更新userInfo内容</div>
}
//获取我们的值
const TestView = () => {
const [clientInfo] = useContext(ClientContext)
return <div>clientName:{clientInfo.name}</div>
}
ps:将 ActionType 换成 Object 有惊喜,会发现,和 useContext 的使用没啥区别了,代码甚至还多了😂
context、useContext、useReducer 对比思考
他们两个都通过 context 的方式共享数据,这方面非常相似,通过案例也可以看出来
ps:把 useReducer 里面的 action 换成 Object 是不是感觉跟用的 useContext 一样了呢😂
也同时可以看到,reducer 事件中定义了类型,可以用于不同类型事件的分发,某种程度上,更适合处理一些不同业务之间的数据共享问题,根据类型创建计划,避免了其他人乱修改或者错误修改数据的情况,但也增加了了复杂度,实际上可以根据情况使用 useReducer、useContext
redux、reducer使用问题
为了避免将 reducer 发展成一些项目中臃肿的人人喊打的 redux,下面简单介绍下问题,以及解决方案
很多人使用 redux 的时候,将所有界面的 state 放到了一个 state 中进行操作,个人感觉这个就是过度使用了(可能是吹的太高了,毕竟过犹不及),其会导致页面 state 过于庞大,难以维护,且很容易被其他页面的操作所影响,bug也难以追踪,最重要的是项目一大,性能问题就比较明显
因此,在使用 redux、reducer时,我们只将一些公共的资源分离出来,以便于页面之间的交互,其他的状态,在自己页面内维护即可,且无需更新的公共数据,也用不到 reducer,直接使用 useContext 获取即可
redux、reducer可以作为公共资源交互错综复杂的一种解决方案,而不是说大项目一定要用,使用某些出来的新技术是,即使他思想很优秀,也不是啥也不用考虑一把嗦,这样其他人看了也头疼,有些问题短期内也是不容易爆发的,只能自己吃哑巴亏,碰到新技术先思考优缺点,取其精华去其糟粕才是关键,而不是全盘接收,碰壁之后,也顺道忽略了他的发光点
父组件更新问题处理
当然他们也都存在一个共同的问题,前面说过了当父组件更新时,子组件的数据也会更新,我们在局部使用 context时,如果父组件更新,默认值会被修改,因此所有子组件会被重新更新渲染,当然可能页面并没有什么变化,会造成性能浪费,我们可以适当会用 useMemo 优化我们的数据
//通过 useMemo 保存我们的数据,避免数据被父组件无关紧要更新导致重新渲染
const list = useMemo(() => {
return [];
}, [state])
无UI组件数据共享问题
全局的共享数据使用
这个很简单,我们赋予初值时,将我们在 context 中声明的全局初始值赋值给我们的 useState、useReducer 即可,直接使用 export 导出的该参数即可(我们无论怎么更新,更新的都是里面的内容,外面指针不变)
export const initializeClientInfo: ClientInfo = {}
局部模块共享数据使用
第一种(非UI用的模块公共资源比较多):可以跟上面一样也定义成全局变量或者单例使用,该模块释放时,同时置空该资源,这种方法还是比较推荐的,简单粗暴易维护,例如:某一个模块的网络请求统一增加了一个额外的参数,没有时不传递,直接使用全局或者单例的方式即可
第二种(非UI用的模块公共资源比较少):定义到该模块最上层的局部变量中,每次都会初始化,里面用到的组件,通过 useContext 来获取内容,然后间接传递到非 UI 组件中,例如:某一个功能,用到了模块公共数据,直接带过去即可
模棱两可的可以自行选择,另外可能还有更高级更优秀的传参方法,这里暂不讨论了,等后续自己了解到且想起来的话会来再补上😂
最后
这个不仅仅在 react 中使用, react-native 中照样可以使用,快来尝试一下吧,会让你爱不释手