- 一、开发依赖
- 二、jsx语法
- 三、jsx语法的本质
- 四、虚拟dom
- 五、React组件
- 六、react-redux
- 七、react-router
- 12.1 前端路由
- 12.1 URL的hash
- 12.2 HTML5的history
- Link 与 a标签的区别
- 对比,Link组件避免了不必要的重渲染
- react-router:只更新变化的部分从而减少DOM性能消耗">react-router:只更新变化的部分从而减少DOM性能消耗
- 12.2 react-router实现原理
- 12.3 补充:手动路由跳转问题
- 12.4 react-router-config
- 12.1 前端路由
- 八、Hooks
- 九、fiber
- React面经:
- 【React深入】React事件机制
新手学习 react 迷惑的点(完整版): https://mp.weixin.qq.com/s/VoZpdK8DO6LaQ9XgP1_RGg
一、开发依赖
- react:包含react所必须的核心代码
- react-dom:react渲染在不同平台所需要的核心代码
- babel:将jsx转换成React代码的工具
1.react
在React的0.14版本之前是没有react-dom这个概念的,所有功能都包含在react里。 为什么要进行拆分呢?原因就是react-native。react包中包含了react和react-native所共同拥有的核心代码。
2.react-dom
react-dom针对web和native所完成的事情不同:
web端:react-dom会讲jsx最终渲染成真实的DOM,显示在浏览器中
native端:react-dom会讲jsx最终渲染成原生的控件(比如Android中的Button,iOS中的UIButton)。
3.Babel
当下很多浏览器并不支持ES6的语法,但是确实ES6的语法非常的简洁和方便,我们开发时希望使用它。
那么编写源码时我们就可以使用ES6来编写,之后通过Babel工具,将ES6转成大多数浏览器都支持的ES5的语法。
React和Babel的关系:
默认情况下开发React其实可以不使用babel。
但是前提是我们自己使用 React.createElement
来编写源代码,它编写的代码非常的繁琐和可读性差。
那么我们就可以直接编写jsx(JavaScript XML)
的语法,并且让babel帮助我们转换成React.createElement
。
小案例
在页面渲染一个hello react,并通过点击按钮改变渲染内容。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<!-- 1.添加React依赖 -->
<!-- crossorigin 拿到脚本的错误信息 -->
<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>
<!-- 2.注意事项:适用jsx,并且希望在script中解析jsx代码,必须在script标签中添加一个属性 type="text/babel" -->
<script type="text/babel">
let message = "hello react"
<!-- 3.点击改变文本 -->
function btnClick() {
message = '你好,react'
}
// ReactDOM.render(渲染内容, 挂载对象)
ReactDOM.render(
<div>
<h2>{message}</h2>
<button onClick={btnClick}>改变文本</button>
</div>
, document.getElementById("app"))
</script>
</body>
</html>
此时点击按钮,发现页面内容并没有发生改变,但是通过打印可以看到message的值发生了改变
这是因为react需要在值发生改变之后,手动调用render函数,才能渲染dom。
修改代码:
<script type="text/babel">
let message = "hello react"
function btnClick() {
message = '你好,react'
render()
}
// ReactDOM.render(渲染内容, 挂载对象)
function render() {
ReactDOM.render(
<div>
<h2>{message}</h2>
<button onClick={btnClick}>改变文本</button>
</div>
, document.getElementById("app"))
}
render()
</script>
1.组件化思想实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<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>
<script type="text/babel">
// 封装App组件
class App extends React.Component {
render () {
return (
<div>
<h2>hello react</h2>
<button>改变文本</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById("app"))
</script>
</body>
</html>
将需要显示的内容封装在属性中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<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>
<script type="text/babel">
// 封装App组件
class App extends React.Component {
constructor () {
super() //初始化父类实例
// this.message = "hello react" // 这种写法界面不会根据数据的变化渲染
// 要显示的数据绑定在 state 中
this.state = {
message: "hello react"
}
}
render () {
return (
<div>
<h2>{this.state.message}</h2>
<button onClick={this.btnCLick.bind(this)}>改变文本</button>
</div>
)
}
btnCLick(){
console.log(this) // undefined 通过 bind 可以给绑定 this是 App
// this.message = '你好,react'
// 修改数据
this.setState({
message: '你好,react'
})
}
}
ReactDOM.render(<App />, document.getElementById("app"))
</script>
</body>
</html>
1.1数据定义在哪里?
以通过在构造函数中 this.state = {定义的数据}
,当我们的数据发生变化时,我们可以调用this.setState
来更新数据,setState通知React进行update操作 ,在进行update操作时,就会重新调用render函数,并且使用最新的数据,来渲染界面。
1.2为什么在React Component需要bind绑定事件
https://zhuanlan.zhihu.com/p/54962688
https://www.zhihu.com/question/337893251
React class 组件中,事件的 handle 方法其实就相当于回调函数传参方式赋值给了 callback,在执行 click 事件时 类似 element.addEventListener('click', callback, false )
, handle 失去了隐式绑定的上下文,this 的值为 undefined。
为什么是undefined?
我们 来看一个例子
"use strict"
let obj = {
display: function() {
console.log(this)
}
};
function handleClick(callback) {
callback()
}
handleClick(obj.display) // undefined
obj.display
以回调函数传参方式赋值给了callback
,就会失去上下文。所以严格模式下是undefined。
在类中当调用静态或原型方法时没有指定 this 的值,那么方法内的 this 值将被置为 **undefined**
。即使你未设置 “use strict” ,因为 **class**
体内部的代码总是在严格模式下执行。
也就是说我们只是把这个函数传给了button组件,并没有把函数的上下文传给button,button组件并没有收到这个上下文(即组件实例),当然也就不知道该把handleClick函数里的this设置为你期望的组件实例了。
react-dom 中 合成事件的处理源码:
// react-dom/src/events/EventListener.js
export function addEventBubbleListener(
element: Document | Element,
eventType: string,
listener: Function,
): void {
element.addEventListener(eventType, listener, false);
}
// 调用处
addEventBubbleListener(element, getRawEventName(topLevelType)
我们在绑定的函数中,可能想要使用当前对象,比如执行 this.setState 函数,就必须拿到当前对象的this 。
我们就需要在传入函数时,给这个函数直接绑定this
类似于下面的写法:
<button onClick={this.btnCLick.bind(this)}>改变文本</button>
频繁使用bind并不好,因为bind每次都会创建一个新的函数,建议在constructor中绑定。也可以通过属性初始化或者箭头函数的方法解决这个问题。
http://react.html.cn/docs/handling-events.html
https://www.zhihu.com/question/68092028
二、jsx语法
1.通过两种方式进行列表渲染
在React中并没有像Vue模块语法中的v-for指令,而且需要我们通过JavaScript代码的方式组织数据,转成JSX,React中的JSX正是因为和JavaScript无缝的衔接,让它可以更加的灵活;在React中,展示列表最多的方式就是使用数组的map高阶函数;很多时候我们在展示一个数组中的数据之前,需要先对它进行一些处理: 比如过滤掉一些内容:filter函数 ,比如截取数组中的一部分内容:slice函数
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<title>Page Title</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel='stylesheet' type='text/css' media='screen' href='main.css'>
<script src='main.js'></script>
</head>
<body>
<div class="app"></div>
<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>
<script type="text/babel">
class App extends React.Component{
constructor() {
super()
this.state = {
movies : ['星际穿越', '大话西游', '盗梦空间', '少年派']
}
}
render() {
// 第一种
const liArray = []
for(let movie of this.state.movies) {
liArray.push(<li>{movie}</li>)
}
return (
<div>
<h2>电影系列1</h2>
<ul>
{liArray}
</ul>
// 第二种
<h2>电影系列2</h2>
<ul>
{
this.state.movies.map((item) => {
return <li>{item}</li>
})
}
</ul>
</div>
)
}
}
</script>
</body>
</html>
2.JSX嵌入表达式
- 运算表达式
- 三元运算符
- 执行一个函数
<script type="text/babel">
class App extends React.Component{
constructor() {
super()
this.state = {
firstname: 'kobe',
lastname: 'bryant',
isLogin: true
}
}
render() {
const {firstname, lastname, isLogin} = this.state
return (
<div>
{/*1.运算符表达式*/}
<h2>{firstname + "" + lastname}</h2>
{/*2.三元表达式*/}
<h2>{isLogin ? " 欢迎回来~" : '请先登录~'}</h2>
{/*3.进行函数调用*/}
<h2>{this.getFullName()}</h2>
</div>
)
}
getFullName() {
console.log(this)
return this.state.firstname + " " + this.state.lastname
}
}
ReactDOM.render(<App />, document.getElementById("app"))
</script>
3.jsx绑定属性
<script type="text/babel">
// 格式化请求图片的大小
function getSizeImage(imgUrl, size) {
return imgUrl + `?param=${size}x${size}`
}
class App extends React.Component{
constructor() {
super()
this.state = {
title: '标题',
imgUrl: "http://p2.music.126.net/XlCoeRkQNmvcan4_2kXXmA==/109951165303231927.jpg", // 网络请求回来将数据绑定到这里
link: "www.baidu.com",
active: true
}
}
render() {
const {title, imgUrl, link, active} = this.state
return (
<div>
{/*title绑定*/}
<h2 title={title}>属性绑定</h2>
{/*src绑定*/}
<img src={imgUrl} alt="" />
<img src={getSizeImage(imgUrl, 140)} alt="" />
{/*href绑定*/}
<a href={link} target="_blank"></a>
{/*class绑定* class 属于es6 关键字,不能用于属性的绑定,需要用className */}
<div className="box title">div元素</div>
{/*动态class绑定* 当active为true时增加activeClass */}
<div className={"box title " + (active ? "activeClass" : " ")} >div元素</div>
<label htmlFor=""></label>
{/*style绑定 驼峰 */}
<div style={{color: "red", fontSize: "20px"}}>绑定style属性</div>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById("app"))
</script>
4.jsx绑定事件
- 使用bind 显示绑定定义函数时,
- 使用箭头函数
- 直接传入一个箭头函数,在箭头函数中调用需要执行的函数, 箭头函数没有this,此时this会向上层找,找到render函数的this,就是组件的this
<script type="text/babel">
class App extends React.Component{
constructor() {
super()
this.state = {
message: '你好啊'
}
this.btnClick = this.btnClick.bind(this)
}
render() {
return (
<div>
{/* 1.方案一: 使用bind 显示绑定*/}
<button onClick={this.btnClick.bind(this)}>点击1</button>
<button onClick={this.btnClick}>点击2</button>
{/* 2.方案二: 定义函数时,使用箭头函数*/}
<button onClick={this.increament}>+1</button>
{/* 3.方案三(推荐): 直接传入一个箭头函数,在箭头函数中调用需要执行的函数, 箭头函数没有this,此时this会向上层找,找到render函数的this,就是组件的this*/}
<button onClick={ () => {this.decrement()}}>-1</button>
</div>
)
}
btnClick(e) {
console.log(e) // 此时的event是默认传递的
// 当按钮发生点击的时候,react内部会将this绑定为undefined,必须通过bind绑定this
console.log(this)
console.log('按钮发生了点击')
}
increament = () => {
console.log('加一')
}
decrement() {
console.log(this)
}
}
ReactDOM.render(<App />, document.getElementById("app"))
</script>
为什么vue组件中的方法使用箭头函数绑定的时候this是undefined,而react中的箭头函数this会指向组件实例?
vue中,箭头函数的this是绑定到当前上下文,也就是父级函数运行时的this的,而我们的组件定义根本没父级函数。他的this指向全局对象,在严格模式下,全局对象就是undfined。
是因为写法的不一样,react的组件定义只是类的声明,创建实例后才会运行,而创建组件实例时,会初始化this,这时候this自然指向组件对象。
而vue的组件定义是methods对象式的写法,对象式的定义方式下methods绑定到了全局对象,在定义的过程中箭头函数就已经绑定到了当前上下文,而这时候组件还没创建,这时候this就是undefined。
所以,react组件的定义时方法可以使用箭头函数,而vue的组件定义时methods不可以使用箭头函数。
5.条件渲染
- 通过逻辑判断
- 三元运算符
- 逻辑与: 一个条件不成立后面的条件都不会进行判断
- 对于需要频繁切换显示隐藏的,可以通过display属性隐藏显示
<script type="text/babel">
class App extends React.Component{
constructor() {
super()
this.state = {
isLogin: true
}
}
render() {
// 方案1.通过逻辑判断
const {isLogin} = this.state
let message = null
if(isLogin) {
message = "欢迎回来"
}else {
message = "请先登录"
}
return (
<div>
<h2>{message}</h2>
{/* 方案2.三元运算符*/}
<button onClick={ e => this.loginClick()}>{isLogin ? "退出" : '登录'}</button>
<hr />
<h2>{isLogin ? "管理员你好" : null}</h2>
{/* 方案3.逻辑与: 一个条件不成立后面的条件都不会进行判断*/}
<h2>{isLogin && "管理员你好"}</h2>
{/* 方案4. 对于需要频繁切换显示隐藏的,可以通过display属性隐藏显示*/}
<h2 style={{display: (isLogin ? "block" : "none")}}>通过display属性隐藏显示</h2>
</div>
)
}
loginClick(){
this.setState({
isLogin: !this.state.isLogin
})
}
}
ReactDOM.render(<App />, document.getElementById("app"))
</script>
6.列表渲染
使用map、filter(过滤)、slice(截取)等高阶函数
<script type="text/babel">
class App extends React.Component{
constructor() {
super()
this.state = {
movies : ['星际穿越', '大话西游', '盗梦空间', '少年派'],
nums: [200, 234, 111, 45, 60, 67, 80, 90]
}
}
render() {
const liArray = []
for(let movie of this.state.movies) {
liArray.push(<li>{movie}</li>)
}
return (
<div>
<h2>电影系列1</h2>
<ul>
{liArray}
</ul>
<h2>电影系列2</h2>
<ul>
{
this.state.movies.map((item) => {
return <li>{item}</li>
})
}
</ul>
<ul>
{
this.state.nums.filter(item => {
return item > 100
}).map(item => {
return <li>{item}</li>
})
}
</ul>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById("app"))
</script>
三、jsx语法的本质
https://zh-hans.reactjs.org/docs/jsx-in-depth.html
jsx 仅仅只是 React.createElement(component, props, ...children)
函数的语法糖。
所有的jsx最终都会被转换成React.createElement
的函数调用。
<div className="test" title={'测试组件'}>
组件的生命周期过程
<h2>{this.state.count}</h2>
<button onClick={e => this.increament()}>+</button>
<hr />
<button onClick={ e => this.changeState()}>切换Cpn显示隐藏</button>
{this.state.isShow && <Cpn />}
</div>
上面的写法等价与下面的写法:
React.createElement("div", {
className: "test",
title: '测试组件'
}, React.createElement("h2", null, (void 0).state.count), React.createElement("button", {
onClick: function onClick(e) {
return _this.increament();
}
}, "+"),React.createElement("hr", null), React.createElement("button", {
onClick: function onClick(e) {
return _this.changeState();
}
}, "\u5207\u6362Cpn\u663E\u793A\u9690\u85CF"), (void 0).state.isShow && React.createElement(Cpn, null));
jsx语法是通过babel语法转换成了React.createElement
函数调用。
通过 jsx 语法代码简洁清晰,可读性强。不用像vue 那样引入新的概念
源码:
/**
101. React的创建元素方法
*/
export function createElement(type, config, children) {
// propName 变量用于储存后面需要用到的元素属性
let propName;
// props 变量用于储存元素属性的键值对集合
const props = {};
// key、ref、self、source 均为 React 元素的属性,此处不必深究
let key = null;
let ref = null;
let self = null;
let source = null;
// config 对象中存储的是元素的属性
if (config != null) {
// 进来之后做的第一件事,是依次对 ref、key、self 和 source 属性赋值
if (hasValidRef(config)) {
ref = config.ref;
}
// 此处将 key 值字符串化
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 接着就是要把 config 里面的属性都一个一个挪到 props 这个之前声明好的对象里面
for (propName in config) {
if (
// 筛选出可以提进 props 对象里的属性
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// childrenLength 指的是当前元素的子元素的个数,减去的 2 是 type 和 config 两个参数占用的长度
const childrenLength = arguments.length - 2;
// 如果抛去type和config,就只剩下一个参数,一般意味着文本节点出现了
if (childrenLength === 1) {
// 直接把这个参数的值赋给props.children
props.children = children;
// 处理嵌套多个子元素的情况
} else if (childrenLength > 1) {
// 声明一个子元素数组
const childArray = Array(childrenLength);
// 把子元素推进数组里
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
// 最后把这个数组赋值给props.children
props.children = childArray;
}
// 处理 defaultProps
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
上面的转换结果对应源码的 部分:
createElement需要传递三个参数:
- type:用于标识节点的类型。它可以是类似“h1”“div”这样的标准 HTML 标签字符串,也可以是 React 组件类型或 React fragment 类型。
- config:以对象形式传入,组件所有的属性都会以键值对的形式存储在 config 对象中。
- children:以对象形式传入,它记录的是组件标签之间嵌套的内容,也就是所谓的“子节点”“子元素”。
如果文字描述使你觉得抽象,下面这个调用示例可以帮你增进对概念的理解:
React.createElement("ul", {
// 传入属性键值对
className: "list"
// 从第三个入参开始往后,传入的参数都是 children
}, React.createElement("li", {
key: "1"
}, "1"), React.createElement("li", {
key: "2"
}, "2"));
这个调用对应的 DOM 结构如下:
<ul className="list">
<li key="1">1</li>
<li key="2">2</li>
</ul>
四、虚拟dom
组件在初始化时,会通过调用生命周期中的 render 方法,生成虚拟 DOM,然后再通过调用 ReactDOM.render 方法,实现虚拟 DOM 到真实 DOM 的转换。当组件更新时,会再次通过调用 render 方法生成新的虚拟 DOM,然后借助 diff(这是一个非常关键的算法,我将在“模块二:核心原理”重点讲解)定位出两次虚拟 DOM 的差异,从而针对发生变化的真实 DOM 作定向更新。
以上就是 React 框架核心算法的大致流程。
1.虚拟DOM创建过程
通过上面jsx语法的学习,我们知道jsx语法最终会通过babel语法转换成React.createElement
函数调用,并且通过 React.createElement
最终创建出来一个 ReactElement
对象:
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement
$$typeof: REACT_ELEMENT_TYPE,
// 内置属性赋值
type: type,
key: key,
ref: ref,
props: props,
// 记录创造该元素的组件
_owner: owner,
};
//
if (__DEV__) {
// 这里是一些针对 __DEV__ 环境下的处理,对于大家理解主要逻辑意义不大,此处我直接省略掉,以免混淆视听
}
return element;
};
`ReactElement`的作用是什么?React为什么要创建它呢?
原因是React利用ReactElement对象组成了一个JavaScript的对象树;
JavaScript的对象树就是大名鼎鼎的虚拟DOM(Virtual DOM);
我们可以将之前的jsx返回结果进行打印查看ReactElement结构;
<script type="text/babel">
class App extends React.Component{
constructor() {
super()
}
render() {
//1. React.createElement函数语法
let elObj = React.createElement("div", null, /*#__PURE__*/React.createElement("h2", {
className: "header"
}, "\u6807\u9898"), /*#__PURE__*/React.createElement("div", {
className: "content"
}, "\u5185\u5BB9\u90E8\u5206"), /*#__PURE__*/React.createElement("h2", {
className: "footer"
}, "\u9875\u811A"));
console.log('elObj:',elObj)
return elObj
//2. jsx语法 这两种方法都可以查看ReactElement对象树
// return (
// <div>
// <h2 className="header">标题</h2>
// <div className="content">内容部分</div>
// <h2 className="footer">页脚</h2>
// </div>
// )
}
}
ReactDOM.render(<App />, document.getElementById("app"))
</script>
可以看到跟上面返回的对象结构是相同的。
这个就是虚拟dom,那么这个虚拟dom是怎么转换成真实dom的呢?
就是通过下面这句代码:
ReactDOM.render(<App />, document.getElementById("app"))
通过调用render函数转换成了真实dom
ReactDOM.render(
// 需要渲染的元素(ReactElement)
element,
// 元素挂载的目标容器(一个真实DOM)
container,
// 回调函数,可选参数,可以用来处理渲染结束后的逻辑
[callback]
)
其中第二个参数就是一个真实的 DOM 节点,这个真实的 DOM 节点充当“容器”的角色,React 元素最终会被渲染到这个“容器”里面去。
至此我们了解到react的整体渲染流程就是:
jsx语法 -> createElement函数 -> ReactElement (对象树) -> ReactDOM.render -> 真实dom
2.为什么使用虚拟DOM?
为什么要采用虚拟DOM,而不是直接修改真实的DOM呢?
- 很难跟踪状态发生的改变:原有的开发模式,我们很难跟踪到状态发生的改变,不方便针对我们应用程序进行调试;
- 操作真实DOM性能较低:传统的开发模式会进行频繁的DOM操作,而这一的做法性能非常的低;
- DOM操作性能非常低:
首先,document.createElement本身创建出来的就是一个非常复杂的对象; https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createElement
其次,DOM操作会引起浏览器的回流和重绘,所以在开发中应该避免频繁的DOM操作; - 虚拟DOM帮助我们从命令式编程转到了声明式编程的模式
React官方的说法:Virtual DOM 是一种编程理念。 在这个理念中,UI以一种理想化或者说虚拟化的方式保存在内存中,并且它是一个相对简单的JavaScript对象 。我们可以通过ReactDOM.render让 虚拟DOM 和 真实DOM同步起来,这个过程中叫做协调(Reconciliation);
虚拟dom的初衷是跨平台
五、React组件
1.类组件
- 组件的名称是大写字符开头(无论类组件还是函数组件)
- 类组件需要继承自 React.Component
- 类组件必须实现render函数
- constructor是可选的,我们通常在constructor中初始化一些数据;
- this.state中维护的就是我们组件内部的数据;
export default class App extends React.Component {
constructor() {
super();
this.state = {
message: "你好啊",
};
}
render() {
return (
<div>
<h1>Hello react</h1>
<h2>{this.state.message}</h2>
</div>
);
}
}
2.函数式组件
函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容
函数式组件的特点:
- 没有this(组件实例);
- 没有内部状态state(所以有了 hooks)
- 也会被更新并挂载,但是没有生命周期函数;
function App() {
return (
<div>
<h1>函数式组件</h1>
{/* <h2>{this.state.message}</h2> */}
</div>
);
}
react 15 生命周期
Mounting 阶段:组件的初始化渲染(挂载)
componentWillMount、componentDidMount 方法同样只会在挂载阶段被调用一次。其中 componentWillMount 会在执行 render 方法前被触发,一些同学习惯在这个方法里做一些初始化的操作,但这些操作往往会伴随一些风险或者说不必要性。(componentWillMount 结束后,render 会迅速地被触发,所以说首次渲染依然会在数据返回之前执行。这样做不仅没有达到你预想的目的,还会导致服务端渲染场景下的冗余请求等额外问题,得不偿失”。除此之外,在 Fiber 带来的异步渲染机制下,componentWillMount 可能会导致非常严重的 Bug。)
接下来 render 方法被触发。注意 render 在执行过程中并不会去操作真实 DOM(也就是说不会渲染),它的职能是把需要渲染的内容返回出来。真实 DOM 的渲染工作,在挂载阶段是由 ReactDOM.render 来承接的。
componentDidMount 方法在渲染结束后被触发,此时因为真实 DOM 已经挂载到了页面上,我们可以在这个生命周期里执行真实 DOM 相关的操作。此外,类似于异步请求、数据初始化这样的操作也大可以放在这个生命周期来做(侧面印证了 componentWillMount 真的很鸡肋)。
Updating 阶段:组件的更新
组件的更新分为两种:一种是由父组件更新触发的更新;另一种是组件自身调用自己的 setState 触发的更新。
componentReceiveProps 并不是由 props 的变化触发的,而是由父组件的更新触发的,这个结论,请你谨记。
组件自身 setState 触发的更新 是从 shouldComponentUpdate 开始的。
React 组件会根据 shouldComponentUpdate 的返回值,来决定是否执行该方法之后的生命周期,进而决定是否对组件进行re-render(重渲染)。shouldComponentUpdate 的默认值为 true,也就是说“无条件 re-render”。在实际的开发中,我们往往通过手动往 shouldComponentUpdate 中填充判定逻辑,或者直接在项目中引入 PureComponent 等最佳实践,来实现“有条件的 re-render”。
Unmounting 阶段:组件的卸载
组件销毁的常见原因有以下两个。
- 组件在父组件中被移除了:这种情况相对比较直观,对应的就是我们上图描述的这个过程。
- 组件中设置了 key 属性,父组件在 render 的过程中,发现 key 值和上一次不一致,那么这个组件就会被干掉。
我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的;(后面我们可以通过hooks来模拟一些生命周期的回调)
react 16 生命周期
React 16 基于两个原因做出了生命周期的调整,其一:为同步渲染改异步渲染的 Fiber 铺路,把 有可能多次执行的 render 阶段中 componentWillMount/componentWillUpdate/componentWillRecevieProps 三个方法弃用;其二:为在一定程度上防止用户对生命周期的错用和滥用,把新增的 getDerivedStateFromProps 用 static 修饰,阻止用户在其内部使用 this 。
react16.3 Mounting 阶段:组件的初始化渲染(挂载)
废弃了 componentWillMount,新增了 getDerivedStateFromProps
废弃了 componentWillMount 的原因是?
新增了 getDerivedStateFromProps是试图替换掉 componentWillReceiveProps,因此它有且仅有一个用途:使用 props 来派生/更新 state。
一、getDerivedStateFromProps 是一个静态方法。静态方法不依赖组件实例而存在,因此你在这个方法内部是访问不到 this 的。
二、该方法可以接收两个参数:props 和 state,它们分别代表当前组件接收到的来自父组件的 props 和当前组件自身的 state。
三、getDerivedStateFromProps 需要一个对象格式的返回值。
3.1 constructor
constructor中通常只做两件事情:
- 通过给 this.state 赋值对象来初始化内部的state;
- 为事件绑定实例(this);
3.2 componentDidMount
componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。
componentDidMount中通常进行哪里操作呢?
- 依赖于DOM的操作可以在这里进行;
- 在此处发送网络请求就最好的地方;
- (官方建议) 可以在此处添加一些订阅(会在componentWillUnmount取消订阅);
3.3 componentDidUpdate
componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执 行此方法。
- 当组件更新后,可以在此处对 DOM 进行操作;
- 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网 络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。
3.4 componentWillUnmount
componentWillUnmount() 会在组件卸载及销毁之前直接调用。
- 在此方法中执行必要的清理操作; 例如,清除 timer,取消网络请求或清除 在 componentDidMount() 中创建的订阅等;
对于上面的生命周期函数进行实际练习一遍:
import React, { Component } from 'react'
// 子组件 移除
class Cpn extends Component {
render () {
return (
<h2>我是cpn组件</h2>
)
}
componentWillUnmount() {
console.log('5.组件即将被移除')
}
}
// 父组件
export default class App extends Component {
constructor(){
super()
this.state = {
count: 0,
isShow: true
}
console.log('1.执行了组件的constructor')
}
render() {
console.log('2.执行了组件的render函数')
return (
<div>
组件的生命周期过程
<h2>{this.state.count}</h2>
<button onClick={e => this.increament()}>+</button>
<hr />
<button onClick={ e => this.changeState()}>切换Cpn显示隐藏</button>
{this.state.isShow && <Cpn />}
</div>
)
}
increament() {
this.setState({
count: this.state.count + 1
})
}
changeState() {
this.setState({
isShow: !this.state.isShow
})
}
componentDidMount() {
console.log('3.组件挂载成功了')
}
componentDidUpdate() {
console.log('4.组件更新了')
}
}
首次渲染会执行1,2,3,
调用increament方法改变了state,所以会执行3,4,
更新组件,调用changeState方法会改变了state,同时也会隐藏显示dom元素,所以会执行3,4,5。
4.不常用生命周期函数
官网给出了更多的生命周期函数: https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
getDerivedStateFromProps:state 的值在任何时候都 依赖于 props时使用;该方法返回一个对象来更新state;
getSnapshotBeforeUpdate:在React更新DOM之前回 调的一个函数,可以获取DOM更新前的一些信息(比如 说滚动位置);
5.组件间通信
5.1父传子组件间的通信(props)
5.1.1 类组件
父组件通过props属性向子组件传递参数,子组件接收到后进行相应的处理。
import React, {Component} from 'react'
class ChildCpn extends Component {
// 旧的写法
constructor(props){
super()
this.props = props
}
// 新的写法 也可以不写
constructor(props){
super(props) // 继承自父类
}
render() {
const {name, age, height} = this.props
return (
<h2>子组件展示数据:{name + " " + age + " " + height}</h2>
)
}
}
export default class App extends Component {
render() {
return (
<div>
<ChildCpn name="leah" age="18" height="160"></ChildCpn>
</div>
)
}
}
对于子组件接收参数的方式,进行以下解析:
每个子组件在接收到父组件传过来的参数的时候都需要通过以下方式获取,这样就很繁琐,所以react的就在父组件里面保存了props,这样子组件只需要继承一下父组件的属性即可,不需要自己维护props。react源码对应如下:
function Component(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}
所有的组件都继承自Component组件,Component组件保存了公共属性。
如果还看不懂,那么看一个经典的例子:
class Person {
constructor(name, age, height) {
this.name = name
this.age = age
this.height = height
}
}
class Student extends Person {
constructor(name, age, score){
super(name, age) // 继承自父组件
this.score = score
}
}
class Teacher extends Person {
constructor(name, age, lesson) {
super(name, age) // 继承自父组件
this.lesson = lesson
}
}
父组件保存了子组件需要的公共的属性。子组件只需要传参给父组件。
上面es6代码转为es5之后就是下面这样:
var Student = /*#__PURE__*/function (_Person) {
_inherits(Student, _Person);
var _super = _createSuper(Student);
function Student(name, age, score) {
var _this;
_classCallCheck(this, Student);
_this = _super.call(this, name, age); // 继承自父组件 this ->Student
_this.score = score;
return _this;
}
return Student;
}(Person);
var Teacher = /*#__PURE__*/function (_Person2) {
_inherits(Teacher, _Person2);
var _super2 = _createSuper(Teacher);
function Teacher(name, age, lesson) {
var _this2;
_classCallCheck(this, Teacher);
_this2 = _super2.call(this, name, age); // 继承自父组件 this ->Teacher
_this2.lesson = lesson;
return _this2;
}
return Teacher;
}(Person);
5.1.2 函数组件
import React, {Component} from 'react'
function ChildCpn (props) {
const {name, age, height} = props
return (
<h2>{name + age + height}</h2>
)
}
export default class App extends Component {
render() {
return (
<div>
<ChildCpn name="leah" age="18" height="160"></ChildCpn>
</div>
)
}
}
函数式组件没有自己的状态。
5.2子组件向父组件通信(props)
通过props属性,让父组件给子组件传递一个回调函数,在子组件中调用这个函数便可向父组件通信。
import React, {Component} from 'react'
// 子组件
class BtnCpn extends Component {
constructor () {
super()
}
render() {
const {onClick} = this.props // 拿到
return (
<button onClick={onClick}>+1</button> // 调用
)
}
}
// 父组件
export default class App extends Component {
constructor (){
super()
this.state = {
counter: 0
}
}
render(){
return (
<div>
<h2>计数值:{this.state.counter}</h2>
<BtnCpn onClick={this.increament}></BtnCpn>
</div>
)
}
increament = () => {
this.setState({
counter: this.state.counter +1
})
}
}
5.3跨组件通信(Context)
React是单向数据流,数据是从上往下单向传递的,每个组件都可以接收父组件的属性和状态,也可以把属性和状态向下传递给子组件,但是当层级特别多的时候就会变得非常繁琐。Context 提供了一种在组件之间共享此类值得方式,而不必逐层传递。它主要是用来解决祖先组件向后代组件传递数据的问题(prop drilling
)。方便组件之间跨层级传递数据。
举个例子:用户登录之后,很多组件需要拿到用户相关信息,如果按照prop传递的方式获取,会变得异常繁琐,而且很难判断数据的真正来源。使用Context,就可以在_Provider_
的后代组件的任意位置,都可以_Consumer_
数据。
- 通过
createContext
创建Context - 使用
Context.Provider
组件发布数据(通过给 - .Provider
传递
value`属性)。 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。 - 通过 contextType 属性获取由 React.createContext() 创建的 Context 对象, 这能让我们使用 this.context 来消费最近 Context 上的那个值;也可以在任何生命周期中访问到它,包括 render 函数中;
- 如果是函数组件,那么所有后代组件,都可以通过
Context.Consumer
消费数据 - 如果是类组件,那么所有的后代组件都需要通过 this.context.xxx 来获取。
5.3.1 类组件
import React from "react";
import ReactDOM from "react-dom";
let ThemeContext = React.createContext();
class Content extends React.Component {
//取父组件传递过来得值
//先定义一个静态属性contextType 通过this.context.color拿到值
static contextType = ThemeContext
render() {
return <div style={{ border: `1px solid ${this.context.color}` }}>内容</div>;
}
}
class Main extends React.Component {
//取父组件传递过来得值
//先定义一个静态属性contextType 通过this.context.color拿到值
static contextType = ThemeContext
render() {
return (
<div style={{ border: `1px solid ${this.context.color}` }}>
子Main
<Content}></Content>
</div>
);
}
}
class Title extends React.Component {
//取父组件传递过来得值
//先定义一个静态属性contextType 通过this.context.color拿到值
static contextType = ThemeContext
render() {
return <div style={{ border: `1px solid ${this.context.color}` }}>标题</div>;
}
}
class Header extends React.Component {
//取父组件传递过来得值
//先定义一个静态属性contextType 通过this.context.color拿到值
static contextType = ThemeContext
render() {
return (
<div style={{ border: `1px solid ${this.context.color}` }}>
子Header
<Title></Title>
</div>
);
}
}
class Panel extends React.Component {
state = { color: "green" };
render() {
let colorvalue = { color: this.state.color };
// Provider 表示提供者,负责向下层所有得组件提供数据value 它得所有子组件都可以通过拿到value值
return (
<ThemeContext.Provider value={colorvalue}>
<div style={{ border: `1px solid ${this.state.color}`, width: `200px` }}>
Panel
<Header></Header>
<Main></Main>
</div>
</ThemeContext.Provider>
);
}
}
ReactDOM.render(<Panel></Panel>, document.getElementById("root"));
5.3.2 函数组件
import React from "react";
import ReactDOM from "react-dom";
let ThemeContext = React.createContext();
function Content (props){
//Consumer 消费者 消费上下文中的value,通过value.xxx来获取发布的数据
return (
<ThemeContext.Consumer>
{
(value) => (
<div style={{ border: `1px solid ${value.color}` }}>
内容
</div>
)
}
</ThemeContext.Consumer>
)
}
function Main (props){
//Consumer 消费者 消费上下文中的value,通过value.xxx来获取发布的数据
return (
<ThemeContext.Consumer>
{
(value) => (
<div style={{ border: `1px solid ${value.color}`}}>
子Main
<Content style={{ border: `1px solid ${value.color}` }}></Content>
</div>
)
}
</ThemeContext.Consumer>
)
}
function Title (props){
//Consumer 消费者 消费上下文中的value,通过value.xxx来获取发布的数据
return (
<ThemeContext.Consumer>
{
(value) => (
<div style={{ border: `1px solid ${value.color}` }}>标题</div>
)
}
</ThemeContext.Consumer>
)
}
function Header (props){
//Consumer 消费者 消费上下文中的value,通过value.xxx来获取发布的数据
return (
<ThemeContext.Consumer>
{
(value) => (
<div style={{ border: `1px solid ${value.color}` }}>
子Header
<Title></Title>
</div>
)
}
</ThemeContext.Consumer>
)
}
class Panel extends React.Component {
state = { color: "green" };
changeColor = (color) => {
this.setState({color})
}
render() {
let colorvalue = { color: this.state.color, changeColor: this.changeColor };
// Provider 表示提供者,负责向下层所有得组件提供数据value 它得所有子组件都可以通过拿到value值
return (
<ThemeContext.Provider value={colorvalue}>
<div style={{ border: `1px solid ${this.state.color}`, width: `200px` }}>
Panel
<Header></Header>
<Main></Main>
</div>
</ThemeContext.Provider>
);
}
}
ReactDOM.render(<Panel></Panel>, document.getElementById("root"));
5.3.3 多个context嵌套使用
import React, { Component } from 'react';
// 创建Context对象
const UserContext = React.createContext({
nickname: "aaaa",
level: -1
})
const ThemeContext = React.createContext({
color: "black"
})
function ProfileHeader() {
// jsx -> 嵌套的方式
return (
<UserContext.Consumer>
{
value => {
return (
<ThemeContext.Consumer>
{
theme => {
return (
<div>
<h2 style={{color: theme.color}}>用户昵称: {value.nickname}</h2>
<h2>用户等级: {value.level}</h2>
<h2>颜色: {theme.color}</h2>
</div>
)
}
}
</ThemeContext.Consumer>
)
}
}
</UserContext.Consumer>
)
}
function Profile(props) {
return (
<div>
<ProfileHeader />
<ul>
<li>设置1</li>
<li>设置2</li>
<li>设置3</li>
<li>设置4</li>
</ul>
</div>
)
}
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
nickname: "kobe",
level: 99
}
}
render() {
return (
<div>
<UserContext.Provider value={this.state}>
<ThemeContext.Provider value={{ color: "red" }}>
<Profile />
</ThemeContext.Provider>
</UserContext.Provider>
</div>
)
}
}
5.4 事件传递(EventBus)
通过Context主要实现的是数据的共享,但是在开发中往往需要跨组件之间进行事件传递。
在React中,我们可以依赖一个使用较多的库 events 来完成对应的操作;
events常用的API:
创建EventEmitter对象:eventBus对象;
- 发出事件:eventBus.emit(“事件名称”, 参数列表);
- 监听事件:eventBus.addListener(“事件名称”, 监听函数);
- 移除事件:eventBus.removeListener(“事件名称”, 监听函数);
import React, { PureComponent } from 'react';
import { EventEmitter } from 'events';
// 事件总线: event bus
const eventBus = new EventEmitter();
class Home extends PureComponent {
// 添加事件监听
componentDidMount() {
eventBus.addListener("sayHello", this.handleSayHelloListener);
}
// 取消事件监听
componentWillUnmount() {
eventBus.removeListener("sayHello", this.handleSayHelloListener);
}
handleSayHelloListener(num, message) {
console.log(num, message);
}
render() {
return (
<div>
Home
</div>
)
}
}
class Profile extends PureComponent {
render() {
return (
<div>
Profile
<button onClick={e => this.emmitEvent()}>点击了profile按钮</button>
</div>
)
}
// 发射事件
emmitEvent() {
eventBus.emit("sayHello", 123, "Hello Home");
}
}
export default class App extends PureComponent {
render() {
return (
<div>
<Home/>
<Profile/>
</div>
)
}
}
6.组件属性类型验证
对应的官网地址:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html
PropTypes 类型校验
defaultProps 默认值
**defaultProps**
作为静态属性
7.react实现插槽效果
在vue中想要实现可扩展组件使用slot就可以,但是react并没有slot的概念,想要实现插槽功能,可以通过父组件给子组件传递属性的 方式实现。
方式一:children属性
前面我们看过jsx转换成React.createElement
函数调用的源码,组件中的包含的组件或者div、span等标签会被赋值给children属性进行保存。
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
if (__DEV__) {
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
}
所以我们就可以通过children属性获取到父组件传递过来的div。
父组件
import React, { Component } from 'react'
import NavBar from './NavBar'
export default class App extends Component {
render() {
return (
<div>
<NavBar>
<div>左边</div>
<div>中间</div>
<div>右边</div>
</NavBar>
</div>
)
}
}
子组件:
在组件中通过this.props.children获取
import React, { Component } from 'react'
export default class NavBar extends Component {
render() {
return (
<div className="nav-item nav-bar">
<div className="left-nav">
{this.props.children[0]}
</div>
<div className="center-nav">
{this.props.children[1]}
</div>
<div className="right-nav">
{this.props.children[2]}
</div>
</div>
)
}
}
还有一种方式就是直接通过props属性传递。
方式二:props属性
父组件:
import React, { Component } from 'react'
import NavBar from './NavBar'
import NavBar2 from './NavBar2'
export default class App extends Component {
render() {
return (
<div>
<NavBar2 leftSlot={<div>左边</div>}
centerSlot={<div>中间</div>}
rightSlot={<div>右边</div>}>
</NavBar2>
</div>
)
}
}
子组件:
在组件中通过this.props获取
import React, { Component } from 'react'
export default class NavBar extends Component {
render() {
const {leftSlot,centerSlot,rightSlot} = this.props
return (
<div className="nav-item nav-bar">
<div className="left-nav">
{leftSlot}
</div>
<div className="center-nav">
{centerSlot}
</div>
<div className="right-nav">
{rightSlot}
</div>
</div>
)
}
}
看看实现效果都是一样的:
8. setState更新机制
https://zhuanlan.zhihu.com/p/95865701
8.1 不能直接修改state的值
开发中我们并不能直接通过修改state的值来让界面发生更新: 因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变 化; React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化;必须通过setState来告知React数据已经发生了变化。
setState依次触发以下4个组件的生命周期
1)shouldComponentUpdate(被调用时,state还没有更新,如果返回false,不会再触发其他生命周 期,但是state依然会被更新)
2)componentWillUpdate(被调用时state还没有被更新)
3)render(调用时state已经被更新)
4)componentDidUpdate
8.2 state的更新可能是异步
我们知道调用setState会触发更新操作,这个过程包括更新state,创建新的VNode,在经过diff算法对比差异,决定需要渲染那一部分,假如他是同步更新的话,每次调用都要执行一次前面的流程,这样会造成很大的性能问题,所以需要将多个setState放进一个队列里面,、然后再一个一个执行,最后再一次性更新视图,这样会提高性能。而且,如果同步更新了state的话,由于还没有执行render函数,那么state和props不能保持同步会在开发中产生很多的问题;比如页面卡顿等。异步更新可以给浏览器一些时间更新页面。比如点击一个按钮+1,点击 的速度很快,按点击的次数已经加到10了,但是由于前面几次的render函数还没有执行完,页面可能显示的还是7,8,这样的情况。比如通过for循环对一个数字加1,加1000次,如果是同步更新的话,每次加1都需要执行一次render,就很影响性能。
8.3 获取异步更新结果
通过上面源码的学习我们知道,要想获取异步更新结果,可以给setState传入第二个参数回调函数,在这个回调函数中可以拿到更新后的值。也可以在componentDidUpdate这个生命周期中获取到更新后的值。
import React, { Component } from 'react'
export default class App extends Component {
constructor(){
super()
this.state = {
counter: 0
}
}
render() {
return (
<div>
<h2>当前计数:{this.state.counter}</h2>
<button onClick={e => this.increament()}>+</button>
</div>
)
}
componentDidUpdate() {
console.log(this.state.counter)
}
increament() {
this.setState({
counter: this.state.counter + 1
}, () =>{
console.log(this.state.counter)
})
}
}
8.4 setState同步更新的情况
8.4.1 将setState放入定时器执行
import React, { Component } from 'react'
let renderTimes = 0
export default class App extends Component {
constructor(){
super()
this.state = {
message: '你好'
}
}
render() {
renderTimes += 1
return (
<div>
<h1>{renderTimes}</h1>
<h2>{this.state.message}</h2>
<button onClick={e => this.changeText()}>改变文本</button>
</div>
)
}
changeText() {
setTimeout(() => {
this.setState({
message:'hello react'
})
console.log(this.state.message)
})
}
}
8.4.2 原生dom事件
import React, { Component } from 'react'
let renderTimes = 0
export default class App extends Component {
constructor(){
super()
this.state = {
message: '你好'
}
}
render() {
renderTimes += 1
return (
<div>
<h1>{renderTimes}</h1>
<h2>{this.state.message}</h2>
<button onClick={e => this.changeText()}>改变文本1</button>
<button id="btn2">改变文本2</button>
</div>
)
}
componentDidMount() {
const btnEl = document.getElementById("btn2")
btnEl.addEventListener('click', () =>{
this.setState({
message: 'hello react'
})
})
}
}
8.5 setState 数据合并
当调用setState的时候,React会把要修改的那一部分的对象合并到当前的state上面。
在源码中对应下面这行代码:
// Merge the partial state and the previous state.
return Object.assign({}, prevState, partialState);
Object.assign()
方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
当对象中只有一级属性,没有二级属性的时候,此方法为深拷贝,但是对象中有对象的时候,此方法,在二级属性以后就是浅拷贝。
也就是说,如果对象的属性值为简单类型(如string, number),通过Object.assign({},srcObj);得到的新对象为深拷贝
;如果属性值为对象或其它引用类型,那对于这个对象而言其实是浅拷贝
的。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
https://www.jianshu.com/p/1b212581a8d5
8.6 更新合并
- 异步更新之后state的值并不会立即更新,所以每次拿到的state都是 0
import React, { Component } from 'react'
export default class App extends Component {
constructor(){
super()
this.state = {
counter: 0
}
}
render() {
return (
<div>
<h2>当前计数:{this.state.counter}</h2>
<button onClick={e => this.increament()}>+</button>
</div>
)
}
// 获取异步更新结果
componentDidUpdate() {
console.log(this.state.counter)
}
increament() {
// 1. 异步更新之后state的值并不会立即更新,所以每次拿到的state都是 0
this.setState({
counter: this.state.counter + 1
})
this.setState({
counter: this.state.counter + 1
})
this.setState({
counter: this.state.counter + 1
})
}
}
这个代码打印出的结果是 1,我们期望的是 3,这是因为异步更新之后state的值并不会立即更新,所以每次拿到的state都是 0,都是在 0 的基础上进行加 1 。点击一次的结果就是 加 1。
import React, { Component } from 'react'
export default class App extends Component {
constructor(){
super()
this.state = {
counter: 0
}
}
render() {
return (
<div>
<h2>当前计数:{this.state.counter}</h2>
<button onClick={e => this.increament()}>+</button>
</div>
)
}
// 获取异步更新结果
componentDidUpdate() {
console.log(this.state.counter)
}
increament() {
//2. 给setState()传递一个函数作为参数,在这个函数中可以拿到每次改变后的值,作为下一次的计算基数
this.setState((prevState, props) => {
return {
counter: prevState.counter +1
}
})
this.setState((prevState, props) => {
return {
counter: prevState.counter +1
}
})
this.setState((prevState, props) => {
return {
counter: prevState.counter +1
}
})
}
}
给setState()传递一个函数作为参数,在这个函数中可以拿到每次改变后的值,作为下一次的计算基数
8.7 手写setState源码
8.7.1 实现异步更新
let state = {number: 0}
function setState(newState) {
state = newState
console.log(state)
}
setState({number: state.number + 1})
setState({number: state.number + 2})
setState({number: state.number + 3})
这段代码会通过 setState 方法改变state值,我们看看打印结果:
可以看到每次调用setState都会改变state的值并且进行渲染,这将是一个非常消耗性能的问题。
所以React针对setState做了一些特别的优化:将多个setState的调用放进了一个队列,合并成一个来执行,这意味着当调用setState时,state并不会立即更新,看下面这个例子:
let state = {number: 0}
let updataQueue = []
function setState(newState) {
updataQueue.push(newState)
}
setState({number: state.number + 1})
setState({number: state.number + 2})
setState({number: state.number + 3})
updataQueue.forEach(item =>{
state = item
})
console.log(state) // 3
我们预想的是结果等于6.但是输出的却是3,这是因为变成异步更新之后state的值并不会立即更新,所以每次拿到的state都是 0 ,如果我们想要让结果等于 6,也就是每次都能拿到最新值,那就需要给setState()传递一个函数作为参数,在这个函数中可以拿到每次改变后的值,并通过这个函数的返回值得到下一个状态。
let state = { number: 0 }
let updataQueue = [] //更新函数队列
let callbackQueue = [] //回调函数队列
function setState(updataState,callback) {
//入队
updataQueue.push(updataState)
callbackQueue.push(callback)
}
//清空队列
function flushUpdata () {
for(let i = 0; i < updataQueue.length; i++) {
state = updataQueue[i](state) //拿到每次改变后的值作为下一个的状态
}
state = state
callbackQueue.forEach(callbackItem => callbackItem())
}
function add(){
setState(preState => ({ number: preState.number + 1}),() => {
console.log(state)
})
setState(preState => ({ number: preState.number + 2}),() => {
console.log(state)
})
setState(preState => ({ number: preState.number + 3}),() => {
console.log(state)
})
//批量更新
flushUpdata()
}
add()
console.log(state) // 6
由于回调函数也是异步执行的,所以最后一次性输出的都是6.
改写成class类的形式如下:
class Component {
constructor() {
this.state = {
number: 0
}
this.batchUpdata = false
this.updataQueue = [] //更新队列
this.callbackQueue = [] //回调函数队列
}
setState(updataState, callback) {
if (this.batchUpdata) {
this.updataQueue.push(updataState) //放入队列
this.callbackQueue.push(callback)
}
}
flushUpdata() {
let state = this.state
// this.updataQueue.forEach(newStateitem => this.state = newStateitem)
for(let i = 0; i < this.updataQueue.length; i++) {
state = this.updataQueue[i](state)
}
this.state = state
this.callbackQueue.forEach(callback => callback())
}
add() {
this.batchUpdata = true //开启合并模式
this.setState(preState => ({ number: preState.number + 1}),() => {
console.log(this.state)
})
this.setState(preState => ({ number: preState.number + 2}),() => {
console.log(this.state)
})
this.setState(preState => ({ number: preState.number + 3}),() => {
console.log(this.state)
})
//批量更新
this.flushUpdata()
}
}
let c = new Component()
c.add()
console.log(c.state)
现在这个逻辑对于setState传入的参数是函数很适合,但是有时候我们希望传入的是对象,且希望利用setState执行完之后做一些操作,比如在请求到数据之后隐藏进度条等,这个时候就需要setState能变为同步执行,这个时候我们会借助promise、setTimeout等方法来改变setState让它变为同步的。也就是不用放入队列,而是立即执行,但是以上逻辑不支持同步的情况,我们需要修改:
class Component {
constructor() {
this.state = {
number: 0
}
this.batchUpdata = false
this.updataQueue = [] //更新队列
this.callbackQueue = [] //回调函数队列
}
setState(updataState, callback) {
if (this.batchUpdata) { //批量更新
this.updataQueue.push(updataState) //放入队列
this.callbackQueue.push(callback)
}else { //直接更新
console.log('直接更新')
//如果是函数需要把老值传进去
if(typeof updataState === 'function') {
this.state = updataState(this.state)
}else {
this.state = updataState
}
}
}
flushUpdata() {
let state = this.state
// console.log(this.updataQueue)
for(let i = 0; i < this.updataQueue.length; i++) {
//为了兼容参数为函数和对象的情况需要判断一下 参数为对象的时候不用传上一个的状态值,参数为函数的时候需要传上一个的状态给下一个状态
if(typeof this.updataQueue[i] === 'function') {
state = this.updataQueue[i](state)
}else {
state = this.updataQueue[i]
}
}
this.state = state
this.callbackQueue.forEach(callback => {
if(callback) callback() //为了兼容参数为函数和对象的情况需要判断一下,参数为对象的时候没有回调函数就不执行
})
this.batchUpdata = false //更新完毕置为false
}
add() {
this.batchUpdata = true //开启合并模式
//不会放进更新队列
setTimeout(() => {
this.setState({number: this.state.number + 4})
console.log(this.state)
},1000)
this.setState({number: this.state.number + 1})
// this.setState(preState => ({ number: preState.number + 1}),() => {
// console.log(this.state)
// })
// this.setState({number: this.state.number + 1})
//批量更新
this.flushUpdata()
}
}
let c = new Component()
c.add()
console.log('end'+ JSON.stringify(c.state))
批量处理机制就是为了减少setState刷新页面的次数,setTimeout,promise等异步方法可以直接跳过批量处理机制,setState调几次就改几次。
https://www.cnblogs.com/jiuyi/p/9263114.html这篇文章对于同步更新讲的比较好
8.7.2 seState的更新数据会被合并
当调用setState的时候,React会把你要修改的那一部分的对象合并到当前的state上面,举个栗子:
class Counter extends React.Component{
constructor(props) { //构造函数是唯一给状态赋值的地方
super(props)
this.add = this.add.bind(this)
//定义状态的地方
this.state = {name: 'leah' ,number: 0}
}
add (event) {
console.log(event)
// this.state.number += 1 不能直接修改state的值
this.setState({number: this.state.number + 1})
}
render(){
console.log(this)
//当我们调用setState的时候会引起状态的改变和组件的更新
console.log('render')
return (
<div>
<p>{this.state.name}</p>
<p>{this.state.number}</p>
<button onClick={this.add}>+</button>
</div>
)
}
}
当前我们只修改了state.number这个时候,name还是会渲染,我们需要对这部分进行合并
class Component {
constructor() {
this.state = {
name: 'leah',
number: 0
}
this.batchUpdata = false
this.updataQueue = [] //更新队列
this.callbackQueue = [] //回调函数队列
}
setState(updataState, callback) {
if (this.batchUpdata) { //批量更新
this.updataQueue.push(updataState) //放入队列
this.callbackQueue.push(callback)
}else { //直接更新
console.log('直接更新')
//如果是函数需要把老值传进去
if(typeof updataState === 'function') {
this.state = updataState(this.state)
}else {
this.state = updataState
}
}
}
flushUpdata() {
let state = this.state
// console.log(this.updataQueue)
for(let i = 0; i < this.updataQueue.length; i++) {
//为了兼容参数为函数和对象的情况需要判断一下 参数为对象的时候不用传上一个的状态值,参数为函数的时候需要传上一个的状态给下一个状态
let partialState = typeof this.updataQueue[i] === 'function' ? this.updataQueue[i](this.state) : this.updataQueue[i]
state = {...state, ...partialState} // 合并数据
}
this.state = state
this.callbackQueue.forEach(callback => {
if(callback) callback() //为了兼容参数为函数和对象的情况需要判断一下,参数为对象的时候没有回调函数就不执行
})
this.batchUpdata = false //更新完毕置为false
}
add() {
this.batchUpdata = true //开启合并模式
//不会放进更新队列
setTimeout(() => {
this.setState({number: this.state.number + 4})
console.log(this.state)
},1000)
this.setState({number: this.state.number + 1})
// this.setState(preState => ({ number: preState.number + 1}),() => {
// console.log(this.state)
// })
// this.setState({number: this.state.number + 1})
//批量更新
this.flushUpdata()
}
}
let c = new Component()
c.add()
console.log('end'+ JSON.stringify(c.state))
数据合并与源码中的方法并不相同。源码中是使用object.assign()实现了对象的合并。在这里我们使用了使用了展开运算符,也就是Object Spread()方法。
https://blog.csdn.net/weixin_39647035/article/details/103233926
合成事件原理:利用事件冒泡机制
如果react事件绑定在了真实DOM节点上,一个节点同时有多个事件时,页面的响应和内存的占用会受到很大的影响。因此SyntheticEvent作为中间层出现了。
事件没有在目标对象上绑定,而是在document上监听所支持的所有事件,当事件发生并冒泡至document时,react将事件内容封装并叫由真正的处理函数运行。
我们的更新其实并不是真正的异步处理,而是更新的时候把更新内容放到了更新队列中,最后批次更新,这样才表现出异步更新的状态。setTimeout,promise等异步方法可以直接跳过批量处理机制,setState调几次就改几次。
9. react组件更新机制
先看一下react生命周期函数
上图展示了react 在各个阶段对应的生命周期函数。
9.1 shouldComponentUpdate钩子函数决定是否调用render方法。
React在props或state发生改变时,会进入 shouldComponentUpdate钩子函数,如果该函数返回值为true,就会调用React的render方法,会创建一颗不同的虚拟dom树。如果false就不会调用render方法。
9.2 diff算法 决定是否会重新渲染DOM
调用render方法会重新创建一颗虚拟DOM树,react会对新旧虚拟 DOM树进行对比,找出差异,如果有差异就重新渲染差异的地方,如果没差异,就不会重新渲染dom,这也是react高性能的体现。
9.2.1 diff算法
- 同层节点之间相互比较,不会夸节点比较;
- 不同类型的节点,产生不同的树结构;
- 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;
情况一:对比不同类型的元素
当节点为不同的元素,React会拆卸原有的树,并且建立起新的树:
当一个元素从 <a>
变成 <img>
,从 <Article>
变成 <Comment>
,或从 <Button>
变成 <div>
都会触发一个完整的重建 流程;
当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行 componentWillUnmount() 方法;
当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行 componentWillMount() 方法, 紧接着 componentDidMount() 方法;
当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行 componentWillUnmount() 方法;
当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行 componentWillMount() 方法, 紧接着 componentDidMount() 方法;
比如下面的代码更改: React 会销毁 Counter 组件并且重新装载一个新的组件,而不会对Counter进行复用;
情况二:对比同一类型的元素
当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。
比如下面的代码更改: 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 className 属性;
情况三:对子节点进行递归
在默认条件下,当递归 DOM 节点的子元素时,React 会同 时遍历两个子元素的列表;当产生差异时,生成一个 mutation。
举个例子,在最后插入一条数据的情况:
前面两个比较是完全相同的,所以不会产生mutation; 最后一个比较,产生一个mutation,将其插入到新的 DOM树中即可;
但是如果在中间插入一条数据:
React会对每一个子元素产生一个mutation,而不是保持星际穿越和盗梦空间的不变;这种低效的比较方式会带来一定的性能问题;
9.4 key 优化
当给每一个子元素添加key属性时,React 会使用 key 来匹配原有树上的子元素以及最新树上的子元素,根据key定位一个唯一的元素。在这种情况下,上面例子中的星际穿越和盗梦空间就会被复用,而不是重新创建,对性能有一定的优化。
9.3 PureComponent 和 memo
https://www.jianshu.com/p/5795c00aa8b8
props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回true或者false; 但是react官方并不建议我们手动编写 shouldComponentUpdate,而是使用内置的[**PureComponent**](https://zh-hans.reactjs.org/docs/react-api.html#reactpurecomponent)
组件。
memo
组件和PureComponent
组件,分别用于减少函数组件和类组件的重复渲染 。
PureComponent和memo实现了 实现 shouldComponentUpdate()
,会在render之前会进行浅比较。
import React, { PureComponent, memo } from 'react'
// 函数组件 让memo包裹函数组件就会进行浅比较
const MemoList = memo(function ProductList() {
console.log('ProductsList被调用了')
return (
<ul>
<li>列表1</li>
<li>列表2</li>
<li>列表3</li>
<li>列表4</li>
</ul>
)
})
// 类组件 让类组件继承PureComponent组件,就会进行浅比较
export default class App extends PureComponent {
constructor() {
super()
this.state = {
message: '我是文本内容'
}
}
render() {
return (
<div>
<h2>{this.state.message}</h2>
<MemoList />
</div>
)
}
}
9.4.1 PureComponent
对应的源码:
function checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext,
) {
const instance = workInProgress.stateNode;
// 如果这个组件实例自定义了shouldComponentUpdate生命周期函数
if (typeof instance.shouldComponentUpdate === 'function') {
startPhaseTimer(workInProgress, 'shouldComponentUpdate');
// 执行这个组件实例自定义的shouldComponentUpdate生命周期函数
const shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
nextContext,
);
stopPhaseTimer();
return shouldUpdate;
}
// 判断当前组件实例是否是PureReactComponent
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
/**
* 1. 浅比较判断 oldProps 与newProps 是否相等;
* 2. 浅比较判断 oldState 与newState 是否相等;
*/
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
return true;
}
如果一个 PureComponent 组件自定义了shouldComponentUpdate
生命周期函数,则该组件是否进行渲染取决于shouldComponentUpdate
生命周期函数的执行结果,不会再进行额外的浅比较。如果未定义该生命周期函数,才会浅比较状态 state 和 props。
9.4.2 memo
React.memo
为高阶组件。它与React.PureComponent
非常相似,但它适用于函数组件,但不适用于 class 组件。
React.memo
对应源码:
function updateMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
updateExpirationTime,
renderExpirationTime: ExpirationTime,
): null | Fiber {
/* ...省略...*/
// 判断更新的过期时间是否小于渲染的过期时间
if (updateExpirationTime < renderExpirationTime) {
const prevProps = currentChild.memoizedProps;
// 如果自定义了compare函数,则采用自定义的compare函数,否则采用官方的shallowEqual(浅比较)函数。
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
/**
* 1. 判断当前 props 与 nextProps 是否相等;
* 2. 判断即将渲染组件的引用是否与workInProgress Fiber中的引用是否一致;
*
* 只有两者都为真,才会退出渲染。
*/
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
// 如果都为真,则退出渲染
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
}
/* ...省略...*/
}
如果自定义了compare函数,则采用自定义的compare函数,否则采用官方的shallowEqual(浅比较)函数。
updateMemoComponent
函数决定是否退出渲染取决于以下两点:
- 当前 props 与 nextProps 是否相等;
- 即将渲染组件的引用是否与 workInProgress Fiber 中的引用是否一致;
9.4.3 shallowEqual 源码
具体是怎么浅比较的 ,我们来看看源码 shallowEqual:
function shallowEqual(objA: mixed, objB: mixed): boolean {
// 1.判断两个值是否是相同的 两个对象是否是同一个引用地址 Object.is() 方法严格相等,NaN不等于自身,以及+0等于-0
// +0 === -0 //true
// NaN === NaN // false
if (is(objA, objB)) {
return true;
}
//2. 如果有任何一个对象不是object类型或者是null就直接返回false
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA); // 拿到A的key
const keysB = Object.keys(objB); // 拿到B的key
// 3.如果两个对象的长度不相等就返回false
if (keysA.length !== keysB.length) {
return false;
}
// hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。
// Test for A's keys different from B.
// 4. 判断两个对象的键是否不同。
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) || // 检查B对象上是否有A的键
!is(objA[keysA[i]], objB[keysA[i]]) // 检查A对象上是否有B的键
) {
return false;
}
}
return true;
}
export default shallowEqual;
Object.is():https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is
hasOwnProperty():https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty
特别注意,因为是浅层比较,所以对于引用类型的数据,如果更改前后的对象引用地址是相同的,那么即使他内部数据发生了变化, 使用PureComponent时很可能不会把新值渲染到DOM上 。看下面的这个例子:
点击按钮增加一条数据。
错误的写法:
import React, { Component } from 'react'
export default class App extends Component {
constructor() {
super()
this.state = {
frends: [
{name: 'A', age: 18},
{name: 'B', age: 15},
{name: 'C', age: 10},
]
}
}
render() {
return (
<div>
<h2>好友列表</h2>
<ul>
{
this.state.frends.map((item, index) => {
return <li key={item.name}>姓名:{item.name}, 年龄:{item.age}</li>
})
}
</ul>
<button onClick={e => this.insertData()}>添加数据</button>
</div>
)
}
// 性能优化 如果数据发生了变化就调用render,如果没有变化就不调用render
shouldComponentUpdate(newProps,newState) {
if(newState.frends !== this.state.frends) {
return true
}
return false
}
insertData() {
// 更新前后的对象都指向了同一个引用地址,在浅比较的时候会认为是同一个对象没有任何变化就不会重新渲染
const newData = {name: 'D', age: 30}
this.state.frends.push(newData)
this.setState({
frends: this.state.frends
})
}
}
这样写看似没问题,但是因为两个对象是同一个引用地址,react在进行浅层比较的时候就认为新旧state是相同的,没有发生变化就不会调用render重新渲染。
正确的写法:
import React, { Component } from 'react'
export default class App extends Component {
constructor() {
super()
this.state = {
frends: [
{name: 'A', age: 18},
{name: 'B', age: 15},
{name: 'C', age: 10},
]
}
}
render() {
return (
<div>
<h2>好友列表</h2>
<ul>
{
this.state.frends.map((item, index) => {
return <li key={item.name}>姓名:{item.name}, 年龄:{item.age}</li>
})
}
</ul>
<button onClick={e => this.insertData()}>添加数据</button>
</div>
)
}
// 性能优化
shouldComponentUpdate(newProps,newState) {
if(newState.frends !== this.state.frends) {
return true
}
return false
}
insertData() {
// 展开运算符,对于只有一层数据的数组来说是深拷贝,他会创建一个新的内存地址来保存。
const newFriends = [...this.state.frends]
newFriends.push({name: 'D', age: 30})
this.setState({
frends: newFriends // 让旧的对象地址指向新的地址
})
}
}
这种写法是使用扩展运算符,对以旧的friends对象数组进行了一层深拷贝,就会开辟一个新的内存地址,然后在新的内存地址中新增数据,最后再让旧的对象地址指向新的地址。这样前后两个引用地址就是不同的,在进行比较的时候就会返回false调用render重新渲染。
10.ref
10.1 创建和访问
ref 的值根据节点的类型而有所不同:
- 当
ref
属性用于 HTML 元素时,构造函数中使用React.createRef()
创建的ref
接收底层 DOM 元素作为其current
属性。 - 当
ref
属性用于自定义 class 组件时,ref
对象接收组件的挂载实例作为其current
属性。 - 不能在函数组件上使用
ref
属性,因为他们没有实例。若是想用需要经过特殊处理
10.1.1 ref=字符串 (已经废弃)
class Cualculator extends React.Component {
add =() => {
let num1 = parseInt(this.refs.num1.value)
let num2 = parseInt(this.refs.num2.value)
let result = num1 +num2
this.refs.result.value = result
}
render() {
return (
<div>
<input ref="num1" />+<input ref="num2"/><button onClick={this.add}>=</button><input ref="result"/>
</div>
)
}
}
num1:对应真实dom num1
num2:对应真实dom num2
10.1.2 ref=函数 (不推荐)
class Cualculator extends React.Component {
add =() => {
let num1 = parseInt(this.num1.value)
let num2 = parseInt(this.num2.value)
let result = num1 +num2
this.result.value = result
}
//ref值是一个函数的时候,此函数会在虚拟dom转为真实dom插入也买你之后执行,参数就是真实dom
render() {
return (
<div>
<input ref={instance => this.num1 = instance} />+<input ref={instance => this.num2 = instance}/><button onClick={this.add}>=</button><input ref={instance => this.result = instance}/>
</div>
)
}
}
10.1.3 ref = React.createRef() (推荐使用)
通过React.createRef() 创建的ref属性有以下几个特点:
当 ref 被传递给 render
中的元素时,对该节点的引用可以在 ref 的 current
属性中被访问。
- 当
ref
属性用于 HTML 元素时,构造函数中使用React.createRef()
创建的ref
接收底层 DOM 元素作为其current
属性。 - 当
ref
属性用于自定义 class 组件时,ref
对象接收组件的挂载实例作为其current
属性。 - 不能在函数组件上使用
ref
属性,因为他们没有实例。若是想用需要经过特殊处理
1.3.1 Html元素
class Cualculator extends React.Component {
constructor(){
super()
this.num1 = React.createRef() //{current:null} current在虚拟dom转为真实dom插入页面之后变成真实dom
this.num2 = React.createRef() //{current:null} current在虚拟dom转为真实dom插入页面之后变成真实dom
this.result = React.createRef() //{current:null} current在虚拟dom转为真实dom插入页面之后变成真实dom
}
add =() => {
let num1 = parseInt(this.num1.current.value)
let num2 = parseInt(this.num2.current.value)
let result = num1 +num2
this.result.current.value = result
}
//ref值是一个函数的时候,此函数会在虚拟dom转为真实dom插入页面之后执行,参数就是真实dom
render() {
return (
<div>
<input ref={this.num1} />+<input ref={this.num2}/>
<button onClick={this.add}>=</button>
<input ref={this.result}/>
</div>
)
}
}
ReactDOM.render(<Cualculator></Cualculator>,document.getElementById('root'))
dom元素作为current属性的值
1.3.2 class组件
class UserName extends React.Component{
constructor(){
super()
this.inputRef = React.createRef()
}
render(){
return (
<input ref={this.inputRef}></input>
)
}
}
class Form extends React.Component{
constructor(){
super()
this.username = React.createRef() //this.username 就是UserName组件的实例 this.username.current = new UserName()
}
getFocus = () => {
this.username.current.inputRef.current.focus() //this.username.current.inputRef.current 获取到组件对应的真实dom节点 就是 input框
}
render(){
return (
<form>
<UserName ref={this.username}/>
<button type="button" onClick={this.getFocus}>让用户名获得焦点</button>
</form>
)
}
}
组件的实例等于current属性的值
1.3.3 函数组件
ref React.createRef()会获取到一个真实dom或者是一个组件实例对象 但是函数组件没有实例,那怎么获取函数组件的ref属性,这个时候就需要特殊处理
function UserName(props,ref) {
return <input ref={ref}></input>
}
const ForwordUsername = React.forwardRef(UserName)
class Form extends React.Component{
constructor(){
super()
this.username = React.createRef() //this.username 就是ForwordUsername组件的实例 this.username.current = new ForwordUsername()
}
getFocus = () => {
this.username.current.focus() //this.username.current.inputRef.current 获取到组件对应的真实dom节点 就是 input框
}
render(){
return (
<form>
<ForwordUsername ref={this.username}/>
<button type="button" onClick={this.getFocus}>让用户名获得焦点</button>
</form>
)
}
}
React.forwardRef会穿透UserName组件,获取到input的真实dom元素。
10.2 React.forwardRef()的底层实现
function UserName(props,ref) {
return <input ref={ref}></input>
}
function forwardRef (functionComponent) {
return class extends React.Component {
render() {
return functionComponent(this.props,this.props.ref2)
}
}
}
const ForwordUsername = forwardRef(UserName) //React.forwardRef返回一个类组件,将这个类组件传给
class Form extends React.Component{
constructor(){
super()
this.username = React.createRef() //this.username 就是UserName组件的实例 this.username.current = new UserName()
}
getFocus = () => {
this.username.current.focus() //this.username.current.inputRef.current 获取到组件对应的真实dom节点 就是 input框
}
render(){
return (
<form>
<ForwordUsername ref2={this.username}/>
<button type="button" onClick={this.getFocus}>让用户名获得焦点</button>
</form>
)
}
}
它是将函数组件转换成了类组件,当然也可以直接返回一个转化之后的函数组件
function UserName(props) {
return <input ref={props.ref2}></input>
}
//函数组件没有this,可以通过
function forwardRef (functionComponent) {
return props => functionComponent(props,props.ref2)
}
const ForwordUsername = forwardRef(UserName) //React.forwardRef返回一个类组件,将这个类组件传给
class Form extends React.Component{
constructor(){
super()
this.username = React.createRef() //this.username 就是UserName组件的实例 this.username.current = new UserName()
}
getFocus = () => {
this.username.current.focus() //this.username.current.inputRef.current 获取到组件对应的真实dom节点 就是 input框
}
render(){
return (
<form>
<ForwordUsername ref2={this.username}/>
<button type="button" onClick={this.getFocus}>让用户名获得焦点</button>
</form>
)
}
}
ReactDOM.render(<Form></Form>,document.getElementById('root'))
10.3 Refs 使用场景
- 处理焦点、文本选择或者媒体的控制
- 触发必要的动画
- 集成第三方 DOM 库
补充:
函数组件执行完毕之后就被释放了,但是会返回一个react元素,这个元素会一直存在,最后会被渲染成真实dom
11.react 调度机制原理
https://developer.51cto.com/art/202102/643992.htm?pc
六、react-redux
https://www.cnblogs.com/liuheng/p/11796819.html
https://segmentfault.com/a/1190000019849834?utm_source=tag-newest
管理不断变化的state是非常困难的:
状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;
当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;
Redux设计思想
它将整个应用状态存储到store里面,组件可以派发(dispatch)修改数据(state)的行为(action)给store,store内部修改之后,其他组件可以通过订阅(subscribe)中的状态state来刷新(render)自己的视图。
画图举栗子:
组件B,C需要根据组件A的数据来修改自己的视图,组件A不能直接通知组件BC,这个时候就可以将数据存储到sore里面,然后A组件向store里面的处理函数派发指令,reducer接收到指令action后修改state数据,组件BC可以通过订阅来修改自己的视图。
Redux应用的三大原则
- 单一数据源
我们可以把Redux的状态管理理解成一个全局对象,那么这个全局对象是唯一的,所有的状态都在全局对象store下进行统一”配置”,这样做也是为了做统一管理,便于调试与维护。 - State是只读的
与React的setState相似,直接改变组件的state是不会触发render进行渲染组件的。同样,在Redux中唯一改变state的方法就是触发action,action是一个用于描述发生了什么的“关键词”,而具体使action在state上更新生效的是reducer,用来描述事件发生的详细过程,reducer充当了发起一个action连接到state的桥梁。这样做的好处是当开发者试图去修改状态时,Redux会记录这个动作是什么类型的、具体完成了什么功能等(更新、传播过程),在调试阶段可以为开发者提供完整的数据流路径。 - Reducer必须是一个纯函数
Reducer用来描述action如何改变state,接收旧的state和action,返回新的state。Reducer内部的执行操作必须是无副作用的,不能对state进行直接修改,当状态发生变化时,需要返回一个全新的对象代表新的state。这样做的好处是,状态的更新是可预测的,另外,这与Redux的比较分发机制相关,阅读Redux判断状态更新的源码部分(combineReducers),发现Redux是对新旧state直接用来进行比较,也就是浅比较,如果我们直接在state对象上进行修改,那么state所分配的内存地址其实是没有变化的,“”是比较对象间的内存地址,因此Redux将不会响应我们的更新。之所以这样处理是避免对象深层次比较所带来的性能损耗(需要递归遍历比较)。
- 单一数据源
Redux的使用过程
1.创建一个对象,作为我们要保存的状态:
2.创建Store来存储这个state ,创建store时必须创建reducer; 我们可以通过 store.getState 来获取当前的state
3.通过action来修改state ,通过dispatch来派发action; 通常action中都会有type属性,也可以携带其他的数据;
4.修改reducer中的处理代码,这里一定要记住,reducer是一个纯函数,不需要直接修改state;
5.可以在派发action之前,监听store的变化。
11.1 手动关联redux
创建store.js
import { createStore} from 'redux';
import reducer from './reducer.js';
const store = createStore(reducer);
export default store;
创建两个组件home与about,并将组件与redux关联
home.js
import React, { PureComponent } from 'react';
import store from '../store';
import {
addAction
} from '../store/actionCreators'
export default class Home extends PureComponent {
constructor(props) {
super(props);
this.state = {
counter: store.getState().counter
}
}
componentDidMount() {
this.unsubscribue = store.subscribe(() => {
this.setState({
counter: store.getState().counter
})
})
}
componentWillUnmount() {
this.unsubscribue();
}
render() {
return (
<div>
<h1>Home</h1>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={e => this.increment()}>+1</button>
<button onClick={e => this.addNumber(5)}>+5</button>
</div>
)
}
increment() {
store.dispatch(addAction(1));
}
addNumber(num) {
store.dispatch(addAction(num));
}
}
about.js
import React, { PureComponent } from 'react';
import store from '../store';
import {
subAction
} from "../store/actionCreators";
export default class About extends PureComponent {
constructor(props) {
super(props);
this.state = {
counter: store.getState().counter
}
}
// 订阅,当store数据发生了变化才会调用render重新渲染
componentDidMount() {
this.unsubscribue = store.subscribe(() => {
this.setState({
counter: store.getState().counter
})
})
}
// 取消订阅
componentWillUnmount() {
this.unsubscribue();
}
render() {
return (
<div>
<hr/>
<h1>About</h1>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={e => this.decrement()}>-1</button>
<button onClick={e => this.subNumber(5)}>-5</button>
</div>
)
}
decrement() {
// 派发事件
store.dispatch(subAction(1));
}
subNumber(num) {
//派发事件
store.dispatch(subAction(num));
}
}
在 componentDidMount 中定义数据的变化,当数据发生变化时重新设置 counter;
在发生点击事件时,调用store的dispatch来派发对应的action;
此时当store数据发生改变,视图也会更新。
上面得例子中每个组件要向使用store必须是每个都自己引入,两个组件在关联redux的过程中有很多重复的代码,如下:
// 赋值
this.state = {
counter: store.getState().counter
}
}
//订阅
componentDidMount() {
this.unsubscribue = store.subscribe(() => {
this.setState({
counter: store.getState().counter
})
})
}
// 取消订阅
componentWillUnmount() {
this.unsubscribue();
}
// 派发事件
increment() {
store.dispatch(addAction(1));
}
addNumber(num) {
store.dispatch(addAction(num));
}
需要将这些重复的代码封装成一个函数在组件中共享。
react提供了一个对象Provider和connect,可以通过这两个对象把react组件和store连接起来,不必再每个都引入。如果想在某个子组件中使用Redux维护的store数据,它必须是包裹在Provider中并且被connect过的组件,Provider的作用类似于提供一个大容器,将组件和Redux进行关联,在这个基础上,connect再进行store的传递。
下面我们自己先来实现一下:
11.2 connect源码实现
创建store
import { createStore, applyMiddleware, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import createSagaMiddleware from 'redux-saga';
import saga from './saga';
import reducer from './reducer.js';
// composeEnhancers函数
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
// 应用一些中间件
// 1.引入thunkMiddleware中间件(上面)
// 2.创建sagaMiddleware中间件
const sagaMiddleware = createSagaMiddleware();
const storeEnhancer = applyMiddleware(thunkMiddleware, sagaMiddleware);
const store = createStore(reducer, composeEnhancers(storeEnhancer));
sagaMiddleware.run(saga);
export default store;
创建connect.js
import React, { PureComponent } from "react";
import store from '../store' // 导入创建好的store
export function connect(mapStateToProps, mapDispachToProp) {
return function enhanceHOC(WrappedComponent) {
class EnhanceComponent extends PureComponent {
constructor(props) {
super(props);
this.state = {
// 根据用户传入的state
storeState: mapStateToProps(store.getState())
}
}
componentDidMount() {
this.unsubscribe = this.context.subscribe(() => {
this.setState({
// 根据用户传入的state
storeState: mapStateToProps(store.getState())
})
})
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
// {...this.props} 是别的调用的地方传入的props
// {...mapStateToProps(store.getState())} {...mapDispachToProp(store.dispatch)}是高阶函数传入的
return <WrappedComponent {...this.props}
{...mapStateToProps(store.getState())}
{...mapDispachToProp(store.dispatch)} />
}
}
}
}
connect返回一个高阶组件
home.js
import React, { PureComponent } from 'react';
import {connect} from '../utils/connect';
import {
incAction,
addAction
} from '../store/actionCreators'
class Home extends PureComponent {
render() {
return (
<div>
<h1>Home</h1>
<h2>当前计数: {this.props.counter}</h2>
<button onClick={e => this.props.increment()}>+1</button>
<button onClick={e => this.props.addNumber(5)}>+5</button>
</div>
)
}
}
// 组件对应的state
const mapStateToProps = state => ({
counter: state.counter
})
// 组件需要派发的dispatch
const mapDispatchToProps = dispatch => ({
increment() {
dispatch(incAction());
},
addNumber(num) {
dispatch(addAction(num));
}
})
export default connect(mapStateToProps, mapDispatchToProps)(Home);
about.js
import React from 'react';
import { connect } from '../utils/connect';
import {
decAction,
subAction
} from "../store/actionCreators";
function About(props) {
return (
<div>
<hr />
<h1>About</h1>
<h2>当前计数: {props.counter}</h2>
<button onClick={e => props.decrement()}>-1</button>
<button onClick={e => props.subNumber(5)}>-5</button>
</div>
)
}
const mapStateToProps = state => {
return {
counter: state.counter
}
};
const mapDispatchToProps = dispatch => {
return {
decrement: function() {
dispatch(decAction());
},
subNumber: function(num) {
dispatch(subAction(num))
}
}
};
export default connect(mapStateToProps, mapDispatchToProps)(About);
上面的connect源码有一个很大的缺陷:依赖我们自己创建的 store,我们应该提供一个Provider,Provider来自于我们 创建的Context,让用户将store作为value传入到connect中即可;
11.3 Provider源码实现
创建上下文context.js
import React from 'react';
const StoreContext = React.createContext();
export {
StoreContext
}
然后用户可以在入口文件中将store传入
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import store from './store';
import { StoreContext } from './utils/context';
import App from './App';
ReactDOM.render(
<StoreContext.Provider value={store}>
<App />
</StoreContext.Provider>,
document.getElementById('root')
);
connect.js
import React, { PureComponent } from "react";
import { StoreContext } from './context';
export function connect(mapStateToProps, mapDispachToProp) {
return function enhanceHOC(WrappedComponent) {
class EnhanceComponent extends PureComponent {
constructor(props, context) {
super(props, context);
this.state = {
// storeState: mapStateToProps(store.getState())
storeState: mapStateToProps(context.getState())
}
}
componentDidMount() {
this.unsubscribe = this.context.subscribe(() => {
this.setState({
// storeState: mapStateToProps(store.getState())
storeState: mapStateToProps(this.context.getState())
})
})
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return <WrappedComponent {...this.props}
{...mapStateToProps(this.context.getState())}
{...mapDispachToProp(this.context.dispatch)} />
}
}
EnhanceComponent.contextType = StoreContext;
return EnhanceComponent;
}
}
在connect中通过this.context获取store中传过来的数据。
原理总结:
通过创建context上下文,然后在入口文件中通过context.Provider将store传入,在connect文件中通过this.context获取store中传过来的数据。
11.4 官方react-redux-connect的使用与源码导读
import React from 'react';
import ReactDOM from 'react-dom';
import store from './store';
// import { StoreContext } from './utils/context';
import { Provider } from 'react-redux';
import App from './App';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
引入import { Provider } from ‘react-redux’;
使用Provider将store值传入进去,可以发现,官方的使用和我们自己封装的connect在使用上有一点不一样的地方,官方是直接使用Provider,我们是 storeContext.Provider value={store},这在本质是没什么区别,看官方源码就知道了,其底层实现还是contxt.Provider,只是导出了Provider而已。源码如下:
11.5 redux中间件
中间件的目的是在dispatch的action和最终达到的reducer之间,扩展一些自己的代码; 比如日志记录、调用异步接口、添加代码调试功能等等;
正常我们的redux是这样的工作流程,dispatch -> action -> reducer ,这相当于是同步操作,由dispatch触发action之后直接去reducer执行相应的操作。但有时候我们会实现一些异步任务,像点击按钮 -> 获取服务器数据 ->渲染视图,这个时候就需要引入中间件改变redux同步执行流程,形成异步流程来实现我们的任务。有了中间件redux的工作流程就是action -> 中间件 -> reducer ,点击按钮就相当于dispatch 触发action,接着就是服务器获取数据middlewares执行,成功获取数据后触发reducer对应的操作,更新需要渲染的视图数据。
中间件的机制就是改变数据流,实现异步acation。
11.5.1 redux-thunk中间件
thunk这个中间件就是用来给store派发函数类型的actions的。
默认情况下的dispatch(action),action需要是一个JavaScript的对象;redux-thunk可以让dispatch(action函数),action可以是一个函数; 该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数; dispatch函数用于我们之后再次派发action; getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态。
使用redux-thunk发送异步请求
1.在store中导入中间件
import { createStore, applyMiddleware, compose } from 'redux';
// 1.引入thunkMiddleware中间件
import thunkMiddleware from 'redux-thunk';
import reducer from './reducer.js';
// 2.使用applyMiddleware应用中间件
const storeEnhancer = applyMiddleware(thunkMiddleware);
// 创建中间件
const store = createStore(reducer, storeEnhancer);
export default store;
在创建store时传入应用了middleware的enhance函数;
通过applyMiddleware来结合多个Middleware, 返回一个enhancer;
将enhancer作为第二个参数传入到createStore中;
2.reducer.js
import {
ADD_NUMBER,
SUB_NUMBER,
INCREMENT,
DECREMENT,
CHANGE_BANNERS,
CHANGE_RECOMMEND
} from './constants.js';
const defaultState = {
counter: 0,
banners: [],
recommends: []
}
function reducer(state = defaultState, action) {
switch (action.type) {
case ADD_NUMBER:
return { ...state, counter: state.counter + action.num };
case SUB_NUMBER:
return { ...state, counter: state.counter - action.num };
case INCREMENT:
return { ...state, counter: state.counter + 1 };
case DECREMENT:
return { ...state, counter: state.counter - 1 };
case CHANGE_BANNERS:
return { ...state, banners: action.banners };
case CHANGE_RECOMMEND:
return { ...state, recommends: action.recommends };
default:
return state;
}
}
export default reducer;
3.home组件
import React, { PureComponent } from 'react';
// import {connect} from '../utils/connect';
import { connect } from 'react-redux';
// 导入action
import {
incAction,
addAction,
getHomeMultidataAction
} from '../store/actionCreators'
class Home extends PureComponent {
componentDidMount() {
this.props.getHomeMultidata();
}
render() {
return (
<div>
<h1>Home</h1>
<h2>当前计数: {this.props.counter}</h2>
<button onClick={e => this.props.increment()}>+1</button>
<button onClick={e => this.props.addNumber(5)}>+5</button>
</div>
)
}
}
const mapStateToProps = state => ({
counter: state.counter
})
const mapDispatchToProps = dispatch => ({
increment() {
dispatch(incAction());
},
addNumber(num) {
dispatch(addAction(num));
},
// 派发异步事件
getHomeMultidata() {
dispatch(getHomeMultidataAction); //参数是函数
}
})
export default connect(mapStateToProps, mapDispatchToProps)(Home);
在组件中派发了异步事件。因为导入了redux-thunx中间件,所以dispatch 的 action可以是个函数。
4.actionCreators.js
import axios from 'axios';
import {
ADD_NUMBER,
SUB_NUMBER,
INCREMENT,
DECREMENT,
CHANGE_BANNERS,
CHANGE_RECOMMEND,
FETCH_HOME_MULTIDATA
} from './constants.js';
export const addAction = num => ({
type: ADD_NUMBER,
num
});
export const subAction = num => ({
type: SUB_NUMBER,
num
});
export const incAction = () => ({
type: INCREMENT
});
export const decAction = () => ({
type: DECREMENT
});
// 轮播图和推荐的action
export const changeBannersAction = (banners) => ({
type: CHANGE_BANNERS,
banners
});
export const changeRecommendAction = (recommends) => ({
type: CHANGE_RECOMMEND,
recommends
});
// redux-thunk中定义的action函数
export const getHomeMultidataAction = (dispatch, getState) => {
axios({
url: "http://123.207.32.32:8000/home/multidata",
}).then(res => {
const data = res.data.data;
dispatch(changeBannersAction(data.banner.list)); // 再次派发action
dispatch(changeRecommendAction(data.recommend.list)); // 再次派发action
})
}
接着在action中定义了请求函数。
该函数在dispatch之后会被调用,并且会传给这个函数一个dispatch函数和getState函数; dispatch函数用于之后再次派发action; getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态。
11.5.2 redux-saga
saga中间件使用了ES6的generator语法,
redux-saga其实是在dispatch和reducer之间架设了一层异步处理层,专门来处理异步任务。 在sagaMiddleware初始化run时,对入口的saga进行了自执行,开始了对action的监听。遇到yield的effect时交由对应的runEffect执行,命中action时则派生对应的saga任务,这就是redux-saga大概的原理。
11.6 中间件的源码实现
11.6.1 打印日志需求
//1.备份原生的dispatch方法
const next = store.dispatch;
//2.重写dispatch方法 做一些额外操作
store.dispatch = function (action) {
console.log("dispatch前---dispatching action:", action);
//触发原生dispatch方法
next(action);
console.log("dispatch后---new state:", store.getState());
}
在重写dispatch方法之前先备份原生的dispatch方法,这个写法和vue中监听数组的变化方式很相似。
这种写法是直接对dispatch进行重写,不利于维护,当有多个中间件的时候也没法调用,所以要改成下面的写法
function patchLogging(store) {
const next = store.dispatch;
function dispatchAndLogging(action) {
console.log("dispatch前---dispatching action:", action);
next(action);
console.log("dispatch后---new state:", store.getState());
}
// store.dispatch = dispatchAndLogging;
return dispatchAndLogging;
}
11.6.2 thunk中间件
function patchThunk(store) {
const next = store.dispatch;
function dispatchAndThunk(action) {
if (typeof action === "function") { // 函数就执行函数
action(store.dispatch, store.getState)
} else { // 对象就就将对象作为参数传入
next(action);
}
}
// store.dispatch = dispatchAndThunk;
return dispatchAndThunk;
}
11.6.3 applyMiddleware的实现
function applyMiddlewares(...middlewares) {
// const newMiddleware = [...middlewares];
middlewares.forEach(middleware => {
store.dispatch = middleware(store);
})
}
applyMiddlewares(patchLogging, patchThunk);
11.7 reducer
reducer 就是一个函数,接收旧的 state 和 action,返回新的 state。
11.7.1 Reducer代码拆分
如果将所有的状态都放到一个reducer中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。
因此,我们可以对reducer进行拆分:
我们先抽取一个对counter处理的reducer;
再抽取一个对home处理的reducer;
最后将它们合并起来;
拆分后的代码结构如下:
counter reducer.js
import {
ADD_NUMBER,
SUB_NUMBER,
INCREMENT,
DECREMENT
} from './constants.js';
// 拆分counterReducer
const initialCounterState = {
counter: 0
}
function counterReducer(state = initialCounterState, action) {
switch (action.type) {
case ADD_NUMBER:
return { ...state, counter: state.counter + action.num };
case SUB_NUMBER:
return { ...state, counter: state.counter - action.num };
case INCREMENT:
return { ...state, counter: state.counter + 1 };
case DECREMENT:
return { ...state, counter: state.counter - 1 };
default:
return state;
}
}
export default counterReducer;
home reducer.js
import {
CHANGE_BANNERS,
CHANGE_RECOMMEND
} from './constants.js';
// 拆分homeReducer
const initialHomeState = {
banners: [],
recommends: []
}
function homeReducer(state = initialHomeState, action) {
switch (action.type) {
case CHANGE_BANNERS:
return { ...state, banners: action.banners };
case CHANGE_RECOMMEND:
return { ...state, recommends: action.recommends };
default:
return state;
}
}
export default homeReducer;
11.7.2 合并reducer
import { reducer as counterReducer } from './counter';
import { reducer as homeReducer } from './home';
import { combineReducers } from 'redux';
function reducer(state = {}, action) {
return {
counterInfo: counterReducer(state.counterInfo, action),
homeInfo: homeReducer(state.homeInfo, action)
}
}
export default reducer;
目前我们合并的方式是通过每次调用reducer函数自己来返回一个新的对象。
事实上,redux给我们提供了一个combineReducers函数可以方便的让我们对多个reducer进行合并:
combineReducers(reducers)方法,把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer 调用 createStore。
合并后的 reducer 可以调用各个子 reducer,并把它们的结果合并成一个 state 对象。state 对象的结构由传入的多个 reducer 的 key 决定。
import { reducer as counterReducer } from './counter';
import { reducer as homeReducer } from './home';
import { combineReducers } from 'redux';
const reducer = combineReducers({
counterInfo: counterReducer,
homeInfo: homeReducer
});
export default reducer;
11.7.3 combineReducers源码
redux性能问题
状态树层级
数据规范化
查询层级不能太深
内存占用
七、react-router
12.1 前端路由
整个SPA(单页面富应用)应用只有实际上只有一个页面,当URL发生改变时,并不会从服务器请求新的静态资源; 而是通过JavaScript监听URL的改变,并且根据URL的不同去渲染新的页面。
前端路由维护着URL和渲染页面的映射关系;路由可以根据不同的URL,最终让我们的框架(比如Vue、React、Angular)去渲染不同的组件; 最终我们在页面上看到的实际就是渲染的一个个组件页面。
URL发生变化,同时不引起页面的刷新有两个办法:
- 通过URL的hash改变URL;
- 通过HTML5中的history模式修改URL;
当监听到URL发生变化时,我们可以通过自己判断当前的URL,决定到底渲染什么样的内容。
12.1 URL的hash
使用window.location.hash属性及窗口的onhashchange事件,可以实现监听浏览器地址hash值变化,执行相应的js切换网页。下面具体介绍几个使用过程中必须理解的要点:
hash指的是地址中#号以及后面的字符,也称为散列值。hash也称作锚点,本身是用来做页面跳转定位的。如http://localhost/index.html#abc,这里的#abc就是hash;
散列值是不会随请求发送到服务器端的,所以改变hash,不会重新加载页面;
监听 window 的 hashchange 事件,当散列值改变时,可以通过 location.hash 来获取和设置hash值;
location.hash值的变化会直接反应到浏览器地址栏;
触发hashchange事件的几种情况:
浏览器地址栏散列值的变化(包括浏览器的前进、后退)会触发window.location.hash值的变化,从而触发onhashchange事件;
当浏览器地址栏中URL包含哈希如 http://www.baidu.com/#home,这时按下输入,浏览器发送http://www.baidu.com/请求至服务器,请求完毕之后设置散列值为#home,进而触发onhashchange事件;
当只改变浏览器地址栏URL的哈希部分,这时按下回车,浏览器不会发送任何请求至服务器,这时发生的只是设置散列值新修改的哈希值,并触发onhashchange事件;
html中标签的属性 href 可以设置为页面的元素ID如 #top,当点击该链接时页面跳转至该id元素所在区域,同时浏览器自动设置 window.location.hash 属性,地址栏中的哈希值也会发生改变,并触发onhashchange事件;
URL的hash也就是锚点(#), 本质上是改变window.location的href属性;
我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新;
下面简单实现一下这两种方法。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<a href="#/home">首页</a>
<a href="#/about">关于</a>
<div class="router-view"></div>
</div>
<script>
// 获取router-view的DOM
const routerViewEl = document.getElementsByClassName("router-view")[0];
// 监听URL的改变hashchange事件
window.addEventListener("hashchange", () => {
switch (location.hash) {
case "#/home":
routerViewEl.innerHTML = "首页";
break;
case "#/about":
routerViewEl.innerHTML = "关于";
break;
default:
routerViewEl.innerHTML = "";
}
})
</script>
</body>
</html>
不同的hash值会渲染不同的页面内容。但是不会刷新页面,也就是不会向后端发送请求。
12.2 HTML5的history
history接口是HTML5新增的, 它有l六种模式改变URL而不刷新页面:
- replaceState:替换原来的路径;
- pushState:使用新的路径;
- popState:路径的回退;
- go:向前或向后改变路径;
- forward:向前改变路径;
- back:向后改变路径;
每当 history 对象出现变化时,就会触发 popstate 事件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<a href="/home">首页</a>
<a href="/about">关于</a>
<div class="router-view"></div>
</div>
<script>
// 1.获取router-view的DOM
const routerViewEl = document.getElementsByClassName("router-view")[0];
// 获取所有的a元素, 自己来监听a元素的改变,阻止a的默认行为,a元素默认会跳转到新的页面,强制刷新页面。
const aEls = document.getElementsByTagName("a");
for (let el of aEls) {
el.addEventListener("click", e => {
e.preventDefault(); // 阻止默认跳转行为
const href = el.getAttribute("href");
history.pushState({}, "", href); // 切换路由
urlChange(); // 渲染不同的页面
})
}
// 执行返回操作时, 依然来到urlChange
window.addEventListener('popstate',urlChange);
// 监听URL的改变
function urlChange() {
// location.pathname 拿到当前路径名称
switch (location.pathname) {
case "/home":
routerViewEl.innerHTML = "首页";
break;
case "/about":
routerViewEl.innerHTML = "关于";
break;
default:
routerViewEl.innerHTML = "";
}
}
</script>
</body>
</html>
Link 与 a标签的区别
对比,Link组件避免了不必要的重渲染
A — 通过标签实现页面跳转:(图中的例子将会在下面详细解答)
—>—>
B —通过组件实现页面跳转:
———>
react-router:只更新变化的部分从而减少DOM性能消耗
react的创新之处在于,它利用虚拟DOM的概念和diff算法实现了对页面的”按需更新”,react-router很好地继承了这一点,譬如上图所示,导航组件和三个Tab组件(通过…,通过…,通过…)的重渲染是我们不希望看到的,因为无论跳转到页面一或是页面二,它只需要渲染一次就够了。组件帮助我们实现了这个愿望,反观标签,每次跳转都重渲染了导航组件和Tab组件试想一下,在一个浩大的项目里,这多么可怕!我们的”渲染”做了许多”无用功”,而且消耗了大量弥足珍贵的DOM性能!
12.2 react-router实现原理
https://segmentfault.com/a/1190000004527878
react-router有两种模式:
- BrowserRouter使用history模式,是利用history库里面的createBrowserHistory这个方法创建。
- HashRouter使用hash模式,是利用history库里面的createHashHistory这个方法创建。
BrowserRouter源码:
HashRouter源码:
react路由的实现时基于history库实现的,这个history库并不是window.history,而是在window.history的基础上又进行了一层封装实现的。我们可以看看这个库的源码:
createHashHistory核心源码:
下面简单实现一下核心代码:
12.2.1 History整体介绍
history是一个独立的第三方js库,可以用来兼容在不同浏览器、不同环境下对历史记录的管理,拥有统一的API。具体来说里面的history分为三类:
- 老浏览器的history: 主要通过hash来实现,对应
createHashHistory
- 高版本浏览器: 通过html5里面的history,对应
createBrowserHistory
- node环境下: 主要存储在memeory里面,对应
createMemoryHistory
上面针对不同的环境提供了三个API,但是三个API有一些共性的操作:
var history = {
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref: createHref,
push: push,
replace: replace,
go: go,
goBack: goBack,
goForward: goForward,
block: block,
listen: listen
};
return history;
这些方式是history内部最基础的方法,createHashHistory
、createBrowserHistory
、createMemoryHistory
只是覆盖其中的某些方法而已。其中需要注意的是,此时的location跟浏览器原生的location是不相同的,最大的区别就在于里面多了key
字段,history
内部通过key
来进行location
的操作。
function createLocation() {
return {
pathname, // url的基本路径
search, // 查询字段
hash, // url中的hash值
state, // url对应的state字段
action, // 分为 push、replace、pop三种
key // 生成方法为: Math.random().toString(36).substr(2, length)
}
}
12.2.2 内部解析
三个API的大致的技术实现如下:
createBrowserHistory
: 利用HTML5里面的historycreateHashHistory
: 通过hash来存储在不同状态下的history信息createMemoryHistory
: 在内存中进行历史记录的存储
执行URL前进
createBrowserHistory
: pushState、replaceStatecreateHashHistory
:location.hash=***
location.replace()
createMemoryHistory
: 在内存中进行历史记录的存储
伪代码实现如下:
// createBrowserHistory(HTML5)中的前进实现
function push(location) {
...
const historyState = { key };
...
if (location.action === 'PUSH') ) {
window.history.pushState(historyState, null, path);
} else {
window.history.replaceState(historyState, null, path)
}
}
// createHashHistory的内部实现
function push(location) {
...
if (location.action === 'PUSH') ) {
window.location.hash = path;
} else {
window.location.replace(
window.location.pathname + window.location.search + '#' + path
);
}
}
// createMemoryHistory的内部实现
entries = [];
function push(location) {
...
switch (location.action) {
case 'PUSH':
entries.push(location);
break;
case 'REPLACE':
entries[current] = location;
break;
}
}
检测URL回退
createBrowserHistory
:popstate
createHashHistory
:hashchange
createMemoryHistory
: 因为是在内存中操作,跟浏览器没有关系,不涉及UI层面的事情,所以可以直接进行历史信息的回退
伪代码实现如下:
// createBrowserHistory(HTML5)中的后退检测
function PopStateEvent({ transitionTo }) {
function popStateListener(event) {
...
transitionTo( getCurrentLocation(event.state) );
}
addEventListener(window, 'popstate', popStateListener);
...
}
// createHashHistory的后退检测
function HashChangeEvent({ transitionTo }) {
function hashChangeListener(event) {
...
transitionTo( getCurrentLocation(event.state) );
}
addEventListener(window, 'hashchange', hashChangeListener);
...
}
// createMemoryHistory的内部实现
function go(n) {
if (n) {
...
current += n;
const currentLocation = getCurrentLocation();
// change action to POP
history.transitionTo({ ...currentLocation, action: POP });
}
}
12.3 补充:手动路由跳转问题
手动路由跳转的前提:必须获取到history对象。这个history对象并不是window.history,而是Router传递过来的。下面看一个例子:
about.js
import React, { PureComponent } from 'react'
import { NavLink, Switch, Route } from 'react-router-dom';
import { renderRoutes, matchRoutes } from 'react-router-config';
export function AboutHisotry(props) {
return <h2>企业成立于2000年, 拥有悠久的历史文化</h2>
}
export function AboutCulture(props) {
return <h2>创新/发展/共赢</h2>
}
export function AboutContact(props) {
return <h2>联系电话: 020-68888888</h2>
}
export function AboutJoin(props) {
return <h2>投递简历到aaaa@123.com</h2>
}
export default class About extends PureComponent {
render() {
console.log(this.props.route);
const branch = matchRoutes(this.props.route.routes, "/about");
console.log(branch);
return (
<div>
<NavLink exact to="/about" activeClassName="about-active">企业历史</NavLink>
<NavLink exact to="/about/culture" activeClassName="about-active">企业文化</NavLink>
<NavLink exact to="/about/contact" activeClassName="about-active">联系我们</NavLink>
<button onClick={e => this.jumpToJoin()}>加入我们</button>
<Switch>
<Route exact path="/about" component={AboutHisotry}/>
<Route path="/about/culture" component={AboutCulture}/>
<Route path="/about/contact" component={AboutContact}/>
<Route path="/about/join" component={AboutJoin}/>
</Switch>
</div>
)
}
// 手动点击button按钮跳转
jumpToJoin() {
// console.log(this.props.history);
// console.log(this.props.location);
// console.log(this.props.match);
//这个history对象并不是window对象,而是一个在window.history基础上封装的第三方库
this.props.history.push("/about/join");
}
}
app.js
import React from 'react';
import './App.css';
import {
BrowserRouter,
Route,
Link,
}from 'react-router-dom'
import Home from './pages/home';
import About from './pages/about';
import Profile from './pages/profile';
function App() {
return (
<div>
<BrowserRouter>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/profile">我的</Link>
<Route exact path="/" component={Home}></Route>
<Route path="/about" component={About}></Route>
<Route path="/profile" component={Profile}></Route>
</BrowserRouter>
</div>
);
}
export default App;
此时我们的组件是可以获取到history对象的,因为我们的About组件是被BrowserRouter包裹渲染的,Router内部会将这些属性传递给要渲染的子组件。但如果某个组件并没有被路由容器包裹,就获取不到history对象。如下:
import React, { PureComponent } from 'react';
import './App.css';
import {
BrowserRouter,
Route,
Link,
}from 'react-router-dom'
import Home from './pages/home';
import About from './pages/about';
import Profile from './pages/profile';
class App extends PureComponent {
render(){
return (
<div>
<BrowserRouter>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/profile">我的</Link>
<button onClick={e => this.jumpToProduct()}>商品</button>
<Route exact path="/" component={Home}></Route>
<Route path="/about" component={About}></Route>
<Route path="/profile" component={Profile}></Route>
</BrowserRouter>
</div>
);
}
jumpToProduct() {
this.props.history.push("/product"); // undefined
}
}
export default App;
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(
<React>
<App />
</React>,
document.getElementById('root')
);
此时在App组件里面是拿不到history属性的,因为App组件渲染的时候没有包裹在路由容器里里面。
改写:
1.App组件必须包裹在Router组件之内:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import {BrowserRouter} from 'react-router-dom'
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
2.App组件使用withRouter高阶组件包裹:
import React, { PureComponent } from 'react';
import './App.css';
import {
BrowserRouter,
Route,
Link,
}from 'react-router-dom'
import Home from './pages/home';
import About from './pages/about';
import Profile from './pages/profile';
class App extends PureComponent {
render(){
return (
<div>
<BrowserRouter>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/profile">我的</Link>
<button onClick={e => this.jumpToProduct()}>商品</button>
<Route exact path="/" component={Home}></Route>
<Route path="/about" component={About}></Route>
<Route path="/profile" component={Profile}></Route>
</BrowserRouter>
</div>
);
}
jumpToProduct() {
this.props.history.push("/product"); // undefined
}
}
export default withRouter(App);
这样就可以拿到history属性了。
从源码角度看看为什么被路由包裹的组件能获取到this.props.history 、this.props.location 、this.props.match 这些属性的。
拿到了BrowserRouter的原型,在原型上添加了render方法,并将history、children属性传递给Router
Router通过context.Provider为它的子组件提供history、location、match等属性。
通过withRouter包裹的组件,可以通过context.Consumer 使用 Router提供的属性。
12.4 react-router-config
上面我们的路由是通过接使用Route组件,并且添加属性来完成的。
这样的方式会让路由变得非常混乱,我们希望将所有的路由配置放到一个地方进行集中管理:
这个时候可以使用react-router-config来完成;
安装react-router-config :yarn add react-router-config
配置路由映射的关系数组
import Home from '../pages/home';
import About, { AboutHisotry, AboutCulture, AboutContact, AboutJoin } from '../pages/about';
import Profile from '../pages/profile';
import User from '../pages/user';
const routes = [
{
path: "/",
exact: true,
component: Home
},
{
path: "/about",
component: About,
routes: [
{
path: "/about",
exact: true,
component: AboutHisotry
},
{
path: "/about/culture",
component: AboutCulture
},
{
path: "/about/contact",
component: AboutContact
},
{
path: "/about/join",
component: AboutJoin
},
]
},
{
path: "/profile",
component: Profile
},
{
path: "/user",
component: User
}
]
export default routes;
使用renderRoutes函数完成配置
import React, { PureComponent } from 'react';
import { renderRoutes } from 'react-router-config';
import routes from './router';
import {
BrowserRouter,
Link,
Route,
NavLink,
Switch,
withRouter
} from 'react-router-dom';
import './App.css';
class App extends PureComponent {
render() {
const id = "123";
const info = {name: "why", age: 18, height: 1.88};
return (
<div>
<NavLink exact to="/" activeClassName="link-active">首页</NavLink>
<NavLink to="/about" activeClassName="link-active">关于</NavLink>
<NavLink to="/profile" activeClassName="link-active">我的</NavLink>
<NavLink to="/abc" activeClassName="link-active">abc</NavLink>
<NavLink to="/user" activeClassName="link-active">用户</NavLink>
<NavLink to={`/detail/${id}`} activeClassName="link-active">详情</NavLink>
<NavLink to={`/detail2?name=why&age=18`} activeClassName="link-active">详情2</NavLink>
<NavLink to={{
pathname: "/detail3",
search: "name=abc",
state: info
}}
activeClassName="link-active">
详情3
</NavLink>
<button onClick={e => this.jumpToProduct()}>商品</button>
{/* 2.Switch组件的作用: 路径和组件之间的映射关系 */}
{/* <Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/profile" component={Profile} />
<Route path="/:id" component={User} />
<Route path="/user" component={User} />
<Route path="/login" component={Login} />
<Route path="/product" component={Product} />
<Route path="/detail/:id" component={Detail} />
<Route path="/detail2" component={Detail2} />
<Route path="/detail3" component={Detail3} />
<Route component={NoMatch} /> // NoMatch所有的都可以匹配
</Switch> */}
{/* 使用renderRoutes代替上面的Switch Route配置*/}
{renderRoutes(routes)}
</div>
)
}
// 只有通过路由包裹(BrowserRouter)渲染的组件才有props.history属性,而此时的APP组件是通过最普通的方式进行渲染的,<React.StrictMode><App /></React.StrictMode>,所以没有props.history属性
// 改写:1.App组件必须包裹在Router组件之内:<BrowserRouter><App/></BrowserRouter>,App组件使用withRouter高阶组件包裹:export default withRouter(App);
jumpToProduct() {
this.props.history.push("/product");
}
}
export default withRouter(App);
上面的路由映射表中about页面也有路由,在about页面配置如下:
import React, { PureComponent } from 'react'
import { NavLink, Switch, Route } from 'react-router-dom';
import { renderRoutes, matchRoutes } from 'react-router-config';
export function AboutHisotry(props) {
return <h2>企业成立于2000年, 拥有悠久的历史文化</h2>
}
export function AboutCulture(props) {
return <h2>创新/发展/共赢</h2>
}
export function AboutContact(props) {
return <h2>联系电话: 020-68888888</h2>
}
export function AboutJoin(props) {
return <h2>投递简历到aaaa@123.com</h2>
}
export default class About extends PureComponent {
render() {
console.log(this.props.route);
const branch = matchRoutes(this.props.route.routes, "/about");
console.log(branch);
return (
<div>
<NavLink exact to="/about" activeClassName="about-active">企业历史</NavLink>
<NavLink exact to="/about/culture" activeClassName="about-active">企业文化</NavLink>
<NavLink exact to="/about/contact" activeClassName="about-active">联系我们</NavLink>
<button onClick={e => this.jumpToJoin()}>加入我们</button>
{/* <Switch>
<Route exact path="/about" component={AboutHisotry}/>
<Route path="/about/culture" component={AboutCulture}/>
<Route path="/about/contact" component={AboutContact}/>
<Route path="/about/join" component={AboutJoin}/>
</Switch> */}
{renderRoutes(this.props.route.routes)}
</div>
)
}
// 手动点击button按钮跳转
jumpToJoin() {
// console.log(this.props.history);
// console.log(this.props.location);
// console.log(this.props.match);
//这个history对象并不是window对象
this.props.history.push("/about/join");
}
}
需要通过this.props.route.routes
拿到about页面的路由映射表,然后通过{renderRoutes(this.props.route.routes)}来配置
源码理解
通过源码我们来了解一下这样做的原理:
/**
*
* @param {*} routes 就是我们传入的路由映射表
* @param {*} extraProps 传入的其他属性
* @param {*} switchProps 传入的switch属性
*/
function renderRoutes(routes, extraProps, switchProps) {
//三元运算符判断有没有传入routes,没有就什么都不渲染,返回null,
//传入了就创建Switch,并且通过map遍历每一个routes选项,创建对应的Route标签
return routes ? React.createElement(reactRouter.Switch, switchProps, routes.map(function (route, i) {
return React.createElement(reactRouter.Route, {
key: route.key || i,
path: route.path,
exact: route.exact,
strict: route.strict,
render: function render(props) {
// 每一个route如果有render,就进行合并操作,并将route作为属性传递给对应组件
return route.render ? route.render(_extends({}, props, {}, extraProps, {
route: route
// 如果没有render函数,就根据传入的路由的component,通过React.createElement创建对应的组件,并将route作为属性传递给对应组件,
//所以我们可以在about页面通过this.props.route.routes拿到对应的子路由
})) : React.createElement(route.component, _extends({}, props, extraProps, {
route: route
}));
}
});
})) : null;
}
可以看到renderRoutes
就是将传入的路由映射表转换成Switch包裹的Route组件的形式,和上面我们写的路由是一样的。
结合路由映射表和路由Route组件来理解一下:
八、Hooks
Hook 是 React 16.8 的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期)。
13.1 React为什么要搞一个Hooks
我们都知道React的核心思想就是,将一个页面拆分成一堆独立的,可以复用的组件,并且用自上而下的单向数据流的形式将这些组件串联起来。但如果在大型的项目中使用React,那么我们就会发现项目中实际上很多React组件冗长且难以复用。尤其是那些写成class的组件,他们本身本来就包含了状态(state),所以复用这类组件就变得很麻烦。
那之前,官方解决这个问题的办法是使用渲染属性(Render Props) 和高阶组件(Highter-Order Components)
渲染属性:渲染属性指的是使用一个值为函数的prop来传递需要动态渲染的nodes或组件。
高阶组件:说白了就是一个函数接受一个组件作为参数,经过一系列加工后,最后返回一个新的组件。
这两种方法都有各自的缺点:
高阶组件的开发对开发者不友好:开发者(特别是初级开发者)需要花费一段时间才能搞懂其中的原理并且适应它的写法。如果你使用高阶组件已经很久了,你看到这个说法可能会有些不以为然。可是我相信你在最开始接触高阶组件的时候肯定也花了一段时间才能搞懂它的原理,而且从上面的例子来看高阶组件其实是十分笨重的。试想一下,某天你的项目来了一个React新手,估计他也得花费一段时间才能理解你写的那些高阶组件代码吧。
高阶组件之间组合性差:使用过高阶组件的同学一定试过由于要为组件添加不同的功能,我们要为同一个组件嵌套多个高阶组件,例如这样的代码:
withAuth(withRouter(withUserStatus(UserDetail)))
。这种嵌套写法的高阶组件可能会导致很多问题,其中一个就是props丢失的问题,例如withAuth传递给UserDetail的某个prop可能在withUserStatus组件里面丢失或者被覆盖了。如果你使用的高阶组件都是自己写的话还好,因为调试和修改起来都比较简单,如果你使用的是第三方的库的话就很头痛了。
还有其他一些缺点:
- class组件可以定义自己的state,用来保存组件自己内部的状态; 函数式组件不可以,因为函数每次调用都会产生新的临时变量
- class组件有自己的生命周期,我们可以在对应的生命周期中完成自己的逻辑; 比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次; 函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求;
- class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等; 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次;
13.2 useState
useState
是一个 Hook,允许我们将 React state(状态) 添加到函数式组件中。解决了函数组件没有state(状态)的情况。
useState来自react,需要从react中导入,它是一个hook;
参数:
初始化值,如果不设置为undefined;
返回值:是一个数组,包含两个元素;元素一:当前状态的值(第一调用为初始化值); 元素二:设置状态值的函数;
下面的例子:
class组件中使用state
import React, { PureComponent } from 'react'
export default class CounterClass extends PureComponent {
constructor(props) {
super(props);
this.state = {
counter: 0
}
}
render() {
console.log("class counter渲染");
return (
<div>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={e => this.increment()}>+1</button>
<button onClick={e => this.decrement()}>-1</button>
</div>
)
}
increment() {
this.setState({counter: this.state.counter + 1})
}
decrement() {
this.setState({counter: this.state.counter - 1})
}
}
函数式组件中使用state
import React, { useState} from 'react'
export default function CouterHook() {
console.log('CouterHook被渲染')
const [count, setCount] = useState(0)
// 调用setCount 会做两件事:1.修改count的值,2.调用render函数重新渲染组件
return (
<div>
<h2>{count}</h2>
<button onClick={ e => setCount(count + 1)}>+</button>
<button onClick={ e => setCount(count -1)}>-</button>
</div>
)
}
可以看到函数式组件代码更见简洁。
调用setCount 会做两件事:1.修改count的值,2.调用render函数重新渲染组件
13.2 Effect Hook
Effect Hook 可以让我们来完成一些类似于class中生命周期的功能; 类似于网络请求、手动更新DOM、一些事件的监听等。比如页面page改变发送请求list数据等。
通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作;
import React, { useState, useEffect } from 'react'
export default function HookCounterChangeTitle() {
const [counter, setCounter] = useState(0);
// 等到页面渲染完成执行,不论是第一次渲染还是组件更新,都会执行,相当于合并了class组件中的 componentDidMount、 componentDidUpdate 两个声明周期
useEffect(() => {
document.title = counter;
})
return (
<div>
<h2>当前计数: {counter}</h2>
<button onClick={e => setCounter(counter + 1)}>+1</button>
</div>
)
}
useEffect要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数;
默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数;
清除Effect:
在class组件的编写过程中,某些副作用的代码,我们需要在componentWillUnmount中进行清除:
在Effect Hook中我们可以通过给回调函数传入另一个回调函数来清除Effect。
点击按钮切换h2的显示与隐藏。
import React, { useEffect, useState } from 'react'
export default function EffectHookCancelDemo() {
const [count, setCount] = useState(0);
const [show, setShow] = useState(true);
useEffect(() => {
console.log("订阅一些事件");
//组件发生更新就会执行这个回调函数
return () => {
console.log("取消订阅事件")
}
}, []);
return (
<div>
{show && <h2>EffectHookCancelDemo</h2>}
<h2>{count}</h2>
<button onClick={e => setCount(count + 1)}>+1</button>
<button onClick={e => setShow(!show)}>切换</button>
</div>
)
}
清除函数在从 UI 中删除组件之前会运行,以防止内存泄漏。
多个Effect一起使用:
import React, { useState, useEffect } from 'react'
export default function MultiEffectHookDemo() {
const [count, setCount] = useState(0);
const [isLogin, setIsLogin] = useState(true);
// useEffect默认在组件发生更新的时候会执行
// [count] 表示在count发生改变的时候会执行
useEffect(() => {
console.log("修改DOM", count);
}, [count]);
// [] 表示不依赖任何东西,只在第一次渲染的时候执行一次
useEffect(() => {
console.log("订阅事件");
}, []);
// [] 表示不依赖任何东西,只在第一次渲染的时候执行一次
useEffect(() => {
console.log("网络请求");
}, []);
return (
<div>
<h2>MultiEffectHookDemo</h2>
<h2>{count}</h2>
<button onClick={e => setCount(count + 1)}>+1</button>
<h2>{isLogin ? "coderwhy": "未登录"}</h2>
<button onClick={e => setIsLogin(!isLogin)}>登录/注销</button>
</div>
)
}
13.3 useContext
在之前的开发中,我们要在组件中使用共享的Context有两种方式:
类组件可以通过 类名.contextType = MyContext方式,在类中获取context;
多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context;
但是多个Context共享时的方式会存在大量的嵌套:,Context Hook允许我们通过Hook来直接获取某个Context的值;
export const UserContext = createContext();
export const ThemeContext = createContext();
import ContextHookDemo from 'somewhere';
export default function App() {
return (
<div>
<UserContext.Provider value={{name: "why", age: 18}}>
<ThemeContext.Provider value={{fontSize: "30px", color: "red"}}>
<ContextHookDemo/>
</ThemeContext.Provider>
</UserContext.Provider>
<button onClick={e => setShow(!show)}>切换</button>
</div>
)
}
import React, { useContext } from 'react';
import { UserContext, ThemeContext } from "../App";
export default function ContextHookDemo(props) {
const user = useContext(UserContext);
const theme = useContext(ThemeContext);
console.log(user, theme);
return (
// 以前的做法
// <div>
// <UserContext.Consumer>
// {
// user => {}
// }
// </UserContext.Consumer>
// </div>
//useContext的写法
<div>
<h2>{user}, {theme}</h2>
</div>
)
}
13.4 useReducer
useReducer仅仅是useState的一种替代方案:
在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分;
或者这次修改的state需要依赖之前的state时,也可以使用;
//外界仅仅是共享reducer函数,并不是共享state,各个组件可以将自己的state传入,互相不影响
export default function reducer(state, action) {
switch(action.type) {
case "increment":
return {...state, counter: state.counter + 1};
case "decrement":
return {...state, counter: state.counter - 1};
default:
return state;
}
}
import React, { useState, useReducer } from 'react';
import reducer from './reducer';
export function Home() {
// const [count, setCount] = useState(0);
//dispatch派发action给reducer修改state的值,useReducer的第二个参数是初始化值
const [state, dispatch] = useReducer(reducer, {counter: 0});
return (
<div>
<h2>Home当前计数: {state.counter}</h2>
<button onClick={e => dispatch({type: "increment"})}>+1</button>
<button onClick={e => dispatch({type: "decrement"})}>-1</button>
</div>
)
}
export function Profile() {
// const [count, setCount] = useState(0);
const [state, dispatch] = useReducer(reducer, { counter: 0 });
return (
<div>
<h2>Profile当前计数: {state.counter}</h2>
<button onClick={e => dispatch({ type: "increment" })}>+1</button>
<button onClick={e => dispatch({ type: "decrement" })}>-1</button>
</div>
)
}
组件之间数据state是不会共享的,它们只是使用了相同的reducer的函数而已,所以,useReducer只是useState的一种替代品,并不能替代Redux。
13.5 useCallback
useCallback是对函数做优化
useCallback会返回一个函数的 memoized(记忆的) 值; 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
当子组件的回调依赖父组件的回调时,会引起死循环,父组件值发生更新传递给子组件,子组件在调用回调函数的时候会引起整个组件的更新,父组件的值又发生了更新,又传值给子组件,子组件接着调用回调函数,又会引起整个组件的更新,,,,,这样就是一个死循环。
// 用于记录 getData 调用次数
let count = 0;
function App() {
const [val, setVal] = useState("");
function getData() {
setTimeout(() => {
setVal("new data " + count);
count++;
}, 500);
}
return <Child val={val} getData={getData} />;
}
function Child({val, getData}) {
useEffect(() => {
getData();
}, [getData]);
return <div>{val}</div>;
}
https://segmentfault.com/a/1190000020108840
13.6 useMemo
useMemo 是对返回值做优化
https://blog.csdn.net/sinat_17775997/article/details/94453167
九、fiber
https://blog.csdn.net/qiqingjin/article/details/80118669
React面经:
https://juejin.im/post/6844904147045580813
https://juejin.im/post/6844904025066831879
https://www.jianshu.com/p/1630d6c1dd71
https://zhuanlan.zhihu.com/p/77760631
https://www.cnblogs.com/passkey/p/10428228.html
https://segmentfault.com/a/1190000016885832?utm_source=tag-newest
能介绍一下 hook 吗
- 比起 hoc,hook 的复用性高
- useState、useEffect、useRef 用法
- 优化 usecallback、useMemo
有了解过 React 的 fiber
- fiber 诞生的背景,为何 react 有时间切片而 vue 不需要
了解 fiber 吗
- 答案之前有
了解 hook 吗
- 答案之前有
为何 react 点击事件放在 settimeout 会拿不到 event 对象
- react 的事件合成
setState 是异步还是同步
- 本质上都是同步,只不过改变 state 的时机不同
- 由一个是是否批量更新变量来决定
- 放在 setTimeout 就能实时改变
能简单介绍一下 react 执行过程吗
- jsx 经过 babel 转变成 render 函数
- create update
- enqueueUpdate
- scheduleWork 更新 expiration time
- requestWork
- workLoop大循环
- performUnitOfWork
- beginWork
- completeUnitOfWork
redux-saga 和 mobx 的比较
- saga 还是遵循 mvc 模型,mobx 是接近 mvvm 模型
- 介绍项目为何要使用 mobx 更合适
- 由于是直播相关的 electron 项目,存在音视频流,和一些底层 OS 操作,那么我们是否可以以麦克风视图开关对于音频流的处理为例子,把 OS 的一些操作与数据做一个映射层,就像数据和视图存在映射关系一样,那么数据的流动就是 view -> 触发action -> 数据改变 -> 改变视图 -> 进行 os 操作
- 然后说了一下 mobx 大概实现的原理,如数据劫持,发布订阅。
react 里如何做动态加载
- react.lazy
const LazyComponent = React.lazy(() =>
import(/* webpackChunkName: 'lazyComponent'*/ "../components/LazyComponent")
);
复制代码
配合suspense,
return (
<div>
<MainComponet />
<React.Suspense fallback={<div>正在加载中</div>}>
<LazyComponent />
</React.Suspense>
</div>
)
复制代码
fallback中可以修改为任意的spinner,本次不做过多优化。假如你不使用React.suspense,则React会给出你错误提示,因此记得React.lazy和React.Suspense搭配使用。 此时我们可以从网络请求中看到,动态加载的lazyComponet组件被单独打包到一个bundle里面.
return (
<div>
<MainComponet />
{this.state.showLazyComponent && (
<React.Suspense fallback={<div>正在加载中</div>}>
<LazyComponent />
</React.Suspense>
)}
</div>
)
复制代码
另外通过 webpack 的动态加载:import() 和 ensure.require。