这是我参与「第五届青训营」伴学笔记创作活动的第 15 天
1.1 起步及JSX介绍
React是用于构建界面的JavaScript库,优点:
- 组件化开发,提升效率
- 虚拟DOM + 优秀的DIFF算法,减少与真实DOM的交互
- 可以使用React Native开发移动端
第一个app
-
引入React ReactDOM 和 Babel
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script> <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script> -
创建容器,虚拟DOM并挂载
<body> <!-- 挂载的容器 --> <div id="app"></div> <!-- 此处一定要写babel --> <script type="text/babel"> // 创建虚拟DOM const VirtualDOM = <h1>hello, react</h1> // 渲染虚拟DOM到页面 ReactDOM.render(VirtualDOM, document.getElementById('app')) </script> </body>
关于虚拟DOM
- 本质上是个object类型的对象
- 虚拟DOM的属性比真实DOM少,更轻量
- 虚拟DOM最终会被React转化为真实DOM
JSX语法规则
- 定义虚拟DOM时,不要加引号
- 标签中混入JS表达式,要加花括号;JSX中不能引入语句,只能有表达式
- 引用样式时类名用className
- 写行内样式要用双花括号,第一个花括号代表JS表达式,第二个花括号代表对象
- 不能有多个根标签,标签必须闭合
- 标签首字母小写,则转换为html元素;若标签首字母大写,则转换为对应组件
<script type="text/babel">
const id = "20221260148"
const name = 'guo'
const data = ['angular','react','vue']
// 创建虚拟DOM
const VirtualDOM = (
<div>
<h1 style={{color: 'red', fontSize: '16px'}} className="student" id={id}>hello, {name}</h1>
<input type="text"/>
<ul>
{data.map((item, index) => <li id={index}>{item}</li>)}
</ul>
</div>
)
// 渲染虚拟DOM到页面
ReactDOM.render(VirtualDOM, document.getElementById('app'))
</script>
2.2 组件三大属性
state
state是组件对象最重要的属性, 值是一个对象,通过更新组件的state来更新对应的页面显示
<script type="text/babel">
class Demo extends React.Component {
//初始化状态用赋值语句,不将其放在原型对象上,而是作为私有属性放在实例本身
state = {
isHot: true
}
render() {
const {isHot} = this.state
return <h1 onClick={this.change}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>
}
//自定义方法,用箭头函数(箭头函数没有自己的this,自动找外层的this。类中this正好指向实例对象)
change = () => {
const {isHot} = this.state
this.setState({isHot: !isHot})
}
}
ReactDOM.render(<Demo/>, document.getElementById('app'))
</script>
注意事项
-
组件render方法中的this为组件实例对象
-
组件自定义的方法中this为undefined,如何解决?
- 强制绑定this: 通过函数对象的bind()
- 箭头函数
-
状态数据,不能直接修改或更新,要用setState()
props
作用:通过标签属性从组件外向组件内传递变化的数据
注意:组件内部不要修改props数据
类组件props传值
<script type="text/babel">
//创建组件
class Person extends React.Component {
render() {
const {name, sex, age} = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
//单个传值
ReactDOM.render(<Person name="小明" sex="男" age="19"/>, document.getElementById('app1'))
//批量传值
const data = {name: "张三", sex: "男", age: 18}
ReactDOM.render(<Person {...data}/>, document.getElementById('app2'))
</script>
函数组件props传值
<script type="text/babel">
//创建组件
function Person(props) {
const {name, sex, age} = props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
ReactDOM.render(<Person name="小明" sex="男" age="19"/>, document.getElementById('app1'))
</script>
refs
refs可以获取到元素真实DOM节点
字符串形式ref(不推荐)
class Demo extends React.Component {
showData = () => {
alert(this.refs.curNode.value)
}
render() {
return (
<div>
<input ref="curNode" type="text"/>
<button onClick={this.showData}>点我提示数据</button>
</div>
)
}
}
回调函数ref
class Demo extends React.Component {
showData = () => {
alert(this.curNode.value)
}
render() {
return (
<div>
<input ref={curNode => this.curNode = curNode} type="text"/>
<button onClick={this.showData}>点我提示数据</button>
</div>
)
}
}
createRef
class Demo extends React.Component {
inputRef = React.createRef()
showData = () => {
alert(this.inputRef.current.value)
}
render() {
return (
<div>
<input ref={this.inputRef} type="text"/>
<button onClick={this.showData}>点我提示数据</button>
</div>
)
}
}
2.3 生命周期与diff算法
组件生命周期:组件从创建到死亡它会经历一些特定的阶段
React组件中包含一系列钩子函数(生命周期回调函数), 会在特定的时刻调用
我们在定义组件时,会在特定的生命周期回调函数中,做特定的工作
生命周期的三个阶段
1. 初始化阶段: 由ReactDOM.render()触发---初次渲染
- constructor()
- componentWillMount()
- render()
- componentDidMount()
2. 更新阶段: 由组件内部this.setSate()或父组件重新render触发
- shouldComponentUpdate()
- componentWillUpdate()
- render()
- componentDidUpdate()
3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发
- componentWillUnmount()
三个重要的钩子函数
- render() :初始化渲染或更新渲染调用
- componentDidMount() :开启定时器,发送ajax请求登
- componentWillUnmount() :做一些收尾工作, 如:清理定时器,取消事件监听等
diff算法
当状态中数据发生变化时,React会根据新数据生成新的虚拟DOM,随后将新的虚拟DOM与旧的虚拟DOM进行diff算法比较
-
当旧虚拟DOM中找到与新虚拟DOM相同的key:
逐层对比新旧虚拟DOM的内容,若内容没变,直接用之前的真实DOM;若内容改变,生成新的真实DOM并替换掉旧的真实DOM
-
当旧虚拟DOM中没有找到与新虚拟DOM相同的key:直接创建新的真实DOM渲染到页面
用遍历的index作为key可能引发的问题
- 对数据进行改变顺序的操作时,会产生没有必要的DOM更新,效率低
- 如果结构中包含输入类的DOM,会发生渲染错误
开发中最好选择每条数据的唯一标识作为key
2.5 React Hooks
React Hooks可以让函数式组件也可以使用state及其他特性
Hooks可以反复多次使用,并且每个Hook相互独立
使用Hook的两条规则
- 只在函数最顶层使用Hook(不在循环或条件语句中使用Hook)
- 只在React函数组件中使用Hook(不在普通的JavaScript函数中使用Hook)
useState
useState() 传入一个初始值,返回一个包含数据和修改数据的方法
import React, { useState } from "react";
export default function Demo() {
const [count, setCount] = useState(0);
return (
<div>
<p>当前count {count}</p>
<button onClick={() => setCount(count + 1)}>点击+1</button>
</div>
);
}
setCount 可以有两种写法。一种是直接传递一个新的值,另一种是传递一个函数,现在的值将以参数的形式传给该函数,函数的返回值用于更新状态
<button onClick={() => setCount((count) => count + 1)}>点击+1</button>
useEffect
基本使用
useEffect 可以让你在函数组件中执行副作用操作(不知道执行的操作会发生什么,例如网络请求等等),用于替代类组件中的生命周期函数
import React, { useState, useEffect } from "react";
export default function EffectHook() {
const [count, setCount] = useState(0);
useEffect(() => {
//更新网页的标题为count次数
document.title = `count = ${count}`;
});
return (
<div>
<button onClick={() => setCount(count + 1)}>点击+1</button>
</div>
);
}
react在首次渲染和之后的每次渲染都会调用一遍传给 useEffect 的函数,而类组件中需要用两个生命周期函数来分别表示首次渲染(componentDidMount),和之后的更新渲染(componentDidUpdate)
useEffect 中定义的副作用函数的执行不会阻碍浏览器更新视图,也就是说这些函数是异步执行的,而类组件的生命周期函数则是同步执行的
react提供了同步执行的副作用钩子 useLayoutEffect ,使用方法和 useEffect 是一样的
解绑副作用
这种场景很常见,当我们在componentDidMount里添加了一个注册,我们得马上在 componentWillUnmount 中,也就是组件被注销之前清除掉我们添加的注册,否则内存泄漏的问题就出现了
在 useEffect 中,我们只需要让副作用函数 return 一个新的函数即可,这个新的函数将会在组件下一次重新渲染之后执行
要注意不同点,componentWillUnmount 只会在组件被销毁前执行一次而已,而 useEffect 里的函数,每次组件渲染后都会执行一遍,包括副作用函数返回的这个清理函数也会重新执行一遍
useEffect的第二个参数
之前说到每次重新渲染都要执行一遍这些副作用函数,显然是不经济的。怎么跳过一些不必要的计算呢?只需要给 useEffect 传第二个参数即可
用第二个参数来告诉react只有当这个参数的值发生改变时,才执行我们传的副作用函数
useEffect(() => {
document.title = `count = ${count}`;
}, [count]); // 只有当count的值发生变化时,才会重新执行`document.title`这一句
第二个参数可以是
- 不传值(在首次渲染和每次更新渲染时都会重新执行,相当于
componentDidMount,componentDidUpdate和componentWillUnmount的结合) - 空数组(只在首次渲染的时候执行,相当于
componentDidMount和componentWillUnmount的结合) - 数组(只有当数组内的状态修改时才会重新执行)
useContext
在react中,父子组件通信一般直接用 props ,但是如果有多层组件的嵌套呢?连续地使用 props 传值就会很麻烦
在祖先组件与后代组件进行通信时,可以使用 context 对象
import React, { useState, createContext, useContext } from "react";
//第一步,创建context容器对象,放在祖先组件和后代组件都能共享的位置,可以有初始值
const MyContext = createContext();
export default function ContextHook() {
const [count, setCount] = useState(0);
return (
<div>
<h2>ContextHook</h2>
<p>祖先组件的count {count}</p>
<button onClick={() => setCount(count + 1)}>点击+1</button>
{/* 第二步,渲染子组件时用容器名.Provider包裹,通过value属性给后代传递数据 */}
<MyContext.Provider value={count}>
<Child />
</MyContext.Provider>
</div>
);
}
function Child() {
return (
<div>
<Grand />
</div>
);
}
//第三步,使用数据时用容器名.Consumer包裹
function Grand() {
return (
<div>
<MyContext.Consumer>
{(value) => <p>后代组件的count {value}</p>}
</MyContext.Consumer>
</div>
);
}
后代组件使用 useContext 改写,调用了 useContext 的组件总会在 context 值变化时重新渲染
//使用useContext钩子
function Grand() {
const count = useContext(MyContext)
return (
<div>
<p>后代组件的count {count}</p>
</div>
)
}
useRef
在类组件中,使用 ref 有三种方式,推荐使用 createRef 的写法创建一个容器
class Demo extends React.Component {
inputRef = React.createRef()
showData = () => {
alert(this.inputRef.current.value)
}
render() {
return (
<div>
<input ref={this.inputRef} type="text"/>
<button onClick={this.showData}>点我提示数据</button>
</div>
)
}
}
在函数式组件中,可以使用 useRef 这个钩子完成同样的事
import React, { useRef } from "react";
export default function Demo() {
const inputRef = useRef();
return (
<div>
<h2>RefHook</h2>
输入数据:
<input type="text" ref={inputRef} />
<button onClick={() => alert(inputRef.current.value)}>
点击提示数据
</button>
</div>
);
}
useReducer
useState 的进阶版,可以将 state 与其相关操作定义在 reducer 里,思想参考 redux
import React, { useReducer } from "react";
export default function Demo() {
// 返回值1:状态值
// 返回值2:修改状态的派发器,具体的修改行为由reducer函数执行
// 参数1:reducer是一个整合函数,该函数的返回值将会作为新的状态值
// 参数2:状态初始值
const [count, countDispatch] = useReducer((state, action) => {
// reducer函数有两个参数,第一个参数为当前state,第二个参数为action对象
switch (action.type) {
case "add":
return state + action.payload;
case "sub":
return state - action.payload;
default:
break;
}
}, 0);
const add = () => {
countDispatch({ type: "add", payload: 1 });
};
const sub = () => {
countDispatch({ type: "sub", payload: 1 });
};
return (
<div>
<h2>ReducerHook</h2>
<p>当前count {count}</p>
<button onClick={add}>点击+1</button>
<button onClick={sub}>点击-1</button>
</div>
);
}
useMemo
react 组件发生重新渲染的情况
- 当组件自身的 state 发生变化
- 当组件的父组件重新渲染,子组件也会重新渲染
通过 React.memo(子组件) 方法,只有当子组件的 props 发生改变时,子组件才会重新渲染
通过 useMemo 钩子,可以实现数据的监听和缓存,只有当数据的依赖项修改时,才会重新计算
import React, { useMemo, useState } from "react";
export default function Demo() {
const [countA, setCountA] = useState(0);
const [countB, setCountB] = useState(0);
// useMemo传入两个参数,第一个参数为计算的函数,第二个参数为依赖项
const countC = useMemo(() => countA + countB, [countA, countB]);
return (
<div>
<h2>MemoHook</h2>
<p>当前countA {countA}</p>
<p>当前countB {countB}</p>
<button onClick={() => setCountA(countA + 1)}>点击countA+1</button>
<button onClick={() => setCountB(countB + 1)}>点击countB+1</button>
<p>当前countC {countC}</p>
</div>
);
}
useCallback
useCallback 和 useMemo 两者接收的参数是一样的,第一个参数表示一个回调函数,第二个表示依赖的数据
在依赖数据发生变化的时候,才会调用传进去的回调函数去重新计算结果,起到一个缓存的作用
不同点:useMemo 缓存的结果是回调函数中return回来的值,useCallback 缓存的结果是函数
当组件的函数由 useCallback 返回时,只有依赖项发生改变才会重新渲染此函数
useCallback 应该和 React.memo 配套使用
自定义Hook
自己创建的可以调用其他钩子函数的Hook,命名规则:useXxx
在 src 下创建 hooks 文件夹存储自定义钩子,并暴露该函数
需要将一定数据或方法作为返回值,供调用时接收