不知道大家有没有发现随着版本的升级 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
来说要大一点。