React入门应该是这样的
源码:https://github.com/dansoncut/React-beginner-tutorial-TeacherEgg.git
视频地址:https://www.bilibili.com/video/BV1be411w7iF/?spm_id_from=333.337.search-card.all.click&vd_source=ae42119b44d398cd8fe181740c3eb984
1.组件和JSX
react并不是原生的JavaScript
所以使用react可以通过官网创建脚手架创建项目
但是我们不建议直接使用脚手架创建项目–这里我们使用CDN引入
CDN链接:https://react.docschina.org/docs/cdn-links.html
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
render方法里面是可以传入组件的,根一般都叫<App />组件名–组件又分为函数组件和类组件
JSX是原JS的一种扩展,jsx只能有一个根元素,不仅可以写html还可以写JS
2.插值和状态
对数组进行遍历,数组的位置需要放到APP组件里面方便复用,又因为数组的内容在类初始化的时候就有了,因此写在constructor里面,因为我们已经使用extend继承父类,所以constructor里也要用super来调用父类的函数。
map是回调函数,也就是说li标签是后面加入页面的,页面就变成动态的了,从静态变成动态就存在状态了。
我们把数组的内容放到React状态里,就可以动态地进行操作,因此我们需要定义状态值。
我们可以使用this.state,this.state是一个对象,在这个对象里面我就可以定义各种状态了
比如我们就可以把刚刚的数组用键值对的方式保存,这里的值就是初始化的状态值了
刚才的数组也就不需要了,相应的我们也需要在下面map那行加上this.state进行调用,我们的页面也就可以看到遍历的列表了
3.挂载和状态设置
实际的数据我能很少会进行写死,因此我们打算获取API的数据并且放到这个数组里面,我们需要思考一下什么时候把获取到的API呈现到页面里?
一般来说,我们都是想要先呈现整个页面的轮廓,在把外面获取回来的数据呈现到页面上,这样用户就不会看到先数据后轮廓的奇怪现象了
React提供了componentDidMount方法,这个方法可以使我们在组件挂载完毕后再执行这个方法里面的内容
因为要获取API的数据,这里就使用Fetch,并且把获取到的数据转化成json格式,然后我们看看控制台输出了东西。
这里可以看到有个warning,我们等会解释,想看看我们获取到的数据,但还不是我们真正想要的东西,得到results里面的数组,
很明显我们需要用这个API的数组来替代初始化状态里的数组,我们可不可以直接用赋值的方式来操作呢,同时在控制台输出看看结果如何
我们发现this.state里面的东西改了,但是我们的页面居然没有更新,这是怎么回事,和原生JS里的不是一回事?
其实这是react的其中一个特点,react不会让我们的代码像以前那样频繁操作DOM,
如果我们直接用赋值的方式修改对象属性,对象属性所处的内存位置还是没有改变的
我们要把这个对象属性的内存位置完全改了,最简单的就是再创建一个新的对象,
还好React提供了一个this.setState方法,也就是改变状态值的方法,
我们可以直接在setState里面传入一个对象,并且输入我们要改变的键值对。
于此同时我们还需要把我们li标签里的插值表达式更新一下,因为数组里面是以对象的形式存储元素的
刷新页面,网页内容就变得丰富起来了
不过,有些同学可能会有疑问,如果我们初始状态值里面有其它值,我改写一下,那其它的不也被覆盖了
很简单,我们在初始状态值这里新增一对键值对,并且在setState后面再把state对象的内容输出就知道了
我估计有同学卡在这里,我们的页面正常显示新的状态值,可是输出的却是原始的状态值,为什么呢
其实setState如果这样设置会进行回调,因此大部分情况任务队列会先执行console.log,然后再执行this.setState里的回调,这属于JS的基础了
不理解也没关系,因为setState里面可以输入两个函数作为参数,第一个函数主要用来更新状态值,第二个函数可以用来更新状态值后的操作
异步–第二个函数会在第一个函数后执行,那我们就可以在第一个函数里面返回我们要更新的状态值,还是用对象的方式返回,第二个函数就输出我们的状态值
结果大家可以发现一下子解决了两个问题
首先更新后的状态值被正常输出到控制台,其次我们并没有在更新的时候把原来的状态值给覆盖掉,这里其实叫浅合并
把两个对象的键值对来一个合并,但是只修改需要修改的,不用修改的就不动它
4.生命周期
目前:我们控制台还剩下一个报错问题–其实是我们遍历的时候,应该为每一个子项目增加一个唯一的key属性
为什么需要这么麻烦呢,其实对我们的性能有大大的好处。
如果我们后期对我们的列表中其中一个子项目进行动态更新,React就可以根据这里唯一的key属性进行定位更新,而不是把使用的DOM都更新一遍
还好我们的子元素里面有个url可以作为唯一的key,这里就以这个url为key添加到子项目上。
可是当我们刷新页面的时候,控制台还是有报错信息,按道理应该不会出错的,也是符合官方文档的要求把key设置好的呀
为了查看是不是key在捣蛋,我们不妨用map遍历的时候产生的index参数作为key,因为每个元素的index值都不一样,我们暂且拿来使用
大家可以发现控制台没有报错了,可以看出确实是key的问题,但是我们确实又能够把状态值输出出来,这里就涉及到比较底层的概念了
也就是代码的执行顺序–我们在类组件中分别为constructor,componentDidMounted和render方法添加输出内容来查看执行的顺序
并且把key改为我们原先出错的那个值,大家看看输出的顺序
首先是构造函数,因为里面是初始化的内容, 接着是render渲染,注意并没有从上往下这样执行,初始化之后就直接进行渲染了
接着再执行组价挂载后的内容,最后再次进行渲染
这其实是比较简单也比较核心的React生命周期,这里我们也得到了问题的答案
我们第一次渲染的时候,这里的key就被设置了,而此时初始化数组是由字符串组成的,而不是对象组成的,根本就获取不到对应的值,根本就没有pokemon.url属性值,所以就会提前进行报错,在执行下一步组件已挂载之前就给你报错了,即使后面能获取到新的key值,这个错误也是已经存在了
千万不要觉得这个流程没什么,很多人不理解React就是没理解基本的生命周期
我们要解决这里的问题也很简单,直接把初始的数组值设置为空就好(设置为空就不会进行遍历,也就不会执行代码,就不会报错了)。
所以我们在开始设计应用的时候就把这些因素给考虑清楚,除了要考虑这些问题以外,我们还有一个重要的问题,现在的代码越来越多
5.拆组件
虽然说JSX让我们可以在JS上写HTML,但是我们把所有的JS写在HTML文件上也是不对劲的
因此我们需要把JS全部挪出来,这里就创建一个src文件夹,在里面新增一个index.js文件夹,并且把所有的JS代码都剪切过去
移动完JS代码以后,我们记得在HTML里的script标签上注明要引用的JS文件
如果这个时候你是直接打开HTML文件的方式进行预览的话,会遇到跨域问题,可以用VSCODE的liveServer来打开网页文件
或者用http-server这样的工具来打开网页,就会解决跨域
没问题的话回到JS代码里面
这里我们最开始的createRoot和最后的render的两行代码目前不需要操作,浪费屏幕空间,我们可以把这个App组件给拆出来
创建App.js的文件,也是放在src文件夹里面,然后我们把整个类组件放到App.js里面,完成之后我们把App.js引入到HTML文档里面
注意是先引用App.js组件文件,再引用index.js文件,原本可以使用import和export来展示为了降低难度,把重点放到React核心里面,就以这种原始的方式演示给大家看,大家抓住重点就好
6.事件处理器
接着我们实现一个功能,就是输入框输入内容的时候可以自动过滤下底下的列表
那我们肯定要获取输入框的内容对吧,因此我们需要有监听输入框的能力
原生的JS用的是事件处理器,React也有事件处理器,语法稍微有点点不一样,但非常像
比如当输入框有变动的时候,我们就可以用onChange来进行监听,并且在里面的函数输入我们要执行的内容,这里注意事件名是以小驼峰来命名的
其次事件接的逻辑要先用大括号括起来,函数里面也是有event,可以先打印event回忆一下
如果我们在输入框中输入b,我们就可以看到event的输出内容,这里event的target属性,target里面的value值就是value值的输入框的内容了
正是我们的需要的内容,如果用户不断输入或者删减,检索的列表就会随之变动,这里就存在状态
我们应该把输入框的值保存起来,因此我们需要在初始状态值开先进行提前定义,接着我们要更新这个searching状态值,只需要在有输入的时候进行setState就行了,操作和刚才一样,也是来个浅合并,合并后查看更新后的值。
大家可以看到当我们输入值的时候,状态值也随之变动,和输入框里的是一致的,
但是如果把这么多内容的函数写在HTML标签里面,我们是很难轻松判断网页结构的,
因此我们设置一个类方法onChangeHandler并且把onChange里的函数复制过去,我们就可以删除写在HTML里面的逻辑了
这里特别要注意的是调用类方法的时候要加上this,接下来我们就要根据输入的内容来更新列表了,
但是我们不能对原来的数组数据信息更新,应该创建一个过滤后的数组,而且过滤后的数组也是有状态的,因此我们需要在创建一个状态值
这个过滤数组的初始值需要和初始的pokemon状态值一致,因此我们还需要在Fetch的时候同时更新这个值,然后我们再把ul标签的逻辑更改为过滤数组
问题来了,我们要在那个位置更新这个过滤数组呢,一定要想清楚刚刚说的生命周期,在下确定,可以使用排除法
首先构造函数里面已经有了初始值了,所以肯定不是在构造函数中更新过滤数组
那能不能在组件已挂载后更新数组呢?也不好,因为组件挂载后我们初次需要获取的是原始的数据,而不是过滤的数据
那我们只能在渲染里面更新数组了?更不好,如果直接在render方法里面使用setState,会容易造成死循环,
我们又要进行React进行渲染,渲染的时候又要改变状态值,状态值修改又会触发渲染,这让React怎么办才好了
因此我们需要在有事件的时候才会更新这个过滤数组,直接在onChangeHandler里先创建一个临时的变量,然后我们对原数组进行过滤
如果输入的内容匹配得了,那我们就返回到这个临时变量,最后我们进行setState的时候就只需要更新过滤数组了,
过滤数组才是我们输出的内容,我们暂时也不需要searching这个状态值了,就可以删除掉了
这个时候我们输入内容并且删除内容的时候,下面列表就会自动更新了,当然实际中应该考虑大小写,时间问题这里就不考虑了
7.props
现在我们代码又突然变得多起来了,我们需要再来拆组件让我们的项目结构更加清晰才行,目前比较需要拆出来的是这个列表。
因为在结构里面写逻辑会让我们结构不直观。
这里就在src文件夹里创建一个components文件夹,这个文件夹用来存放可复用的组件,在components文件夹里,为了分清楚不同的组件,
我在创建一个lists文件夹来存放列表组件,最后就可以创建我们的组件文件了
这里就命名为lists-component.jsx,我们是可以用jsx后缀来存放组件的,用js后缀也是没错的,两个命名方式 ,而且也更容易区分可复用组件
接着还是创建一个类组件,并且把基本的结构先写出来,记得在HTML文档中插入组件,回到App.js就可以把ul里的内容做一个剪切
并且用标签的形式写入我们的Lists组件,然后把剪切的内容在粘贴到Lists组件里面,
Lists组件这样写能运行吗?这里有两个新手常见的问题
首先我们大括号执行的逻辑会返回多个同级的li标签,我们必须进行包裹(只能有一个根组件),返回同属于一个父元素的组件。
比较简单的操作就是把App.js的ul标签也复制过来作为包裹,把原来App.js的ul标签给删除掉
第二个问题,Lists组件是没有自己的状态值的,因此这里用this是调用不了任何东西的,这个状态值是从App.js里面拿回来的
所以我们要想办法从父组件往子组件里传东西,组件里面有一个属性props,也就是properties的缩写
这里在子组件渲染的时候打印给大家看看,暂时先把逻辑代码注释掉,大可以看见控制台里面,lists组件渲染后,果然可以打印出一个对象出来
只是这个对象里面现在没有东西,因为我们根本没有往组件里面传东西,当然是为空的,
传值很简单,就先给HTML加属性一样,比如我们要给子组件传入一个叫pokemonsLists的属性,这个属性值就用我们过滤后的数组,
这个时候我们就可以看到我们控制台这个对象有东西了,而且是随着状态值的更新而更新的
我们就可以把props里我们需要的属性值先取出来(通过解构)并且保存为变量,然后再用这个变量进行遍历就好了
页面依旧可以显示出来,这个组件就成功被拆出来了
虽然我们的结构看起来比较清晰,但是如果我们要复用这个输入框也是不方便的,因此我们可以把输入框也拆解出来
首先:创建一个input文件夹,再这个里面创建一个input-component.jsx作为组件文件,这里加上components为了检索时更清晰一些
还是一样,我们先把类组件基本的东西给写一下,然后再HTML里面引入文件,最后回到App.js里面把组件
这次的input标签并不像列表那样有很多东西,主要就是属性,也就是这里的onChange里的值需要传进来。
那我们就可以直接给子组件传入一个叫onChangeHandler的属性,属性值就调用父类的方法就好,还是一样,先另存这个变量属性,传入进去就好了
8.遍历
现在我们想为每个列表添加对应的图片,我们要获取的图片,当我们改变数字的时候,图片也会相应地发生变化
现在我们就可以在map遍历的时候把这图片也加进去,这个时候我们就需要用到map遍历时候的index,还是一样
return的时候只能是一个父元素,因此我们就用div进行包裹,接着把li和img标签放在同级,并且用src属性引入我们的图片
因为我们要操作图片地址,需要用到js逻辑。因此这里需要先用大括号,再用模板字面量,又因为序号是从0开始的,图片是从1开始的
因此这里要index+1,还有一点要注意,因为我们实际上既遍历产生li标签,也遍历产生img标签,因此两个标签都需要加上key属性方便React提高性能
当然我们也可以直接为父元素div标签直接添加key,这样也是不会报错的,在看我们页面的时候,图片就会显示出来
但是还是有问题的,不管检索那个字符,第一张图片还是原封不动在哪里,肯定不对,以后的实战开发也会遇到这样的问题
在我们input组件里面,每次遍历的时候是根据index遍历的,如果我们过滤后的数组只有三个,我们的index始终是02,而并不是24或者5~7
我们状态值并没有和这个index有什么关系,要解决这个问题有很多方法,比如说我们就可以在组件挂载后获取API数据的时候,为每一个元素对象增加一个属性,
只需要用map遍历,并且用map的index作为属性值就好了,这个时候大家可以看到挂载后每个元素就多了一个属性了
接着我们只需要回到input组件里面,因为props接受过来的是过滤后的数组,把模板字面量里的index+1改为pokemon.id就好了
因为前面已经+1了,这里就不用+1了,而且当前map的index参数我们也不需要用到了
9.样式CSS
添加样式–如果我们要在JSX上写样式也是没问题的,但是我们不能想原生那样写样式,直接写style是会出错的
因为在JSX里style的值需要传入一个对象,有因为JSX里的逻辑需要用大括号括起来,所以这里需要用到两个大括号
既然是对象就用户键值对的形式来写,style接收一个对象,我们索性另外保存一个对象,并且为这个对象添加多个CSS键值对,最后应用到style也是可以的
这样就不用写两个大括号了,但是实际中我们会有多个组件样式文件,样式文件里一般会设置有很多class
要在JSX里使用到这些class,我们需要用到className
比方说我们打算使用ul-container的class,我们可以用引号围起来,接着新创建一个lists-style.css文件
并且在里面填写我们需要的样式
如果是用脚手架来创建项目一般都会单独引入到各个组件里面,这里就直接引入到HTML的head标签里面,
这个时候我们就可以看到样式了
10.函数式组件–useState
如果能够把当前类组件转化成函数式组件,那你对react的核心就有了一个质的了解
现在就来把每个文件都检查一遍,index.js不用动,这里我们就是直接使用组件而已
所以我们来看App.js,我们先把类组件的东西全部注释掉,并且创建一个函数组件的基本结构
其实就是一个函数,里面进行return,这里没有render方法,我们需要渲染什么,直接在return里面写就好了
所以我们把类组件的render方法里的return内容复制过来就好,接着我们的类方法就直接在App函数组件里创建一个函数就好了
也是一样直接复制过来并且作变量声明就好了,但是很明显这个事件处理函数是不能用的
因为这里是函数组件,不是类组件,这里的this是会出错的,因此下一步我们要好好考虑如果设置状态值
因为函数组件没有构造函数,因此React提供了useState函数来初始化状态值,这个useState函数创建的时候,我们可以用解构赋值的形式来命名两个变量
第一个是状态值,第二个是设置状态值的方法,我们打印一下结果,让大家有个初步的概念
大家可以看到打印了空数组,其他报错信息先不过
那么同理我们还可以设置多个状态值,比如过滤的数组我们也需要设置一下
既然有了初始值以后我们先修改一下下面调用的状态值,首先我们不用this.state这样来调用了,直接向正常变量那样进行使用就好
其次我们在修改状态值的时候,直接用useState设置的第二个值,并且传入新的值就好了,
接下来我们把下面的this也给去掉,向正常调用就好了,这样页面就没有报错了
11.函数式组件–useEffect
有了初始状态值,我们还是一样要用API的数据,但是函数组件里面并没有什么componentDidMount方法
于是我们目前只能把fetch放在初始状态值下面执行,当然我们记得用函数组件的方式更新状态值,但在我们保存文件前
我们先在Fetch前后作两个控制台输出,如果我们现在保存文件,查看控制台发现刚才两行console.log都在不断执行
也就是说我们不断进行Fetch操作,原因呢
函数组件和类组件虽然很不同,但是依旧是当状态值更新的时候才会去执行组件里面的内容,
函数组件不同的时,函数组件会把组件里的代码从上到下在运行一次,又因为Fetch到的内容是会把状态值的内容给改写
因此改了之后又会执行函数组件的内容,又Fetch,又改写,形成了死循环
换句话说,Fetch过来的数据对原本的App函数是有副作用的,并不是App函数能够完全掌控的了的,
对于有副作用的东西,React提供了useEffect函数来处理,useEffect接受两个参数,
第一个参数就是我们要执行什么内容的函数,那我们就直接把Fetch的内容放到这个函数里面
接着我们思考一个问题,我们的Fetch究竟什么时候第一次执行,什么时候在执行呢,因此就需要useEffect的第二个参数了
第二个参数是一个数组,第二个参数让我们决定什么时候再次执行第一个参数里的内容
如果我们把数组设置为空,表示我们只需要执行一次
以后并没有什么条件可以让这个Fetch再次执行,这样就是我们想要的结果
死循环就消失了
App.js改造完成后我们就来到Lists组件这里,
还是一样我们先直接创建一个Lists函数组件,并且把需要渲染的内容放入return里面,但是这里涉及到从父组件传值到子组件,也就是props
实际上这个props就是创建函数组件的一个参数,如果我们把他输入到控制台是可以看到内容的
因此我们还是可以直接用解构赋值的方式获取属性值,只不过我们并不需要this而已
另外我们把前面设置好的CSS对象也一起定义了,这个时候我我们的控制台就不会报错
但实际上props本来就是参数,我们可以直接在定义函数的时候就进行解构赋值,优化代码
最后我们来到input组件
简单修改一下