写了 react 有一个半月,现在又有半个月没写了,感觉对其仍旧比较陌生。
本文分两部分,首先聊一下 react 的相关概念,然后不使用任何语法糖(包括 jsx)或可能隐藏底层技术的便利措施来构建 React 组件。
Tip:从一项新技术的底层元素起步有利于使用者更好的长期使用它
大部分 react 应用是在 Web 平台上。而 React Native 和 React VR 这样的项目则创造了 react 应用在其他平台上运行的可能
组件是 React 中最基本单元。
组件通常对应用户界面的一部分,比如导航。也可以担任数据格式化等职责。
可以将任何东西作为组件,尽管并不是所有东西作为组件都有意义。
如果将整个界面作为组件,并且没有子组件或进一步的细分,那么对自己并没有什么帮助。倘若,将界面不同部分拆解成可以组合,复用的部分,却很有帮助。
组件具有良好的封装性、复用性和组合性。有助于为使用者提供一个更简单的方式来思考和构建用户界面。使用 React 构建应用就像使用积木来搭建项目,而构建应用时有取之不尽的“积木”
将 UI 分解成组件可以让人更轻松的处理应用不同的部分。
组件需要一起工作,也就是说组件可以组合起来形成新的组件。组件组合也是 React 最强大的部分之一。
如果身处一个中大型团队,可以将组件发布到私有注册中心(npm 或者其他)
React 组件还有一个方面就是生命周期方法。当组件经过其生命周期的不同时期时(挂在、更新、卸载等),可以使用可预测、定义良好的方法。
React 核心库与 react-dom 和 react-native 紧密配合,侧重组件的规范和定义。能让开发者构建一个组件树,该组件树能够被浏览器和其他平台所使用。
react-dom 就是一个渲染器。针对浏览器环境和服务端渲染。
比如我们要将组件渲染到浏览器,就得用到 react-dom。
React Native 库专注于原生平台,能够为 ios、android 和其他平台创建 react 应用。
React 不自带 http 等其他前端常用工具库。开发者可以自由的选择对于工作最好的工具。
而 angular 属于 通用型,其内置了许多解决方案,例如 http 调用、路由、国际化、字符串和数字格式化…
Tip:通常一些优秀的团队会用这两种方式。
React 的创建主要用于 Facebook 的 UI 需求。虽然大多数的 web 应用在此范围之内,但也有一些应用不在。
React 是一种抽象,也存在抽象的代价。React 以特定的方式构建并通过 api 向外暴露,开发者会失去对底层的可见性。当然 React 也提供了紧急出口,让开发者深入较低的抽象层级,仍然可以使用 jQuery,不过需要以一种兼容 React 的方式使用。
有时还需要为 React 的行事方式买单。或许会影响应用的小部分(即不太适合用 React 的方式来工作)
使用 React 时所做的权衡有助于使用者成为更好的开发者。
React 旨在将复杂的任务简单化,把不必要的复杂性从开发者身上剥离出来。
鼓励开发者使用声明式的编程而非命令式,也就是开发者声明组件在不同状态下的行为和外观即可,React 负责渲染以及更新 UI,并将性能做到恰到好处。从而让研发人员腾出时间思考其他方面。
驱动这些的主要技术之一就是虚拟dom。
Tip:有关虚拟dom 的介绍可以参考
什么使 React 成为大型团队的宠儿?首先是简单,其次是非固化。
简单的技术让人更容易理解和使用。
简单和非固化的特性,以及恰到好处的性能,让它非常适合大大小小的项目。
组件可以独立存在,也可用来创建其他组件。人们认为组件可以创建很多不同类的关系,从某种意义这是对的。
建立组件关系的过程对每个团队或项目都不尽相同,组件关系也可能会随时间而改变,我们可以不期望一次就建立完美,也无需太过担心,因为 React 会让我们的 UI 迭代没那么困难。
首先我们将组件的框架写好:
<div id="root"> div> <script src="https://unpkg.com/react@17/umd/react.development.js">script> <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js">script> <script src="https://unpkg.com/prop-types@15.6/prop-types.js">script> <script> const reactElem = React.createElement( h1, {title: i am h1}, Hello, world! ) ReactDOM.render( reactElem, document.getElementById(root) ); script>
Tip:这是一个普通的 html 页面,直接通过 vscode 的 Live Server 插件运行即可
运行后的网页显示 Hello, world!。生成的元素结构如下:
"root"> <h1 title="i am h1">Hello, world!h1>
下面我们稍微分析一下这个页面:
首先定义了一个 div 元素,接着引入三个包,作用如下:
接着通过 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! )
一句话:React.createElement() 在问:
假如我们需要在页面显示如下元素:
<h2>i am h2h2> <a href="www.baidu.com">go baidua> <p> <em>i am em elementem> p>
可以这么写:
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 中:
<span class="hljs-keyword">const</span> c = <span class="hljs-title class\_">React</span>.<span class="hljs-property">createElement</span> <span class="hljs-keyword">const</span> reactElem = <span class="hljs-title class\_">React</span>.<span class="hljs-title function\_">createElement</span>( <span class="hljs-string">div</span>, {}, <span class="hljs-title function\_">c</span>(<span class="hljs-string">h2</span>, {}, <span class="hljs-string">i am h2</span>), <span class="hljs-title function\_">c</span>(<span class="hljs-string">a</span>, {<span class="hljs-attr">href</span>: <span class="hljs-string">www.baidu.com</span>}, <span class="hljs-string">go baidu</span>), <span class="hljs-title function\_">c</span>(<span class="hljs-string">p</span>, {}, <span class="hljs-title function\_">c</span>(<span class="hljs-string">em</span>, {}, <span class="hljs-string">a am em element</span>) ) ) <span class="hljs-title class\_">ReactDOM</span>.<span class="hljs-title function\_">render</span>( reactElem, <span class="hljs-variable language\_">document</span>.<span class="hljs-title function\_">getElementById</span>(<span class="hljs-string">root</span>) );
如果我们需要扩展 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:那些不直接显示任何东西而是修改或增强其他组件的组件(称高阶组件),后续再讨论。
我们将上面示例改成组件形式:
<span class="hljs-keyword">class</span> <span class="hljs-title class\_">MyComponent</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class\_ inherited\_\_">React.Component</span>{ <span class="hljs-title function\_">render</span>(<span class="hljs-params"></span>){ <span class="hljs-keyword">const</span> c = <span class="hljs-title class\_">React</span>.<span class="hljs-property">createElement</span> <span class="hljs-keyword">return</span> <span class="hljs-title class\_">React</span>.<span class="hljs-title function\_">createElement</span>( <span class="hljs-string">div</span>, {}, <span class="hljs-title function\_">c</span>(<span class="hljs-string">h2</span>, {}, <span class="hljs-variable language\_">this</span>.<span class="hljs-property">props</span>.<span class="hljs-property">cnt1</span>), <span class="hljs-title function\_">c</span>(<span class="hljs-string">a</span>, {<span class="hljs-attr">href</span>: <span class="hljs-string">www.baidu.com</span>}, <span class="hljs-string">go baidu</span>), <span class="hljs-comment">// class 属性在需要改为 className </span> <span class="hljs-title function\_">c</span>(<span class="hljs-string">p</span>, {<span class="hljs-attr">className</span>: <span class="hljs-variable language\_">this</span>.<span class="hljs-property">props</span>.<span class="hljs-property">aClass</span>}, <span class="hljs-title function\_">c</span>(<span class="hljs-string">em</span>, {}, <span class="hljs-variable language\_">this</span>.<span class="hljs-property">props</span>.<span class="hljs-property">cnt2</span>) ) ) } } <script> // createElement 第一个参数可以是标签名字符串(如 div 或 span),也可以是 React 组件 类型 (class 组件或函数组件),或是 React fragment 类型 const App = React.createElement(MyComponent, { cnt1: i am h2, aClass: p-class, cnt2: a am em element }) ReactDOM.render( App, document.getElementById(root) ); script>
生成的 html 如下:
<h2>i am h2h2> <a href="www.baidu.com">go baidua> <p class="p-class"> <em>a am em elementem> p>
React 中通过 this.props 就能获取传递给组件的属性。
MyComponent 中没有初始化 props 的代码,既然自己没做,那么肯定是父类帮忙做了。
就像这样:
<span class="hljs-comment">// 父类</span> <span class="hljs-keyword">class</span> <span class="hljs-title class\_">Rectangle</span> { <span class="hljs-title function\_">constructor</span>(<span class="hljs-params"></span>) { <span class="hljs-comment">// 子类接收的参数,这里 arguments 都能接收到</span> <span class="hljs-keyword">const</span> args = <span class="hljs-title class\_">Array</span>.<span class="hljs-title function\_">from</span>(<span class="hljs-variable language\_">arguments</span>) <span class="hljs-comment">// args= (3) [{…}, b, c]</span> <span class="hljs-variable language\_">console</span>.<span class="hljs-title function\_">log</span>(<span class="hljs-string">args=</span>, args) <span class="hljs-variable language\_">this</span>.<span class="hljs-property">props</span> = args[<span class="hljs-number">0</span>] } } <span class="hljs-comment">// 子类</span> <span class="hljs-keyword">class</span> <span class="hljs-title class\_">Square</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class\_ inherited\_\_">Rectangle</span> { <span class="hljs-title function\_">render</span>(<span class="hljs-params"></span>){ <span class="hljs-comment">// name= pjl</span> <span class="hljs-variable language\_">console</span>.<span class="hljs-title function\_">log</span>(<span class="hljs-string">name=</span>, <span class="hljs-variable language\_">this</span>.<span class="hljs-property">props</span>.<span class="hljs-property">name</span>) } } <span class="hljs-keyword">let</span> square = <span class="hljs-keyword">new</span> <span class="hljs-title class\_">Square</span>({<span class="hljs-attr">name</span>: <span class="hljs-string">pjl</span>}, <span class="hljs-string">b</span>, <span class="hljs-string">c</span>) square.<span class="hljs-title function\_">render</span>()
如果需要自己写 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 元素,而且还能做得更多。
能力越大,责任也越大,我们需要使用一些方法来验证所使用的属性,防止缺陷、规划组件所使用的数据种类。
在上面示例基础上,我们增加类型检测:
<span class="hljs-keyword">class</span> <span class="hljs-title class\_">MyComponent</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class\_ inherited\_\_">React.Component</span> { ... } <span class="hljs-comment">// 属性类型</span> <span class="hljs-title class\_">MyComponent</span>.<span class="hljs-property">propTypes</span> = { <span class="hljs-attr">cnt1</span>: <span class="hljs-title class\_">PropTypes</span>.<span class="hljs-property">number</span>, <span class="hljs-comment">// 注:函数不是 function,而是 func</span> <span class="hljs-comment">// 函数类型,并且必填</span> <span class="hljs-attr">func</span>: <span class="hljs-title class\_">PropTypes</span>.<span class="hljs-property">func</span>.<span class="hljs-property">isRequired</span> } <span class="hljs-comment">// 默认值</span> <span class="hljs-title class\_">MyComponent</span>.<span class="hljs-property">defaultProps</span> = { <span class="hljs-attr">cnt1</span>: <span class="hljs-string">defaultName</span> }
类型检测生效了。控制台报错如下:
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 类增加了两个静态成员,用于类型检测。我们可以自己模拟一下,请看示例:
<span class="hljs-keyword">class</span> <span class="hljs-title class\_">Rectangle</span> { <span class="hljs-title function\_">constructor</span>(<span class="hljs-params"></span>) { <span class="hljs-variable language\_">this</span>.<span class="hljs-property">props</span> = <span class="hljs-variable language\_">arguments</span>[<span class="hljs-number">0</span>] <span class="hljs-comment">// 模拟类型验证</span> <span class="hljs-variable language\_">this</span>.<span class="hljs-title function\_">validate</span>(<span class="hljs-variable language\_">this</span>.<span class="hljs-property">constructor</span>) } <span class="hljs-title function\_">validate</span>(<span class="hljs-params">subClass</span>) { <span class="hljs-title class\_">Object</span>.<span class="hljs-title function\_">keys</span>(subClass.<span class="hljs-property">propTypes</span>).<span class="hljs-title function\_">forEach</span>(<span class="hljs-function"><span class="hljs-params">key</span> =></span> { <span class="hljs-keyword">const</span> propType = subClass.<span class="hljs-property">propTypes</span>[key] <span class="hljs-keyword">const</span> type = <span class="hljs-keyword">typeof</span> <span class="hljs-variable language\_">this</span>.<span class="hljs-property">props</span>[key] <span class="hljs-keyword">if</span> (type !== propType) { <span class="hljs-variable language\_">console</span>.<span class="hljs-title function\_">error</span>(<span class="hljs-string">`Warning: <span class="hljs-subst">${key}</span> 属性 - 期待类型是 <span class="hljs-subst">${propType}</span>,所传入的类型确是 <span class="hljs-subst">${type}</span>`</span>) } }) } } <span class="hljs-keyword">class</span> <span class="hljs-title class\_">Square</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class\_ inherited\_\_">Rectangle</span> { <span class="hljs-title function\_">render</span>(<span class="hljs-params"></span>) { } } <span class="hljs-title class\_">Square</span>.<span class="hljs-property">propTypes</span> = { <span class="hljs-attr">name</span>: <span class="hljs-string">string</span>, <span class="hljs-attr">age</span>: <span class="hljs-string">number</span> } <span class="hljs-keyword">let</span> square = <span class="hljs-keyword">new</span> <span class="hljs-title class\_">Square</span>({ <span class="hljs-attr">name</span>: <span class="hljs-number">18</span>, <span class="hljs-attr">age</span>: <span class="hljs-string">pjl</span> }) square.<span class="hljs-title function\_">render</span>()
类型检测结果如下:
// 浏览器控制台输出: 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 中非常强大的功能。比如一个页面,我们可以通过组件进行拆分,单独开发,最终却是需要将组件组合成一个页面,否则就不好玩了。
将上面组件拆成两个,稍作变动,代码如下:
<span class="hljs-keyword">class</span> <span class="hljs-title class\_">MyComponent</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class\_ inherited\_\_">React.Component</span> { <span class="hljs-title function\_">render</span>(<span class="hljs-params"></span>) { <span class="hljs-keyword">const</span> c = <span class="hljs-title class\_">React</span>.<span class="hljs-property">createElement</span> <span class="hljs-keyword">return</span> <span class="hljs-title class\_">React</span>.<span class="hljs-title function\_">createElement</span>( <span class="hljs-string">div</span>, { <span class="hljs-attr">className</span>: <span class="hljs-string">parent-class</span> }, <span class="hljs-title function\_">c</span>(<span class="hljs-string">h2</span>, {}, <span class="hljs-variable language\_">this</span>.<span class="hljs-property">props</span>.<span class="hljs-property">cnt1</span>), <span class="hljs-comment">// 核心是:this.props.children。</span> <span class="hljs-variable language\_">this</span>.<span class="hljs-property">props</span>.<span class="hljs-property">children</span> ) } } <span class="hljs-keyword">class</span> <span class="hljs-title class\_">MySubComponent</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class\_ inherited\_\_">React.Component</span> { <span class="hljs-title function\_">render</span>(<span class="hljs-params"></span>) { <span class="hljs-keyword">const</span> c = <span class="hljs-title class\_">React</span>.<span class="hljs-property">createElement</span> <span class="hljs-keyword">return</span> <span class="hljs-title class\_">React</span>.<span class="hljs-title function\_">createElement</span>( <span class="hljs-string">div</span>, { <span class="hljs-attr">className</span>: <span class="hljs-string">sub-class</span> }, <span class="hljs-title function\_">c</span>(<span class="hljs-string">a</span>, { <span class="hljs-attr">href</span>: <span class="hljs-string">www.baidu.com</span> }, <span class="hljs-string">go baidu</span>), <span class="hljs-title function\_">c</span>(<span class="hljs-string">p</span>, { <span class="hljs-attr">className</span>: <span class="hljs-variable language\_">this</span>.<span class="hljs-property">props</span>.<span class="hljs-property">aClass</span> }, <span class="hljs-title function\_">c</span>(<span class="hljs-string">em</span>, {}, <span class="hljs-variable language\_">this</span>.<span class="hljs-property">props</span>.<span class="hljs-property">cnt2</span>) ) ) } } <script> // createElement 第一个参数可以是标签名字符串(如 div 或 span),也可以是 React 组件 类型 (class 组件或函数组件),或是 React fragment 类型 const App = React.createElement(MyComponent, { cnt1: i am h2, }, React.createElement(p, {}, i am p element), React.createElement(MySubComponent, { aClass: p-class, cnt2: a am em element } )) ReactDOM.render( App, document.getElementById(root) ); script>
核心是 this.props.children,每个组件都可以获取到 props.children。最终渲染 html 结构如下:
"root"> <div class="parent-class"> <h2>i am h2h2> <p>i am p elementp> <div class="sub-class"> <a href="www.baidu.com">go baidua> <p class="p-class"> <em>a am em elementem> p> div> div>
现在我们已经为组件添加了 render 方法和一些 propTypes。上面示例也仅仅显示一些静态文案,但要创建动态组件,远不止这些。
React 提供了某些特殊方法,当 React 管理虚拟 dom 时,react 会按顺序调用它们,render 方法只是其中之一。
状态可以让组件交互并鲜活起来。
Tip: 状态其他特性如下:
下面我们使用一下 state,既然 state 是可变状态,那么我们就创建一个表单组件,里面有一个 input,一个提交按钮。
代码如下:
<span class="hljs-keyword">class</span> <span class="hljs-title class\_">MyComponent</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class\_ inherited\_\_">React.Component</span> { <span class="hljs-title function\_">render</span>(<span class="hljs-params"></span>) { <span class="hljs-keyword">const</span> c = <span class="hljs-title class\_">React</span>.<span class="hljs-property">createElement</span> <span class="hljs-keyword">return</span> <span class="hljs-title class\_">React</span>.<span class="hljs-title function\_">createElement</span>( <span class="hljs-string">div</span>, { <span class="hljs-attr">className</span>: <span class="hljs-string">parent-class</span> }, <span class="hljs-variable language\_">this</span>.<span class="hljs-property">props</span>.<span class="hljs-property">children</span> ) } } <span class="hljs-keyword">class</span> <span class="hljs-title class\_">MySubComponent</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class\_ inherited\_\_">React.Component</span> { <span class="hljs-title function\_">constructor</span>(<span class="hljs-params">props</span>){ <span class="hljs-comment">// 如果不需要使用 this.props,则可以是 super()</span> <span class="hljs-variable language\_">super</span>(props) <span class="hljs-comment">// 在构造函数中访问 this 之前,一定要调用 super(),它负责初始化 this</span> <span class="hljs-variable language\_">this</span>.<span class="hljs-property">state</span> = {<span class="hljs-attr">age</span>: <span class="hljs-number">18</span>} } <span class="hljs-title function\_">render</span>(<span class="hljs-params"></span>) { <span class="hljs-keyword">const</span> c = <span class="hljs-title class\_">React</span>.<span class="hljs-property">createElement</span> <span class="hljs-keyword">return</span> <span class="hljs-title class\_">React</span>.<span class="hljs-title function\_">createElement</span>( <span class="hljs-string">form</span>, { <span class="hljs-attr">className</span>: <span class="hljs-string">sub-class</span> }, <span class="hljs-title function\_">c</span>(<span class="hljs-string">p</span>, { }, <span class="hljs-title function\_">c</span>(<span class="hljs-string">input</span>, {<span class="hljs-attr">type</span>: <span class="hljs-string">text</span>, <span class="hljs-attr">value</span>: <span class="hljs-variable language\_">this</span>.<span class="hljs-property">state</span>.<span class="hljs-property">age</span>}) ), <span class="hljs-title function\_">c</span>(<span class="hljs-string">p</span>, { }, <span class="hljs-title function\_">c</span>(<span class="hljs-string">input</span>, {<span class="hljs-attr">type</span>: <span class="hljs-string">submit</span>, <span class="hljs-attr">value</span>: <span class="hljs-string">submit</span>}) ), ) } }
生成的表单也很简单,状态数据 age 也已经在 input 元素中成功显示:
"root"> <div class="parent-class"> <form class="sub-class"> <p><input type="text" value="18">p> <p><input type="submit" value="submit">p> form> div> div>
现在需要专门的方法更新 state 中的数据。不能直接修改(例如 this.state.age = 19),因为 React 需要跟踪状态,并保证虚拟 dom 和真实 dom 的同步。得通过 React 提供的特殊通道(this.setState()) 来更新 React 类组件中的状态。
setState 不会立即更新组件,React 会根据状态变化批量更新以便使效率最大化,也就是说 React 会以它最高效的方法基于新状态更新 dom,做到尽可能快。
Tip: 不要直接修改 state 的示例请看
以前我们直接操作 dom,于是可以通过 addEventListener 注册事件;现在不直接操作 dom,而是和 React 元素打交道,那么 React 应该提供对应的事件机制,最好和我们之前的习惯相同,而 React 确实是这样做的。
React 实现了一个合成事件系统作为虚拟 Dom 的一部分,它会将浏览器中的事件转为 React 应用的事件。可以设置响应浏览器事件的事件处理器,就像通常用 js 那样做就好。区别是 React 的事件是设置在 React 元素或组件自身上,而不是用 addEventListener。
React 能监听浏览器中很多不同事件,涵盖了几乎所有的交互(点击、提交、滚动等)
接下来我们就可以用来自这些事件(比如文本变化时的事件 onchange)的数据来更新组件状态。
接着上面示例,需求是:更改 input[type=text] 的值,对应 state 中的 age 也会同步,点击 submit 能提交。
代码如下:
<span class="hljs-keyword">class</span> <span class="hljs-title class\_">MyComponent</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class\_ inherited\_\_">React.Component</span> { <span class="hljs-title function\_">render</span>(<span class="hljs-params"></span>) { <span class="hljs-keyword">const</span> c = <span class="hljs-title class\_">React</span>.<span class="hljs-property">createElement</span> <span class="hljs-keyword">return</span> <span class="hljs-title class\_">React</span>.<span class="hljs-title function\_">createElement</span>( <span class="hljs-string">div</span>, { <span class="hljs-attr">className</span>: <span class="hljs-string">parent-class</span> }, <span class="hljs-variable language\_">this</span>.<span class="hljs-property">props</span>.<span class="hljs-property">children</span> ) } } <span class="hljs-keyword">class</span> <span class="hljs-title class\_">MySubComponent</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class\_ inherited\_\_">React.Component</span> { <span class="hljs-title function\_">constructor</span>(<span class="hljs-params">props</span>) { <span class="hljs-comment">// 如果不需要使用 this.props,则可以是 super()</span> <span class="hljs-variable language\_">super</span>(props) <span class="hljs-comment">// 在构造函数中访问 this 之前,一定要调用 super(),它负责初始化 this</span> <span class="hljs-variable language\_">this</span>.<span class="hljs-property">state</span> = { <span class="hljs-attr">age</span>: <span class="hljs-number">18</span> } <span class="hljs-comment">// 自定义的方法,react 没有帮我们处理 this。所以这里需要我们自己绑定一下</span> <span class="hljs-variable language\_">this</span>.<span class="hljs-property">handleTextChange</span> = <span class="hljs-variable language\_">this</span>.<span class="hljs-property">handleTextChange</span>.<span class="hljs-title function\_">bind</span>(<span class="hljs-variable language\_">this</span>) <span class="hljs-variable language\_">this</span>.<span class="hljs-property">handleSubmit</span> = <span class="hljs-variable language\_">this</span>.<span class="hljs-property">handleSubmit</span>.<span class="hljs-title function\_">bind</span>(<span class="hljs-variable language\_">this</span>) } <span class="hljs-title function\_">handleTextChange</span>(<span class="hljs-params">evt</span>) { <span class="hljs-comment">// 不手动绑定 this,则为 undefined</span> <span class="hljs-comment">// console.log(this, this)</span> <span class="hljs-keyword">const</span> v = evt.<span class="hljs-property">target</span>.<span class="hljs-property">value</span> <span class="hljs-comment">// 用来自事件的数据更新组件状态。否则界面是看不到 age 的最新值的</span> <span class="hljs-variable language\_">this</span>.<span class="hljs-title function\_">setState</span>({ <span class="hljs-attr">age</span>: v }) } <span class="hljs-comment">// React 要阻止默认行为,必须显式的使用 preventDefault</span> <span class="hljs-title function\_">handleSubmit</span>(<span class="hljs-params">evt</span>) { evt.<span class="hljs-title function\_">preventDefault</span>() <span class="hljs-comment">// 提交表单。state {age: 18}</span> <span class="hljs-variable language\_">console</span>.<span class="hljs-title function\_">log</span>(<span class="hljs-string">提交表单。state</span>, <span class="hljs-variable language\_">this</span>.<span class="hljs-property">state</span>) } <span class="hljs-title function\_">render</span>(<span class="hljs-params"></span>) { <span class="hljs-keyword">const</span> c = <span class="hljs-title class\_">React</span>.<span class="hljs-property">createElement</span> <span class="hljs-keyword">return</span> <span class="hljs-title class\_">React</span>.<span class="hljs-title function\_">createElement</span>( <span class="hljs-string">form</span>, { <span class="hljs-attr">className</span>: <span class="hljs-string">sub-class</span>, <span class="hljs-attr">onSubmit</span>: <span class="hljs-variable language\_">this</span>.<span class="hljs-property">handleSubmit</span> }, <span class="hljs-title function\_">c</span>(<span class="hljs-string">p</span>, {}, <span class="hljs-title function\_">c</span>(<span class="hljs-string">input</span>, { <span class="hljs-attr">type</span>: <span class="hljs-string">text</span>, <span class="hljs-attr">value</span>: <span class="hljs-variable language\_">this</span>.<span class="hljs-property">state</span>.<span class="hljs-property">age</span>, <span class="hljs-comment">// React 事件的命名采用小驼峰式(camelCase),而不是纯小写。例如在 html 中通常都是小写(onclick)</span> <span class="hljs-attr">onInput</span>: <span class="hljs-variable language\_">this</span>.<span class="hljs-property">handleTextChange</span> }) ), <span class="hljs-title function\_">c</span>(<span class="hljs-string">p</span>, {}, <span class="hljs-title function\_">c</span>(<span class="hljs-string">input</span>, { <span class="hljs-attr">type</span>: <span class="hljs-string">submit</span>, <span class="hljs-attr">value</span>: <span class="hljs-string">submit</span> }) ), ) } }
利用函数可以将子组件的数据传递给父组件。核心代码如下:
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),结构如下:
class=componentA> <p class=componentB> apple p> <button class=componentC>add applebutton>
数据存在 AComponent 中,每点击一次 CComponent 组件,就会要求 AComponent 增加一个 apple,渲染到页面的 BComponent 组件也相应增加。
全部代码如下:
<span class="hljs-keyword">class</span> <span class="hljs-title class\_">AComponent</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class\_ inherited\_\_">React.Component</span> { <span class="hljs-title function\_">constructor</span>(<span class="hljs-params">props</span>) { <span class="hljs-variable language\_">super</span>(props) <span class="hljs-variable language\_">this</span>.<span class="hljs-property">state</span> = {<span class="hljs-attr">apples</span>: <span class="hljs-variable language\_">this</span>.<span class="hljs-property">props</span>.<span class="hljs-property">apples</span>} <span class="hljs-variable language\_">this</span>.<span class="hljs-property">handleAddApple</span> = <span class="hljs-variable language\_">this</span>.<span class="hljs-property">handleAddApple</span>.<span class="hljs-title function\_">bind</span>(<span class="hljs-variable language\_">this</span>) } <span class="hljs-title function\_">handleAddApple</span>(<span class="hljs-params">data</span>) { <span class="hljs-variable language\_">this</span>.<span class="hljs-title function\_">setState</span>({<span class="hljs-attr">apples</span>: [data, ...<span class="hljs-variable language\_">this</span>.<span class="hljs-property">state</span>.<span class="hljs-property">apples</span>]}) } <span class="hljs-title function\_">render</span>(<span class="hljs-params"></span>) { <span class="hljs-keyword">const</span> c = <span class="hljs-title class\_">React</span>.<span class="hljs-property">createElement</span> <span class="hljs-keyword">return</span> <span class="hljs-title function\_">c</span>( <span class="hljs-string">div</span>, { <span class="hljs-attr">className</span>: <span class="hljs-string">componentA</span> }, <span class="hljs-variable language\_">this</span>.<span class="hljs-property">state</span>.<span class="hljs-property">apples</span>.<span class="hljs-title function\_">map</span>(<span class="hljs-function">(<span class="hljs-params">item, index</span>) =></span> <span class="hljs-title function\_">c</span>(<span class="hljs-title class\_">BComponent</span>, { <span class="hljs-attr">content</span>: item, <span class="hljs-comment">// 需要增加 key 属性,否则报错:</span> <span class="hljs-comment">// Warning: Each child in a list should have a unique "key" prop.</span> <span class="hljs-attr">key</span>: index })), <span class="hljs-title function\_">c</span>(<span class="hljs-title class\_">CComponent</span>, { <span class="hljs-attr">onHandleAddApple</span>: <span class="hljs-variable language\_">this</span>.<span class="hljs-property">handleAddApple</span> }) ) } } <span class="hljs-keyword">class</span> <span class="hljs-title class\_">BComponent</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class\_ inherited\_\_">React.Component</span> { <span class="hljs-title function\_">render</span>(<span class="hljs-params"></span>) { <span class="hljs-keyword">const</span> c = <span class="hljs-title class\_">React</span>.<span class="hljs-property">createElement</span> <span class="hljs-keyword">return</span> <span class="hljs-title class\_">React</span>.<span class="hljs-title function\_">createElement</span>( <span class="hljs-string">p</span>, { <span class="hljs-attr">className</span>: <span class="hljs-string">componentB</span>, <span class="hljs-comment">// key: this.props.key</span> }, <span class="hljs-variable language\_">this</span>.<span class="hljs-property">props</span>.<span class="hljs-property">content</span> ) } } <span class="hljs-keyword">class</span> <span class="hljs-title class\_">CComponent</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class\_ inherited\_\_">React.Component</span> { <span class="hljs-title function\_">constructor</span>(<span class="hljs-params">props</span>) { <span class="hljs-variable language\_">super</span>(props) <span class="hljs-variable language\_">this</span>.<span class="hljs-property">handleAdd</span> = <span class="hljs-variable language\_">this</span>.<span class="hljs-property">handleAdd</span>.<span class="hljs-title function\_">bind</span>(<span class="hljs-variable language\_">this</span>) } <span class="hljs-comment">// React 要阻止默认行为,必须显式的使用 preventDefault</span> <span class="hljs-title function\_">handleAdd</span>(<span class="hljs-params">evt</span>) { <span class="hljs-variable language\_">this</span>.<span class="hljs-property">props</span>.<span class="hljs-title function\_">onHandleAddApple</span>(<span class="hljs-string">apple</span>) } <span class="hljs-title function\_">render</span>(<span class="hljs-params"></span>) { <span class="hljs-keyword">const</span> c = <span class="hljs-title class\_">React</span>.<span class="hljs-property">createElement</span> <span class="hljs-keyword">return</span> <span class="hljs-title class\_">React</span>.<span class="hljs-title function\_">createElement</span>( <span class="hljs-string">button</span>, { <span class="hljs-attr">className</span>: <span class="hljs-string">componentC</span>, <span class="hljs-attr">onClick</span>: <span class="hljs-variable language\_">this</span>.<span class="hljs-property">handleAdd</span> }, <span class="hljs-string">add apple</span> ) } } <script> const App = React.createElement(AComponent, { apples: [apple] }, ) ReactDOM.render( App, document.getElementById(root) ); script>
JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖 —— react 官网-深入 JSX
我们建议在 React 中配合使用 JSX,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式 —— react 官网-JSX 简介
jsx 让人编写类似于(但不是) HTML 的代码。
将上面增加 apple 的例子改为 jsx。全部代码如下:
<div id="root">div> <script src="https://unpkg.com/react@17/umd/react.development.js">script> <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js">script> <script src="https://unpkg.com/prop-types@15.6/prop-types.js">script> <script src="https://unpkg.com/@babel/standalone/babel.min.js">script> <script type="text/babel"> class AComponent extends React.Component { constructor(props) { super(props) this.state = { apples: this.props.apples } this.handleAddApple = this.handleAddApple.bind(this) } handleAddApple(data) { this.setState({ apples: [data, ...this.state.apples] }) } render() { return <div className=componentA> { this.state.apples.map( (item, index) => (<BComponent content={item} key={index} />) ) } <CComponent onHandleAddApple={this.handleAddApple} /> div> } } class BComponent extends React.Component { render() { return <p className=componentB>{this.props.content}p> } } class CComponent extends React.Component { constructor(props) { super(props) this.handleAdd = this.handleAdd.bind(this) } handleAdd(evt) { this.props.onHandleAddApple(apple) } render() { return <button className="componentC" onClick={this.handleAdd}>add applebutton> } } script> <script type="text/babel"> const App = <AComponent apples={[apple]} /> ReactDOM.render( App, document.getElementById(root) ); script>
jsx 除了类似于 HTML 且语法简单,另一个好处是声明式和封装。通过将组成视图的代码和相关联的方法包含在一起,使用者创建了一个功能组。本质上,需要知道的有关组件的所有信息都汇聚在此,无关紧要的东西都被隐藏起来,意味着使用者更容易的思考组件,并且更加清楚他们作为一个系统是如何工作的。