一文入门React与Vue(适用于只会两者其一的框架学习者)

3,720 阅读10分钟

前言

本人原本是一个React开发者,由于最近项目需要维护,技术栈需要会使用Vue,我在之前也从来没有看过Vue的文档,就花了一个周末的时间简单学习了一下Vue的知识点。这里分享一点Vue的快速入门笔记,本文也同样适用于目前只会Vue,但想快速学习React的同学。

实例

Vue实例

每个 Vue 应用都是通过用 Vue 函数创建一个新的 Vue 实例开始,分别添加Vue的周边生态:路由 vue-router 、国际化 vue-i18n 、状态管理 vuex ,和将要渲染的节点#app

new Vue({   
  el: '#app',
  router,  //vue-router
  i18n,    //vue-i18n
  store,   //vuex
  render: h => h(App)
})

React实例

React也是同理,但是它是将组件一层层包裹,通过context上下文和props来传递,添加React的周边生态:路由 react-router-dom 、国际化 react-i18next 、状态管理 react-redux ,同时使用 react-dom提供的ReactDom 来将页面渲染到相应的#app节点上。

ReactDom.render(
    <StoreProvider>
        <I18n>
            <BrowerRouter>
                {route}
            </BrowerRouter>
        </I18n>
    </StoreProvider>,
    document.getElementById('app'),
)

模板

Vue模板

Vue的一个页面构成比较简单明了,更像是原来的html结构

template里编写标签来生成dom节点,script里编写JavaScript来处理逻辑style里编写css来处理样式。

<template>
  <div class='text'> 
      {{msg}}
  </div>
</template>

<script>
export default {
   name:'hello',
   //定义成函数是为了组件复用
   data:function(){
        return{
            msg:'Hello Vue!'
        }
    },
}
</script>

<style>
    .text{
        color:'red';
    }
</style>

React模板

对于比React来看,它是纯JavaScript编写代码来渲染dom处理逻辑,定义state的值来控制页面的渲染renderreturn标签来生成dom节点

//react的jsx语法
class Hello extends React.Component{
    state = {
        msg:'hello React!'
    }
    render(){
        const {msg} = this.state;
        return(
            <div className = 'text'>
                {msg}
            </div>
        )
    }
}

对比一下语法:

  • Vue可以直接在模板里使用 msg属性 而不是 this.data.msg 这样使用,模板里使用 双括号
  • React定义的 msg属性 放在 this.state 上的,但是却需要用 this.state.msg 取值,模板里使用 单括号

具体Vue实例化为什么可以直接能获取到data内的数据,可以参考这个回答 为什么Vue实例化后,通过this.能获取到data内的数据

常用指令

v-if/v-else/v-else-if

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回true值的时候被渲染, 也可以用 v-else-if 继续做判断,最后再用 v-else 做最后处理。

//Vue的模板语法  这里只有v-else的文字会显示出来
<template>
  <div id='app'> 
      <div v-if='flag === 0'>
          由于v-if里的条件判断结果是false,故现在不能看到文字
      </div>
      <div v-else-if = 'flag === 2'>
          v-else-if必须紧跟v-if后面,否则会无效
      </div>
      <div v-else>
           v-else必须紧跟v-if或者v-else-if后面,否则会无效
      </div>
  </div>
</template>

<script>
export default {
   name:'hello',
   data:function(){
        return{
            flag:1
        }
    },
}
</script>

<style>
</style>	

v-show

另一个用于根据条件展示元素的选项是 v-show 指令。

//Vue的模板语法
<template>
  <div id='app'> 
      <div v-show='flag'>
          由于v-show里的条件判断结果是true,故现在可以看到文字
      </div>
  </div>
</template>

<script>
export default {
   name:'hello',
   data:function(){
        return{
            flag:true
        }
    },
}
</script>

<style>
</style>

这里就会发现 v-ifv-show 都可以达到隐藏文字和显示文字的作用,但是两者还是有区别的,使用 v-if 时当其值是false时,dom节点不会被渲染,但是使用 v-show 时无论其值是true还是false,dom节点都会被渲染。

再讲一下 v-ifv-show 的原理:

  • v-if 就像if和else一样动态地创建元素,当v-if为false时会将 dom节点移除。所以当v-if控制的是组件,切换过程中条件块内的事件监听器和子组件会被 销毁和重建 ,会触发组件和子组件的生命周期。
  • v-show 初始化时为false时会添加 style:'display:none' ,为true时会移除 display:none,不管渲染条件是什么,元素总是会被渲染,然后再进行css的操作。

React模拟v-if和v-show

知道了原理再来看看React模拟 v-ifv-show 的实现,React对于 v-if 一般可以用 三目表达式 表示,对于 v-show 可以对 style 直接赋值切换

//React
class Hello extends React.Component{
    state = {
       flag:'1',
       show: false
    }
    render(){
        const { flag,show } = this.state;
        return(
            <div id='app'> 
            //对于v-if的模拟
              {
                   flag === '0' ? 
                   (
                       <div>
                           类似于Vue里的v-if
                       </div>
                   )
                   : flag === '2' ?
                   (
                       <div>
                           类似于Vue里的v-else-if
                       </div>
                   )
                   :
                   (
                       <div>
                           类似于Vue里的v-else
                       </div>
                   )
              }
              //对于v-show的模拟
              <div style={show ? {} : {display:'none'}}>
                  类似于Vue里的v-show
              </div>
            </div>
        )
    }
}

v-bind

一些指令能够接收一个 “参数”,在指令名称之后以冒号表示。例如,v-bind 指令可以用于响应式地更新 dom属性

//Vue的模板语法
<template>
  <div id='app'> 
      <a v-bind:href='url' >
          跳转到百度
      </a>
      //缩写
      <a  :href='url' >
          跳转到百度
      </a>
  </div>
</template>

<script>
export default {
   name:'hello',
   data:function(){
        return{
            url:'www.baidu.com'
        }
    }
}
</script>

<style>
</style>

其实 v-bind 类似于React中的绑定值

class Hello extends React.Component{
    state = {
       url:'www.baidu.com'
    }
    render(){
        const {url} = this.state;
        return(
            <div id='app'> 
              <a href={url}>
                  跳转到百度
              </a>
            </div>
        )
    }
}

v-on

用于指出一个指令应该以特殊方式绑定。例如,v-on:click.prevent 修饰符告诉 v-on 指令对于触发点击的事件时并调用 event.preventDefault(),常用修饰符除.prevent以外常用的还有:

  • .stop - 调用 event.stopPropagation();
  • .once - 只触发一次回调;
//Vue的模板语法
<template>
  <div id='app'> 
      <span>{{ num }}</span>
      <button v-on:click.prevent="add">增加</button>

      //缩写
      <button @click.prevent="add">增加</button>
  </div>
</template>

<script>
export default {
   name:'hello',
   data:function(){
        return{
            num:0
        }
    },
   methods:{
       add(){
           this.num++;
       } 
   }
}
</script>

<style>
</style>

但是在React里,事件驱动还是以on开头进行编写,如onClick、onChange等

class Hello extends React.Component{
    state = {
       num:0
    }
    
    add = e => {
        e.preventDefault();
        this.setState((state)=>({
            num: state.num+1 
        }))
    }
    
    render(){
        const {num} = this.state;
        return(
            <div id='app'> 
                <span>{{ num }}</span>
                <button onClick={this.add}>
                  增加
                </button>
            </div>
        )
    }
}

Vue的计算属性与侦听器

Vue模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护,所以任何复杂逻辑,应当使用计算属性。

<template>
  <div id='app'> 
  
      //bad example
      <div id='bad'>
          {{msg.split('').reverse().join('')}}  
      </div>
      
      //good example
      <div id='good'>
          {{reverseMsg}}
      </div>
  </div>
</template>

<script>
export default {
   name:'hello',
   data:function(){
        return{
            msg:'Hello'
        }
    },
    computed:{
        reverseMsg: function(){
            return this.msg.split('').reverse().join('')
        }
    }
}
</script>

绑定普通属性一样在模板中绑定计算属性,Vue 知道 reversedMsg 依赖于 this.msg ,因此当 this.message 发生改变时,所有依赖 reversedMsg 的绑定也会更新。

Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性 watch ,可以用来监听值的变化来做一些事情,但是如果有一些数据要随着其他数据变动而变动时,应该使用 computed 而不是 watch

<script>
export default {
   name:'hello',
   data:function(){
        return{
            a:{
                b:{
                    c:5
                }
            }
        }
    },
    watch: {
        c:{
            //属性变化时触发的事件
            handler:function(value,oldValue){
                console.log('新数据:'+value,'原数据:'+oldValue)
            },
            //该回调会在任何被侦听的对象的属性改变时被调用,不论其被嵌套多深
            deep:true,
            //该回调将会在侦听开始之后立马被调用
            immediate:true
        }
    } 
}
</script>

在React通过 state 里的值来进行做计算属性,可以将变量单独定义在 render 中。但是通过 state 的值来进行做 侦听 的事情,应当是在 更新阶段 执行,这里放到生命周期处再做介绍。

列表渲染

Vue的列表渲染

我们可以用 v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组,而 item 则是被迭代的数组元素的别名。

//Vue的模板语法
<template>
  <div id='app'> 
      <ul id="example">
          <li 
              v-for="(item,index) in items" 
              :key="item.message"
          >     
              {{ item.message }} - {{index}}
          </li>
      </ul>
  </div>
</template>

<script>
export default {
   name:'hello',
   //定义成函数是为了组件复用
   data:function(){
        return{
            items: [       
                { message: 'Foo' },       
                { message: 'Bar' }     
            ]
        }
    },
}
</script>

//React
class Hello extends React.Component{
    state = {
       items: [       
                { message: 'Foo' },       
                { message: 'Bar' }     
            ]
    }
    
    render(){
        const {items} = this.state;
        return(
            <div id='app'> 
                <ul id="example">
                    {
                        items.map((item,index)=>{
                            return(
                            <li  
                              key={item.message}
                            >     
                              { item.message } - {index}
                             </li>
                            )
                        })
                    }
                </ul>
            </div>
        )
    }
}

数组更新检测

Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:push()pop()shift()unshift()splice()sort()reverse()

//Vue的模板语法
<template>
  <div id='app'> 
      <span>{{ arr }}</span>
      <button @click="arr.pop()">出栈</button>
   </div>
</template>

<script>
export default {
   name:'hello',
   data:function(){
        return{
            arr:[1,2,3,4,5]        
        }
    },
}
</script>

当点击按钮以后,数组每次就会 出栈一个元素,并且视图发生更新,但是当使用Array的filter()concat()slice() 等api时,它们将不会改变原数组,而是返回一个新数组,直接使用时不会更新视图,这里可以用新数组去替换原数组。

//Vue的模板语法
<template>
  <div id='app'> 
      <span>{{ arr }}</span>
      <button @click="filterArray">筛选大于等于3的元素</button>
   </div>
</template>

<script>
export default {
   name:'hello',
   data:function(){
        return{
            arr:[1,2,3,4,5]        
        }
    },
    methods:{
        filterArray(){
            this.arr = this.arr.filter.filter(function(item){
                return item >= 3
            })
        }
    }
}
</script>

React的列表渲染

React的 setState 机制是直接赋值,所以使用pushpop等改变原数组的操作时,需要先取 原始值 再赋值给 state

class Hello extends React.Component{
    state = {
       arr:[1,2,3]
    }
    
    addArr = ()=>{
        //使用浅拷贝复制数组再执行添加
        const data = this.state.arr.concat();
        data.push(4)
        this.setState({
          arr: data
        })

    }
    
    filterArr = ()=>{
        //由于filter返回一个新数组,这里正好返回给arr
        this.setState((state)=>({
            arr: state.arr.filter(item => item > 2)
        }))
    }
    
    render(){
        const {arr} = this.state;
        return(
            <div id='app'> 
                <span>{ arr }</span>
                <button onClick={this.filterArr}>
                      添加数字
                </button>
                <button onClick={this.filterArr}>
                      筛选大于等于3的数
                </button>
            </div>
        )
    }
}

组件传值与插槽

Vue的组件传值与插槽

Vue 实现了一套内容分发的 API,将 <slot> 元素作为承载分发内容的出口。

Vue的子组件传递给父组件时,如果子组件触发的是 方法,则使用 this.$emit(eventName, args) 用来做自定义事件。如果子组件获取父组件的 属性值, 则使用 props 进行定义 类型默认值

//Vue子组件
<template>
    <div>
        <div @click="say">
            {{msg}}
            //插槽,类似于react里的{children}
            <slot></slot>
        </div>
    </div>
</template>

<script>
export default {
    name:'Hello',
    //定义props里的值类型和默认值
    props:{
        msg:{
            type:String,
            default:''
        }
    },
    methods:{
        say(){
            console.log('触发子方法给父组件')
            //给父组件的自定义方法
            this.$emit('say')
        }
    }
}
</script>

//Vue父组件
<template>
    <div>
        <Hello :msg="msg" @say="say">
          <div>插槽文字</div>
        </Hello>
    </div>
</template>

<script>
import Hello from "./Hello.vue";
export default {
    name:'App',
    //定义props里的值类型和默认值
    data:function(){
        return{
            msg:'传递给子组件的值'
        }
    },
    //注册组件
    components: {
        Hello,
    },
    methods:{
        say(){
            console.log('Hello')
        }
    }
}
</script>

React的组件传值与插槽

React 的插槽比较简单,就是一个包裹 this.props.children 就可以了。具体原因主要是React的每个 JSX 元素只是调用 React.createElement(component, props, ...children) 的语法糖,相当于是自带插槽。

React 的子组件向父组件传递时,无论是 触发方法 还是获取父组件传递下来的 属性值 ,都是使用 this.props.[protoitype] 的方式命名。

class Child extends React.Component{
    
    say = ()=>{
        console.log('触发子方法');
        //直接定义say方法到父组件里提供调用
        this.props.say();    
    }
    
    render(){
        const {msg,children} = this.props;
        return(
            <div id='app'> 
                <div onClick={this.say}>
                    {msg}
                    <div>
                        {children}
                    </div>
                </div>
            </div>
        )
    }
}

class Parent extends React.Component{

    state = {
        msg: '传递给子组件的值'
    }
    
    say = ()=>{
        console.log('父组件里触发方法');    
    }
    
    render(){
        const {msg} = this.state;
        return(
            <div> 
                <Child msg={msg} say={this.say}>
                    <div>
                        插槽文字
                    </div>
                </Child>
            </div>
        )
    }
}

生命周期

Vue生命周期

Vue的生命周期比较于React来说相对简单点,Vue大体划分四个阶段:

  • 初始化:beforeCreate、created
  • 渲染:beforeMount、mounted
  • 更新:beforeUpdate、updated
  • 卸载:beforeDestroy、destroy 还有三个生命周期较少使用,在updated后触发:
  • activated ( keep-alive 组件激活时调用 )
  • deactivated ( keep-alive 组件停用时调用 )
  • errorCaptured (当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播。)
<script>
        new Vue({
            el:'#app',
            data:{
                title:'hello word',
            },
            methods:{
                top(){
                    this.title="hi"
                }
            },
            beforeCreate(){
                //不能获得实例化data里的值
                //页面加载出来就会执行
                console.log(this.title)
                console.log('创建之前')
                //页面没加载出来,可以写加载的loading图
            },
            created(){
                console.log(this.title)
                console.log('创建之后')
            },
            beforeMount(){
                //把当前实例化的Vue挂载到绑定的DOM元素上
                //this.$el是获取当前实例化内的所有DOM节点
                //此时DOM中的变量没有被渲染
                //页面加载出来就会执行
                console.log(this.$el)
                console.log('挂载之前')
            },
            mounted(){
                //此时DOM内的变量已经被渲染
                console.log(this.$el)
                console.log('挂载之后') 
            },
            beforeUpdate(){
            //数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
                //你可以在这个钩子中进一步地更改状态,
                //这不会触发附加的重渲染过程。
                //改变的是DOM元素里的数据变更
                //data里的数据变更不会触发
                //页面加载出来不会执行,当数据变更才会执行
    console.log(document.querySelector('#val').innerHTML)
                console.log('更新之前')
                //该钩子在服务器端渲染期间不被调用。
            },
            updated(){
                //此时的DOM已经更新
                //避免在此期间更改状态,因为这可能会导致更新无限循环。
    console.log(document.querySelector('#val').innerHTML)
                console.log('更新之后'); 
            },
            beforeDestroy(){
                //实例销毁之前调用。在这一步,实例仍然完全可用。
                console.log('催毁之前'); 
            },
            destroy(){
        //Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定
         // 所有的事件监听器会被移除,所有的子实例也会被销毁。
                console.log('摧毁之后')
            }
        })
    </script>

关于网络请求放在哪个生命周期里?

可以放置于created、beforeMount、mounted中进行,因为这三个生命周期里 this.data 已经被创建,可以将服务端返回数据进行赋值。

但是目前Vue的项目多用于 ssr服务端渲染ssr 不支持 beforeMount、mounted生命周期函数,故推荐在 created 里调用请求,并且它能更快获取到服务端数据,减少loading时间。

React生命周期

React(16.4+)的生命周期大体划分三个阶段:

  • 挂载:constructor、getDerivedStateFromProps、render、componentDidMount
  • 更新:getDerivedStateFromProps、shouldComponentUpdate、render、getSnapshotBeforeUpdate、componentDidUpdate
  • 卸载:componentWillUnmount

具体生命周期实例参考官网

React的网络请求一般放置于 componentDidMount 生命周期中,在 render渲染 完成以后执行。

回到 侦听计算属性,React可以直接在render中定义 计算属性,因为 state 发生更新以后,整个 render 会重新渲染。侦听 可以获取上一个值和最新的值,可以在 componentDidUpdate 中手动处理。

class Child extends React.Component{

    state = {
        msg:'hello',
        num:0
    }
    
    componentDidUpdate(prevProps,prevState){
    	if(prevState.num !== this.state.num){
        	console.log(prevState.num,'原来的值')
        	console.log(this.state.num, '最新的值')
        }  
    }
            
    render(){
        const {msg} = this.state;
        
        const reverseMsg = msg.split('').reverse().join('')
        return(
            <div> 
                <div>
                    {reverseMsg}
                </div>
            </div>
        )
    }
}

Vue的v-model机制

可以用 v-model 指令在表单 input、textareaselect 元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但 v-model 本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。

v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:

  • texttextarea 元素使用 value propertyinput 事件;
  • checkboxradio 使用 checked propertychange 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件。
//Vue的模板语法
<template>
  <div id='app'> 
      <input v-model="message" placeholder="edit me">
      <p>Message is: {{ message }}</p>   
  </div>
</template>

<script>
export default {
   name:'hello',
   data:function(){
        return{
            message:''        
        }
    },
}
</script>

修饰符: .lazy、.number、.trim

<!-- 在“change”时而非“input”时更新 -->
<input v-model.lazy="msg">

<!-- 自动将用户的输入值转为数值类型 -->
<input v-model.number="age" type="number">

<!-- 自动过滤用户输入的首尾空白字符 -->
<input v-model.trim="msg">

自定义 v-model,前面已经知道了父子组件的传值与触发方法和v-model的原理,现在来自己写一个自定义组件使用v-model的例子

//Vue子组件
<template>
    <input       
        type="checkbox"       
        v-bind:checked="checked"       
        v-on:change="$emit('change', $event.target.checked)"                    
     />   
</template>

<script>
export default {
    name:'base-checkbox',
    //允许一个自定义组件在使用 v-model 时定制 prop 和 event
    model:{
        prop:'checked',
        event:'change'
    },
    //定义props里的值类型和默认值
    props:{
        checked: Boolean
    },
}
</script>

//使用
<base-checkbox v-model="lovingVue"></base-checkbox>

Vue自定义指令

除了核心功能默认内置的指令 (v-model 和 v-show)Vue 也允许 注册自定义指令,看一个官方的例子

Vue.directive 里会携带多个钩子函数:

  • bind :只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

再来看看钩子函数的参数

  • el:指令所绑定的元素,可以用来直接操作 DOM。
  • binding:一个对象,包含以下 property:
    1. name:指令名,不包括 v- 前缀。
    2. value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    3. oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用,无论值是否改变都可用。
    4. expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    5. arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
    6. modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为{ foo: true, bar: true }。
    7. vnode:Vue 编译生成的虚拟节点。
  • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用。
<div v-demo:foo.a.b="message"></div>

Vue.directive('demo', {  
  bind: function (el, binding, vnode) {
    var s = JSON.stringify
    el.innerHTML =
      'name: '       + s(binding.name) + '<br>' +
      'value: '      + s(binding.value) + '<br>' +
      'expression: ' + s(binding.expression) + '<br>' +
      'argument: '   + s(binding.arg) + '<br>' +
      'modifiers: '  + s(binding.modifiers) + '<br>' +
      'vnode keys: ' + Object.keys(vnode).join(', ')
  }
})

显示结果如下:

再写一个input框防抖的例子

Vue.directive('debounce', {  
  inserted: function (el, binding) {
    let timer;
    el.addEventListener('click',()=>{
        if(timer) clearTimeout(timer)
        timer = setTimeout(()=>{
            binding.value()
        },1000)
    })
  }
})

<template>
    <button v-debounce="debounceClick">防抖</button>
</template>
<script>
export default {
  methods: {
    debounceClick () {
      console.log('只触发一次')
    }
  }
}
</script>