三、使用 Vue 编写简洁的代码

在本节中,我们将通过查看 Vue 如何处理 Vue.js 实例,来研究 Vue.js 实例如何在较低级别上工作。我们还将研究实例上的各种属性,如数据、计算、监视,以及使用每种属性时的最佳实践。此外,我们将研究 Vue 实例中可用的各种生命周期挂钩,使我们能够在应用的各个阶段调用特定函数。最后,我们将研究文档对象模型DOM)以及 Vue 实现虚拟 DOM 以提高性能的原因。

在本章结束时,您将:

  • 更好地了解this关键字在 JavaScript 中的工作方式
  • 了解 Vue 如何在 Vue 实例中代理this关键字
  • 使用数据属性创建反应式绑定
  • 使用计算属性根据数据模型创建声明性函数
  • 使用监视的属性访问异步数据,并在计算属性的基础上构建
  • 使用生命周期挂钩在 Vue 生命周期的特定阶段激活功能
  • 调查 DOM 和虚拟 DOM,了解 Vue 如何将数据呈现到屏幕上

首先,让我们先看看这在 JavaScript 中是如何工作的,以及它如何与 Vue 实例中的上下文相关。

代理

到目前为止,您可能已经与 Vue 应用进行了交互,并在想:this是如何工作的?在研究 Vue.js 如何处理this之前,让我们先看看它在 JavaScript 中是如何工作的。

“this”在 JavaScript 中的工作原理

在 JavaScript 中,this具有不同的上下文,从全局窗口上下文到 eval、newable 和函数上下文。由于默认上下文与全局范围相关,因此这是我们的窗口对象:

/**
 * Outputting the value of this to the console in the global context returns the Window object
 */
console.log(this);

/**
 * When referencing global Window objects, we don't need to refer to them with this, but if we do, we get the same behavior
 */
alert('Alert one');
this.alert('Alert two');

根据我们在范围内的位置,这种情况的背景会发生变化。这意味着,如果我们有一个具有特定值的Student对象,例如firstNamelastNamegrades等等,this的上下文将与对象本身相关:

/**
 * The context of this changes when we enter another lexical scope, take our Student object example:
 */
const Student = {
 firstName: 'Paul',
 lastName: 'Halliday',
 grades: [50, 95, 70, 65, 35],
 getFullName() {
  return `${this.firstName} ${this.lastName}` 
 },
 getGrades() {
  return this.grades.reduce((accumulator, grade) => accumulator + grade);
 },
 toString() {
  return `Student ${this.getFullName()} scored ${this.getGrades()}/500`;
 }
}

当我们使用console.log(Student.toString())运行前面的代码时,我们得到:Student Paul Halliday scored 315/500,因为它的上下文现在的作用域是对象本身,而不是全局窗口作用域。

如果我们想在文档中显示这一点,我们可以这样做:

let res = document.createTextNode(Student.toString());
let heading = document.createElement('h1');
heading.appendChild(res);
document.body.appendChild(heading);

请注意,对于前面的代码,我们再次不必使用this,因为全局上下文不需要它。

现在我们已经了解了this在基本级别上的工作原理,我们可以研究 Vue 代理this在实例中如何使与各种属性的交互更加容易。

Vue 如何处理“此”

到目前为止,您可能已经注意到,我们可以使用this语法引用数据、方法和其他对象内部的值,但我们实例的实际结构是this.data.propertyNamethis.methods.methodName;由于 Vue 代理实例属性的方式,所有这些都是可能的。

让我们来看看一个非常简单的 VUE 应用,它有一个实例。我们有一个data对象,它有一个message变量和一个名为showAlert的方法;Vue 如何知道如何使用this.message访问我们的警报文本?

<template>
 <button @click="showAlert">
 Show Alert</button>
</template>

<script>
export default {
 data() {
  return {
   message: 'Hello World!',
  };
 },
 methods: {
  showAlert() {
   alert(this.message);
  },
 },
};
</script>

Vue 将实例属性代理到顶级对象,使我们能够通过该代理访问这些属性。如果我们将实例注销到控制台(在 Vue.js devtools 的帮助下),我们将得到以下结果:

Console logout

在前面的屏幕截图中要查看的关键属性是messageshowAlert,这两个属性都是在我们的 Vue 实例上定义的,但在初始化时已代理到根对象实例。

数据属性

当我们向数据对象添加变量时,我们实际上是在创建一个被动属性,该属性在视图发生任何更改时都会更新视图。这意味着,如果我们有一个名为firstName的属性的数据对象,那么每次值更改时,该属性都会在屏幕上重新呈现:

<!DOCTYPE html>
<html>
<head>
 <title>Vue Data</title>
 <script src="https://unpkg.com/vue"></script>
</head>
<body>
 <div id="app">
  <h1>Name: {{ firstName }}</h1>
  <input type="text" v-model="firstName">
 </div>

 <script>
 const app = new Vue({
  el: '#app',
  data: {
   firstName: 'Paul'
  }
 });
 </script>
</body>
</html>

这种反应性不适用于在数据对象之外创建 Vue 实例后添加到该实例的对象。如果我们有另一个例子,但这次包括将另一个属性(如fullName)附加到实例本身:

<body>
 <div id="app">
  <h1>Name: {{ firstName }}</h1>
  <h1>Name: {{ name }}</h1>
  <input type="text" v-model="firstName">
 </div>

 <script>
 const app = new Vue({
  el: '#app',
  data: {
   firstName: 'Paul'
  }
 });
 app.fullName = 'Paul Halliday';
 </script>
</body>

即使此项位于根实例上(与我们的firstName变量相同),fullName也不是被动的,不会在任何更改后重新呈现。这不起作用,因为当 Vue 实例初始化时,它映射到每个属性,并向每个数据属性添加一个 getter 和 setter,因此,如果我们在初始化后添加一个新属性,它将缺少此属性,并且不是被动的。

Vue 如何实现反应性特性?目前,它使用Object.defineProperty为实例内部的项定义自定义 getter/setter。让我们在具有标准get/set功能的对象上创建我们自己的属性:

 const user = {};
 let fullName = 'Paul Halliday';

 Object.defineProperty(user, 'fullName', {
  configurable: true,
  enumerable: true,
  get() {
   return fullName;
  },
  set(v) {
   fullName = v;
  }
 });

 console.log(user.fullName); // > Paul Halliday
 user.fullName = "John Doe";
 console.log(user.fullName); // > John Doe

由于观察者是使用自定义属性 setter/getter 设置的,因此在初始化后仅向实例添加属性是不允许反应的。这可能会在 Vue 3 中发生变化,因为它将使用较新的 ES2015+代理 API(但可能不支持较旧的浏览器)。

我们的 Vue 实例不仅仅是一个数据属性!让我们使用 computed 根据数据模型中的项创建反应性的派生值。

计算属性

在本节中,我们将查看 Vue 实例中的计算属性。这允许我们创建小型的声明性函数,这些函数根据数据模型中的项返回单个值。为什么这很重要?好吧,如果我们将所有逻辑都保存在模板中,那么我们的团队成员和未来的自己都必须做更多的工作来理解我们的应用的功能。

因此,我们可以使用计算属性大大简化模板,并创建可以引用的变量,而不是逻辑本身。它不仅仅是一种抽象;计算出的属性将被缓存,并且不会重新计算,除非依赖项已更改。

让我们创建一个简单的项目来了解这一点:

# Create a new Vue.js project
$ vue init webpack-simple computed 

# Change directory
$ cd computed

# Install dependencies
$ npm install

# Run application
$ npm run dev

插值功能强大;例如,在我们的 Vue.js 模板中,我们可以使用一个字符串(例如,firstName)并使用reverse()方法将其反转:

<h1>{{  firstName.split('').reverse().join('') }}</h1>

我们现在将展示我们的firstName的反向版本,因此 Paul 将成为 luaP。这样做的问题是,在模板中保留逻辑不是很实际。如果要反转多个字段,则必须在每个属性上添加另一个split()reverse()join()。为了使其更具声明性,我们可以使用计算属性。

App.vue内部,我们可以添加一个新的计算对象,其中包含一个名为reversedName的函数;这需要我们反转字符串的逻辑,并允许我们将其抽象为包含逻辑的函数,否则会污染模板:

<template>
 <h1>Name: {{ reversedName }}</h1>
</template>

<script>
export default {
 data() {
  return {
   firstName: 'Paul'
  }
 },
 computed: {
  reversedName() {
   return this.firstName.split('').reverse().join('')
  }
 }
}
</script>

然后,我们可以在 Vue.js devtools 中查看有关计算属性的更多信息:

Using devtools to display data

在我们的简单示例中,重要的是要认识到,虽然我们可以将其作为一种方法,但我们有理由将其保留为计算属性。计算属性会被缓存,并且不会重新呈现,除非它们的依赖关系发生变化,如果我们有一个更大的数据驱动应用,这一点尤为重要。

监视的属性

计算属性并不总是解决被动数据问题的答案,有时我们需要创建自己的自定义监视属性。计算属性只能是同步的,必须是纯的(例如,没有观察到的副作用),并返回一个值;这与监视属性形成直接对比,监视属性通常用于处理异步数据。

监视属性允许我们在数据更改时反应性地执行函数。这意味着,每当数据对象中的某个项发生更改时,我们都可以调用一个函数,并且我们可以将此更改后的值作为参数进行访问。让我们来看一个简单的例子:

Note: Axios is a library that will need to be added to the project. To do so, head to https://github.com/axios/axios and follow the installation steps provided.

<template>
 <div>
  <input type="number" v-model="id" />
  <p>Name: {{user.name}}</p>
  <p>Email: {{user.email}}</p>
  <p>Id: {{user.id}}</p>
 </div>
</template>

<script>
import axios from 'axios';

export default {
 data() {
  return {
   id: '',
   user: {}
  }
 },
 methods: {
  getDataForUser() { 
   axios.get(`https://jsonplaceholder.typicode.com/users/${this.id}`)
 .then(res => this.user = res.data);
  }
 },
 watch: {
  id() {
   this.getDataForUser();
  }
 }
}
</script>

在本例中,每当文本框更改为新的id(1-10)时,我们都会得到关于该特定用户的信息,如下所示:

这将有效地监视id上的任何更改,并调用getDataForUser方法,检索有关此用户的新数据。

虽然监视属性确实有很多功能,但不应低估计算属性对性能和易用性的好处;因此,在可能的情况下,将计算属性置于监视属性之上。

生命周期挂钩

我们可以访问各种各样的生命周期挂钩,这些挂钩在创建 Vue 实例期间的特定点上触发。这些钩子的范围从创建之前的beforeCreate到实例之后的mounteddestroyed等等。

如下图所示,创建新的 Vue 实例会在实例生命周期的不同阶段触发功能。

我们将在本节中了解如何激活这些挂钩:

Vue.js instance lifecycle hooks

利用生命周期挂钩(https://vuejs.org/v2/guide/instance.html )可以以与我们的 Vue 实例上的任何其他属性类似的方式完成。让我们来看看我们如何与每个钩子相互作用,从顶部开始;我将基于标准webpack-simple模板创建另一个项目:

// App.vue
<template>
</template>

<script>
export default {
 data () {
   return {
    msg: 'Welcome to Your Vue.js App'
   }
 },
 beforeCreate() {
  console.log('beforeCreate'); 
 },
 created() {
  console.log('created');
 }
}
</script>

请注意,我们只是简单地将这些函数添加到实例中,而没有任何额外的导入或语法。然后,我们在控制台中得到两个不同的日志语句,一个在创建实例之前,一个在创建实例之后。我们实例的下一个阶段是beforeMountedmounted挂钩;如果添加这些,我们将能够再次在控制台上看到一条消息:

beforeMount() {
 console.log('beforeMount');
},
mounted() {
 console.log('mounted');
}

如果我们修改我们的模板,使其有一个按钮更新我们的一个数据属性,我们就可以启动一个beforeUpdatedupdated挂钩:

<template>
 <div>
  <h1>{{msg}}</h1>
  <button @click="msg = 'Updated Hook'">Update Message</button>
 </div>
</template>

<script>
export default {
 data () {
   return {
    msg: 'Welcome to Your Vue.js App'
   }
 },
 beforeCreate() {
  console.log('beforeCreate'); 
 },
 created() {
  console.log('created');
 },
 beforeMount() {
  console.log('beforeMount');
 },
 mounted() {
  console.log('mounted');
 },
 beforeUpdated() {
  console.log('beforeUpdated'); 
 },
 updated() {
  console.log('updated');
 }
}
</script>

每当我们选择Update Message按钮时,我们的beforeUpdatedupdated都会触发。这允许我们在生命周期的这个阶段执行一项操作,只留下beforeDestroy和尚未覆盖的破坏。让我们向我们的实例添加一个按钮和一个调用$destroy的方法,允许我们触发适当的生命周期挂钩:

<template>
  <div>
    <h1>{{msg}}</h1>
    <button @click="msg = 'Updated Hook'">Update Message
    </button>
    <button @click="remove">Destroy instance</button>
  </div>
</template>

然后我们可以将remove方法添加到我们的实例中,以及允许我们捕获适当钩子的函数:

methods: {
  remove(){
   this.$destroy();
  }
},
// Other hooks
  beforeDestroy(){
  console.log("Before destroy");
},
  destroyed(){
  console.log("Destroyed");
}

当我们选择destroy实例按钮时,beforeDestroydestroy生命周期挂钩将启动。这允许我们在销毁实例时清理任何订阅或执行任何其他操作。在现实场景中,生命周期控制应该由数据驱动的指令决定,例如v-ifv-for。我们将在下一章更详细地了解这些指令。

Vue.js 和虚拟 DOM

在性能改进的主题上,让我们考虑 Vue.js 为什么广泛使用虚拟 DOM 来在屏幕上呈现元素。在研究虚拟 DOM 之前,我们需要对 DOM 实际上是什么有一个基本的了解。

多姆

DOM 用于描述 HTML 或 XML 页面的结构。它创建了一个树状结构,使我们能够在 JavaScript 中完成从创建、读取、更新和删除节点到遍历树和更多功能的所有操作。让我们考虑下面的 HTML 页面:

<!DOCTYPE html>
<html lang="en">
<head>
 <title>DOM Example</title>
</head>
<body>
 <div>
  <p>I love JavaScript!</p>
  <p>Here's a list of my favourite frameworks:</p>
  <ul>
   <li>Vue.js</li>
   <li>Angular</li>
   <li>React</li>
  </ul>
 </div>

 <script src="app.js"></script>
</body>
</html>

我们可以查看 HTML,看到我们有一个div、两个p、一个ulli标签。浏览器解析此 HTML 并生成 DOM 树,该树在较高级别上类似于以下内容:

然后,我们可以通过TagName使用document.getElementsByTagName()与 DOM 交互以访问这些元素,并返回一个 HTML 集合。如果我们想映射这些集合对象,我们可以使用Array.from创建这些元素的数组。以下是一个例子:

const paragraphs = Array.from(document.getElementsByTagName('p'));
const listItems = Array.from(document.getElementsByTagName('li'));

paragraphs.map(p => console.log(p.innerHTML));
listItems.map(li => console.log(li.innerHTML));

然后将每个项目的innerHTML记录到数组内部的控制台,从而显示如何访问 DOM 内部的项目:

虚拟 DOM

更新 DOM 节点在计算上非常昂贵,并且取决于应用的大小,这会大大降低应用的性能。虚拟 DOM 采用了 DOM 的概念,并为我们提供了一个抽象,它允许使用扩散算法来更新 DOM 节点。为了充分利用这一点,这些节点不再使用文档前缀进行访问,而是通常表示为 JavaScript 对象。

这允许 Vue 精确地计算出什么发生了变化,并且只在实际 DOM 中重新呈现与之前不同的项。

总结

在本章中,我们进一步了解了 Vue 实例,以及如何利用各种属性类型,如数据、观察者、计算值等。我们已经了解了this在 JavaScript 中的工作原理以及在 Vue 实例中使用它时的区别。此外,我们还研究了 DOM 以及 Vue 为什么使用虚拟 DOM 创建性能良好的应用。

总之,数据属性允许在模板中使用被动属性,计算属性允许我们使用模板和过滤逻辑并将其分离为可在模板中访问的性能属性,监视属性允许我们处理异步操作的复杂性。

在下一章中,我们将深入了解 Vue 指令,如v-ifv-modelv-for,以及如何使用它们创建功能强大的反应式应用。