Hooks

359 阅读7分钟

1. 简介

  • 什么是UI(头/尾) => Hooks解决方案 => API&使用建议 => 最佳实践demo => 闭坑指南
  • 什么是UI
    • UI = f(data) 函数f将数据(data)映射到用户界面(UI)
  • 状态是数据吗?
  • 状态(state)是什么
    • 状态有一个隐含的意思,就是存在改变状态的行为(behavior)
    • 例如:点赞数隐含了一个点赞的行为,而点赞这个行为只有在某种上下文(context)中才会存在

2. Hooks基本概念

  • 如何描述UI(User Interface)
  • UI = 数据 => 视图 => 消息(输入&操作) => 重算 => 数据 => 视图 ...
    • 消息和重算可以合并成一种行为
  • UI = 数据 => 视图 => 行为 => 数据 => 视图 ...
    • 行为有同步行为和异步行为,包含重算的逻辑
    • 数据不对应行为,把状态对应行为
    • 数据拆分:不变的是属性,变化的是状态
    • 以不变的属性传入视图,用行为去驱动状态

image.png

  • 再简化
    • 视图只需要感知状态,而不需要感知行为

image.png

  • 再抽象出作用的概念
    • 与状态变化有关的行为
    • 与上下文存在的状态,会改变视图的行为,类似于跳转等,根据与当前视图无关的上下文状态有关的行为
    • 行为本身就是作用的一种

image.png

  • 最后抽象

image.png

  • 与视图的关联(状态 & 作用 & 上下文)都希望做到松散的耦合,放在外面,从而做到复用

  • 关联的关系称之为Hooks

  • 重新定义UI界面

    • state hook 状态的hook
    • effect hook 作用的hook
    • context hook 上下文的hook
    • 定义:函数V(视图) = f(props, state)
    • UI = V usehook1() usehook2() ... 视图使用了第一个hook,第二个hook ...

3. Hooks Api

3.1 基本用法

  • 三个基础hook:状态、作用、上下文

3.1.1 状态

  • 在某个上下文中(用户界面)数据和改变数据的行为
const [count, setCount] = useState(0)
// [状态,行为] = hooks API
// React hooks会把数据和行为绑定
  • 基本使用
import React, { useState } from "react";

export default () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
};

  • 简单的抽离hooks
    • 封装一个addCount的行为
    • 改变count是依靠addCount的行为,把这个hook挂在视图上,和UI解耦,触发行为即可,根据状态渲染
import React, { useState } from "react";

function useCount(initialValue) {
  const [count, setCount] = useState(initialValue);
  return [
    count,
    () => {
      setCount(count + 1);
    }
  ];
}

export default () => {
  const [count, addCount] = useCount(0);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => addCount()}>+1</button>
    </div>
  );
};

  • 对比state以及setState方式
//相同类型的事务要有相同类型的行为,不能组合成一团处理,要分散解耦
state = { count1: 0, count2: 1}
setState({count1..., count2... })
  • 状态和背后的行为封装在一起,才是一个独立的行为模块
import React, { useState } from "react";

function useCount(initialValue) {
  const [count, setCount] = useState(initialValue);
  return [
    count,
    () => {
      setCount(count + 1);
    }
  ];
}

export default () => {
  const [count1, addCount1] = useCount(0);
  const [count2, addCount2] = useCount(0);
  return (
    <div>
      <p>{count1}</p>
      <p>{count2}</p>
      <button onClick={() => addCount1()}>+1</button>
      <button onClick={() => addCount2()}>+1</button>
    </div>
  );
};

3.1.2 作用(Effect)

  • UI不仅仅是一个将数据映射到视图的函数

  • 客观世界存在输入输出以外的东西

    • 改变URL
    • console.log()
    • set cookie 的过程...
  • 基本用法

import React, { useState, useEffect } from "react";

export default () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(`you clicked count ${count} times`);
  });

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
};
// useEffect每次重新render都会创建内部新函数
  • 此种好处是每次重新render不用创建新函数
  • useEffect中依赖(deps)的用法
import React, { useEffect, useState } from "react";

function log(count) {
  console.log(`you clicked count ${count} times`);
}
export default () => {
  const [count, setCount] = useState(0);

  // 读作:依赖[]中状态变化的作用
  // 不写[], 只要状态有变化就触发
  // 依赖发生变化就会执行一个新的效果
  useEffect(log.bind(null, count), [count]);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
};
  • 跳转demo
import React, { useEffect, useState } from "react";

export default () => {
  const [count, setCount] = useState(0);

  useEffect(()=>{
    if(count > 5) {
      window.location.href="https://www.baidu.com"
    }
  }, [count]);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
};
  • 副作用的销毁以及传值的小坑(箭头函数,闭包)
  • 不要直接set,用函数的形式set
import React, { useEffect, useState } from "react";

function useInterval(callback, time) {
  // 只执行一次定时器,依赖[]
  useEffect(() => {
    const I = setInterval(callback, time);
    return () => {
      clearInterval(I);
    };
  }, []);
}

export default () => {
  const [count, setCount] = useState(0);

  useInterval(() => {
    console.log(count);
    setCount((count) => count + 1);
  }, 1000);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
};

3.2 拟人方法理解Context

  • UI产生过程中,能够从context中获取信息(知识)
  • UI更像一个人而不是机械的结构
UI => (data) => {
  const { userType } = useContext(userTypeContext)
  switch(userType) {
    ...不同的渲染逻辑
  }
}
  • 点击切换按钮颜色
import React, { useContext, useState } from "react";

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

// 创建context
const ThemeContext = React.createContext({
  theme: themes.light,
  toggle: () => {}
});

// 传递Context,React.createContext创建
// value中重写Context值
// set中函数传递,不要直接setTheme
// value 中传递知识,下面包含的组件都要理解
export default () => {
  const [theme, setTheme] = useState(themes.light);
  return (
    <ThemeContext.Provider
      value={{
        theme,
        toggle: () => {
          setTheme((theme) => {
            setTheme(theme === themes.light ? themes.dark : themes.light);
          });
        }
      }}
    >
      <Toolbar />
    </ThemeContext.Provider>
  );
};

const Toolbar = () => {
  return <ThemedButton />;
};

// 传递理解Context用useContext使用
const ThemedButton = () => {
  const context = useContext(ThemeContext);
  return (
    <button
      style={{
        fontSize: "32px",
        color: context.theme.foreground,
        background: context.theme.background
      }}
      onClick={() => {
        context.toggle();
      }}
    >
      Click Me!
    </button>
  );
};

3.3 Reducer的作用和使用场景

  • reducer 减压器,是一个设计模式,也是一个计算过程的抽象
  • reducer是一个函数,把很多意图(action)映射成一个状态(state),state驱动视图渲染,dispatch是发送一个action的方法
  • dispatch是派发,派发一个action
  • useState没有抽象成一套

image.png

// 派发 action,改变state
import React, { useReducer } from "react";

const initialState = { count: 0 };

function reducer(state, action) {
  console.log(state, action);
  switch (action.type) {
    case "add":
      return { count: state.count + 1 };
    case "sub":
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

export default function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      {state.count}
      <button
        onClick={() => {
          dispatch({ type: "add" });
        }}
      >
        +
      </button>
      <button
        onClick={() => {
          dispatch({ type: "sub" });
        }}
      >
        -
      </button>
    </div>
  );
}

3.4 ref引用

  • 引用行为 reference,引用一个对象

  • 引用React管理以外的对象

    • 需要在React之外做一些事情,例如,input的focus,要拿到输入框的实例,媒体操作等,例如canvas,video,React不管理这些东西,让用户用引用自己管理
    • 引用通常搭配作用useEffect使用
  • 附带作用:方便的保存值

  • 点击focus,输入框聚焦

import React, { useRef } from "react";

export default function App() {
  const refInput = useRef();
  return (
    <div>
      <input ref={refInput} />
      <button
        onClick={() => {
          refInput.current.focus(); 
          //refInput.current 拿到当前值
        }}
      >
        Foucs
      </button>
    </div>
  );
}
  • 暂存上一次的值
import React, { useRef, useState } from "react";
//每次重新渲染都会创建一个新的App
// 全局定义一个值,污染每次的App const prev = null;
export default function App() {
  const [count, setCount] = useState(0);
  const prev = useRef(null);

  return (
    <div>
      <p>当前值:{count}</p>
      <p>之前值:{prev.current}</p>
      <button
        onClick={() => {
          prev.current = count;
          setCount((x) => x + 1);
        }}
      >
        Click Me to add
      </button>
      <button
        onClick={() => {
          prev.current = count;
          setCount((x) => x - 1);
        }}
      >
        Click Me to remove
      </button>
    </div>
  );
}

3.5 缓存设计

  • 为什么要缓存

    • V视图 = f(state, props) useHooks 渲染视图并用了hooks
    • 想在f中 new Object(),只能创建一次
    • 一些复杂计算只有在状态改变后才做
  • 缓存一个函数 useCallback

  • 缓存一个值 useMemo

  • 缓存值

import React, { useMemo, useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);
  const memorizedText = useMemo(() => {
    console.log("run useMemo function");
    return `this is a memorized text ${Date.now()}`;
  }, [Math.floor(count / 10)]);
  // 逢10 缓存刷新
  // [count % 10 === 0] 10和11时候true false变换了两次
  return (
    <div>
      {memorizedText}
      <p>U clicked {count} times</p>
      <button
        onClick={() => {
          setCount((x) => x + 1);
        }}
      >
        Click Me
      </button>
    </div>
  );
}
  • 每次渲染都会创建新的add函数
import React, { useState } from "react";

const s = new Set();

export default function App() {
  const [count, setCount] = useState(0);
  function add() {
    setCount((x) => x + 1);
  }
  s.add(add);
  console.log(s.size);  // 每次都在创建add函数,点击一次长度加一
  return (
    <div>
      <p>U clicked {count} times</p>
      <button onClick={add}>Click Me</button>
    </div>
  );
}
  • 缓存函数,除非影响性能,否则意义不大,阅读成本
import React, { useCallback, useState } from "react";

const s = new Set();
export default function App() {
  const [count, setCount] = useState(0);

  const add = useCallback(() => {
    setCount((x) => x + 1);
  }, []);
  s.add(add);

  console.log(s.size); // 只创建一次

  return (
    <div>
      <p>U clicked {count} times</p>
      <button onClick={add}>Click Me</button>
    </div>
  );
}

3.6 避坑指南 & 使用建议

3.6.1. 使用memo减少重绘次数

6.6.2. hooks同步问题

import React, { useEffect, useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setInterval(() => {
      console.log(count);  // 一直输出0 显示count在变化
      setCount((x) => x + 1);
    }, 1000);
  }, []);

  return (
    <div>
      <p> {count} times</p>
    </div>
  );
}
import React, { useEffect, useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  function myEffect() {
    const I = setInterval(() => {
      console.log(count);  // 更改依赖 count有变化
      setCount((x) => x + 1);
    }, 1000);
    return () => clearInterval(I);
  }

  // 效果的依赖是空,效果只执行使用一次
  // myEffect只有第一次生效,每次重新渲染,都会产生一个新的myEffect, 但是只有第一次的生效
  // 第一个count是0,这个count在第一个函数的闭包中被创建,后面每次都是拿第一个count 0
  // 要想更新console的count,必须更新依赖,但是每次都会创建新的setInterval,必须回收
  // 如果每次依赖count重新创建myEffect产生新的效果,销毁旧的效果,setInterval就变成了setTimeout
  useEffect(myEffect, [count]);

  return (
    <div>
      <p> {count} times</p>
    </div>
  );
}

3.6.3. 可以构造自己的hooks封装行为

  • 模拟获取列表
import React, { useEffect, useState } from "react";

function sleep(time) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, time);
  });
}

async function getPerson() {
  await sleep(200);
  return ["a", "b", "c"];
}

function usePerson() {
  const [list, setList] = useState(null);
  async function request() {
    const list = await getPerson();
    setList(list);
  }
  useEffect(request, []);
  return list;
}

export default function App() {
  const list = usePerson();
  if (list === null) {
    return <div>loading</div>;
  }
  return (
    <div>
      {list.map((name, i) => {
        return <li key={i}>{name}</li>;
      })}
    </div>
  );
}

3.6.4. 每种行为一个hook

  • 每一个hook应该是一种行为
  • 网络请求是一个行为
  • 耦合示例,这应该是不同行为操作的
import React, { useEffect, useState } from "react";

export default function App() {
  const [state, setState] = useState({
    count: 0,
    company: "Apple"
  });
  return (
    <div>
      {state.count}
      {state.company}
      <button
        onClick={() => {
          setState((prev) => {
            return {
              company: prev.company,
              count: prev.count + 1
            };
          });
        }}
      >
        +
      </button>
    </div>
  );
}

3.6.5. 不要考虑生命周期

4. 一个拖拽列表

  • 左边视图(data = state + props),右边行为(状态 + 事件)

image.png

import React, { useEffect, useState } from "react";

const list = [
  {
    src:
      "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fwww.xkzzz.com%3A8080%2Fzb_users%2Fupload%2F2017%2F12%2F20171212134240_55395.png&refer=http%3A%2F%2Fwww.xkzzz.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1627636569&t=6c8a0114e80f2a528f9331b1cca8ab18",
    title: "React"
  },
  {
    title: "Angular",
    src:
      "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.mukewang.com%2F5801a11f000140fa03120325.png&refer=http%3A%2F%2Fimg.mukewang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1627636608&t=22b5288cbac4af4128f73299491e5232"
  },
  {
    title: "Vue",
    src:
      "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20190114135012858.png&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1627636640&t=a7316f589673a0a93eea2b688ca40540"
  }
];

export default function App() {
  return (
    <div className="App">
      <Card {...list[0]} />
    </div>
  );
}

function DraggableList() {}

function Draggable() {}

// 占空间的部分
function Bar() {}

function Card({ title, src }) {
  return (
    <div className="card">
      <img src={src} />
      <span className="text">{title}</span>
    </div>
  );
}