跟着尚硅谷的天禹老师学习React
看视频可以直接点击 b站视频地址
1、简介
原生JavaScript的问题
1、原生JavaScript操作DOM繁琐,效率低,可以将DOM操作托管给React。尽管有了jQuery,最后的代码量也是很大的。
2、使用JavaScript直接操作DOM,会触发大量的重绘重排,React采用了虚拟DOM。
3、原生JavaScript没有组件化方案,代码复用率低。
React的特点
1、采用组件化模式,声明式编码,提高开发效率和组件复用率。
2、在React Native中可以使用React语法进行移动端开发。
3、使用虚拟DOM和Diffing算法,减少与真实DOM的交互
虚拟DOM特点
本质是一个js的Object
虚拟DOM比真实DOM要”轻“一点,最终会被React转换成真实DOM
jsx注意事项
- 定义虚拟DOM时不要写引号,直接写标签就行
- 混入js表达式时用单花括号
- class使用className,因为js操作DOM的时候就是用的className,class是当时js的保留字
- 关于style使用变量需要双花括号,可以理解成先用单花括号表示这是一个变量,然后传入一个对象,或许也可以传入一个字符串。同时短横杠的属性写法要转成驼峰形式。
const className = 'test'
const VDOM = (
<div>
<p className={className} style={{fontSize:'22px'}}>
</div>
)
- jsx只允许有一个根元素,可以参考Vue
- 标签必须闭合
- 标签如果用小写的标签,会被React自动理解成html的同名标签,找不到就报错;如果是组件就需要写成大写开头,React来渲染这个组件,找不到就报错。甚至还可以写一个中文标签。
- 单花括号内只能混入表达式,不能混入语句(比如for循环、if、switch等等)
- 一个表达式会产生一个值 ,注意函数也可以用表达式来声明
- 如果需要用if就用三目运算符,如果需要用for就用forEach、map等等
- 如果要用循环输出一个jsx?(一般来讲不会用index作为key,这里只是简单地试一下)
const frame = ['React','Vue','Angular']
const VDOM = {
<div>
frame.map((item,index)=>{
<p key={index}>{item}</p>
})
</div>
}
2、组件实例三大核心属性,state、proprs、refs
模块与组件
模块应该是专指js模块。
组件是实现一个完整功能的代码和资源的集合(比如会包含html、css、js、音视频等等)
模块化:所有js代码都按一定规则抽象。
组件化:各个功能按照一定规则抽象
js类
class Person{
// 构造方法
constructor(name,age){
// this指向new出来的实例对象
this.name = name;
this.age = age
}
// 一般方法
speak(){
// 一般方法在原型上,供实例使用
// this指向实例对象
console.log(`my name is ${this.name}`)
}
}
(new Person('zzy',22)).speak.call({a:1,b:1}) // my name is
class Student extends Person{
// 即使不写构造器也会直接调用父类的构造方法,当写了子类构造方法,必须明确地调用super
contructor(name,age,grade){
super(name,age); // super要置于最顶端
this.grade = grade;
}
// 重载
speak(){
console.log(`my name is ${this.name}, im in grade ${this.grade}`)
}
}
const s1 = new Student('zzy',22,16)
s1.speak() // my name is zzy,im in grade 16
- 类的构造方法不是必须要写的,除非要对实例属性进行一些初始化
- 如果子类写了构造方法,那么必须调用super,并且放在最顶端
- 类中定义的所有方法,都是放在实例上面
组件
- 函数式组件
function Demo(){
console.log(this) // undefined
return <p>函数式组件</p>
}
React.render(<Demo/>,document.getElementById('someid'))
注意这里的this指向,为undefined,因为开启了严格模式,禁止了this指向window。
另外中文在产物中会被转成unicode码
- 函数式组件的渲染过程:
- React解析组件标签Demo,然后找到了Demo
- 发现是函数式的组件,立即调用这个函数,获取返回值,转为真实DOM。
- 类组件(比较适用于复杂组件)
class Demo extends React.component{
render(){
return (
<p>{ 'this is demo'}</p>
)
}
}
ReactDOM.render(<Demo/>,document.getElementById('id'))
- 必须写render并有返回值
- 函数式组件的渲染过程:
- React解析组件标签Demo,然后找到了Demo
- 发现是类定义的组件,立即调用new出一个实例,并通过这个实例调用render函数。
- 将render函数返回的虚拟DOM渲染成真实DOM
注意组件的三个属性,refs、state、props,这些在React.component中已经被定义过。
Demo也称组件实例对象。
关于简单组件和复杂组件,有状态的就是复杂组件,适用类组件,没有状态的就是简单组件,适用函数组件。虽然hooks也提供了函数组件保存状态的能力,还是推荐按照上面的建议来定义组件。
React通过组件的状态驱动视图。
state
初始化state
<div id="container">
</div>
<script>
// 创建组件
class Weather extends React.component{
constructor(props){
super(props)
// 初始化状态
this.state = {
isHot:false
}
}
render(){
return (
<h1>今天天气很{this.isHot?'热':'冷'}</h1>
)
}
}
ReactDOM.render(<Weather/>,document.getElementById('container'))
</script>
js中绑定事件处理函数
<div>
<button id='btn1'>
<button id='btn2'>
<button id='btn3' onclick="test()">
</div>
<script>
const btn1 = document.getElementById('btn1')
const btn2 = document.getElementById('btn2')
const test = function(){
alert('click')
}
btn1.addEventListener('click',test)
btn2.onclick = ()=>{test()}
</script>
注意React将所有的绑定的事件名都给封装了,比如onclick应该用onClick来写。
<div id="container">
</div>
<script>
// 创建组件
class Weather extends React.component{
constructor(props){
super(props)
// 初始化状态
this.state = {
isHot:false
}
}
render(){
const { isHot } = this.state
return (
// 注意这里一定不要写成带括号的,不然会将clickHandler的返回值绑定到onClick上
<h1 onClick={this.clickHandler}>今天天气很{isHot ?'热':'冷'}</h1>
)
}
clickHandler(){
this.state.isHot = !this.state.isHot;
}
}
ReactDOM.render(<Weather/>,document.getElementById('container'))
</script>
书写上述代码时会发现,控制台报错了,这是因为等到事件处理时,相当于一个普通的函数,不会被,可以参考下面的两段代码
class Person{
// 构造方法
constructor(name,age){
// this指向new出来的实例对象
this.name = name;
this.age = age
}
// 一般方法
speak(){
// 一般方法在原型上,供实例使用
// this指向实例对象
console.log(`my name is ${this.name}`)
}
}
const p1 = new Person('tom',12)
p1.speak() // my name is tom
speak = p1.speak
// speak() // Cannot read properties of undefined (reading 'name')
// 这是因为class中代码,会自动将转为严格模式,防止this指向window
const f1 = function(){
console.log(this)
}
const f2 = function(){
'use strict'
console.log(this)
}
f1() // window
f2() // undefined
- 定义事件处理函数时,将其放在原型对象上(类里面),供实例使用
- 由于事件处理函数是作为onClick的回调,所以不是通过实例直接调用的,是直接调用
- 类内部默认开始了局部的严格模式,所以事件处理函数中的this是undefined
解决类中this指向的问题
比如上面可以改成下面的代码,使用bind改变this指向并返回新的函数。
constructor(props){
super(props)
// 初始化状态
this.state = {
isHot:false
}
// 解决clickHandler中this指向问题
this.clickHandler = this.clickHandler.bind(this)
}
相当于实例上加了这么一个方法,注意在clickHandler依然需要在类内声明。
setState
state是不可以被直接赋值的,需要借助setState。
修改clickHandler代码:
clickHandler(){
//this.state.isHot = !this.state.isHot;
this.setState({
isHot:!isHot
})
}
注意这里setState是一个更新/合并操作,并不是整体的赋值
这里的construcor只调用了一次,而不是每次setState都要重新new,而render会除了初始化的那一次,只要setState修改了状态,render就会重新被调用。
state的简写方法
实例对象的属性的简写
class Car{
constructor(name,price){
this.name = name
this.price = price
this.wheel = 4
}
// 类中可以写赋值语句,下方代码就是给实例添加一个属性a
a = 1
}
const c1 = new Car('宾利',299)
const c2 = new Car('宝马',199)
下面的代码才是开发中会常用的形式
<div id="container"></div>
<script>
class Weather extends React.component{
// 直接在这里声明state即可,不用写在构造函数中
state = {
isHot:false
}
constructor(props){
super(props)
// 初始化状态
}
render(){
const { isHot } = this.state
return (
// 注意这里一定不要写成带括号的,不然会将clickHandler的返回值绑定到onClick上
<h1 onClick={this.clickHandler}>今天天气很{isHot?'热':'冷'}</h1>
)
}
// 自定义方法写成用赋值语句+箭头函数的形式
// 这里的自定义方法一定要写成箭头函数
clickHandler = ()=>{
const isHot = this.state.isHot
this.setState({
isHot:!isHot
})
console.log(this) // weather实例
}
}
ReactDOM.render(<Weather/>,document.getElementById('container'))
</script>
理解State
1、组件可以被理解为“状态机”,通过更新组件的State来更新对应页面的显示(重新渲染组件)
2、组件中render方法中的this为组件的实例对象
3、组件自定义方法中this为undefined,如何解决?
- 强制绑定this,通过bind方法返回一个新的具有this指向的方法
- 箭头函数(以及赋值语句,也是最常用的形式)
4、状态(state)数据不能直接修改或者更新
Props
简介
<div id="container"></div>
<script>
class Person extends React.Component{
render(){
const {name,sex,age}
return (
<ul>
<li>{name}</li>
<li>{sex}</li>
<li>{age}</li>
</ul>
)
}
}
const person = {
name:'1',
age:'1',
sex:'1'
}
// 下面两种方式是等价的
ReactDOM.render(<Person name={person.name} age={person.age} sex={person.sex}/>),document.getElementById('container')
ReactDOM.render(<Person {...person}/>),document.getElementById('container')
</script>
注意"…"运算符
const person = {
name:'1',
age:'1',
sex:'1'
}
// 构造字面量时使用展开语法
const person2 = {...person}
const person3 = {...person,name:"3"} // 合并操作
console.log(...person) // 报错
console.log(person2) // person的复制
补充,这里的"…"运算符并非数组的扩展运算符,而是使用对象字面量复制一个对象。详见展开运算符。
但是要注意,React中的这段代码,单花括号是指在此处使用js表达式,而非上面的复制对象,是使用reactbabel来编译过的。
ReactDOM.render(<Person {...person}/>),document.getElementById('container')
对Props进行限制
注意,向标签种传入的值,都被默认成字符串,像下面的age就会被认为是字符串19,如果在一些操作的里边,使用"+"运算符,会被认为成字符串运算
ReactDOM.render(<Person age="
19" name="1" sex="1"/>),document.getElementById('container')
所以应当改成下面的代码
ReactDOM.render(<Person age={19}
/>),document.getElementById('container')
当别人复用我们的组件时,不知道如何使用,这就要加一些限制。在Person类同级目录下中添加下面的代码。
// 需要引入 prop-types库
// 在React16版本后单独分出一个prop-types库
// 之所以修改是因为,React官方不希望React库变得太大,并且一般组件也有可能不需要限制Props
Person.propTypes = {
// 注意这里都是需要写成小写,为了和内置类型区分
name:React.propTypes.string.isRequired, // 必填且为字符串
sex:React.propTypes.string,
sex:React.propTypes.number,
speak:propTypes.func, // func是为了和function关键字区分
}
// 赋默认值
Person.defaultProps = {
sex:"unknown",
age:18,
name:"unknown"
}
Props的简写方式
注意props是禁止修改的,这里好像和Vue是一样的。
要把props加入到类的静态属性中(static)
Person extends React.Component{
state = { }
render(){ }
static propTypes = {
// 注意这里都是需要写成小写,为了和内置类型区分
name:React.propTypes.string.isRequired, // 必填且为字符串
sex:React.propTypes.string,
sex:React.propTypes.number,
speak:propTypes.func, // func是为了和function关键字区分
}
static defaultProps = {
sex:"unknown",
age:18,
name:"unknown"
}
}
类式组件中的构造函数
通常,在React中,构造函数仅用于以下两种i情况:
- 通过给 this.state 赋值对象来初始化内部state
- 为事件处理函数绑定实例
在React组件挂载之前,会调用它的构造函数。在为React.Compnent子类实现构造函数时,应在其他域据之前调用super(props),否则,this.proprs在构造函数中可能会出现未定义的bug。
总结:类中的构造函数完全可以省略,但是如果写了构造函数,并且在构造函数的声明中写了props,那就需要在super中传入props。也就是说,构造函数是否接受过props,是否传给super,取决于是否希望在构造函数中通过this访问props。
但既然传入了props,也没有必要使用this来访问props。
这样来看,class中的构造函数写不写都行。
函数式组件使用props
函数式组件因为没有this,所以不可以访问state,refs,但是可以接收props,通过传参来实现。
function Person(props) {
const { name, age, sex }
return (
<h1>
<p>{name}</p>
<p>{age}</p>
<p>{sex}</p>
</h1>
)
}
// 函数式组件只能这样来限制props
Person.defaultProps = {
sex: "unknown",
age: 18,
name: "unknown"
}
Person.propTypes = {
// 注意这里都是需要写成小写,为了和内置类型区分
name: React.propTypes.string.isRequired, // 必填且为字符串
sex: React.propTypes.string,
sex: React.propTypes.number,
speak: propTypes.func, // func是为了和function关键字区分
}
理解Props
- 一定要写好propTypes,如果写成小写t,也不会报错,但是效果就没有了,defaultProps同理。
- 每个组件对象都会有props属性,保存了通过标签传入的属性。
- 组件标签的所有属性都保存在props上
- 在组件内一定不要修改props数据
- props的作用是从组件外部向组件内传递变化的数据
refs的基本使用
字符串形式的ref:
实现点击按钮弹窗和input框失焦弹窗
<div id="container"></div>
<script>
class Demo extends React.Component{
showData = () => {
const { input1 } = this.refs
alert(input1.value) // 注意这里拿到的是真实的DOM
}
showData2 = () => {
const { input2 } = this.refs
alert(input2.value)
}
render(){
return (
<div>
<input ref="input1" type="text" placeholder="点击按钮"/>
<button ref="btn" onClick={this.showData}>点击</button>
<input onBlur={this.showData2} ref="input2" type="text" placeholder="失去焦点"/>
</div>
)
}
}
React.DOM(<Demo />,docuemnt.getElementById('container'))
</script>
回调形式的ref:过时的API
React官方未来可能会废弃掉上面的字符串形式的ref,可能是因为字符串形式的ref效率不高。一般会考虑使用回调函数形式或者createRef这个API
<div id="container"></div>
<script>
class Demo extends React.Component{
showData = () => {
const { input1 } = this // 注意这里修改了,不是原来的this.refs
alert(input1.value)
}
showData2 = () => {
const { input2 } = this // 注意这里修改了,不是原来的this.refs
alert(input2.value)
}
render(){
return (
<div>
<input ref={currentNode => this.input1 = currentNode } type="text" placeholder="点击按钮"/>
<button ref="btn" onClick={this.showData}>点击</button>
<input ref={currentNode => this.input2 = currentNode } onBlur={this.showData2} type="text" placeholder="失去焦点"/>
</div>
)
}
}
React.DOM(<Demo />,docuemnt.getElementById('container'))
</script>
关于这里的ref回调函数中的this,因为写的是箭头函数,所以this和render中的this相同,render的this实际指向了实例本身。
回调函数中直接在实例内部声明了input1、input2,所以在事件处理函数中直接用this.input1、this.input2来拿数据。
回调函数执行次数问题
如果ref回调函数(就是上面的写法)是用内联函数形式定义的,更新过程中它会被执行两次。这是因为,在每次渲染时会创建一个新的函数实例,所以React清空旧的ref并且设置新的。通过将ref的回调函数定义成class绑定的函数的方式,可以避免上述问题,但大多数情况下它是无关紧要的。
第一次传入了null,这是为了确认原来的旧值能够确认被清空;第二次传入了节点信息。
试一下"class的绑定函数":
saveInput = (currentNode) => {
this.input1 = currentNode
}
render(){
return (
<div>
<input ref={this.saveInput} type="text" placeholder="点击按钮"/>
<button ref="btn" onClick={this.showData}>点击</button>
</div>
)
}
补充:如何在jsx中写注释:
render(){
return (
<div>
{ // 加上花括号以写注释 }
</div>
)
}
这里只是来看一下class绑定函数,实际开发最好还是写成内联的,不然逻辑就更多了。
createRef API : 最推荐的形式
React.createRef调用后可以返回一个容器,这个容器可以存储被ref所标识的节点
可以理解成,当input的ref传入了myRef,这个input节点会被存入React专门存放节点的容器中
但是要注意,这个容器最好只保存一个节点,“专人专用”,不然就被其他节点给占用
<div id="container"></div>
<script>
class Demo extends React.Component{
/*
React.createRef调用后可以返回一个容器,这个容器可以存储被ref所标识的节点
可以理解成,当input的ref传入了myRef,这个input节点会被存入React专门存放节点的容器中
但是要注意,这个容器最好只保存一个节点,“专人专用”,不然就被其他节点给占用
*/
myRef = React.createRef()
showData = () => {
alert(this.myRef.current.value) // 注意这里有改动
}
}
render(){
return (
<div>
<input ref={ this.myRef } type="text" placeholder="点击按钮"/>
<button ref="btn" onClick={this.showData}>点击</button>
</div>
)
}
}
React.DOM(<Demo />,docuemnt.getElementById('container'))
</script>
理解ref
- 字符串形式的尽量避免但老代码中肯定很常见,经常用的是回调形式并且内联的很方便,createRef API被官方推荐