不知道大家有没有发现随着版本的升级 vuereact 越来越像了。

2019年年初,react16.8.x 版本正式具备了 hooks 能力。

2019年6月,尤雨溪提出了关于 vue3 Component API 的提案。笔者理解这其实是 vue 版本的 hooks

VueReact 相继都推出了Hooks,那么今天我们就通过对比的方式来学习 VueReactHook

为什么需要 Hooks

使在组件之间复用状态逻辑更简单

vue中我们使用mixinsextends来复用逻辑,在react中可以使用render props 或者 HOC来复用逻辑。但是它们都会有弊端。

比如vue中的mixins,当我们一个组件引入很多mixin的时候,多个mixin的同名、合并等问题随之而来,而且也不利于我们代码理解和问题排查。

比如react中的render props 或者 HOC,传递渲染属性和高阶组件的层层嵌套包裹,也不利于代码的理解和维护。

这个时候Hook就能很好的解决了

Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。

让相关代码聚合在一起

vue2版本的时候,我们的一个简单业务代码会分得很散,比如data定义了数据,methods里面定义了方法,生命周期函数里面又做了处理等等。代码就会很分散,不利于维护和阅读。所以vue3就推出了composition api。这样让相关逻辑代码聚合在一起。

react class组件也有类似问题,一个简单业务代码会分得很散,可能state定义在constructor里面,生命周期函数里面又做了处理等等。代码就会很分散,不利于维护和阅读。hooks的推出让相关逻辑代码聚合在一起,代码能更好的阅读和维护了。

Hooks带来的好处是显而易见的: “高度聚合,可阅读性提升” 。伴随而来的便是 “效率提升,bug变少”

让组件更容易理解

这里重点说下this

vue2里面this可能还好点,都是指向当前vue实例,但是react class组件里面经常需要处理一些this问题,比如函数要bind(this)等。

但在Hooks 写法中,你就完全不必担心 this 的问题了。Hooks 写法直接告别了 this 问题。

副作用的关注点分离

副作用指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等

以往这些副作用都是写在类组件生命周期函数中的。

在react中,我们可以使用 useEffectuseLayoutEffect来替代类组件生命周期函数。useEffect 在全部渲染完毕后才会执行,useLayoutEffect 会在浏览器 layout 之后,painting 之前执行。

React Hooks

React HookReact 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

React Hook 使用规则

  1. 只能在函数式组件或自定义Hook中调用。
  2. 只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook

下面来说说常用的一些Hook

useState

在之前的react版本中我们知道函数式组件是没有state的。有了Hooks后我们可以使用useState来定义函数式组件的状态。

它接收一个参数,作为state的初始值,返回一个数组,数组第一个值是state的值,第二个参数用来设置state的值。

比如下面的例子,count初始值为1,点击按钮后会触发setCount修改count的值。

import { useState } from "react";

function StateHook() {
  const [count, setCount] = useState(1);

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

export default StateHook;

获取之前值

使用setState我们可以通过回调函数获取之前state的值,使用useState也是一样的,通过回调函数能获取之前state的值。

class组件

this.setState((state, props) => {
  // 之前的state和目前组件的props
  console.log(state, props);
  return {
    user: { ...state.user, name: 'demi' },
  };
});

在函数式组件,但是请注意它的回调函数是获取不到props的。

setUser((state) => {
  // 之前的state,没有props
  console.log(state);
  return { ...state.user, name: "jack" };
});

获取之后值

我们知道setState是异步的,有时我们需要获取修改后state的值,但是不特殊处理在后面是获取不到最新的state值。

class组件我们可以通过回调函数和async await两种方式获取,但是函数式组件useState是都不支持的

// 回调函数
this.setState({ ...this.state.user, name: "jack" }, () => {
  // 最新user
  console.log(this.state.user)
})

//async await
await this.setState({ ...this.state.user, name: "jack" })
// 最新user
console.log(this.state.user)

useReducer

useReduceruseState差不多,都是用来定义state的。它接收一个形如 (state, action) => newStatereducer方法,和一个初始state,并返回当前的 state 以及与其配套的 dispatch 方法。可以说是useState的一个高级版。

自定义初始值

const [state, dispatch] = useReducer(reducer, initialState,);

import { useReducer } from "react";

function ReducerHook() {
  const reducer2 = (state, action) => {
    console.log("reducer2", action);
    switch (action.type) {
      case "left":
        return { name: action.payload.o + state.name };
      case "right":
        return { name: state.name + action.payload.o };
      default:
        return { ...state };
    }
  };
  
  const [state2, dispatch2] = useReducer(reducer2, { name: "randy" });

  return (
    <div>
      <div>{state2.name}</div>
      <div>
        <button
          onClick={() => dispatch2({ type: "left", payload: { o: "#" } })}>
          left
        </button>
      </div>
      <div>
        <button
          onClick={() => dispatch2({ type: "right", payload: { o: "*" } })}>
          right
        </button>
      </div>
    </div>
  );
}

export default ReducerHook;

上面的例子初始state2{ name: "randy" },当我们点击left会在name的左边加上#,当我们点击right的时候会在name的右边加上*

传递初始值

useReducer还有另外一种写法,你可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)

const [state, dispatch] = useReducer(reducer, initialState, init);

这个在我们父组件传递初始值给子组件时会很有用。并且 还可以调用初始化方法恢复到初始值。

// 父组件,传递initialCount作为reducer的初始值
<ReducerHook1 initialCount={100}></ReducerHook1>

// 子组件 ReducerHook1
import { useReducer } from "react";

const init = (initialCount) => {
  return { count: initialCount, name: "randy" };
};

const reducer = (state, action) => {
  switch (action.type) {
    case "increment":
      return { ...state, count: state.count + 1 };
    case "decrement":
      return { ...state, count: state.count - 1 };
    case "reset":
      return init(action.initialCount);
    default:
      return { ...state };
  }
};
  
function ReducerHook1(props) {
  // props.initialCount会被作为init方法的参数
  const [state, dispatch] = useReducer(reducer, props.initialCount, init);

  return (
    <div>
      <div>
        {state.count}, {state.name}
      </div>
      <div>
        <button onClick={() => dispatch({ type: "increment" })}>
          increment
        </button>
      </div>
      <div>
        <button onClick={() => dispatch({ type: "decrement" })}>
          decrement
        </button>
      </div>
      <div>
        <button onClick={() => dispatch({ type: "reset", initialCount: 0 })}>
          reset
        </button>
      </div>
    </div>
  );
}

export default ReducerHook1;

useEffect

useEffect 可以看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。可以弥补函数组件没有生命周期的缺点。

useEffect接收两个参数,第二个参数可选。第一个参数是一个函数,第二个参数是依赖项(数组),当依赖发生变化的时候会重新运行前面的函数。

初始化和更新的时候被调用

没有依赖项的useEffect会在组件初始化和组件更新的时候被调用。(任何引起组件更新的操作都会导致运行)。

useEffect(() => {
  console.log("没有依赖项,组件初始化和组件更新的时候就会被调用");
});

类似class组件的

componentDidMount() {
  console.log("没有依赖项,组件初始化和组件更新的时候就会被调用");
}

componentDidUpdate(prevProps, prevState, snapshot) {
  console.log("没有依赖项,组件初始化和组件更新的时候就会被调用");
}

如果想只在某个state发生改变的时候才被调用可以传递依赖项。

初始化和具体state更新的时候被调用

这个依赖countuseEffect会在组件初始化和仅count发生变化的时候被调用。这个类似vue里面的immediate watch

useEffect(() => {
  console.log("依赖count", count);
}, [count]);

清除副作用

有些时候effect可能会有些副作用需要清除(比如定时器,事件监听),这时我们就可以在 effect 里面返回一个清除函数。

清除函数会在依赖发生改变和组件卸载的时候运行。首次是不会运行的。

比如我们想实现一个计时器,计时器统计name没变改变的时间,只要name改变了就重新计时。

import { useState, useEffect } from "react";

function EffectHook() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("randy");

  useEffect(() => {
    let timer = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);

    // 该方法会在依赖数据更新和组件卸载的时候运行,也就是只有首次不运行
    return () => {
      // 清除上一个定时器
      clearInterval(timer);
      setCount(0);
    };
  }, [name]);

  return (
    <div>
      <div>{count}秒</div>
      <div>{name}</div>
      <div>
        <button onClick={() => setName(name + "!")}>update name</button>
      </div>
    </div>
  );
}

export default EffectHook;

初始化和卸载的时候被调用

如果我们不依赖state,只想在组件初始化和组件卸载的时候调用呢?我们可以将第二个参数设置为[],并返回清除函数。

useEffect(() => {
  console.log("我仅在组件挂载时执行");

  return () => {
    console.log("清除函数仅在组件卸载时执行");
  };
}, []);

这个就相当于class组件的

componentDidMount() {
  console.log("我仅在组件挂载时执行");
}

componentWillUnmount() {
  console.log("清除函数仅在组件卸载时执行");
}

useEffect 不能接收 async 作为回调函数

useEffect 接收的函数,要么返回一个能清除副作用的函数,要么就不返回任何内容。而 async 返回的是 promise

所以我们在用接口请求后台数据的时候需要这样写。

useEffect(() => {
// 更优雅的方式
const fetchData = async () => {
  const result = await axios(
    'https://.com/api/xxx',
  );
  setData(result.data);
};
fetchData();
}, []);

// 而不是这样写
// 注意 async 的位置
// 这种写法,虽然可以运行,但是会发出警告
// 每个带有 async 修饰的函数都返回一个隐含的 promise
// 但是 useEffect 函数有要求:要么返回清除副作用函数,要么就不返回任何内容
useEffect(async () => {
const result = await axios(
  'https://xxx.com/api/xxx',
);
setData(result.data);
}, []);

useLayoytEffect

useLayoytEffectuseEffect 基本相同,只是一个是同步执行一个是异步执行。

怎么理解这句话呢?我们来看下面的例子

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

function LayoutEffectHook() {
  const [text, setText] = useState("hello world");
  const [count, setCount] = useState(0);

  // useEffect是异步执行
  // 会闪烁
  // useEffect(() => {
  //   let i = 0;
  //   while (i <= 100000000) {
  //     i++;
  //   }
  //   setText("world hello");
  // }, []);

  // useLayoutEffect是同步执行
  // 换成 useLayoutEffect 之后闪烁现象就消失了
  useLayoutEffect(() => {
    let i = 0;
    while (i <= 100000000) {
      i++;
    }
    setText("world hello");
  }, [count]);

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

export default LayoutEffectHook;

因为useEffect是异步执行所以页面首先渲染出hello world,然后变为world hello,会有一个闪烁。而useLayoutEffect是同步执行,所以页面不会闪烁,会直接显示world hello

总结

  1. useEffect 的执行时机是浏览器完成渲染后再异步调用。而 useLayoutEffect 的执行时机是浏览器把内容真正渲染到界面之前,和 componentDidMount 等价。
  2. useLayoutEffect 先于useEffect执行,但是可能会阻塞浏览器的渲染。所以优先使用 useEffect,因为它是异步执行的,不会阻塞渲染。
  3. 会影响到渲染的操作尽量放到 useLayoutEffect 中去,避免出现闪烁问题。

memo

memoPureComponent作用类似,可以用作性能优化,memo 是高阶组件,函数组件和类组件都可以使用。

当我们使用了memo就类似class组件继承了PureComponent,会自动进行性能优化。

// 父组件
<Memo2 name={name}></Memo2>

// 子组件
import { memo } from "react";

function Memo1({ count }) {
  console.log("memo1 render");
  return <div>{count}</div>;
}

export default memo(Memo1);

但是 memo只能对props的情况确定是否渲染,而PureComponent可以针对propsstate

我们还可以使用memo的第二个参数实现类似shouldComponentUpdate的自定义渲染效果。

第二个参数,可以根据一次更新中props是否相同决定原始组件是否重新渲染。是一个返回布尔值,true 证明组件无须重新渲染,false证明组件需要重新渲染,这个和类组件中的shouldComponentUpdate正好相反。

memo: 第二个参数 返回 true 组件不渲染 , 返回 false 组件重新渲染。

shouldComponentUpdate: 返回 true 组件渲染 , 返回 false 组件不渲染。

// 父组件
<Memo2 name={name}></Memo2>

// 子组件
import { memo } from "react";

function Memo2({ name }) {
  console.log("memo2 render");
  return <div>{name}</div>;
}

// 当依赖name没变就不渲染
const controlIsRender = (preProps, nextProps) => {
  console.log(preProps, nextProps);
  if (preProps.name === nextProps.name) {
    return true;
  }
  return false;
};

export default memo(Memo2, controlIsRender);

useMemo

useMemo接受两个参数,第一个参数是一个函数,返回值用于产生保存值。 第二个参数是一个数组,作为dep依赖项,数组里面的依赖项发生变化,重新执行第一个函数,产生新的值

缓存值

我们可以用来缓存值,使用过vue的同学应该知道,类似computed

import { useMemo, useState } from "react";

function MemoHook() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("randy");

  // 1. 用来缓存值,当依赖变化值才变,类似vue里面的computed
  // 首次渲染是会执行的
  // 当count改变useMemo1才会重新计算,改变name并不会重新计算
  const useMemo1 = useMemo(() => {
    // console.log("useMemo2", count);
    // 返回值等于useMemo的返回值
    return count;
  }, [count]);

  return (
    <div>
      <div>useMemo1,我是依赖count{useMemo1}</div>
      <div>{count}</div>
      <div>
        <button onClick={() => setCount(count + 1)}>add count</button>
      </div>
      
       <div>{name}</div>
      <div>
        <button onClick={() => setName(name + "!")}>
          change name 没什么依赖我
        </button>
      </div>
    </div>
  );
}

export default MemoHook;

缓存组件

类似 memo 缓存组件,我们使用useMemo也可以实现类似功能。只不过需要自定定义依赖,没memo那么智能(memo能自动比较props是否改变)。

import { useMemo, useState } from "react";

function MemoHook1({ count }) {
  console.log("MemoHook1 render");
  return <div>我依赖count,count:{count}</div>;
}

export default MemoHook1;

function MemoHook() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("randy");

  // 当count变,组件才重新渲染
  const MemoMemoHook1 = useMemo(
    () => <MemoHook1 count={count}></MemoHook1>,
    [count]
  );

  return (
    <div>
      {/* 依赖 count,按理来说只有count改变才会重新渲染,但是name改变也会重新渲染 */}
      {/* <MemoHook1 count={count}></MemoHook1> */}

      {/* 前面说到使用memo可以解决,这里使用 useMemo 也可以解决 */}
      {MemoMemoHook1}

      <div>{name}</div>
      <div>
        <button onClick={() => setName(name + "!")}>
          change name 没什么依赖我
        </button>
      </div>
    </div>
  );
}

export default MemoHook;

优化列表渲染

当我们有长列表需要渲染的时候,每次组件更新长列表都会重新渲染,我们可以使用useMemo直接进行优化。

import { useMemo, useState } from "react";

function MemoHook() {
  // 3. 优化列表渲染
  const [lists, setLists] = useState(["a", "b", "c"]);

  return (
    <div>
      {/* 这种方式在name改变也会重新渲染 */}
      {/* {lists.map((item, index) => {
        console.log("map render");
        return <div key={index}>{item}</div>;
      })} */}

      {/* 使用useMemo优化,当lists改变才重新渲染 */}
      {useMemo(() => {
        return lists.map((item, index) => {
          console.log("map render");
          return <div key={index}>{item}</div>;
        });
      }, [lists])}
      
      <div>
        <button onClick={() => setLists(["d", "e", "f"])}>change lists</button>
      </div>

      <div>{name}</div>
      <div>
        <button onClick={() => setName(name + "!")}>
          change name 没什么依赖我
        </button>
      </div>
    </div>
  );
}

export default MemoHook;

useCallback

useCallbackuseMemo 接收的参数都是一样,都是在其依赖项发生变化后才执行。区别在于 useMemo 返回的是函数运行的结果, useCallback 返回的是函数。

下面我们来看例子

import { useCallback, useState } from "react";

// 子组件
function CallbackHook1({ count, say }) {
  console.log("CallbackHook1 render");
  return (
    <div>
      <div>我依赖count:{count} 不依赖name</div>
      <div>
        <button onClick={say}>say</button>
      </div>
    </div>
  );
}

// 父组件
function MemoHook() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("randy");

  // 这种写法是实时的
  const callback1 = () => {
    console.log(count + name);
  };

  // 相当于只有count发生变化的时候 callback返回的函数才会重新计算
  // 不是实时的
  const callback2 = useCallback(() => {
    console.log(count + name);
  }, [count]);

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

      <CallbackHook1 count={count} say={callback1}></CallbackHook1>
      {/* <CallbackHook1 count={count} say={callback2}></CallbackHook1> */}

      <div>{name}</div>
      <div>
        <button onClick={() => setName(name + "!")}>改变name</button>
      </div>
    </div>
  );
}

export default MemoHook;

当我们使用callback1回调方法的时候,每次点击触发say方法都会获取最新的countname值。

但是我们使用callback2回调方法的时候,每次点击触发say方法,只有在count发生变化的时候才会重新计算,name的改变不会触发,所以name的值可能就不是最新的。

useMemo类似,useCallback也可以缓存一些东西,可以做一些性能优化提升性能。

useContext

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定。

useContext 可以代替 context.Consumerstatic contextType = xxxContext 来获取 Provider 中保存的 value 值。

const NameContext = React.createContext("randy");

// 父组件
<NameContext.Provider value="demi"></NameContext.Provider>

class子组件中

...

// 第一种方法
static contextType = NameContext;

// 第二种方法
Context2.contextType = NameContext;

// 使用this.context就能得到值

在函数组件中使用useContext

import { useContext } from "react";

// 使用name就能得到值
const name = useContext(NameContext);

当然我们还可以使用Consumer来接收。这种方式在class组件和函数式组件都支持,并且当有多个context的时候只能使用这种方式接收。

<NameContext.Consumer>
{(name) => {
  return <div>{name}</div>
}}
</NameContext.Consumer>

useRef

useRef很简单,用来在函数组件中创建ref,和classcreateRef功能一样。

class组件

import { createRef } from "react";

const ref1 = createRef();

函数组件

import { useRef } from "react";

const ref1 = useRef();

不要以为useRef就是用来创建ref的,它其实还有个重要功能是可以缓存数据。

我们知道在class组件,可以在constructor里面定义数据,组件刷新constructor并不会重新运行,所以数据相当于是缓存起来了(我们的修改有效)。但是在函数式组件中,每次组件刷新,整个函数重新运行,所以我们定义的变量又会被初始化一次,这样就没法缓存数据(我们的修改无效)。

使用useRef就可以解决这个问题,下面看例子。

import { useRef, useState } from "react";

const RefTest2 = () => {
  let [data, setData] = useState(0);

  let initData = {
    name: "randy",
    age: 26,
  };
  
  
  // 缓存起来
  let refData = useRef(initData);

  console.log(initData); // 每次输出 { name: "randy", age: 26 }
  console.log(refData.current); // 不会被初始化 所以age一直累加

  // 触发重新渲染
  const changeData = () => {
    setData(data + 1);
    // age同时加1
    initData.age = initData.age + 1;
    refData.current.age = refData.current.age + 1;
  };

  return (
    <div>
      <div>{data}</div>
      <button onClick={changeData}>改变数据触发重新渲染</button>
    </div>
  );
};

export default RefTest2;

在上面这个例子中,每次点击按钮,修改data的值,会触发组件重新渲染。没有使用useRef缓存的initData 每次输出 { name: "randy", age: 26 },但是使用useRef缓存的refDataage属性会一直累加。

useImperativeHandle

我们知道,对于子组件,如果是class类组件,我们可以通过ref获取类组件的实例,但是在子组件是函数组件的情况,如果我们不能直接通过ref的。

函数组件只能通过forwardRef获取到组件内部的dom元素,如果想要获取组件直接使用组件上的属性或方法还是差了点。

我们可以使用 useImperativeHandle 配合 forwardRef 自定义暴露给父组件的实例值。这样就能实现类似获取组件的功能,把想要暴露的属性或方法通过useImperativeHandle的返回值暴露出去。

//父组件
<Ref7 ref={this.ref7}></Ref7>

// 子组件
import { useImperativeHandle, useRef, forwardRef } from "react";

const Ref7 = forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => {
    // 这个对象在父组件能通过.current获取到
    return {
      focus: () => {
        inputRef.current.focus();
      },
      blur: () => {
        inputRef.current.blur();
      },
      changeValue: () => {
        inputRef.current.value = "randy";
      },
    };
  });
  return (
    <div>
      <input type="text" ref={inputRef} defaultValue="ref7" />
    </div>
  );
});

export default Ref7;

这样我们在父组件就能通过ref访问到我们返回的那个对象啦,就能直接调用子组件里面的方法。

Vue Hooks

有人会觉得vue没有Hook,但笔者觉得 vue3composition api 可以理解成vue版的Hook

composition api代码都写在 setup 函数里面,让逻辑关注点相关代码收集在一起。而且不再使用选项式写法,需要什么函数引入什么函数。

Vue Hook 使用规则: 只能在setup函数里面使用。

下面来说说常用的一些Hook

ref

接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。

一般用来定义基本类型的响应式数据。注意这里说的是一般,并不是说ref就不能定义引用类型的响应式数据。

使用ref定义的响应式数据在setup函数中使用需要加上.value,但在模板中可以直接使用。

这个就类似react里面的useState

<template>
  <h3>count1</h3>
  <div>count1: {{ count1 }}</div>
  <button @click="plus">plus</button>
  <button @click="decrease">decrease</button>
  
  <div>user1: {{ user1.name }}</div>
  <button @click="updateUser1Name">update user1 name</button>
</template>

<script>
import { defineComponent, ref } from "vue";
export default defineComponent({
  setup() {
    const count1 = ref(0);

    const plus = () => {
      count1.value++;
    };
    const decrease = () => {
      count1.value--;
    };
    
    const user1 = ref({ name: "randy1" });
    const updateUser1Name = () => {
      // ref定义的变量需要使用.value修改
      user1.value.name += "!";
    };

    return {
      count1,
      plus,
      decrease,
      user1,
      updateUser1Name
    };
  },
});
</script>

reactive

reactive用来定义引用类型的响应式数据。注意,不能用来定义基本数据类型的响应式数据,不然会报错。

reactive定义的对象是不能直接使用es6语法解构的,不然就会失去它的响应式,如果硬要解构需要使用toRefs()方法。

这个就类似react里面的useState

<template>
  <div>
    <h3>user2</h3>
    <div>user2: {{ user2.name }}</div>
    <button @click="updateUser2Name">update user2 name</button>

    <h3>user3</h3>
    <div>user3 name: {{ name }} user3 age: {{ age }}</div>
    <button @click="updateUser3Name">update user3 name</button>

    <h3>count2</h3>
    <div>count2: {{ count2 }}</div>
    <button @click="plus2">plus2</button>
    <button @click="decrease2">decrease2</button>
  </div>
</template>

<script>
import { defineComponent, reactive, toRefs } from "vue";
export default defineComponent({
  setup() {
    const _user = { name: "randy2" }
    const user2 = reactive(_user);
    const updateUser2Name = () => {
      // reactive定义的变量可以直接修改
      user2.name += "!";
      
      // 原始对象的修改并不会响应式,也就是页面并不会重新渲染
      // _user.name += "!";
      // 代理对象被改变的时候,原始对象会被修改
      // console.log(_user);
    };
    
    // 使用toRefs可以响应式解构出来,在模板能直接使用啦。
    const user3 = reactive({ name: "randy3", age: 24 });
    const updateUser3Name = () => {
      user3.name += "!";
    };

    // 使用reactive定义基本数据类型会报错
    const count2 = reactive(0);

    const plus2 = () => {
      count2.value++;
    };
    const decrease2 = () => {
      count2.value--;
    };
    
    return {
      user2,
      updateUser2Name,
      // ...user3, // 直接解构不会有响应式
      ...toRefs(user3),
      updateUser3Name,
      count2,
      plus2,
      decrease2,
    };
  },
});
</script>

reactive 将解包所有深层的 refs,同时维持 ref 的响应性。

怎么理解这句话呢,就是使用reactive定义响应式对象,里面的属性是ref定义的话可以直接赋值而不需要再.value,并且数据的修改是响应式的。

const count = ref(1)
// 可以直接定义,而不是{count: count.value}
const obj = reactive({ count })

// 这种写法也是支持的
// const obj = reactive({})
// obj.count = count

// ref 会被解包
console.log(obj.count === count.value) // true

// 它会更新 `obj.count`
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2

// 它也会更新 `count` ref
obj.count++
console.log(obj.count) // 3
console.log(count.value) // 3

computed

computed是计算属性,意思就是会缓存值,只有当依赖属性发生变化的时候才会重新计算。

类似react里面的useMemo。不同的是vue不需要显示传递依赖。这点我觉得是vue做得非常棒的。

<template>
  <div>
    <div>{{ user1.name }}</div>
    <div>{{ user1.age }}</div>
    <div>{{ fullName1 }}</div>
    <button @click="updateUser1Name">update user1 name</button>

    <div>{{ user2.name }}</div>
    <div>{{ user2.age }}</div>
    <div>{{ fullName2 }}</div>
    <button @click="updateUser2Name">update user2 name</button>
  </div>
</template>
<script>
import { defineComponent, reactive, computed } from "vue";
export default defineComponent({
  setup() {
    const user1 = reactive({ name: "randy1", age: 24 });
    // 接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象
    // 这里的fullName1是不能修改的
    const fullName1 = computed(() => {
      return `${user1.name}今年${user1.age}岁啦`;
    });
    const updateUser1Name = () => {
      user1.name += "!";
    };

    const user2 = reactive({ name: "randy2", age: 27 });
    // 接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。
    // 这里的fullName2是可以修改的
    let fullName2 = computed({
      get() {
        return `${user2.name}今年${user2.age}岁啦`;
      },
      set(val) {
        user2.name = val;
      },
    });
    const updateUser2Name = () => {
      // 需要使用value访问
      fullName2.value = "新的name";
    };

    return {
      user1,
      fullName1,
      updateUser1Name,
      user2,
      fullName2,
      updateUser2Name,
    };
  },
});
</script>

watchEffect

立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

怎么理解这句话呢?就是它会自动收集依赖,不需要手动传入依赖。当里面用到的数据发生变化时就会自动触发watchEffect。并且watchEffect 会先执行一次用来自动收集依赖。而且watchEffect 无法获取到变化前的值,只能获取变化后的值。

类似reactuseEffect。不同的是vue不需要显示传递依赖。这点我觉得是vue做得非常棒的。

<script>
import { defineComponent, reactive, watchEffect } from "vue";
export default defineComponent({
  setup() {
   const user2 = reactive({ name: "randy2", age: 27 });
  
    const updateUser2Age = () => {
      user2.age++;
    };
    
    watchEffect(() => {
      console.log("watchEffect", user2.age);
    });
  }
})
</script>

在上面这个例子中,首先会执行watchEffect输出27,当我们触发updateUser2Age方法改变age的时候,因为user2.agewatchEffect的依赖,所以watchEffect会再次执行,输出28。

停止侦听

watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。

在一些情况下,也可以显式调用返回值以停止侦听:

const stop = watchEffect(() => {
  /* ... */
})

// later
stop()

清除副作用

有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除。所以侦听副作用传入的函数可以接收一个 onInvalidate 函数作入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:

  • 副作用即将重新执行时
  • 侦听器被停止 (如果在 setup() 或生命周期钩子函数中使用了 watchEffect,则在组件卸载时)

清除副作用很多同学可能不太理解,下面笔者用个例子解释下。

假设我们需要在input框输入关键字进行实时搜索,又不想请求太频繁我们就可以用到这个功能了。

<template>
  <input type="text" v-model="text" />
</template>

const text = ref("randy");

watchEffect((onInvalidate) => {
  const timer = setTimeout(() => {
    console.log("input", text.value);
    // 模拟调用后端接口
    // getDate(text.value)
  }, 1000);
  
  onInvalidate(() => {
    // 清除上一次请求
    clearTimeout(timer);
  });
  console.log("watchEffect", text.value);
});

上面的例子中watchEffect依赖了text.value,所以我们只要在input输入值就会立马进入watchEffect。如果不处理的话后端服务压力可能会很大,因为我们只要输入框值改变了就会发送请求。

我们可以利用清除副作用回调函数,在用户输入完一秒后再向后端发送请求。因为第一次是不会执行onInvalidate回调方法的,只有在副作用重新执行或卸载的时候才会执行该回调函数。

所以在我们输入的时候,会一直输出"watchEffect" text对应的值,当我们停止输入一秒后会输出"input" text对应的值,然后发送请求给后端。这样就达到我们最开始的目标了。

类似的还可以应用到事件监听上。这个小伙伴们可以自己试试。

副作用刷新时机

Vue 的响应性系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个“tick” 中多个状态改变导致的不必要的重复调用。在核心的具体实现中,组件的 update 函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时,默认情况下,会在所有的组件 update 执行。也就是会在组件生命周期函数onBeforeUpdate之前执行。

const updateUser2Age = () => {
  user2.age++;
};
    
watchEffect(
  () => {
    console.log("watchEffect", user2.age);
  }
);

onBeforeUpdate(() => {
  console.log("onBeforeUpdate");
});

上面的例子,当我们触发updateUser2Age方法修改age的时候,会先执行watchEffect然后执行onBeforeUpdate

如果需要在组件更新重新运行侦听器副作用,我们可以传递带有 flush 选项的附加 options 对象 (默认为 pre)。

const updateUser2Age = () => {
  user2.age++;
};
    
watchEffect(
  () => {
    console.log("watchEffect", user2.age);
  },
  {
    flush: "post",
  }
);

onBeforeUpdate(() => {
  console.log("onBeforeUpdate");
});

上面的例子,当我们触发updateUser2Age方法修改age的时候,会先执行onBeforeUpdate然后执行watchEffect

flush 选项还接受 sync,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。sync这个参数是什么意思呢?很多同学可能不理解,这里我们重点解释下。

watchEffect只有一个依赖的时候这个参数和pre是没区别的。但是当有多个依赖的时候,flush: postflush: pre只会执行一次副作用,但是sync会执行多次,也就是有一个依赖改变就会执行一次。

下面我们看例子

const user3 = reactive({ name: "randy3", age: 27 });

const updateUser3NameAndAge = () => {
  user3.name += "!";
  user3.age++;
};

watchEffect(
  () => {
    console.log("watchEffect", user3.name, user3.age);
  },
  {
    flush: "sync",
  }
);

onBeforeUpdate(() => {
  console.log("onBeforeUpdate");
});

在上面的例子中,watchEffectnameage两个依赖,当我们触发updateUser3NameAndAge方法的时候,如果flush: "sync"这个副作用会执行两次,依次输出watchEffect randy3! 27watchEffect randy3! 28onBeforeUpdate

如果你想让每个依赖发生变化都执行watchEffect但又不想设置flush: "sync"你也可以使用nextTick等待侦听器在下一步改变之前运行。

import { nextTick } from "vue";
const updateUser3NameAndAge = async () => {
  user3.name += "!";
  await nextTick()
  user3.age++;
};

上面的例子会依次输出watchEffect randy3! 27onBeforeUpdatewatchEffect randy3! 28onBeforeUpdate

Vue 3.2.0 开始,我们也可以使用别名方法watchPostEffectwatchSyncEffect,这样可以用来让代码意图更加明显。

watchPostEffect

watchPostEffect就是watchEffect 的别名,带有 flush: 'post' 选项。

watchSyncEffect

watchSyncEffect就是watchEffect 的别名,带有 flush: 'sync' 选项。

侦听器调试

onTrackonTrigger 选项可用于调试侦听器的行为。

  • onTrack 将在响应式 propertyref 作为依赖项被追踪时被调用。
  • onTrigger 将在依赖项变更导致副作用被触发时被调用。

这个有点类似前面说的生命周期函数renderTrackedrenderTriggered,一个最初次渲染时调用,一个在数据更新的时候调用。

这两个回调都将接收到一个包含有关所依赖项信息的调试器事件。

watchEffect(
  () => {
    /* 副作用 */
  },
  {
    onTrack(e) {
      console.log("onTrack: ", e);
    },
    onTrigger(e) {
      console.log("onTrigger:", e);
    },
  }
)

onTrackonTrigger 只能在开发模式下工作。

watch

watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。

watchEffect 相比,watch 有如下特点

  1. 惰性地执行副作用
  2. 更具体地说明应触发侦听器重新运行的状态
  3. 可以访问被侦听状态的先前值和当前值

类似react里面的useEffect

监听单一源

<script>
import { defineComponent, reactive, watchEffect } from "vue";
export default defineComponent({
  setup() {
    const user1 = reactive({ name: "randy1", age: 24 });
    // source: 可以支持 string,Object,Function,Array; 用于指定要侦听的响应式变量
    // callback: 执行的回调函数
    // options:支持 deep、immediate 和 flush 选项。
    watch(
      () => user1.name,
      (newVal, oldVal) => {
        console.log(newVal, oldVal);
      }
    );
    watch(
      () => user1.age,
      (newVal, oldVal) => {
        console.log(newVal, oldVal);
      }
    );
  }
})
</script>

监听多个源

监听多个源我们使用数组。

这里我们需要注意,监听多个源只要有一个源发生变化,回调函数都会执行。

<script>
import { defineComponent, reactive, watchEffect } from "vue";
export default defineComponent({
  setup() {
    const user1 = reactive({ name: "randy1", age: 24 });
    // source: 可以支持 string,Object,Function,Array; 用于指定要侦听的响应式变量
    // callback: 执行的回调函数
    // options:支持 deep、immediate 和 flush 选项。
    watch(
      [() => user1.name, () => user1.age],
      ([newVal1, newVal2], [oldVal1, oldVal2]) => {
        console.log(newVal1, newVal2);
        console.log(oldVal1, oldVal2);
      }
    );
  }
})
</script>

监听引用数据类型

有时我们可能需要监听一个对象的改变,而不是具体某个属性。

const user2 = reactive({ name: "randy2", age: 27 });
watch(
  user2 ,
  (newVal, oldVal) => {
    console.log(newVal, oldVal); // {name: 'randy2', age: 28} {name: 'randy2', age: 28}
  }
);

const updateUser2Age = () => {
  user2.age++;
};

上面的写法有没有问题呢?当我们触发updateUser2Age方法修改age的时候可以发现我们输出newVal, oldVal两个值是一样的。这就是引用数据类型的坑。当我们不需要知道oldVal的时候这样写没问题,但是当我们需要对比新老值的时候这种写法就不行了。

我们需要监听这个引用数据类型的拷贝。当引用数据类型简单的时候我们可以直接解构成新对象。

这样输出来的值才是正确的。

const user2 = reactive({ name: "randy2", age: 27 });
watch(
  // 这只是浅拷贝,解决第一层问题
  () => ({ ...user2 }),
  (newVal, oldVal) => {
    console.log(newVal, oldVal); // {name: 'randy2', age: 28} {name: 'randy2', age: 27}
  },
);

const updateUser2Age = () => {
  user2.age++;
};

但是当引用数据类型复杂的时候我们就需要用到深拷贝了。深拷贝前面笔者有文章介绍,可以自己写深拷贝方法或者引用lodash库。

比如这里,我们想监听city就必须使用深度拷贝,不然返回的新老值还会是一样的。

const user2 = reactive({ name: "randy2", age: 27, address: {city: '汨罗'} });

vue2中好像没办法解决这个问题。

深度监听和立即执行

watch还是支持vue2的深度监听deep: true和立即执行immediate: true

const flushOptions = reactive({ name: "flushOptions", num: 1 });
watch(
  () => ({ ...flushOptions }),
  (newVal, oldVal) => {
    console.log(newVal, oldVal);
  },
  {
    // deep: true, // 深度监听
    // immediate: true, // 立即监听
  }
);

watch还支持 watchEffect的停止侦听、清除副作用、副作用刷新时机、侦听器调试,下面笔者只简单介绍使用方法,就不详细解释了。

停止侦听

watch中,停止侦听用法和watchEffect一样。

const stop = watch(
  () => user1.name,
  (newVal, oldVal) => {/* ... */}
)

// later
stop()

清除副作用

watch中,onInvalidate函数会作为回调的第三个参数传递进来。

const invalidate = reactive({ name: "onInvalidate" });
watch(
  () => ({ ...invalidate }),
  (newVal, oldVal, onInvalidate) => {
    onInvalidate(() => {
      console.log("清除副作用");
    });
    console.log(newVal, oldVal);
  }
);

副作用刷新时机

watch中,副作用刷新时机是在第三个参数中配置。

const flushOptions = reactive({ name: "flushOptions", num: 1 });
watch(
  () => ({ ...flushOptions }),
  (newVal, oldVal) => {
    console.log(newVal, oldVal);
  },
  {
    // flush: "pre", // 默认
    // flush: "post",
    // flush: "sync",
  }
);

侦听器调试

watch中,侦听器调试是在第三个参数中配置。

const trackOptions = reactive({ name: "trackOptions"});
watch(
  () => ({ ...trackOptions }),
  (newVal, oldVal) => {
    console.log(newVal, oldVal);
  },
  {
    onTrack(e) {
      console.log("onTrack: ", e);
    },
    onTrigger(e) {
      console.log("onTrigger:", e);
    },
  }
);

自定义Hook

通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。ReactVue都支持自定义Hook

下面我们分别用ReactVue实现一个实现鼠标打点的自定义 Hook

React

React自定义Hook不管内置Hook还是自定义Hook都必须以use开头。

import { useEffect, useState } from "react";

const usePoint = () => {
  const [point, setPointe] = useState({ x: 0, y: 0 });

  const savePoint = (e) => {
    setPointe({ x: e.pageX, y: e.pageY });
  };

  useEffect(() => {
    window.addEventListener("click", savePoint);

    return () => {
      window.removeEventListener("click", savePoint);
    };
  }, []);

  return point;
};

function CustomHook() {
  const point = usePoint();
  return (
    <div>
      <div>
        x: {point.x} y: {point.y}
      </div>
    </div>
  );
}

export default CustomHook;

Vue

Vue 自定义Hook 没有强制规则,随意。

// hook/point.js

import { reactive, onMounted, onBeforeUnmount } from "vue";

export default function() {
    //保存鼠标“打点”相关的数据
    let point = reactive({
        x: 0,
        y: 0,
    });

    //实现鼠标“打点”相关的方法
    function savePoint(event) {
        point.x = event.pageX;
        point.y = event.pageY;
    }

    //实现鼠标“打点”相关的生命周期钩子
    onMounted(() => {
        window.addEventListener("click", savePoint);
    });

    onBeforeUnmount(() => {
        window.removeEventListener("click", savePoint);
    });

    return point;
}

使用

<template>
<h2>当前点击时鼠标的坐标为:x:{{point.x}},y:{{point.y}}</h2>
</template>

<script>
import usePoint from '../hook/point.js'
export default {
  name:'CustomHook',
  setup(){
    const point = usePoint()
    return {point}
  }
}
</script>

这在vue2,如果想要实现这样一个功能是不是需要创建一个vue组件然后引用过来使用呢?因为vue2生命周期没办法在普通js中使用,响应式data也没办法在普通js函数中使用。

但在vue3这一切都可以实现,我们直接创建一个自定义Hook就能复用逻辑,类似一个组件,是不是很好用呢。(vue3的定义响应式数据、生命周期函数、watch监听等方法都能在普通js中使用,大大提高了复用效率。)

总结

相同点

  1. 总体思路是一致的 都遵照着 "定义状态数据","操作状态数据","隐藏细节" 作为核心思路。
  2. 都是为了能更好的复用逻辑、让相关代码聚合在一起、更好的代码理解。

不同点

  1. vue3 的组件里, setup 是作为一个早于 created 的生命周期存在的,无论如何,在一个组件的渲染过程中只会进入一次。React函数组件 则完全不同,如果没有被 memorized,它们可能会被不停地触发,不停地进入并执行方法,因此上手难度相较于Vue来说要大一点。