认识组件的嵌套
组件之间存在嵌套关系
- 在之前的案例中, 我们只是创建了一个组件
App
- 如果我们一个应用程序将所有的逻辑都放在一个组件中, 那么这个组件就会变得非常臃肿和难以维护
- 所以组件化的核心思想应该是对组件进行拆分, 拆分成一个个小的组件
- 再将这些组件组合嵌套在一起, 最终形成我们的应用程序
1 | import React, { Component } from 'react' |
上面代码嵌套的逻辑及它们存在的关系如下
App
组件是Header
、Main
、Footer
组件的父组件Main
组件是Banner
、ProductList
组件的父组件
在开发过程中, 我们经常遇到需要组件之间相互进行通信
- 比如
App
可能使用了多个Header
, 每个地方的Header
展示的内容不同, 那么我们就需要使用者传递给Header
一些数据, 让其进行展示 - 又比如我们在
Main
中一次性请求了Banner
数据和ProductList
数据, 那么就需要传递给他们相应的数据来进行展示 - 也可能是子组件中发生了事件, 需要由父组件来完成某些操作, 那就需要子组件向父组件传递事件
总之, 在一个 React 项目中, 组件之间的通信是非常重要的环节
父组件在展示子组件, 可能会传递一些数据给子组件
- 父组件通过属性=值的形式来传递给子组件数据
- 子组件通过 props 参数获取父组件传递过来的数据
父组件传递子组件
子组件是 class 组件
1 | import React, { Component } from 'react' |
按照上面的结构, 我们每一个子组件都需要写构造器来完成: this.props = props
其实大可不必这样做, 因为我们可以调用super(props)
, 我们来看一下Component
的源码
1 | function Component(props, context, updater) { |
- 这里我们先不关心
context
、updater
- 我们发现传入的
props
会被Component
设置到this
中(父类的对象), 那么子类就可以继承过来
所以, 我们的构造方法可以换成下面的写法
1 | constructor(props) { |
甚至我们可以省略, 为什么可以省略呢?
如果不指定构造方法, 则使用默认构造函数, 对于基类, 默认构造函数是
1 | constructor() {} |
对于派生类, 默认构造函数是
1 | constructor(...args) { |
子组件是 function 组件
1 | function ChildCpn2(props) { |
function
组件相对来说比较简单, 因为不需要由构造方法, 也不需要有this
参数验证 propTypes
对于传递给子组件的数据, 有时候我们可能希望进行验证, 特别是对于大型项目来说
- 当然, 如果你的项目中默认继承了 Flow 或者 TypeScript, 那么就可以直接进行类型验证
- 但是, 即使我们没有使用 Flow 或者 TypeScript, 也可以通过
prop-types
库来进行参数验证
注意:
自 React v15.5 起,
React.PropTypes
已移入另一个包中, 请使用prop-types
库 代替
我们对之前的 class
组件进行验证
1 | import PropTypes from 'prop-types' |
这个时候, 控制台就会报警告
此时, 我们就得传入正确的类型才能避免警告
1 | <ChildCpn1 name="lion" age={18} height={1.80} /> |
更多的验证方式, 可以参考官网
- 比如验证数组, 并且数组中包含哪些元素
- 比如验证对象, 并且对象中包含哪些
key
及value
是什么类型 - 比如某个元素是必须的, 使用
requiredFunc: PropTypes.func.isRequired
如果父组件没有传递数据, 而我们希望子组件有默认值呢?
- 那么使用
defaultProps
就可以了
1 | ChildCpn1.defaultProps = { |
子组件传递父组件
某些情况, 我们也需要子组件向父组件传递数据
- 在 Vue 中是通过自定义事件来完成的
- 在 React 中同样是通过
props
传递消息, 只是让父组件给子组件传递一个回调函数, 在子组件中调用这个函数即可
我们这里来完成一个案例
- 将计数器案例进行拆解
- 将按钮封装到子组件中:
CounterButton
CounterButton
发生点击事件, 将内容传递到父组件中, 父组件修改counter
的值
1 | import React, { Component } from 'react' |
由子组件监听按钮事件, 事件触发后调用父组件传递的btnClick
函数, 父组件向子组件传递函数时使用的是箭头函数, 所以不必担心this
指向问题
React 插槽实现
为什么使用插槽?
在开发中, 我们抽取了一个组件, 但是为了让这个组件具备更强的通用性, 我们不能将组件中的内容限制为固定的div
、span
等元素
我们应该让组件使用者可以决定某一块区域的存放内容
举个例子: 假如我们定制一个通用的导航组件NavBar
- 这个组件分成三块区域: 左边-中间-右边, 每块区域的内容是不固定的
- 左边区域可能是一个菜单图标, 也可能是一个返回按钮, 也可能什么都不显示
- 中间区域可能是一个搜索框, 也可能是一个列表, 也可能是一个标题等等
- 右边可能是一个文字, 也可能是一个图标, 也可能什么都不显示
这种需求在 Vue 当中有一个固定的做法就是通过slot
来完成的, React 呢?
- React 对于这种需要插槽的情况非常灵活
- 有两种方案可以实现:
children
和props
children 实现
每个组件都可以获取到props.children
: 它包含组件的开始标签和结束标签之间的内容
1 | <Welcome>Hello React</Welcome> |
在Welcome
组件中获取props.children
, 就可以得到字符串Hello React
1 | function Welcome(props) { |
- 如果只有一个元素, 那么
children
指向该元素 - 如果有多个与安素, 那么
children
指向的是数组, 数组中包含多个元素
那么, 我们的NavBar
可以进行如下实现
1 | import React, { Component } from 'react' |
props 实现
通过children
实现的方案虽然可行, 但是有一个弊端: 通过索引值获取传入的元素很容易出错, 不能精准的获取传入的元素
另外一种方案就是使用props
实现
- 通过具体的属性名, 可以让我们在传入和获取时更加精准
1 | import React, { Component } from 'react' |
问题
为什么 constructor 中不传入 props 也能在别处使用
在进行 React 开发中, 有一个很奇怪的现象
- 在调用
super()
的时候, 不传入props
, 但是在下面的render
函数中我们依然可以使用props
- 如果你自己编写一个基础的类, 可以尝试一下: 理论上来说这种情况
props
应该是undefined
, 但是结果却不是undefined
1 | class ChildCpn extends Component { |
为什么这么神奇呢?
因为 React 担心你有时会忘了为super()
传入props
而进行了一些操作
我们来看一下这个组件时怎么被创建出来的
我们找到其中的render
函数
render
函数中有这样一段代码
这个_instance
实例就是组件对象
我们再来看一下, 它在哪里重新赋值
这里还包括通过this._instance
的方式回调生命周期函数
结论: 仅当你想在constructor
内使用props
才将props
传入super()
, 你无论是否手动的将props
保存到组件的实例上, React 内部都会帮你保存