React的组件化给前端开发带来了史无前例的体验,咱们能够像玩乐高玩具同样将一个组件堆积拼接起来,就组成了一个完整的UI界面,在加快了开发速度的同时又提升了代码的可维护性。可是随着业务功能复杂度提升,业务代码不得不和生命周期函数糅合到一块儿。这样不少重复的业务逻辑代码很难被抽离出来,为了快速开发不得不Ctrl+C,若是业务代码逻辑发生变化时,咱们又不得不一样时修改多个地方,极大的影响开发效率和可维护性。为了解决这个业务逻辑复用的问题,React官方也作了不少努力:javascript
React mixin 是经过React.createClass建立组件时使用的,如今主流都是经过ES6方式建立react组件,官方由于mixin很差追踪变化以及影响性能,因此放弃了对其支持,同时也不推荐咱们使用。这里就简单介绍下mixin:html
mixin的原理其实就是将[mixin]里面的方法合并到组件的prototype上前端
var logMixin = {
alertLog:function(){
alert('alert mixin...')
},
componentDidMount:function(){
console.log('mixin did mount')
}
}
var MixinComponentDemo = React.createClass({
mixins:[logMixin],
componentDidMount:function(){
document.body.addEventListener('click',()=>{
this.alertLog()
})
console.log('component did mount')
}
})
// 打印以下
// component did mount
// mixin did mount
// 点击页面
// alert mixin...
复制代码
能够看出来mixin
就是将logMixn
的方法合并到MixinComponentDemo
组件中,若是有重名的生命周期函数都会执行(render除外),若是有重名的函数会报错。可是因为mixin的问题比较多因此这里就不展开讲。点击了解更多java
组件是 React 中代码复用的基本单元。但你会发现某些模式并不适合传统组件。react
例如:咱们有个计时器和日志记录组件ios
class LogTimeComponent extends React.Component{
constructor(props){
super(props);
this.state = {
index: 0
}
this.show = 0;
}
componentDidMount(){
this.timer = setInterval(()=>{
this.setState({
index: ++index
})
},1000)
console.log('组件渲染完成----')
}
componentDidUpdate(){
console.log(`我背更新了${++this.show}`)
}
componentWillUnmount(){
clearInterval(this.timer)
console.log('组件即将卸载----')
}
render(){
return(
<div> <span>{`我已经显示了:${this.state.index}s`}</span> </div>
)
}
}
复制代码
上面就简单的实现了简单的日志和计时器组件。那么问题来了假若有三个组件分别是LogComponent
(须要记录日志)、SetTimeComponent
(须要记录时间)、LogTimeShowComponent
(日志和时间都须要记录);怎么处理呢?把上面逻辑 Ctrl+C 而后 Ctrl+V 吗?若是记录日志的文案改变须要每一个组件都修改么?官方给咱们提供了高阶组件(HOC)的解决方案:git
function logTimeHOC(WrappedComponent,options={time:true,log:true}){
return class extends React.Component{
constructor(props){
super(props);
this.state = {
index: 0
}
this.show = 0;
}
componentDidMount(){
options.time&&this.timer = setInterval(()=>{
this.setState({
index: ++index
})
},1000)
options.log&&console.log('组件渲染完成----')
}
componentDidUpdate(){
options.log&&console.log(`我背更新了${++this.show}`)
}
componentWillUnmount(){
this.timer&&clearInterval(this.timer)
options.log&&console.log('组件即将卸载----')
}
render(){
return(<WrappedComponent {...this.state} {...this.props}/>) } } } 复制代码
logTimeHOC
就是一个函数,接受一个组件返回一个新的组件(其实高阶组件就是一个函数)。咱们用这个高阶组件来构建咱们上面的三个组件:github
LogComponent
:打印日志组件redux
class InnerLogComponent extends React.Component{
render(){
return(
<div>我是打印日志组件</div>
)
}
}
// 使用高阶组件`logTimeHOC`包裹下
export default logTimeHOC(InnerLogComponent,{log:true})
复制代码
SetTimeComponent
:计时组件axios
class InnerSetTimeComponent extends React.Component{
render(){
return(
<div> <div>我是计时组件</div> <span>{`我显示了${this.props.index}s`}</span> </div>
)
}
}
// 使用高阶组件`logTimeHOC`包裹下
export default logTimeHOC(InnerSetTimeComponent,{time:true})
复制代码
LogTimeShowComponent
:计时+打印日志组件
class InnerLogTimeShowComponent extends React.Component{
render(){
return(
<div> <div>我是日志打印+计时组件</div> </div>
)
}
}
// 使用高阶组件`logTimeHOC`包裹下
export default logTimeHOC(InnerLogTimeShowComponent)
复制代码
这样不只复用了业务逻辑提升了开发效率,同时还方便后期维护。固然上面的案例只是为了举例而写的案例,实际场景咱们须要本身去合理抽取业务逻辑。高阶组件虽然很好用可是也有一些自身的缺陷:
上面说了不少,无非就是告诉咱们已经有解决功能复用的方案了。为啥还要React Hooks这个呢?上面例子能够看出来,虽然解决了功能复用可是也带来了其余问题。由此官方给我带来React Hooks
,它不只仅解决了功能复用的问题,还让咱们以函数的方式建立组件,摆脱Class方式建立,从而没必要在被this的工做方式困惑,没必要在不一样生命周期中处理业务。
import React,{ useState, useEffect } from 'react'
function useLogTime(data={log:true,time:true}){
const [count,setCount] = useState(0);
useEffect(()=>{
data.log && console.log('组件渲染完成----')
let timer = null;
if(data.time){
timer = setInterval(()=>{setCount(c=>c+1)},1000)
}
return ()=>{
data.log && console.log('组件即将卸载----')
data.time && clearInterval(timer)
}
},[])
return {count}
}
复制代码
咱们经过React Hooks
的方式从新改写了上面日志时间记录高阶组件。若是不了解React Hooks
的基本用法建议先阅读react hook文档。若是想深刻了解setInterval在Hook中的表现能够看这篇从新 Think in Hooks。
假设咱们已经掌握了React Hooks
,那么我来重写下上面的三个组件:
LogComponent
:打印日志组件
export default function LogComponent(){
useLogTime({log:true})
return(
<div>我是打印日志组件</div>
)
}
复制代码
SetTimeComponent
:计时组件
export default function SetTimeComponent (){
const {count} = useLogTime({time:true})
return(
<div> <div>我是计时组件</div> <span>{`我显示了${count}s`}</span> </div>
)
}
复制代码
LogTimeShowComponent
:计时+打印日志组件
export default function LogTimeShowComponent (){
const {count} = useLogTime()
return(
<div> <div>我是日志打印+计时组件</div> <div>{`我显示了${count}s`}</div> </div>
)
}
复制代码
咱们用React Hooks
实现的这个三个组件和高阶组件一比较是否是发现更加清爽,更加PF。将日志打印和记录时间功能抽象出一个useLogTime
自定义Hooks。若是其余组件须要打印日志或者记录时间,只要直接调用useLogTime
这个自定义Hooks就能够了。是否是有种封装函数的感受。
若是让咱们来实现一个React Hooks
咱们如何实现呢?好像毫无头绪,能够先看一个简单的useState
:(这部份内容只是帮咱们更好的理解Hooks工做原理,想了解Hooks最佳实践能够直接查看React 生产应用)
function App(){
const [count,setCount] = useState(0);
useEffect(()=>{
console.log(`update--${count}`)
},[count])
return(
<div> <button onClick={()=>setCount(count+1)}> {`当前点击次数:${count}`} </button> </div>
)
}
复制代码
上面能够看出来当调用useState
时;会返回一个变量和一个函数,其参数时返回变量的默认值。咱们先构建以下的useState函数:
function useState(initVal) {
let val = initVal;
function setVal(newVal) {
val = newVal;
render(); // 修改val后 从新渲染页面
}
return [val, setVal];
}
复制代码
咱们能够在代码中来使用useState
--查看demo;
不出意外当咱们点击页面上的按钮时候,按钮中数字并不会改变;看控制台中每次点击都会输出0,说明useState是执行了。因为val是在函数内部被声明的,每次useState都会从新声明val从而致使状态没法被保存,所以咱们须要将val放到全局做用域声明。
let val; // 放到全局做用域
function useState(initVal) {
val = val|| initVal; // 判断val是否存在 存在就使用
function setVal(newVal) {
val = newVal;
render(); // 修改val后 从新渲染页面
}
return [val, setVal];
}
复制代码
修改useState
后,点击按钮时按钮就发生改变了--修改后Demo
useEffect
是一个函数,有两个参数一个是函数,一个是可选参数-数组,根据第二个参数中是否有变化来判断是否执行第一个参数的函数:
// 实现初版 不考虑第二个参数
function useEffect(fn){
fn();
}
复制代码
ok!不考虑第二个参数很简单,其实就是执行下函数--这里查看Demo(控制台中能看到useEffect执行了)。可是咱们须要根据第二个参数来判断是否执行,而不是一直执行。因此咱们还须要有一个判断逻辑去执行函数。
let watchArr; // 为了记录状态变化 放到全局做用域
function useEffect(fn,watch){
// 判断是否变化
const hasWatchChange = watchArr?
!watch.every((val,i)=>{ val===watchArr[i] }):true;
if( hasWatchChange ){
fn();
watchArr = watch;
}
}
复制代码
完成好useEffect
咱们在去测试下 --测试demo
打开测试页面咱们每次点击按钮,控制台会打印当前更新的count;到目前为止,咱们模拟实现了useState
和useEffect
能够正常工做了。不知道你们是否还记得咱们经过全局变量来保证状态的实时更新;若是组件中要屡次调用,就会发生变量冲突的问题,由于他们共享一个全局变量。如何解决这个问题呢?
useState
useEffect
的问题// 经过数组维护变量
let memoizedState = [];
let currentCursor = 0;
function useState(initVal) {
memoizedState[currentCursor] = memoizedState[currentCursor] || initVal;
function setVal(newVal) {
memoizedState[currentCursor] = newVal;
render();
}
// 返回state 而后 currentCursor+1
return [memoizedState[currentCursor++], setVal];
}
function useEffect(fn, watch) {
const hasWatchChange = memoizedState[currentCursor]
? !watch.every((val, i) => val === memoizedState[currentCursor][i])
: true;
if (hasWatchChange) {
fn();
memoizedState[currentCursor] = watch;
currentCursor++; // 累加 currentCursor
}
}
复制代码
修改核心是将useState
,useEffect
按照调用的顺序放入memoizedState中,每次更新时,按照顺序进行取值和判断逻辑--查看Demo
如上图咱们根据调用hooks顺序,将hooks依次存入数组memoizedState
中,每次存入时都是将当前的currentcursor
做为数组的下标,将其传入的值做为数组的值,而后在累加currentcursor
,因此hooks的状态值都被存入数组中memoizedState
。
上面状态更新图,咱们能够看到执行setCount(count + 1)
或setData(data + 2)
时,先将旧数组memoizedState
中对应的值取出来从新复值,从而生成新数组memoizedState
。对因而否执行useEffect
经过判断其第二个参数是否发生变化而决定的。
这里咱们就知道了为啥官方文档介绍:不要在循环,条件或嵌套函数中调用 Hooks, 确保老是在你的 React 函数的最顶层调用他们。由于咱们是根据调用hooks的顺序依次将值存入数组中,若是在判断逻辑循环嵌套中,就有可能致使更新时不能获取到对应的值,从而致使取值混乱。同时useEffect
第二个参数是数组,也是由于它就是以数组的形式存入的。
固然,react官方不会像咱们这么粗暴的方式去实现的,想了解官方是如何实现能够去这里查看。
在说到React实际工做应用以前,但愿你能对React Hooks有作过了解,知道如useState
、useEffect
、useContext
等基本Hooks的使用,以及如何自定义Hooks,若是不了解能够点击这里了解关于Hook的知识点。
前端页面免不了要和数据打交道,在Class组件中咱们一般都是在componentDidMount
生命周期中发起数据请求,然而咱们使用Hooks时该如何发送请求呢?
import React,{ useState,useEffect } from 'react';
export default function App() {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const result = await axios(
"https://easy-mock.com/mock/5b514734fe14b078aee5b189/example/queryList"
);
setData(result.data); // 赋值获取后的数据
};
fetchData();
});
return (
<div> {data ? ( <ul> <li>{`id:${data.id}`}</li> <li>{`title:${data.title}`}</li> </ul> ) : null} </div>
);
}
复制代码
能够查看Demo,咱们发现页面报错。根据咱们了解到的知识,若是 useEffect 第二个参数不传入,致使每次data更新都会执行,这样就陷入死循环循环了。咱们须要改造下
...
useEffect(() => {
...
},[]);
'''
复制代码
咱们给第二个参数加上一个[]发现页面就能够显示了,将这个Demo中注释解除了。,咱们就能够发现页面正常显示了。
咱们一个程序会有多个组件,不少组件都会有请求接口的逻辑,不能每一个须要用到这个逻辑的时候都从新写或者Ctrl+C。因此咱们须要将这个逻辑抽离出来做为一个公共的Hooks来调用,那么咱们就要用到自定义Hooks。
// config => 指望格式
// {
// method: 'post',
// url: '/user/12345',
// data: {
// firstName: 'Fred',
// lastName: 'Flintstone'
// }
// }
function useFetchHook(config){
const [data,setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const result = await axios(config);
setData(result.data)
};
fetchData();
},[]);
return { data }
}
复制代码
如今咱们就将请求接口的逻辑单独抽出来了,若是那个组件须要使用能够直接引入useFetchHook
这里能够查看Demo。
上面的useFetchHook
虽然能够解决咱们请求接口的问题;若是咱们如今是一个分页接口,每次传入不一样的page都会从新请求,因此咱们还须要修改下:
// watch => 指望格式是 []
function useFetchHook(config,watch){
const [data,setData] = useState(null);
useEffect(() => {
...
},
watch?[...watch]:[] // 判断是否有须要监测的属性
);
return { data }
}
复制代码
点击查看Demo,咱们如今点检页面上的按钮发现页面的数据户一直发生变化,控制台也会打印,说明咱们更改page时都会从新请求接口,上面的问题就解决了。
上面的useFetchHook
虽然能够解决大部分状况,可是一个健全的 接口请求Hooks 还须要告知使用者接口请求状态的成功、失败。咱们继续---
function useFetchHook(config,watch){
// status 标识当前接口请求状态 0:请求中 1:请求成功 2:请求失败
const [status,setStatus] = useState(0);
const [data,setData] = useState(null);
useEffect(() => {
try{
...
setStatus(1) // 成功
}catch(err){
setStatus(2) // 失败
}
},
watch?[...watch]:[] // 判断是否有须要监测的属性
);
return { data, status }
}
复制代码
点击这里能够查看;咱们改造后发现页面按钮多了接口状态,点击时也会发生改变,为了测试失败状态,咱们将 Chrome - network - Offine 改成 offine状态,再次点击发现状态就变成2(失败)。
尚未完呢!使用者知道了状态后能够作相应的 loading... 操做等等;可是对于接口的报错咱们也能够作一个埋点信息或者给一个友善的提示---至于后面怎么写我相信你们均可以发挥本身的想象。下面是useFetchHook
完整代码:
function useFetchHook(config, watch) {
const [data, setData] = useState(null);
const [status, setStatus] = useState(0);
useEffect(
() => {
const fetchData = async () => {
try {
const result = await axios(config);
setData(result.data);
setStatus(1);
} catch (err) {
setStatus(2);
}
};
fetchData();
},
watch ? [watch] : []
);
return { data, status };
}
复制代码
class App extends Component{
render() {
return
<div> <Button onClick={ () => { console.log('do something'); }} /> </div>; } } 复制代码
上面App组件若是props发生改变时,就会从新渲染组件。若是这个修改并不涉及到Button组件,可是因为每次render的时候都会产生新的onClick函数,react就认为其发生了改变,从而产生了没必要要的渲染而引发性能浪费。
class App extends Component{
constructor(){
super();
this.buttonClick = this.buttonClick.bind(this);
}
render() {
return
<div> <Button onClick={ this.buttonClick } /> </div>; } } 复制代码
在类组件中咱们能够直接将函数绑定到this对象上。在Hooks组件中怎么解决呢?
function App(){
const buttonClick = useCallback(
() => { console.log('do something'),[]
)
return(
<div> <Button onClick={ buttonClick } /> </div> ) } 复制代码
如上直接用useCallback
生成一个记忆函数,这样更新时就不会发生渲染了。在react Hooks 中 还有一个useMemo
也能实现一样的效果。
React是从上而下的单向数据流,父子组件之间信息传递能够经过Props实现,兄弟组件的信息传递咱们能够将Props提高到共同的父级实现信息传递,若是组件层级嵌套过深,对开发者来讲是十分痛苦的。因此社区基于redux产生了react-redux工具,固然咱们这里对react-redux作讲解,而是提供一种新的解决方案。
// 建立Context
const AppContext = React.createContext();
const AppDispatch = (state, action) => {
switch (action.type) {
case "count.add":
return { ...state, count: state.count + 1 };
case "count.reduce":
return { ...state, count: state.count - 1 };
case "color":
return { ...state, color: colorArr[getRandom()] };
default:
return state;
}
};
// 建立Provider
const AppProvider = props => {
let [state, dispatch] = useReducer(AppDispatch, context);
return (
<AppContext.Provider value={{ state, dispatch }}> {props.children} </AppContext.Provider> ); }; // ... function Demo3() { // 使用 Context const { state, dispatch } = useContext(AppContext); return ( <div className="demo" style={{ backgroundColor: state.color }} onClick={() => { dispatch({ type: "count.add" }); dispatch({ type: "color" }); }} > <div className="font">{state.count}</div> </div> ); } // ... // 将 AppProvider 放到根组件 ReactDOM.render( <AppProvider> <App /> </AppProvider>, rootElement ); 复制代码
在开始封装经常使用Hooks以前插一个题外话,咱们在开发中时,不可能都是新项目;对于那些老项目(react已经升级到16.8.x)咱们应该如何去使用Hooks呢?其实很简单咱们能够开发一些经常使用的hooks,当咱们老项目有新的功能咱们彻底能够用Hooks去开发,若是对老的组件进行修改时咱们就能够考虑给老组件上Hooks;不建议一上来就进行大改。随着咱们的经常使用Hooks组件库的丰富,后期改起来也会很是快。
在使用Hooks时不免少不了一些经常使用的Hooks,若是能够将这些经常使用的Hooks封装起来岂不是美滋滋!
首先能够建立以下目录结构:
index.js文件
import useInterval from './useInterval'
// ...
export{
useInterval
// ...
}
复制代码
lib中存放经常使用Hooks, 如实现一个useInterval:为啥咱们须要一个useInterval的自定义Hooks呢?
在程序中直接使用 setInterval
function App(){
const [count,setCount] = useState(0);
useEffect(()=>{
console.log(count)
setInterval(()=>{
setCount(count+1)
})
})
return <p>{count}</p>
}
复制代码
上面代码直接运行咱们会发现页面上的 count 越加越快,是因为 count 每次发生改变都致使定时器触发。因此须要每次在清除下定时器:
function App(){
const [count,setCount] = useState(0);
useEffect(()=>{
console.log(count)
const timer = setInterval(()=>{
setCount(count+1)
})
// 清除反作用
return ()=>{ clearInterval(timer) }
})
return <p>{count}</p>
}
复制代码
改动代码后页面好像能够正常显示了,咱们打开控制台能够看到一直会打印 count ,这样对于性能来将无疑是一种浪费,咱们只须要执行一次就能够了,在改下代码
function App(){
const [count,setCount] = useState(0);
useEffect(()=>{
console.log(count)
const timer = setInterval(()=>{
setCount(count+1)
})
return ()=>{ clearInterval(timer) }
},[]) // 添加第二个参数
return <p>{count}</p>
}
复制代码
在看页面,发现控制台好像是只打印一次了,可是页面上的 count 以及不发生改变了,这不是咱们想要的,还须要改变下:
function App(){
const [count,setCount] = useState(0);
useEffect(()=>{
console.log(count)
const timer = setInterval(()=>{
setCount(count+1)
})
return ()=>{ clearInterval(timer) }
},[count]) // 添加 count 变量
return <p>{count}</p>
}
复制代码
Ok!如今好像解决了上面的问题了,可是这个只是一个定时器累加的任务并且只涉及到一个变量,若是是定时执行其余任务,同时有多个变量,那么岂不是又要修改。因此为了解决这个问题咱们迫切须要一个useInterval自定义钩子。
function useInterval(callback,time=300){
const intervalFn = useRef(); // 1
useEffect(()=>{
intervalFn.current = callback; // 2
})
useEffect(()=>{
const timer = setInterval(()=>{
intervalFn.current()
},time)
return ()=>{ clearInterval(timer) }
},[time]) // 3
}
复制代码
简单介绍下useInterval钩子: 1.经过useRef建立一个对象;2.将须要执行的定时任务储存在这个对象上;3.将time做为第二个参数是为了当咱们动态改变定时任务时,能太重新执行定时器。
开发中使用useInterval
以下:
useInterval(() => {
// you code
}, 1000);
复制代码
是否是很简单有很方便,如今将useInterval
放到lib文件夹中,再在index.js文件中导出一下,其余地方要用的时候直接import就能够了。
开放思惟
问题:作一个useImgLazy的 hooks 函数。
为提升网页的性能咱们通常都会网页上图片资源作一些优化,懒加载就是一种方案,useImgLazy就是实现懒加载的 Hooks
// 判断是否在视口里面
function isInWindow(el){
const bound = el.getBoundingClientRect();
const clientHeight = window.innerHeight;
return bound.top <= clientHeight + 100;
}
// 加载图片真实连接
function loadImg(el){
if(!el.src){
const source = el.getAttribute('data-sourceSrc');
el.src = source;
}
}
// 加载图片
function checkImgs(className){
const imgs = document.querySelectorAll(`img.${className}`);
Array.from(imgs).forEach(el =>{
if (isInWindow(el)){
loadImg(el);
}
})
}
function useImgLazy(className){
useEffect(()=>{
window.addEventListener('scroll',()=>{
checkImgs(className)
});
checkImgs(className);
return ()=>{
window.removeEventListener('scroll')
}
},[])
}
复制代码
上面代码逻辑就是 经过getBoundingClientRect
获取图片元素的位置,从而判断是否显示图片真实地址,用useEffect
模拟页面加载成功(onload事件)同时监听scroll
事件。
在须要使用图片懒加载的项目中使用:
function App(){
// ...
useImgLazy('lazy-img')
// ...
return (
<div> // ... <img className='lazy-img' data-sourceSrc='真实图片地址'/> </div> ) } 复制代码
以上useImgLazy
代码我是写这篇文章时忽然诞生的一个想法,没有验证,若是哪位同窗验证后有问题还请告知我在这里是反馈问题,如生产上使用产生问题我一律不负责。
我相信你们看了这篇文章必定会蠢蠢欲动,建立一个自定义 Hooks 。点击这里大家使用过哪些自定义Hooks函数你能够分享、学习其余人是如何自定义有趣的Hooks。
这里能够分享Hooks的最佳实践帮助咱们更快的使用React Hooks说说Hooks中的一些最佳实践##