一个 Vuer 初学 React

前言

学了几天 React hook了,虽然还没有打过几行代码但是还是小有感触,记录一下。

作为一个 Vuer,深深感到 Vue 与 react hook 的思维太不一样了,它们完全是两种思维方式。

不过还是还是有不少 React 的 api 作用可以套用 Vue 的 api 来理解的。

理解 hooks —— useReducer

React 文档总是以 useState 开始介绍 hook

设置 hook 中的状态

import React, { useState } from 'react';

function Example() {
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
复制代码
<template>
	<div>
      <p>You clicked {{count}} times</p>
      <!-- 我实际是 this.count++ -->
      <button @click="count++"> 
        Click me
      </button>
    </div>
</template>
<script>
export default{
	data(){
        return {
            count:0
        }
    }
}
</script>
复制代码

如果你以 Vue 的思维理解这个 Example 组件,这个函数只会执行一次,而下面返回的 render 函数会被执行多次;

显然,函数执行一次是肯定不能完成 jsx 的状态随组件状态改变,比如 count,它设置为 0,这个 jsx 上的 count就是 0,如果只执行一次,jsx 上这里永远都是 0;而 Vue 的模板在编译时做了处理,让模板上绑定的 count 访问到 this.count,因为是引用的值,所以随时都可以访问到最新的值。

正确的理解是,函数组件每一轮渲染都会被执行一次,jsx 也就每一轮都能访问到最新值了。

React 哲学中有个很著名的公式 UI = fn(state) 状态经过处理展现 UI,又总是提及它的函数式特性和数据不可变的特性,看起来高深莫测,其实不然,当你理解了 useReducer 就大致理解了。

我们首先得明白,useState 是 useReducer 的简单版

import React, { useState , useReducer} from 'react';
function Example() {
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0);
  setCount(count + 1)
}
// 等价写法
function Example() {
  // 用 useReducer 声明一个叫 "count" 的 state 变量
  const [count, dispatch] = useReducer((state,action)=>action,0);
  dispatch(count + 1)
}
复制代码

第一个参数接收一个函数,state 表示上一轮接收的值,action 表示上一轮调用 dispatch 传入的值,函数返回值作为当前状态值即 count,第二个参数为初始默认值。

不知道你发现没有,它简直与数组的 reduce 一模一样!

const [count, dispatch] = useReducer((state,action)=>action,0)

[1,2,3].reduce((preCount,curCount)=>curCount,0)

数组是有限的,而状态流是无限的,每一个状态依据上一个状态生成,状态生成后就无法改变了,而 setState 或者 setReducer 就是推进状态流动的动力,React会根据最新的状态重新渲染 UI

不知道你是否能意会 React 的函数式和不可变特性呢?

hooks API

useState,useReducer

useState 和 useReducer 已经提及,一般来说,在状态转移的逻辑比较繁杂时,使用 useReducer 更为合适

useState 和 useReducer 有缓存的特点,如果不调用 setState ,useState 始终返回最后的状态而并非新建一个状态(对于引用类型这很重要)

function Example(){
	const [ref] = useState({ current:1 }) // useState 始终返回第一个传入的对象
    ref.current = ref.current + 1
}
复制代码

useEffect,useMemo,useCallback

三个 api 都是需要依赖项,根据依赖项进行更新且都有缓存的特点。

判断依赖是否更新

react 的判断方式很简单,直接使用 === 符号判断

如果 useState(obj) 时,遵循 react 的不可变特性,应该 setObj({ …obj,a: 1 }) ,而不是 obj.a = 1;setObj(obj)

function Example(prop){
	const [obj,setObj] = useState(prop.obj)
    obj.a = 1
    setObj(obj) // 这不是依赖变更,不会触发更新
}
复制代码

因为 obj 始终是同一个,所以它并不会触发更新。

但是稍不注意,会引发不必要的更新或渲染

function Son(prop){
	useEffect(callback,[prop.arr]) // callback 每次都会执行
}
function Father(){
	Son({arr:[1,2,3]})
}
复制代码

arr 看起来每次传入的都是一样的,实际上它们是不同的数组,所以 Son 的 effect 回调每次都会执行。

useEffect

可以比拟 Vue 中的 watch,第一个参数是执行的函数 callback,第二个参数数组就是它的观察的依赖,当依赖项发生变化,重新执行 callback 并缓存 callback返回值。

同时,它远比 watch 强大,因为它蕴含了生命周期执行时机 ,useEffect 传入的函数一定会执行第一次,执行的生命周期就是 mouted;更新时,执行的生命周期就是 updated;在卸载时, 缓存的 callback 返回值也会被执行一次,执行的生命周期就是 destroyed。

// 传一个空依赖数组,它就可以用作生命周期函数
// 在 mouted 执行一次 callback
export const useMount = (callback: () => void) => {
  useEffect(() => {
    callback()
  }, [])
}
// 在 unmouted 执行一次 callback
export const useUnMount = (callback: () => void) => {
  useEffect(() => () => callback(), [])
}

// 可以围绕 a 写入在挂载和卸载中的逻辑
const DemoEffect = ({ a }) => {
    /* 模拟事件监听处理函数 */
    const handleResize =()=>{}
    useEffect(()=>{
       /* 定时器 延时器等 */
       const timer = setInterval(()=>console.log(666),1000)
       /* 事件监听 */
       window.addEventListener('resize', handleResize)
       /* 此函数用于清除副作用 */
       return function(){
           clearInterval(timer) 
           window.removeEventListener('resize', handleResize)
       }
    },[ a ])
    return (<div  >
    </div>)
}
复制代码

补充:useLayoutEffect

useEffect执行顺序: 组件更新挂载完成 -> 浏览器 dom 绘制完成 -> 执行 useEffect 回调。

useLayoutEffect 执行顺序: 组件更新挂载完成 -> 执行 useLayoutEffect 回调-> 浏览器dom绘制完成。

所以说 useLayoutEffect 代码可能会阻塞浏览器的绘制 。我们写的 effectuseLayoutEffectreact在底层会被分别打上PassiveEffectHookLayout,在commit阶段区分出,在什么时机执行。

useMemo 和 useCallback

Memo 是 Memory 的缩写,顾名思义就是缓存的意思,useMemo 与 Vue 中的 computed 基本一模一样,用途也相同,封装一些逻辑或者避免大量的运算

function Example(){
	const [count,useCount] = count(1)
	const handleCount = useMemo(()=>{
        ...用count进行大量运算
        return result
	},[count]) // count 不变就返回上次运算的结果
}
复制代码

因为 jsx 的动态性,配合 useMemo,可以避免进行重复的 jsx 的运算

/* 用 useMemo包裹的list可以限定当且仅当list改变的时候才更新此list,这样就可以避免selectList重新循环 */
 {useMemo(() => (
      <div>{
          selectList.map((i, v) => (
              <span
                  className={style.listSpan}
                  key={v} >
                  {i.patentName} 
              </span>
          ))}
      </div>
), [selectList])}
复制代码

useCallback 是用来缓存函数的,其实就是 useMemo 的函数专用版;

function Example(){
	const [count,setCount] = useState(0)
    useCallback(()=>count + 1,[count]) //等价于 useMemo(()=>()=>count + 1,[count])
}
复制代码

缓存注意点

三个 api 都有缓存的特性( useEffect 是缓存callback的返回值,即缓存 unmouted 回调函数),仅在依赖变更时,才更新缓存。既然是缓存,那么它使用的一定是缓存时的状态而并非当前的状态

function Example(){
	const [count,useCount] = count(1)
	useEffect(()=>{
        return ()=>console.log(count)
    },[])
    const logCount = useCallback(()=>console.log(count),[])
    setCount(count + 1)
}
复制代码

在 unmouted 阶段或者调用 logCount 会打印什么呢?如果你的答案不为 1,说明你对“缓存”的理解还不够透彻。它们都打印出 1,因为它们只在初始化执行过一次,此时的状态被函数的闭包保存,后续没有更新缓存,所以闭包上的 count 一直都是初始化的值 1。

一般情况下函数内部使用的依赖都应该写入依赖数组里。

如果函数内使用了依赖却没写入依赖数组,eslint 会报错以防止这种看起来疑惑的表现

Vue 的 computed 是在第一次执行函数时,自动收集使用到的所有依赖

useRef

我个人认为是一种以显示声明的方式摆脱不可变特性的一个 api ,useRef 会返回一个对象,对象上只有一个属性就是 current 属性。

useRef 初始化后,每次都返回第一次初始化的对象,所以在不同的状态下,我们都可以用 ref.current 访问到最新的 current 值,它挺像 Vue 中的 this

  • 用法一:可以用来获取dom元素,或者class组件实例 。
const DemoUseRef = ()=>{
    const dom= useRef(null)
    const handerSubmit = ()=>{
        /*  <div >表单组件</div>  dom 节点 */
        console.log(dom.current)
    }
    return <div>
        {/* ref 标记当前dom节点 */}
        <div ref={dom} >表单组件</div>
        <button onClick={()=>handerSubmit()} >提交</button> 
    </div>
}
复制代码

ref 属性会自动将 div 的 dom节点(如果是类组件就是组件的实例,如果是函数组件需要使用 useImperativeHandle) 赋予 dom.current

  • 用法二: 通过 useRef 保存一些不同于当前状态的数据。
// 上面函数的迷惑表现用 ref 就不会出现了
function Example(){
	const countRef = useRef(1)
    const {current:count} = countRef
	useEffect(()=>{
        return ()=>console.log(count)
    },[])
    const logCount = useCallback(()=>console.log(count),[])
    countRef.current = current + 1
}
复制代码
// 这个组件作用是 初始化用 ref 保存初始 title,使用时传入新的 title,document.title 就更改
// 组件挂载时,恢复页面标题为初始 title
export const useDocumentTitle = (title: string, keepOnUnmount = true) => {
  const oldTitle = useRef(document.title).current;
  // 页面加载时: 旧title
  // 加载后:新title

  useEffect(() => {
    document.title = title;
  }, [title]);

  useEffect(() => {
    return () => {
      if (!keepOnUnmount) {
        // 如果不指定依赖,读到的就是旧title
        document.title = oldTitle;
      }
    };
  }, [keepOnUnmount, oldTitle]);
};
复制代码

不知道你是否记得第一个例子,其实原理上 useRef 并没有多大区别

function Example(){
	const [ref] = useState({ current:1 })
    ref.current = ref.current + 1
}
复制代码

为何不能用上面的方式呢?因为 state 在 React 是不可变的,useRef 就是显式表明了引用,React 开了一道口子

useImperativeHandle

使用 ref 引用某个组件时,class 组件引用到实例,dom 引用到真实的 dom 节点,而函数组件没有实体,无法用 ref 直接引用,useImperativeHandle 的出现就是解决这个问题,它给予函数组件自定义暴露给父组件 ref 对象的能力

useImperativeHandle接受三个参数:

  • 第一个参数ref: 接受 forWardRef 传递过来的 ref
  • 第二个参数 createHandle :处理函数,返回值作为暴露给父组件的ref对象。
  • 第三个参数 deps:依赖项 deps,依赖项更改形成新的ref对象。
function Son (props,ref) {
    const inputRef = useRef(null)
    const [ inputValue , setInputValue ] = useState('')
    useImperativeHandle(ref,()=>{
       // 自定义 ref
       const handleRefs = {
           /* 声明方法用于聚焦input框 */
           onFocus(){
              inputRef.current.focus()
           },
           /* 声明方法用于改变input的值 */
           onChangeValue(value){
               setInputValue(value)
           }
       }
       return handleRefs
    },[])
    return <div>
        <input
            placeholder="请输入内容"
            ref={inputRef}
            value={inputValue}
        />
    </div>
}
复制代码

useContext

在 hook 时代新的状态管理方式

  • 首先使用 React.createContext() 创建 Context
  • Context 上的属性 Provider 和 Consumer,它们都是虚拟组件
  • Provider 提供属性,Customer 接收属性
  • useContext 可以代替 Customer 接收属性
// 创建上下文
const AuthContext = React.createContext(undefined)
AuthContext.displayName = 'AuthContext'
// 作为包装层
export const AuthProvider = ({ children }: { children: ReactNode }) => {
  const [user, setUser] = useState(null)
  // 作为内部共享的值
  return (
    <AuthContext.Provider
      children={children}
      value={{ user }}
    />
  )
}

export const useAuth = () => {
  const context = React.useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth 必须在 AuthProvider 中使用')
  }
  return context
}

// 某个子组件内
<AuthContext.Consumer>
  {({user}) => /* 基于 context 值进行渲染*/}
</AuthContext.Consumer>
// 或者用 useAuth
const {user} = useAuth() // 后面 jsx 就可以用到这个数据了
复制代码

Provider 和 Customer 顾名思义,提供者和消费者,其实它与 Vue 的 provide 和 inject 非常相似,一个外层提供数据,内层消费数据,不过 React 是更为明显得将各个提供者,消费者也进行了划分。

hook 魔法

使用hook总是有些疑问,令我不安

  • useState 等 hook 是如何与状态一一对应起来的?

    function Example(){
    	const [count1,setCount1] = useState(0)
    	const [count2,setCount2] = useState(0)
    }
    复制代码

    同样的输入,为何第一个 useState 就是返回 count1 而 第二个 useState 返回 count2?

  • 多处使用 Example 组件,如何做到每处都像类组件一样拥有自己的状态?

首先函数组件自己本身肯定是无状态的,但是别忘了多处使用的函数组件是由 React 调用的,所以它肯定为每处的函数组件生成了包含了此次函数组件信息对象,就是 workInProgress

React 执行函数组件的时,会为每个hook生成一个hook 节点,hook 节点的 next 指向下一个 hook 节点,这个 hook 链表头节点挂在 workInProgress.memoizedState 上

因为是按照顺序记录的,下一次函数组件执行时,某个位置就可以访问到的上次状态对应位置的值了。这也是 React 要求 hook 一定要放在最外层的原因,因为如果某个 hook 不执行,next 指针就会错乱,后续的 hook 访问值就可能错乱

Css in JS

react 如何编写 css 呢?一个很流行的方案就是 css in js,一个出名的实现库就是 emotion

import styled from "@emotion/styled";

export const Row = styled.div<{
  gap?: number | boolean;
  between?: boolean;
  marginBottom?: number;
}>`
  display: flex;
  align-items: center;
  justify-content: ${(props) => (props.between ? "space-between" : undefined)};
  margin-bottom: ${(props) => props.marginBottom + "rem"};

  > * {
    margin-top: 0 !important;
    margin-bottom: 0 !important;
    margin-right: ${(props) =>
      typeof props.gap === "number"
        ? props.gap + "rem"
        : props.gap
        ? "2rem"
        : undefined};
  }
`;
// jsx 中使用时
<Row between={ true } marginBottom={ 11 }  />
复制代码

可以看到使用很简单

  • styled.div 被适用 css 的标签
  • “` ` 是 es6 中的语法 “模板标签
  • 最大的特点就是复用静态的 css,通过传入 prop 定义动态css;Vue中的方案是切换 css 类名或者动态的行内 style
  • 可以方便得复用伪类(style 中无法使用伪类)
  • 缺点:没有高亮,没有 css 提示

我个人使用感觉,在使用 css in js 定义某些小型的通用组件是一种非常不错的方案,不过在业务组件等还是使用 Vue 的 <style scoped></style>是最舒服的,不过目前 react 并没有官方方案

参考

「React进阶」 React全部api解读+基础实践大全

「react进阶」一文吃透react-hooks原理

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享