资讯专栏INFORMATION COLUMN

vue的生命周期解析并通过表单理解MVVM(不仅理论,图文并茂)

silvertheo / 999人阅读

摘要:在前端页面中,把用纯对象表示,负责显示,两者做到了最大限度的分离。的显示与否和的布尔值有关,还是只关注数据的变化。两个组件的布尔值通过两个临近的按钮控制,初始值和的结果都是。组件的声明在组件上,则完全没有进入生命周期。

开始前说一说 吐槽

首先, 文章有谬误的地方, 请评论, 我会进行验证修改。谢谢。

vue真是个好东西,但vue的中文文档还有很大的改进空间,有点大杂烩的意思,对于怎么把html文件(通过

生命周期参考文章=> vue生命周期探究(一)
生命周期参考文章=> Vue2.0 探索之路——生命周期和钩子函数的一些理解

MVVM

本段内容摘录自廖雪峰老师的MVVM

什么是MVVM?MVVM是Model-View-ViewModel的缩写。

要编写可维护的前端代码绝非易事。我由于前端开发混合了HTML、CSS和JavaScript,而且页面众多,所以,代码的组织和维护难度其实更加复杂,这就是MVVM出现的原因。

在了解MVVM之前,我们先回顾一下前端发展的历史。

用JavaScript在浏览器中操作HTML,经历了若干发展阶段:

第一阶段,直接用JavaScript操作DOM节点,使用浏览器提供的原生API:

var dom = document.getElementById("name");
dom.innerHTML = "Homer";
dom.style.color = "red";

第二阶段,由于原生API不好用,还要考虑浏览器兼容性,jQuery横空出世,以简洁的API迅速俘获了前端开发者的芳心:

$("#name").text("Homer").css("color", "red");

第三阶段,MVC模式,需要服务器端配合,JavaScript可以在前端修改服务器渲染后的数据。

现在,随着前端页面越来越复杂,用户对于交互性要求也越来越高,想要写出Gmail这样的页面,仅仅用jQuery是远远不够的。MVVM模型应运而生。

在前端页面中,把Model用纯JavaScript对象表示,View负责显示,两者做到了最大限度的分离。

把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model。

ViewModel如何编写?需要用JavaScript编写一个通用的ViewModel,这样,就可以复用整个MVVM模型了。

一个MVVM框架和jQuery操作DOM相比有什么区别?

我们先看用jQuery实现的修改两个DOM节点的例子:


Hello, Bart!

You are 12.

Hello, Bart!

You are 12.

用jQuery修改name和age节点的内容:

"use strict";
----
var name = "Homer";
var age = 51;

$("#name").text(name);
$("#age").text(age);
----
// 执行代码并观察页面变化, 请点击本节开头引用链接去该文章原创页面更改会生效。

如果我们使用MVVM框架来实现同样的功能,我们首先并不关心DOM的结构,而是关心数据如何存储。最简单的数据存储方式是使用JavaScript对象:

var person = {
    name: "Bart",
    age: 12
};

我们把变量person看作Model,把HTML某些DOM节点看作View,并假定它们之间被关联起来了。

要把显示的name从Bart改为Homer,把显示的age从12改为51,我们并不操作DOM,而是直接修改JavaScript对象:
Hello, Bart!
You are 12.

"use strict";
----
person.name = "Homer";
person.age = 51;
----
// 执行代码并观察页面变化, 同上原创页面更改

执行上面的代码,我们惊讶地发现,改变JavaScript对象的状态,会导致DOM结构作出对应的变化!这让我们的关注点从如何操作DOM变成了如何更新JavaScript对象的状态,而操作JavaScript对象比DOM简单多了!

这就是MVVM的设计思想:关注Model的变化,让MVVM框架去自动更新DOM的状态,从而把开发者从操作DOM的繁琐步骤中解脱出来!

简易点击开头链接把之后的两节单向绑定和双向绑定看一看,我想你会受益匪浅,廖老师写的很好。

vue发音同view, vue的api, v-model双向数据绑定 => view-model就是MVVM里面的VM, 通过前文可以通俗的理解为:

v-model => viewModel: 视图层(用户看到的界面view)和数据层(Model模型,vue实例中的data,computed,props等都属于数据层)之间相互"映射", 即两者任意一个发生改变都会触发对方的变为一致, 只关注数据(model)的变化

v-if => view-if => if和数据相关, 如果某个数据的结果为true则渲染这个view,否则不渲染, 也是只关注数据(model)的变化

v-show => view-show => view的显示(diplay)与否和show的布尔值有关, 还是只关注数据(model)的变化

v-bind => view-bind => 和view相关的数据与另外一个数据进行绑定, 显示的是绑定的数据对应的view, 还是只关注数据(model)的变化

v-on => view-on => view监听一个事件,即vue实例中对应的方法method, 其实还是通过click等事件,出发数据的改变(data,computed,props),通过数据(model)的变化再反馈给view,还是只关注数据(model)的变化

v-for => view-for => 把一个数组等容器形式存在的数据(model)以for循环的方式来渲染view, 还是只关注数据(model)的变化。

MVVM中的VM => viewModel 实际上实现了生产力的解放, 应用的设计脱离了固有的DOM结构, 而是我有数据(model)我想把数据展示(view)出来,其他人或服务通过看到这个试图(view)就可以获得数据(model),编辑数据,而不用再拘泥于形式(DOM等),vue框架封装好了这些操作,让编程变的高效简洁,大道至简。

在vue实例中的生命周期, 方法method, computed, watch, filter, props等, 都是用来处理数据, 然后"映射"到视图(view)上, 核心就是数据层(Model), 所以, 用vue这个框架来进行前端的页面的模块化编程, 组件实例的作用域是孤立的, 需要解决的就是不同组件(父子组件和非父子组件)之间的通信问题, 来进行数据传递, 而这个过程会往往伴随这组件实例间的切换, 就有老组件实例的销毁和新组件实例的挂载, 理解组件实例的生命周期对于数据能否精准的传递至关重要。

正文 1 v-if 在组件上或组件根元素上生命周期对于数据传递的影响

不知道你有没有碰到过这种情况, 有一个表单并带有增删改查的功能, 那么vue-cli项目构建的方式就会需要两个组件实例, 一个组件Table.vue作为表单部分, 另一个组件Crud.vue作为添加create, research, update, delete的模态框。

那么以MVVM的思想, 增加或修改一行数据, 点击按钮就会用v-if渲染出Crud.vue组件实例, 期间会把Table.vue组件实例的一行数据(data)以正确的组件通信方式传递给Crud.vue组件实例, 该组件实例会把传递过来的数据"映射"到模态框对应的输入框(viuw)中, 然后编辑完成以后再以同样的方式传回数据, Table.vue组件实例就是渲染相应的新数据信息到表单上, 这就模拟完成了一个简单的表单编辑功能。流程图如下:

这个过程模态框组件实例Crud.vue因为v-if的不同声明位置而经历不一样的生命周期, 这会导致Crud.vue
的需要在不同的状态下才能接收到数据。验证demo如下:

首先, 新建一个index.html文件(章节末尾有完整代码), 在html文件中注册两个全局组件
my-component-one

template: "
A component!
"

my-component-two

template: "
A component!
"

且都加上了如下代码(命名为组件生命周期测试代码组):

beforeRouteEnter(to, from, next) {
        console.log(this) //undefined,不能用this来获取vue实例
        console.log("组件路由勾子:beforeRouteEnter")
        next(vm => {
          console.log(vm) //vm为vue的实例
          console.log("组件路由勾子beforeRouteEnter的next")
        })
      },
      beforeCreate() {
        console.log("组件:beforeCreate")
        console.log("%c%s", "color:red", "el     : " + this.$el); //undefined
        console.log("%c%s", "color:red", "data   : " + this.$data); //undefined 
        console.log("%c%s", "color:red", "message: " + this.message)
      },
      created() {
        this.$nextTick(() => {
          console.log("nextTick")
        })
        console.log("组件:created")
        console.log("%c%s", "color:red", "el     : " + this.$el); //undefined
        console.log("%c%s", "color:red", "data   : " + this.$data); //已被初始化 
        console.log("%c%s", "color:red", "message: " + this.message); //已被初始化
      },
      beforeMount() {
        console.log("组件:beforeMount")
        console.log("%c%s", "color:red", "el     : " + (this.$el)); //已被初始化
        console.log("%c%s", "color:red", "data   : " + this.$data); //已被初始化  
        console.log("%c%s", "color:red", "message: " + this.message); //已被初始化
      },
      mounted() {
        console.log("组件:mounted")
      },
      beforeUpdate() {
        console.log("beforeUpdate")
      },
      updated() {
        console.log("updated")
      },
      beforeDestroy: function() {
        console.log("beforeDestroy 销毁前状态===============》");
        console.log("%c%s", "color:red", "el     : " + this.$el);
        console.log("%c%s", "color:red", "data   : " + this.$data);
        console.log("%c%s", "color:red", "message: " + this.message);
      },
      destroyed: function() {
        console.log("destroyed 销毁完成状态===============》");
        console.log("%c%s", "color:red", "el     : " + this.$el);
        console.log("%c%s", "color:red", "data   : " + this.$data);
        console.log("%c%s", "color:red", "message: " + this.message)
      },
      beforeRouteLeave(to, from, next) {
        console.log(this) //可以访问vue实例
        console.log("组件路由勾子:beforeRouteLeave")
        next()
      }

然后声明组件

  

组件my-component-one的v-if声明在组件上, 对应按钮IfOnComponent

组件my-component-two的v-if声明在组件根元素上。对应按钮IfOnRootElement

两个组件的布尔值通过两个临近的按钮控制(toggle),v-if初始值activeOneactiveTwo的结果都是flase

打开index.html, F12打开开发者工具。页面刚加载时控制台如下图:

如图所示, 可以看到在v-if声明在组件根元素上, 初始值为false, 页面加载时该组件my-component-two的生命周期会处于mounted挂载状态, 但是没有被渲染在DOM节点树上。 组件my-component-onev-if声明在组件上, 则完全没有进入生命周期。

情况1 v-if声明在组件根元素上

点击按钮IfOnRootElement, 效果如下图红框部分:

如图所示, 点击按钮后activeTwo值的改变成true, 而触发beforeUpdate => updated

再点击按钮IfOnRootElement, 效果如下图红框部分:

如图所示, 再次点击按钮后activeTwo值的变回false, 又触发beforeUpdate => updated

所以,v-if声明在组件根元素上。 组件实例数据的改变会触发beforeUpdate => updated

情况2 v-if声明在组件上

页面刚加载时, my-component-one没有进入生命周期, 先清空控制台, 点击按钮IfOnComponent, 效果如下图红框部分:

如图所示, 点击按钮后activeOne值的改变成true, 而触发beforeCreate => create => beforeMount => Mounted, 组件实例挂载并被渲染到DOM节点树中。

再点击按钮IfOnComponent, 效果如下图红框部分:

如图所示, 再次点击按钮后activeOne值的变回false, 触发beforeDestroy => destroyed, 组件不会触发beforeUpdate => updated而直接进入销毁。

所以,v-if声明在组件上。 组件实例与v-if相关数据的改变不会触发beforeUpdate => updated

本demo完整代码如下:





  
  
  vue-lifecycle
  



  

这个demo写下来我们需要下面两条结论, 来作为接下来实际vue-cli项目的支持.
所以,v-if声明在组件根元素上。 组件实例数据的改变会触发beforeUpdate => updated
所以,v-if声明在组件上。 组件实例与v-if相关数据的改变不会触发beforeUpdate => updated

2 表单中v-if在组件上或组件根元素上生命周期对于数据传递的影响

我在章节1的开头说过表单会面临的问题, 不记得同学可以回去看.

如果有vue init webpck my-project 并(cnpm install)安装好依赖模块的项目, 建议在干净的项目上直接拷贝下文的代码, 否则建议直接点击github链接clone项目或者下载压缩包.并按步骤启动项目, 经过测试, 能顺利运行.

本章github源码链接

目录结构

vue init webpck my-project 构建完成项目以后,
cd my-project,

npm installcnpm install

安装完成后, 先在srcassets文件夹下创建了css文件夹并在里面编写了需main.css, 代码如下:
(先不要处理细节, copy代码先让项目能运行, 本文的主要关注点是生命周期和MVVM, 对于不知道怎么把html文件中

Table.vue:





Crud.vue:





最后后修改了router目录下的index.js文件, 代码如下:
index.js:

import Vue from "vue"
import Router from "vue-router"
import HelloWorld from "@/components/HelloWorld"
import Table from "@/components/Table"

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: "/helloWorld",
      name: "HelloWorld",
      component: HelloWorld
    },
    {
      path: "/",
      name: "Table",
      component: Table
    }
  ]
})

这里插一句, 分配路由的时候, 每个name对应字符串, 会出现在F12的vue devtools的组建实例结构中, 如下图所示.

组件是组件, 其子组件是就是,

表单组件是的路由组件, 是编辑模态框存在表单中.

export default {
  name: "Crud",
  data() {
    ...
  },

如上图所示, 由于Crud.vue定义的name是Crud所以在dev tools中显示的是, 类似的,

等组件的组件名字都可以自定义, 最好使命名能有自描述性.

my-project项目目录的根目录, 输入命令npm start即可在本地8080打开项目.

如上组件层级图所示, 这个项目一共三个组件来模拟表单操作。即:

App.vue项目自带组件, 显示一张Vue的图片, 路由的根组件。
Table.vue是表单组件。如下图所示:

Crud.vue是一个编辑模态框如下图所示:

现在我们应该对模块化表单组件结构有了一定理解了, 先无视代码细节, 来关注和第一章一样的v-if声明位置对于组件生命周期的影响,以表单的编辑功能为例。

思考一下, 现在需要一个具有增删改查功能的表单, 有表格主体, 有一个能添加一列信息的表格, 有一个能添加新信息和编辑修改信息模态框, 还有删除按钮, 和搜索框。

为了编写可复用的组件, 把一个简易的表单分成两个组件, 一个是展示表单信息的表格Table.vue, 还有一个模态框Crud.vue, 当需要删除一行数据的时候删除对应行的数据(model), 搜索框忽略, 增加和编辑信息需要用到模态框, 那么就涉及组件之间的通信问题来传递数据(model), 那么问题来了。

这两个组件的组合方式是同级组件(非父子组件方式通信), 还是父子组件呢? 这你可以自行设计。

本项目采用的父子组件的方式来组合两个组件, 即在Table.vue中有如下代码:

引入Crud.vue


声明该组件到模板中



这样写Crud.vue就是Table.vue的子组件。

Table.vue初始渲染数据(假设是从数据库取出来的)如下:

打开项目显示如下:

情况1 v-if声明在组件根元素上

还记得之前的结论么, v-if声明在组件根元素上, 组件会在页面渲染完成后处于mounted状态!

让我们来看一看打开项目的初始生命周期:

页面的渲染顺序是, 所有组件先从最高父级组件至最低子组件都先经历beforeCreate => created => beforeMount => 暂停, 下一个组件! (App.vue => Table.vue => Crud.vue)

当最后一个子组件达到beforeMount,然后全部组件以相反的顺序进入挂载状态mounted。 (Crud.vue => Table.vue => App.vue)

事件循环的顺序(nextTick)是父 => 子

了解nextTick

现在我想要编辑第一行学生名字叫做李明的数据, 点击编辑按钮拿到李明的数据, 要怎么唤醒组件Crud然后传递李明的数据进行编辑呢?

记住, Props向下传递, 事件向上传递

Crud声明




Table通过v-bind命令传递数据给子组件



注意, HTML 特性是不区分大小写的。所以,当使用的不是字符串模板时,camelCase (驼峰式命名) 的 prop 需要转换为相对应的 kebab-case (短横线分隔式命名)。 例如, html标签上的modify-list等价于props中的"modifyList"。

点击编辑, 拿到李明对象数据之后赋值给selectedList, 父组件的selectedList发生了变化, 且isActive变化为true, v-bind 来动态地将 prop(modifyList, isActive) 绑定到父组件的数据。每当父组件的数据变化时,该变化也会传导给子组件Crud.vue, 子组件有段代码this.list = this.modifyList, list是和模态框输入框input进行双向绑定v-model的数据。

子组件的isAcitve布尔值转换为true, 模态框显示出来。

重点来了, 假设我们不了解vue实例的生命周期, 不了解不同v-if声明位置的生命周期, 那么Crud.vue组件接收到的李明对象selectedListisActive的数据该在什么哪个生命周期进行赋值呢, created,mounted 还是updated, 一个一个的试? 这对于高效简洁的编程是有阻碍的, 在这个思维胡乱的过程中, 怎么能写出模块化高的程序?怎么保证程序没有bug?

好了, 现在我们知道了v-if声明在组件根元素上在打开页面时就出在生命周期的mounted状态, 如果在created或mounted之前的生命周期会接收不到Table父组件传递的数据。 list为空对象, 所以视图显示如下:

mounted之后的生命周期beforeUpdate和updated可以接收到数据。视图显示如下:

然后点击保存按钮, 把Crud组件修改的数据list通过$emit事件发出数据, 父组件Table通过$on监听并接收收到的数据list, Table组件根据list中的名字更新相应行的数据(model), 然后通过双向绑定v-model把更新的树反应在表格视图中。

Crud.vue:

modify() {
      this.$emit("edit", this.list);
    }
Table.vue:



然后随意修改数据,如下图

注意到, 表格中的用户名也跟着变化。 再来1张图:

关键的是, 点击美容会被修改:

这是因为在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。

目前的解决方法是克隆一个内存空间中新生成对象李明, 在进行数据传递就不会出现问题。

this.selectedList = JSON.parse(JSON.stringify(this.students[index]))

即先把组件Table的李明对象转成JSON字符串, 在转回来, 达到"克隆"。 还有其他方法么? 欢饮讨论。

问题(待解决): 在vue的编码规范中有如下声明:

传递过于复杂的对象使得我们不能够清楚的知道哪些属性或方法被自定义组件使用,这使得代码难以重构和维护。

所以, 我考虑Table.vue传递李明的字符串, edit()方法修改如下:

edit(index) {
   this.selectedList = JSON.stringify(this.students[index]);
   this.toggleEdit();
}

然后再在Crud.vue中解析成对象, 修改如下:

updated() {
  this.list = JSON.parse(this.modifyList);
}

此时点击一行数据进行编辑, 浏览器会进入死循环, 卡死。 解析放到beforeupdate

beforeupdate() {
  this.list = JSON.parse(this.modifyList);
}

点击编辑, 循坏100来此报错:

哪位大神能详尽的解释一下么? updated状态下进行解析生成新对象, 组件Crud.vue又会进入beforeUpdate => updated状态又成新解析的对象, 无限循环直到内存溢出, 那么为什么解析放在updated中回掉浏览器器会直接卡死, 而beforeUpdated中递归会中止并报错?

情况2 v-if声明在组件上

还要删除组件Crud.vue根组件,

元素标签上的v-if="isActive"

那么还记得之前的结论么, v-if声明在组件上, 组件会在v-iftrue后页面才开始渲染显示到DOM节点树, 并显示相应的视图, 且组件关闭后后被销毁

让我们来看一看打开项目的初始生命周期:

可以看到没有组件Crud.vue的控制台数据。

现在, 传递的数据给list可以放在created beforeMount mounted任一生命周期中, 项目就能顺利运行, 但是不能放在beforeUpdate updated生命周期中, 因为数据动态绑定的关系, 组件Crud.vue渲染到生命周期的created状态时props中定义的数据就已经被传递数据并赋值了, 所以点击编辑打开模态框并不会触发beforeUpdate updated的生命周期, 在里面进行list是赋值行为是没有意义的。

现在可以遵守规范, 让组件Table.vue可以传递李明的字符串, 再在Crud.vue中解析成对象, 因为上述原理, 解析的新对象不会递归出发组件beforeUpdate updated的生命周期而进入死循环。 为了理解更深你不妨试试。

vue组件实例的生命周期还有需要探究的地方。

本章github源码链接

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93012.html

相关文章

  • Vue面试中,经常会被问到面试题/知识点(2019改进版)

    摘要:在第一版的基础上进行了优化,新增一些面试题知识点,对一些知识点进行更加深入的描述。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。改变中的状态的唯一途径就是显式地提交。这两个可以在不进行刷新的情况下,操作浏览器的历史纪录。 在第一版的基础上进行了优化,新增一些面试题/知识点,对一些知识点进行更加深入的描述。 一、对于MVVM的理解? MVVM 是 Model-View-Vie...

    singerye 评论0 收藏0
  • vue常用知识点总结

    摘要:这里借鉴了一下的处理方式,我们把单独模块的包装成一个函数,提供一个全局的回调方法,加载完成时候再调用回调函数。 感谢本文引用链接的各位大佬们,小菜鸟我只是个搬运工 1.谈一谈你理解的vue是什么样子的? vue是数据、视图分离的一个框架,让数据与视图间不会发生直接联系。MVVM 组件化:把整体拆分为各个可以复用的个体 数据驱动:通过数据变化直接影响bom展示,避免dom操作。 可以在...

    xiaokai 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<