这是我参与「第四届青训营 」笔记创作活动的第1天
React
React入门
原生JS痛点
- 原生JS操作DOM繁琐、效率低。
- 使用JS直接操作DOM,浏览器会进行大量重绘重拍。
- 原生JS没有组件化编码方案,代码复用效率低。
React的特点
-
采用组件化模式、声明式编码,提高开发效率及组件复用率。
- 命令式:喝热水 => 打开水壶,倒水,开火,等待,倒热水
- 声明式:喝热水 => 喝热水,over
-
在React Native中可以使用React语法进行移动端开发。
-
使用虚拟DOM和优秀的Diffing算法,尽量减少与真实DOM的交互。
- Diffing算法:寻找新虚拟DOM树(JS维护的对象)和旧虚拟DOM树(浏览器维护)的差别。
- 通过Diffing算法找出两颗树的不同之处,只部分地更新真实DOM。
HelloReact
-
创建虚拟DOM
-
渲染虚拟DOM到页面
<div id="app"> </div> 引入React核心库、React-DOM、babel <!--babel用来将jsx语法翻译成js--> <script type="text/babel"> // 创建虚拟DOM(jsx语法) const VDOM = <h1>Hello React!</h1> // 不用jsx,纯js创建虚拟DOM,text/babel换掉 // const VDOM = React.createElement("标签名", {标签属性}, "标签体内容"); // babel就是将react做的的jsx语法翻译成这种,浏览器才认识 // 渲染虚拟DOM到页面 ReactDOM.render(虚拟DOM, 节点); </script>
关于VDOM:
- 本质是js的Object类型的对象。
- 与document.getXxx获得的真实DOM对象相比,虚拟DOM身上的属性比较少,真实DOM多很多。
- 最终会被React转化成真实DOM,呈现在页面上。
JSX
-
全称:JavaScript XML
-
是React定义的一种JS的扩展语法
-
语法规则:
-
定义虚拟DOM时不要写引号
-
标签内如果想要读取js变量,要用大括号
<h1 id={myId.toLowerCase()}>{value}</h1>
-
如果想用样式类名,不要用
class
,要用className
。为了避开ES6的类 -
如果想用内联
style
,要用大括号,里面一个对象,且短线改大驼峰<div style={ {color: "red", marginTop: "10px"} }></div>
-
只能有一个根标签
-
标签必须闭合
-
标签首字母:
- 小写字母开头,转为HTML中同名元素,若无该标签对应同名元素,则报错。
- 大写字母开头,react去渲染对应的组件,若组件没有定义,则报错。
-
React面向组件编程
函数式组件
通过函数来定义组件:
function Demo() {
// this是undifine(babel翻译完开启严格模式,严格模式禁止自定义函数this指向window)
return <h1>Hello Component!</h1>
}
-
名字大写
-
使用:
<Demo/>
或<Demo></Demo>
-
执行
ReactDOM.render(<Demo/>, document.getElementById("app"))
之后,发生了什么:- react解析组件标签,找到了Demo组件。
- 发现组件是函数定义的,调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中。
类式组件
通过类来定义组件:
class MyComponent extends React.Component { // 继承
// 需要初始化就写构造函数
// 不需要就不写
// 写了就要super
// 一般函数render必须要写,render必须有返回值
render() {
return <h1>Hello Component!</h1>
}
}
ReactDOM.render(<MyCOmponent/>, document.getXxx....);
-
继承
-
render函数
-
render函数的返回值
-
执行
ReactDOM.render(<MyCOmponent/>, document.getXxx....);
后:- react解析组件标签,找到了MyComponent组件
- react做了new实例对象的操作
- 调用了实例对象的render方法,得到虚拟DOM
- render中的this:当前实例对象 => 组件实例对象
State
- 简单组件:没有状态(State)
- 复杂组件:有状态(State)
- 状态中存有数据。组件自己的:state,外部传进来的:prop,组件唯一标识:ref。组件实例的三大属性。 (既然是实例,那就只有类组件才有实例,函数组件没有,不过新版本React函数组件有hook,也可以有这三个东东,后面说)
class Weather extends React.Component {
// 有自己的状态就要构造器,有构造器就要super
constructor(props) {
super(props);
// props不是State这里的目的,下面才是:自己的状态
this.state = {isHot: true};
}
render() {
// return <h1>今天天气很{this.state.isHot ? "炎热" : "凉爽"}</h1>
// es6解构赋值:
const {isHot} = this.state;
return <h1>今天天气很{isHot ? "炎热" : "凉爽"}</h1>
}
}
事件:React重新设计了原生的所以onxxx事件,名字成了onXxx。
<h1 onClick={demo}></h1>
......
function demo() {
console.log("点击...");
}
注意不要写:onClick={demo()}
,这样写的意思是,执行demo()
函数,把该函数的返回值交给onClick。最后函数执行,返回值是undifine。
函数中要拿到state怎么办?是不是应该把事件回调写到类里面。
JS类中方法的this指向:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
speak() {
console.log(this);
}
}
const p1 = new Person("小明", 18);
p1.speak(); // this是实例对象
const x = p1.speak;
x(); // this是undifine
原因:x()
调用的时候不是实例调用,是直接调用,直接调用的时候this就是window,如果开了严格模式就是undifine,类中的方法自动会开局部严格模式。
写到类里面:
class Weather extends React.Component {
// ...构造函数...
render() {
return <h1 onClick={this.changeWeather}></h1>
}
changeWeather() {
console.log(this) // 点击h1的时候执行这个方法,this是谁?undifine!
}
}
- changeWeather放在Weather的原型对象上,供实例调用。
- changeWeather作为onClick的回调,不是通过实例调用的,而是直接调用。
- 类中的方法默认开启局部严格模式,所以为undifine。
解决this指向:
class Weather extends React.Component {
constructor() {
// 等号右边:顺着原型链找到changeWeather函数,调用bind方法
// bind方法生成了一个新的函数,且新函数的this是传的参数
// 构造器中的this一定是实例对象
// 最终实例对象身上多了一个changeWeather方法,内容和等号右边一样,且里面的this是实例对象自身
// 点击的时候的changeWeather就是自身且this是实例的这个,就不会因为没找到而顺着原型找了
this.changeWeather = this.changeWeather.bind(this);
}
render() {
return <h1 onClick={this.changeWeather}></h1>
}
changeWeather() {
console.log(this)
}
}
改:理解清楚了就知道怎么改。
class Weather extends React.Component {
constructor() {
// 等号右边:顺着原型链找到changeWeather函数,调用bind方法
// bind方法生成了一个新的函数,且新函数的this是传的参数
// 构造器中的this一定是实例对象
// 最终实例对象身上多了一个changeWeather方法,内容和等号右边一样,且里面的this是实例对象自身
// 点击的时候的changeWeather就是自身且this是实例的这个,就不会因为没找到而顺着原型找了
this.haha = this.changeWeather.bind(this);
}
render() {
return <h1 onClick={this.haha}></h1>
}
changeWeather() {
console.log(this)
}
}
bind:
function demo() {
console.log(this)
}
demo(); // 输出:this -> window
const x = demo.bind({a:10,b:"haha"});
x(); // 输出:this -> {a:10,b:"haha"}
最后改state不能直接改:
const isHot = this.state.isHot // 可以取到isHot的值
this.state.isHot = !isHot // 可以修改isHot的值
但是:React不认可这样的更改!!即这样更改无法响应式,即页面不会因为数据改变而改变!!
正确地:
const isHot = this.state.isHot
this.setState({
isHot:!isHot
})
这个和微信小程序很像,微信小程序整体和vue很像,但是改数据这里vue可以直接改,而微信小程序和react都需要setData / setState来改。
react状态更新是替换还是合并:
this.state = {isHot:false,wind:"微风"}
//.....
//.....
this.setState({isHot:true})
执行后isHot变为true,而wind还在,是合并不是替换。同名的复写掉,不同名的任然在。
构造器只调用一次(new一次实例),render()
调用 1 + n 次,最开始第一次,后面state通过setState改变,则进行调用,渲染更新页面。
简写
class Weather extends React.component {
state = {isHot:false,wind:"微风"}
changeWeather = () => {
const isHot = this.state.isHot
this.setState({isHot:!isHot})
}
render() {
const {isHot,wind} = this.state
return <h1 onClick="{this.changeWeather}">天气:{isHot ? "炎热" : "凉爽"},{wind}</h1>
}
}
-
类里面直接写赋值语句:所以实例身上都有这个属性。
- JS中函数也可以作为变量赋值 => 这样写changeWeather由原型对象上变为了实例身上。
-
箭头函数的特点:没有自己的this,但是函数体内也可以使用this,使用this会去找其外层的this作为自己的this使用。
- 所以必须使用箭头函数才能确保this是Weather实例对象,普通函数依然是:如果是对象调用的就是实例对象;如果是直接调用的就是undifine。
-
构造器里边写
this.state = {isHot:false}
就是为了让每个实例身上都有state属性,可以看到构造器里的state又不是说接外面的,就是一定让每个实例构造的时候都有这个state属性,那写外面直接写赋值语句不就所以实例都有了。 -
注意:类里面直接写赋值语句:所以实例身上都有这个属性 => 和静态属性不同,不是静态属性还是实例属性!!
Prop
从外部传入数据到组件。
传:<Person name="tom" age="18" sex="男" />
用:this.props.name...
,解构赋值:const {name,age,sex} = this.props
批量传入:
const p = {name:"xm",age:18,sex:"男"}
<Person {...p} />
...
运算符:
let arr1 = [1,2,3,4]
let arr2 = [5,6,7,8,9]
console.log(...arr1) // 展开数组:1 2 3 4
let arr3 = [...arr1, ...arr2] // 连接数组
function sum(...numbers) { // 不定参数
console.log(numbers) // 数组
return numbers.reduce((preValue, currentValue) => {
return preValue + currentValue
});
}
console.log(sum(1,2,3,4,5,6))
let person = {name:"xm",age:19,sex:"男"}
// console.log(...person) 报错,此运算符不能用于展开对象
let person2 = {...person} // 复制person的属性
let person3 = {...person,name:"lisi"} // 复制的同时修改个别属性
注意:react里<Person {...p} />
,这个{}
是react的语法,即这里写的JS就是...p
,本来原生JS确实不可以展开对象,但是react + babel
就可以了。不过也仅仅适用于标签属性的传递。
对props的限制:
- 限制必须传某个属性
- 限制传递的属性的类型
- 给属性指定默认值
class Person extends ... {
render() {
......
}
}
// 组件Person身上的一个属性,名字不能随便取,React会找这个属性的值来对props作规则限制
Person.propTypes = {
// 下面的PropTypes是React身上的,注意区分
name: React.PropTypes.string.isRequired, // props.name必须是string类型且必须
age: React.PropTypes.number // 表示props.age必须是数字类型
// 限制函数:React.PropTypes.func
}
// 组件Person身上的一个属性,名字不能随便取,React会找这个属性的值来对props作默认值
Person.defaultProps = {
sex: "男"
}
React15.5
之前,PropTypes
都是React身上的,React16以后,因为加上PropTypes以后React感觉太大了,而PropTypes又不是每次都必须,所以React16开始PropTypes不再在React身上,而是专门引入一个依赖库来使用PropTypes。
注意:在组件内部,props是只读的,不允许修改。
JS的类上:
class A {
// 实例对象上的属性a1,a2,有没有由创建对象时传不传决定
constructor(a1,a2) {
this.a1 = a1;
this.a2 = a2;
}
// 所有实例对象上都加上a3,a4属性
a3 = 10
a4 = "hello"
// 给类加上静态属性
static a5 = 999
}
之前的事件方法是给所有组件实例加,每个由组件类构造的组件实例有各自的事件方法。而这次的规则限制是给所有组件实例都有同样的规则限制,所以这次写到类里边的时候应该用static
加。
只要保证类自身有那两个属性,React就能添加规则限制。
讲讲构造器:
class Person extends React.Component {
// 官网说构造器会接到props,把它交给super。
constructor(props) {
super(props)
}
// 但其实下面这样也完全没问题(都可以省略了)
// constructor() {
// super()
// }
// 区别就是下面这种,如果要再在构造器里通过this访问props的话会出bug访问不到
}
React官网说:构造器仅仅适用于以下两种情况使用:
- 给
this.state
赋值来初始化state
- 为事件处理函数绑定
this
那么这两种已经都用过了,同时也都可以不使用构造器实现,所以构造器是完全可以省略的。
函数组件的Prop
组件实例的三大属性:State、ref、Prop,而函数没有什么实例,所以三大属性不适用于函数,但是函数有props,因为函数可以接受参数。
function Person(props) {
const {name, age} = props // 解构赋值
// 相当于 const name = props.name;const age = props.age;
return (
<ul>
<li>{name}</li>
<li>{age></li>
</ul>
)
}
函数式组件只有props,没有state和refs,不过React最新版本提出了函数式组件的hooks,实现了state和refs,后面讲。
对props进行限制还是可以的,不过一定要写在外面:
function Person() {
...
}
Person.propTypes = {}
Person.defaultProps = {}
refs
相当于原生html
的id
属性,唯一标识一个组件,只要打了标识就能把html标签收集到this.refs
(实例对象的refs属性)身上。拿到的就是真实DOM节点,虚拟DOM对应的真实DOM节点。
字符串形式的ref(已经不被React官方推荐了):
class Demo extends React.Component {
clickBtn = ()=>{
console.log(this.refs.btn)
}
render() {
// 字符串形式的ref
return <button ref="btn" onClick={clickBtn}>点我</button>
}
}
为什么字符串形式的ref不推荐?存在效率问题,效率不高,可能会在未来的版本移除。
回调形式的ref:
回调函数:我定义的,我没调用,最终执行了。
class Demo extends React.Component {
clickBtn = ()=>{
console.log(this.myBtn)
}
render() {
// 回调形式的ref
return <button ref={ (currentNode)=>{this.myBtn = currentNode} } onClick={clickBtn}>点我</button>
}
}
回调函数的调用次数:如果回调函数是以内联函数的方式定义的,第一次渲染(调用render)调用一次,参数是当前节点。以后每次更新页面(调用render)都会调用两次,第一次参数是null,第二次才是当前节点。
为什么这样做?每次更新渲染时都会重新调用render,render里遇到ref的时候,发现后面是个函数,因为不是第一次调了,所以React先传入null把之前的清空,再调一次传入新的。
如果不是回调函数就不会调两次:
class Demo extends React.Component {
clickBtn = ()=>{
console.log(this.myBtn)
}
haha = (c)=>{
this.myBtn = c
}
render() {
// 回调形式
return <button ref={ this.haha } onClick={clickBtn}>点我</button>
}
}
这是一个无关紧要的问题。
createRef API形式的ref:
class Demo extends React.Component {
// 调用后返回一个容器,容器可以存储被标识的节点
// 容器是专人专用的,即只能存储一个,再存会替换
// 所以一个ref一个容器
myRef = React.createRef()
clickBtn = ()=>{
console.log(this.myRef.current)
}
render() {
// 新api形式的ref
return <button ref={ this.myRef } onClick={clickBtn}>点我</button>
}
}
React中的事件处理
-
通过onXxx属性指定事件处理函数。
- React使用的是自定义(合成)事件,而不是原生的DOM事件。——为更好的兼容性。
- React事件是通过事件委托的方式处理的(委托给组件最外层的元素)。——为了更高效。
-
通过
event.target
可以拿到发生事件的DOM元素对象。——勿过度使用ref。
受控组件与非受控组件
-
非受控组件:页面中所有输入类标签的内容现用现取。(点击登录后才收集数据)。
-
受控组件:将数据通过onChange事件绑定到state。(就像Vue双向绑定,
data
、v-model:xxx
)React需要手动绑定,就像这样:
class Demo extends React.Component { state = {username: "", password: ""} saveUsername = (e)=>{ this.setState({username: e.target.value}) } savePassword = (e)=>{ this.setState({password: e.target.value}) } render() { return ( <div> 用户名<input onChange={this.saveUsername} text="type" name="username" /><br/> 密码<input onChange={this.savePassword} text="password" name="password" /> </div> ) } }
高阶函数
上面的代码是否可以优化:
class Demo extends React.Component {
state = {username: "", password: ""}
saveFormData = (e)=>{
console.log(e.target)
}
render() {
return (
<div>
用户名<input onChange={this.saveFormData} text="type" name="username" /><br/>
密码<input onChange={this.saveFormData} text="password" name="password" />
</div>
)
}
}
前面说过,事件处理:onChange={xxx}
,这里xxx不能加括号,因为这样写的意思是把xxx交给onChange作为事件发生的回调,xxx是一个函数。如果写成xxx()
则是调用xxx函数,并把xxx函数的返回值作为事件发生的回调。
那么,我们收集数据的时候都用同一个函数saveFormData
,标识到底是username还是password,就要通过参数来标识,所以就会写成saveFormData("username")
,那么很自然,函数就必须返回一个函数:
saveFormData = (valueType)=>{
return ()=>{
// ...
}
}
那么,参数valueType就是传过来的username或password,而返回值是一个函数,执行saveFormData函数,返回一个函数,这个返回的函数就是事件回调函数,那么它不就可以接到参数:
saveFormData = (valueType)=>{
return (e)=>{
// ...{valueType: e.target.value}
}
}
那么,只需要这样就能实现功能:
saveFormData = (valueType)=>{
return (e)=>{
this.setState({
[valueType]: e.target.value
})
}
}
为什么要[]
,搞清楚JS对象:
let obj = {
"username": "xiaoming" // 左边的username是字符串,这是最本质的写法,我们习惯简写把双引号省略
}
let obj2 = {
username: "xiaoming" // 和上面完全等价
}
// 那么
let a = "username";
let obj3 = {
a: "xiaoming" // 显然这个对象是:{a:"xiaoming"}
}
// 怎么办?
// 想想访问对象属性
let temp = obj2.username
// 本质上?
let temp2 = obj2["username"]
// 那么
let obj4 = {
["username"]: "xiaoming"
}
// 显然
let obj5 = {
[a]: "xiaoming" // obj5:{username: "xiaoming"}
}
最终完成了组件:
class Demo extends React.Component {
state = {username: "", password: ""}
saveFormData = (valueType)=>{
return (e)=>{
this.setState({
[valueType]: e.target.value
})
}
}
render() {
return (
<div>
用户名<input onChange={this.saveFormData("username")} text="type" name="username" /><br/>
密码<input onChange={this.saveFormData("password")} text="password" name="password" />
</div>
)
}
}
高阶函数:
- 像
saveFormData
一样,它是一个函数,它的返回值还是一个函数,这样的函数就属于高阶函数。 - 如果A函数接受的参数是一个函数,那么A函数也属于高阶函数。
- 常见的高阶函数:Promise、setTimeout、setInterval、数组身上的方法...
函数的柯里化:通过函数调用继续返回函数的方式,实现多次接受参数最后统一处理的函数编码形式。saveFormData
就是函数的柯里化。
不用柯里化实现:
class Demo extends React.Component {
state = {username: "", password: ""}
saveFormData = (valueType, e)=>{
this.setState({
[valueType]: e.target.value
})
}
render() {
return (
<div>
用户名<input onChange={(event)=>{this.saveFormData("username", event)}} text="type" name="username" /><br/>
密码<input onChange={e => this.saveFormData("password", e)} text="password" name="password" />
</div>
)
}
}
组件的生命周期
React找到组件类,创建组件实例对象,用实例调方法,哪些方法?生命周期方法和render方法。
之前讲过的render
:初始化渲染、状态更新以后。
组件挂载完毕时:componentDidMount()
。对应Vue的mounted()
。
组件将要卸载时:componentWillUnmount()
。对应Vue的destroyed()
。
旧版生命周期:
组件挂载:
constructor
componentWillMount
render
componentDidMount
组件更新:
-
setState
更新状态shouldComponentUpdate
:组件是否应该被更新,返回true更新,否则不更新(下面的生命周期都不会走),不写这个函数默认返回true,写了函数返回值不是布尔报错componentWillUpdate
render
componentDidUpdate(oldProps, oldState)
-
forceUpdate
强制更新状态:绕过shouleComponentUpdate
,其他和上面一样。this.forceUpdate() // 页面状态没有改变,强制更新一下
-
父组件render
componentWillReceiveProps(props)
:组件将要接收新的props,第一次不算,以后(父组件状态更新了)才算- 后面的和setState一样
组件卸载:
-
componentWillUnmount
-
代码卸载组件:
ReactDOM.unmountComponentAtNode(document.get....节点)
-
新版生命周期(React17版本以后,还是能用旧的,只是控制台有警告)
弃用三个:三个componentWillXxx
的前面加上UNSAFE_
。(一共四个willXxx
,除了componentWillUnmount
)
为什么?——React正在研究异步渲染,unsafe
不是表示“不安全”,而是在未来版本异步渲染的时候可能有BUG。
新增两个(这两个用法也很罕见):
-
挂载和更新时:
getDerivedstateFromProps(props, state)
- 这个钩子不是给实例
.
调用的,是静态方法,static
。 - 还要有返回值,返回状态对象
state Object
或null
。(不能undifine
) - 返回了状态对象,该状态对象会派生到组件实例的state里。
- 用于:state的值在任何时候都取决于props。
- 这个钩子不是给实例
-
更新时:
getSnapshotBeforeUpdate(oldProps, oldState)
-
Snapshot:快照
-
必须返回一个快照值
snapshot value
或null
。(不能undifine
) -
snapshot value
:随便什么值,数字、字符串等都可以 -
返回的值会传给
componentDidUpdate()
componentDidUpdate
实际上还有一个参数,共三个。componentDidUpdate(oldProps, oldState, snapshot)
-
在进行页面更新之前获取一些已有的旧信息,并可以通过快照往下传。(例如滚轮滚动位置等)
-
常用的生命周期: