跟着尚硅谷的天禹老师学习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{
// 即使不写构造器也会直接调用父类的构造方法,当写了子类构造方法,必须明确地调用supercontructor(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) // undefinedreturn <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.statereturn (// 注意这里一定不要写成带括号的,不然会将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 = namethis.price = pricethis.wheel = 4}// 类中可以写赋值语句,下方代码就是给实例添加一个属性aa = 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.statereturn (// 注意这里一定不要写成带括号的,不然会将clickHandler的返回值绑定到onClick上<h1 onClick={
this.clickHandler}>今天天气很{
isHot?'热':'冷'}</h1>)}// 自定义方法写成用赋值语句+箭头函数的形式// 这里的自定义方法一定要写成箭头函数clickHandler = ()=>{
const isHot = this.state.isHotthis.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.refsalert(input1.value) // 注意这里拿到的是真实的DOM}showData2 = () => {
const {
input2 } = this.refsalert(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.refsalert(input1.value)}showData2 = () => {
const {
input2 } = this // 注意这里修改了,不是原来的this.refsalert(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被官方推荐