React是一个简单的javascript UI库,用于构建高效
、快速
的用户界面。它是一个轻量级库,因此很受欢迎。它遵循组件设计模式
、声明式编程范式
和函数式编程概念
,以使前端应用程序更高效。它使用虚拟DOM来有效地操作DOM。它遵循从高阶组件到低阶组件的单向数据流。
声明式编程
是一种编程范式
,它关注的是你要做什么
,而不是如何做
。它表达逻辑
而不显式
地定义步骤
。这意味着我们需要根据逻辑的计算来声明要显示的组件。它没有描述控制流步骤。声明式编程的例子有HTML、SQL
等
HTML file
// HTML
Declarative Programming
SQL file
select * from studens where firstName = 'declarative';
下面是一个例子,数组中的每个元素都乘以 2,我们使用声明式map函数,让编译器来完成其余的工作,而使用命令式,需要编写所有的流程步骤。
const numbers = [1,2,3,4,5];
// 声明式
const doubleWithDec = numbers.map(number => number * 2);
console.log(doubleWithDec)
// 命令式
const doubleWithImp = [];
for(let i=0; i
函数式编程是声明式编程
的一部分。javascript中的函数是第一类公民,这意味着函数是数据,你可以像保存变量一样在应用程序中保存、检索和传递这些函数。
函数式编程有些核心的概念,如下:
不可变性(Immutability)
不可变性意味着不可改变。 在函数式编程中,你无法更改数据,也不能更改。 如果要改变或更改数据,那你应该创建一个新的对象用来修改,而不是修改已有的对象。
例如,这是一个student对象和changeName函数,如果要更改学生的名称,则需要先复制 student 对象,然后返回新对象。
在javascript中,函数参数是对实际数据的引用,你不应该使用 student.firstName =“testing11”
,这会改变实际的student 对象,应该使用Object.assign
复制对象并返回新对象。
let student = {
firstName: "testing",
lastName: "testing",
marks: 500
}
function changeName(student) {
// student.firstName = "testing11" //should not do it
let copiedStudent = Object.assign({}, student);
copiedStudent.firstName = "testing11";
return copiedStudent;
}
console.log(changeName(student));
console.log(student);
纯函数
纯函数是始终接受一个或多个参数并计算参数并返回数据或函数的函数。 它没有副作用,例如设置全局状态,更改应用程序状态,它总是将参数视为不可变数据
。
我想使用 appendAddress 的函数向student对象添加一个地址。 如果使用非纯函数,它没有参数,直接更改 student 对象来更改全局状态。
使用纯函数,它接受参数,基于参数计算,返回一个新对象而不修改参数。
let student = {
firstName: "testing",
lastName: "testing",
marks: 500
}
// 非纯函数
function appendAddress() {
student.address = {streetNumber:"0000", streetName: "first", city:"somecity"};
}
console.log(appendAddress());
// 纯函数
function appendAddress(student) {
let copystudent = Object.assign({}, student);
copystudent.address = {streetNumber:"0000", streetName: "first", city:"somecity"};
return copystudent;
}
console.log(appendAddress(student));
console.log(student);
数据转换
我们讲了很多关于不可变性的内容,如果数据是不可变的,我们如何改变数据。如上所述,我们总是生成原始数据的转换副本,而不是直接更改原始数据。
再介绍一些 javascript内置函数,当然还有很多其他的函数,这里有一些例子。所有这些函数都不改变现有的数据,而是返回新的数组或对象。
let cities = ["irving", "lowell", "houston"];
console.log(cities.join(',')) // irving,lowell,houston
// 获取i开头的项
const citiesI = cities.filter(city => city[0] === "i");
console.log(citiesI // [ 'irving' ]
// 转换大写字母
const citiesC = cities.map(city => city.toUpperCase());
console.log(citiesC) // [ 'IRVING', 'LOWELL', 'HOUSTON' ]
高阶函数
高阶函数是将函数作为参数或返回函数的函数,或者有时它们都有。 这些高阶函数可以操纵其他函数。
Array.map,Array.filter和Array.reduce
是高阶函数,因为它们将函数作为参数。
const numbers = [10,20,40,50,60,70,80]
const out1 = numbers.map(num => num * 100);
console.log(out1);
// [ 1000, 2000, 4000, 5000, 6000, 7000, 8000 ]
const out2 = numbers.filter(num => num > 50);
console.log(out2);
// [ 60, 70, 80 ]
const out3 = numbers.reduce((out,num) => out + num);
console.log(out3);
// 330
下面是另一个名为isPersonOld的高阶函数示例,该函数接受另外两个函数,分别是 message和isYoung 。
const isYoung = age => age < 25;
const message = msg => "He is "+ msg;
function isPersonOld(age, isYoung, message) {
const returnMessage = isYoung(age)?message("young"):message("old");
return returnMessage;
}
console.log(isPersonOld(13,isYoung,message))
// He is young
递归
递归是一种函数在满足一定条件之前调用自身的技术。只要可能,最好使用递归而不是循环。你必须注意这一点,浏览器不能处理太多递归和抛出错误。
下面是一个演示递归的例子,在这个递归中,打印一个类似于楼梯的名称。我们也可以使用for循环,但只要可能,我们更喜欢递归。
function printMyName(name, count) {
if(count <= name.length) {
console.log(name.substring(0,count));
printMyName(name, ++count);
}
}
console.log(printMyName("Bhargav", 1));
/*
B
Bh
Bha
Bhar
Bharg
Bharga
Bhargav
*/
// withotu recursion
var name = "Bhargav"
var output = "";
for(let i=0; i
组合
在React中,我们将功能划分为小型可重用的纯函数,我们必须将所有这些可重用的函数放在一起,最终使其成为产品。 将所有较小的函数组合成更大的函数,最终,得到一个应用程序,这称为组合。
实现组合有许多不同方法。 我们从Javascript中了解到的一种常见方法是链接。 链接是一种使用点表示法调用前一个函数的返回值的函数的方法。
这是一个例子。 我们有一个name,如果firstName和lastName大于5个单词的大写字母,刚返回,并且打印名称的名称和长度。
const name = "Bhargav Bachina";
const output = name.split(" ")
.filter(name => name.length > 5)
.map(val => {
val = val.toUpperCase();
console.log("Name:::::"+val);
console.log("Count::::"+val.length);
return val;
});
console.log(output)
/*
Name:::::BHARGAV
Count::::7
Name:::::BACHINA
Count::::7
[ 'BHARGAV', 'BACHINA' ]
*/
在React中,我们使用了不同于链接的方法,因为如果有30个这样的函数,就很难进行链接。这里的目的是将所有更简单的函数组合起来生成一个更高阶的函数。
const name = compose(
splitmyName,
countEachName,
comvertUpperCase,
returnName
)
console.log(name);
无状态
主要是强调对于一个函数,不管你何时运行,它都应该像第一次运行一样,给定相同的输入,给出相同的输出,完全不依赖外部状态的变化。
那么纯函数带来的意义:
便于测试和优化:这个意义在实际项目开发中意义非常大,由于纯函数对于相同的输入永远会返回相同的结果,因此我们可以轻松断言函数的执行结果,同时也可以保证函数的优化不会影响其他代码的执行。
可缓存性:因为相同的输入总是可以返回相同的输出,因此,我们可以提前缓存函数的执行结果。
更少的 Bug:使用纯函数意味着你的函数中不存在指向不明的 this,不存在对全局变量的引用,不存在对参数的修改,这些共享状态往往是绝大多数 bug 的源头。
框架的好处:
React 16之后有三个生命周期被废弃(但并未删除),最新版本是17.0.2
官方计划在17版本完全删除这三个函数,只保留UNSAVE_前缀
的三个函数,目的是为了向下兼容,但是对于开发者而言应该尽量避免使用他们,而是使用新增的生命周期函数替代它们
目前React 16.8 +
的生命周期分为三个阶段,分别是挂载阶段、更新阶段、卸载阶段
挂载阶段:
static getDerivedStateFromProps(nextProps, prevState)
,这是个静态方法,当我们接收到新的属性想去修改我们state,可以使用getDerivedStateFromProps更新阶段:
shouldComponentUpdate(nextProps, nextState)
,有两个参数nextProps和nextState,表示新的属性和变化之后的state,返回一个布尔值,true表示会触发重新渲染,false表示不会触发重新渲染,默认返回true,我们通常利用此生命周期来优化React程序性能必须与componentDidUpdate搭配使用
卸载阶段:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tJfTXSCo-1666777625421)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1ebb39993da4d35b1dc805cb53dda8a~tplv-k3u1fbpfcp-watermark.image)]
React的异步请求到底应该放在哪个生命周期里,有人认为在componentWillMount
中可以提前进行异步请求,避免白屏,其实这个观点是有问题的.
由于JavaScript中异步事件的性质,当您启动API调用时,浏览器会在此期间返回执行其他工作。当React渲染一个组件时,它不会等待componentWillMount它完成任何事情 - React继续前进并继续render,没有办法“暂停”渲染以等待数据到达。
而且在componentWillMount
请求会有一系列潜在的问题,首先,在服务器渲染时,如果在 componentWillMount 里获取数据,fetch data会执行两次,一次在服务端一次在客户端,这造成了多余的请求,其次,在React 16进行React Fiber重写后,componentWillMount可能在一次渲染中多次调用.
目前官方推荐的异步请求是在componentDidmount
中进行.
如果有特殊需求需要提前请求,也可以在特殊情况下在constructor中请求:
react 17之后componentWillMount会被废弃,仅仅保留UNSAFE_componentWillMount
有时表现出异步,有时表现出同步
React能够控制
的范围被调用(合成事件
和钩子函数
)中是异步的。原生JavaScript控制
的范围被调用,它就是同步的。比如原生事件处理函数,定时器回调函数,Ajax 回调函数中,此时 setState
被调用后会立即更新 DOM 。调用顺序在更新之前
,导致在合成事件和钩子函数中没法立马拿到
更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。React组件间通信方式:
props
的方式,向子组件进行通讯抛开已经被官方弃用的Mixin,组件抽象的技术目前有三种比较主流:
React Fiber 是一种基于浏览器的单线程调度算法.
React 16之前 ,reconcilation 算法实际上是递归,想要中断递归是很困难的,React 16 开始使用了循环来代替之前的递归.
Fiber:一种将 recocilation (递归 diff),拆分成无数个小任务的算法;它随时能够停止,恢复。停止恢复的时机取决于当前的一帧(16ms)内,还有没有足够的时间允许计算。
时间分片
首先,我们看下几个核心概念:
然后我们过下整个工作流程:
首先,用户(通过View)发出Action,发出方式就用到了dispatch方法。
然后,Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State
State一旦有变化,Store就会调用监听函数,来更新View。
到这儿为止,一次用户交互流程结束。可以看到,在整个流程中数据都是单向流动的,这种方式保证了流程的清晰。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hMdVhF2J-1666777625422)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/366336f391e5415981fab5137e80f9a7~tplv-k3u1fbpfcp-watermark.image)]
JSX是javascript
的语法扩展
。它就像一个拥有javascript全部功能的模板语言
。它生成React元素,这些元素将在DOM中呈现。React建议在组件使用JSX。在JSX中,我们结合了javascript和HTML,并生成了可以在DOM中呈现的react元素。
下面是JSX的一个例子。我们可以看到如何将javascript和HTML结合起来。如果HTML中包含任何动态变量,我们应该使用表达式{}。
import React from 'react';
export const Header = () => {
const heading = 'TODO App'
return(
{heading}
)
}
定义组件有两个要求:
函数组件
function Welcome (props) {
return Welcome {props.name}
}
ReactDOM.render( , document.getElementById('root'));
函数组件接收一个单一的 props 对象并返回了一个React元素
类组件
class Welcome extends React.Component {
render() {
return (
Welcome { this.props.name }
);
}
}
ReactDOM.render( , document.getElementById('root'));
区别 | 函数组件 | 类组件 |
---|---|---|
是否有 this | 没有 | 有 |
是否有生命周期 | 没有 | 有 |
是否有状态 state | 没有 | 有 |
props和state是普通的 JS 对象。虽然它们都包含影响渲染输出的信息,但是它们在组件方面的功能是不同的。即
React提供的这个ref属性,表示为对组件真正实例的引用,其实就是ReactDOM.render()
返回的组件实例;
ref可以挂载到组件上也可以是dom元素上;
没有实例
ref属性可以设置为一个回调函数
React 支持给任意组件添加特殊属性。ref 属性接受一个回调函数
,它在组件被加载
或卸载
时会立即执行
。
componentDidMount
或 componentDidUpdate
这些生命周期回调之前执行。当给组件、H5标签添加 ref 属性后,此实例只能在当前组件中被访问到,父组件的 refs 中是没有此引用的,例如:
var Parent = React.createClass({
render: function(){
return (
)
},
componentDidMount(){
console.log(this.refs.child); // 访问挂载在组件上ref
console.log(this.refs.child.refs.update); // 访问挂载在dom元素上的ref
}
})
var Child = React.createClass({
render: function() {
return (
);
}
});
ReactDOM.render(
,
document.getElementById('example')
);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tKwZs0lQ-1666777625423)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/51671ac3a9494e93b2135e65c818667a~tplv-k3u1fbpfcp-watermark.image)]
更新后,可以拿到它的真实 dom更新
。
后,拿到的是组件的实例(上图中的Constructor)获取ref引用组件对应的dom节点
不管ref设置值是回调函数还是字符串,都可以通过ReactDOM.findDOMNode(ref)
来获取组件挂载后真正的dom节点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kFgLVF4L-1666777625423)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/485f3db3decc490db0554c1210b10ee5~tplv-k3u1fbpfcp-watermark.image)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IuwIkO4U-1666777625424)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5be2b85eb1724b84a5a3212f95f87917~tplv-k3u1fbpfcp-watermark.image)]
但是对于html元素使用ref的情况,ref本身引用的就是该元素的实际dom节点,无需使用ReactDOM.findDOMNode(ref)来获取,该方法常用于React组件
上的ref。
this.refs 和 ReactDOM.findDOMNode的区别
新版本的React已经不推荐我们使用ref string转而使用ref callback
this._child = child}/> // child是获取的dom节点
console.log(ReactDOM.findDOMNode(this._child))
# 用动画和实战打开 React Hooks(一):useState 和 useEffect
# 用动画和实战打开 React Hooks(二):自定义 Hook 和 useCallback
“hooks” 直译是 “钩子”,它并不仅是 react
,甚至不仅是前端界的专用术语,而是整个行业所熟知的用语。通常指:
系统运行到某一时期时,会调用被注册到该时机的回调函数。
比较常见的钩子有:windows
系统的钩子能监听到系统的各种事件,浏览器提供的 onload
或 addEventListener
能注册在浏览器各种时机被调用的方法。
以上这些,都可以被称一声 “hook”。
在 [email protected]
之前,当我们谈论 hooks
时,我们可能谈论的是“组件的生命周期”。
但是现在,hooks
则有了全新的含义。
以 react
为例,hooks
是:
一系列以
“use”
作为开头的方法,它们提供了让你可以完全避开class式写法
,在函数式组件中完成生命周期、状态管理、逻辑复用等几乎全部组件开发工作的能力。
简化一下:
一系列方法,提供了在函数式组件中完成开发工作的能力。
import { useState, useEffect, useCallback } from 'react';
// 比如以上这几个方法,就是最为典型的 Hooks
hooks
Hook 的状态复用写法:
// 单个name的写法
const { name, setName } = useName();
// 梅开二度的写法
const { name : firstName, setName : setFirstName } = useName();
const { name : secondName, setName : setSecondName } = useName();
为什么在有了复用的mixins后还会出现hooks,这是因为hooks解决了mixins 几个问题
那么 Hooks
写法在代码组织上究竟能带来怎样的提升呢?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XQiVrumJ-1666777625424)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/de451c029a3043bd80c9c03294165dc2~tplv-k3u1fbpfcp-watermark.image?)]
这样带来的好处是显而易见的: “高度聚合,可阅读性提升” 。伴随而来的便是 “效率提升,bug变少” 。
class
组件更容易理解在 react
的 class
写法中,随处可见各种各样的 .bind(this)
。(甚至官方文档里也有专门的章节描述了“为什么绑定是必要的?”这一问题)
为什么class定义组件需要绑定this:
更深层次的原因还是es5中this执行最后调用他的对象,不一定会执行组件实例
无论是vue还是react,都在官方文档中强调,需要注意this的指向丢失。但有趣的是,为了达到同样的目的,一个是不能使用箭头函数,一个是使用箭头函数便能解决
react
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ElXbGqBT-1666777625424)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cc2f234cf3b9406c88a7f0690db7f266~tplv-k3u1fbpfcp-watermark.image?)]
vue
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Car29vg-1666777625425)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b9d10c0b486345a7af7bbde43024289c~tplv-k3u1fbpfcp-watermark.image?)]
react中的this绑定:
class Demo extends React.Component {
state = {
someState: 'state',
};
// ✅推荐
arrowFunMethod = () => {
// 指向demo实例
console.log('THIS in arrow function:', this);
this.setState({ someState: 'arrow state' });
};
// ❌需要处理this绑定
ordinaryFunMethod() {
// 指向undefined
console.log('THIS oridinary function:', this);
this.setState({ someState: 'ordinary state' });
}
render() {
return (
<div>
<h2>{this.state.someState}</h2>
<button onClick={this.arrowFunMethod}>call arrow function</button>
<button onClick={this.ordinaryFunMethod}>call ordinary function</button>
</div>
);
}
}
ReactDOM.render(<Demo />, document.getElementById('root'));
以上代码可以简化为:
class ReactDemo {
// ✅推荐
arrowFunMethod = () => {
console.log('THIS in arrow function:', this);
};
// ❌this指向丢失
ordinaryFunMethod() {
console.log('THIS in oridinary function:', this);
}
}
const reactIns = new ReactDemo();
let arrowFunWithoutCaller = reactIns.arrowFunMethod;
let ordinaryFunWithoutCaller = reactIns.ordinaryFunMethod;
arrowFunWithoutCaller();
ordinaryFunWithoutCaller();
// 结果同上面一样
从react代码运行的角度来解释一下:
首先是事件触发时,回调函数的执行。回调函数不是像这样直接由实例调用:reactIns.ordinaryFunMethod()
,而是像上面代码中的,做了一次“代理”,最后被调用时,找不到调用对象了:ordinaryFunWithoutCaller()
。这时就出现了this指向undefined的情况。
但为什么使用箭头函数,this又可以正确指向组件实例呢?首先回顾一个简单的知识点:class是个语法糖,本质不过是个[构造函数]),把上面的代码用它最原始的样子写出来:
'use strict'
function ReactDemo() {
// ✅推荐
this.arrowFunMethod = () => {
console.log('THIS in arrow function:', this)
}
}
// ❌this指向丢失
ReactDemo.prototype.ordinaryFunMethod = function ordinaryFunMethod() {
console.log('THIS in oridinary function:', this)
}
const reactIns = new ReactDemo()
可以看到:写成普通函数的方法,是被挂载到原型链上的;而使用箭头函数定义的方法,直接赋给了实例,变成了实例的一个属性,并且最重要的是:它是在「构造函数的作用域」被定义的。
我们知道,箭头函数没有自己的this,用到的时候只能根据作用域链去寻找最近的那个。放在这里,也就是构造函数这个作用域中的this
——组件实例。
这样就可以解释为什么react组件中,箭头函数的this能正确指向组件实例。
vue中this的丢失
const Demo = Vue.createApp({
data() {
return {
someState: 'state',
};
},
methods: {
// ❌this指向丢失
arrowFunMethod: () => {
// 指向window
console.log('THIS in arrow function:', this);
this.someState = 'arrow state';
},
// ✅推荐
ordinaryFunMethod() {
// 指向实例
console.log('THIS in oridinary function:', this);
this.someState = 'ordinary state';
},
},
template: `
{{this.someState}}
`,
});
Demo.mount('#root');
先看下vue的源码是怎么处理methods的:
function initMethods(vm: Component, methods: Object) {
for (const key in methods) {
vm[key] = bind(methods[key], vm);
}
}
vue会把我们传入methods
遍历,再一个个赋给到组建实例上,在这个过程就处理了this的绑定(bind(methods[key], vm)
):把每一个方法中的this都绑定到组件实例vm上。
普通函数都有自己的this,所以绑定完后,被调用时都能正确指向组件实例。
但箭头函数没有自己的this,便无从谈及修改,它只能去找父级作用域中的this。这个父级作用域是谁呢?是组件实例吗?我们知道[作用域]只有两种:全局作用域和函数作用域。回到我们写的vue代码,它本质就是一个对象(具体一点,是一个组件的配置对象,这个对象里面有data、mounted、methods等属性)也就是说,我们在一个对象里面去定义方法,因为对象不构成作用域,所以这些方法的父作用域都是全局作用域。箭头函数要去寻找this,就只能找到全局作用域中的this——window对象了。
上面说了这么多,总结一下:vue对传入的方法methods
对象做了处理,在函数被调用前做了this指向的绑定,只有拥有this的普通函数才能被正确的绑定到组件实例上。而箭头函数则会导致this的指向丢失。
总结:
「为什么react中用箭头函数,vue中用普通函数」这是一个挺很有意思的问题,简单来说,这种差异是由于我们写的react是一个类,而vue是一个对象导致的。
在类中定义只有箭头函数才能根据作用域链找到组件实例;在对象中,只有拥有自身this的普通函数才能被修改this指向,被vue处理后绑定到组件实例。
很显然,绑定虽然“必要”,但并不是“优点”,反而是“故障高发”地段。
但在Hooks
写法中,你就完全不必担心 this
的问题了。
因为:
Hooks
写法直接告别了 this
,从“函数”来,到“函数”去。
// my-component.js
import { useState, useEffect } from 'React'
export default () => {
// 通过 useState 可以创建一个 状态属性 和一个赋值方法
// name是变量,setName是设置name的值,useState设置name的初始值
const [ name, setName ] = useState('')
// 通过 useEffect 可以对副作用进行处理
useEffect(() => {
// 监听到name的变化就会触发这里,类似vue的watch
console.log(name)
}, [ name ])
// 通过 useMemo 能生成一个依赖 name 的变量 message,类似vue的computed
const message = useMemo(() => {
return `hello, my name is ${name}`
}, [name])
return <div>{ message }</div>
}
例如要实现:
const { name, setName } = useName();
// 随机生成一个状态属性 name,它有一个随机名作为初始值
// 并且提供了一个可随时更新该值的方法 setName
react写法如下:
import React from 'react';
export const useName = () => {
// 这个 useMemo 很关键
const randomName = React.useMemo(() => genRandomName(), []);
const [ name, setName ] = React.useState(randomName)
// 最终返回的一个对象,也就对应上了{ name, setName }
return {
name,
setName
}
}
vue写法如下:
import { ref } from 'vue';
export const useName = () => {
const name = ref(genRandomName())
const setName = (v) => {
name.value = v
}
return {
name,
setName
}
}
function ExampleWithManyStates() {
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
// 多次调用useState并不会影响之前fruit的取值,每个useState都是相互独立的
react是怎么保证多个useState的相互独立的?
答案是,react是根据useState出现的顺序来定的。我们具体来看一下:
//第一次渲染
useState(42); //将age初始化为42
useState('banana'); //将fruit初始化为banana
useState([{ text: 'Learn Hooks' }]); //...
//第二次渲染
useState(42); //读取状态变量age的值(这时候传的参数42直接被忽略)
useState('banana'); //读取状态变量fruit的值(这时候传的参数banana直接被忽略)
useState([{ text: 'Learn Hooks' }]); //...
假如我们改一下代码:
let showFruit = true;
function ExampleWithManyStates() {
const [age, setAge] = useState(42);
if(showFruit) {
const [fruit, setFruit] = useState('banana');
showFruit = false;
}
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
这样一来,
//第一次渲染
useState(42); //将age初始化为42
useState('banana'); //将fruit初始化为banana
useState([{ text: 'Learn Hooks' }]); //...
//第二次渲染
useState(42); //读取状态变量age的值(这时候传的参数42直接被忽略)
// useState('banana');
useState([{ text: 'Learn Hooks' }]); //读取到的却是状态变量fruit的值,导致报错
鉴于此,react规定我们必须把hooks写在函数的最外层,不能写在ifelse等条件语句当中,来确保hooks的执行顺序一致。
<div onClick={this.handleClick.bind(this)}>点我</div>
React并不是将click事件绑定到了div的真实DOM上
,而是在document
处监听了所有的事件,当事件发生并且冒泡
到document处的时候,React将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗
,还能在组件挂在销毁时统一订阅和移除事件
。
除此之外,冒泡到document上的事件也不是原生
的浏览器事件
,而是由react自己实现的合成事件(SyntheticEvent)
。因此如果不想要是事件冒泡的话应该调用event.preventDefault()
方法,而不是调用event.stopProppagation()
方法。
合成事情:它符合W3C标准,且与原生的浏览器事件拥有同样的接口,支持冒泡机制,所有的事件都自动绑定在最外层上。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9n1oPjZJ-1666777625425)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f18581560c9b4e73b29f4258599d8437~tplv-k3u1fbpfcp-watermark.image?)]
实现合成事件的目的如下:
抹平
了浏览器之间的兼容问题
,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器
开发的能力;区别:
preventDefault()
来阻止默认行为。什么是合成事件:
React 合成事件(SyntheticEvent)是 React 模拟原生 DOM 事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器。它根据 W3C 规范 来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口。
在 React 中,所有事件都是合成的,不是原生 DOM 事件,但可以通过 e.nativeEvent 属性获取 DOM 事件。 比如:
const button = <button onClick={handleClick}>react 按钮</button>
const handleClick = (e) => console.log(e.nativeEvent); //原生事件对象
合成事件是 react 模拟原生 DOM 事件所有能力的一个事件对象,其优点如下:
事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用
,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到document 上合成事件才会执行。
这三者是目前react解决代码复用的主要方式:
不是 React API 的一部分
,它是一种基于 React 的组合特性而形成的设计模式
。具体而言,高阶组件是参数为组件
,返回值为新组件
的函数
,本质它就是一个函数。(1)HOC 官方解释∶
简言之,HOC是一种组件的设计模式,HOC接受一个组件和额外的参数(如果需要),返回一个新的组件。HOC 是纯函数,没有副作用。
// hoc的定义
function withSubscription(WrappedComponent, selectData) {
// 接收一个组件,和额外参数,返回一个组件
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
data: selectData(DataSource, props)
};
}
// 一些通用的逻辑处理
render() {
// ... 并使用新数据渲染被包装的组件!
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
// 使用,BlogPost就是一个组件,后面接着一个函数
const BlogPostWithSubscription =
withSubscription(BlogPost,(DataSource, props) => DataSource.getBlogPost(props.id));
HOC的优缺点∶
(2)Render props 官方解释∶
"render prop"是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术
具有render prop 的组件接受一个返回React元素的函数,将render的渲染逻辑注入到组件内部。在这里,"render"的命名可以是任何其他有效的标识符。
// DataProvider组件内部的渲染逻辑如下
class DataProvider extends React.Components {
state = {
name: 'Tom'
}
render() {
return (
共享数据组件自己内部的渲染逻辑
// 这里this.props.render对应是下面的render传进来的函数
{ this.props.render(this.state) }
);
}
}
// 调用方式,render对应的是一个函数
(
Hello {data.name}
)}/>
由此可以看到,render props的优缺点也很明显∶
(3)Hooks 官方解释∶
Hook是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。通过自定义hook,可以复用代码逻辑。
// 自定义一个获取订阅数据的hook
function useSubscription() {
const data = DataSource.getComments();
return [data];
}
//
function CommentList(props) {
const {data} = props;
const [subData] = useSubscription();
...
}
// 使用
<CommentList data='hello' />
以上可以看出,hook解决了hoc的prop覆盖的问题,同时使用的方式解决了render props的嵌套地狱的问题。hook的优点如下∶
需要注意的是:hook只能在组件顶层使用,不可在分支语句中使用。
总结∶ Hoc、render props和hook都是为了解决代码复用的问题,但是hoc和render props都有特定的使用场景和明显的缺点。hook是react16.8更新的新的API,让组件逻辑复用更简洁明了,同时也解决了hoc和render props的一些缺点。
React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程期间, React 会占据浏览器资源,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿。
为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。
所以 React 通过Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:
核心思想: Fiber 也称协程或者纤程。它和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的让出机制。让出 CPU 的执行权,让 CPU 能在这段时间执行其他的操作。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。
(1)哪些方法会触发 react 重新渲染?
setState 是 React 中最常用的命令,通常情况下,执行 setState 会触发 render。但是这里有个点值得关注,执行 setState 的时候不一定会重新渲染。当 setState 传入 null 时,并不会触发 render。
class App extends React.Component {
state = {
a: 1
};
render() {
console.log("render");
return (
{this.state.a}
);
}
}
只要父组件重新渲染了,即使传入子组件的 props 未发生变化,那么子组件也会重新渲染,进而触发 render
(2)重新渲染 render 会做些什么?
React 的处理 render 的基本思维模式是每次一有变动就会去重新渲染整个应用。在 Virtual DOM 没有出现之前,最简单的方法就是直接调用 innerHTML。Virtual DOM厉害的地方并不是说它比直接操作 DOM 快,而是说不管数据怎么变,都会尽量以最小的代价去更新 DOM。React 将 render 函数返回的虚拟 DOM 树与老的进行比较,从而确定 DOM 要不要更新、怎么更新。当 DOM 树很大时,遍历两棵树进行各种比对还是相当耗性能的,特别是在顶层 setState 一个微小的修改,默认会去遍历整棵树。尽管 React 使用高度优化的 Diff 算法,但是这个过程仍然会损耗性能.
组件状态的改变可以因为props
的改变,或者直接通过setState
方法改变。组件获得新的状态,然后React决定是否应该重新渲染组件。只要组件的state发生变化,React就会对组件进行重新渲染。这是因为React中的shouldComponentUpdate
方法默认返回true
,这就是导致每次更新都重新渲染的原因。
当React将要渲染组件时会执行shouldComponentUpdate
方法来看它是否返回true
(组件应该更新,也就是重新渲染)。所以需要重写shouldComponentUpdate
方法让它根据情况返回true
或者false
来告诉React什么时候重新渲染什么时候跳过重新渲染。
在React中,组件返回的元素
只能有一个根元素
。为了不添加多余的DOM节点
,我们可以使用Fragment标签来包裹所有的元素,Fragment标签不会渲染出任何元素。React官方对Fragment的解释:
React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。
import React, { Component, Fragment } from 'react'
// 一般形式
render() {
return (
);
}
// 也可以写成以下形式
render() {
return (
<>
>
);
}
可以用ref来获取某个子节点的实例,然后通过当前class组件实例的一些特定属性来直接获取子节点实例。ref有三种实现方法:
span
this.info = ele}>
formRef = React.createRef();
const labels = formRef.current.querySelectorAll("label");
<>
<span id="name" ref={this.spanRef}>{this.state.title}</span>
<span>{
this.spanRef.current ? '有值' : '无值'
}</span>
</>
不可以,render 阶段 DOM 还没有生成
,无法获取 DOM。DOM 的获取需要在 pre-commit
阶段和 commit
阶段:
(1)受控组件 在使用表单来收集用户输入时,例如等元素都要绑定一个change事件,当表单的状态发生变化,就会触发onChange事件,更新组件的state。这种组件在React中被称为受控组件,在受控组件中,组件渲染出的状态与它的value或checked属性相对应,react通过这种方式消除了组件的局部状态,使整个状态可控。react官方推荐使用受控表单组件。
受控组件更新state的流程:
受控组件缺陷: 表单元素的值都是由React组件进行管理,当有多个输入框,或者多个这种组件时,如果想同时获取到全部的值就必须每个都要编写事件处理函数,这会让代码看着很臃肿,所以为了解决这种情况,出现了非受控组件。
(2)非受控组件 如果一个表单组件没有value props(单选和复选按钮对应的是checked props)时,就可以称为非受控组件。在非受控组件中,可以使用一个ref来从DOM获得表单值。而不是为每个状态更新编写一个事件处理程序。
React官方的解释:
要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以使用 ref来从 DOM 节点中获取表单数据。 因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。
例如,下面的代码在非受控组件中接收单个属性:
class NameForm extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(event) {
alert('A name was submitted: ' + this.input.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
// 现用现取
<input type="text" ref={(input) => this.input = input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
总结: 页面中所有输入类的DOM如果是现用现取的称为非受控组件,而通过setState将输入的值维护到了state中,需要时再从state中取出,这里的数据就受到了state的控制,称为受控组件。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3hMF82w6-1666777625426)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5595c0ef836940679e60a836dfc4d43c~tplv-k3u1fbpfcp-watermark.image?)]
具体的执行过程如下(源码级解析):
setState
入口函数,入口函数在这里就是充当一个分发器的角色,根据入参的不同,将其分发到不同的功能函数中去;ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
}
enqueueSetState
方法将新的 state
放进组件的状态队列里,并调用 enqueueUpdate
来处理将要更新的实例对象;enqueueSetState: function (publicInstance, partialState) {
// 根据 this 拿到对应的组件实例
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
// 这个 queue 对应的就是一个组件实例的 state 数组
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
// enqueueUpdate 用来处理当前的组件实例
enqueueUpdate(internalInstance);
}
复制代码
enqueueUpdate
方法中引出了一个关键的对象——batchingStrategy
,该对象所具备的isBatchingUpdates
属性直接决定了当下是要走更新流程,还是应该排队等待;如果轮到执行,就调用 batchedUpdates
方法来直接发起更新流程。由此可以推测,batchingStrategy
或许正是 React 内部专门用于管控批量更新的对象。function enqueueUpdate(component) {
ensureInjected();
// 注意这一句是问题的关键,isBatchingUpdates标识着当前是否处于批量创建/更新组件的阶段
if (!batchingStrategy.isBatchingUpdates) {
// 若当前没有处于批量创建/更新组件的阶段,则立即更新组件
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 否则,先把组件塞入 dirtyComponents 队列里,让它“再等等”
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}
复制代码
注意: batchingStrategy
对象可以理解为“锁管理器”。这里的“锁”,是指 React 全局唯一的 isBatchingUpdates
变量,isBatchingUpdates
的初始值是 false
,意味着“当前并未进行任何批量更新操作”。每当 React 调用 batchedUpdate
去执行更新动作时,会先把这个锁给“锁上”(置为 true
),表明“现在正处于批量更新过程中”。当锁被“锁上”的时候,任何需要更新的组件都只能暂时进入 dirtyComponents
里排队等候下一次的批量更新,而不能随意“插队”。此处体现的“任务锁”的思想,是 React 面对大量状态仍然能够实现有序分批处理的基石。
(1)React中setState后发生了什么
在代码中调用setState函数之后,React 会将传入的参数对象
与组件当前的状态
合并,然后触发调和过程
(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态
构建 React 元素树并且着手重新渲染整个UI界面。
在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。
如果在短时间内频繁setState。React会将state的改变压入栈中,在合适的时机,批量更新state和视图,达到提高性能的效果。
(2)setState 是同步还是异步的
假如所有setState是同步
的,意味着每执行一次setState时(有可能一个同步代码中,多次setState),都重新vnode diff + dom修改,这对性能来说是极为不好的。如果是异步
,则可以把一个同步代码中的多个setState合并成一次组件更新
。所以默认
是异步
的,但是在一些情况下是同步的。
setState 并不是单纯同步/异步的
,它的表现会因调用场景
的不同而不同。在源码中,通过 isBatchingUpdates
来判断setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。
可以控制
的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。无法控制
的地方,比如原生事件,具体就是在 addEventListener 、setTimeout、setInterval 等事件中,就只能同步更新。一般认为,做异步设计是为了性能优化、减少渲染次数:
setState
设计为异步,可以显著的提升性能。如果每次调用 setState
都进行一次更新,那么意味着render
函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新;state
,但是还没有执行render
函数,那么state
和props
不能保持同步。state
和props
不能保持一致性,会在开发中产生很多的问题;通过实现组件的getDefaultProps,对属性设置默认值(ES5的写法):
var ShowTitle = React.createClass({
getDefaultProps:function(){
// 设置默认的props
return{
title : "React"
}
},
render : function(){
return <h1>{this.props.title}</h1>
}
});
setState
的第二个参数是一个可选的回调函数。这个回调函数将在组件重新渲染后执行。等价于在 componentDidUpdate
生命周期内执行。通常建议使用 componentDidUpdate
来代替此方式。在这个回调函数中你可以拿到更新后 state
的值:
this.setState({
key1: newState1,
key2: newState2,
...
}, callback) // 第二个参数是 state 更新完成后的回调函数
(1)setState() setState()用于设置状态对象,其语法如下:
setState(object nextState[, function callback])
复制代码
合并nextState和当前state,并重新渲染组件。setState是React事件处理函数中和请求回调函数中触发UI更新的主要方法。
(2)replaceState() replaceState()方法与setState()类似,但是方法只会保留nextState中状态,原state不在nextState中的状态都会被删除。其语法如下:
replaceState(object nextState[, function callback])
总结: setState 是修改其中的部分状态,相当于 Object.assign,只是覆盖,不会减少原来的状态。而replaceState 是完全替换原来的状态,相当于赋值,将原来的 state 替换为另一个对象,如果新状态属性减少,那么 state 中就没有这个状态了。
(1)props
props是一个从外部
传进组件的参数,主要作为就是从父组件
向子组件
传递数据,它具有可读性和不变性,只能通过外部组件主动传入的props的setxxx来修改
(2)state
state的主要作用是用于组件保存、控制以及修改自己的状态,它只能在constructor中初始化,它算是组件的私有属性,不可通过外部访问和修改,只能通过组件内部的this.setState来修改,修改state属性会导致组件的重新渲染。
(3)区别
保证 React 单向数据流的设计模式,使状态可预测。
如果允许子组件修改,那么一个父组件将状态传递给好几个子组件,这几个子组件随意修改,就完全不可预测,不知道在什么地方修改了状态。
例如下面的代码,如果能修该,那么怎么判断是子组件修改了theme还是孙组件修改的
class App extends React.Component {
// 第一步:给节点设置属性 `theme`
render() {
return <Toolbar theme="dark" />;
}
}
function Toolbar(props) {
// 第二步:子节点可以访问父节点的props属性,但只能读取不能修改
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
class ThemedButton extends React.Component {
// 第三步:孙子节点依然可访问props属性,同样只能读不能修改
render() {
return <Button theme={this.props.theme} />;
}
}