Vue和React对比学习之Hooks

简介: Hooks

简介

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

2019年年初,react 在 16.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 Hook 是 React 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 可以看做 componentDidMountcomponentDidUpdate 和 componentWillUnmount 这三个函数的组合。可以弥补函数组件没有生命周期的缺点。

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

useLayoytEffect与 useEffect 基本相同,只是一个是同步执行一个是异步执行。

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

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.Consumer 或 static 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: post和 flush: 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 开始,我们也可以使用别名方法watchPostEffect 和 watchSyncEffect,这样可以用来让代码意图更加明显。

watchPostEffect

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

watchSyncEffect

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

侦听器调试

onTrack 和 onTrigger 选项可用于调试侦听器的行为。

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

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

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

watchEffect(
  () => {
    /* 副作用 */
  },
  {
    onTrack(e) {
      console.log("onTrack: ", e);
    },
    onTrigger(e) {
      console.log("onTrigger:", e);
    },
  }
)
onTrack 和  onTrigger 只能在开发模式下工作。

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来说要大一点。

系列文章

Vue和React对比学习之生命周期函数(Vue2、Vue3、老版React、新版React)

Vue和React对比学习之组件传值(Vue2 12种、Vue3 9种、React 7种)

Vue和React对比学习之Style样式

Vue和React对比学习之Ref和Slot

Vue和React对比学习之Hooks

Vue和React对比学习之路由(Vue-Router、React-Router)

Vue和React对比学习之状态管理 (Vuex和Redux)

Vue和React对比学习之条件判断、循环、计算属性、属性监听

后记

感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!

相关文章
|
7天前
|
前端开发 JavaScript 安全
学习如何为 React 组件编写测试:
学习如何为 React 组件编写测试:
22 2
|
8天前
|
前端开发 JavaScript 开发者
React与Vue:前端框架的巅峰对决与选择策略
【10月更文挑战第23天】React与Vue:前端框架的巅峰对决与选择策略
|
8天前
|
前端开发 JavaScript 开发者
“揭秘React Hooks的神秘面纱:如何掌握这些改变游戏规则的超能力以打造无敌前端应用”
【10月更文挑战第25天】React Hooks 自 2018 年推出以来,已成为 React 功能组件的重要组成部分。本文全面解析了 React Hooks 的核心概念,包括 `useState` 和 `useEffect` 的使用方法,并提供了最佳实践,如避免过度使用 Hooks、保持 Hooks 调用顺序一致、使用 `useReducer` 管理复杂状态逻辑、自定义 Hooks 封装复用逻辑等,帮助开发者更高效地使用 Hooks,构建健壮且易于维护的 React 应用。
21 2
|
8天前
|
前端开发 JavaScript 数据管理
React与Vue:两大前端框架的较量与选择策略
【10月更文挑战第23天】React与Vue:两大前端框架的较量与选择策略
|
8天前
|
JavaScript 前端开发 算法
在性能上,React和Vue有什么区别
【10月更文挑战第23天】在性能上,React和Vue有什么区别
9 1
|
8天前
|
前端开发 JavaScript 开发者
React与Vue:前端框架的巅峰对决与选择策略
【10月更文挑战第23天】 React与Vue:前端框架的巅峰对决与选择策略
|
8天前
|
JavaScript 前端开发 数据管理
React和Vue的优缺点
【10月更文挑战第23天】React和Vue的优缺点
6 0
|
8天前
|
开发框架 JavaScript 前端开发
React和Vue之间的区别是什么
【10月更文挑战第23天】React和Vue之间的区别是什么
9 0
|
28天前
|
前端开发 JavaScript 开发者
深入理解React Hooks:提升前端开发效率的关键
【10月更文挑战第5天】深入理解React Hooks:提升前端开发效率的关键
|
23天前
|
前端开发
深入解析React Hooks:构建高效且可维护的前端应用
本文将带你走进React Hooks的世界,探索这一革新特性如何改变我们构建React组件的方式。通过分析Hooks的核心概念、使用方法和最佳实践,文章旨在帮助你充分利用Hooks来提高开发效率,编写更简洁、更可维护的前端代码。我们将通过实际代码示例,深入了解useState、useEffect等常用Hooks的内部工作原理,并探讨如何自定义Hooks以复用逻辑。