前言
接上文:20分钟带你vue转react
本系列的文章适合 vue
转 react
的同志,建议接上文来看,目前反馈均不错
react
函数组件写法才是官方推荐的,官方推荐我们使用函数组件就是因为官方封装了很多 hooks
函数给我们,hooks
函数就是钩子函数,生命周期函数也可以称之为钩子函数,但是钩子函数并不是生命周期函数,一个大一个小的包含关系。
vue 的 hooks 就是比如
useRoute
、useRouter
、useStore
这样带 use 开头的函数,这就是 yyx 借鉴react
的体现之处
react
中的 hooks
本质就是为了让函数组件更加强大,官方给我们提供的 hooks
就 5,6 个的样子,接下来就详细学习下这几个 hooks
useState
之前类组件的时候有个 constructor
用于存放组件用到的状态(数据源),在函数组件中,数据源可以直接写在函数中,下面写一个效果,就是点击按钮可以实现累加,先看下类组件的写法
scala
import React, { Component } from 'react'
export default class State extends Component {
constructor () {
super()
this.state = {
count: 0
}
}
add = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<button onClick={() => this.add()}>{this.state.count}</button>
</div>
)
}
}
现在用函数组件来写
javascript
import React from 'react';
const State = () => {
let count = 0
const add = () => {
count++
}
return (
<div>
<button onClick={() => add()}>{count}</button>
</div>
);
};
export default State;
你会发现这样实现不了,其实 count
确实实现了 ++
的效果,相比较类组件的写法,你会发现目前的函数写法没有一个 setState
,之前的类组件中因为可以利用 class
的继承特性拿到 Component
中的 setState
,但是现在的函数写法拿不到 setState
,那如何实现试图更新呢
函数写法中,用得就是 useState
这个钩子函数
现在我想要更改 count
的值
scss
const [count, setCount] = useState(0)
引入 useState
后,调用返回一个数组,这个数组里面你写上自己的数据源,以及一个函数用于修改数据,这个函数前缀是 set
, useState
里面的参数就是数据源
其实第二个参数你可以任意取名,写
abc
都可以,这只是约定俗成的语义化
这个 setCount
就相当于类组件中的 setState
,现在的 "State" 可以更改了,其实这个时候我们自己也知道, setCount
无非就是干了两件事,一个是更改数据,另一个是触发 render
而 setCount
里面写的值一定是返回给 count
,最终带上 useState
写法如下
javascript
import React, { useState } from 'react';
const State = () => {
const [count, setCount] = useState(10) // useState()的执行结果一定是个数组
const add = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={() => add()}>{count}</button>
</div>
);
};
export default State;
因此 useState
的作用就是为函数组件提供状态
何谓组件的状态?光是一个普通的数据是不能称之为状态的,状态需要组件的值改了可以重新渲染页面,或者保持 vue
的思想,称之为响应式也可以
执行过程
读取代码的时候执行一遍 useState
, setCount
让 count
更改,并执行 render
,当我们 render
完后又会再一次读取 count
值, useState
依旧会重新调用,其实 render
一遍,这个函数组件就会重新执行一遍
或许你会疑惑:为何
render
了一次,整个函数都要执行一次,只更改响应式数据不可吗,响应式数据依靠函数中的代码,执行函数的代码是由v8
负责的,v8
性能是很强大的,因此无需担心这个问题
这个时候你肯定会疑惑,我 count
后面加到了 11
,重新 render
时就意味着再次执行 useState
,那 count
岂不是又会初始化到 10
?这就是 useState
的奥义所在,它会帮你把上一次的数据源拿到,也就是 setCount
中的值
不能放在 if 或者 for 循环语句中
我们看看下面这种写法
flag
甚至都是暗的,其实完全可以理解, flag
在外面拿不到就是以为 const
与{ }
形成了块级作用域,作用域链只能从里往外,而不能从外往里。当然你用 var
可以拿到,但是现在基本上不会去写 var
了,其实这里就算你用 var
也看不到内容,因为 html
中展示只能是字符串或者数字等,布尔是看不到的
为什么不建议用
var
,我们都知道这东西会出现声明提升,声明提升就会导致可能与其他语句相冲突
接受回调函数
有时候你也不知道 useState
的初始值是多少,你可能需要一个函数计算出来,这个时候可以传递一个回调进去
应用场景:比如购物车,初始时就计算好价格
依旧拿这个点击按钮实现累加的效果
javascript
import React, { useState } from 'react';
const State2 = () => {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(count+1)}>{count}</button>
</div>
);
};
export default State2;
比如这个值是根据父组件的值来决定的,父组件<State2 num={10} />
进行传参,子组件的 count
初始值为其一倍,写法就是如下,记得给函数加上 props
形参
dart
const [count, setCount] = useState(() => {
return props.num * 2
})
useEffect
看到这个 effect
你就会想起 vue
中 effect
函数了,就是副作用函数,所谓副作用并不是指的不好的影响,而是你在干这件事,附带产生了其他效果
在 react
中,我们可以把作用分为主作用和副作用,主作用就是根据数据来渲染页面,其他的作用全是副作用,常见的副作用有 useEffect
vue
中的副作用函数常见的就是computed
,watch
,只要状态值变了,computed
就会监听执行
在类组件中我们有生命周期钩子用于发接口请求啥的,但是函数组件我们就用 useEffect
,比如依旧是 button
那个栗子, count
值变了,去发接口请求
javascript
import React, { useEffect, useState} from 'react';
function Effect(props) {
const [count, setCount] = useState(0)
useEffect(() => {
console.log(`当前点击了${count}次`);
})
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
);
}
export default Effect;
你去控制台你就会发现页面初次加载就打印了两次,这就有点像是 vue
中的 watch
可以设置 immediate
,然后点击按钮,打印一次
若我们打印中不用上 count
呢, count
值变了依旧会触发 useEffect
的执行
其实也很好接受,刚才说了只要是 render
了,那么函数组件代码就会重新执行一遍,因此 useEffect
无论是否用上响应式数据,都会顺带执行一次
因此 useEffect
在页面初次加载时就会执行一次,并且组件中任意状态变更导致需要重新 render
就会让 useEffect
重新执行一次
第二个参数
useEffect
中的第二个参数是个数组,数组中有东西变更就会执行一次,若是什么都不放,就等于不会变更,因此执行了页面初次加载后,后续不会执行
scss
useEffect(() => {
console.log(`当前点击了10次`);
}, [])
从这个效果上来看, useEffect
很像是一个生命周期,没错!这种写法就可以充当 componentDidMount
另外,当你有多个状态时,比如我这里再加上一个 name
,再来一个按钮,点击可以更改 name
,我在第二个参数中只写上 count
,那么带来的效果是只有改变了 count
才会执行 useEffect
scss
useEffect(() => {
console.log(`当前点击了10次`);
}, [count])
// ......
<button onClick={() => setCount(count + 1)}>{count}</button>
<button onClick={() => setName('海豚')}>{name}</button>
这么来看,这个效果就可以充当 componentDidUpdate
,并且这个效果很像 vue
中的 computed
或者 watch
因此这个钩子很强大啊!
刚刚有说到只要是 render
完毕后,就会触发组件(函数)的重新执行,其实重新执行之前就会卸载上一次的组件,因此这又可以看成是卸载 componentWillUnmount
这个钩子
比如这里我写一个定时器间隔 1s
执行一次
scss
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1)
}, 1000)
}, [count])
居然出现了卡顿?!这是为什么? useEffect
在页面初次加载会执行一次,执行一次就代表会创建一个 timer
,这个 timer
又会更改 count
,更改了 count
又会导致 render
, render
又会导致 useEffect
的重新执行,这样下去会创建很多个定时器,浏览器的定时器线程支持不了很多定时器,只要变多了就会出现卡顿,因此需要对组件进行卸载
在 useEffect
中,卸载写在回调的 return
中
scss
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => { // 会在组件卸载时触发
// 清除副作用
clearInterval(timer)
}
}, [count])
这样就不会出现卡顿
每次卸载之前都把当前的 timer
给清除掉了,另外 count
会被 setCount
保存下来,实现累加的效果
因此函数组件中没有生命周期这一说法就是因为 useEffect
太强大了都可以充当,恰恰充当了最常用的三个生命周期函数,它还能充当vue
中的 computed
和 watch
useRef
在类组件中,我们拿到 dom
结构需要用 createRef
创建一个 dom
容器。在函数组件中就需要用 useRef
,跟 vue
写法几乎一致, vue
直接写 ref
,没有 use
写法如下,我们可以试着打印下这个 dom
javascript
import React, { useRef } from 'react';
const Ref = () => {
const h2Ref = useRef(null) // 得到一个可以存放dom结构的对象
console.log(h2Ref);
return (
<div>
<h2 ref={h2Ref}>Ref</h2>
</div>
);
};
export default Ref;
打印效果一看,居然都是 null
打印两次是因为:有一次是
react
源码中编译执行的,另一次才是浏览器执行的
这其实是因为打印代码的位置不对,函数组件和类组件其实是一样的,在类组件中, return
那里多了个 render
,其实函数组件中是一样的,也就是说打印代码在 render
之前先执行,此时还没渲染,必然拿不到 dom
。
因此想要拿到 dom
,需要用上钩子 componentDidMount
,在函数组件中就是 useEffect
并且第二个参数放一个空数组
javascript
import React, { useRef, useEffect } from 'react';
const Ref = () => {
const h2Ref = useRef(null) // 得到一个可以存放dom结构的对象
useEffect(() => {
console.log(h2Ref);
}, [])
return (
<div>
<h2 ref={h2Ref}>Ref</h2>
</div>
);
};
export default Ref;
拿到 dom
useContext
我们清楚父组件向后代组件跨组件通讯用的是 Provider
和 Consumer
,这是类组件的写法,在函数组件中也能用,不过需要借助 useContext
比如这里的情景,让 App.jsx
当爷爷组件,子组件 Context.jsx
,孙子组件 ContextChild.jsx
和类组件同样的写法, Provider
和 Consumer
需要来自同一个地方,因此需要额外有个文件提供 Provider
, Consumer
,其实就是用的 useContext
这种写法很像是
vue
中的eventBus
,写到全局中去,提供给别的组件使用
src/_context.js
javascript
import { createContext } from 'react'
const Con = createContext()
export default Con
子组件src/components/Context.jsx
javascript
import React, { useContext } from 'react';
import ContextChild from './ContextChild';
import Con from '../_context'
const Context = () => {
const msg = useContext(Con)
return (
<div>
<h3>子组件 -- {msg}</h3>
<ContextChild />
</div>
);
};
export default Context;
孙子组件src/components/ContextChild.jsx
javascript
import React from 'react';
import { useContext } from 'react'
import Con from '../_context'
const ContextChild = () => {
const msg = useContext(Con)
return (
<div>
孙子组件 - {msg}
</div>
);
};
export default ContextChild;
语法就是 src
下新建一个总线,通过 createContext
提供 Context
,这个 Context
让父组件去拿到 Provider
提供 value
给到后代组件,后代组件需要引入 Context
,在借助引入的 useContext
传入 Context
拿到 value
。相比较类组件,子组件就不需要 consumer
了,直接借助 useContext
来拿值
写法会复杂点,这点你要承认
vue
的provide
,inject
封装得更好
打造一个简单的 hooks
获取页面滚动距离
现在我们来尝试自己打造一个 hooks
函数,来获取页面滚动的距离
先看看直接用 js
如何拿到,如下
javascript
import React, { useState } from 'react';
const Myhooks = () => {
const [y, setY] = useState(0)
window.addEventListener('scroll', (e) => {
console.log(document.documentElement.scrollTop);
})
return (
<div style={{height: '200vh'}}>
<h2>当前页面的滚动距离:{y}</h2>
</div>
);
};
export default Myhooks;
所有的 hooks
函数都是 use
开头的 ,接下来打造一个 useScroll
函数,让它拿到滚动距离,给他默认距离 10
javascript
import React, { useState } from 'react';
import { useScroll } from '../_hooks/useScroll'
const Myhooks = () => {
const [pageY] = useScroll(10)
return (
<div style={{height: '200vh'}}>
<h2>当前页面的滚动距离:{pageY}</h2>
</div>
);
};
export default Myhooks;
好,src
下新建一个文件夹 _hooks
,新建一个文件放自己打造的 hooks
src/_hooks/useScroll.js
这个函数待会儿要抛出去,然后 hooks
一定会返回一个数组,数组里面就是想要的 y
值,这个 y
值可以有初始值,就是 useScroll
传入的参数
bash
export function useScroll (instance) {
let y = instance
return [y]
}
现在再把 y
改了,y
就是 document.documentElement.scrollTop
javascript
export function useScroll (instance) {
let y = instance
const handleScroll = () => {
y = document.documentElement.scrollTop
}
window.addEventListener('scroll', handleScroll)
return [y]
}
但是目前来看, y
虽然是改了,但是页面没有更新。这是因为赋值完给 pageY
后,组件加载完毕后,不会重新 render
,这样也就不会让 y
二次 return
既然要驱动 render
,那么我就在 y
中用 useState
,打造 hooks
时是可以用官方提供的 hooks
的
javascript
import { useState } from 'react'
export function useScroll (instance) {
const [y, setY] = useState(instance)
const handleScroll = () => {
setY(document.documentElement.scrollTop)
console.log(y);
}
window.addEventListener('scroll', handleScroll)
return [y]
}
目前这个效果其实已经打造完成了,但是当我们打印 y
的时候你会发现一个问题,假设页面已经滚到中间了,此时重新刷新,屏幕上的值不会跟着刷新变成 10
,这个效果是正确的,因为 useState
将上次数据缓存起来了
这里可以进行优化,当组件卸载时,这个事件监听器可以移除掉,因此整个事件我都可以写入 useEffect
中,这样方便我们移除掉事件,如下
javascript
import { useState, useEffect } from 'react'
export function useScroll (instance) {
const [y, setY] = useState(instance)
useEffect(() => {
const handleScroll = () => {
setY(document.documentElement.scrollTop)
console.log(y);
}
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
})
return [y]
}
这么来看,其实封装 hooks
很像是自行封装 utils
工具函数
再打造一个 hooks
,将按钮点击次数存到浏览器本地存储中
按钮点击次数存储到浏览器本地存储中
实现效果:可以修改 count
值,并将其存储到 LocalStorage
javascript
import React, { useState } from 'react';
import { useLocal } from '../_hooks/useLocal';
const Myhooks = () => {
const [count, setCount] = useLocal('count', 0)
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
);
};
export default Myhooks;
这里我待会儿打造的 useLocal
我传入两个参数,第一个是 key
,存到 LocalStorage
中的 key
,第二个是初始值
先保证这个 useLocal
具有 useState
的能力,可以修改 count
javascript
import { useState } from 'react'
export function useLocal (key, value) {
const [n, setN] = useState(value)
return [n, setN]
}
接下来就是使用 useEffect
来往 localStorage
中存 key-value
了
javascript
import { useState, useEffect } from 'react'
export function useLocal (key, value) {
const [n, setN] = useState(value)
useEffect(() => {
window.localStorage.setItem(key, n)
})
return [n, setN]
}
目前这样来看是没有问题的,但是,这个组件被拿到根组件中调用,根组件中若有其他的数据源变更到了 render
,那么这个 useLocal
就会重新调用一次,比如上面写的滚动距离,我们没点击按钮,仅仅滚动页面,这个往 localStorage
存值的行为依旧会发生,这就显得非常没有必要,因此,这就要动用 useEffect
的第二个参数,往里面放 n
即可,这样别人即使 render
了,自己的 useEffect
没有看到 count
值变,就不会执行
scss
useEffect(() => {
window.localStorage.setItem(key, n)
}, [n])
好了,目前可以存值又不受其他钩子的影响,或者说是不受其他数据源导致的 render
的影响
其实封装 hooks
就是封装一个函数
另外封装 hooks
时,命名文件无需命名为 jsx
后缀,这不是个组件,否则又会被 react
源码进行一个编译解析
TodoList: antd
现在我们可以试着用函数组件实现一个 todolist
,为了让 todolist
更美观,这里用上了 antd 这个 ui
组件库,这个 ui
库原本打造就是为了提供给 react
用的,但是现在也有个 vue
版本的 antd
安装
这里我用的包管理工具是 npm
,就是npm i antd --save
bun
也是个包管理工具,国外用的多
实现
可以按需引入,就是引入到需要用上的那个组件中,也可以直接引入到 main.js
中, antd
的引入就有一个,不想国内的组件库还需要引入 css
, antd
天生就是按需加载,用到哪个组件才加载哪个组件的 css
,如果想要用 antd
中的 icon
得话还需要额外安装一个 icon
包
这里实现,我依旧把 todoList
分成两个组件来写,父组件是上面的输入框和确认按钮,子组件是列表,列表的数据源我又拿到父组件。输入框,按钮和列表我都用 antd
提供的
src/todo/TodoList.jsx
javascript
import React, { useRef } from 'react';
import TodoItem from './TodoItem';
import { Input, Button } from 'antd'
const data = [
'Racing car sprays burning fuel into crowd.',
'Japanese princess to wed commoner.',
'Australian walks 100km after outback crash.',
'Man charged over missing wedding girl.',
'Los Angeles battles huge wildfires.',
];
// 父组件
const TodoList = () => {
const handleClick = () => {}
return (
<div style={{width: '400px'}}>
<header style={{display: 'flex'}}>
<Input placeholder="Basic usage" />
<Button onClick={handleClick}>提交</Button>
</header>
<section>
<TodoItem data={data}/>
</section>
</div>
);
};
export default TodoList;
src/todo/Item.jsx
javascript
import React from 'react';
import { List, Tag } from 'antd'
// 子组件
const TodoItem = (props) => {
return (
<div>
<List
bordered
dataSource={props.data}
renderItem={(item) => (
<List.Item>
{item}
<Tag closeIcon onClose={() => {}}></Tag>
</List.Item>
)}
/>
</div>
);
};
export default TodoItem;
子组件用的
tag
,tag
就是带会儿用来做删除用的
对于这个 button
,当我们用了它的组件的时候,就最好检查一下事件名是否改写,这里的 button
依旧是 onClick
拿到 input
框中的值有两种方法,一个受控组件,一个非受控组件,这里我就采用非受控写法,因为有个 useRef
钩子
csharp
import React, { useRef } from 'react';
// ......
const inputRef = useRef(null)
// ......
<Input placeholder="Basic usage" ref={inputRef} />
拿到 input
值后,需要塞入到 data
数据源中,塞之前将 data
响应式处理,也就是 useState
可以通过 button
点击事件将 input
值塞入到 data
中, data
就是 useState
的初始值,然后再通过这个点击事件更改这个 data
, 用解构的写法,如下
javascript
import React, { useRef, useState } from 'react';
import TodoItem from './TodoItem';
import { Input, Button } from 'antd'
// 父组件
const TodoList = () => {
const data = [
'Racing car sprays burning fuel into crowd.',
'Japanese princess to wed commoner.',
'Australian walks 100km after outback crash.',
'Man charged over missing wedding girl.',
'Los Angeles battles huge wildfires.',
];
const [newData, setNewData] = useState(data)
const inputRef = useRef(null)
const handleClick = () => {
console.log(inputRef.current.input.value);
setNewData([...newData, inputRef.current.input.value])
}
return (
<div style={{width: '400px'}}>
<header style={{display: 'flex'}}>
<Input placeholder="Basic usage" ref={inputRef} />
<Button onClick={handleClick}>提交</Button>
</header>
<section>
<TodoItem data={newData}/>
</section>
</div>
);
};
export default TodoList;
这个更改值用解构很优雅,此前用类组件得话,浅拷贝一个新的数组,对这个新的数组进行赋值,然后放入 setState
中,将会是下面这种写法
ini
const handleClick = () => {
let arr = newData
arr.push(inputRef.current.input.value)
setNewData(arr)
}
其实这么写是不行的,当我们对 arr
修改的时候,其实就是对 newData
修改,这个修改并没有用上 setNewData
,因此不会生效,因为你自始至终都是对原来的 newData
进行操作
你肯定想问,最后不是还
setNewData
了吗,在执行setNewData
时,此时的newData
已经被添加了input
值,想要用usestate
修改值,必须与原来的值不同,这里已经相同了,因此不会生效
想要借助另外一个数组,就必须是深拷贝,得是另一份地址,如下写法,但是很没必要,直接用解构,解构本身就是深拷贝
scss
const handleClick = () => {
let arr = structuredClone(newData)
arr.push(inputRef.current.input.value)
setNewData(arr)
}
目前效果如下
好了,现在去实现子组件的删除功能
那就需要给删除按钮绑定一个点击事件,然后将当前项下标进行传参,子组件删掉父组件的数据就是子父传参,需要父组件传递一个函数进来,最终如下
父组件TodoList.jsx
javascript
import React, { useRef, useState } from 'react';
import TodoItem from './TodoItem';
import { Input, Button } from 'antd'
// 父组件
const TodoList = () => {
const data = [];
const [newData, setNewData] = useState(data)
const inputRef = useRef(null)
const handleClick = () => {
console.log(inputRef.current.input.value);
setNewData([...newData, inputRef.current.input.value])
}
const onDelete = (i) => {
let arr = newData.filter((str, index) => index !== i)
setNewData(arr)
}
return (
<div style={{width: '400px'}}>
<header style={{display: 'flex'}}>
<Input placeholder="Basic usage" ref={inputRef} />
<Button onClick={handleClick}>提交</Button>
</header>
<section>
<TodoItem data={newData} cb={onDelete}/>
</section>
</div>
);
};
export default TodoList;
子组件TodoItem.jsx
javascript
import React from 'react';
import { List, Tag } from 'antd'
// 子组件
const TodoItem = (props) => {
const onDel = (e, i) => {
e.preventDefault() // 文档上写的:阻止一个默认行为
props.cb(i)
}
return (
<div>
<List
bordered
dataSource={props.data}
renderItem={(item, i) => (
<List.Item>
{item}
<Tag closeIcon onClose={(e) => onDel(e, i)}>
删除
</Tag>
</List.Item>
)}
/>
</div>
);
};
export default TodoItem;
最终效果如下
总结
hooks
的目的就是让函数组件更强大,因此所有的 hooks
只能在函数组件中使用
useState
:为函数组件提供状态useEffect
:不依赖指定数据源,默认执行一次,当组件中有状态变更导致组件重新render
,该函数会重新执行;- 第二个参数传入空数组可以充当
componentDidMount
- 第二个参数传入指定数据源作数组元素可以充当
componentDidUpdate
- 回调中返回的函数可以充当
componentWillUnmount
- 第二个参数传入空数组可以充当
useRef
:在函数组件中获取dom
结构useContext
:在函数组件中实现祖先组件向后代组件跨组件传值
最后
函数式组件因为没有 class
,不方便继承状态和生命周期,就借助了 hooks
弥补这一问题,并且更加优雅。其实面试中,面试官很喜欢让你手写一个 hooks
, hooks
聊完了,接下来准备出一期文章来聊 react
中的 router
,感兴趣的小伙伴可以关注我或者这一专栏,皆是方便各位 vue
转 react
,一步一个脚印
另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请 "点赞+评论+收藏" 一键三连,感谢支持!