React.js入门笔记

2022-12-07,,,

React.js入门笔记


核心提示

这是本人学习react.js的第一篇入门笔记,估计也会是该系列涵盖内容最多的笔记,主要内容来自英文官方文档的快速上手部分和阮一峰博客教程。当然,还有我自己尝试的实例。日后还将对官方文档进阶和高级部分分专题进行学习并记录。

尽管前端学习面临着各种各样的焦虑,尽管越来越多的框架出现,然而无可否认的是,它们都在从不同的角度提高生产力——从这个角度而言,之所以焦虑,本质原因是因为行业的门槛其实是降低了,而自己变得“不值钱”起来。在目前的环境下,无论如何必须承认,学习一样技能不可能让你一辈子靠它吃饭了。如果真有,那就是原生的,基础的,底层的知识——当然这是旁话了。

假使者互联网的历史是一本薄薄的小册子,那么我经常看到一个历史事实:第一页说今天某个框架很火,有多少人在用,一时间风头无两。但翻到历史的下一页就是一句话:又出来一个新的框架,原来那个竞争不过,就怂了。

所以,给自己打个气吧:什么都可以怂,但是你,别怂了。


声明

React.js可以轻松创建交互式ui。 为你的webAPP设计出各种状态的简单视图效果。当数据更改时,react组件可以有效地反映出来。

声明式的方法使你的代码更容易可控和调试。
基于组件

封装了各种状态组件,然后组成复杂的ui。

因为JavaScript编写的组件逻辑而不是模板,您可以很容易地给你的APP创建丰富的数据,并通过DOM操控它们的状态。
学习一次,用在任何地方

我们不假设你其他的技术栈,所以你可以用react开发新特性时,不必重写现有代码。

语言:基于javascript,同时也涉及了ES6的部分语法,比如箭头函数(Arrow functions)、javascript类(Class>)、模板字符串(Template literals)等。


准备工作:安装react

笔者操作时基于如下布局。相关文件可以在官网下载到。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title></title>
<link rel="stylesheet" type="text/css" href="css/css.css"/>
<!-- 核心 -->
<script src="js/react.js"></script>
<!-- 加载dom方法 -->
<script src="js/react-dom.js"></script>
<!-- 将 JSX 语法转为 JavaScript 语法 -->
<script src="js/browser.min.js"></script>
<!-- 自身的javascript代码 -->
<script type="text/javascript" src="js/js.js"></script>
</head> <body>
<div id="example"></div> <!-- 凡是用到jsx语法的地方type应该为text/babel -->
<script type="text/babel"> </script>
</body>
</html>

上面代码有两个地方需要注意。首先,最后一个 <script> 标签的 type 属性为 text/babel 。这是因为 React 独有的 JSX 语法,跟 JavaScript 不兼容。凡是使用 JSX 的地方,都要加上 type="text/babel"

其次,上面代码一共用了三个库: react.jsreact-dom.jsBrowser.js ,它们必须首先加载。其中,react.js 是 React 的核心库,react-dom.js 是提供与 DOM 相关的功能,Browser.js 的作用是将 JSX 语法转为 JavaScript 语法,这一步很消耗时间,实际上线的时候,应该将它放到服务器完成。

——如果你把react语句写到外链的js里面,chrome无法支持非http协议的跨域读取。所以需要在服务器环境下使用。


一. Hello World!——ReactDOM.render()方法。

ReactDOM.render 是 React 的最基本方法,render直接理解为“渲染”,“表达”、“表述”也没啥问题。用于将模板转为 HTML 语言,并插入指定的 DOM 节点。

使用实例

使用方法如下:

ReactDOM.render(要插入的html内容,选择器)

ReactDOM.render(
<h1>Hello World!</h1>,
document.getElementById('example')
)

上面代码将一个 h1 标题,插入 example 节点。

渲染模块:React每次渲染只更新有必要更新的地方,而不会更新整个ui。

第一个参数怎么写

ReactDOM.render()方法的第一个参数有点像定义innerHTML,然而你不能这么恶搞:

<h1>hello world!</h1><h2>hehe</h2>//报错,两个顶层标签

但这样写是可以的:

<h1>hello <small>world!</small></h1>//通过:只有一个顶层标签

说白了,假设第一个参数是集装箱,那么就是你一次只能放一个箱子!

你可以给标签加上data-xxx或是class(注意,写作className),id等属性,但是你加style属性又会报错:

<h1>hello <small style="color:red;">world!</small></h1>//有行间样式会报错

style具体怎么写,会在后面提及。


二. JSX 语法

上面第一个参数的代码叫做JSX语法

所谓JSX语法,既不是javascript里的字符串,也不是html。像是在Javascript代码里直接写XML的语法,每一个XML标签都会被JSX转换工具转换成纯Javascript代码,并不加任何引号。

JSX允许你使用它生成React里的元素(Element),JSX可以编译为javascript

React 官方推荐使用JSX, 当然你想直接使用纯Javascript代码写也是可以的,只是使用JSX,组件的结构和组件之间的关系看上去更加清晰。

JSX语法规则的简单速成

基本语法规则:允许 HTML 与 JavaScript 的混写。遇到 HTML 标签(以 < 开头),就用 HTML 规则解析;遇到代码块(以 { 开头),就用 JavaScript 规则解析。

这意味着javascript里的语法,方法到了jsx一样适用。

另一方面,你可以按照xml的结构为JSX里的元素指定子类和属性。

然而需要注意的是,JSX的血缘相对于html,还是更亲近于javascript,属性还是得采用驼峰式写法,之前提到元素的class必须写成calssName,就是一个例子。

JSX代表一个对象。比如说我创建一个如下的jsx元素有一个基本结构是<h1 class="greeting">Hello world!</h1>,通常是这样写:

var  element  =  (  <h1  className="greeting">  Hello,  world!  </h1>  );

或者这样:

var element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);

实际上完整版是这样一个React对象:

var  element  =  {  type:  'h1',  props:  {  className:  'greeting',  children:  'Hello, world'  }  };

数组——塞箱子问题

  var arr=['hello','React','Vue'];
arr.push('Angular');
ReactDOM.render(
<div>
{
arr.map(function(ele){
return <h2>{ele}!</h2>
})
}
</div>,
document.getElementById('example')
)

效果将打出4个h2。

这似乎马上颠覆了刚刚建立起来关于集装箱每次只能放一个箱子的认知。其实更为正确的理解是:ReactDOM.render()作为一个模板被执行了4次。通过它,可以用较少的语句实现塞4个箱子。

其它往集装箱塞多个箱子的方法——还是数组

你还可以试试别的javascript语句能不能成。比如:

  var arr=['hello','React','Vue','Angular'];
var str=arr.join(' '); ReactDOM.render(
<div>
{
<h1>{arr}!</h1>
}
</div>,
document.getElementById('example')
)

说明代码块内可以放变量。

这种功能看起来稍显老旧,不如这么做:

  var arr=[
<h1 key={0}>hello</h1>,
<h2 key={1}>React,</h2>,
<h2 key={2}>Vue,</h2>,
<h2 key={3}>Angular!!!</h2>
];
ReactDOM.render(
<div>{arr}</div>,
document.getElementById('example')
);

上面代码的arr变量是一个数组,结果 JSX 会把它的所有成员,添加到模板。

笔者按:没加key值,会提示错误。

key值本质是一个字符串。React用它来识别数组中内容的变化。虽然React容忍了把数字作为key的行为。但是更标准的做法是key={1.toString()},当然你有ID名的话,把id作为key也是推荐的。
key只在数组遍历时用到。
同辈元素之间的key必须是唯一的。
加key只是框架内部的需要,不会帮你实现识别元素的功能。


三. 组件:React.createClass及调用

基本理解

区别元素(Elemnt)和组件(Component)

元素是组件的组成部分

组件和属性(props)

组件让你UI分割为若干个独立的、可复用的部分。

从概念上讲,组件就像JavaScript函数。 他们接受任意的参数(“props”)并返回React的元素。

组件可以套组件。

理解组件最简单的方法就是写一个函数

function  Welcome(props)  {  return  <h1>Hello,  {props.name}</h1>;  }

用ES6语法写成的组件函数是:

class  Welcome  extends  React.Component  {  render()  {  return  <h1>Hello,  {this.props.name}</h1>;  }  }

上面的代码创建一个函数允许你把props值作为参数传进去。而实际上,在react里面有自己封装组件的方法。

封装组件

React 允许将代码封装成组件(component),然后像插入普通 HTML 标签一样,在网页中插入这个组件。React.createClass 方法,顾名思义,就用于生成一个组件类。

    var Message=React.createClass({
render:function(){
return <h1>你是我的 {this.props.name}</h1>;
}
}); ReactDOM.render(
<Message name="小甜甜" />,
document.getElementById('example')
);

显示为h1标题。

让我们回顾这个例子:

我们称ReactDOM.render()方法渲染的<Message name="小甜甜" />为元素。
{name="小甜甜}作为Message组件的props。
<h1>你是我的小甜甜</h1>是这个组件渲染的结果。

你可以为这个组件类对象创建多个属性(this.props.xxx),然后通过给属性赋值来实例化它们.

所谓组件类,由React.createClass 方法生成。他的参数可以理解为一个对象。对象的constructor指向该组件。组件怎么表示呢?首先必须存在一个大写字母开头的变量比如Message里。然后在ReactDOM.render()方法中引用——<Message name="..."/>。实际上是个xml。注意是可以个自闭合标签。

跟之前插入html代码一样,实例内只能有一个顶层标签。

react是非常灵活的,但有一个严格的规则:

所有react组件的行为必须像纯函数,忠于它们的属性。

初步小结

于是我们对ReactDOM.render()方法又有了新的认识。到目前为止,ReactDOM.render()方法中,第一个参数必须只有一个顶层,它本质是元素:

xml语法的元素,
允许数组遍历方法(简单语句塞多个箱子,本质还是多次渲染)
还可以放组件类对象实例


四. this.props.children

理解children

前面说到,组件类方法React.createClass()的参数是个对象。你可以用this.props定义它的各种属性,而这个参数与组件的属性一一对应,但是,有一个例外,就是 this.props.children 属性。说白了就是你的属性用什么英文名都行,除了children

所谓children表示组件的所有子节点。

既然是所有属性的集合,不如试试这样恶搞:

var Message=React.createClass({
render:function(){
return <h1>你是我的 {this.props.children}</h1>;
}
});
ReactDOM.render(
<Message name="小甜甜" and="和" xxx="牛夫人" />,
document.getElementById('example')
);

虽然不报错,但你会发现什么属性都没显示出来。

尝试给这个this.prop.children加.name或是["name"]再或是[0]后缀,都不显示东西。

对children究竟是什么类型,比较靠谱的解释是:

这里需要注意, this.props.children 的值有三种可能:如果当前组件没有子节点,它就是 undefined ;如果有一个子节点,数据类型是 object ;如果有多个子节点,数据类型就是 array 。所以,处理 this.props.children 的时候要小心。

this.prop.children怎么用——React.Children.map()方法

数组确实是强大的功能,有了它可以很快地往箱子里塞东西。同理,React 提供一个工具方法 React.Children来处理 this.props.children 。我们可以用 React.Children.map 来遍历子节点,而不用担心 this.props.children 的数据类型是 undefined 还是 object

React.Children.map()方法结合this.props.children可以帮助我们快速地把组件类的对象打出来。

React.Children.map(this.prop.children,function(child){
return {child}
})

因为children没有指定对象是谁,所以要把第一个参数改写为xml的形式

   var Message=React.createClass({
render:function(){
return <h2>你是我的
{
React.Children.map(this.props.children,function(child){
return child//如果你想自动生成span那就应该是<span>{child}<span>
})
}</h2>;
}
}); ReactDOM.render(
<Message name="haha" named="hehe">//在此定义name等属性,不会显示。
<span>小甜甜</span>
<span>、牛夫人</span>
<span>和小亲亲~~</span>
</Message>,
document.getElementById('example')
);

打印出来的结构是:

span也不是必须加的,你完全可以把render表达式写成:

ReactDOM.render(
<Message>小甜甜、牛夫人和小亲亲~~</Message>,
document.getElementById('example')
);

这是只有一个子节点的情况。而多个子节点可以应用在ul/ol-li这样的体系中。

React.Children基本方法

我们可以用 React.Children.map 来遍历子节点.

1.React.Children.map

object React.Children.map(object children, function fn [, object context])

在每一个直接子级(包含在 children 参数中的)上调用 fn 函数,此函数中的 this 指向 上下文。如果 children 是一个内嵌的对象或者数组,它将被遍历:不会传入容器对象到 fn 中。如果 children 参数是 null 或者 undefined,那么返回 null 或者 undefined 而不是一个空对象。

2.React.Children.forEach

React.Children.forEach(object children, function fn [, object context])

类似于 React.Children.map(),但是不返回对象。

3.React.Children.count

number React.Children.count(object children)

返回 children 当中的组件总数,和传递给 map 或者 forEach 的回调函数的调用次数一致。

4.React.Children.only

object React.Children.only(object children)

返回 children 中仅有的子级。否则抛出异常。

组合和继承

React有一个强大的组合模型,官方建议组件之间多使用组合,而不是继承。

本节将研究如何使用组合做到继承的事。

容器

一些组件在应用之前,可能不知道他们的children。 比如常见的sidebar(组件栏)或对话框。

建议这样的组件通过props传递到子组件,以期望影响它们的输出效果:

    var Content=React.createClass({//子组件
render:function(){
return (
<div style={{color:this.props.color}}>
{this.props.children}
</div>
);
}
}); var App=React.createClass({//父组件
render:function(){
return (
<Content color="blue">
<h1>欢迎欢迎</h1>
<h2>热烈欢迎</h2>
</Content>
);//return的内容(props)是到用时再定义的
}
}); ReactDOM.render(
<App/>,
document.getElementById('example')
);

有的时候或许还需要预留接口,你可以定义:

var Xxx=React.createClass({
render:function(){
return (
<h1>欢迎欢迎</h1>
)
}
}); var Yyy=React.createClass({
render:function(){
return (
<h2>热烈欢迎</h2>
)
}
}) var Content=React.createClass({//子组件
render:function(){
return (
<div>
{this.props.xxx}
{this.props.yyy}
</div>
);
}
}); var App=React.createClass({//父组件
render:function(){
return (
<Content xxx={<Xxx/>} yyy={<Yyy/>}/>
);//把子组件和孙组件一次性封装,通过props
}
}); ReactDOM.render(
<App/>,
document.getElementById('example')
);

在上面这段代码中,Content的属性xxx,和yyy都是可以自定义组件。允许你灵活地放孙级组件。

上面代码中,xxx,yyy都是灵活的,如果你想让它变得不可定义,把它写死就行了。

进一步组合

var Xxx=React.createClass({
render:function(){
return (
<h1>欢迎欢迎</h1>
)
}
}); var Yyy=React.createClass({
render:function(){
return (
<h2>热烈欢迎</h2>
)
}
}); var Zzz=React.createClass({
render:function(){
return (
<h3>我是this.props.children</h3>
);
}
}); var Content=React.createClass({//子组件
render:function(){
return (
<div>
{this.props.xxx}
{this.props.yyy}
{this.props.children}
</div>
);
}
}); var App=React.createClass({//父组件
render:function(){
return (
<Content xxx={<Xxx/>} yyy={<Yyy/>}>
<Zzz/>
</Content>
);//在此代码中<Zzz/>属于props.children
}
}); ReactDOM.render(
<App/>,
document.getElementById('example')
);

继承呢?

暂时没有发现非使用继承不可的地方。

props和组合给你所需要的灵活性,借此可以明确而安全地定制组件的外观和行为。 记住,组件可以接受任意的props,包括原始值、react元素,或函数。

如果你在组件间复用非ui相关的函数,建议把它提取到一个单独的JavaScript模块。 组件可以调用和使用这个函数,对象,或是类,而不必去扩展它。


五. 组件的protoTypes和React的PropTypes

验证数据类型——React.PropTypes.number.isRequired

组件的属性可以接受任意值,字符串、对象、函数等等都可以。有时,我们需要一种机制,验证别人使用组件时,提供的参数是否符合要求,主要用于调试。

笔者按:

如果你尝试对组件属性存放一个Object对象或是其实例,或是Date对象,会弹出错误!直接提示你对象不适合作为React的一个子属性,因此愚以为阮氏至少有点片面。
再者,属性不一定都是要拿来显示到网页上的。所以你存个函数,存个布尔值,存个对象的方法,都没问题。只是当你尝试显示返回非字符串,数字等内容的时候,显示为空白。
然后,注意大小写区别。同时也要注意他和prototype原型的区别。

之前提到,React.createClass()方法的参数是一个对象,设计者还给它放置了组件类的protoTypes子属性(注意是小写开头!)。我的理解就是存放限制子属性设置的地方。

而这个React.PropTypes属性(注意是大写开头!),就是用来验证组件实例的属性是否符合要求。

console.log(React.ProtoType),可以看到它的属性都是一个对象,拥有各种方法。

其中最为广泛最多的就是这个isRequire。理解为是“必须的”就可以了。

比如这个例子:

  var result=[1,2,3,4]
var Message=React.createClass({
propTypes: {
sum: React.PropTypes.number.isRequired//调用验证
}, render:function(){
return <h2>1+1=
{
this.props.sum
}
</h2>
}
});
console.log(React.PropTypes)
ReactDOM.render(
<Message sum={result}/>
,document.getElementById('example')
);

输出为1+1=1234虽然怎么看1234都像数字,但它确实是个数组。即使能显示,但是会报错。

getDefaultProps

此外,getDefaultProps 方法可以用来设置组件属性的默认值。

这个功能与其说是get不如说是set。当你定义了各种属性之后,可以在该组件prototype下的constructor.defaultProps找到它们。原来它们在本质上还是一个object对象。

借助这个框架,你可以快速地封装自己想要的子属性和方法。

var Xxx=React.create({
protoTypes:{
...
},
getDefaultProp:function(){
return {
prop1:...
prop2:...
prop3:...
}
},//放函数,不要直接放对象!
render:function(){
...
}
}
})

有了它,似乎可以不必在行间定义各种属性值了,看起来相当美观。

然而,面临这样一个问题。

  var Message=React.createClass({
getDefaultProps:function(){
return {
name1:'小亲亲',
name2:'小甜甜',
name3:'牛夫人'
}
}, render:function(){
return <h2>你是我的
{
this.props.name1
}
</h2>
}
});
//console.log(Message)
ReactDOM.render(
<Message name1="xxx"/>
,document.getElementById('example')
);

打印出的效果是你是我的xxx

constructor.defaultProps中是找不到的行间定义的name的。


六. 获取真实的DOM节点——ref的定义与调用

关于ref

在React的典型数据流中,props是组件和它的子属性交互的唯一方式。每次修改一个child,你就得给再渲染它一次。然而,在个别情况下你需要强行绕过典型数据流修改child,这个child可能是组件的实例。也可能是DOM的节点,因此React提供了一个修改途径——ref。

React支持特殊的属性ref,你可以附加到任何组件上。 ref属性需要一个回调函数,此函数将组件安装或卸载后立即执行。ref属性用于HTML元素时,ref回调接收底层的DOM元素作为它的参数。

再通俗一点点

组件并不是真实的 DOM 节点,而是存在于内存之中的一种数据结构,叫做虚拟 DOM (virtual DOM)。只有当它插入文档以后,才会变成真实的 DOM 。根据 React 的设计,所有的 DOM 变动,都先在虚拟 DOM 上发生,然后再将实际发生变动的部分,反映在真实 DOM上,这种算法叫做 DOM diff ,它可以极大提高网页的性能表现。

但是,有时需要从组件获取真实 DOM 的节点,这时就要在你需要的节点中插入 ref 属性,然后通过相关事件去定义它,注意,调用时的节点为this.refs.ref名。

案例:

一个组件,返回一个文本框和一个按钮。要求点击按钮后,获取文本框节点

    var MyComponment=React.createClass({
btnClick:function(){//btnClick是自定义的函数名,用于定义触发事件后实行的方法,调用为this.btnClick
this.refs.myTextInput.focus();//获取焦点。
},
turnGreen:function(){//载定义一个改变value值的方法
this.refs.myTextInput.value="我怎么绿了";
this.refs.myTextInput.style.color="green";
},
render:function(){
return (
<div>
<input type="text" ref="myTextInput" onFocus="this.turnGreen" />//要求自闭合标签全部写上“/”!否则报错
<input type="button" value="Focus this text input" onClick={this.btnClick} />
</div>
);
}
}); ReactDOM.render(
<MyComponment/>
,document.getElementById('example')
);

上面代码中,组件 MyComponent 的子节点有一个文本输入框,用于获取用户的输入。这时就必须获取真实的 DOM 节点,虚拟 DOM 是拿不到用户输入的。为了做到这一点,文本输入框必须有一个 ref 属性,然后 this.refs.[refName] 就会返回这个真实的 DOM 节点。

在获取焦点之后,马上对其执行turnGreen方法。使得里面的样式变绿。

如果直接在输入框节点中指定value值,结果是只读的。用户无法修改。报错信息如下

react.js:19287 Warning: Failed form propType: You provided a value prop to a form field without an onChange handler. This will render a read-only field. If the field should be mutable use defaultValue. Otherwise, set either onChange or readOnly. Check the render method of MyComponment.

解决思路参见第八章 表单

需要注意的是,由于** this.refs.[refName] 属性获取的是真实 DOM ,所以必须等到虚拟 DOM 插入文档以后,才能使用这个属性,否则会报错。**上面代码中,通过为组件指定 Click 事件的回调函数btnClick,确保了只有等到真实 DOM 发生 Click 事件之后,才会读取 this.refs.[refName] 属性。

React 组件支持很多事件,除了 Click 事件以外,还有 KeyDownCopyScroll 等,完整的事件清单请查看官方文档。

不要滥用ref

学习了refs之后,你的第一反应就是“用事件触发它发生”。如果真是这样的话,不如花点事件想想你的组件结构应该有哪些状态(state),显然,每个组件应该有自己的合适状态。参见组件的生命周期。


七. 状态——state

组件免不了要与用户互动,React 的一大创新,就是将组件看成是一个状态机,一开始有一个初始状态,然后用户互动,导致状态变化,从而触发重新渲染 UI。

根据状态响应不同的内容

接下来结合ref来做个demo

    var ToggleState=React.createClass({
getInitialState:function(){//getInitialState是固有方法
return {check:false};
},//设置一个布尔值状态属性check
toggleClick:function(event){
this.setState({
check:!this.state.check
});//每次触发就改变布尔值
if(this.state.check){
this.refs.para.innerText='我不喜欢';
this.refs.para.style.color="green";
}else{
this.refs.para.innerText='我喜欢';
this.refs.para.style.color="purple";
}
},
render:function(){
return (
<p ref="para" onClick={this.toggleClick}>
你喜欢男人吗?点击切换。
</p>
)
}
}); ReactDOM.render(
<ToggleState/>,
document.getElementById('example')
);

上面代码是一个 toggleState 组件,它的 getInitialState 方法用于自定义一个check属性并设置其初始状态,可以通过 this.state 属性读取。当用户点击组件,导致状态变化,this.setState 方法修改状态值,每次修改以后,都会自动调用 this.render 方法,再次渲染组件。

由于 this.propsthis.state 都用于描述组件的特性,可能会产生混淆。一个简单的区分方法是,this.props 表示那些一旦定义后只读的特性,是静态的。而 this.state 是会随着用户互动而产生变化的特性。是动态的。

正确使用状态

setState({xx:yyy}),不要用this.state.xx=yyy
一个看上去很悲剧的事实:状态更新可能是异步的。

因为this.propsthis.state可能都是异步刷新,因此,不要根据state的值去计算并定义下一个状态。比如:

this.setState({  counter:  this.state.counter  +  this.props.increment,  });

实际上,setState方法还可以接收两个参数

// Correct
this.setState((prevState, props) => ({ counter: prevState.counter + props.increment }));//相当于return
// Correct
this.setState(function(prevState, props) { return { counter: prevState.counter + props.increment }; });

以上两种写法是一样效果的。第一个参数是前面前一个状态,第二个参数是当下时刻的props值。

状态更新合并

当你调用设置setState,React对象可以根据你所提供的新状态值重新计算合并为一个新的state。

例如,你的状态可能包含几个独立变量:

状态的数据流

无论父或子组件都无法知道某个组件有没有state,它们也不关心这个组件被定义为一个函数或是一个类。

这就是为什么state通常只在组件内部封装和调用。除了组件自身,外部组件是无法访问到该组件的state对象的。

组件可以选择通过其state来作为props:比如本章案例中的toggleClick函数。在组件嵌套的时候,你也可以把父组件的state传给子组件——通过设置字组件props。

重点来了:这通常被称做“自上而下”或“单向”数据流的行为方式了。 任何收到其它组件(比如组件A)state影响的组件(比如组件B),A必定是B的父级组件。子组件不可能通过自身的state影响上层的父级组件。

假想组件嵌套是一条河流,如果说props是河床,那么每个子组件的state就是各个小支流的源头。水不可能往高处流。

比如说本章例子,我创建一个新组件,包括了三个:

ReactDOM.render(
<div>
<ToggleState/>
<ToggleState/>
<ToggleState/>
</div>,
document.getElementById('example')
);

在这个例子中,三个子组件彼此都是孤立的,自己拥有各自的state,互不影响。

共享状态

通常,几个组件需要反映相同的数据变化。 最好的办法是几个子组件共享它们共同父组件的state。

比如说

在本节中,我们将创建一个计算器来计算温度的水是否会煮在一个给定的温度。

我们将从一个组件称为BoilingVerdict开始。 它接受摄氏温度作为支撑,并打印是否足以煮水:

事件处理方法

react的元素处理事件方法非常类似于DOM元素的行间处理事件方法。

它不是行间javascript

现在我们知道在行间加javascript处理函数是非常不规范的。但是react的加事件处理函数并不是真正的行间js。在语法和函数使用上可以看出本质差异:

react使用驼峰命名命名事件,而不是小写。
JSX语法下,你的事件处理程序传递的是一个函数一个函数,而不是一个字符串。

<p onclick="toggleClick()"><!--行间javascript-->

在react是这样:

<p ref="para" onClick={this.toggleClick}>//这是react的方式

阻止浏览器默认行为:react不允许用return false!而应该用

toggleClick:function(event){
event.preventDefault();
}

这里用到了参数event

事件处理方法的参数event

在这里,event是一个合成的事件。它由React根据W3C规范定义,所以你不必担心跨浏览器兼容性。 看到SyntheticEvent参考指南了解更多信息。

使用React时你通常不需要调用addEventListener侦听器。 相反,只需要提供一个侦听器时最初渲染的元素。

比如我有一个按钮组。当点击一个按钮想获得该按钮的响应,获取方法就是event.target

必须留意JSX回调的内涵。 在JavaScript中,对象方法不受限制。在本章案例中调用toggleClick方法时,如果你不给toggleClick绑定this,就将其传递给onClick,得到的将是undefined

这是一个JavaScript函数的基本原理之一。


八. 表单

表单是用户和网页实现动态交互的最直接实例。用户在表单填入的内容,属于用户跟组件的互动,由于虚拟DOM的特性,所以不能用 this.props

在HTML表单元素,如<input><textarea>,和<select>通常根据用户输入情况而更新。 在react中,可变状态通常保存在组件的state里面,想要实时更新,只有用setState()方法。

应用于输入框

    var Input=React.createClass({
getInitialState:function(){
return {
value:'文本框内容随输入变化而变化噢'
};
},//定义设置value的初始状态为hello
change:function(event){//定义输入框value改变的回调函数
this.setState({
value:event.target.value//注意事件对象
});
},//触发事件后,vlue随着用户输入的value而变化。
render:function(){
var value=this.state.value;
return (
<div>
<input type="text" value={value} onChange={this.change}/>
<p>{value}</p>
</div>
);
}
}); ReactDOM.render(
<Input/>,
document.getElementById('example')
)

上面代码中,文本输入框的值,不能用 this.props.value 读取,而要定义一个 onChange 事件的回调函数,通过 event.target.value 读取用户输入的值。textarea 元素、select元素、radio元素都属于这种情况,更多介绍请参考官方文档。

应用于下拉框

先看一个案例:实现一个下拉菜单(select-dropdown)。要求html渲染出以下信息:

<form>
<label>
你喜欢:
<select>
<option value="男人">男人</option>
<option value="女人">女人</option>
<option selected value="都喜欢">都喜欢</option><!--被选中状态-->
<option value="都不喜欢">都不喜欢</option>
</select>
</label>
<input type="submit" value="提交!"/>
</form>

点击提交时,弹出对应的信息。

分析:有一点需要注意:初始状态是“都喜欢”被选中。你不能直接在option里面加selected属性。在JSX语法中。定义下拉菜单的选中状态是<selecte>元素的value值(对应option的内容)。

    var App=React.createClass({
getInitialState:function(){
return ({
list:{
"v1":"男人",
"v2":"女人",
"v3":"都喜欢",
"v4":"都不喜欢"
},
value:"都喜欢"
})
},
submit:function(e){
var info='Yooooo,原来你喜欢'+this.state.value+'呀!'
alert(info);
e.preventDefault();//jQuery阻止冒泡
},
change:function(e){
console.log(e)
this.setState({
value:e.target.value
})
},
render:function(){
var list=[];
for(var i=1;i<=Object.getOwnPropertyNames(this.state.list).length;i++){
var listInfo=this.state.list["v"+i];
list.push(
<option key={i.toString()} value={listInfo}>{listInfo}</option>
); }
console.log(list)
return (
<form>
<label>
你喜欢:
<select value={this.state.value} onChange={this.change}>{list}</select>
</label>
<input type="submit" value="提交" onClick={this.submit}/>
</form>
)
}
}); ReactDOM.render(
<App/>,
document.getElementById('example')
);


九. 组件的生命周期(lifecycle)

组件的生命周期分成三个状态:

Mounting:已插入真实 DOM
Updating:正在被重新渲染
Unmounting:已移出真实 DOM

React 为每个状态都提供了两种处理函数,will 函数在进入状态之前调用,did 函数在进入状态之后调用,三种状态共计五种处理函数。

componentWillMount():组件插入前执行
componentDidMount():组件插入后执行(重要)
componentWillUpdate(object nextProps, object nextState)
componentDidUpdate(object prevProps, object prevState):组件被移除后执行
componentWillUnmount():组件被移除前执行

此外,React 还提供两种特殊状态的处理函数。

componentWillReceiveProps(object nextProps):已加载组件收到新的参数时调用
shouldComponentUpdate(object nextProps, object nextState):组件判断是否重新渲染时调用

这些方法的详细说明,可以参考官方文档。

接下来这个demo

    var Hello=React.createClass({
getInitialState:function(){
return {
opacity:1.0
};
}, componentDidMount:function(){//组件插入后执行!
this.timer=setInterval(function(){
var opacity=this.state.opacity;
opacity-=0.05;
if(opacity<0.1){
opacity=1.0;
}
this.setState({
opacity:opacity
});
}.bind(this),100);//定时器必须绑定this,否则出错
}, render:function(){
return (
<div style={{opacity:this.state.opacity}}>
Hello {this.props.name}
</div>
)
}
}); ReactDOM.render(
<Hello name="World" />,
document.getElementById('example')
)

上面代码在hello组件加载以后,通过 componentDidMount 方法设置一个定时器,每隔100毫秒,就重新设置组件的透明度,从而引发重新渲染。

另外,组件的style属性的设置方式也值得注意,不能写成

style="opacity:{this.state.opacity};"

而要写成

style={{opacity: this.state.opacity}}

这是因为 React 组件样式是一个对象,所以第一重大括号表示这是 JavaScript 语法,第二重大括号表示样式对象。


十. Ajax方法

组件的数据来源,通常是通过 Ajax 请求从服务器获取,可以使用 componentDidMount 方法设置 Ajax 请求,等到请求成功,再用 this.setState 方法重新渲染 UI)。

还是老方法

为了获取数据并把它打到页面上其实思路和上面例子差不多。 componentDidMount()方法执行插入后渲染。

为了简化操作,我们引入jquery文件到页面中(React本身没有任何依赖)。同时,自己做一个json文件,放到demo根目录下,在本地服务器环境下运行网页。

[
{
"owner":{
"login":"Dangjingtao"
},
"url":"http://www.baidu.com"
}
]

接下来使用getJSON方法获取这个json数据。完全没有用新的其它react方法:

    var Info=React.createClass({
getInitialState:function(){
return {
userName:'',
lastGistUrl:''
};
},
componentDidMount:function(){
$.getJSON(this.props.source,function(data){
console.log(data)
var lastVisit=data[0];
if(this.isMounted()){
this.setState({
userName:lastVisit.owner.login,
lastVisitUrl:lastVisit.url
});
}
}.bind(this));//注意,涉及到的方法都绑定!
}, render:function(){
return (
<div>
{this.state.userName} is Last visit is
<a href={this.state.lastVisitUrl}>here</a>
</div>
);
}
}); ReactDOM.render(
<Info source="json.json" />,
document.getElementById('example')
);

here链接向百度。

扩展

我们甚至可以把一个Promise对象传入组件。

var RepoList = React.createClass({
getInitialState: function() {
return { loading: true, error: null, data: null};
}, componentDidMount() {
this.props.promise.then(
value => this.setState({loading: false, data: value}),
error => this.setState({loading: false, error: error}));
}, render: function() {
if (this.state.loading) {
return <span>Loading...</span>;
}
else if (this.state.error !== null) {
return <span>Error: {this.state.error.message}</span>;
}
else {
var repos = this.state.data.items;
var repoList = repos.map(function (repo) {
return (
<li>
<a href={repo.html_url}>{repo.name}</a> ({repo.stargazers_count} stars) <br/> {repo.description}
</li>
);
});
return (
<main>
<h1>Most Popular JavaScript Projects in Github</h1>
<ol>{repoList}</ol>
</main>
);
}
}
});

上面代码从Github的API抓取数据,然后将Promise对象作为属性,传给RepoList组件。

如果Promise对象正在抓取数据(pending状态),组件显示"正在加载";如果Promise对象报错(rejected状态),组件显示报错信息;如果Promise对象抓取数据成功(fulfilled状态),组件显示获取的数据。


十一. 实例1:显示在线时间

思路:

首先设置一个初始状态,获取系统时间的时刻。
在插入文档之后,调用定时器,每个一秒刷新时间。

    var Time=React.createClass({
getInitialState:function(){
return {
now:new Date().toLocaleTimeString()
}
},
componentDidMount:function(){//组件插入后执行!
console.log(this)
this.timer=setInterval(function(){
this.setState({
now:new Date().toLocaleTimeString()
});
}.bind(this),1000);//定时器必须绑定this,否则出错
},
componentDidUnmount:function() {
console.log('组件已被移除!')
//clearInterval(this.timer);
},
render:function(){
return (
<div>
<h1>Hello, world!</h1>
<h2>现在是: {this.state.now}.</h2>
</div>
)
}
}); ReactDOM.render(
<Time/>,
document.getElementById('wrap')
);

让我们快速回顾一下发生了什么:

1)当<Time/>传递给ReactDOM.render(),组件的调用构造函数。 由于时钟需要显示当前时间,它需要初始化。状态对象包括当前时间。 在插入文档之后,我们会更新这个状态。

2)接着React调用render()方法。 把初始状态反映在屏幕上。

3)插入到真实DOM后,调用componentDidMount()。 它在插入后每秒执行一个函数,而每次执行都会重新渲染,重新插入到页面中。

4)如果组件被移除,将激活componentDidUnmount()


十二. 实例2:华氏摄氏温标转换

实际上这是数据双向绑定的例子

实现基本功能:两个输入框,在第一个摄氏温标输入框输入数字。第二个输入框自动绑定计算后的华氏温标数据。反之亦然。

另外,根据100度沸腾的原则。判定当前温度是否沸腾。

思路:输入框要注意这么一个事实:如果不进行双向绑定,会导致输出失败

var Judge=React.createClass({
getInitialState:function(){
return ({
info:'',
})
}, render:function(){
console.log(this.state);
//var info='';
var info='';
if(this.props.judge>=100){
info="水开了"
}else{
info="水没开"
};
return (
<span>{info}</span>
)
}
}); var App=React.createClass({
getInitialState:function(){
return (
{
valueC:'',
valueF:''
}
)
},
change:function(e){
this.setState({
valueC:e.target.value,
valueF:this.toF(e.target.value)
})
}, toC:function(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
},
toF:function(celsius) {
return (celsius * 9 / 5) + 32;
}, render:function(){
return (
<form>
摄氏温度:<input type="text" onChange={this.change} value={this.state.valueC} /><br/>
华氏温度:<input type="text" onChange={this.change} value={this.state.valueF} /><br/>
<Judge judge={this.state.valueC}/>
</form>
)
}
}); ReactDOM.render(
<App/>,
document.getElementById('example')
)

曾经考虑过给子组件的判断函数设置状态,如果你在render函数内部(非“行间”)设置状态,会导致死循环,报错信息建议把状态设置到生命周期相关的函数中,但是这样做非常非常之卡。因此我摒弃了这个做法。


十三. 实例3:表单校验

模拟一个表单,完成实时输入校验。

思路

实际上就是设置一个状态。组件会监听你输入的文字内容,触发规则之后,改变状态,再根据这个状态修改其它监听的内容。

规则显示模块用一个子组件Judge来实现。

    var Judge=React.createClass({//子组件

      judge:function(type,propsValue){
var content='';
if(type=="email"){//如果type属性为email
var reEmail=/^\w+@[a-z0-9]+\.[a-z]{2,4}$/;
if(this.props.value==''){
content="";
}else if(reEmail.test(propsValue)){
content="输入正确!";
}else{
content="请输入正确的邮箱名!";
}
} return content;
}, setColor:function(ref,contentType){
//根据内容判断颜色
if(ref){//在首次加载渲染时,this.refs.main为undefined
if(contentType=="输入正确!"){
ref.style.color="green";
}else{
ref.style.color="red";
}
}
}, render:function(){
var content=this.judge(this.props.type);
this.setColor(this.refs[this.props.type],content);
return (
<span ref={this.props.type}>{content}</span>
)
}
}); var App=React.createClass({
getInitialState:function(){
return ({
emailValue:''
});
},
change:function(e){
this.setState({
emailValue:e.target.value
});
},
render:function(){
return (
<form>
邮箱:<input type="text" onChange={this.change} value={this.state.emailValue} /><br/>
<Judge type="email" value={this.state.emailValue}/><br/>
</form>
)
}
});

基本效果:

到目前为止已经实现了对Judge组件的初步封装

到目前为止,组件实现看起来都很简单。

加上密码校验。

根据同样的原理,再实现密码校验

    var Judge=React.createClass({//子组件

      judge:function(type,propsValue){
var content='';
if(type=="email"){//如果type属性为email
var reEmail=/^\w+@[a-z0-9]+\.[a-z]{2,4}$/;
if(propsValue==''){
content="";
}else if(reEmail.test(propsValue)){
content="输入正确!";
}else{
content="请输入正确的邮箱名!";
}
}else if(type=="password"){//如果是password
var rePassword=/^[a-zA-Z0-9]{6,10}$/;
if(propsValue==''){
content='';
}else if(rePassword.test(propsValue)){
content="输入正确!";
}else{
content="密码必须包括6-10位英文字母和数字!";
}
} return content;
}, setColor:function(ref,contentType){
//根据内容判断颜色
if(ref){//在首次加载渲染时,this.refs.main为undefined
if(contentType=="输入正确!"){
ref.style.color="green";
}else{
ref.style.color="red";
}
}
}, render:function(){
var content=this.judge(this.props.type,this.props.value);
this.setColor(this.refs[this.props.type],content); return (
<span ref={this.props.type}>{content}</span>
)
}
}); var App=React.createClass({
getInitialState:function(){
return ({
emailValue:'',
passwordValue:''
});
},
changeEmail:function(e){
this.setState({
emailValue:e.target.value,
});
},
changePassword:function(e){
this.setState({
passwordValue:e.target.value,
});
},//此函数不太能传参,不得不多设一个
render:function(){
return (
<form>
邮箱:<input type="text" onChange={this.changeEmail} value={this.state.emailValue} /><br/>
<Judge type="email" value={this.state.emailValue}/><br/>
密码:<input type="text" onChange={this.changePassword} value={this.state.passwordValue} /><br/>
<Judge type="password" value={this.state.passwordValue}/><br/>
</form>
)
}
}); ReactDOM.render(
<App/>,
document.getElementById('example')
);

效果可以自己试试。

按钮

重置按钮只需要把App组件的全部状态清空就行了。

没什么说的。

提交按钮

实现提交按钮,把行为指向百度。如果有一个不符合规范,就不能通过。

因为Judge组件不能很方便把状态返回到上层,我觉得这是该表单验证架构的最大短板。

但是父级组件可以读取下层的信息,可以给Judge组件填上自己的ref:,那么父组件就可以通过两个this.refs.xxx.refs.xxx访问到子组件返回的html结构内容。有了内容,就可以做判断了。

var Judge=React.createClass({//子组件
getInitialState:function(){
return {
check:false
}
}, judge:function(type){
var content='';
if(type=="email"){//如果type属性为email
var reEmail=/^\w+@[a-z0-9]+\.[a-z]{2,4}$/;
if(this.props.value==''){
content="";
}else if(reEmail.test(this.props.value)){
content="输入正确!"; }else{
content="请输入正确的邮箱名!"; }
}else if(type=="password"){//如果是password
var rePassword=/^[a-zA-Z0-9]{6,10}$/;
if(this.props.value==''){
content=''; }else if(rePassword.test(this.props.value)){
content="输入正确!";
}else{
content="密码必须包括6-10位英文字母和数字!"; }
} return content;
}, setColor:function(ref,contentType){
//根据内容判断颜色
if(ref){//在首次加载渲染时,this.refs.main为undefined
if(contentType=="输入正确!"){
ref.style.color="green"; }else{
ref.style.color="red"; }
}
}, render:function(){
var content=this.judge(this.props.type);
this.setColor(this.refs[this.props.type],content); return (
<span ref={this.props.type}>{content}</span>
)
}
}); var App=React.createClass({
getInitialState:function(){
return ({
emailValue:'',
passwordValue:''
});
},
changeEmail:function(e){
this.setState({
emailValue:e.target.value,
});
},
changePassword:function(e){
this.setState({
passwordValue:e.target.value
});
},//此函数不太能传参,不得不多设一个
reset:function(){//重置实现
this.setState({
emailValue:'',
passwordValue:''
})
},
submit:function(e){
// console.log(this.refs.email.refs.email.innerText);
// console.log(this.refs.password.refs.password.innerText);
var checkEmail=this.refs.email.refs.email.innerText;
var checkPassword=this.refs.password.refs.password.innerText;
if(checkEmail=="输入正确!"&&checkPassword=="输入正确!"){
alert('注册成功!');
}else{
alert('请检查你填写的信息!');
e.preventDefault();
} },
render:function(){
return (
<form method="post" action="http://www.baidu.com">
邮箱:<input type="text" onChange={this.changeEmail} value={this.state.emailValue} /><br/>
<Judge ref="email" type="email" value={this.state.emailValue}/><br/> 密码:<input type="text" onChange={this.changePassword} value={this.state.passwordValue} /><br/>
<Judge ref="password" type="password" value={this.state.passwordValue}/><br/> <input type="button" value="重置" onClick={this.reset}/>
<input type="submit" value="注册" onClick={this.submit}/>
</form>
)
}
}); ReactDOM.render(
<App/>,
document.getElementById('example')
);

十四.实例4 选项卡实例

选项卡怕是每个网页设计者做的第一个组件。而第一次总是看遍原理却无从下手。在此就用前面的知识做一个选项卡吧。

基本样式设计

一个选项卡,基本结构就是两个ul-li点击来回切换。所以主要的结构应该是:

<ul class="btns">
<li><a class="active" href="javascript:;">1</a></li>
<li><a href="javascript:;">2</a></li>
<li><a href="javascript:;">3</a></li>
<li><a href="javascript:;">4</a></li>
</ul>
<ul class="imgs">
<li class="active"><img src="images/1.jpg"/></li>
<li><img src="images/2.jpg"/></li>
<li><img src="images/3.jpg"/></li>
<li><img src="images/4.jpg"/></li>
</ul>
</div>

css

/*css-reset*/
*{
margin:0;
padding: 0;
}
ul li{
list-style: none;
}
a{
text-decoration: none;
} /******************/
.tabs{
width: 400px;
margin: 200px auto;
}
.btns a{
display: block;
width: 30px;
float: left;
line-height: 30px;
text-align: center;
}
.btns li{
float: left;
} .btns .active{
background: #ccc;
}
.imgs li{
display: none;
}
.imgs .active{
display: block;
}

基本效果

接下来就通过react的渲染来实现功能。

第一个问题:塞箱子

事实上我觉得这个最难的问题。

首先封装按钮ul组件和图片库ul组件,然后把它加到div#tabs里面去。考虑用做两个数组。

那么基本初始化样式就有了。在组件类的render函数下用循环自动生成两个数组——然后再把此数组插入到对应结构的html中。

render函数如下:


render:function(){
var numArr=[];
var imgArr=[];
for(var i=0;i<this.state.num;i++){
if(i===0){
numArr.push(
<li key={i+1}><a className="active" href="javascript:;" onClick={this.change}>{i+1}</a></li>
);
imgArr.push(
<li className="active" key={i+1}><img src="images/1.jpg" /></li>
);
}else{
var str="images/"+(i+1).toString()+".jpg"
numArr.push(
<li key={i+1}><a href="javascript:;" onClick={this.change}>{i+1}</a></li>
);
imgArr.push(
<li key={i+1}><img src={str} /></li>
);
} } return (
<div className="tabs">
<ul className="btns">
{numArr}
</ul>
<ul className="imgs">
{imgArr}
</ul>
</div>
)
},

主要思想是:设置一个名字为num的state,接收来自服务器的数据(传进来图片的张数)。然后根据这个num来设置数组需要哪些元素。

事件回调

在上面的结构生成中,有个点击触发的change方法。里面的dom操作全部依赖于点击事件的this。实际上点击发生的对象来自该函数的第一个参数event.target。

change:function(event){
var $all=$(event.target).parent().siblings().children();
$all.removeClass('active');
$(event.target).addClass('active'); var index=$(event.target).parent().index();//获取索引值
var $allImgList=$(event.target).parent().parent().next().children();
$allImgList.hide();
$allImgList.eq(index).fadeIn(100);
},

那么这个函数就没问题了,跟jquery选显卡的代码差不多。

获取数据

我们首先写一个json.json文件到根目录,来模拟获取的服务器数据

[
{
"num":"4"
}
]

设置初始状态:

getInitialState:function(){
return {
num:0//来自服务器
}
},

然后在虚拟dom插入到页面之前就拿到数据——提示用componentWillMount。这里是图片所展现的张数。

在此我定义一个getListUrl属性,

componentWillMount:function(){
$.getJSON(this.props.getListUrl,function(data){
//console.log(this.state);
this.setState({
num:data[0]["num"]//设置状态值为获取到的数据
})
}.bind(this))
},

实现

全部代码如下

    var App=React.createClass({

      getInitialState:function(){
return {
num:0//来自服务器
}
}, componentWillMount:function(){
$.getJSON(this.props.getListUrl,function(data){
//console.log(this.state);
this.setState({
num:data[0]["num"]
})
}.bind(this))
}, change:function(event){
var $all=$(event.target).parent().siblings().children();
$all.removeClass('active');
$(event.target).addClass('active'); var index=$(event.target).parent().index();
var $allImgList=$(event.target).parent().parent().next().children();
$allImgList.hide();
$allImgList.eq(index).fadeIn(100); }, render:function(){
var numArr=[];
var imgArr=[];
for(var i=0;i<this.state.num;i++){//
if(i===0){
numArr.push(
<li key={i+1}><a className="active" href="javascript:;" onClick={this.change}>{i+1}</a></li>
);
imgArr.push(
<li className="active" key={i+1}><img src="images/1.jpg" /></li>
);
}else{
var str="images/"+(i+1).toString()+".jpg"
numArr.push(
<li key={i+1}><a href="javascript:;" onClick={this.change}>{i+1}</a></li>
);
imgArr.push(
<li key={i+1}><img src={str} /></li>
);
} } return (
<div className="tabs">
<ul className="btns">
{numArr}
</ul>
<ul className="imgs">
{imgArr}
</ul>
</div>
)
},
}); ReactDOM.render(<App getListUrl="json.json"/>,document.getElementById('wrap'));

效果



参考资料

笔记基于阮一峰的网络日志React教程
笔记主要论述基于官方文档根据自身理解改写。
后面大案例来自笔者的尝试,不代表官方做法。

React.js入门笔记的相关教程结束。

《React.js入门笔记.doc》

下载本文的Word格式文档,以方便收藏与打印。