不知道大家有没有发现随着版本的升级 vue
和 react
越来越像了。
2019年年初,react
在 16.8.x
版本正式具备了 hooks
能力。
2019年6月,尤雨溪提出了关于 vue3 Component API
的提案。笔者理解这其实是 vue
版本的 hooks
。
Vue
和 React
相继都推出了Hooks
,那么今天我们就通过对比的方式来学习 Vue
和 React
的 Hook
。
在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 Hook 是 React 16.8
的新增特性。它可以让你在不编写 class
的情况下使用 state
以及其他的 React
特性。
React Hook
使用规则
只能在函数式组件或自定义Hook
中调用。
只在最顶层使用 Hook
,不要在循环,条件或嵌套函数中调用 Hook
。
下面来说说常用的一些Hook
。
在之前的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
和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
可以看做 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合。可以弥补函数组件没有生命周期的缺点。
useEffect
接收两个参数,第二个参数可选。第一个参数是一个函数,第二个参数是依赖项(数组),当依赖发生变化的时候会重新运行前面的函数。
没有依赖项的useEffect
会在组件初始化和组件更新的时候被调用。(任何引起组件更新的操作都会导致运行)。
useEffect(() => { console.log("没有依赖项,组件初始化和组件更新的时候就会被调用"); });
类似class组件的
componentDidMount() { console.log("没有依赖项,组件初始化和组件更新的时候就会被调用"); } componentDidUpdate(prevProps, prevState, snapshot) { console.log("没有依赖项,组件初始化和组件更新的时候就会被调用"); }
如果想只在某个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
返回的是 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
与 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
和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
接受两个参数,第一个参数是一个函数,返回值用于产生保存值。 第二个参数是一个数组,作为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
和 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
也可以缓存一些东西,可以做一些性能优化提升性能。
接收一个 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
很简单,用来在函数组件中创建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
属性会一直累加。
我们知道,对于子组件,如果是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
没有Hook
,但笔者觉得 vue3
的 composition api
可以理解成vue
版的Hook
。
composition api
代码都写在 setup
函数里面,让逻辑关注点相关代码收集在一起。而且不再使用选项式写法,需要什么函数引入什么函数。
Vue Hook
使用规则: 只能在setup
函数里面使用。
下面来说说常用的一些Hook
。
接受一个内部值并返回一个响应式且可变的 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
定义的对象是不能直接使用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
是计算属性,意思就是会缓存值,只有当依赖属性发生变化的时候才会重新计算。
类似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
无法获取到变化前的值,只能获取变化后的值。
类似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
就是watchEffect
的别名,带有 flush: 'post'
选项。
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
需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。
与 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
,可以将组件逻辑提取到可重用的函数中。React
和Vue
都支持自定义Hook
。
下面我们分别用React
和Vue
实现一个实现鼠标打点的自定义 Hook
。
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
自定义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对比学习之Style样式
Vue和React对比学习之Ref和Slot
Vue和React对比学习之Hooks
Vue和React对比学习之路由(Vue-Router、React-Router)
Vue和React对比学习之状态管理 (Vuex和Redux)
Vue和React对比学习之条件判断、循环、计算属性、属性监听
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!
本文转自 https://juejin.cn/post/7103010557736779789,如有侵权,请联系删除。