vue 快速入门 系列 —— vue 的基础应用(下)
阅读原文时间:2021年04月20日阅读:1

其他章节请看:

vue 快速入门 系列

上篇聚焦于基础知识的介绍;本篇聚焦于基础知识的应用

递归组件

组件是可以在它们自己的模板中调用自身的。不过它们只能通过 name 选项来做这件事。我们实现一个自定义树的组件。请看示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src='vue.js'></script>
</head>
<body>
  <div id='app'>
    <custom-tree :list='treeData'></custom-tree>
  </div>

<script>
  // 递归组件 - 自定义树
  Vue.component('custom-tree', {
   // 给组件命名
   name: 'custom-tree',               // {1}
   props: ['list'],
   template: `
    <ul>
      <li v-for='item in list'>
        {{item.name}}
        <!-- v-if 指定退出的条件,防止无线递归 -->
        <custom-tree
          :list='item.children'
          v-if='item.children'
        ></custom-tree>
      </li>
    </ul>
    `
  })
  const app = new Vue({
     el: '#app',
     // 数据也需要符合递归的条件
     data: {
       treeData: [                   // {2}
         {
           name: 'a',
           children: [
             {
               name: 'b'
             },
             {
               name: 'c'
             }
           ]
         },
         {
           name: 'd',
           children: [
             {
               name: 'e',
               children: [
                 {
                   name: 'f'
                 },
                 {
                   name: 'g'
                 }
               ]
             }
           ]
         }
       ]
     }
   })
</script>
</body>
</html>


// 页面输出:
a
  b
  c
d
  e
    f
    g

有3点需要注意:

  • 给组件设置 name (行{1})
  • 使用一个条件来结束无限递归。这里使用了 v-if
  • 数据得满足递归(行{2})

Tip: 后续不在提供完整的代码,省略 head、body 等。

动态组件

vue 提供了 <component> 来动态的挂载组件。请看示例:

<div id='app'>
  <!-- vue 提供了 <component> 来动态的挂载组件 -->
  <component v-bind:is="currentComponent"></component>
  <button @click='switchHandle'>切换组件</button>
</div>

<script>
  var comC = {
    template: `<p>我是组件 C</p>`
  };

  var app = new Vue({
    el: '#app',
    data: {
      currentComponent: 'comB'
    },
    // 局部注册。components 选项中定义你想要使用的组件
    components: {
      comA: {
        template: `<p>我是组件 A</p>`
      },
      comB: {
        template: `<p>我是组件 B</p>`
      },
      comC: comC
    },
    methods: {
      switchHandle: function(){
        let map = {
          'comA': 'comB',
          'comB': 'comC',
          'comC': 'comA'
        };
        // 动态切换组件
        this.currentComponent = map[this.currentComponent]
      }
    }
  })
</script>


// 页面输出:
我是组件 A

切换组件

// 点击按钮(‘切换组件’),依次显示'我是组件 B'、'我是组件 C'...

内置的组件 component 根据属性 is 的值来决定哪个组件被渲染。

nextTick

Vue.nextTick( [callback, context] )

用法

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。—— 不明白的化,请看示例:

  <div id="example">{{message}}</div>

  <script>
  var vm = new Vue({
    el: '#example',
    data: {
      message: '123'
    }
  })
  vm.message = 'new message' // 更改数据               //        {20}
  console.log(vm.$el.textContent === 'new message')   // false  {21}
  Vue.nextTick(function () {
    console.log(vm.$el.textContent === 'new message') // true   {22}
  })
  </script>

更改数据后(行{20},dom 元素中的内容其实没有得到更新,输出 false(行{21});在 Vue.nextTick() 方法中才被更新,输出 true(行{22})。

这里涉及到 vue 中一个概念:异步更新

假如在更改状态(行{20})后,dom 元素立马得到更新(行{21}),也就是输入出 true,那么用户使用 for 循环改变某个状态 100 次,dom 元素就得更新 100 次,是否觉得浪费!所以 vue 的策略是:使用异步更新,也就是不会马上更新 dom。

手动挂载

vm.$mount( [elementOrSelector] )

用法

如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态。我们可以使用 vm.$mount() 方法手动。

我们创建一个组件,三秒后再挂载它。请看示例:

<div id="app"></div>

<script>
  // Vue.extend() 使用基础 Vue 构造器,创建一个“子类”
  var MyComponent = Vue.extend({
    template: '<div>Hello!</div>'
  })

  // 创建并挂载到 #app (会替换 #app)
  setTimeout(function(){
    // 3 秒后页面上才会看见 Hello!
    new MyComponent().$mount('#app')
  }, 3000)
</script>

3 秒后,页面上才会看见 Hello!

数字输入框组件

需求:数字输入框只能输入数字,有两个按钮,分别是减1和加1。此外还可以设置初始值、最大值、最小值,数字改变时通知父组件。

请看完整代码:

<div id='app'>
    父组件 value = {{value}}
    <!--  v-model 实现双向绑定 -->
    <custom-number
      v-model='value'
      :max-value='maxValue'
      :min-value='minValue'
      :step='10'
    ></custom-number>
</div>

<script>
  Vue.component('custom-number', {
    props:{
      // 必须是一个数字
      value: {
        type: Number,
        default: 0
      },
      maxValue: {
        type: Number,
        // 正无穷大
        default: Number.POSITIVE_INFINITY
      },
      minValue: {
        type: Number,
        // 负无穷大
        default: Number.NEGATIVE_INFINITY
      },
      // 默认加减 1
      step: {
        type: Number,
        default: 1
      }
    },
    data: function(){
      return {
        inputValue: this.value
      }
    },
    created: function(){
      // 处理:初始值不在最大值和最小值的范围内
      this.update(this.value)
    },
    computed: {
      // 减(-)不可点
      minIsDisabled: function(){
        return this.inputValue <= this.minValue
      },
      maxIsDisabled: function(){
        return this.inputValue >= this.maxValue
      }
    },
    watch: {
      // 监听 inputValue,通知父组件
      inputValue: function(val, oldVal){
        this.$emit('input', val)
      },
      // 父组件改变值,子组件的值也跟着改变
      value: function(val){
        this.update(val);
      }
    },
    template: `
      <div>
        <button :disabled="minIsDisabled" @click='minus'> - </button>
        <input :value='inputValue' @change='changeHandle' >
        <button :disabled="maxIsDisabled" @click='add'> + </button>
        <p>
          子组件 inputValue = {{inputValue}}
        </p>
      </div>
      `,
    methods: {
      // 如果输入值不是数字,则不会更改值
      changeHandle: function(e){
        var val = e.target.value;
        this.update(val, e.target)
      },
      // obj 是否是数字。摘自 jquery
      isNumeric: function(obj) {
        return !isNaN( parseFloat(obj) ) && isFinite( obj );
      },
      minus: function(){
        this.update(this.inputValue - this.step);
      },
      add: function(){
        this.update(this.inputValue + this.step);
      },
      // 是数字才更新
      update: function(val, target={}){
        if(!this.isNumeric(val)){
          // 将 input 值置为上次的值
          target.value = this.inputValue;
          return;
        }
        val = Number(val);
        // 大于最大值,则重置为最大值
        if (val > this.maxValue){
          val = this.maxValue
        }else if(val < this.minValue){
          val = this.minValue
        }
        this.inputValue = val;
      }
    }
  });

  var app = new Vue({
    el: '#app',
    data: {
      value: 10,
      maxValue: 100,
      minValue: 1
    }
  })
</script>


// 页面输出:
父组件 value = 10
- 10 +
子组件 inputValue = 10

// 点击 +(每次加减10)
父组件 value = 20
- 20 +
子组件 inputValue = 20

// 继续点击2次
// 减号(-)变为不可点
父组件 value = 1
- 1 +
子组件 inputValue = 1

Tabs 标签页

需求:实现一个常用的组件 —— tabs 标签页。

:不明白需求的可以看element-ui-tabs

思路:

  • 定义组件 el-tabs
  • 定义 el-tabs 的子组件 tab-pane
  • 父子组件通信使用 vm.$parent 和 vm.$children

请看完整代码:

<style>
  ul{margin:0;padding: 0;border-bottom: 1px solid;margin-bottom: 10px;}
  li{display:inline-block;margin-right:10px;cursor:pointer;}
  .active{color:#409eff;}
</style>

<div id='app'>
  <el-tabs v-model="activeKey">
    <el-tab-pane label="用户管理">
      用户管理内容
      <p>我是 A</p>
    </el-tab-pane>
    <el-tab-pane label="配置管理" name="second">配置管理内容</el-tab-pane>
    <el-tab-pane label="角色管理">
      角色管理内容
      <p>我是 C</p>
    </el-tab-pane>
    <el-tab-pane label="定时任务补偿" name="fourth">定时任务补偿内容</el-tab-pane>
  </el-tabs>
</div>

<script>
  // 父组件
  Vue.component('el-tabs', {
    props:{
      value:{
        type: [String, Number]
      }
    },
    data: function(){
      return {
        currentTab: this.value,
        // 存放 tab
        tabLists: []
      }
    },
    watch: {
      currentTab: function(){
        this.updateStatus();
      },
      // 处理:父组件更改 value
      value: function(val, oldVal){
        this.currentTab = val
      }
    },
    template: `
      <div>
        <ul>
          <li
            v-for='(item, index) in tabLists'
            :class='{active: item.name === currentTab}'
            @click='handleClick(index)'
          >{{item.label}}</li>
        </ul>
        <slot></slot>
      </div>
    `,
    methods: {
      // 取得 tab-pane
      getTabPanes: function(){
        return this.$children.filter(item => {
          return item.$options.name === 'tab-pane'
        })
      },
      // 更新 tabLists
      updateTabLists: function(){
        let tabLists = [];
        this.getTabPanes().forEach((item, index) => {
          if(!item.id){
            item.id = index
          }
          tabLists.push({
            label: item.label,
            name: item.id
          })

          // 默认展示第一个
          if(index === 0){
            if(!this.currentTab){
              this.currentTab = item.id;
            }
          }
        })
        this.tabLists = tabLists;
        this.updateStatus()
      },
      handleClick: function(index){
        this.currentTab = this.tabLists[index].name;
        console.log(`name = ${this.currentTab}`)
        this.updateStatus()
      },
      // 让子组件显示或隐藏
      updateStatus: function(){
        this.getTabPanes().forEach(item => {
          item.updateShow(this.currentTab === item.id)
        })
      }
    }
  });

  // 子组件
  Vue.component('el-tab-pane', {
    name: 'tab-pane',
    props: {
      // 标签标题
      label:{
        type: String,
        default: ''
      },
      // pane 的名字,不必填
      name: [String, Number]
    },
    data: function(){
      return {
        // 显示与否,由父组件决定
        show: false,
        // 不允许通过父组件更改 props 中的属性 name
        // 用 id 来替代 name 属性
        id: this.name
      }
    },
    created: function(){
      this.$parent.updateTabLists();
    },
    template: `
      <div v-if='show'>
        <slot></slot>
      </div>
    `,
    methods: {
      updateShow: function(v){
        this.show = v;
      }
    }
  });

  const app = new Vue({
    el: '#app',
    data: {
      // 当前选中的 tab
      activeKey: 2
    }
  })
</script>


// 页面输出:
// 第一行是 4 个 tab,现在是`角色管理`被选中
用户管理 配置管理 角色管理 定时任务补偿

角色管理内容

我是 C

其他章节请看:

vue 快速入门 系列