写了 react 有一个半月,现在又有半个月没写了,感觉对其仍旧比较陌生。
本文分两部分,首先聊一下 react 的相关概念
,然后不使用任何语法糖(包括 jsx)或可能隐藏底层技术的便利措施来构建 React 组件
。
Tip:从一项新技术的底层元素起步有利于使用者更好的长期使用它
大部分 react 应用是在 Web 平台
上。而 React Native
和 React VR
这样的项目则创造了 react 应用在其他平台上运行的可能
组件
组件是 React 中最基本单元
。
组件通常对应用户界面的一部分,比如导航。也可以担任数据格式化等职责。
可以将任何东西作为组件,尽管并不是所有东西作为组件都有意义。
如果将整个界面作为组件,并且没有子组件或进一步的细分,那么对自己并没有什么帮助。倘若,将界面不同部分拆解成可以组合,复用的部分,却很有帮助。
组件具有良好的封装性
、复用性
和组合性
。有助于为使用者提供一个更简单的方式来思考和构建用户界面。使用 React 构建应用就像使用积木来搭建项目,而构建应用时有取之不尽的“积木”
将 UI 分解成组件可以让人更轻松的处理应用不同的部分。
组件需要一起工作,也就是说组件可以组合起来形成新的组件。组件组合
也是 React 最强大的部分之一。
如果身处一个中大型团队,可以将组件发布到私有注册中心(npm 或者其他)
React 组件还有一个方面就是生命周期方法
。当组件经过其生命周期的不同时期时(挂在、更新、卸载等),可以使用可预测、定义良好的方法。
React 库
React 核心库与 react-dom 和 react-native 紧密配合,侧重组件的规范和定义
。能让开发者构建一个组件树,该组件树能够被浏览器和其他平台所使用。
react-dom 就是一个渲染器
。针对浏览器环境和服务端渲染。
比如我们要将组件渲染到浏览器,就得用到 react-dom。
React Native
库专注于原生平台,能够为 ios、android 和其他平台创建 react 应用。
第三方库
React 不自带 http
等其他前端常用工具库。开发者可以自由的选择对于工作最好的工具。
react 属于 专一型
,主要关注 UI 试图方面。
而 angular 属于 通用型
,其内置了许多解决方案,例如 http 调用、路由、国际化、字符串和数字格式化...
Tip:通常一些优秀的团队会用这两种方式。
React 的创建主要用于 Facebook 的 UI 需求。虽然大多数的 web 应用在此范围之内,但也有一些应用不在。
React 是一种抽象,也存在抽象的代价
。React 以特定的方式构建并通过 api 向外暴露,开发者会失去对底层的可见性
。当然 React 也提供了紧急出口
,让开发者深入较低的抽象层级,仍然可以使用 jQuery,不过需要以一种兼容 React 的方式使用。
有时还需要为 React 的行事方式买单。或许会影响应用的小部分(即不太适合用 React 的方式来工作)
使用 React 时所做的权衡有助于使用者成为更好的开发者。
React 旨在将复杂的任务简单化,把不必要的复杂性从开发者身上剥离出来。
鼓励开发者使用声明式
的编程而非命令式,也就是开发者声明组件在不同状态下的行为和外观即可,React 负责渲染以及更新 UI,并将性能做到恰到好处。从而让研发人员腾出时间思考其他方面。
驱动这些的主要技术之一就是虚拟dom
。
Tip:有关虚拟dom 的介绍可以参考 vue 快速入门-虚拟dom
虚拟 Dom 不是我们关注的重点。这正是 React 简单
的地方:开发者被解放出来,去关注最关注的部分。
什么使 React 成为大型团队的宠儿?首先是简单
,其次是非固化
。
简单的技术让人更容易理解和使用。
React 是一个非常轻量的库,只关注应用的视图。更加容易与使用者当前的技术集成,并在其他方面为使用者留下了选择的空间。一些功能固化的框架和库要求使用者要么全盘接受要么彻底不用。
简单和非固化的特性,以及恰到好处的性能,让它非常适合大大小小的项目。
组件可以独立存在,也可用来创建其他组件。人们认为组件可以创建很多不同类的关系,从某种意义这是对的。
但组件更多的是以灵活的方式被使用,应该关注其独立性
和常常不带任何负担
,可组合使用。所以组件只关注其父母和孩子
,兄弟关系可以不管。
建立组件关系的过程对每个团队或项目都不尽相同,组件关系也可能会随时间而改变,我们可以不期望一次就建立完美,也无需太过担心,因为 React 会让我们的 UI 迭代没那么困难。
首先我们将组件的框架写好:
Tip:这是一个普通的 html 页面,直接通过 vscode 的 Live Server 插件运行即可
运行后的网页显示 Hello, world!
。生成的元素结构如下:
Hello, world!
下面我们稍微分析一下这个页面:
首先定义了一个 div 元素,接着引入三个包,作用如下:
react.js
,React 的核心库,用于定义组件的规范react-dom.js
,渲染器,用于浏览器和服务端渲染,用于创建组件和管理组件prop-types.js
,传递给组件的数据做类型检查接着通过 React.createElement
创建一个 react 元素。
React.createElement(
type,
[props],
[...children]
)
Tip:react 元素是什么?
普通对象
最后使用 ReactDOM.render
将 React 元素渲染到 div#root
中。
// 在提供的 container 里渲染一个 React 元素,并返回对该组件的引用
ReactDOM.render(element, container[, callback])
Tip:调用 react-dom 的 render() 方法来让 React 将组建渲染出来,并对组件进行管理。
React 元素
是你想让 React 渲染的东西的轻量表示。它可以表示为一个 Dom 元素,上文我们已经用其创建了一个 h1
的 dom 元素。
有必要再来分析一下 createElement()
的参数:
// 创建并返回指定类型的新 React 元素。其中的类型参数既可以是标签名字符串(如 'div' 或 'span'),也可以是 React 组件 类型 (class 组件或函数组件),或是 React fragment 类型。
React.createElement(
type,
[props],
[...children]
)
const reactElem = React.createElement(
'h1',
{title: 'i am h1'},
'Hello, world!'
)
type
,一个 html 标签("div"、"h1")或 React 类props
,指定 html 元素上要定义哪些属性或组件类的实例上可以使用哪些属性children
,还记得 React 组件是可以组合的吗?一句话:React.createElement()
在问:
我在创建什么?
,是 Dom 元素,是 React 组件,还是React fragment。我怎么配置它?
它包含什么?
假如我们需要在页面显示如下元素:
可以这么写:
const c = React.createElement
const reactElem2 = React.createElement(
'div',
{},
c('h2', {}, 'i am h2'),
c('a', {href: 'www.baidu.com'}, 'go baidu'),
c('p', {},
c('em', {}, 'i am em element')
)
)
React 是怎么把那么多 React.createElement
转换成屏幕上看到的东西的?这里得用到虚拟 dom。
虚拟 dom 和真实 dom 有着相似的结构。
为了从 React 元素中形成自己的虚拟 DOM 树
,React 会对 React.createElement 的全部 children 属性进行求值,并将结果传递给父元素。就像一个小孩反复再问 X是什么?
,直到理解 X 的每个细节,直到他能形成一棵完整的树。
看看这段代码,我们创建了一个 React 元素并将其放入 dom 中:
如果我们需要扩展 reactElem 的功能、样式以及其他UI相关?这时可以使用组件
。
组件可以将这些有效的组织在一起。
所以,要真正构建东西,不仅仅需要 React 元素,还需要组件。
React 组件就像是 React 元素,但 React 组件拥有更多特性。React 组件是帮助将 React 元素和函数组织到一起的类
我们可以使用函数或 js 类
创建组件。
使用 es6 的 class 来定义组件。就像这样:
class MyComponent extends React.Component {
// 必须定义 render()。否则会报错:
// MyComponent(...): No `render` method found on the returned component instance: you may have forgotten to define `render`.
render() {
// 返回单个 React 元素或 React 元素的数组
return reactElem
}
}
通常需要至少定义一个 render() 方法,几乎任何向屏幕显示内容的组件都带有 render 方法
Tip:那些不直接显示任何东西而是修改或增强其他组件的组件(称高阶组件),后续再讨论。
我们将上面示例改成组件形式:
生成的 html 如下:
React 中通过 this.props
就能获取传递给组件的属性。
this.props 是怎么来的
MyComponent 中没有初始化 props 的代码,既然自己没做,那么肯定是父类
帮忙做了。
就像这样:
如果需要自己写 constructor ,则需要手动调用 super()
,否则会报错。就像这样:
// 控制台输入如下代码,报错
// Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
// Uncaught ReferenceError:在访问“this”或从派生构造函数返回之前,必须在派生类中调用超级构造函数
class A{}
class B extends A{
constructor(){
}
}
let b = new B()
Tip: 有关 super 更多介绍请看 这里
constructor(props)
如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。通常,构造函数仅用于以下两种情况:
类型检测
类组件能使用自定义属性。
通过组件好像能创建自定义 html 元素,而且还能做得更多。
能力越大,责任也越大,我们需要使用一些方法来验证所使用的属性,防止缺陷、规划组件所使用的数据种类。
在上面示例基础上,我们增加类型检测:
类型检测生效了。控制台报错如下:
Warning: Failed prop type: Invalid prop `cnt1` of type `string` supplied to `MyComponent`, expected `number`
警告:失败的属性类型:提供给“MyComponent”的类型为“string”的属性“cnt1”无效,应为“number”`
Warning: Failed prop type: The prop `func` is marked as required in `MyComponent`, but its value is `undefined`.
警告:失败的属性类型:属性“func”在“MyComponent”中标记为必需,但其值为“undefined”。
除了控制台发出 Warning,页面显示仍旧正常。
这里其实就是按照特定规定,给 MyComponent 类增加了两个静态成员,用于类型检测。我们可以自己模拟一下,请看示例:
类型检测结果如下:
// 浏览器控制台输出:
Warning: name 属性 - 期待类型是 string,所传入的类型确是 number
Warning: age 属性 - 期待类型是 number,所传入的类型确是 string
现在这么写有些零散,我们可以使用 static
语法来对其优化。就像这样:
class Square extends Rectangle {
static propTypes = {
name: 'string',
age: 'number'
}
render(){}
}
Tip: 有关 static 更多介绍可以百度 mdn static
嵌套组件
我们已经创建了一个类组件,并传入了一些属性,现在我们可以尝试嵌套组件。
前面我们已经提到,组件组合
是 React 中非常强大的功能。比如一个页面,我们可以通过组件进行拆分,单独开发,最终却是需要将组件组合成一个页面,否则就不好玩了。
将上面组件拆成两个,稍作变动,代码如下:
核心是 this.props.children
,每个组件都可以获取到 props.children
。最终渲染 html 结构如下:
现在我们已经为组件添加了 render 方法和一些 propTypes。上面示例也仅仅显示一些静态文案,但要创建动态组件
,远不止这些。
React 提供了某些特殊方法,当 React 管理虚拟 dom 时,react 会按顺序调用它们,render 方法只是其中之一。
状态
状态可以让组件交互并鲜活起来。
Tip: 状态其他特性如下:
下面我们使用一下 state,既然 state 是可变状态,那么我们就创建一个表单组件,里面有一个 input,一个提交按钮。
代码如下:
生成的表单也很简单,状态数据 age 也已经在 input 元素中成功显示:
现在需要专门的方法更新 state 中的数据。不能直接修改(例如 this.state.age = 19
),因为 React 需要跟踪状态,并保证虚拟 dom 和真实 dom 的同步。得通过 React 提供的特殊通道(this.setState()
) 来更新 React 类组件中的状态。
setState 不会立即更新组件,React 会根据状态变化批量更新以便使效率最大化,也就是说 React 会以它最高效的方法基于新状态更新 dom,做到尽可能快。
Tip: 不要直接修改 state 的示例请看 这里
事件与 React 如何协作
以前我们直接操作 dom,于是可以通过 addEventListener
注册事件;现在不直接操作 dom,而是和 React 元素打交道,那么 React 应该提供对应的事件机制,最好和我们之前的习惯相同,而 React 确实是这样做的。
React 实现了一个合成事件系统作为虚拟 Dom 的一部分,它会将浏览器中的事件转为 React 应用的事件。可以设置响应浏览器事件的事件处理器,就像通常用 js 那样做就好。区别是 React 的事件是设置在 React 元素或组件自身上,而不是用 addEventListener。
React 能监听浏览器中很多不同事件,涵盖了几乎所有的交互(点击、提交、滚动等)
接下来我们就可以用来自这些事件(比如文本变化时的事件 onchange
)的数据来更新组件状态。
接着上面示例,需求是:更改 input[type=text]
的值,对应 state 中的 age 也会同步,点击 submit 能提交。
代码如下:
函数传递数据
利用函数
可以将子组件的数据传递给父组件。核心代码如下:
class MyComponent extends React.Component {
constructor(props) {
super(props)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleSubmit(data) {
console.log('提交表单 data=', data)
}
render() {
const c = React.createElement
return React.createElement(
'div',
{ className: 'parent-class' },
React.createElement(MySubComponent, {
aClass: 'p-class',
cnt2: 'a am em element',
onFormSubmit: this.handleSubmit
})
)
}
}
class MySubComponent extends React.Component {
handleSubmit(evt) {
evt.preventDefault()
this.props.onFormSubmit(this.state)
}
}
在 React 中,数据自顶向下流动,可以通过 props 向子组件传递信息并在子组件中使用这些信息。表明可以将子组件的数据存储在父组件中,并从那里将数据传递给子组件。做个实例来验证一下,定义三个组件(A、B、C),结构如下:
apple
数据存在 AComponent 中,每点击一次 CComponent 组件,就会要求 AComponent 增加一个 apple,渲染到页面的 BComponent 组件也相应增加。
全部代码如下:
JSX 仅仅只是 React.createElement(component, props, ...children)
函数的语法糖 —— react 官网-深入 JSX
我们建议在 React 中配合使用 JSX,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式 —— react 官网-JSX 简介
jsx 让人编写类似于(但不是) HTML 的代码。
将上面增加 apple 的例子改为 jsx。全部代码如下:
jsx 除了类似于 HTML 且语法简单,另一个好处是声明式
和封装
。通过将组成视图的代码和相关联的方法包含在一起,使用者创建了一个功能组。本质上,需要知道的有关组件的所有信息都汇聚在此,无关紧要的东西都被隐藏起来,意味着使用者更容易的思考组件,并且更加清楚他们作为一个系统是如何工作的。
主要注意,JSX 不是 html,只会转译成常规 React 代码。它的语法和惯例也不完全相同,需要关注一些细微的差异(偶尔有些“破费思量之处”),比如: