Ant Design Pro 教程 -- 3.一切都是组件
3.1 Counter 组件
React 是基于组件的声明式用户界面开发库。在 React 世界,一切都是组件。传统前端开发,主要是基于页面的开发方式,需要我们用 HTML 把页面整个构造出来,通过 CSS 来指定样式,再通过 JavaScript 来从后台拉取数据,将数据设置到 DOM 元素属性上去,绑定 DOM 元素事件,响应处理用户操作,触发的事件。
我们也可以使用组件,比如基于 EasyUI 的前端开发,但是通常我们只是使用组件,而不会自己去开发可复用的组件,而且即使是使用 EasyUI 组件,也要求我们用 HTML 将组件需要的 HTML 描述出来,再由 EasyUI 组件将 DOM 元素渲染成 EasyUI 组件。
我们也可以借助模板语言,来开发前端页面,本质上还是一样的,需要我们用 HTML 来描述整个页面,只是页面中嵌入模板语法而已,这样的模板语言有很多,比如 Jsp、Asp、Freemarker 等等。模板语言通常也允许我们将可以复用在多个页面的页面片段实现在单独的文件中,在需要使用的地方引入进来,但是本质上还是基于页面的开发方式。
而 React 则是一种创新式的前端开发方式。将整个应用看做由组件装配起来的,开发就是用一种分而治之的方法,使用和开发组件的过程。这些组件可以在应用中复用,甚至是跨应用复用。下面我们以一个简单的例子来一起学习 React 开发。
我们要实现的例子是实现一个简单的计数器,这个计数器有两个按钮,一个按钮控制增加计数,一个按钮控制减少计数,和一个文本显示当前计数。最终显示效果如图:
counter.png
用如下命令创建一个 counter 工程
create-react-app counter
修改 src/index.js,引入我们即将实现的 Counter 组件,设置组件的初始 count 为 0,并将组件渲染到 root。将 src/index.js 用下面代码替换:
import React from 'react';
import ReactDOM from 'react-dom';
//引入我们将要开发的 Counter 组件
import Counter from './componenets/Counter'
ReactDOM.render(<Counter initCount={0} />, document.getElementById('root'));
然后在 src 目录下新建一个文件 components 文件夹,用于存放我们自己开发的组件,在 components 文件夹下新建一个 Counter.js。组件的 JS 源代码文件名通常首字母大写。Counter.js 代码如下:
import React, { Component } from 'react';
class Counter extends Component{
constructor(props){
super(props);
this.state = {
count: props.initCount || 0
};
this.onIncrement = this.onIncrement.bind(this);
this.onDecrement = this.onDecrement.bind(this);
}
onIncrement(){
this.setState({
count: this.state.count+1
});
}
onDecrement(){
this.setState({
count: this.state.count-1
});
}
render(){
return (
<div >
<span>Clicked: {this.state.count} times</span>
<button onClick={this.onIncrement}> + </button>
<button onClick={this.onDecrement}> - </button>
</div>
)
}
}
export default Counter;
在工程文件夹目录下,用 npm start 命令启动工程,在浏览器下打开工程页面,将会看到我们实现的计数器,点击“+”按钮和“-”按钮体验下计数的变化。下面我们来分析下代码。
Counter.js 文件实现了 Counter 组件 。Counter 组件继承了 React 提供的 Component 类。Counter 组件有四个方法,第一个是类构造函数,React 约定构造函数有个 props 参数,用来设置组件初始化的属性,构造函数首先需要调用父类的构造函数。然后初始化了类属性 state,一个只包含 count 属性的对象,并且将 count 初始化为外部传入的初始值 props.initValue。state 是 React 的一个很重要的概念,用于保存组件的状态。然后将 将两个按钮的点击事件响应函数绑定到当前组件对象。如果你对于这两个按钮的点击事件的响应函数为什么要绑定不太理解,请参考章节 3.2
onIncrement 和 onDecrement 是两个按钮的事件响应方法。这两个响应方法很简单,都是通过继承自 Component的 setState 方法更新组件状态。需要注意的是,一定要通过 setState 来修改组件的状态,不能直接修改 this.state.count,因为 setState 除了修改组件的状态,还会触发组件的刷新。
第四个方法是组件渲染方法 render,你一定对这个方法的代码感动奇怪,这是 JSX(JavaScript eXtension)语法。JSX 允许我们可以像这个 render 方法一样,直接在 JavaScript 代码中编写像是 HTML 一样的代码,用来声明组件显示外观。除了标准的 HTML 元素外,还可以用自定义组件,JSX 用组件名来区分是标准的 HTML 元素,还是自定义组件。如果组件名用小写字母表示则是标准的 HTML 元素;组件名首字母大写,则表示自定义组件。
当 JSX 语句有多行的时候,需要用括号()将多行语句包起来。如果是单行的话,是不需要括号的。Counter 组件很简单包含一个 span 来显示当前计数,一个“+”按钮和一个“-”按钮。JSX 允许外观元素中直接访问 JavaScript 的对象,需要用到花括号{},比如示例中的计数绑定到当前组件对象状态的 count 属性值。通过两个按钮的 onClick,设置了按钮点击事件的响应方法。注意是onClick,而不是 HTML 的 onclick。标准的 HTML 元素的事件名称都需要和 onClick 一样,修改为驼峰式结构。
定义好组件后,就可以在需要用到组件的地方引入组件使用了,前面我们已经修改该了 src/index.js 使用了这个 Counter 组件了。到这里,这个 Counter 组件示例就完成了。到工程目录,执行如下命令:
npm start
打开浏览器,输入地址 http://localhost:3000 尝试点击 “+”,“-”,看看效果吧。
3.2 事件响应函数 bind 上下文
对于 Counter 组件构造函数中,为什么事件响应函数需要调用一下 bind呢?这个对于 JavaScript 当前执行上下文不太理解的话,会觉得比较费解,这里解释一下。这个和 JavaScript 如何确定函数调用时 this 的值有关。JavaScript 确定 this 值的 5 种模式:
3.2.1 函数调用模式
如果不是以对象 "." 的方式调用,而是直接调用函数的话,那么 this 指向的是当前上下文的全局对象,比如在浏览器环境中的 window 对象。第 5 行 foo() 调用,this 指向的是当前调用上下文,所以 this.scope 指向全局变量 scope。第 17 行 func() 调用,虽然 func 指向的是对象 obj 的 foo 方法,但是在直接函数调用的时候, this 指向的依然是当前调用上下文,所以 this.scope 指向还是全局变量 scope,而不是对象 obj 的scope 属性。
var scope = "outer context"
function foo() {
console.log("this points to " + this.scope);
}
// console 日志输出:this points to outer context
foo();
var obj = {
scope: "obj",
foo: function() {
console.log("this points to " + this.scope);
}
};
var func = obj.foo;
// console 日志输出:this points to outer context
func()
3.2.2 方法调用模式
上面代码中第 19 行 obj.foo() 是对象方法调用模式。对象方法调用模式中,方法中的 this 指向当前调用对象实例,所以上例第 19 行 console 日志输出:this points to obj
3.2.3 构造函数调用模式
当用 new 关键字配合函数调用的时候,这个时候是构造函数模式,在构造函数中 this 指向 new 新创建的空对象。如果没有 new 关键字,直接调用构造函数,则构造函数就是普通函数,上下文(this)还是由调用的时候上下文决定。下例中第 9 行调用输出如第 7,8 行注释所示,this 指向新创建的空对象。下例中第 13 行调用输出如第 11,12 行注释所示,这种调用方式是函数调用方式,this 指向的是当前调用的上下文,函数调用后,上下文中的 scope 值变成了 ”obj“。
1 var scope = "outer context"
2 function Cat() {
3 console.log("this points to " + this.scope);
4 this.scope = "obj";
5 console.log("this points to " + this.scope);
6 }
7 // console 日志输出:this points to undefined
8 // console 日志输出:this points to obj
9 var cat = new Cat();
10
11 // console 日志输出:this points to outer context
12 // console 日志输出:this points to obj
13 Cat()
14
15 // console 日志输出:this points to obj
16 console.log("this points to " + this.scope);
3.2.4 apply 调用模式
JavaScript 中,我们还可以用 call 和 apply 方式来调用函数。call 和 apply 调用函数的时候,可以传递一个上下文对象作为函数调用的第一个参数。call 和 apply 的区别是,call 调用的时候,从第二个参数开始,是原函数的参数;而调用 apply 的时候,原函数的参数以数组的形式,作为 apply 的第二个参数。
var scope = "outer context"
function foo(a,b) {
console.log("this points to " + this.scope + ", a="+ a +", b="+ b);
}
var obj = {
scope:"obj"
}
// console 日志输出:this points to obj, a=a, b=b
foo.call(obj, "a", "b");
// console 日志输出:this points to obj, a=a, b=b
foo.apply(obj, ["a", "b"]);
3.2.5 bind 调用模式
JavaScript 中的 bind 方法是函数对象的属性,用来创建一个新函数,并将新函数的上下文(this)绑定到指定的对象上。下例中我们用 bind 函数创建一个新函数并赋值给 func,这个新函数的上下文 bind 到了对象 obj。
var scope = "outer context"
function foo() {
console.log("this points to " + this.scope);
}
var obj = {
scope:"obj"
}
var func = foo.bind(obj);
// console 日志输出:this points to obj
func();
3.2.6 箭头函数调用模式
箭头函数有个非常有用的特性,箭头函数的上下文是其定义的的地方决定的,而不是调用的时候决定的。下面用两个例子来说明一下。
var scope = "outer context"
var obj = {
scope:"obj",
foo:function(){
return function() {
console.log("this points to " + this.scope);
}
}
}
var func = obj.foo();
// console 日志输出:this points to outer context
func();
上例第 11 行是函数调用模式,所以 func 调用的时候,this 指向的是调用上下文,所以 this.scope 为 "outer context"。
1 var scope = "outer context"
2 var obj = {
3 scope:"obj",
4 foo:function(){
5 return (ev) => console.log("this points to " + this.scope);
6 }
7 }
8 var func = obj.foo();
9 // console 日志输出:this points to obj
10 func();
本例中,将 foo 方法返回的函数换成了箭头函数。第 10 行是函数调用模式,但是由于 func 现在指向的是一个箭头函数,而根据刚刚提到的箭头函数的特性”箭头函数的上下文是其定义的的地方决定的,而不是调用的时候决定的。“,所以 func 调用的时候,this 指向的箭头函数定义时的上下文,也就是 obj 对象,因此,函数中的 this.scope 为 "obj"。
3.2.7 React 事件响应函数需要 bind
经过以上内容的描述,我们就能够理解为什么 React 事件响应函数需要 bind 了。在 3.1 部分的代码中,第 29,30 行将Counter 组件的事件响应函数赋值 button 组件的 onClick。当 button 组件点击事件触发的时候,button 组件将会调用 onClick(),这是函数调用模式,如果没有预先绑定的话,调用上下文的 this 将不会是 Counter 组件实例。