一个 Vuer 初学 React

我心飞翔 分类:javascript

前言

学了几天 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 上

一个 Vuer 初学 React

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

一个 Vuer 初学 React

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原理

回复

我来回复
  • 暂无回复内容