不知道大家有没有发现随着版本的升级 vue 和 react 越来越像了。
2019年年初,react 在 16.8.x 版本正式具备了 hooks 能力。
2019年6月,尤雨溪提出了关于 vue3 Component API 的提案。笔者理解这其实是 vue 版本的 hooks。
Vue 和 React 相继都推出了Hooks,那么今天我们就通过对比的方式来学习 Vue 和 React 的 Hook。
为什么需要 Hooks
使在组件之间复用状态逻辑更简单
在vue中我们使用mixins或extends来复用逻辑,在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中,我们可以使用 useEffect和useLayoutEffect来替代类组件生命周期函数。useEffect 在全部渲染完毕后才会执行,useLayoutEffect 会在浏览器 layout 之后,painting 之前执行。
React Hooks
React Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
React Hook 使用规则
-
只能在函数式组件或自定义
Hook中调用。 -
只在最顶层使用
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
useReducer和useState差不多,都是用来定义state的。它接收一个形如 (state, action) => newState 的 reducer方法,和一个初始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 可以看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。可以弥补函数组件没有生命周期的缺点。
useEffect接收两个参数,第二个参数可选。第一个参数是一个函数,第二个参数是依赖项(数组),当依赖发生变化的时候会重新运行前面的函数。
初始化和更新的时候被调用
没有依赖项的useEffect会在组件初始化和组件更新的时候被调用。(任何引起组件更新的操作都会导致运行)。
useEffect(() => {
console.log("没有依赖项,组件初始化和组件更新的时候就会被调用");
});
类似class组件的
componentDidMount() {
console.log("没有依赖项,组件初始化和组件更新的时候就会被调用");
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log("没有依赖项,组件初始化和组件更新的时候就会被调用");
}
如果想只在某个state发生改变的时候才被调用可以传递依赖项。
初始化和具体state更新的时候被调用
这个依赖count的useEffect会在组件初始化和仅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。
总结
-
useEffect的执行时机是浏览器完成渲染后再异步调用。而useLayoutEffect的执行时机是浏览器把内容真正渲染到界面之前,和componentDidMount等价。 -
useLayoutEffect先于useEffect执行,但是可能会阻塞浏览器的渲染。所以优先使用useEffect,因为它是异步执行的,不会阻塞渲染。 -
会影响到渲染的操作尽量放到
useLayoutEffect中去,避免出现闪烁问题。
memo
memo和PureComponent作用类似,可以用作性能优化,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可以针对props和state。
我们还可以使用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
useCallback 和 useMemo 接收的参数都是一样,都是在其依赖项发生变化后才执行。区别在于 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方法都会获取最新的count和name值。
但是我们使用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,和class的createRef功能一样。
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缓存的refData,age属性会一直累加。
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,但笔者觉得 vue3 的 composition 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 无法获取到变化前的值,只能获取变化后的值。
类似react的 useEffect。不同的是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.age是watchEffect的依赖,所以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");
});
在上面的例子中,watchEffect有name和age两个依赖,当我们触发updateUser3NameAndAge方法的时候,如果flush: "sync"这个副作用会执行两次,依次输出watchEffect randy3! 27、watchEffect randy3! 28、onBeforeUpdate。
如果你想让每个依赖发生变化都执行watchEffect但又不想设置flush: "sync"你也可以使用nextTick等待侦听器在下一步改变之前运行。
import { nextTick } from "vue";
const updateUser3NameAndAge = async () => {
user3.name += "!";
await nextTick()
user3.age++;
};
上面的例子会依次输出watchEffect randy3! 27、onBeforeUpdate、watchEffect randy3! 28、onBeforeUpdate。
从 Vue 3.2.0 开始,我们也可以使用别名方法watchPostEffect 和 watchSyncEffect,这样可以用来让代码意图更加明显。
watchPostEffect
watchPostEffect就是watchEffect 的别名,带有 flush: 'post' 选项。
watchSyncEffect
watchSyncEffect就是watchEffect 的别名,带有 flush: 'sync' 选项。
侦听器调试
onTrack 和 onTrigger 选项可用于调试侦听器的行为。
onTrack将在响应式property或ref作为依赖项被追踪时被调用。onTrigger将在依赖项变更导致副作用被触发时被调用。
这个有点类似前面说的生命周期函数renderTracked和renderTriggered,一个最初次渲染时调用,一个在数据更新的时候调用。
这两个回调都将接收到一个包含有关所依赖项信息的调试器事件。
watchEffect(
() => {
/* 副作用 */
},
{
onTrack(e) {
console.log("onTrack: ", e);
},
onTrigger(e) {
console.log("onTrigger:", e);
},
}
)
onTrack和onTrigger只能在开发模式下工作。
watch
watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。
与 watchEffect 相比,watch 有如下特点
- 惰性地执行副作用
- 更具体地说明应触发侦听器重新运行的状态
- 可以访问被侦听状态的先前值和当前值
类似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,可以将组件逻辑提取到可重用的函数中。React和Vue都支持自定义Hook。
下面我们分别用React和Vue实现一个实现鼠标打点的自定义 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中使用,大大提高了复用效率。)
总结
相同点
-
总体思路是一致的 都遵照着 "定义状态数据","操作状态数据","隐藏细节" 作为核心思路。
-
都是为了能更好的复用逻辑、让相关代码聚合在一起、更好的代码理解。
不同点
vue3的组件里,setup是作为一个早于created的生命周期存在的,无论如何,在一个组件的渲染过程中只会进入一次。React函数组件则完全不同,如果没有被memorized,它们可能会被不停地触发,不停地进入并执行方法,因此上手难度相较于Vue来说要大一点。
系列文章
Vue和React对比学习之生命周期函数(Vue2、Vue3、老版React、新版React)
Vue和React对比学习之组件传值(Vue2 12种、Vue3 9种、React 7种)
Vue和React对比学习之路由(Vue-Router、React-Router)
Vue和React对比学习之状态管理 (Vuex和Redux)
Vue和React对比学习之条件判断、循环、计算属性、属性监听
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!
本文转自 https://juejin.cn/post/7103010557736779789,如有侵权,请联系删除。
浙公网安备 33010602011771号