Vue 中的事件处理和实例生命周期(五)

前言

本章我们会学习 Vue 中的事件处理、监听属性、计算属性以及 Vue 实例的生命周期。


事件处理

监听事件

上一章节,我们提到v-on(缩写@)指令是用来监听 DOM 事件或自定义事件的,现在我们体验一个简单例子:

<button @click="counter += 1">+1</button>
<p>这个按钮被点击了 {{ counter }} 次。</p>
复制代码
// <script>块
export default {
  data(){
    return {
      counter: 0
    }    
  }
}
复制代码

效果:

20220310_160920.gif

事件处理方法

然而许多事件处理逻辑会更为复杂,所以直接把 JavaScript 代码写在 v-on 指令中是不可行的。因此 v-on 还可以接收一个需要调用的方法名称,而方法需要在 methods 对象中定义。

<button @click="greet">Greet</button>
复制代码
// <script>块
export default {
  data(){
    return {
      name: 'Vue.js'
    }    
  },
  methods: {
    greet(event) {
      // `this` 在方法里指向当前 Vue 实例
      alert('Hello ' + this.name + '!')
      // `event` 是原生 DOM 事件
      if (event) {
        alert(event.target.tagName)
      }
    }
  }
}
复制代码

效果:

20220310_162424.gif

除了直接绑定到一个方法,也可以在内联 JavaScript 语句中调用方法:

<button @click="say('hi')">Say hi</button>
<button @click="say('what')">Say what</button>
复制代码
// <script>块
export default {
  data(){
    return {
      name: 'Vue.js'
    }    
  },
  methods: {
    say(message) {
      alert(`${message} ${this.name}`)
    }
  }
}
复制代码

效果:

20220310_163258.gif

有时也需要在内联语句处理器中访问原始的 DOM 事件。可以用特殊变量 $event 把它传入方法:

<form action="https://www.baidu.com/" method="get">
  <button type="submit" @click="warn('Form cannot be submitted yet.', $event)">
    Submit
  </button>    
</form>
复制代码
// <script>块
export default {
  methods: {
    warn(message, event) {
      // 现在我们可以访问原生事件对象
      if (event) {
        // 阻止默认提交行为,不会跳往百度
        event.preventDefault()
      }
      alert(message)
    }
  }
}
复制代码

事件修饰符

在事件处理程序中调用 event.preventDefault() 或 event.stopPropagation() 是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。

为了解决这个问题,Vue.js 为 v-on 提供了事件修饰符。修饰符是由点开头的指令后缀来表示的:

  • .stop
  • .prevent
  • .capture
  • .self
  • .once
  • .passive
  • .once
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>

<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>

<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>

<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>

<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>

<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>

<!-- 点击事件将只会触发一次 --> 
<a v-on:click.once="doThis"></a>
复制代码

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击。

实例生命周期

每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。比如created钩子可以用来在一个实例被创建之后执行代码:

export default {
  data() {
    return {
      message: 'Hello'
    }
  },
  methods: {
    getNowTime() {
      return Date.now()
    }
  },
  created() {
    // 当前钩子可以通过 this 访问实例
    console.log(`${this.message} created: ${this.getNowTime()}`)
  }
}
复制代码

image.png

所有生命周期钩子的 this 上下文将自动绑定至实例中,因此你可以访问 data、computed 和 methods。这意味着你不应该使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos())。因为箭头函数绑定了父级上下文,所以 this 不会指向预期的组件实例,并且this.fetchTodos 将会是 undefined。

现在我们通过浏览器控制台观测一下各个钩子执行的时机:

export default {
  data() {
    return {
      message: 'Hello'
    }
  },
  beforeCreate() {
    console.log('beforeCreate:在实例初始化之后,进行数据侦听和事件/侦听器的配置之前同步调用。')
  },
  created() {
    console.log('created:在实例创建完成后被立即同步调用。在这一步中,实例已完成对选项的处理,意味着以下内容已被配置完毕:数据侦听、计算属性、方法、事件/侦听器的回调函数。然而,挂载阶段还没开始,且 $el property 目前尚不可用。')
  },
  beforeMount() {
    console.log('beforeMount:在挂载开始之前被调用:相关的 render 函数首次被调用。')
  },
  mounted() {
    console.log('mounted:实例被挂载后调用,这时 el 被新创建的 vm.$el 替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.$el 也在文档内。')
  },
  beforeUpdate() {
    console.log('beforeUpdate:在数据发生改变后,DOM 被更新之前被调用。这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器。')
  },
  updated() {
    console.log('updated:在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。')
  }
}
复制代码

20220315_150907.gif

除上述代码所示的钩子函数外,常用的钩子还有beforeDestroy(实例销毁之前调用,在这一步,实例仍然完全可用)和destroyed(实例销毁后调用,该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁)。下图是实例生命周期的示意图,你现在不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。

1

计算属性和监听属性

computed

模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如:

<div>
  {{ message.split('').reverse().join('') }}
</div>
复制代码

在这个地方,模板不再是简单的声明式逻辑。你必须看一段时间才能意识到,这里是想要显示变量message的翻转字符串。当你想要在模板中的多处包含此翻转字符串时,就会更加难以处理。所以,对于任何复杂逻辑,你都应当使用计算属性

<p>原始的 message: "{{ message }}"</p>
<p>计算反转后的 message: "{{ reversedMessage }}"</p>
复制代码
// <script>块
export default {
  data() {
    return {
      message: 'Hello Vue.js!'
    }
  },
  computed: {
    // 这里相当于声明了计算属性 reversedMessage 的 getter 函数
    reversedMessage() {
      return this.message.split('').reverse().join('')
    }
  }
}
复制代码

渲染结果:

image.png

我们可以打开浏览器的控制台,自行修改 data 中的message ,会发现reversedMessage 的值始终取决于message的值。

20220314_101539.gif

你可能已经注意到我们可以通过在表达式中调用方法来达到同样的效果,我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。这也同样意味着下面的计算属性将不再更新,因为 Date.now() 不是响应式依赖:

<template v-if="trigger">
  <div>1、getTimeByMethod:{{ getTimeByMethod() }}</div>
  <div>1、getTimeByComputed:{{ getTimeByComputed }}</div>
</template>
<template v-else>
  <div>2、getTimeByMethod:{{ getTimeByMethod() }}</div>
  <div>2、getTimeByComputed:{{ getTimeByComputed }}</div>
</template>
<button @click="trigger = !trigger">trigger</button>
复制代码
//<script>块
export default {
  data() {
    return {
      trigger: true
    }
  },
  methods: {
    getTimeByMethod() {
      return Date.now()
    }
  },
  computed: {
    getTimeByComputed() {
      return Date.now()
    }
  }
}
复制代码

20220314_103532.gif

计算属性默认只有 getter,不过在需要时你也可以提供一个 setter:

<div>{{ fullName }}</div>
<button @click="fullName = 'LeBron James'">Update fullName</button>
复制代码
// <script>块
export default {
  data() {
    return {
      firstName: 'Kobe',
      lastName: 'Bryant'
    }
  },
  computed: {
    fullName: {
      // getter
      get() {
        return this.firstName + ' ' + this.lastName
      },
      // setter
      set(newValue) {
        var names = newValue.split(' ')
        this.firstName = names[0]
        this.lastName = names[names.length - 1]
      }
    }
  }
}
复制代码

现在再修改 fullName = 'LeBron James' 时,setter 会被调用,firstNamelastName也会相应地被更新:

20220314_105147.gif

watch

watchcomputed很相似,watch 用于观察和监听页面上的 Vue 实例,当然在大部分情况下我们都会使用 computed,但如果要在数据变化的同时进行异步操作或者是比较大的开销,那么 watch 为最佳选择。watch 为一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象:

export default {
  data() {
    return {
      isShowInfo: false,
      student: {
        name: '李四',
        age: 14
      }
    }
  },
  watch: {
    isShowInfo(newVal, oldVal){
      alert(`student.age->new:${newVal} old:${oldVal}`)
    },
    'student.age': {
      handler(newVal, oldVal){
        alert(`student.age->new:${newVal} old:${oldVal}`)
      },
      // immediate: true, // 默认只有数据改变才会监听,第一次不会执行,设置true则第一次也能执行
      // deep: true // 为了发现对象内部值的变化,可以设置true进行深层监听
    }
  }
}
复制代码

20220315_163747.gif


内容预告

本章我们介绍了 Vue 中的事件处理、监听属性、计算属性以及 Vue 实例的生命周期。在下一章节中,我们会学习如何进行表单绑定以及 Vue 的数组更新检测

猜你喜欢

转载自juejin.im/post/7075247710508417032