目标
- React 基础
- 了解组件化思想
- 掌握组件状态
- JSX 语法
- 掌握条件渲染
- 掌握事件处理的方法
- 修改状态
- 修改样式
- 了解组件间传值的方法
React 基础
通过上一节课的课后任务我们可以感受到,手动创建、管理页面的 DOM 是非常繁琐的,而且需要手动管理状态,保证数据和 DOM 的同步,如果遇到一些交互比较复杂的场景,实现起来又会变得更加困难。现在通过 React 可以让我们从 DOM 操作中解放出来,让创建大型复杂应用变得非常轻松,这节课来学习一下 React 的基础知识。
组件化思想
组件系统是现代 Web 开发中的一个重要概念,它允许我们使用小型、独立通常可复用的组件构建大型应用,几乎任意类型的应用界面都可以抽象为一个组件树。我们可以简单认为组件是 Web 页面中的一个独立的功能单元,它可以有自己的状态、DOM 结构、逻辑交互,组件最大的价值是可复用性,我们只需要将独立的功能代码封装成组件,需要使用的时候传递给它对应的数据就可以了,不需要再额外编写重复的功能代码。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zTbJSwv1-1640574606913)(imgs/components.png)]
上图来自 Vue 文档,我们可以看到,左侧的页面结构经过拆分,可以用右侧的组件树来表示,上层的节点通常是一些布局用的组件,可复用度比较低,最底层的通常是一些最小的功能单元,可复用性比较高。
React 模块
React 框架包含 react
、react-dom
两个模块,其中 react
包含核心功能部分,不依赖具体平台,可用于浏览器、Node 服务端渲染、移动 App,组件的定义需要依赖该模块,react-dom
用于处理和浏览器渲染相关的功能,比如 ReactDOM.render()
可以将组件渲染到页面上,ReactDOM.renderToString()
可以输出 html 字符串给 Node 服务端渲染使用。
使用 React
使用 React 的方法有很多种,本节课我们主要介绍两种方式,都是基于类的组件
- 第一种,为了快速学习 React 相关概念和语法,我们采用 JavaScript 开发、script 标签引入的方式,它的优点是无需构建工具,可以直接通过浏览器执行,但是这种方式只适用于构建一些简单的学习示例,因为我们使用了
jsx
语法,它并不能直接被浏览器识别,需要借助于babel
来对它进行转换执行,所以效率十分低下,我们只用来学习 JS 环境下 React 的使用。 - 第二种,我们使用了
TypeScript
和原生Modules
的方式来开发 React 应用,它的好处是无需复杂构建工具,只需要 TS 一次性编译,速度很快,可以用来做一些没有外部依赖的简单的应用,同时又能体验 TS 开发的便利。
对于复杂项目,我们会采用 webpack
工具链来构建,这一部分会在后面的课程中继续讲解。
简单组件
一个简单的 React
组件采用类似下面的代码进行定义
class HelloMessage extends React.Component {
render() {
return <div>Hello {this.props.name}</div>
}
}
我们将每一个 React 组件定义为一个 class,它继承自 React.Component
,组件可以使用一个名为 render()
的方法,接收输入的数据并返回需要展示的内容。在上面的示例中这种类似于 XML
的写法被称为 JSX
,被传入的数据在组件中通过 this.props
进行访问。
有状态组件
上面的简单组件只用来展示外部传入的数据(通过 this.props
访问),有些组件逻辑比较复杂,可能会有自己内部的状态数据(通过 this.state
访问)需要管理。当组件的状态数据改变时,组件会再次调用 render()
方法重新渲染对应的标记。
class Counter extends React.Component {
constructor() {
super()
this.state = {
count: 0,
}
}
increase() {
this.setState({
count: this.state.count + 1,
})
}
render() {
return (
<div>
<span>{this.state.count}</span>
<button onClick={this.increase.bind(this)}>Increase</button>
</div>
)
}
}
我们通过 this.state
定义了当前组件的内部状态,默认的状态数据为 count: 0
,在 render()
函数中可以通过 this.state.count
来引用这个状态,我们给后面的 button
按钮绑定了一个 click
事件,当用户点击它之后,我们通过 this.setState
来改变 count
的值,让它每次加一,这会引起 render()
函数重新执行,React 比较前后的内容变化,进行实际的 DOM 更新。
注意,直接改变 this.state
中的值并不能直接引发组件的重新渲染,需要调用 this.setState
才可以
JSX 语法
https://react.docschina.org/docs/introducing-jsx.html
JSX
是 React 用来描述 DOM 结构的一种语法,它非常类似于原始的 html 写法,但是又有一些自己的规则。TypeScript
语言原生支持 JSX
,两者配合可以让 TS 覆盖到界面部分类型检查,开发体验非常棒,JSX 让我们可以完全使用 JS 来开发界面部分(HTML)和逻辑部分(JS),可以充分利用 JS 的灵活性。
JSX
本质上是一种语法糖,它并不是合法的 JavaScript
代码,所以需要将它转换成 React
中原始的创建 DOM 结构的 API 才能执行,比如下面的 JSX
代码
render() {
return (
<div className="message">
<h1>Hello</h1>
</div>
)
}
会被转换为实际可执行的 JS 代码,类似于原生的 document.createElement
render() {
return React.createElement(
'div',
{ className: 'message' },
React.createElement('h1', null, 'Hello')
)
}
很明显使用 JSX 语法更简单明了,也更贴合我们的开发习惯。直接进行 DOM 操作是执行效率比较低的,React 会在内存中维护一个虚拟 DOM 结构,render()
并不会直接更新 DOM,如果状态的改变引起了 render()
重新计算,React 会根据重新计算的虚拟 DOM 结构和之前的作比较,然后对发生改变的部分进行 DOM 更新,这样效率会更高。
我们也可以直接给一个变量赋值为 JSX
,然后在 render()
中引用它
let el = <h1>hello</h1>
这是一个非常有趣的写法,它既不是字符串也不是 html,通过上面的例子我们可以得知,它等价于
let el = React.createElement('h1', null, 'hello')
插值
插值是最基本的用法,它可以让我们在 JSX
中嵌入变量或者表达式,通过大括号来进行包裹,在前面的例子中我们已经体验过
render() {
let name = 'React'
return <div>Hello {name}</div>
}
我们还可以在 JSX
的大括号内放置任何有效的 JavaScript
表达式,例如 1 + 1
,user.firstName
,formatName(user)
都是有效的表达式。
function formatName(user) {
return user.firstName + ' ' + user.lastName
}
const user = {
firstName: 'Jack',
lastName: 'Lee'
}
render() {
return (
<h1>Hello, {formatName(user)}</h1>
)
}
JSX 也是一个表达式
因为在编译之后 JSX 表达式会被转为普通的 JavaScript 函数调用,所以我们可以在 if
、for
循环中使用 JSX,将 JSX 表达式赋值给变量,把 JSX 当作参数传递,或者从函数中返回 JSX,当你需要比较复杂的逻辑来构造一个 JSX 表达式的时候,可以用这种方式:
function getGreeting(user) { if (user) { return <h1>Hello, {formatName(user)}!</h1> } return <h1>Hello, Stranger.</h1>}render() { return ( <div>{getGreeting(user)}</div> )}
添加属性
添加属性与插值的写法类似,如:
render() { return ( <div tabIndex="0"> <img src={user.avatarUrl} /> </div> )}
警告:JSX 语法上更接近 JavaScript,并非和 HTML 完全对应,所以 React DOM 使用
camelCase
(小写驼峰命名)来定义属性的名称,而不使用 HTML 属性名称的约定。比如最常见的,JSX 里面的class
变成了className
render() { return <div className="message">Hello</div>}
嵌入子元素
JSX 表达式和 HTML 类似,标签可以嵌入子内容,如果一个标签没有内容,可以使用 />
来闭合,如:
render() { return <img src={user.avatarUrl} />}
这一点和 HTML 不同,在 HTML 中你可以写一个不闭合的标签,但是在 JSX 中不可以
<img src="logo.png" />
条件渲染
React 中的条件渲染和 JavaScript 一样,使用 JS 运算符或者条件运算符,可以让你对状态数据做出判断,来告诉 React 如何更新 UI。通常我们可以选择两种方式来进行条件渲染,对于简单的条件,我们可以用与运算符 &&
或者三目运算,例如:
constructor() { super() this.state = { finished: true, count: 0 }}render() { return ( <div> <p>status: {this.state.finished && <span>finished</span>}</p> <p>status: {this.state.finished ? <span>finished</span> : <span>not finished</span>}</p> <p>status: {this.state.count && <span>finished</span>}</p> <p>status: {this.state.count ? <span>finished</span> : <span>not finished</span>}</p> </div> )}
需要注意的是,如果是使用与运算符 &&
来做条件渲染,前半段的表达式返回的一定要是 boolean
类型的值,也就是 true
或者 false
,这一点和 JS 的逻辑判断并不一样,这里是基于 React 的一个特性,那就是不会渲染 boolean
类型的值,如果 &&
返回的是其他类型,比如数字 0
或者空字符串,则不会起到预期的效果。
对于一些复杂的条件判断,我们可以编写一个专门的函数来完成,例如:
constructor() { super() this.state = { count: 0 }}renderText() { if (this.state.count < 10) { return <span>小于10</span> } if (this.state.count < 20) { return <span>小于20</span> } return <span>大于等于20</span>}render() { return ( <div> <p>count: {this.renderText()}</p> </div> )}
有时候我们希望阻止组件的渲染,那么可以让 render()
返回 null
constructor() { super() this.state = { ready: false }}render() { if (this.state.ready === false) return null return ( <div>Ready!</div> )}
列表渲染
在 React 中,我们可以使用数组的 map
方法来将一个数组数据转换为一个元素集合,来实现列表渲染
constructor() { super() this.state = { rows: [1, 2, 3, 4, 5] }}render() { return ( <ul> {this.state.rows.map(item => <li>{item}</li>)} </ul> )}
运行上面的代码,我们会得到 React 的一条警告
Each child in a list should have a unique "key" prop.
这是因为当我们创建一个 React 列表的时候,需要给每一个列表元素添加一个 key
属性,这个 key
来帮助 React 识别哪些元素被改变了,比如添加或删除,因此你应当给数组中的每一个元素赋予一个确定的标识,如
constructor() { super() this.state = { rows: ['a', 'b', 'c'] }}render() { return ( <ul> {this.state.rows.map(item => <li key={item}>{item}</li>)} </ul> )}
一个元素的 key
最好是这个元素在列表中拥有的独一无二的值,上面这个例子直接使用数组项的值作为 key
,如果数组中有重复的值,这样使用是不可以的,通常我们使用数据的 id
来作为元素的 key
,当元素没有确定的 id
的时候,万不得已可以使用元素的索引 index
作为 key
,如
constructor() { super() this.state = { areas: [{id: 1, name: '内地'}, {id: 2, name: '港台'}, {id: 3, name: '欧美'}] }}render() { return ( <ul> {this.state.areas.map(item => <li key={item.id}>{item.name}</li>)} </ul> )}
key
有几点规则需要注意
key
只有放在就近的数组上下文中才有意义,key
应该放在最外层的列表项元素上面,而不应该放在列表项内部的元素上面key
只是在兄弟节点之间必须唯一,不同的列表之间不需要唯一,互不影响
事件处理
React 元素的事件处理和 DOM 元素很类似,但是语法上有一些不同
- React 事件的命名采用小驼峰模式(camelCase),而不是纯小写
- 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串
传统 HTML
<button onclick="onclick()">Click</button>
React 中有所不同
onBtnClick() { console.log('button clicked!')}render() { return <button onClick={this.onBtnClick.bind(this)}></button>}
在 React 里面我们不需要使用 addEventListener
来给元素添加事件监听
我们还可以向事件处理函数传递参数,以前面列表渲染的例子为例
constructor() { super() this.state = { areas: [{id: 1, name: '内地'}, {id: 2, name: '港台'}, {id: 3, name: '欧美'}] }}delItem(area) { let index = this.state.area.indexOf(area) this.state.area.splice(index, 1)}render() { return ( <ul> {this.state.areas.map(item => ( <li key={item.id}>{item.name} - <button onClick={() => this.delItem(item)}>删除</button></li> ))} </ul> )}
受控组件
一般的,表单元素(例如 input、textarea、select 等)都是受控组件。
受控组件有一个特点,它的 value 一般都绑定 state 中的值,并且只能通过 setState 来进行修改。
constructor() { super() this.state = { inputValue: '' }}render() { return ( <input value={this.state.inputValue} onChange={(e) => this.setState({ inputValue: e.target.value })}/> )}
Fragments
在 React 中一个 JSX 表达式只能包含一个根节点,如果我们想要返回多个元素,需要用一个容器比如 div
来包裹,如
render() { return ( <div> <span>内地</span> <span>港台</span> <span>欧美</span> </div> )}
这种规则大部分时候不会有什么问题,但是有时候外层的 div
可能会影响我们的一些样式,或者没必要额外嵌套一层在外面,这时候我们可以使用 React.Fragment
来返回一组元素,而不会在外面创建一个额外的容器,如
render() { return ( <React.Fragment> <span>内地</span> <span>港台</span> <span>欧美</span> </React.Fragment> )}
通过这种写法,父组件在引用该组件的时候,只会渲染三个 span
标签,而不会在外层嵌套一个 div
,它还有一种短语法,两者作用是一样的,使用一个空标签来包裹
render() { return ( <> <span>内地</span> <span>港台</span> <span>欧美</span> </> )}
Fragment
可以用在列表渲染中作为列表项,也支持传入 key
属性,需要注意,短语法 <></>
不支持添加 key
constructor() { this.state = { areas: [{id: 1, name: '内地'}, {id: 2, name: '港台'}, {id: 3, name: '欧美'}] }}render() { return ( <div> {this.state.areas.map(area => ( <React.Fragment key={area.id}> <span>{area.name}</span> <i>/</i> </React.Fragment> ))} </div> )}
ref
有时候我们需要在 React 应用中获得对 DOM 的直接访问,这时候可以使用 ref
constructor() { super() this.myRef = React.createRef()}componentDidMount() { console.log(this.myRef)}render() { return <div ref={this.myRef} />}
Props
前面我们看到,React 组件概念上类似于 JavaScript 函数,它接收任意的入参,即 props
,然后根据传入的数据来渲染组件,props
传入的状态数据发生变化之后,会引起当前组件的重新渲染
class Welcome extends React.Component { render() { return <div>Hello {this.props.name}</div> }}class App extends React.Component { constructor() { super() this.state = { name: 'Tom', } } render() { return ( <div> <Welcome name={this.state.name} /> </div> ) }}ReactDOM.render(<App />, document.getElementById('app'))
我们看一下这个例子发生了什么:
- 我们调用
ReactDOM.render()
函数,并且传入<App />
作为参数,表示渲染App
这个组件 App
组件调用了Welcome
组件,并将{ name: 'Tom' }
作为props
传入Welcome
组件将<div>Hello Tom</div>
元素作为返回值
注意:JSX 中引用其他组件的时候组件名必须以大写字母开头
React 会将小写字母开头的组件视为原生 DOM 标签,例如
<div />
代表 HTML 的 div 标签,而<Welcome />
则代表一个组件,并且需要在作用域内使用Welcome
关于 props
有一些重要的概念需要注意,子组件永远不要直接修改父组件传递过来的 props
,需要保持只读的状态,props
只能由父组件进行修改,这样做的目的是为了保持数据流的单向性,也就是说状态数据应该只能从父组件往子组件流动,而不应该反过来,这样容易造成混乱。比如上面的例子,子组件 Welcome
接收父组件 App
传递过来的 this.props.name
,在父组件 App
中,这个 name
对应的是自己内部的状态 this.state.name
,如果我们要改变 Welcome
中接收到的 name
,需要在 App
中通过 setState
来对 name
进行修改,这样来引起子组件中 props
的变化
React + TypeScript
TypeScript 原生支持 JSX,可以自动帮我们把 JSX 表达式转换成原生 JS,在 React 中使用 TS 也非常的简单,demo 中已经有写好的例子,下面我们一步一步来看一下,如何借助于原生 Modules 和 TS 来开发一个 React 页面,而先不使用构建工具 webpack
创建工程
首先创建一个空目录,作为我们项目的根目录,例如 react-ts
,我们已经创好了一个模板工程 template
,可以直接将该目录的文件拷贝到新创建的目录进行开发,注意,开发之前记得先在工程根目录执行命令 npm i
,用来安装所需的 TS 类型提示文件
编写代码
- 首先在工程根目录创建
index.html
,因为我们未使用构建工具,所以需要全局引入 React 和 ReactDOM,这里可以直接引入 CDN 上的资源,如
<script src="https://cdn.staticfile.org/react/17.0.2/umd/react.development.min.js"></script><script src="https://cdn.staticfile.org/react-dom/17.0.2/umd/react-dom.development.min.js"></script>
- 引入我们目标入口 JS 文件,记得加上
module
类型
<script src="scripts/main.js" type="module"></script>
- 创建目录
src
用来存放我们的 TS 代码文件 - 添加代码文件
src/main.tsx
,注意,如果要在 TS 中使用 JSX 语法,需要使用.tsx
的文件后缀,而不是.ts
,使用 TS 来编写 React 组件,我们需要定义几个类型,最关键的就是关于state
和props
,比如下面的示例
Welcome.tsx
interface Props { name: string}class Welcome extends React.Component<Props> { render() { return <div>Hello {this.props.name}</div> }}
main.tsx
import Welcome from './Welcome.js'interface State { name: string}class App extends React.Component<Props, State> { state: State = { name: 'Tom', } render() { return ( <div> <Welcome name={this.state.name} /> </div> ) }}ReactDOM.render(<App />, document.getElementById('app'))
React.Component
可以传入两个类型变量,第一个是 props
的类型, 第二个是 state
的类型
设计哲学
React 应用是由一个一个的组件组成的组件树,通常我们可以按作用不同将组件分为两类
- 页面组件:这种组件一般是用来实现页面的整体布局框架和容器,通常不具备可复用性
- 功能组件:这种组件是一些独立的 UI 功能单元,通常具备比较强的可复用性,功能组件一般会被嵌入到页面组件中使用
当我们要开发一个 React 应用时,首先需要考虑的就是应该如何划分组件层级,也就是该将哪些部分划分到一个功能组件中,通常来说,一个功能组件应该只负责一个功能,如果他需要负责更多的功能,这时候就需要考虑是否将它拆分到更小的组件。但是也应该避免过度拆分组件,这样会增加代码的复杂度,反而给维护增加工作量。
确定好组件的划分之后,我们一般会先实现一个静态版本的组件,这个版本暂不包含各种动态的数据绑定或事件处理,只是先单纯的实现组件的 UI 布局。静态版本实现之后,我们要考虑该如何放置组件的状态,应该在组件内维护 state
还是应该通过父组件传入 props
,确定好之后,我们再使用状态数据替换静态组件中需要填充的部分。
额外任务
自行学习 React 文档中的“核心概念”部分的十二个章节,该任务不需要提交具体的作业