推荐使用 antDesign
组件库
可视化antV
专注于构建用户界面的JavaScript库,和Vue、angular并称前端三大框架,其中react是目前世界范围内最流行的js前端框架,版本已经更新到了18。
文档(英文)https://reactjs.org/
文档(中文)https://zh-hans.reactjs.org/
新文档 https://beta.reactjs.org/
声明式UI(jSX)
写UI就和写普通HTML一样,抛弃命令式的繁琐实现,不需要逐步编写dom逻辑去实现。
组件化
组件是React中最重要的内容,组件可以通过搭积木的方式拼成一个完整的页面,通过组件的抽象可以增加复用能力和提高可维护性。
一次学习,跨平台编写
react既可以开发web应用,也可以使用同样的语法开发原生应用(react-native), 比如安卓和ios应用,甚至可以使用react开发VR应用,它像一个元框架为各种领域赋能。
1 使用脚手架创建项目
npx create-react-app my-app
# npx命令会帮助我们临时安装create-react-app包,然后初始化项目完成之后会自动删除,不需要全局安装create-react-app
2 项目目录说明
React: 框架核心包。
ReactDOM: 专门做渲染相关的包。
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
// 引入根组件
import App from './App'
// 渲染根组件App到一个id为root的dom节点上
ReactDOM.reader(
// 严格模式节点,会影响useEffect的执行时机
<React.StrictMode>
<App/>
<React.StrictMode>,
document.getElementById('root')
)
安装VSCode prettier 插件
修改配置文件setting.json
{
"git.enableSmartCommit": true,
// 修改注释颜色
"editor.tokenColorCustomizations": {
"comments": {
"fontStyle": "bold",
"foreground": "#71d151dd"
}
},
// 配置文件类型识别
"files.associations": {
"*.js": "javascript",
"*.json": "jsonc",
"*.cjson": "jsonc",
"*.wxss": "css",
"*.wxs": "javascript"
},
"extensions.ignoreRecommendations": false,
"files.exclude": {
"**/.DS_Store": true,
"**/.git": true,
"**/.hg": true,
"**/.svn": true,
"**/CVS": true,
"**/node_modules": false,
"**/tmp": true
},
// "javascript.implicitiProjectConfig.experimentalDecorators": true,
"explorer.confirmDragAndDrop": false,
"typescript.updateImportsOnFileMove.enabled": "prompt",
"git.confirmSync": false,
"editor.tabSize": 2,
"editor.fontWeight": "300",
"[json]": {},
"editor.tabCompletion": "on",
"vsicons.projectDetection.autoReload": true,
"editor.fontFamily": "Monaco, 'Courier New', monospace, Meslo LG M for Powerline",
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
},
"editor.fontSize": 16,
"debug.console.fontSize": 14,
"vsicons.dontShowNewVersionMessage": true,
"editor.minimap.enabled": true,
"emmet.extensionsPath": [
""
],
// vue eslint start 保存时自动格式化代码
"editor.formatOnSave": true,
// eslint 配置项,保存时自动修复错误
"editor.codeActionsOnSave": {
"source.fixAll": true
},
"vetur.ignoreProjectWarning": true,
// 让vetur 使用vs自带的js格式化工具
// uni-app和vue项目使用
"vetur.format.defaultFormatter.js": "vscode-typescript",
"javascript.format.semicolons": "remove",
// 指定 *.vue 文件的格式化工具为vetur
"[vue]": {
"editor.defaultFormatter": "octref.vetur"
},
// 指定 *.js 文件的格式化工具为vscode自带
"[javascript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
// 默认使用prettier 格式化支持的文件
"editor.defaultFormatter": "esbenp.prettier-vscode",
"prettier.jsxBracketSameLine": true,
// 函数前面加个空格
"javascript.format.insertSpaceBeforeFunctionParenthesis": true,
"prettier.singleQuote": true,
"prettier.semi": false,
// eslint end
// react
// 当按tab键的时候,会自动展示
"emmet.triggerExpansionOnTab": true,
"emmet.showAbbreviationSuggestions": true,
"emmet.includeLanguages": {
// jsx 的提示
"javascript": "javascriptreact",
"vue-html": "html",
"vue": "html",
"wxml": "html"
},
// end
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
// @路径提示
"path-intellisense.mappings": {
"@": "${workspaceRoot}/src"
},
"security.workspace.trust.untrustedFiles": "open",
"git.ignoreMissingGitWarning": true,
"workbench.colorTheme": "Dracula Soft",
"workbench.iconTheme": "vscode-icons",
"window.zoomLevel": 1
}
vscode ErrorLens: 错误提示,实时的。
Snipaste 截图
概念:JSX是JavaScript XML(HTML)的缩写,表示在 JS 代码中书写 HTML 结构。
作用: 在 React 中创建 HTML 结构(页面 UI 结构)
优势:
- 采用类似 HTML 的语法,降低学习成本。(声明式)
- 充分利用 JS 自身的可编程能力创建 HTML 结构。
JSX 并不是标准的 JS 语法,是 JS 的语法扩展,浏览器默认是不识别的,脚手架中内置的 @babel/plugin-transform-react-jsx 包,用来解析该语法。
语法{js 表达式}
const name = '柯基'
你好,我叫{name}
可以使用的表达式
特别注意
if 语句/switch-case语句/变量声明语句,这些叫做语句,不是表达式,不能出现在 {}
中!
页面的构建离不开重复的列表结构,比如商品列表、图书列表,vue 中使用的是v-for, react中使用的是数组
map
方法。
// eg
const songs = [
{id: 1, name: '晚风'},
{id: 2, name: '夏日'},
{id: 3, name: '穠芳'}
]
// 遍历列表时,需要一个类型为 number/string 的不重复值作为key,提高React中diff算法的性能
// key 仅仅在内部使用,不会出现在真实的 dom 节点上
function App(){
return (
{ songs.map(item => - {item.name}
) }
)
}
条件渲染:可以是模板中写入三元运算、与或操作、抽离出的函数等,这些都可以展示出条件渲染。
行内样式 style
function App() {
return (
this is a div
)
}
export default App
行内样式 style 更优写法
const styleObj = {
color: red
}
function App() {
return (
this is a div
)
}
export default App
类名样式
// app.css
.active {
color: blue;
}
// App.js
const activeFlag = false
function App() {
return (
this is a div
动态控制active类名
)
}
export default App
<>>
(幽灵节点)代替。function App() {
return (
<>
app
app
>
)
}
class -> className for -> htmlFor
()
包裹,防止出现bug。 上图所示的左侧页面效果对应到React中,它是一个组件树的形式。
使用 JS 的函数(或箭头函数)创建的组件,就叫做
函数组件
。
// 函数组件的创建和渲染
// 创建
function Hello () {
return <div>hello</div>
}
// 渲染
// 渲染
function App () {
return (
<div>
<Hello/>
</div>
)
}
export default App
约定:
必须首字母大写
,react 内部会根据这个来判断是组件还是普通HTML标签。必须有返回值
,表示该组件的 UI 结构;如果不需要渲染任何内容,则返回null。返回值
就是对应的内容。使用 ES6 的class 创建的组件,叫做类(class)组件。
// 类组件的创建和渲染
// 创建
class HelloComponent extends React.Component{
render() {
return <div> 这是一个类组件!</div>
}
}
// 渲染(成对出现/自闭合)
function App () {
return (
<div>
<HelloComponent></HelloComponent>
</div>
)
}
export default App
约定:
语法: on + 事件名称 = {事件处理程序},如
{}}>/div>事件名称采用驼峰命名方法,如: onMouseEnter、onFocus
// 函数组件绑定 function Hello () { const clickHandler = () => { console.log('函数组件的事件被触发了!') } return <div onClick={clickHandler}>hello</div> } // 类组件绑定 class HelloComponent extends React.Component{ // 事件回调函数(标准写法,避免this指向不明) clickHandler = (msg) =>{ console.log('类组件的事件被触发了!',msg); } render() { return (<div onClick={()=>this.clickHandler('疾风知行')}> 这是一个类组件!</div>) } }
3.2.2 获取事件对象
通过事件处理程序的参数获取事件对象e。
// 函数组件 function HelloFn () { // 定义事件回调函数 const clickHandler = (e) => { // 阻止默认操作 e.preventDefault() console.log('事件被触发了', e) } return ( // 绑定事件 <a href="http://www。baidu.com/" onClick={clickHandler}>百度</a> ) }
3.2.3 传递自定义参数
// 1. 只需要一个额外参数 {clickHandler -> {() => clickHandler('自定义的参数')} // 2.既需要e也需要额外的参数 {(e)=>clickHandler(e,'自定义的参数')} function HelloFn () { // 定义事件回调函数 const clickHandler = (msg) => { console.log('事件被触发了', msg) } return ( <div onClick={() => clickHandler('传递成功')}>click me</div> )
3.3 组件状态
在 react hook出来之前,函数式组件没有自己的状态。
state 中尽量保持精简,如果数据是组件的状态需要去影响视图,定义到 state中。而如果我们需要的数据状态,不和视图绑定,定义成一个普通的实例属性就可以!
初始化状态 ------> 读取状态-------->影响视图
通过 class 的实例属性 state 来初始化。
state 的值是一个对象结构,表示一个组件可以有多个数据状态。
// 类组件 import React from 'react' class Counter extend React.Component { // 1.初始化状态 state = { // 此处可以定义各种属性,全部都是当前组件的状态 count: 0 } // 事件回调函数 changCount = () => { // 3. 修改 state 中的状态 count // 注: 不可以直接做赋值修改,必须通过一个方法 setState this.setState({ count: this.state.count + 1 }) } render () { // 2. 使用状态 return ( <button onClick={this.changCount}>计数{this.state.count}</button> ) } }
注意:
- 编写组件其实就是编写原生 js 类或者函数。
- 定义状态必须通过 state ,提供一个对象,名称固定叫做 state。
- 修改 state 中的任何属性,都不可以通过直接赋值,必须走 setState 方法。
- 关注 this 指向。
3.4 React 状态不可变
**概念:**不要直接修改状态的值,而是基于当前状态创建新的状态值
基于当前状态创建新值
state = { count: 0, list: [1,2,3], person: { name: 'owei', age: 18 } } this.setState({ count: this.state.count + 1, list: [...this.state.list,4], person: { ...this.state.person, // 覆盖原来的属性,就能修改对象中的属性值!!! name: 'rose' } })
3.5 this 问题说明
上图中 this 沿用父函数(render)中的 this 指向。
3.6 表单处理
使用 React 处理表单元素,一般有两种方式:
- 受控组件(推荐)
- 非受控组件
3.6.1 受控表单组件(√)
受控组件
可以
被 React 的状态控制
的组件。React 组件的状态在 state 中, input 表单元素也有自己的状态是在 value 中,React 将state 与表单元素的值(value)绑定到一起, 由 state 的值来控制表单元素的值,从而保证单一数据源特性。
实现步骤:
以获取文本框的值为例,受控组件的使用步骤如下:
- 在组件的 state 中声明一个组件的状态数据。
- 将状态数据设置为 input 标签元素的 value 属性的值。
- 为 input 添加 change 事件。
- 在事件处理程序中,通过事件对象 e 获取到当前文本框的值(即当前用户的输入值)
- 调用 setState 方法,将文本框的值作为 state状态的更新值。
代码落地(双向绑定的底层写法)
import React from "react" class Counter extends React.Component { // 1. 声明用来控制 input value 的react组件自己的状态 state = { message: 'this is message' } // 回调函数 inputChange = (e) => { // 4.拿到输入框最新的值,交给 state 中的 message this.setState({ message: e.target.value }) } // 产出 UI 模板结构 render () { return ( // 2.给 input 框的value属性绑定 react state // 3.给 input 框绑定一个 change 的事件,为了拿到当前输入框中的数据 <input type='text' value={this.state.message} onChange={this.inputChange} /> ) } } function App () { return ( <div className = "App"> <InputComponent /> </div> ) } export default App
3.6.2 非受控表单组件
非受控组件
非受控组件就是通过手动操作 dom 的方式获取文本框的值,文本框的状态不受 react 组件的 state 中的状态的控制,直接通过原生 dom 获取输入框的值。
实现步骤
- 导入 createRef 函数
- 调用 createRef 函数,创建一个 ref 对象,存储到名为 msgRef 的实例属性中。
- 为 input 添加 ref 属性,值为 msgRef
- 在按钮的事件处理程序中,通过 msgRef.current 即可拿到对应的 dom 元素,而其中 msgRef.current.value 拿到的就是文本框的值。
代码实现
import React, {createRef} from 'react' class Input extends React.Component { msgRef = createRef() getValue = () => { console.log(this.msgRef.current.value) } render () { <> <input type='text' ref={this.msgRef} /> <button onClick={this.getValue}>点击获取输入框的值</button> </> } }
额外补充小知识点:
生成独一无二的 id 可以使用 uuid 包(
yarn add uuid
)import { v4 as uuid } from 'uuid' uuid() // 得到一个独一无二的id
4 React 组件通信
组件通信的意义
组件是独立且封闭的单元,默认情况下组件只能使用自己的数据(state)。组件化开发的过程中,完整的功能会拆分多个组件,在这个过程中不可避免的需要互相传递一些数据。为了让各组件之间可以进行互相沟通,数据传递,这个过程就是组件通信。
父子关系(√)
兄弟关系 - 自定义事件模式产生计数方法 eventBus / 通过共同的父组件通信
其他关系 - mobx/Redux/ 基于hook的方案
4.1 父传子实现
实现步骤
父组件提供要传递的数据
state
// 父组件的数据 state = { message: 'this is message' }
给子组件标签 -
添加属性
值为state的数据。// 子组件 // 子组件身上绑定属性,属性名可以自定义,保持语义化 <SonF msg={this.state.message} />
子组件中通过
props
接收父组件中传过来的数据。
- 类组件使用
this.props
获取props
对象。- 函数式组件直接通过参数获取
props
对象。代码实现
// App 父组件 Son 子组件 import './App.css' import React from 'react' function SonF (props) { return ( <div> <span>{props.msg}</span> </div> ) } class SonG extends React.Component { render () { return ( <div>{this.props.says}</div> ) } } class App extends React.Component { state = { msg: '今天天气真好!', say: 'SonG: 确实不错!' } render () { return ( <div> <p>父组件出现</p> <SonF msg={this.state.msg} /> <SonG says={this.state.say}></SonG> </div> ) } } export default App
4.2 props 说明
1. props 是只读对象(readonly)
根据单项数据流的要求,子组件只能读取 props 中的数据,不能进行修改。
2. props 可以传递任意数据
数字、字符串、布尔值、数组、对象、函数、JSX(vue中传递模板使用插槽)
// App 父组件 Son 子组件 import './App.css' import React from 'react' function SonF (props) { return ( <div> {props.list.map(item => <p key={item}>{item}</p>)} {props.userInfo.name} <button onClick={props.getMes}>触发传入的函数</button> {props.child} </div> ) } class App extends React.Component { state = { list: [1,2,3], userInfo: { name: 'owei', age: 59 }, } render () { return ( <div> <SonF list={this.state.list} userInfo={this.state.name} getMes={()=>{console.log("父组件的函数")}} child={<p>jsx 模板</p>} /> </div> ) } }
4.3 porps 解构赋值
- 对props进行解构
- 在参数处直接解构
porps 其实就是一个普通的 js 对象!
实现代码
import React from 'react' // App 父组件;Son 子组件 // 函数式的Son function SonF (props) { // props 是一个对象,里面存着通过父组件传入的所有数据 console.log(props) // 解构赋值 const { list,userInfo,getMes,child } = props return ( <div> {list.map(item => <p key={item}>{item}</p>)} {userInfo.name} <button onClick={getMes}>触发传入的函数</button> {child} </div> ) } // 或者在参数里解构赋值 function SonG ({ list, userInfo, getMes, child }) { // props 是一个对象,里面存着通过父组件传入的所有数据 console.log(props) return ( <div> {list.map(item => <p key={item}>{item}</p>)} {userInfo.name} <button onClick={getMes}>触发传入的函数</button> {child} </div> ) } class App extends React.Component { state = { list: [1,2,3], userInfo: { name: 'owei', age: 59 }, } render () { return ( <div> <SonF list={this.state.list} userInfo={this.state.name} getMes={()=>{console.log("父组件的函数")}} child={<p>jsx 模板</p>} /> <SonG list={this.state.list} userInfo={this.state.name} getMes={()=>{console.log("父组件的函数")}} child={<p>jsx 模板</p>} /> </div> ) } }
4.4 子传父
子组件调用父组件传递过来的函数,并且把想要传递的数据当成函数的实参传入即可。
代码实现
function Son (props) { const { getSonMsg } = props return ( <div> 子组件 <button onClick= {()=>getSonMsg('这是来自于子组件的参数')}></button> </div> ) } class App extends React.Component { // 准备数据 state = { list: [1,2,3] } // 1.准备一个函数,传个子组件 getSonMsg = (sonMsg) => { console.log(sonMsg) } render () { return ( <div> <Son getSonMsg={this.getSonMsg /> </div> ) } }
4.5 兄弟组件通信
通过状态提升机制,利用共同的父组件实现兄弟通信。
实现步骤
// 实现思路 // 先将SonB中的数据通过子传父,传给App // 再把App接收到的SonB中的数据,通过父传子传递给SonA function SonA (props) { return ( <div>this is SonA <p>A组件{props.aMsg}</p> </div> ) } function SonB (props) { const bMsg = "SOnB中的数据" return ( <div>this is SonB <button onClick={()=>props.getBMsg(bMsg)}>clickB</button> </div> ) } class App extends React.Component { state = { message: '' } // 函数传给SonB getBMsg = (msg) => { console.log(msg) // 将获取的SonB的传值,更新到App中 this.setState({ message: msg }) } render () { <div> <SonA aMsg=this.state.message/> <SonB getBMsg={this.getBMsg}/> </div> } }
4.6 跨组件通信 Context
背景介绍
如上图所示是一个 react 形成的嵌套组件树,如果我们想从 App 组件向任意一个下层组件传递数据,可以怎么做?
- 采用一层一层的props往下传,层级较多时非常麻烦。
- Context 提供了一个无需为每层组件手动添加 props, 就能在组件树间进行数据传递的方法。
实现步骤
创建 Context 对象,导出 Provider 和 Consumer 对象
const { Provider,Consumer} = createContext()
使用 Provider 包裹根组件提供数据
<Provider value = {this.state.message}> {/* 根组件 */} </Provider>
需要用到数据的组件使用 Consumer 包裹获取数据
<Consumer> {value => /* 基于 context 值进行渲染*/} </Consumer>
注意事项
上层组件和下层组件关系是相对的,只要存在上下层关系就可以使用。通过会通过 App 作为数据提供方。
涉及到的语法都是固定的(提供数据的位置必须用
value
,或许数据的位置{value =>{}}
代码实现
// App 组件直接包含 SonA, SonA 组件直接包含 SonC // 需求: App 直接将数据传给 SonC import React, { createContext } from 'react' // 1. 导入 createContext 方法 const { Provider, Consumber } = createContext function SonA () { return ( <div> this is SonA <SonC/> </div> ) } function SonC () { return ( <div> this is SonC // 3. 消费数据 <Consumer> {value=><span>{value}</span>} </Consumer> </div> ) } class App extends React.Component { state = { message: '父组件的值' } render () { return ( // 2. 使用 Provider 包裹根组件 <Provider value = { this.state.message }> <div> this is App <SonA/> </div> </Provider> ) } }
5 组件(进阶)
5.1 children 属性
props 中 children 属性的用法
children 属性表示该组件的子节点,只要组件内部有子节点,props 中就有该属性。children 可以是==普通文本、普通标签元素、函数、JSX。==如果 children 中有多个元素,以数组形式展示。
实现效果
import React from 'react' // 渲染列表 function ListItem ({ children}) { return ( <div> ListItem <p>{children}</p> </div> ) } class App extends React.Component { render () { return ( <div> <ListItem> // 在 ListItem节点中写入“this is child”,props中就有该“this is child" this is child </ListItem> </div> ) } }
5.2 props 校验-常见和使用
对于组件来说,props 是由外部传入的,我们无法保证组件使用者传入了什么格式的数据,如果传入的数据格式不对,就有可能会导致组件内部错误,组件的使用者可能报错了也不知道具体原因。
例如:需要对colors 进行 prop 校验
实现步骤(React 中并不是内置的 prop 校验)
- 安装属性校验包:
yarn add prop-types
。- 导入
prop-types
包。- 使用
组件名.propTypes = {}
给组件添加校验规则。实现代码
// 注意其中的大小写! import React from 'react' // porp-types 含有各种各样的内置校验规则 import PropTypes from 'prop-types' function Test ({ list }) { return ( <div> { list.map(item => <p>{item}</p>)} </div> ) } Test.propTypes = { // 定义规则 list: PropTypes.array // 限定这里的 list 参数类型必须是数组类型 } class App extends React.Component { render () { return ( <div> <Test list={[1,2,3]} /> </div> ) } }
5.3 类型校验说明
四种常见结构
常见类型: array、bool、func、number、object、string
React 元素类型: element(jsx)
必填项: isRequired
Test.propTypes = { list: PropTypes.array.isRequired // 限定这里的list参数类型必须是数组,且是必填 }
特定的结构对象: shape({})
核心代码
// 常见类型 optionalFunc: PropTypes.func, // 必选 requiredFunc: PropTypes.func.isRequired, // 特定结构的对象 optionalObjectWithShape: PropTypes.shape({ color: PropTypes.string, fontSize: PropTypes.number })
5.4 校验中的默认值
通过 defaultProps 可以给组件的 props 设置默认值,在未传入 props 的时候生效。
5.4.1 函数组件
两种传递方式的区别:第一种在用的时候组件内部已经有了 pageSize 这个 prop;第二种只有传递的时候组件内部才有这个 prop。
- 使用 defaultProps
function List(props) { return ( <div> 此处展示props的默认值: {props.pageSize} </div> ) } // 设置默认值 List.defaultProps = { pageSize: 10 } // 不传入pageSize属性 <List />
- 使用函数参数默认值(推荐)
注:函数组件,新版的 react 已经不再推荐使用 defaultProps 来添加默认值,而是推荐
函数参数默认值
来实现。function List ({pageSize = 10}) { return ( <div> 此处展示 props 的默认值: { pageSize } </div> ) }
5.4.2 类组件
- 使用defaultProps
class List extends React.Component { render () { return ( <div> 此处展示props的默认值: {this.props.pageSize} </div> ) } } List.defaultProps = { pageSize: 10 } // 不传入 pageSize 属性 <List />
- 使用类静态属性声明(推荐)
class List extends React.Component { static defaultProps = { pageSize: 10 } render () { return ( <div> 此处展示 props 的默认值:{this.props.pageSize} </div> ) } }
6 组件生命周期
6.1 生命周期概述
组件的生命周期是指组件从被创建到挂载到页面中运行起来,再到组件不用时卸载的过程,注意,只有类组件才有生命周期。(类组件需要实例化,存在生命周期;函数组件,不需要实例化没有生命周期。
不可以在 render/componentDidUpdate 中执行setState.
图例:下图
6.2 挂载阶段
执行顺序 钩子函数 触发时机 作用 1 constructor 创建组件时,最先执行,初始化的时候只执行一次 1.初始化state。2.创建Ref。3.使用bind解决this指向问题等。 2 render 每次组件渲染都会触发(视图变化就会执行) 渲染UI(注意: 不能在里面调用 setState()) 3 componentDidMount 组件挂载(完成DOM渲染)后执行,初始化的时候执行一次 1.发送网络请求。2.DOM操作。 6.3 更新阶段
执行顺序 钩子函数 触发时机 作用 1 render 每次组件渲染都会触发 渲染UI(与挂载阶段是同一个render) 2 componentDidUpdate 组件更新后(DOM渲染完毕) DOM操作,可以获取到更新后的DOM内容,不要直接调用setState 6.4 卸载阶段
钩子函数 触发时机 作用 componentWillUnmount 组件卸载(从页面中消失) 执行清理工作(比如:清理定时器等) 7 Hooks
纯函数(pure function)
给一个函数同样的参数,那么这个函数永远返回同样的值。即React组件输入相同的参数(props),渲染UI应该永远一样。
7.1 概念
Hooks 的本质: 一套能使函数组件更强大,更灵活的 ”钩子“
某种意义上 hook 的出现,就是想不用生命周期概念也可以写业务代码。
React 体系里组件分为 类组件 、函数组件。
函数组件是一个更加匹配 React 的设计理念==
UI=f(data)
==, 也更有利于逻辑拆分与重用的组件表达形式,而先前的函数组件是不具有自己状态的,为了能让函数组件可以拥有自己的状态,所以 react v16.8开始,Hooks 应运而生。注意点:
- 有了 hooks 之后,为了兼容老版本,class 类组件并没有被移除,两者都可以使用。
- 有了hooks 之后,不能再把函数当成无状态组件了,因为 hooks 为函数组件提供了状态。
- hooks 只能在函数组件中使用。
Hooks 的出现解决了两个问题:
组件的状态逻辑复用。
在 hooks 出现之前,react 先后尝试了 mixins 混入,HOC 高阶组件, render-props等模式。但是都有各自的问题,比如 mixin 的数据来源不清晰,高阶组件的嵌套问题等等。
class 组件自身的问题。
class 组件大而全,提供了许多内容,有不可忽视的学习成本,比如各种生命周期,this 指向问题等等。
Hooks 优点
- 告别难以理解的 class
- 解决业务逻辑难以拆分的问题。
- 使状态逻辑复用变得简单可行。
- 函数组件在设计思想上,更加契合 React 的理念。
7.2 useState
状态钩子:useState(),React自带的hook函数,声明组件状态。其返回值为包含count,setCount的数组,分别表示[状态,状态更新函数],userState(0)是初始count(state)值为0。
7.2.1 代码实现
// 累加运算 useState // 快速使用 // 1. 导入 useState函数 react // 2.执行这个函数并且传入初始值,必须在函数组件中。 // 3.[数据,修改数据的方法] // 4.使用数据,修改数据 import React, {useState} from 'react' function App () { const [count, setCount] = useState(0) return ( <div> <button onClick={() => setCount(count + 1)}>{count}</button> </div> ) } export default App
7.2.2 状态的读取和修改
// 状态的读取和修改 // 数组的解构赋值,两者的顺序不可以交换(第一个参数就是数据状态,第二个参数就是修改数据的方法) const [count, setCount] = useState(0) // 1.useState 传过来的参数,作为 count 的初始值。 // 2.useState 返回值是一个数组 // 3.setCount 函数,用来修改count,依旧保持不能直接修改原值,而是生成一个新值替换原值。 // setCount(基于原值计算得到的新值) // 4.count 和 setCount 是一对,setCount只能用来修改对应的count值。
7.2.3 组件的更新过程
函数组件使用 useState hook 之后的执行过程,以及状态值的变化
- 组件第一次渲染(首次渲染)
- 从头开始执行该组件中的代码逻辑
- 调用
useState(0)
将传入的参数作为状态初始值,即: 0- 渲染组件,此时,获取的状态 count 值为: 0
- 组件第二次渲染(更新渲染)
- 点击按钮,调用
setCount(count+1)
修改状态,因为状态发生改变,所以,该组件会重新渲染。- 组件重新渲染时,会再次执行该组件中的代码逻辑。
- 再次调用
useState(0)
,此时 React 内部会拿到最新的状态值而非初始值,比如,上述代码中最新的状态值为1。- 再次渲染组件,此时,获取到状态count 值为: 1
注:
useState
的初始值(参数)只会在组件第一次渲染时生效。以后的每次渲染,useState
获取到的都是最新的状态值, React 组件会记住每次最新的状态值。7.2.4 使用规则
useState
函数可以执行多次,每次执行互相独立,每调用一次为函数组件提供一个状态。function List() { // 以字符串为初始值 const [name,setName] = useState('cp00') // 以数组为初始值 const [list,setList] = useState([]) }
useState
注意事项
只能出现在函数组件中。
不能嵌套在 if/for/其他函数中 (react 按照 hooks 的调用顺序标识每一个 hook)
let num = 1 function List() { num++ if (num/2 === 0) { const [name,setName] = useState('cp00') } const [list,setList] = useState([]) } // 两个hook顺序不固定,不可行!!
可以通过开发者工具查看 hooks 状态。
7.3 useEffect
7.3.1 函数副作用
什么是副作用
副作用是相对于主作用来说的,一个函数除了主作用,其他的作用就是副作用。对于 React 组件来说,主作用是根据数据 (state/props) 渲染 UI, 除此之外都是副作用(比如,手动修改DOM)
常见的副作用
数据请求 ajax 发送。
手动修改DOM
localstorage 操作
useEffect 函数的作用就是为了 react 函数组件提供副作用处理的!
7.3.2 基础使用
7.3.3 依赖项控制执行时机
1.不添加依赖项
组件首次渲染执行一次,以及不管是那一个状态更改引起组件更新时都会重新执行。
- 组件初始渲染
- 组件更新(不管是哪个状态引起的更新)
useEffect(() => { console.log('副作用执行了') })
2.添加空数组
组价只在首次渲染时执行一次(再次更新不执行)
useEffect(() => { console.log('副作用执行了') },[])
3.添加特定依赖项
副作用函数在首次渲染时执行,在依赖项发生变化时重新执行。
function App() { const [count,setCount] = useState(0) const [name,setName] = useState('zs') useEffect(() => { console.log('副作用执行了') // useEffect回调函数中用到的数据状态应该出现在依赖项数组声明中,避免产生问题(数据更新了,内容没有重新渲染) document.title = count console.log(name) },[count,name]) }
7.4 自定义hook
需求:自定义一个hook函数,实现获取滚动距离Y
const[y]=useWindowScroll() y即滚动到顶部的距离
import { useState } from 'react' export function useWindowScroll () { const [y, sety] = useState(0) // 在滚动行为发生的时候,不断获取滚动值,然后交给y window.addEventListener('scroll',() => { const h = document.documentElement.scrollTop sety(h) }) return [y] } import { useWindowScroll } from './hooks/useWindowScroll' function App () { const [y] = useWindowScroll() return ( <div style = {{height:'12000px'}}> {y} </div> ) } export default App
需求: 自定义hook函数,可以自动同步到本地LocalStorage
const [message,setMessage] = useLocalStorage(defaultValue)
- message 可以通过自定义传入默认初始值
- 每次修改message数据的时候,都会自动往本地同步一份
import { useState,useEffect } from 'react' export function useLocalStorage(key,defaultValue) { const [message,setMessage] = useState(defaultValue) // 每次只要message变化,就会自动同步到本地LocalStorage // 副作用本地存储 useEffect(() => { window.localStorage.setItem(key,message) },[message,key]) return [message,setMessage] } import { useLocalStorage } from './hooks/useLocalStorage' function App () { const [message,setMessage] = useLocalStorage('hook-key','阿飞') setTimeout(() => { setMessage('cp') }, 5000) return ( <div style = {{height:'12000px'}}> {y} </div> ) } export default App
8 Hooks 进阶
8.1 useState-回调函数的参数
(useState 回调函数作为参数的使用场景)
实质上就是通过函数获取初始值state!
使用场景
参数只会在数组的初始渲染中起作用,后续渲染时会被忽略。如果初始state需要通过计算才能获得,则可以传入一个函数,在函数中计算并返回初始的state,此函数只在初始渲染时被调用
语法
const [name,setName] = useState(() => { // 初始state需要计算获得 // 编写计算逻辑 })
语法规则
- 回调函数 return 出去的值将作为
name
的初始值。- 回调函数中的逻辑只会在组件初始化的时候执行一次。
语法选择
- 如果就是初始化一个普通的数据,直接使用useState(普通数据)即可
- 如果初始化的数据无法直接得到需要通过计算才能获取到,使用
useState(() => {})
需求(练手)
import { useState } from 'react' function getDefaultValue () { for (let i = 0;i < 1000; i++) { } return '10' } function Counter (props) { const [count, setCount] = useState(() => { // 只要无法直接确定,需要通过一定的操作才能获得,就可以理解为计算 //return props.count return getDefaultValue() }) return ( <button onClick = {() => setCount(count + 1)}>{count}</button> ) } function App () { return ( <div> <Counter count={10} /> <Counter count={10} /> </div> ) }
8.2 useEffect 清理副作用
(清理useEffect 方法)
使用场景
在组件被销毁时,如果有些副作用操作需要被清理,就可以使用此语法,比如常见的定时器。
语法及规则
useEffect(() => { console.log('副作用函数执行了') // 副作用函数的执行时机为:在下一次副作用函数执行之前执行 return () => { console.log('清理副作用的函数执行了') // 在这里写清理副作用的代码 } }) // eg function Test () { useEffect(() => { let timer = setInterval(() => { console.log('定时器执行了') },1000) return () => { clearInterval(timer) } },[]) return ( <div>this is test component! ) }
8.3 useEffect 发送网络请求
类组件发送网络请求:componentDidMount(组件挂载完成,即初始化时dom渲染完毕,只执行一次)。类似于hook中的useEffect (fun,[])
一个组件内部可以有多个useEffect,它们之间相互独立互不影响。
使用场景
如何在 useEffect 中发送网络请求,并且封装同步 async await 操作
语法要求
不可以直接在useEffect 的回调函数外层直接包裹 await ,因为 异步会导致清理函数无法立即返回。
// 错误示例 useEffect(async () => { const res = await axios.get('http://geek.itheima....') console.log(res) },[])
正确写法
在内部单独定义一个函数,然后把这个函数包装成同步
useEffect(() => { async function fetchData() { const res = await axios.get('http://geek.itheima.net/v1_0/channels') console.log(res) } fetchData() },[])
8.4 useRef
使用场景
在函数组件中获取真实的 dom 元素对象或者是组件对象。
使用步骤
- 导入
useRef
函数。- 执行
useRef
函数并传入null,返回值为一个对象,对象内部有一个 current 属性存放拿到的 dom 对象(组件实例)- 通过 ref 绑定要获取的元素或者组件。
获取Dom
import {useEffect, useRef} from 'react' function App () { const h1Ref = useRef(null) useEffect(() => { console.log(h1Ref) },[]) return ( <div> <h1 ref={h1Ref}>thi is h1</h1> </div> ) } export default App
获取组件实例
函数组件由于没有实例,不能使用 ref 获取,如果想获取组件实例,必须是类组件。
8.5 useContext
补充
Context 如果要传递数据,只需要在整个应用初始化的时候传递一次就可以,就可以选择在index.js 文件中做数据提供。(静态的)
如果 Context 需要传递数据并且将来还需要在对数据做修改,底层组件也需要数据同步改变,可以选择在app.js中做数据提供。(动态的)
实现步骤
- 使用
createContext
创建 Context 对象- 在顶层组件通过
Provider
提供数据(可以是App.js,也可以是index.js)- 在底层组件通过
useContext(Context)
函数获取数据代码实现
import { createContext, useContext } from 'react' // 创建Context对象 const Context = createContext() function Foo() { return <div>Foo<Bar/><div> } function Bar() { // 底层组件通过useContext函数获取数据 const name = useContext(Context) return <div>Bar {name}</div> } function App() { return { // 顶层组件通过 Provider 提供数据 // value 数据更新,则使用value值的其他组件数据也会更新 <Context.Provider value={'this is name'}> <div> <Foo/> </div> </Context.Provider> } } export default App
补充
// 1.调用createContext 方法 // 2. 通过顶层组件包裹一下,Context.Provider // 3.底层组件 useContext(createContext 返回的对象) import { createContext } from 'react' const Context = createContext() export default Context