带你再看Vue文档,你就顺便学会了React

20,544 阅读11分钟

介绍

最近接手了一个React开源项目,上手前看了一些react的基础教程就撸了,之前有着半年Angular转2年Vue的前端经验,但是还是对React心生畏惧。

项目做了大概一个周了,发现果然世界上代码都是一样的,React几乎和Vue的用法一样,唯一的学习成本在于语法(如果你能理解前端框架和组件化的概念)。

但是三大框架里面,唯一让人觉得赞许的、适合上手的官方文档,我个人认为,个人!是Vue,所以打算采用Vue的文档结构将Vue中的一些语法和功能对应的React的实现方式,向你们一一道来,带你们再读Vue文档,顺便学会React

  1. 整篇文章结构采用Vue的文档目录,做了一些删减,有的我还没用到,有的可能对应的描述我思路不清晰就摘掉了
  2. 每个部分一般会先把Vue文档中的功能实现或者语法搬过来,然后手打对应的React中实现或者语法。
  3. 整篇文章一万字出头,建议仔细看每个例子,你可能需要20分钟??

开读之前,心理默默大声念出那句:

在这里插入图片描述

Vue.js 是什么

这一部分就不分别介绍两个框架了,因为你看这篇文章代表着一定要熟悉Vue的基本语法和理念,否则建议 × 掉该文章。另外建议你不要是React的精通大神,因为接下来的内容如Vue的官方文档一样基础,建议 × 掉。

起步

vue的起步当然是vue-cli创建一个项目,这个地方就不上vue-cli代码了,react也是一样的。

首先你需要安装一个create-react-app,它就是你要用的“vue-cli”了:

npm install -g create-react-app

然后我们用它构建一个react项目:

create-react-app react-demo

执行命令过程不同于vue-cli会给你选择一些配置项,creat react会一直加载到最后结束,然后你就可以看到一个崭新的react项目基础结构:

在这里插入图片描述

声明式渲染

Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统,语法使用v-bind和双大括号:

<div id="app">
  <span v-bind:title="message">
    {{ name }}
  </span>
</div>
var app = new Vue({
  el: '#app',
  data: {
    name: 'Hello Vue!'
    message: '页面加载于 ' + new Date().toLocaleString()
  }
})

vue的模板绑定语法就不多说了,react中采用了几乎同样的双向绑定方式:

import React, { Component } from 'react'

class DemoComponent extends Component {
    constructor() {
        super()
        // 将this.state看做你vue中的data
        this.state = {
            name: 'hello',
            title: '页面加载于 ' + new Date().toLocaleString()
        }
    }
    render() {
        return (
            // 单大括号的双向绑定语法
            <span title={this.state.title}>{this.state.name}</span> 
        )
    }
}
export default DemoComponent

效果图就不上了,太鸡肋,简单加个tip:

  1. react的双向绑定使用单括号
  2. react的双向绑定属性也直接使用单括号,没有冒号没有引号
  3. react的双向绑定的变量要使用this.state.变量名,而不是直接this引用变量名

条件与循环

vue中条件判断使用v-if和v-show,循环使用v-for

<div id="app">
  <p v-if="seen">现在你看到我了</p>
  <ol>
    <li v-for="todo in todos">
      {{ todo.text }}
    </li>
  </ol>
</div>
var app = new Vue({
  el: '#app',
  data: {
    seen: true,
    todos: [
      { text: '学习 JavaScript' },
      { text: '学习 Vue' },
      { text: '整个牛项目' }
    ]
  }
})

而在react中可能就会略显麻烦:

import React, { Component } from 'react'

class DemoComponent extends Component {
    constructor() {
        super()
        this.state = {
            seen: true,
            todos: [
                { text: '学习 JavaScript' },
                { text: '学习 Vue' },
                { text: '整个牛项目' }
            ]
        }
    }
    render() {
        return (
            <div>
                /* 使用三元表达式 */
                {this.state.seen
                    ? <p>现在你看到我了</p>
                    : null
                }
                
				/* 使用三元表达式的另一种 */
                <p>{this.state.seen ? '现在你看到我了' : null}</p>
                
				/* 使用map()进行dom遍历 */
                <ol>
                    {this.state.todos.map((item) => {
                        return (
                            <li>{item.text}</li>
                        )
                    })}
                </ol>
            </div>
        )
    }
}
export default DemoComponent
  1. react中条件判断基本使用js条件表达式的特性实现(求科普有没有v-show的对应)
  2. react中循环一般使用map方法去遍历return需要循环的dom结构
  3. 当然这只是基本用法

处理用户输入

vue用 v-on 指令添加一个事件监听器,通过它调用在 Vue 实例中定义的方法: vue还提供了 v-model 指令,它能轻松实现表单输入和应用状态之间的双向绑定:

<div id="app">
  <input v-model="message">
  <button v-on:click="reverseMessage">反转消息</button>
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue.js!'
  },
  methods: {
    reverseMessage: function () {
      this.message = this.message.split('').reverse().join('')
    }
  }
})

然后我们看一下相应的如何在react中绑定事件和表单元素:

import React, { Component } from 'react'

class DemoComponent extends Component {
    constructor() {
        super()
        this.state = {
            message: 'Hello Vue.js!'
        }
    }
    reverseMessage(event) {
        this.setState({
            message: this.state.message.split('').reverse().join('')
        })
    }
    render() {
        return (
            <div>
            	/* 单大括号绑定 */
                <input value={this.state.message} />
                <button onClick={this.reverseMessage.bind(this)}>反转消息</button>
            </div>
        )
    }
}
export default DemoComponent

知识点来了:

  1. 因为react使用的render函数,react所有特性几乎都是使用js的语法来实现,而不是指令型语法
  2. 像click等事件,还有标签属性如title,palceholder都是直接使用大括号
  3. react中双向绑定的变量不会随js对变量的一系列操作修改而响应,任何双向绑定的变量要达到在页面渲染更新,需要使用react的this.setState({})语法,key为state中的变量名,value为更新后的变量值(如果写过原生小程序应该很容易理解)
  4. 方法可以直接在Class中定义,不同于vue的存在methods对象中,且一般在绑定方法时通过bind将this传进去,否则你的方法里this.将会报错,经典this作用域问题,自行科普。

Vue 实例

创建一个 Vue 实例

每个 Vue 应用都是通过用 Vue 函数创建一个新的 Vue 实例开始的:

vue实例:

var vm = new Vue({
  // 选项
})

react实例:

import React, { Component } from 'react'

class DemoComponent extends Component {
  // 内容
  render () {
  }
}
export default DemoComponent

知识点:

  1. react的一个组件以一个从react中继承Component的Class类为实例
  2. 一个react组件必须通过render ()返回JSX,且只能返回一个JSX元素(2个必要条件)
  3. vue和react的共同点是最外层必须有一个单一根元素标签

越来越简单是不是,next=>

数据与方法

  • vue的数据使用了一个data函数返回一个对象来保存当前vue实例中的变量,vue实例中的自写方法都放置在methods
  • react的数据都放置在state中,方法可以直接在class中定义,如果你对ES6的class足够了解,你很容易就可以理解我在说什么。(觉得别扭的可以重看一遍ES6)
  • 另外还有一种可以使用内部变量的方法:
render() {
		// render()函数中直接定义变量,毕竟它是个函数,这是合理的
        let tip = "Hello!"
        return (
            <div>
                {{tip}}
            </div>
        )
    }

因为render是一个函数,所以你可以直接在return一些dom结构之前定义函数内的变量。 这刚好再次强调之前我说的“ 因为react是使用的render函数,所以react所有特性几乎都是使用js的语法来实现”

实例生命周期钩子

换个表达方式,这样我可以少打点字:

生命周期钩子 vue react
实例创建前 beforeCreate - - -
实例创建后 created constructor()
DOM挂载前 beforeMount componentWillMount
DOM挂载后 mounted componentDidMount
数据更新时 beforeUpdate componentWillUpdate
数据更新后 updated componentDidUpdate
keep-alive激活时 activated
keep-alive 组件停用时 deactivated
实例销毁前 beforeDestroy componentWillUnmount
实例销毁后 destroyed componentWillUnmount
子组件发生错误时 errorCaptured
  • 当然还是有很多具体的细微的差距,另外还有一些生命周期componentWillReceiveProps 、 componentWillReceiveProps等,但是表中的生命周期对应你熟悉的vue,应该足以让你直接明白react有哪些生命周期,都有什么作用。

模板语法

插值

文本

vue数据绑定最常见的形式就是使用“Mustache”语法 (双大括号) 的文本插值:

<span>Message: {{ msg }}</span>

react直接使用大括号,这个已经提过:

<span>Message: { this.state.msg }</span>

(另外同样不知有没有对应的v-once实现,欢迎评论里科普)

原始 HTML

vue使用v-html直接输出真正的 HTML,假设我们有一个叫rawHtml的html字符串模板:

<p> 
  <span v-html="rawHtml"></span>
</p>

react就简单了,因为它是render函数,所以可以:

render () {
  const rawHtml= `<span> is span </span>`
  return (
    <div>
      <p>{rawHtml}</p>
    </div>
  )
}

特性

vue使用v-bind:作用在 HTML 特性上:

<button v-bind:disabled="isButtonDisabled">Button</button>

react直接使用{}绑定:

<button disabled={this.state.isButtonDisabled}>Button</button>

使用 JavaScript 表达式

vue官方示例的基本表达式使用方式有:

{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div v-bind:id="'list-' + id"></div>

使用简单的运算符,三元表达式,链式调用都可以,而react的使用则完全是你日常js的使用方式,这3种都可以. 另外提一点vue中可以直接使用{{}}绑定某一个methods或者计算属性中的某一个方法名,比如:

<div>{{changeName()}}</div>

react中也可以直接写成一个立即执行函数表达式返回:

<div>{(function () { return 'is div'})()}</div>

计算属性

没看到有关react的计算属性用法,应该是react的getter不会使用缓存,割了

侦听器

vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化,具体不叭叭了,因为:react 的状态都是手动 setstate 变化的,react 不监听数据变化,这不是我割的……

Class 与 Style 绑定

绑定 HTML Class

vue可以传给 v-bind:class 一个对象,以动态地切换 class:

<div
  class="static"
  v-bind:class="{ active: isActive, 'text-danger': hasError }"

></div>

active和 'text-danger' 这2个 class 存在与否将取决于数据属性 isActive和hasError的判断

vue还可以把一个数组传给 v-bind:class,以应用一个 class 列表:

<div v-bind:class="[activeClass, errorClass]"></div>
data: {
  activeClass: 'active',
  errorClass: 'text-danger'
}

而在react中,

  • 首先!不能使用class,因为class是关键字,所以react使用className代替,
  • 而且react中只能使用react的特性,最后将class转为字符串,比如:
<div className={["activeClass",hasError? item.color :'' ].join('')} ></div>
  • 或者,es6模板字符串:
<div className={`activeClass ${hasError?item.color:'' }`} ></div>

绑定内联样式

vue的v-bind:style 的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS 属性名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名:

<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data: {
  activeColor: 'red',
  fontSize: 30
}

当然你也可以将所有样式定义成一个对象变量,直接绑定该对象。

这个时候,看好知识点

在这里插入图片描述
react中绑定style使用单括号{},但需要将style属性以一个对象形式传入:

<div style={{display: this.state.isShow ? "block" : "none"}}></div>

多个样式时,注意对象形式逗号 , 进行分隔!

<div style={{display: this.state.isShow ? "block" : "none", color:"pink"}}></div>

建议你看到这,稍微停顿一下分辨区别。

条件渲染

v-if

vue就是v-if,v-else,v-show就不多说了,这个就比较简单了:

<div v-if="Math.random() > 0.5">
  Now you see me
</div>
<div v-else>
  Now you don't
</div>

react的条件判断上面开始的时候介绍过了,通常使用三元表达式来判断显示,这里补充一种将JSX 元素赋值给了不同变量,再使用三元表达式判断的方式:

render () {
  const display = true
  const showDiv = <p>显示</p>
  const noneDiv = <p>隐藏</p>
  return (
    <div>{display ? showDiv : noneDiv }</div>
  )
}

也没有vue类似v-show这种比较节省dom切换性能消耗的语法,欢迎评论补充。

列表渲染

vue中我们可以用 v-for 指令基于一个数组来渲染一个列表。

<ul id="example-1">
  <li v-for="item in items">
    {{ item.message }}
  </li>
</ul>

至于具体里面的那些为什么要加key啊,怎么遍历对象啊,还有怎么v-for对数组的检测问题就不深究了。

react中的循环在开始也提到过了,使用map()方法遍历,不说了。

表单输入绑定

vue中表单元素的绑定都是使用v-model绑定,拿个input举例:

<input v-model="message" placeholder="edit me">

react中之前也提了使用元素本身的属性value来直接绑定

<input value={this.state.message} placeholder="edit me">

组件基础

基本示例

提过了,基本一个实例就是一个组件且vue中后面也出现了纯函数组件,两个框架组件概念都一样。

组件的复用

vue和react的组件复用方式都是一样的,先引用,然后使用组件名标签。

vue中引用一般习惯使用-间隔标签,例如:

<div id="components-demo">
  <button-counter></button-counter>
</div>
import DuttonCounter from './modules/DuttonCounter'

export default {
  components: {
      DuttonCounter
  },
}

react中引用标签直接使用export的class类名,且组件都必须要用大写字母开头

import DuttonCounter from './modules/DuttonCounter'

function App() {
  return (
    <div className="App">
      <DuttonCounter />
    </div>
  );
}

通过 Prop 向子组件传递数据

来了啊,知识点,要考的!

在这里插入图片描述
首先简单回顾vue如何向子组件传值:

父组件:

<super-man work="超人"></super-man>

子组件接收:

<div>{{work}}</div>
export default {
  props: {
      work: {
        required: true,
        type: String
      }
   },
}

然后,通过一个例子看一下react的props:

class Index extends Component {
  render () {
    return (
      <div>
        <SuperMan work='超人' />
      </div>
    )
  }
}

这是组件传值的方式,和vue大同小异,看一下接收组件传值:

class SuperMan extends Component {
  constructor () {
    super()
    this.state = {}
  }

  render () {
    return (
      <div>{this.props.work}</div>
    )
  }
}

在react中,接收父组件的传值同样使用关键字props,与vue不同的区别在于:

  1. vue的所有传值需要放置在子组件的props的对象中,才可以被当前子组件使用,使用方式如同访问data的变量,直接this.变量使用。 react是使用this.props.变量这种方式就可以直接使用所有父组件传递的变量。
  2. react中向子组件传递对象参数,可以使用{{}}双括号直接传递对象
class Index extends Component {
  render () {
    return (
      <div>
        <SuperMan work={{name: '超人', task: '拯救世界'}} />
      </div>
    )
  }
}

3.甚至于传递函数:

class Index extends Component {
  render () {
    return (
      <div>
        <SuperMan onClick={this.handleClick.bind(this)}/>
      </div>
    )
  }
}

然后,补充一些额外的知识点,

在这里插入图片描述
之前讲过vue中props的用法,可以设置type,required,default,validator等等,不知道的可以看上次的文章《vue文档里你没捡起来的宝藏》结尾处。

那么react如何实现对props传值的参数校验呢: 首先假设父组件没有传值过来,我们可能需要一个默认值,这时候你可能会经常在父组件传值的时候使用三元表达式来判断传值,react只需要:

class SuperMan extends Component {
  // 知识点
  static defaultProps = {
    work: '默认是超人'
  }
  constructor () {
    super()
    this.state = {}
  }

  render () {
    return (
      <div>{this.props.work}</div>
    )
  }
}

react中定义defaultProps就对 props 中各个属性的默认配置。这样我们就不需要判断配置属性是否传进来了:如果没有传进来,可以直接this.props使用 defaultProps 中的默认属性。

其次,关于react中props的校验,先声明一句“react的类型检查PropTypes自React v15.5起已弃用”,那我该怎么继续讲下去?? 这个时候就需要单独安装prop-types,现在通常在react中我们使用prop-types对组件的prop进行类型检测。

npm install --save prop-types

使用方式也很简单,有例子你就能理解:

// 知识点
import PropTypes from 'prop-types'

class SuperMan extends Component {
  // 知识点
  static propTypes = {
    work: PropTypes.string
  }
  
  constructor () {
    super()
    this.state = {}
  }

  render () {
    return (
      <div>{this.props.work}</div>
    )
  }
}

简单明了,通过引入prop-types,并且添加一个类属性 propTypes,在propTypes中为你props传入的变量指定数据类型,这样就相当于vue的type:String

然后,我们看一下required: true在react中如何实现:

import PropTypes from 'prop-types'

class SuperMan extends Component {
  static propTypes = {
    work: PropTypes.string.isRequired  // 知识点
  }
  
  constructor () {
    super()
    this.state = {}
  }

  render () {
    return (
      <div>{this.props.work}</div>
    )
  }
}

仔细看和上面一个例子的区别在于,当我们指定数据类型PropTypes.string之后,同时后面通过 isRequired 关键字来强制组件某个参数必须传入。

最后,如何react实现组件传参校验呢? 这个我还没用到,但是呢专门为你们查了一下,和我猜的差不多,直接使用函数:

import PropTypes from 'prop-types'

class SuperMan extends Component {
  static propTypes = {
    work: function(props,propName,componentName){
    }  // 知识点
  }
  
  constructor () {
    super()
    this.state = {}
  }

  render () {
    return (
      <div>{this.props.work}</div>
    )
  }
}

使用自定义函数,默认传参分别为props,propName,componentName,然后校验错误就return new Error('错误信息');就可以了。

再放上一些看到的其他的例子,虽然我也还没用到,但是是很好的小技巧:

//指定枚举类型:你可以把属性限制在某些特定值之内
optionalEnum: PropTypes.oneOf(['News', 'Photos']),

// 指定多个类型:你也可以把属性类型限制在某些指定的类型范围内
optionalUnion: PropTypes.oneOfType([
  PropTypes.string,
  PropTypes.number,
  PropTypes.instanceOf(Message)
]),

// 指定某个类型的数组
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),

// 指定类型为对象,且对象属性值是特定的类型
optionalObjectOf: PropTypes.objectOf(PropTypes.number),

监听子组件事件

react和vue有着同样的组件特征——单向数据流。 在react中想要改变父组件中的值就要用方法去触发父组件的方法,由父组件自己的方法操作属于父组件的值。

那么如何实现触发父组件或者说父组件监听子组件变化而变化,在vue中是通过$emit 方法来触发父组件方法,例子不举了,直接来看react如何实现:

// 父组件
class Index extends Component {
  this.state = {type:'小超人'}
  
  changeType(data) {
    this.setState({
      type: data //把父组件中type的替换为子组件传递的值
    })  
  }
  
  render () {
    return (
      <div>
        <SuperMan work='超人' super={this.fn.changeType(this)}/> // 知识点
      </div>
    )
  }
}
// 子组件
class SuperMan extends Component {
  constructor () {
    super()
    this.state = {childText:'大超人'}
  }
  
  clickFun(text) {
    this.props.super(text)//这个地方把值传递给了props的事件当中
  }
  
  render () {
    return (
      <div onClick={this.clickFun.bind(this, this.state.childText)}>
        {this.props.work}
      </div>
    )
  }
}

没啥问题,看的懂ok,只是没有使用$emit,而是直接通过this.props调用父组件的方法即可。

通过插槽分发内容

插槽部分简单说下使用,首先我们如何在vue中使用一个插槽:

// 父组件
<my-component>
  <p>超人</p>
</my-component>

// 子组件
<div class="child-page">
  <h1>无限地球危机</h1>
  <slot></slot>
</div>

// 渲染结果
<div class="child-page">
  <h1>无限地球危机</h1>
  <p>超人</p>
</div>

插槽那一堆具体就不解释了,我们只看如何在react也实现像vue这种插槽方式:

// 父组件
ReactDOM.render(
  <MyComponent>
	<p>超人</p>
  </MyComponent>,
  document.getElementById('root')
)

class MyComponent extends Component {
  render () {
    return (
      <div class="child-page">
        <h1>无限地球危机</h1>
        {this.props.children}
      </div>
    )
  }
}

渲染结果和vue的渲染就是一致了,可以看到react中将父组件中的JSX内容传到子组件内部,通过 {this.props.children} 把JSX内容渲染到页面指定结构上,还是很好理解的。

结束了

截止到我写完,已经断断续续一个周过去了,看了看右下角11386字数,项目上的代码研究的也差不多,因为是开源项目所以大量都是高阶组件,一些纯函数组件组成,刚开始还是很迷糊,但是现在已经差不多改掉一些功能了。

至此按照vue文档中的结构(部分做了删减),把对应的react的语法和功能做了一遍梳理,如果你能仔细把每个例子对比看一遍,我觉得你写不了react,也可以看懂一个react项目了。

有补充的评论加个知识点,没补充的捧个赞。

谢谢。