例子依然来自Mdn Web Docs,加上了我本人的了解。长乐未央,长毋相忘。某种意义上算是MDN web docs 中Vue教程的翻译,但又加上了本人的了解。

地址是: https://developer.mozilla.org...

进化: 编辑组件

当初咱们的组件依然不太完满,因为它不能编辑,输错了,就输错了。对此咱们的解决方案是引入一个编辑组件。还是固定的步骤,首先在src/components下建设一个文件,咱们将其命名为ToDoItemEditForm.vue。而后将上面你的代码复制到这个文件中:

<template>  <form class="stack-small" @submit.prevent="onSubmit">    <div>      <label class="edit-label">Edit Name for &quot;{{label}}&quot;</label>      <input :id="id" type="text" autocomplete="off" v-model.lazy.trim="newLabel" />    </div>    <div class="btn-group">      <button type="button" class="btn" @click="onCancel">        勾销        <span class="visually-hidden">正在编辑 {{label}}</span>      </button>      <button type="submit" class="btn btn__primary">        保留        <span class="visually-hidden">对{{label}}进行批改</span>      </button>    </div>  </form></template><script>  export default {    props: {      label: {        type: String,        required: true,      },      id: {        type: String,        required: true,      },    },    data() {      return {        newLabel: this.label,      };    },    methods: {      onSubmit() {        if (this.newLabel && this.newLabel !== this.label) {          this.$emit("item-edited", this.newLabel);        }      },      onCancel() {        this.$emit("edit-cancelled");      },    },  };</script><style scoped>  .edit-label {    font-family: Arial, sans-serif;    -webkit-font-smoothing: antialiased;    -moz-osx-font-smoothing: grayscale;    color: #0b0c0c;    display: block;    margin-bottom: 5px;  }  input {    display: inline-block;    margin-top: 0.4rem;    width: 100%;    min-height: 4.4rem;    padding: 0.4rem 0.8rem;    border: 2px solid #565656;  }  form {    display: flex;    flex-direction: row;    flex-wrap: wrap;  }  form > * {    flex: 0 0 100%;  }</style>

咱们大抵的解读一下这个文件,在这个文件外面咱们创立了一个表单,表单中的input用于编辑待办事项的名称,用v-model和data中的newLabel建设了双向绑定。同时咱们申明了这个组件接管的音讯(变量)prop。

在这个表单中有一个保留和勾销按钮。

  • 点击保留按钮时,组件会通过emit收回item-edited事件。
  • 点击勾销按钮时,组件会通过emit收回edit-cancelled事件

当初咱们来革新一下待办组件,为待办组件增加上编辑和删除性能。咱们的设计指标是在待办上面呈现一个编辑和删除按钮, 当点击编辑按钮的时候,待办组件暗藏,编辑组件呈现。这是一种互斥的关系,咱们须要用一个变量来示意这样的状态,咱们在data外面进行申明:

data () {    return {      isDone: this.done,      isEditing: false    }}

那怎么通关isEditing来管制待办组件的显示和不显示呢,咱们能够应用Vue指令if else指令来实现。咱们为待办组件的template最外层再增加一个div,同时也是为了管制款式,如果!isEditing = true就代表以后不处于编辑状态。同时在待办列表上面增加一个编辑和删除按钮,为编辑按钮绑定处理事件,将isEditing = true。像上面这样:

<template>   <div class="stack-small" v-if="!isEditing">        <div class="custom-checkbox">            <input type="checkbox" :id="id" :checked="isDone" class="checkbox" @change="$emit('todo-complete')"/>            <label :for="id" class="checkbox-label"> {{label}}</label>        </div>        <div class="btn-group">       <button type="button" class="btn"  @click="toggleToItemEditForm">        编辑 <span class="visually-hidden">{{label}}</span>      </button>      <button type="button" class="btn btn__danger" @click="deleteToDo">        删除 <span class="visually-hidden">{{label}}</span>      </button>    </div></div><to-do-item-edit-form v-else :id="id" :label="label"></to-do-item-edit-form></template><script>import ToDoItemEditForm from './ToDoItemEditForm.vue'export default {  name: 'ToDoItem',  components: {    ToDoItemEditForm  },  props: {    label: {required: true, type: String},    done: {default: false, type: Boolean},    id: {required: true, type: String}  },  data () {    return {      isDone: this.done,      isEditing: false    }  },  methods: {    deleteToDo () {      this.$emit('item-deleted')    },    toggleToItemEditForm () {      this.isEditing = true    }  }}</script><style scoped>.custom-checkbox > .checkbox-label {  font-family: Arial, sans-serif;  -webkit-font-smoothing: antialiased;  -moz-osx-font-smoothing: grayscale;  font-weight: 400;  font-size: 16px;  font-size: 1rem;  line-height: 1.25;  color: #0b0c0c;  display: block;  margin-bottom: 5px;}.custom-checkbox > .checkbox {  font-family: Arial, sans-serif;  -webkit-font-smoothing: antialiased;  -moz-osx-font-smoothing: grayscale;  font-weight: 400;  font-size: 16px;  font-size: 1rem;  line-height: 1.25;  box-sizing: border-box;  width: 100%;  height: 40px;  height: 2.5rem;  margin-top: 0;  padding: 5px;  border: 2px solid #0b0c0c;  border-radius: 0;  appearance: none;}.custom-checkbox > input:focus {  outline: 3px dashed #fd0;  outline-offset: 0;  box-shadow: inset 0 0 0 2px;}.custom-checkbox {  font-family: Arial, sans-serif;  -webkit-font-smoothing: antialiased;  font-weight: 400;  font-size: 1.6rem;  line-height: 1.25;  display: block;  position: relative;  min-height: 40px;  margin-bottom: 10px;  padding-left: 40px;  clear: left;}.custom-checkbox > input[type="checkbox"] {  -webkit-font-smoothing: antialiased;  cursor: pointer;  position: absolute;  z-index: 1;  top: -2px;  left: -2px;  width: 44px;  height: 44px;  margin: 0;  opacity: 0;}.custom-checkbox > .checkbox-label {  font-size: inherit;  font-family: inherit;  line-height: inherit;  display: inline-block;  margin-bottom: 0;  padding: 8px 15px 5px;  cursor: pointer;  touch-action: manipulation;}.custom-checkbox > label::before {  content: "";  box-sizing: border-box;  position: absolute;  top: 0;  left: 0;  width: 40px;  height: 40px;  border: 2px solid currentcolor;  background: transparent;}.custom-checkbox > input[type="checkbox"]:focus + label::before {  border-width: 4px;  outline: 3px dashed #228bec;}.custom-checkbox > label::after {  box-sizing: content-box;  content: "";  position: absolute;  top: 11px;  left: 9px;  width: 18px;  height: 7px;  transform: rotate(-45deg);  border: solid;  border-width: 0 0 5px 5px;  border-top-color: transparent;  opacity: 0;  background: transparent;}.custom-checkbox > input[type="checkbox"]:checked + label::after {  opacity: 1;}@media only screen and (min-width: 40rem) {  label,  input,  .custom-checkbox {    font-size: 19px;    font-size: 1.9rem;    line-height: 1.31579;  }}</style>

当初当咱们的页面如下所示:

当点击了编辑之后:

但你会发现点击勾销不能返回,点击保留没反馈。起因在于咱们的编辑组件收回的事件没有被待办组件所理睬,当编辑待办组件收回点击勾销按钮事件,待办组件该当将编辑组件暗藏,也就是将isEditing置为false。当编辑待办组件按钮收回点击保留按钮事件,咱们同样该当将编辑组件暗藏,所以咱们待办组件中的template的编辑待办模板标签变成了上面这样:

<to-do-item-edit-form v-else :id="id" :label="label" @item-edited="itemEdited" @edit-cancelled="editCancelled"></to-do-item-edit-form>

待办组件的methods多了两个办法itemEdited和editCancelled:

itemEdited (newLabel) {  this.$emit('item-edited', newLabel)  this.isEditing = false},editCancelled () {  this.isEditing = false}

当初咱们点击勾销就能回到待办组件,然而点击保留依然没有反馈,起因在于咱们在待办组件外面收回的事件没有被App组件所解决,待办列表的数据在App外面。所以当初咱们就须要转到App组件外面,解决这个事件。首先在待办组件上申明解决此事件的办法:

<to-do-item :label="item.label"  :done="item.done" :id="item.id" @todo-complete="updateToDoStatus(item.id)" @item-edited="editToDo(item.id,$event)" @item-deleted="deleteToDo(item.id)"></to-do-item>

$event是一个非凡的Vue变量,用于携带子组件传递过去的数据。当初咱们须要在methods增加editToDo和deleteToDo办法:

editToDo (toDoId, newLabel) {    const toDoEdit = this.ToDoItems.find((item) => item.id === toDoId)    toDoEdit.label = newLabel },deleteToDo (toDoId) {      const deleteToDoIndex = this.ToDoItems.findIndex(item => item.id === toDoId)      this.ToDoItems.splice(deleteToDoIndex, 1) }

一个小问题

到目前为止所有看起来都很好,然而如果用一下会发现还是会有一点小问题:

  • 尝试选中待办事项的复选框

  • 而后点击该待办事项的的编辑按钮

  • 而后点击勾销

选中状态被失落了,待办事项的统计也出了问题,当你选中会发现统计数据跟你预期的相同,它变成了0:

起因在于加载组件时,复选框的状态取决于isDone,而isDone又取决于done,这个done又由内部传入,所以当咱们选中一个初始状态为未选中的复选框而后点击编辑,又点击勾销,就相当于待办组件又从新被加载,失落了选中的状态。然而侥幸的是解决这个问题也比较简单,咱们能够将isDone转为一个计算属性,计算属性会保留扭转。在Vue的官网文档是这么介绍计算属性的:

计算属性是基于它们的响应式依赖进行缓存的。只在相干响应式依赖产生扭转时它们才会从新求值

这也就是咱们将其转换为计算属性的时,点击选中按钮,计算属性会被计算一次,咱们点击勾销返回时,因为计算属性的依赖并没有产生更新,所以咱们的选中状态得以保留。你看在实践中咱们对Vue的一些了解更加深刻了。最后我对计算属性的了解是相比于在插值表达式中写逻辑判断,可维护性更高,就让template外面只负责显示,另一个方面是计算属性只有在相干响应式依赖产生扭转才会从新求值,这对于一些大型页面来说,如果咱们将简单的逻辑判断写在插值表达式外面,每次渲染都要再执行一遍运算,这会相当损耗性能。当初咱们能够借助计算属性来保留状态。

首先咱们将待办组件的data中勾销上面这一行:

isDone: this.done,

而后在待办组件的计算属性如下申明:

computed: {    isDone () {      return this.done    }},

当初你再保留并从新加载,就会发现问题曾经解决。是不是很有成就感了呢!

自定义事件与原生事件

在这个例子中让咱们感到有点不清晰的,恐怕也就是自定义事件和原生事件了吧。上面这个流程图会让你对事件流转更加清晰:

ref与焦点治理

咱们简直实现了一个小型的利用,但目前认真扫视的话,它的体验依然有些美中不足。比方咱们只用键盘来实现下面的操作。咱们能够借助tabs这个键盘上的按钮,来实现编辑、保留待办。让咱们从新加载页面,而后按tab键,你会发现待办的输入框上会有一个蓝色框框,代表咱们当初处于输入框,向上面这样:

这个蓝色的框框咱们权且称之为焦点(focus) , 再次按下tab键,这个焦点会被挪动到点击增加按钮上。再次按tab键,焦点会呈现在第一个待办的复选框上。接着按,它会停留在第一个待办的编辑按钮上。而后按下Enter键,而后编辑按钮隐没,呈现的是咱们的编辑待办组件,咱们的焦点也隐没了。这样的交互可能让用户的体验没有那么良好。当你再次按下tab键,焦点呈现在哪里,这取决于你应用的浏览器。同样的,如果你按table让焦点再次出现,按下保留或勾销编辑,焦点会再次隐没。

为了给用户更好的体验,咱们将增加代码来管制焦点,以便在编辑表单呈现的时候,让焦点呈现在编辑表单的输入框上。当用户在编辑表单中勾销编辑或保留编辑,让焦点从新回到编辑按钮。 为了做到这一点,咱们都须要对Vue如何工作有更加深刻一点的了解。

Virtual DOM(虚构DOM) and refs

Vue和支流的前端框架一样,抉择应用虚构DOM来治理结点,这意味着Vue在内存中保留了应用程序所有的结点代表(原文为representation),这意味着任何更新都会先达到内存中的结点。而后再对页面理论结点所需的更新进行批量同步。

间接读写实在DOM的结点绝对于虚构结点来说是有些低廉的,虚构结点会有更好的性能。而后这也意味着在框架外面你不能够通过浏览器的原生APIs来在操纵HTML元素(向Document.getElementById),这会导致虚构DOM和实在DOM同步呈现问题(失去同步 原文为going out of sync)。

然而如果你的确是须要操纵实在DOM的结点(像设置焦点),你能够抉择应用Vue ref。对于自定义的组件,你能够抉择应用refs间接拜访子组件的内部结构,然而留神,要小心应用,这会让你的代码看起来难以了解。

如果你想在组件中应用ref,你须要在想要拜访的元素上增加ref属性,并未该属性的值提供字符串标识符。留神在一个组件中ref必须是惟一的。

在待办组件外面增加ref

首先咱们对ToDoItem.vue进行革新,在编辑按钮上为它增加ref:

<button type="button" class="btn"  @click="toggleToItemEditForm" ref="editButton">        编辑 <span class="visually-hidden">{{label}}</span></button>

而后咱们就能够拿到这个结点了,让咱们在toggleToItemEditForm里尝试获取一下ref:

toggleToItemEditForm () {      console.log(this.$refs.editButton)      this.isEditing = true}

点击编辑按钮你就会在控制台发现输入了ref所在按钮的结点。

nextTick办法

当用户保留或勾销他们的编辑时,咱们心愿焦点回到编辑按钮上。所以咱们要对ToDoItem组件中的itemEdited和editCancelled中的办法进行革新。咱们创立一个不带参数的办法,在这个办法中咱们获取下面的ref,ref目前处于编辑按钮上,咱们拿到这个按钮而后设置焦点即可。

focusOnEditButton () {      const editButtonRef = this.$refs.editButton      editButtonRef.focus() }

而后在itemEdited和editCancelled调用即可:

itemEdited (newLabel) {      this.$emit('item-edited', newLabel)      this.isEditing = false      this.focusOnEditButton();},editCancelled () {      this.isEditing = false      this.focusOnEditButton();},

然而即便做了革新,你尝试保留/勾销待办也会发现,焦点没有按咱们料想的回到编辑按钮上,同时在控制台也会看到上面的报错:

[Vue warn]: Error in v-on handler: "TypeError: editButtonRef is undefined"found in---> <ToDoItemEditForm> at src/components/ToDoItemEditForm.vue       <ToDoItem> at src/components/ToDoItem.vue         <App> at src/App.vue           <Root> vue.esm.js:5105TypeError: editButtonRef is undefined    focusOnEditButton ToDoItem.vue:60    editCancelled ToDoItem.vue:56    VueJS 4    onCancel ToDoItemEditForm.vue:43    VueJS 38    toggleToItemEditForm ToDoItem.vue:47    VueJS 21

当咱们点击编辑按钮的时候,咱们还能拿到这个ref,为什么点击勾销和保留就拿不到了呢? 起因在于当咱们点击编辑按钮,此时将isEditing设置true,咱们将不再渲染组件的编辑按钮,这也就意味着ref援用不到按钮,因而咱们无奈取得编辑按钮。

然而这也仿佛有些说不通,在咱们拜访ref之前,咱们不是将isEditing 设置为false了吗?那按钮不就应该显示了吗? 或者说渲染了吗?那为什么咱们取不到呢?这也就是虚构DOM发挥作用的中央,Vue试图优化批量更改,所以在DOM上的更新可能不会立马更新,它会放在一个队列外面,因而当咱们调用focusOnEditButton时,编辑按钮尚未渲染。咱们须要等到下一个DOM的更新周期之后,ref能力获取到按钮。

那有没有什么方法能让在DOM更新之后再调用focusOnEditButton办法呢,Vue提供了一个名为$nextTick的办法,该办法接管一个回调函数,回调函数会在DOM更新之后被调用,所以咱们的focusOnEditButton就变成了上面这样:

focusOnEditButton () {      this.$nextTick(()=>{        const editButtonRef = this.$refs.editButton        editButtonRef.focus()       })}

当初咱们咱们按编辑进入编辑表单,在编辑表单点击勾销或返回会发现编辑按钮上就呈现了焦点。

Vue 的生命周期

Vue的生命周期是一个相当重要的概念,咱们在后面的文章都回避了他,起因在于这个概念配合具体了例子,会让了解更加清晰。当初咱们还没有实现的事件是,在咱们点击编辑按钮的时候,将焦点挪动到表单的输入框,然而编辑按钮位于待办组件,编辑待办的输入框位于编辑组件。所以咱们不能在编辑按钮的点击事件中设置焦点。咱们能够借助点击编辑按钮时都会将ToDoItemEditForm组件从新挂载来解决这个问题。

那到底该如何动手呢? Vue的组件会经验一系列阶段,咱们称之为生命周期。这个生命周期从元素被创立增加到VDOM开始,始终到它们被虚构DOM中被移除完结。

在每个阶段Vue都会有触发的办法,这对于数据获取之类的事件会很有用,比方你须要在组件渲染之前或属性之后获取数据。每个阶段调用的办法如下,按触发程序进行排列:

  1. beforeCreate: 在实例刚在被创立,且未初始化实现时调用。
  2. created: 在实例创立实现后调用。此时实例已实现初始化,然而还没有挂载。
  3. beforeMount: 在挂载开始之前被调用:相干的 render 函数首次被调用。
  4. mounted: 在组件挂载之后调用。此时能够拜访实例上的属性和办法。
  5. beforeUpdate: 在数据更新之前调用。
  6. updated: 在数据更新之后调用。此时能够拜访更新后的 DOM。
  7. beforeDestroy: 在实例销毁之前调用。
  8. destroyed: 在实例销毁之后调用

当初让咱们为ToDoItemEditForm的输入框增加一个ref,如下所示:

<input :id="id" type="text" autocomplete="off" v-model.lazy.trim="newLabel"  ref="labelInput" />

接下来让咱们再导出的script对象中增加一个属性mounted,留神这个属性和计算属性平级,咱们在mounted中获取输入框的ref。

mounted () {    const labelInputRef = this.$refs.labelInput    labelInputRef.focus()}

留神咱们这里并没有应用nextTick,起因在于在Vue的申明周期外面mounted被调用的时候,组件曾经被挂载,咱们能够拜访属性和办法了。

删除的焦点挪动

目前还没有思考的一个问题是,删除的时候焦点应该挪动到哪里,我想此时用户该当关注的是统计信息,也就是还有多少待办。让咱们把眼帘转移到App.vue中的统计信息。咱们还是为统计信息增加上ref。

<h2 id="list-summary" ref="listSummary" tabindex="-1">{{listSummary}}</h2>

当初咱们曾经取得了统计信息的ref,咱们就能够在删除待办的时候,将焦点挪动到这下面:

deleteToDo (toDoId) {  const deleteToDoIndex = this.ToDoItems.findIndex(item => item.id === toDoId)  this.ToDoItems.splice(deleteToDoIndex, 1)  this.$refs.listSummary.focus()}

当初当你删除一个待办,焦点将会被挪动调待办的统计信息上。

总结一下

通过这个例子咱们将(一) (二) (三)的概念串联了起来,在实践中领会实践最为粗浅。依照布局来说,自身打算三篇介绍完Vue的相干理念,然而前面一边学一边发现,一些理念还是在实践中领会比拟深。感激MDN Web Docs 提供的代码示例,十分具体。