• 自学前端开发 - VUE 框架 (二):渲染、事件处理、表单输入绑定、生命周期、侦听器、组件基础


    @[TOC](自学前端开发 - VUE 框架 (二) 事件处理、表单输入绑定、生命周期、侦听器、组件基础)

    模板引用

    模板引用就是直接访问模板对象实例。最经常使用的例子就是使用元素对象组件对象的方法。如果使用的是原生 DOM 对象,还可以使用 JS 的方式来获取元素对象(例如使用 getElementById 方法),对于 VUE 组件对象,就需要使用模板引用了。

    设置模板引用

    使用模板引用时,需要在元素或组件上设置一个特殊属性 ref,它能够在元素或组件挂载后,获取其直接引用。

    <input ref="input">
    <el-table ref="table">
    
    • 1
    • 2

    访问模板引用

    设置了特殊属性 ref 后,可以通过应用对象的 $refs 属性进行访问

    <script>
    export default {
      mounted() {
        this.$refs.input.focus()
      }
    }
    script>
    
    <template>
      <input ref="input" />
    template>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    注意,只可以在组件挂载后才能访问模板引用。

    使用数组

    当在 v-for 中使用模板引用时,相应的引用中包含的值是一个数组

    <script>
    export default {
      data() {
        return {
          list: [
            /* ... */
          ]
        }
      },
      mounted() {
        console.log(this.$refs.items)
      }
    }
    script>
    
    <template>
      <ul>
        <li v-for="item in list" ref="items">
          {{ item }}
        li>
      ul>
    template>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    需要注意的是,ref 数组并不保证与源数组相同的顺序。

    函数模板引用

    除了使用字符串值作名字,ref 属性还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:

    <input :ref="el => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
    
    • 1

    这样就能够在使用数组时保证顺序正确了

        <li v-for="(item, index) in list" :ref="el => items[index] = el">
          {{ item }}
        li>
    
    • 1
    • 2
    • 3

    需要注意的是使用动态的 :ref 绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null。也可以绑定一个组件方法而不是内联函数

    另外,绑定 ref 属性的值可以没有经过定义就进行使用。实际上会将此值作为 this.$refs 这个对象的 key 来保存引用。而使用 :ref 则必须使用函数模板引用,而函数内部使用的变量必须是定义并暴露的,否则将无法正常访问引用。

    使用对象

    使用函数也可以将模板引用赋值给对象的成员,这样可以将若干组件来分类

    <script>
    export default {
    	data() {
    		return {
    			inputRefs: ref({})
    		}
    	},
    	mounted() {
    		console.log(this.$refs.inputRefs)
    	}
    }
    script>
    
    <template>
        <input name='address' :ref="el => inputRefs['address'] = el" />
        <input name='age' :ref="el => inputRefs['age'] = el" />
    template>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    直接定义为对象或数组成员

    直接将对象成员设置为 ref 的属性是不行的,例如

    <input name='age' ref="input['age']" />
    
    • 1

    vue 会将 input['age'] 看作一个字符串整体而不是一个对象和其属性,调用时就成了 this.$refs."input['age']"。对于数组也是同理。

    事件处理

    可以使用 v-on 指令 (简写为 @) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="methodName"@click="handler"。事件处理器的值可以是:

    • 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。
    • 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。

    内联事件处理器

    内联事件处理器通常用于简单场景,例如:

    data() {
      return {
        count: 0
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    <button @click="count++">Add 1button>
    <p>Count is: {{ count }}p>
    
    • 1
    • 2

    方法事件处理器

    随着事件处理器的逻辑变得愈发复杂,内联代码方式变得不够灵活。因此 v-on 也可以接受一个方法名或对某个方法的调用。

    data() {
      return {
        name: 'Vue.js'
      }
    },
    methods: {
      greet(event) {
        // 方法中的 `this` 指向当前活跃的组件实例
        alert(`Hello ${this.name}!`)
        // `event` 是 DOM 原生事件
        if (event) {
          alert(event.target.tagName)
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    
    <button @click="greet">Greetbutton>
    
    • 1
    • 2

    方法事件处理器会自动接收原生 DOM 事件并触发执行。在上面的例子中,我们能够通过被触发事件的 event.target.tagName 访问到该 DOM 元素。

    传递参数给方法事件处理器

    除了直接绑定方法名,还可以在内联事件处理器中调用方法。这允许我们向方法传入自定义参数以代替原生事件:

    methods: {
      say(message) {
        alert(message)
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    <button @click="say('hello')">Say hellobutton>
    <button @click="say('bye')">Say byebutton>
    
    • 1
    • 2

    在内联事件处理器中访问事件参数

    有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event 变量,或者使用内联箭头函数:

    
    <button @click="warn('Form cannot be submitted yet.', $event)">
      Submit
    button>
    
    
    <button @click="(event) => warn('Form cannot be submitted yet.', event)">
      Submit
    button>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    methods: {
      warn(message, event) {
        // 这里可以访问 DOM 原生事件
        if (event) {
          event.preventDefault()
        }
        alert(message)
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    事件修饰符

    在处理事件时调用 event.preventDefault()event.stopPropagation() 是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。为解决这一问题,Vue 为 v-on 提供了事件修饰符。修饰符是用 . 表示的指令后缀,包含以下这些:

    • .stop ——调用 event.stopPropagation()
    • .prevent ——调用 event.preventDefault()
    • .self ——只有事件从元素本身发出才触发处理函数。
    • .capture ——在捕获模式添加事件监听器。
    • .once ——最多触发一次处理函数。
    • .passive ——通过 { passive: true } 附加一个 DOM 事件。
    
    <a @click.stop="doThis">a>
    
    
    <form @submit.prevent="onSubmit">form>
    
    
    <a @click.stop.prevent="doThat">a>
    
    
    <form @submit.prevent>form>
    
    
    
    <div @click.self="doThat">...div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    按键修饰符

    在监听键盘事件时,经常需要检查特定的按键。Vue 允许在 v-on@ 监听按键事件时添加按键修饰符。

    
    <input @keyup.enter="submit" />
    
    • 1
    • 2

    按键别名

    Vue 为一些常用的按键提供了别名:

    • .enter
    • .tab
    • .delete (捕获“Delete”和“Backspace”两个按键)
    • .esc
    • .space
    • .up
    • .down
    • .left
    • .right

    系统按键修饰符

    系统按键修饰符用来触发鼠标或键盘事件监听器,只有当按键被按下时才会触发。

    • .ctrl
    • .alt
    • .shift
    • .meta

    举例来说

    
    <input @keyup.alt.enter="clear" />
    
    
    <div @click.ctrl="doSomething">Do somethingdiv>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    .exact 修饰符

    .exact 修饰符允许控制触发一个事件所需的确定组合的系统按键修饰符。

    
    <button @click.ctrl="onClick">Abutton>
    
    
    <button @click.ctrl.exact="onCtrlClick">Abutton>
    
    
    <button @click.exact="onClick">Abutton>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    鼠标按键修饰符

    这些修饰符将处理程序限定为由特定鼠标按键触发的事件:

    • .left
    • .right
    • .middle

    表单输入绑定

    在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:

    <input
      :value="text"
      @input="event => text = event.target.value">
    
    • 1
    • 2
    • 3

    v-model 指令帮我们简化了这一步骤:

    <input v-model="text">
    
    • 1

    v-model 指令可以用于大多数表单元素,它会根据所使用的元素自动使用对应的 DOM 属性和事件组合。v-model 会忽略任何表单元素上初始的 value、checked 或 selected 属性。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。你应该在 JavaScript 中使用 data 选项来声明该初始值。

    值绑定

    对于单选按钮,复选框和选择器选项,v-model 绑定的值通常是静态的字符串 (或者对复选框是布尔值):

    
    <input type="radio" v-model="picked" value="a" />
    
    
    <input type="checkbox" v-model="toggle" />
    
    
    <select v-model="selected">
      <option value="abc">ABCoption>
    select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    但有时我们可能希望将该值绑定到当前组件实例上的动态数据。这可以通过使用 v-bind 来实现。此外,使用 v-bind 还使我们可以将选项值绑定为非字符串的数据类型。

    复选框

    <input
      type="checkbox"
      v-model="toggle"
      true-value="yes"
      false-value="no" />
    
    • 1
    • 2
    • 3
    • 4
    • 5

    true-valuefalse-value 是 Vue 特有的属性,仅支持和 v-model 配套使用。这里 toggle 属性的值会在选中时被设为 'yes',取消选择时设为 'no'。你同样可以通过 v-bind 将其绑定为其他动态值:

    <input
      type="checkbox"
      v-model="toggle"
      :true-value="dynamicTrueValue"
      :false-value="dynamicFalseValue" />
    
    • 1
    • 2
    • 3
    • 4
    • 5

    true-valuefalse-value 属性不会影响 value 属性,因为浏览器在表单提交时,并不会包含未选择的复选框。为了保证这两个值 (例如:“yes”和“no”) 的其中之一被表单提交,请使用单选按钮作为替代。

    单选按钮

    <input type="radio" v-model="pick" :value="first" />
    <input type="radio" v-model="pick" :value="second" />
    
    • 1
    • 2

    pick 会在第一个按钮选中时被设为 first,在第二个按钮选中时被设为 second。

    选择器选项

    <select v-model="selected">
      
      <option :value="{ number: 123 }">123option>
    select>
    
    • 1
    • 2
    • 3
    • 4

    v-model 同样也支持非字符串类型的值绑定!在上面这个例子中,当某个选项被选中,selected 会被设为该对象字面量值 { number: 123 }

    修饰符

    v-model 可以使用的修饰符有:

    • .lazy
      默认情况下,v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。可以添加 .lazy 修饰符来改为在每次 change 事件后更新数据:
    
    <input v-model.lazy="msg" />
    
    • 1
    • 2
    • .number
      如果你想让用户输入自动转换为数字,你可以在 v-model 后添加 .number 修饰符来管理输入。如果该值无法被 parseFloat() 处理,那么将返回原始值。.number 修饰符会在输入框有 type="number" 时自动启用。
    <input v-model.number="age" />
    
    • 1
    • .trim
      如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model 后添加 .trim 修饰符:
    <input v-model.trim="msg" />
    
    • 1

    组件上的 v-model

    生命周期

    每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。

    vue 应用组件的生命周期

    vue 应用的生命周期为:

    1. 设置(创建代码,还未进入生命周期)
    2. 初始化事件(生命周期的开始)
    3. 创建对象并注入响应式状态(Init Options API)
    4. 模板预处理(pre-compile template)
    5. 初始化并渲染模板,替换数据(initial render)
    6. 组件挂载完成(Mounted),根据需要重渲染(re-render)
    7. 组件卸载(Unmounted)
      在这里插入图片描述
      图示中的红色框即生命周期中的钩子。

    注册周期钩子

    举例来说,mounted 钩子可以用来在组件完成初始渲染并创建 DOM 节点后运行代码:

    export default {
      mounted() {
        console.log(`the component is now mounted.`)
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    还有其他一些钩子,会在实例生命周期的不同阶段被调用,最常用的是 created、mounted、updated 和 unmounted。

    所有生命周期钩子函数的 this 上下文都会自动指向当前调用它的组件实例。注意:避免用箭头函数来定义生命周期钩子,因为如果这样的话将无法在函数中通过 this 获取组件实例。

    created

    created 钩子在初始化完成状态后调用,此时已经能够获取初始化的 data 等状态信息了。通常会在此阶段进行 ajax 请求(methods 中的方法),并加载需要的数据到 data 中。

    mounted

    mounted 钩子在组件挂载完成后被调用,此时组件已经渲染完成(在此之前组件未渲染,没有创建 $el),能够获取渲染后的元素和节点信息。所以一般在此阶段编写初始化页面的操作代码。

    updated

    updated 钩子在响应式状态被改动后调用。此时状态已经被改变,并且也将结果重渲染至页面中。一般在此执行数据改动后同步保存至数据库的 ajax 代码。

    如果需要在渲染前完成某些操作,例如判断数据是否有效,可以在 beforeUpdate 钩子中进行。

    unmounted

    unmounted 钩子在组件被卸载时被调用,主要用来回收资源。

    侦听器

    计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。侦听器会监听指定的数据的变化情况,并触发一个回调函数。

    基本侦听

    可以使用 watch 选项在每次响应式属性发生变化时触发一个函数。此函数函数名为监听的数据变量名,参数接收该变量改动前和改动后的值。

    export default {
      data() {
        return {
          question: '',
          answer: 'Questions usually contain a question mark. ;-)'
        }
      },
      watch: {
        // 每当 question 改变时,这个函数就会执行
        question(newQuestion, oldQuestion) {
          if (newQuestion.includes('?')) {
            this.getAnswer()
          }
        }
      },
      methods: {
        async getAnswer() {
          this.answer = 'Thinking...'
          try {
            const res = await fetch('https://yesno.wtf/api')
            this.answer = (await res.json()).answer
          } catch (error) {
            this.answer = 'Error! Could not reach the API. ' + error
          }
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    <p>
      Ask a yes/no question:
      <input v-model="question" />
    p>
    <p>{{ answer }}p>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    watch 选项也支持把键设置成用 . 分隔的路径:

    export default {
      data:{ return {
        a: {
          b: {
            c: 'hello'
          }
        }
      }},
      watch: {
        // 注意:只能是简单的路径,不支持表达式。
        a.b.c(newValue, oldValue) {
          // ...
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    深层侦听器

    watch 默认是浅层的:被侦听的属性,仅在被赋新值时,才会触发回调函数——而嵌套属性的变化不会触发。如果想侦听所有嵌套的变更,则需要使用深层侦听器:

    export default {
      watch: {
        someObject: {
          handler(newValue, oldValue) {
            // 注意:在嵌套的变更中,
            // 只要没有替换对象本身,
            // 那么这里的 `newValue` 和 `oldValue` 相同
          },
          deep: true
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    需注意,深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此只在必要时才使用它,并且要留意性能。

    即时回调的侦听器

    watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,希望在创建侦听器时,立即执行一遍回调。举例来说,想请求一些初始数据,然后在相关状态更改时重新请求数据。

    我们可以用一个对象来声明侦听器,这个对象有 handler 方法和 immediate: true 选项,这样便能强制回调函数立即执行:

    export default {
      // ...
      watch: {
        question: {
          handler(newQuestion, oldQuestion) {
            // 在组件实例创建时会立即调用
            // 立即调用时,oldQuestion为 null
          },
          // 强制立即执行回调
          immediate: true
        }
      }
      // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    回调函数的初次执行就发生在 created 钩子之前。Vue 此时已经处理了 data、computed 和 methods 选项,所以这些属性在第一次调用时就是可用的。

    回调的触发时机

    当更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。

    如果想在侦听器回调中能访问被 Vue 更新之后的DOM,你需要指明 flush: 'post' 选项:

    export default {
      // ...
      watch: {
        key: {
          handler() {},
          flush: 'post'
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    this.$watch()

    也可以使用组件实例的 $watch() 方法来命令式地创建一个侦听器:

    export default {
      created() {
        this.$watch('question', (newQuestion) => {
          // ...
        })
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果要在特定条件下设置一个侦听器,或者只侦听响应用户交互的内容,这方法很有用。它还允许提前停止该侦听器。

    停止侦听器

    用 watch 选项或者 $watch() 实例方法声明的侦听器,会在宿主组件卸载时自动停止。因此,在大多数场景下,无需关心怎么停止它。在少数情况下,的确需要在组件卸载之前就停止一个侦听器,这时可以调用 $watch() API 返回的函数:

    const unwatch = this.$watch('foo', callback)
    
    // ...当该侦听器不再需要时
    unwatch()
    
    • 1
    • 2
    • 3
    • 4

    组件基础

    组件(component)允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。即使重复使用同一个组件,组件与组件之间也是相互独立的,这使得各组件能够独立维护自身的状态。

    定义一个组件

    当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件 (简称 SFC):

    <script>
    export default {
      data() {
        return {
          count: 0
        }
      }
    }
    script>
    
    <template>
      <button @click="count++">You clicked me {{ count }} times.button>
    template>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    当不使用构建步骤时,一个 Vue 组件以一个包含 Vue 特定选项的 JavaScript 对象来定义:

    // 声明组件
    // 注意,Vue 对象不是 vue 框架的类对象,而应该是 mount 后的 vm 应用实例对象。
    Vue.component('组件名', {
      data() {		// 组件内部的数据
        ...
      },
      methods: {	// 组件内部的方法
      	...
      },
      template: ``,		// 组件的 html 代码
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这里的模板是一个内联的 JavaScript 字符串,Vue 将会在运行时编译它。也可以使用 ID 选择器来指向一个元素 (通常是原生的