React前端-hooks常用属性

172 阅读14分钟

前言

React 推出 react-hooks,为了让开发者更方便的开发应用

其可以不用可以理会 React.Componet的生命周期,对与不关注这些的来说,简直是一个福音,另外他减少了大量的开发代码,尤其是 state相关,尤其是 typescript,可以不用可以维护 statetype,每个state状态单独定义,且使用更为方便

react-hooks 参考地址

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

useContextcreateContext基本上是不可分割的,其主要用于在各个页面共享一些数据

案例是typescriptjavascript请自行简化

静态参数使用

一般直接用的比较多的就是当做静态数据配置信息等

//这里将参数设置为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 的替代方案,或者简化版本

useReduceruseState的替代方案,看到它,估计很多人会想到 redux,他算是一个 redux的简化版,当我们多个组件之间需要进行交互的时候,或者内部组件嵌套比较深的时候,可以将部分 useState 换成它,以优化代码交互

initialState:保存的 state 状态,用于显示和更新渲染内容,和组件中的 state 类似

reducer:更新组件state的实现,同时也在这里也可以负责分发事件,最终返回新的 state 渲染节点,注意分发事件平衡

reducer-stateuseReducer返回的第一个参数,为state状态,可以取内部属性用来渲染节点

reducer-dispatchuseReducer返回的第二个参数,其用来传递参数给 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 中照样可以使用,快来尝试一下吧,会让你爱不释手