《Vue.js 实战》基础篇(下)

本章内容:表单 与 v-model、组件、自定义指令

六、表单 与 v-model

6.1、基本用法

Vue.js 提供了 v-model 指令,同于在表单类元素上双向绑定数据

<body>
  <div id="app">
    <input type="text" v-model="message" placeholder="输入...">
    <p>输入的内容时:{{ message }}</p>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
       message: ''
      }
    })
  </script>
</body>

对于 文本域 textarea 也是同样的用法。

使用 v-model后,表单控件显示的值只依赖所绑定的数据,不再关心 初始化时的 value属性。在中文输入法下,当敲下汉字才会触发更新,如果希望总是实时更新,可以用 @input 来代替 v-model

单选按钮

单选按钮在单独使用时,直接使用 v-bind 绑定一个布尔类型的值即可。

<body>
  <div id="app">
    <input type="radio" :checked="isChecked">
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        isChecked: true
      }
    })
  </script>
</body>

如果是组合使用来实现互斥选择的效果,就需要 v-model 配合 value 来使用:

<body>
  <div id="app">
    <input type="radio" v-model="picked" value="html" id="html">
    <label for="html">HTML</label>

    <input type="radio" v-model="picked" value="css" id="css">
    <label for="css">css</label>

    <input type="radio" v-model="picked" value="javascript" id="javascript">
    <label for="javascript">JavaScript</label>

    <p> 你的选择是 {{ picked }} </p>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        picked: 'javascript'
      }
    })
  </script>
</body>

复选框

复选框在单独使用时,也是有 v-bind 绑定一个布尔类型的值,或者使用 v-model 来绑定一个布尔值。

<body>
  <div id="app">
    <input type="checkbox" name="" :checked="isChecked" id="">
    <input type="checkbox" name="" v-model="isChecked" id="">
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        isChecked: false
      }
    })
  </script>
</body>

组合使用时,也是 v-model 与 value 一起,多个勾选框都绑定到同一个数组类型的数据。

<body>
  <div id="app">
    <input type="checkbox" name="hobby" v-model="checked" value="sleep" id="sleep">
    <label for="sleep">睡觉</label>
    
    <input type="checkbox" name="hobby" v-model="checked" value="eat" id="eat">
    <label for="eat">吃饭</label>

    <input type="checkbox" name="hobby" v-model="checked" value="fishing" id="fishing">
    <label for="fishing">钓鱼</label>

    <input type="checkbox" name="hobby" v-model="checked" value="coding" id="coding">
    <label for="coding">编程</label>

    <p> 你的选择是 {{ checked }} </p>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        checked: ['eat', 'sleep'],
      }
    })
  </script>
</body>

选项列表
同样分为单选和多选两种方式。
先看一下单选的示例代码:

<body>
  <div id="app">
    <select v-model="selected">
      <option>html</option>
      <option value="js">javascript</option>
      <option>css</option>
    </select>

    <p>你的选择是:{{ selected }}</p>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        selected: '',
      }
    })
  </script>
</body>

给 <select> 添加属性 multiple 就可以多选了,此时 v-model 绑定的是一个数组。

<body>
  <div id="app">
    <select v-model="selected" multiple>
      <option>html</option>
      <option value="js">javascript</option>
      <option>css</option>
    </select>

    <p>你的选择是:{{ selected }}</p>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        selected: ['html', 'js']
      }
    })
  </script>
</body>

<select>控件 在实际业务中,因为它的样式依赖平台和浏览器,无法统一,也不太美观,功能也受限,常见的解解方案是用 div 模拟一个类似的控件。

6.2、绑定值

v-model 绑定的值是一个静态字符串 或 布尔值,但在业务中,有时需要绑定一个动态的数据,这时可以用 v-bind 来实现。

<body>
  <div id="app">
    <input type="radio" v-model="picked" name="radio" :value="value1"> 单选按钮
    <input type="radio" v-model="picked" name="radio" :value="value2"> 单选按钮

    <p>{{ picked }}</p>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        picked: true,
        value1: 123,
        value2: 456
      }
    })
  </script>
</body>

在选中时,picked === 对应的value

复选框

<body>
  <div id="app">
    <input type="checkbox" v-model="toggle" :true-value="value1" :false-value="value2">

    <p>{{ toggle }}</p>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        toggle: false,
        value1: 123,
        value2: 456
      }
    })
  </script>
</body>

勾选时,app.toggle === app.value1; 未勾选时,app.toggle === app.value2。

选择列表

<body>
  <div id="app">
    <select v-model="selected">
      <option :value="{ number: 123}">123</option>
    </select>

    {{ selected.number }}
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        selected: ''
      }
    })
  </script>
</body>

当选中时,app.selected 是一个 Object,所以 app.selected.number === 123

6.3、修饰符

与事件的修饰符类似,v-model 也有修饰符,用于控制数据同步的时机。

  • .lazy: 会转变为在 change 事件中同步,在失焦 或 按回车时才更新。
  • .number:可以将 输入转换为 Number 类型。
  • .trim:可以自动过滤输入的首尾空格
<input type="text" v-model.lazy="message" />
<input type="number" v-model.number="message" />
<input type="text" v-model.trim="message" />

七、组件

组件(Component)是 Vue.js 最核心的功能,也是整个框架设计最精彩的部分,当然也是最难掌握的。

7.1、组件与复原

7.1.1、为什么使用组件

Vue.js 的组件化开发有以下的优势:

  • 提高开发效率

  • 方便重复使用

  • 简化调试步骤

  • 提升整个项目的可维护性

  • 便于多人协同开发

7.1.2、组件用法

组件需要注册后才可以使用。注册有 全局注册局部注册两种方式。

全局注册 后 任何 Vue 实例都可以使用,要在父实例中使用组件,必须要在实例创建前注册

<body>
  <div id="app">
    <my-component />
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>

    Vue.component('my-component', {
      // options
      template: '<div>组件 my-conponent</div>'
    })
    const app = new Vue({
      el: '#app',
    })
  </script>
</body>

my-component 就是注册的组件自定义标签名称,推荐使用小写加减号分割的形式命名。 template 的 内容 必须被一个 DOM 元素包含,如果直接写一段文本,而不加上 HTML 标记,那样是不能渲染的。

局部组件,注册后的组件只有在该实例作用域下有效。组件中也可以使用 components 选项来注册组件,使组件可以嵌套。

<body>
  <div id="app">
    <my-component />
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    const Child = {
      template: '<div> 局部组件 </div>'
    }
    const app = new Vue({
      el: '#app',
      components: { // 注册组件
        'my-component': Child
      }
    })
  </script>
</body>

Vue 组件的模板在某些情况下会受到 HTML 的限制,比如 <table> 内规定只允许是 <tr>、<td>、<th>等这些表格元素。这种情况下,可以使用 特殊的 is 属性来挂载组件

<body>
  <div id="app">
    <table>
      <tbody is="my-component"></tbody>
    </table>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    const Child = {
      template: '<div> 局部组件 </div>'
    }
    const app = new Vue({
      el: '#app',
      components: { // 注册组件
        'my-component': Child
      }
    })
  </script>
</body>

渲染后的结构为

<table>
  <div> 局部组件 </div>
</table>

除了 template 选项外,组件中还可以像 Vue 实例那样使用 其他的选项,比如 datacomputedmethods 等。

在使用 data 时,和实例稍有区别,data必须是函数,然后将数据 return 出去。

<body>
  <div id="app">
    <my-component />
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    const Child = {
      template: '<div> 局部组件 | {{ msg }} </div>',
      data() {
        return {
          msg: '组件内容'
        }
      }
    }
    const app = new Vue({
      el: '#app',
      components: { // 注册组件
        'my-component': Child
      }
    })
  </script>
</body>

这样做的原因是因为 JavaScript 对象是引用关系,所以如果 return 出的对象引用了外部的一个对象,那这个对象就是共享的,任何一方修改都会同步。

7.2、使用 props 传递数据

7.2.1、基本用法

组件不仅仅 是要把模板的内容进行复用,更重要的是组件要进行通信。通常父组件的模板中包含子组件,父组件要正向地向子组件传递数据或参数,子组件接受到后根据参数的不同来渲染不同的内容或执行操作。这个正向传递数据的过程就是通过 props 来实现的。

使用 选项 props 来声明需要从腹肌接收的数据,props 的值可以是两种,一种是字符串数组,一种是对象

数组的用法

大部分时候,传递的数据都是通过 父组件的动态数据 而来的,一般使用 v-bind 来动态绑定 props 的值,当父组件的数据变化时,也会传递给子组件

<body>
  <div id="app">
    <input type="text" v-model="message">
    <my-component :parent-msg='message'/>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    const Child = {
      props: ['parentMsg'],
      template: '<div> 组件 {{ parentMsg }} </div>'
    }
    const app = new Vue({
      el: '#app',
      data: {
        message: ''
      },
      components: { // 注册组件
        'my-component': Child
      }
    })
  </script>
</body>

由于 HTML 特性不区分大小写,当使用 DOM 模板时,驼峰命名(camelCase)的 props 名称要转为短横分割命名(kebab-case)
组件中 props 中声明的数据与 组件 data 中的数据主要区别是 ,props 来自父级。这两种数据都可以在模板中的 computed、methods 等地方中使用

7.2.2、单向数据流

业务中会经常遇到 2 种需要改变 prop 的情况

  1. 父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。
    这种情况下可以在组件 data 内再声明一个数据,引用父组件的 prop。
 const Child = {
      props: ['initCount'],
      template: '<div> 组件 {{ count }} </div>',
      data() {
        return {
          count: this.initCount
        }
      }
    }

他在 组件初始化时会获得来自父组件的 initCount,之后就与之无关了,只要进行维护 count,这样就可以避免直接操作 initCount

  1. 另一种情况就是 prop 作为需要被转变的原始值传入。这种情况下可以使用计算属性
 const Child = {
      props: ['width'],
      template: '<div :style="styles"> 组件 </div>',
      computed: {
        styles() {
          return { width: this.width + 'px' }
        }
      }
    }
7.2.3、数据验证

上面介绍的 props 选项的值都是一个数组,除了数组外,还可以是对象,当 prop 需要验证是 ,就需要对象写法。
以下是几个 prop 的示例:

Vue.component('my-componet', {
  props: {
    // 必须是数字类型
    propA: Number,

    // 必须是 字符串 或 数字类型
    propB: [String, Number]

    // Boolean 类型,如果没有定义,默认值就是 true
    propC: {
      type: Boolean,
      default: true
    }

    // 数字,而且是必传项
    propD: {
      type: Number,
      required: true
    }

    // 自定义一个验证函数
    propE: {
      validator: function(value) {
        return value > 10
      }
    }

    // 如果是数组或对象,默认值必须是一个函数来返回
    propF: {
      type: Array, //Object
      default() {
        return [] // {}
      }
    }

  }
})

验证的type 类型可以是

  • String
  • Number
  • Boolean
  • Object
  • Array
  • Function

当 prop 验证失败时,在开发版本下会在控制台抛出一条警告。
type 也可以是一个自定义的构造器,使用 instanceof 检测

7.3、组件通信

组件关系可分为父子组件、兄弟组件通信、跨级组件通信。

7.3.1、自定义事件

当子组件需要向父组件传递数据时,就要用到自定义事件。

如果你了解过去 JavaScript 的设计模式 —— 观察者模式,一定知道 dispatchEvent 和 addEventListener 这两个方法。Vue 组件也有与之类似的一套模式,子组件用 $emit() 来触发事件,父组件在自定义标签上使用 v-on 来监听子组件触发的自定义事件 或 用 $on() 来监听子组件的事件。

<body>
  <div id="app">
    <p>总数:{{ total }}</p>
     <!-- 侦听 increase 和 reduce 事件 -->
    <my-component @increase="handleGetTotal" @reduce="handleGetTotal"/>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.component('my-component', {
      template: `
        <div>
          <button @click="handleIncrease"> +1 </button>
          <button @click="handleReduce"> -1 </button>
        </div>
      `,
      data() {
        return { // 组件内的数据
          counter: 0
        }
      },
      methods: {
        handleIncrease() { // 增加按钮
          this.counter++
          this.$emit('increase', this.counter) // 触发 increase 事件
        },
        handleReduce() { // 减少 按钮
          this.counter--
          this.$emit('reduce', this.counter) // 触发 reduce 事件
        }
      }
    })
    const app = new Vue({
      el: '#app',
      data: {
        total: 0
      },
      methods: {
        handleGetTotal(total) {
          this.total = total
        }
      }
    })
  </script>
</body>

除了用 v-on 在组件上监听自定义事件外,也可以监听 DOM 事件,这时可以用 .native 修饰符表示监听的是一个原生事件,监听的是该组件的根元素。

<!-- 监听原生 click 事件 -->
<my-component @click.native="handleClick"></my-component>
7.3.2、使用 v-model

在 Vue 2.x 中可以在自定义组件上使用 v-model 指令。

<body>
  <div id="app">
    <p>总数:{{ total }}</p>
    <my-component v-model="total"/> <!-- 侦听 increase 和 reduce 事件 -->
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.component('my-component', {
      template: `<button @click="handleClick"> + 1 </button>`,
      data() {
        return {
          counter: 0
        }
      },
      methods: {
        handleClick() {
          this.counter++
          this.$emit('input', this.counter)
        }
      }
    })
    const app = new Vue({
      el: '#app',
      data: {
        total: 0
      }
    })
  </script>
</bod

上面的代码,并没有在 <my-component> 上使用 @input="handler", 而是直接用来 v-model 绑定了一个数据 total。这也可以称作是一个语法糖,因为上面的示例可以直接地用自定义事件实现。

7.3.3、非父子组件通信

在实际业务中,处理父子组件通信外,还有很多非父子组件通信的场景,非父子组件一般有两种,兄弟组件和跨多级组件。

在 Vue 2x 中,使用 一个空的 Vue 实例作为中央事件总线(bus),类似于一个中介。这种方法巧妙而轻量地实现了任何组件的通信,包括父子、兄弟、跨级。如果深入使用,可以扩展 bus 实例,给它添加 data、methods、computed 等选项。

<body>
  <div id="app">
    {{ message}}
    <component-a />
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>

    const bus = new Vue() // 创建中央事件总线

    Vue.component('component-a', {
      template: `<button @click="handleEvent"> 传递事件 </button>`,
      methods: {
        handleEvent() { // 触发事件
          bus.$emit('on-message', '来自组件 component-a 的内容') 
        }
      }
    })
    const app = new Vue({
      el: '#app',
      data: {
        message: 0
      },
      mounted () {
        bus.$on('on-message', msg => { // 监听事件
          this.message = msg
        })
      }
    })
  </script>
</body>

当项目较大的时候,可以更好的选择 状态管理解决方案 vuex

除了 中央事件总线 bus 外,还有两种方法可以实现组件间通信:父链和子组件索引。

父链

在子组件中,使用 this.$parent 可以直接访问该组件的父实例或组件,父组件也可以通过 this.$children 访问它所有的子组件,而且可以递归向上或向下无限访问,直到根实例或最内层的组件。

<body>
  <div id="app">
    {{ message}}
    <component-a />
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.component('component-a', {
      template: `<button @click="handleEvent"> 通过父链直接修改数据 </button>`,
      methods: {
        handleEvent() { 
          // 访问到父链后,可以做任何操作,比如直接修改数据
          this.$parent.message = '来自组件 component-a 的内容'
        }
      }
    })
    const app = new Vue({
      el: '#app',
      data: {
        message: ''
      }
    })
  </script>
</body>

尽管 Vue 中允许这样操作,但在业务中,子组件应该尽可能地避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父子组件紧密耦合

子组件索引
Vue 提供子组件索引方法,用特殊的属性 ref 来为子组件指定一个索引名称。

<body>
  <div id="app">
    <button @click="handleRef">通过 ref 获取子组件实例</button>
    <component-a ref="comA"/>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.component('component-a', {
      template: `<div> 这里 component-a 子组件 </div>`,
      data() {
        return {
          message: '呼叫 0101, over!'
        }
      }
    })
    const app = new Vue({
      el: '#app',
      methods: {
        handleRef() {
          // 通过 $refs 来访问 指定的实例
          var msg = this.$refs.comA.message
          console.log(msg)
        }
      }
    })
  </script>
</body>

这个属性 同样也可以作用与 普通的 DOM 元素,Vue 会自动去判断是普通标签还是组件。

7.4、使用 slot 分发内容

7.4.1、什么是 slot
  • 插槽(Slot)是Vue提出来的一个概念,正如名字一样,插槽用于决定将所携带的内容,插入到指定的某个位置,从而使模板分块,具有模块化的特质和更大的重用性。
  • 插槽显不显示、怎样显示是由父组件来控制的,而插槽在哪里显示就由子组件来进行控制

props 传递数据、events 触发事件、slot 内容分发就构成了 Vue 组件的 3个 API 来源,在复杂的组件也是由这 3 部分构成的。

7.4.2、作用域

了解 slot 之前,需要先知道一个 概念:编译的作用域
比如父组件中有如下模板

<child-component>
  {{ message }}
</child-component>

这里的 message 就是一个 slot,但是它绑定的是父组件的数据,而不是组件 <child-compoent> 的数据。父组件模板的内容实在父组件作用域内编译,子组件模板的内容是在子组件作用域内编译。

7.4.3、slot 用法

单个 slot
在子组件内使用 特殊的 <slot> 元素就可以为这个子组件开启一个 slot(插槽),在父组件模板里,插入在子组件标签内的所有内容将替代子组件的 <slot> 标签及它的内容。

<body>
  <div id="app">
    <component-a>
      <p>谁说没有内容</p>
      <p>谁说的。。。</p>
    </component-a>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.component('component-a', {
      template: `
      <div>
        这里 component-a 子组件
        <slot>
          如果没有插入内容,我将作为默认内容出现
        </slot>
      </div>`
    })
    const app = new Vue({
      el: '#app'
    })
  </script>
</body>

上例渲染后的结果为:

<div id="app">
  <div>
    这里 component-a 子组件
    <p>谁说没有内容</p>
    <p>谁说的。。。</p>
  </div>
</div>

具名 Slot
给 <slot> 元素指定一个 name 后可以分发多个内容,具名 Slot 可以与单个 Slot 共存。

<body>
  <div id="app">
    <component-a>
      <p slot="header">标题</p>
      <p>正文内容</p>
      <p>更多内容</p>
      <p slot="footer">底部信息</p>
    </component-a>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.component('component-a', {
      template: `
      <div>
        <slot name="header" />
        <slot />
        <slot name="footer" />
      </div>`
    })
    const app = new Vue({
      el: '#app'
    })
  </script>
</body>

没有使用 name 特性,它将作为默认 slot 出现,父组件没有使用 slot 特性的元素与内容都将出现在这里。如果没有指定默认的匿名 slot,父组件内多余的内容片段都将被抛弃。

7.4.4、作用域插槽

作用域插槽 是一种特殊的 slot,使用一个可以复用的模板代替以渲染的元素。

<body>
  <div id="app">
    <child-component>
        <template scope="props">
          <p>来自父组件的内容</p>
          <p>{{ props.msg }}</p>
        </template>  
    </child-component>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.component('child-component', {
      template: `
      <div>
        <slot msg="来自子组件的内容" />
      </div>`
    })
    const app = new Vue({
      el: '#app'
    })
  </script>
</body>

作用域插槽更具代表的用例是列表组件,允许组件自定义应该如何渲染列表每一项。

<body>
  <div id="app">
    <my-list :books="books">
      <!-- 作用域插槽也可以是具名的 Slot -->
      <template slot="book" scope="props">
        <li>{{ props.bookName}}</li>
      </template>
    </my-list>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.component('my-list', {
      props: {
        books: {
          type: Array,
          default() {
            return []
          }
        }
      },
      template: `
        <ul>
          <slot name="book" v-for="book in books" :book-name="book.name" />
        </ul>
      `
    })

    const app = new Vue({
      el: '#app',
      data: {
        books: [
          { name: '《Vue.js 实战》'},
          { name: '《JavaScript 语言精粹》'},
          { name: '《JavaScript 高级程序设计》'}
        ]
      }
    })
  </script>
</body>

scope="props" 这里的 props 是一个临时变量,就是 v-for="item in items" 里面的 item 一样。template 内部可以通过临时变量 props 来访问子组件插槽的数据 msg。作用域插槽的使用场景就是 既可以 复用子组件 的 slot,又可以使 slot 内容不一致。

7.4.5、访问 slot

Vue 2.x 中提供了用来访问被 slot 分发的内容的方法 $slots

mouted() {
  console.log(this.$slots.header) // 访问 slot = "header" 的插槽
  console.log(this.$slots.default) // 访问没有 具名的所有插槽
}

7.5、组件高级用法

7.5.1、递归组件

组件在它的模板内可以递归地调用自己,只要给组件设置了 name 的选项就可以了。

<body>
  <div id="app">
    <child-component :count="1" />
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.component('child-component', {
      name: 'child-component',
      props: {
        count: {
          type: Number,
          default: 1
        }
      },
      template: `
      <div class="child">
        xxx
        <child-component :count="count+1" v-if="count < 3" />
      </div> 
      `
    })

    const app = new Vue({
      el: '#app',
    })
  </script>
</body>

不过需要注意的是,必须给一个条件来限制递归数量,否则会抛出错误:max stack size exceeded

7.5.2、内联模板

Vue 提供了一个内联模板的功能,在使用组件时,给组件标签使用 inline-template 特性,组件就会把它的内容当做模板,而不是把它当做内容分发,这让模板更灵活。

<body>
  <div id="app">
    <child-component inline-template>
      <div>
        <h2>在父组件中定义子组件</h2>
        <p>{{ message }}</p>
        <p>{{ msg }}</p>
      </div>
    </child-component>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.component('child-component', {
      data() {
        return {
          msg: '在子组件声明的数据'
        }
      }
    })

    const app = new Vue({
      el: '#app',
      data: {
        message: '在父组件声明的数据'
      }
    })
  </script>
</body>

父组件和子组件中的数据都可以进行渲染(如果同名,优先使用子组件的数据)。这反而是内联模板的缺点,就是作用域比较难理解,如果不是非常特殊的常见,建议不要轻易使用内联模板。

7.5.3、动态组件

Vue.js 提供了一个 特殊的元素 <component> 用来动态挂载不同的组件,使用 is 特性来选择要挂载的组件

<body>
  <div id="app">
    <component :is="currentView" ></component>
    <button @click="toggleView"> 点击切换组件 </button>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.component('comA', {
      template: '<p> 我是组件 A </p>'
    })
    Vue.component('comB', {
      template: '<p> 我是组件 B </p>'
    })

    const app = new Vue({
      el: '#app',
      data: {
        currentView: 'comA'
      },
      methods: {
        toggleView() {
          this.currentView = this.currentView === 'comA' ? 'comB' : 'comA'
        }
      }
    })
  </script>
</body>
7.5.4 异步组件

当工程足够大的时候,在 一开始就把所有的组件都加载是没有必要的一笔开销。Vue.js 允许将组件定义为一个工程函数,动态地解析组件。Vue.js 只在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的再次渲染。

<body>
  <div id="app">
    <child-compont></child-compont>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.component('child-compont', function(resolve, reject) {
      window.setTimeout(() => {
        resolve({
          template: '<p>异步组件加载</p>'
        })
      }, 2000)
    })

    const app = new Vue({
      el: '#app'
    })
  </script>
</body>

resolve 回调中传入的参数 为 组件的 配置选项。也可以调用 reject(error) 指示加载失败。

7.6、其他

7.6.1、$nextTick

先运行如下代码

<body>
  <div id="app">
    <div id="div" v-if="showDiv">这是一段文本</div>
    <button @click="getText">点击 div内容</button>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.component('child-compont', )

    const app = new Vue({
      el: '#app',
      data: {
        showDiv: false
      },
      methods: {
        getText() {
          this.showDiv = true // 显示元素

          // 输出文本
          console.log(document.getElementById('div').innerHTML)
        }
      }
    })
  </script>
</body>

当点击按钮是,控制台会抛出一个错误:Cannot read property 'innerHTML' of null。这里就涉及 Vue 一个重要的概念:异步更新队列

Vue 在观察到数据变化时并不是直接更新 DOM,而是开启一个队列,并缓冲在同一事件循环中发生的所有数据变化。在缓冲时会去除重复数据,从而避免不必要的计算和 DOM操作。然后,在下一个事件循环 tick 中,Vue 刷新队列并执行实际 (以去重的)工作。所以如果你用一个 for 循环来动态改变数据 100次,其实它只会应用最后一次改变,如果没有这种机制,DOM就要重绘 100次。
Vue会根据当前浏览器环境优先使用原生的 Promise.then 和 MutationObserver,如果都不支持就会采用 setTimeout

在执行 this.showDiv=true 时,div仍然还是没有被创建出来,直到下一个 Vue 事件循环时,才开始创建。

$nextTick 就是用来知道什么时候 DOM 更新完成的

<body>
  <div id="app">
    <div id="div" v-if="showDiv">这是一段文本</div>
    <button @click="getText">点击 div内容</button>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.component('child-compont', )

    const app = new Vue({
      el: '#app',
      data: {
        showDiv: false
      },
      methods: {
        getText() {
          this.showDiv = true // 显示元素

          this.$nextTick(function() {
            // 输出文本
            console.log(document.getElementById('div').innerHTML)
          })
        }
      }
    })
  </script>
</body>
7.6.2、X-Templates

Vue 提供了另一种定义模板的方式,在 <script> 标签里使用 text/x-template 类型,并且指定一个 id,将这个 id 赋给 template

<body>
  <div id="app">
    <my-component></my-component>
    <script type="text/x-template" id="my-component">
      <div>这里是 X-Templates</div>
    </script>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.component('my-component', {
      template: '#my-component'
    })

    const app = new Vue({
      el: '#app'
    })
  </script>
</body>
7.6.3、手动挂载实例

我们现在所创建的实例都是通过 new vue() 的形式创建出来的。在一些非常特殊的情况下,我们需要动态地去创建 Vue 实例,Vue 提供了 Vue.extend$mount 两个方法来手动挂载一个实例。

Vue.extend 是基础 Vue 构造器,创建一个 “子类”,参数是一个包含组件选项的对象。

如果 Vue 实例在实例化阶段没有收到 el 选项,他就处于“未挂载” 状态,没有关联的 DOM 元素。可以使用 $mount() 手动地挂载一个未挂载的实例,这个方法返回实例自身,因此可以链式调用其他实例方法。

<body>
  <div id="app">
    
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>

    const MyComponent = Vue.extend({
      template: '<div> Hello:{{ name }} </div>',
      data() {
        return {
          name: 'sakura'
        }
      }
    })

    new MyComponent().$mount('#app')
  </script>
</body>

除了这种常见的写法,还有以下几种方式进行挂载

new MyComponent().$mount('#mount-div')

new MyComponent({
  el: '#mount-div'
})

// 或者,在文档之外渲染并且随后挂载
var component = new MyComponent().$mount()
document.getElementById('mount-div').appendChild(component.$el)

手动挂载实例是一种比较极端的高级用法,一般只会应用到一些复杂的独立组件开发时。

八、自定义指令

Vue.js 内置的指令能够满足我们的绝大部分也无需求,不过在需要一些特殊功能时,我们仍然希望对 DOM 进行底层的操作,这时候就要用到自定义指令

8.1、基本用法

自定义指令的注册方法和组件很小,也分全局注册 和 局部注册。
全局注册

Vue.directive(('focus', {
  // options
})

局部注册

var app = new Vue({
  el: '#app',
  directives: {
    focus: {
      // options
    }
  }
})

自定义指令的选项是由几个钩子函数组成的,每个都是可选的

  • bind:只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个在绑定时执行一次的初始化动作
  • inserted:被绑定元素插入父节点时调用
  • update:被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新
  • componentUpdated:被绑定的元素所在模板完成一次更新周期时调用。
  • unbind:只调用一次,指令与元素解绑时调用
    根据需求在不同的钩子函数内完成逻辑代码。

例如上面的 v-focus 指令,我们希望在元素插入父节点时就调用,那用到的最好是 inserted。

<body>
  <div id="app">
    <input type="text" v-focus>    
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.directive('focus', {
      inserted(el) {
        console.log(el)
        // 聚焦元素
        el.focus()
      }
    })

    const app = new Vue({
      el: '#app'
    })
  </script>
</body>

每个钩子函数都有几个参数可用,比如上面我们用到了 el
它们的含义如下:

  • el:指令所绑定的元素,可以用来直接操作 DOM
  • binding:一个对象,包含以下属性
    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如 v-my-directive=“1 + 1”,value 的值是2.
    • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用
    • expression:绑定值的字符串形式。例如 v-my-directive=“1+1”,expression 的值是“1+1”
    • arg:传给指令的参数。例如 v-my-directive.foo.bar,修饰符对象 modifiers 的值是 {foo: true, bar: true}
  • vnode:Vue编译生成的虚拟节点
  • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可以

下面是结合以上参数的一个具体实例:

<body>
  <div id="app">
    <!-- 如果需要多个值,可以传入一个 JavaScript 对象字面量,只要是合法类型的表达式都可以 -->
    <div type="text" v-test:msg.a.b="message"></div>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script>
    Vue.directive('test', {
      bind(el, binding, vnode) {
        var keys = []
        var html = ''
        for(let key in binding) { // 遍历 binding
          if (typeof binding[key] === 'object') html += key + ': ' + JSON.stringify(binding[key]) + '<br/>'
          else html += key + ': ' + binding[key].toString() + '<br/>'
        }

        for(let i in vnode) { // 遍历 vnode
          keys.push(i)
        }
        
        el.innerHTML = html + keys.join(',')

      }
    })

    const app = new Vue({
      el: '#app',
      data: {
        message: 'some text'
      }
    })
  </script>
</body>

在大多数使用场景,我们会在 bind 钩子里绑定一些事件,比如在 document 上用 addEventListener 绑定,在 unbind 里用 removeEventListener 解绑。

案例

开发 实时时间转换指令 v-time

需求:
根据当前时间计算 某一条信息的相对时间

  • 1分钟之内,显示“刚刚”
  • 1分钟~1小时之间,显示“XX分钟前”
  • 1分钟~1天之间,显示“XX小时之前”
  • 1分钟~一个月(以31天为标准)之间,显示“XX天前”
  • 大于 1 个月,显示“XX年 XX月 XX日”
<body>
  <div id="app">
    <span v-time="timeNow"></span>
    <span v-time="timeBefore"></span>
  </div>

<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<script>
  const Timer = {
    // 获取当前 时间
    getCurrentUnix() {
      const date = new Date()
      return date.getTime()
    },
    // 获取 今天 00:00:00 的时间戳
    getTodayUnix() {
      const date = new Date()
      date.setHours(0)
      date.setMinutes(0)
      date.setSeconds(0)
      date.setMilliseconds(0)

      return date.getTime()
    },
    // 获取标准时间
    getFormatDate(time) {
      const date = new Date(time)

      const year = date.getFullYear()
      const month = (date.getMonth() + 1).toString().padStart(2, '0')
      const day = date.getDate().toString().padStart(2, '0')

      return `${year} - ${month} - ${day}`
    },
    // 获取 时间格式字符串
    getTimeString(timestamp) {
      const now = this.getCurrentUnix()
      const today = this.getTodayUnix()
      const timer = (now - timestamp) / 1000 // 获取相差秒数
      let tip = ''

      if (timer <= 0 || Math.floor(timer / 60) <= 0) tip = '刚刚'
      // 60 * 60 *  一小时内
      else if (timer < 3600) tip = Math.floor(timer / 60) +  '分钟前' 
      else if (timer >= 3600 && (timestamp - today > 0)) tip = Math.floor(timer / 3600) + '小时前'
      // 60 * 60 * 24 
      else if (timer / 86400 <= 31) tip = Math.ceil(timer / 86400) + '天前'
      else tip = this.getFormatDate(timestamp)

      return tip;
    }
  }

  Vue.directive('time', { // 自定义指令
    bind(el, binding) {
      el.innerHTML = Timer.getTimeString(binding.value) 
      el._timeout = setInterval(() => { // 一分钟更新一次
        el.innerHTML = Timer.getTimeString(binding.value)
      }, 60000)
    },
    unbind(el) { // 卸载
      clearInterval(el._timeout)
      delete el._timeout
    }
  })

  const app = new Vue({
    el: '#app',
    data: {
      timeNow: new Date().getTime(), // 当前时间
      timeBefore: 1566890795721
    }
  })
</script>
</body>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容

  • 组件(Component)是Vue.js最核心的功能,也是整个架构设计最精彩的地方,当然也是最难掌握的。...
    六个周阅读 5,580评论 0 32
  • Vue 实例 属性和方法 每个 Vue 实例都会代理其 data 对象里所有的属性:var data = { a:...
    云之外阅读 2,198评论 0 6
  • vue概述 在官方文档中,有一句话对Vue的定位说的很明确:Vue.js 的核心是一个允许采用简洁的模板语法来声明...
    li4065阅读 7,185评论 0 25
  • 什么是组件? 组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装...
    youins阅读 9,443评论 0 13
  • 一切安好,却情绪低落。人在安逸的日子里,是不是都会这样。我珍惜平凡宁静的生活,孩子们活泼可爱成长着,丈夫也还体贴。...
    鉛筆羊阅读 277评论 2 3