类组件

这里十分推荐React.js 小书,里面将React的内容讲的十分的深入浅出,每读一遍都有新的收获。

组件是什么? 如果你搭过积木的话,那么里面的积木就是组件,我们使用积木搭建出一个东西,在React中我们使用组件来搭建一个页面。组件的作用就是复用,这里的复用指的不仅仅是页面的复用,还有逻辑和样式;除此以外,有的组件并不是用来展现页面的,而是用来加载数据的,然后将数据传给子组件,这样会使得处理数据的逻辑和展示数据的逻辑分开,职责分明,逻辑清晰,以及便于后期的维护。另外,有的组件是做权限的认证,以决定展示某些页面与否。总而言之,有许多不同的组件,组件根据功能或写法或有无状态等等可以进行分类,在后面将详细讨论。

按照组件的写法可以分为函数组件和类组件。我们写一个类组件HelloReact

点击查看【codepen】

使用类组件,里面必须有一个render函数,该方法是用来返回JSX代码。我们创建了一个HelloWorld组件

  1. ReactDOM.render(<HelloReact />, document.getElementById("root"))

并且我们通过<HelloReact />的方式使用了组件,此时的<HelloReact />就相当于render函数的返回值

  1. <div>Hello React</div>

<HelloReact />不仅仅是HTML,虽然在这个组件中,我们并没有加上JavaScript的逻辑和CSS样式,但是我们知道一个组件是包括这些的,下面我们为这个类添加样式和点击函数

点击查看【codepen】

两点说明:

  • 由于class在React中是关键字,所以类名要写成className
  • onClick={()=> console.log(“click me”)}为div标签添加了点击事件,onClick={}并不是说onClick是一个对象,{}是React的插值语法,使用插值语法可以将组件的属性与变量(表达式)绑定起来,这样就不会写死,而是会根据变量取不同的值。这里就是将onClick这个React事件与{}里面的箭头函数绑定起来。在{}中的内容只能是JavaScript的表达式,不能是if,while等语句

React事件: 使用原生的JS为DOM元素添加事件,主要有下面三种方法

  1. 在HTML中直接事件绑定
    1. <p onclick="console.log('click')"></p>
  2. 直接绑定
    1. let divObj = document.getElementById("root");
    2. divObj.onclick = function() {}
  3. 对DOM对象进行事件监听处理,事件委托监听方式
    1. let divObj = document.getElementById("root");
    2. divObj.addEventListener('click', function(){})
    至于这三种方法的优缺点与及什么时候使用,这里却不在讨论。那么React中的事件是如何绑定的呢? 在React中事件的绑定是直接写在JSX元素上的,不需要通过addEventListener事件委托的方式进行监听,写法上:
  • 在JSX元素上添加事件,通过on*EventType这种内联方式添加,命名采用小驼峰式(camelCase)的形式,而不是纯小写(原生HTML中对DOM元素绑定事件,事件类型是小写的),无需调用addEventListener进行事件监听,也无需考虑兼容性,React已经封装好了一些的事件类型属性(ps:onClick,onMouseMove,onChange,onFocus)等
  • 使用JSX语法时,需要传入一个函数作为事件处理函数,而不是一个字符串,也就是props值应该是一个函数类型数据,事件函数方法外面得用一个双大括号包裹起来(也就是上面提到的插值语法)
  • on*EventType的事件类型属性,只能用作在普通的原生html标签上(例如:div,input,a,p等,例如:
    ),无法直接用在自定义组件标签上,也就是说下面这么写是没有作用的
    1. <HelloReact onClick={()=> console.log("click me")}>
  • 不能通过返回false的方式阻止默认行为,必须显示使用preventDefault

具体React事件有哪些,可以参照事件系统|React

props

组件最常用的作用就是用来展示数据,那么作为可复用的组件,不同人拿到这个组件所展示的数据也不会相同,那么数据从哪里来? 使用组件的人怎么将数据传给组件,而组件又怎么拿到数据。这就要用到props了。

假如有下面的组件

  1. class DisplayData extends React.Component {
  2. render() {
  3. return (
  4. <div>假设这里是要展示的数据</div>
  5. )
  6. }
  7. }

这个组件的作用就是展示数据,现在我要用DisplayData组件,我怎么把数据传过去,如下

  1. <DisplayData data="数据" />

我们通过给DisplayData组件加上一个data属性,值是”数据”,就可以将数据传给DisplayData,现在怎么拿到数据呢? 每一个组件都有props属性,它是一个对象,通过上面方法传递给组件的属性都会以键值对的形式添加到props对象中,所以在DisplayData组件中,我们就可以通过this.props.data拿到数据

  1. class DisplayData extends React.Component {
  2. render() {
  3. return (
  4. <div>{this.props.data}</div>
  5. )
  6. }
  7. }

点击查看【codepen】

以上我们把DisplayData称之为子组件,而使用DisplayData的组件称之为父组件,上面演示了如何将数据从父组件传给子组件,现在我们考虑一下,如何将子组件的数据传递到父组件呢? 完全是有这个需要的,比如子组件是一个登录组件,我们需要将用户输入的用户名和密码交由父组件进行处理(为什么要交给父组件处理,在子组件中处理不可以吗?如果你在子组件里面处理了数据,那么这个组件就与具体的业务相关了,就不能够被复用了,所以数据处理的工作需要交由父组件来做)。

其实也很简单,我们给子组件传递一个回调函数,那么在子组件的某个时刻(比如子组件的值改变了或者说点击提交)时调用此回调函数并传入数据,这样我们就可以在父组件中拿到数据了

点击查看【codepen】

上面的逻辑代码为当SonComponent中的input发生改变时(输入内容或删减内容等等),会触发子组件中的handleValue回调函数(该回调函数bind了this,后面会进行解释),在子组件的handleValue中,我们根据浏览器传入的event获得了input输入框的值,并且调用ParentComponent传入的handleValue回调函数将此值传入,这样就将子组件的数据传给了父组件,在父组件中,我们定义了这个回调函数接收这个值,并在控制台打印,你可以在上面的input中输入值并在控制台观察结果。

JavaScript函数里面的this是什么: 要想知道JavaScript函数里面的this是什么,就要知道函数调用的4种方式:

  1. 定义在全局作用域中函数

定义在全局作用域中的函数,根据是否是严格模式下,this的取值也不同,如果是在严格模式下,里面的this是undefined,如果是在非严格模式下,里面的this是window

  1. "use strict"
  2. function test() {
  3. console.log(this);
  4. }
  5. test(); //undefined
  1. function test() {
  2. console.log(this);
  3. }
  4. test(); //window
  1. 对象方法

对象中方法中的this如果是对象.的形式调用的,那么对象方法中的this就是该对象

  1. let obj = {
  2. test: function() {
  3. console.log(this);
  4. }
  5. }
  6. obj.test(); // obj

如果以某种角度看的话,第一种情况的非严格模式下是第二种情况的特例,我们知道在全局作用域下声明的变量和函数都会成为window对象的属性,当我们在全局作用域下声明一个test函数,就相当于在window对象中添加了一个test方法(在对象中的函数我们一般称为方法),而调用test()方法就相当于window.test(),按照第二种情况,test中的this就是应该指向window

  1. 作为构造函数被调用

当我们new一个方法的时候,里面的this是一个空对象

  1. function Dog() {
  2. console.log(this)
  3. }
  4. new Dog(); // {}
  1. 使用call, apply, bind方法改变函数的上下文(this)

上面三者都可以改变函数执行时内部this的指向,下面来看一个例子

  1. funtion printName(firstName, lastName) {
  2. console.log(this.fullName)
  3. console.log(`${firstName} ${lastName}`)
  4. }

如果直接执行这个函数的话,那么this.fullName的结果就是undefined,因为window对象没有这个属性,但是如果有以下对象

  1. let obj = {
  2. fullName: "David"
  3. }

下面我们将printName函数执行时内部的this指向obj

  1. let firstName = "firstName"
  2. let lastName = "lastName"
  3. // 将printName内部this指向obj, 后面是printName需要的参数
  4. printName.apply(obj, firstName, lastName)
  5. // 将printName内部this指向obj, 后面是printName需要的参数
  6. printName.call(obj, [firstName, lastName])

这时this.fullName就是obj中的fullName了,因为apply和call方法改变了printName内的this指向。这里我们发现apply和call方法是极其的相似,除了传递参数时格式不一样;事实上也是如此,apply和call的功能是一样的。 说完apply和call,接下来讲一讲bind,bind与上面两者不同,上面改变函数内部的this指向时是立即执行这个函数的,而使用bind改变函数内部的this指向时,这个函数不会立即的执行,如

  1. printName = printName.bind(obj)

printNAme函数内部的this指向已经改变,当printName执行时,打印出的this.fullName就是obj里面的David。

了解完JavaScript中的this是什么,接下来就要解释

  1. <input type="text" placeholder="Please Input" onChange={this.handleValue.bind(this)} />

中onChange={this.handleValue.bind(this)},首先我们来看handleValue中的代码

  1. handleValue(event) {
  2. this.props.handleValue(event.target.value)
  3. }

当input发生改变时便会执行这个函数,但是是谁执行这个函数呢? 是window,而React是运行在严格模式下的,所以这时的this就是undefined,所以我们拿不到我们想要的this,要使得我能拿到的使我们想拿到的this,就是改变函数执行时内部的this指向,考虑到这个函数是作为回调函数而不是立即执行,我们使用bind来绑定this。

机智的你已经发现,ParentComponent中的handleValue没有bind(this),这是因为它在函数里面没有用到this啊,所以什么时候bind(this)是不是已经很清楚了呢!

到这里你有没有发现我们使用组件都是这样

  1. <HelloReact />

居然不是一开一闭的格式,那想必你有疑问,可不可以这样使用

  1. <HelloReact></HelloReact>

答案是可以,那么问题来了? 二者又有什么不同? 使用后面的写法意味着可以在标签里面写子元素,如

  1. <HelloReact>
  2. <div>inner</div>
  3. </HelloReact>

其实这也可以看做是一种传递数据的方式,标签里面的子元素会传给这个组件,传过去的数据会保存在props.children中,在HelloReact中可以通过this.props.children获得数据。

这两种写法都很常见,在写布局,路由,认证等组件时经常使用后面的写法,而在一些展示的组件中,通常只需要父组件传下来的数据,会使用前一种写法,当然这种情况下也可以使用第二种写法。

为了理解children的应用,我们来写简单的Layout组件。所谓的Layout,就是布局
类组件 - 图1
我们简单的把页面上面三个部分,这就是一种布局,一个头部,一个侧边栏,一个内容区。很多时候我们发现一个网站的多个网页之间的布局是一样的,并且很有可能头部和侧边栏是相同,仅仅是内容区不同,作为一名优秀的程序员,当然要尽可能的抽离出这些重复的代码,我们把这个布局抽离为一个组件

点击查看【codepen】

首先希望不要关注样式,因为那不是重点,关注Layout的结构。注意我们将{this.props.children}放在了类名为content的div中,所以如果我们将Content内容区组件放到Layout里面,Content组件就会被放在Layout的内容区,放置不同的Content组件,就会得到多个布局一样,内容不同的页面,这就做到了复用。

整个网站当然不可能只会有一种布局,很多时候我们会写多个布局的组件,明白了布局组件的作用,这些对你来说应当不难。

state

React中一个比较重要的思想就数据驱动视图,例如子组件根据props的内容进行展示,根据不同的props展示不同的内容,不同的props会展现不同的UI,所以可以认为是props决定了视图,每当props变化时,都会引起子组件的渲染,展现不同的视图,视图的改变完全在于数据,所以现在我们操作的重点不再是DOM,而是数据,我们通过操作数据来达到不同的UI效果。

但是仅仅靠props似乎是够的,因为props是只读的,它不能够更改。这意味着什么? 假设父组件传给子组件的props没有发生变化,那么子组件的视图就不会改变,因为数据没有改变。这意味着我们如何与这样一个组件进行交互呢?

这意味组件的内部需要数据来管理UI的变化,我们将组件内部的数据称之为state,state就是状态的意思。比如使用visible这个状态来控制某个对话框是否可见,用户通过改变visible这个状态从而影响组件UI的变化,每次state的变化都会引起组件UI的刷新,从而达到数据驱动视图的目的。从此,我们关心的再也不是DOM操作,而是props, state这些数据,我们操作这些数据来控制视图的更新。

定义一个合适的state,是正确创建组件的第一步。state必须能代表一个组件UI呈现的完整状态集,即组件的任何UI改变,都可以从state的变化中反映出来;同时,state还必须是代表一个组件UI呈现的最小状态集,即state中的所有状态都是用于反映组件UI的变化,没有任何多余的状态,也不需要通过其他状态计算而来的中间状态。

组件中用到的一个变量是不是应该作为组件state,可以通过下面的4条依据进行判断:

  • 这个变量是否是通过props从父组件中获取? 如果是,那么它不是一个状态。
  • 这个变量是否在组件的整个生命周期(后面讲到)中都保持不变? 如果是,那么它不是一个状态。
  • 这个变量是否可以通过其他状态(state)或者属性(props)计算得到?如果是,那么它不是一个状态。
  • 这个变量是否在组件的render方法中使用? 如果不是,那么它不是一个状态(因为state是用来驱动视图的,如果这个变量没有在render方法中使用,意味着该状态的改变并不能使得视图发生变化)。这种情况下,这个变量更适合定义为组件的一个普通属性,例如组件中用到的定时器,就应该直接定义为this.timer,而不是this.state.timer。

现在来写一个计数器,当点击按钮时页面上显示的数字+1,页面上的数字发生改变正是UI的改变,这个时候我们就要用state来管理页面上要展示的数据

点击查看【codepen】

我们在constructor构造函数中初始化state为

  1. this.state = {
  2. number: 0
  3. }

state为一个对象,其中的number属性正是我们要展现的数据,我们将它初始化为0。当我们点击按钮时,触发状态的改变

  1. increment() {
  2. this.setState({
  3. number: ++this.state.number
  4. })
  5. }

注意,状态的改变不能使用this.state.number = ++this.state.number使状态发生改变,要使得状态发生改变,必须使用setState()方法使状态发生改变。

上面的例子进一步验证了数据驱动视图的思想,在计数器的例子中,我们没有手动的更改视图(操作DOM),而是通过改变state来使得视图得到刷新,因为每次state的改变都会引起render()方法的调用,从而使得视图得以刷新。

请务必牢记,并不是组件中用到的所有变量都是组件的状态! 当存在多个组件共同依赖一个状态时,一般的做法是状态上移,将这个状态放到这几个组件的公共父组件中。

所谓状态上移是怎么回事呢? 考虑下面这么一个评论组件
类组件 - 图2
CommentInput用来输入评论,当提交之后会在CommentList中新增一个CommentItem来显示新增的评论,很明显我们需要一个comments数组,当CommentInput提交评论时,将comment添加到comments数组中,CommentList拿到comments数组,comments中的元素会交给其中的CommentItem显示。而作为关键的comments数组,它是多个组件都会用到的,所以它应该作为state保存在公共的父组件中,即Comment组件中。这就是状态上移,将多个组件都会用到的数据上移到共同父组件的state中,当更新父组件的state的时,会使得传到子组件的props相应的更新,从而达到视图 更新的目的。
类组件 - 图3