2-2-4 & 5 & 6 React Hooks基础API

- useState

- useEffect

- useRef

- useMemo

- useCallback

- useContext

useState hook

作用:管理状态,并当状态发生变化的时候反向通知React重绘。

通知方向: state hook -> React Component -> render

import {useState} from 'react'
function App(){
    const [count, setCount] = useState(0)
    const [count, setCount] = useState<number>(0)
    const [count, setCount] = useState(() => 0)
    
    return <div>
        {count}
        <button onClick={() => setCount(x => x + 1)}>+</button>
    </div>
    
}

useEffect hook

当React渲染时,hook中的函数根据依赖变化发生调用。

import {useEffect, useState} from 'react'
import {timer} from 'rxjs'


function App(){
    
    const [count,setCount] = useState()
    

    useEffect(() => {
        const subscription = time(0, 1000)
        	.subscribe(() => {
                setCount( x => x + 1)
            })
				return () => {
            subscription.unsubscribe()            
        }
    }, [0])

    
    useEffect(() => {
        console.log("count changed to", count)
    }, [count])
    return <div>
        {count}
    </div>
}

每次渲染的时候`useEffect` 都会调用,React通过判断依赖(deps)有无发生变化决定是否调用`useEffect` 中的函数。这样就将函数调用和声明式的编程统一。

去掉`useEffect` ,函数组件看上去很【纯】,像是在靠props和state渲染,加上`effect` 函数就不纯了,因为做了渲染之外的事情,比如设置定时器、打印日志、网络请求……

所以为什么叫Effect? 我们通常将计算函数返回值之外的事情都称作Effect。当这种Effect会产生负面效果,就称作Side Effect。

`useEffect` 将渲染之外的事情收敛,集中管理。

useRef hook

这个Hook让函数组件可以在多次渲染间同步引用值。 它为什么是钩子?谁触发它?其实就是每次渲染的时候触发这个hook,然后它负责在保存一个引用。

export default function LogButtonClicks() {
  const countRef = useRef(0);  

  const handle = () => {
    countRef.current++;
    console.log(`Clicked ${countRef.current} times`);
  };
  console.log('rendered')

  return <button onClick={handle}>Click me</button>;
}

利用这种机制,子组件可以向父组件同步数据:

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

思考:`useRef` 每次渲染的时候都调用,是如何做到只初始化一次的?

例如下面这个程序:

export default function LogButtonClicks() {
  const countRef = useRef(0);   
  const [ver, setVer] = useState(0)

  const handle = () => {
    countRef.current++;
    console.log(`Clicked ${countRef.current} times`);
    setVer(ver => ver + 1)
  };  

  return <button onClick={handle}>Click me</button>;
}

React通过记录useRef的**序号**同步引用。 比如countRef是函数组件的第0个Ref,存放在位置0。第一次渲染的时候,React查看位置0中是否有值,如果没有初始化,就调用初始化函数/使用初始值。如果有,就不再初始化。

划重点:hooks本质是一种对行为的描述,不可以在任何流程控制语句中使用。

请求数据的逻辑放 useEffect 里

useMemo hook

`useMemo` hook允许我们在闭包内根据依赖缓存数据。

`useMemo` 的本质是在依赖发生变化的时候,通知React具体的VirtualDOM实例更新自己内部useMemo对应的状态。

`useMemo` 和`useHook` 非常相似,`useHook`帮助函数组件在它的多次调用间同步实例数据。

思考下面这个程序中:

function foo(){
    const x = useRef(1)
    const y = useRef(2)
}

`x`  的值通过引用对象被保存了下来。这个引用对象在哪里? 在React的虚拟DOM对象上。`useRef` 保证了什么?

`useRef` 保证如果是:

- 相同的虚拟DOM对象(比如foo可以被多个虚拟DOM对象使用)

- 相同的位置的`useRef` (比如上面程序中x,y是不同位置的useRef)

那么会拿到相同的ref对象。这个对象可以通过`current` 属性访问,比如`x.current` `y.current`。

从这个角度看`useRef` 帮助我们在一个闭包内缓存**per instance, per location**的数据。

但是如果我们想要根据某种依赖关系更新`x` ,就需要这样做:

function foo(){
    const x = useRef(1)
    
    useEffect(() => {
        // 更新x的逻辑
    }, [someDeps])
}

如果用`useMemo` 问题就得到了简化:

function foo(){
    const x = useMemo(() => {
        // 重新计算x的值      
    }, [deps])
}

而且也不需要`x.current` 引用。 `x` 将成为一个完全由依赖推导出来的值,用户不可以随意设置。

从设计角度,`useMemo` 也在帮助缓存`per instance` `per location`  的数据,只不过增加了一个**计算依赖**,和一个**计算函数**。

这样设计的好处是什么?举一个具体的场景。

function Button(){
  return <div onClick={
    useMemo(() => e => {
        console.log('onclick')
    }, [])
  }
}

上面的程序中onClick方法不会每次Button创建都被创建,相当于在`Button` 闭包内缓存了一个变量,它的值是函数。

当然, 有同学会挑战这段程序——**为了微优化降低了程序的可读性!**

说的没错! **不应该这样写!**应该让onClick的handler每次都重新创建,去消耗内存、消耗计算,来换取可读性。代码是用来读的,说到底人力成本最贵。

所以为了讲述`useMemo` 的用法,我还得再举几个真实场景的例子。

所以为了讲述`useMemo` 的用法,我还得再举几个真实场景的例子。

例子1:缓存对象

const node = useMemo<DragNode>(() => new DragNode(), [])

通过`useMemo` 语义清晰的缓存对象,替代`useRef`

例子2:实现复杂的计算逻辑

function complexComputation(a, b, c) {
   // ....
}

const result = useMemo(
    () =>complexComputation(a, b, c), [a,b,c])

如果有依赖明确的复杂的计算,`useMemo` 可以帮助你完成。这样在依赖`[a,b,c]` 不变动的时候,complexComputation就不会多次触发。这虽然是一个场景,可惜的是,**这样的场景是很少的**。因为前端很少有复杂的计算。甚至,当`complexComputation` 需求真的存在时,也许不用`useMemo` 更好,因为这很可能又是影响可读性的**微优化**!

例子3: 让子组件永不更新

类似skedo中父子组件的关系,它们都依赖Node类。因为Node类继承于Emitter接口,会完成`Point to Point` 的消息通知——因此在Skedo的设计中是不需要父组件渲染连带子组件渲染的。

function ParentComponent(props){
    return useMemo(() => <ChildComponent someProp={props.fooProp} />, [props.someProp])
}

function ChildrenComponent() {
    return <div>....</div>
}

虽然上面这个程序很神奇,但是如果你真的有这种场景,不妨大胆使用。

useCallback hook

`useCallback` 可以看做`useMemo` 的一个语法糖。

例如:

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
)

等价于:

const memoizedCallback = useMemo(
  () => {
      return () => {
          doSomething(a, b)
      }
  }
, [a, b])

总体来说,`useCallback`相当于帮你省去了一级闭包。

官方文档中这样说:

`useCallback(fn, deps)`  is equivalent to `useMemo(() => fn, deps)`.

还记得之前我们`onClick`函数吗? 下面是`useCallback` 的实现:

<div onClick={useCallback(e => {
        console.log("click")
}, [])}

是不是看着清爽很多,~ 但是还是不建议这样做,因为没必要在牺牲可读性的基础上做性能的微优化。

`useMemo` 和`useCallback` 其实是两个低频能力。 总体来说,它们和`useRef` 能力是相似的,都是在闭包间同步一个`per virtualdom instance` `per location` 的值。类似一个静态的,基于词法作用域的缓存。

虽然我不主张用`useMemo` 和`useCallback` 做微优化。 不过作为一个框架,不能阻止用户`useMemo` 和`useCallback` 进行优化。

有这样一段程序:

function useThrottledState<T>(initialState : T, interval = 16) : [T, (val : (T|(() => T))) => void] {
	const state = useRef<T>(initialState)
	const [, setVer] = useState(0)

	const setState = useMemo(() => {
		const fn = (val : T | (() => T))  => {
			if(isFunction(val)) {
				val = val()
			}
			state.current = val
			setVer(x=>x+1)
		}

		return throttle(fn, interval)
	}, [])

	return [state.current, setState]

}

这个程序存在是为了防止高频的`setState`带来的组件高频刷新,用`useThrottledState`代替`useState`。

用法如下:

const [x, setState] = useThrottledState(customData, 100)

这样setState无论调用频率如何,最终刷新频率会在每100ms一次。

useContext hook

这个Hooks将父组件设置的上下文下发,是一种被高频使用、重要的技巧。

上下文是什么?上下文是组件共同享有的信息。

举个例子,我们有一个全局的用户对象,所有组件都需要使用:

通过调用`user.loginStatus` 可以知道用户当前是否登录。

class User {
    
    loginStatus : UserStates
    
    public isLoggedIn() : boolean {
        // ...
    }
    
    public onLoginStatusChanged(handler : Handler) {
        // ...
    }
}

像这样很多组件都依赖的状态就可以用Context下发。

Step1: 创造一个上下文类型

import {createContext} from 'react'
const UserContext = createContext(null)

export default UserContext

Step2:在某个节点提供上下文

<UserContext.Provider value={new User()}>
    <App />
</UserContext.Provider>

Step3:需要用户信息的时候使用这个上下文

import {useContext} from 'react'
import UserContext from '...UserContext'

// 在某个React组件中
const user = useContext(UserContext)

const logined = user.isLoggedIn()

应用场景

总的来说,系统都会有上下文。比如主题色、用户状态等等。

可不可以用 Context传参?

上下文也不局限于此,比如说如果你的系统中某个全局的API地址是需要且换的,比如说某个可以切换到开发环境的调试功能,这个时候Context都很有用。

注意,Context不要作为父子组件传参的渠道。 最核心的原因是语义不符——

Context(上下文)是组件间共享的信息。从这个意义上,你就不应该拿Context传参。写程序一定要符合语义,那么对于阅读者就好理解。

明确下,划重点:Context仅仅用于组件间共享的上下文类信息。什么是上下文类,就是系统设计中大部分组件都需要依赖的数据。

既然不用Context,怎么做跨组件传参?

现在组件关系是A->B->C。

当组件A向自己的孙子组件C传参的时候,一种就是透传。

function B(props){
    return <C {...props} />
}

还有一种就是利用`redux`类似工具直接从State中选择。

const {useSelector} from 'react-redux'

function C() {
    const data = useSelector(state => state.productList)
    
    return <div>...</div>
}

这类工具本质是发布订阅模式,相当于组件监听某个store,然后当store发生变化的时候再从store中取走数据。注意Store的这个语义是数据仓库——因此可以用来存组件C单独依赖的数据。而`productList` 放在Context中就不合适。

划重点:**程序实现除了技术类约束外,大家要重视语义的约束,做到专项专用。**

思考:为什么不把productList从C的爷爷组件A中传下来。

1. passProps太多会造成混乱(比如重名)

2. 造成没有必要的耦合

   - 是C需要productList,但是却经过了B

那么如果你有一个产品列表,多个组件需要监听,如果不想用Redux这类工具,又不想将数据从父节点下传,应该如何做呢?

这个时候可以考虑自己实现一个小型的封装

function C(){
    // ProductStore内部是一个单例
    const product = useMemo(() => new ProductStore(), [])
    const [,setVer] = useState(0)
    const productList = product.getList()
    
    useEffect(() => {
        product.on("list-changed", () => {
            setVer(x => x + 1)
        })
    }, [])
    
    return <div>{productList.map(...)</div>
}

1

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值