开始前说一说
吐槽
首先, 文章有谬误的地方, 请评论, 我会进行验证修改。谢谢。
vue真是个好东西,但vue的中文文档还有很大的改进空间,有点大杂烩的意思,对于怎么把html文件(通过<script>引入vue)简单项目移植成vue-cli(vue init webpack my-project
)构建的模块化组件项目言之甚少,理解了vue的生命周期,才能高效简洁的编写组件间的数据通信代码,这是写vue程序极为重要的关键性的概念也没有官方的demo。
我之前看过facebook发行的Gytsby
这个static-gen
的英文文档,整个tutorial一气呵成,且文章用词把握在3000英文单词左右,新闻撰稿的级别易懂且印象深刻,说实话当时我在看这个文档的时候是欲罢不能了,技术文档越看越爽。
其实我想说vue是个很好的框架,发展到现在更需要的是传播力,技术只有易于传承才能产生更多的生产力。
鉴于之前vue的更新工作主要是尤大一个人作为主力,之后希望能改善vue的官方文档,有循序渐进的demo辅助理解技术,能让新手更顺畅的学习vue, 我个人每次对vue, MVVM理解的提升基本都不是看的vue官方文档,而是浏览大量开源社区的文章和项目实践达到的。
这篇文章本文旨在结合简易项目,说清vue组件生命周期,让新人能快速的理解mvvm和vue的生命周期,跳过那个迷茫前进的过程。
核心概念
- 你需要明白Vue 组件都是 Vue 实例
- 官方vue生命周期配图
生命周期参考文章=> 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节点的例子:
<!-- HTML -->
<p>Hello, <span id="name">Bart</span>!</p>
<p>You are <span id="age">12</span>.</p>
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: ‘<div>A component!</div>‘
my-component-two
。
template: ‘<div v-if="isActive">A component!</div>‘
且都加上了如下代码(命名为组件生命周期测试代码组):
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()
}
然后声明组件
<div id="app" style="float: right;">
<my-component-one v-if="activeOne"></my-component-one>
<button v-on:click="toggleOne">IfOnComponent</button>
<hr>
<my-component-two v-bind:is-active="activeTwo"></my-component-two>
<button v-on:click="toggleTwo">IfOnRootElement</button>
</div>
组件my-component-one
的v-if声明在组件上, 对应按钮IfOnComponent
。
组件my-component-two
的v-if声明在组件根元素上。对应按钮IfOnRootElement
。
两个组件的布尔值通过两个临近的按钮控制(toggle),v-if
初始值activeOne
和activeTwo
的结果都是flase
。
打开index.html, F12打开开发者工具。页面刚加载时控制台如下图:
如图所示, 可以看到在v-if
声明在组件根元素上, 初始值为false
, 页面加载时该组件my-component-two
的生命周期会处于mounted挂载状态, 但是没有被渲染在DOM节点树上。 组件my-component-one
的v-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完整代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>vue-lifecycle</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="app" style="float: right;">
<my-component-one v-if="activeOne"></my-component-one>
<button v-on:click="toggleOne">IfOnComponent</button>
<hr>
<my-component-two v-bind:is-active="activeTwo"></my-component-two>
<button v-on:click="toggleTwo">IfOnRootElement</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
// 注册
Vue.component(‘my-component-one‘, {
name: "2",
template: ‘<div>A component!</div>‘,
beforeRouteEnter(to, from, next) {
console.log(‘组件:my-component-one‘)
console.log(this) //undefined,不能用this来获取vue实例
console.log(‘组件路由勾子:beforeRouteEnter‘)
next(vm => {
console.log(vm) //vm为vue的实例
console.log(‘组件路由勾子beforeRouteEnter的next‘)
})
},
beforeCreate() {
console.log(‘组件:my-component-one‘)
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(‘组件:my-component-one‘)
console.log(‘nextTick‘)
})
console.log(‘组件:my-component-one‘)
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(‘组件:my-component-one‘)
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(‘组件:my-component-one‘)
console.log(‘mounted‘)
},
beforeUpdate() {
console.log(‘组件:my-component-one‘)
console.log(‘beforeUpdate‘)
},
updated() {
console.log(‘组件:my-component-one‘)
console.log(‘updated‘)
},
beforeDestroy: function() {
console.log(‘组件:my-component-one‘)
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(‘组件:my-component-one‘)
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(‘组件:my-component-one‘)
console.log(this) //可以访问vue实例
console.log(‘组件路由勾子:beforeRouteLeave‘)
next()
}
})
Vue.component(‘my-component-two‘, {
name: "1",
template: ‘<div v-if="isActive">A component!</div>‘,
props: ["isActive"],
beforeRouteEnter(to, from, next) {
console.log(‘组件:my-component-two‘)
console.log(this) //undefined,不能用this来获取vue实例
console.log(‘组件路由勾子:beforeRouteEnter‘)
next(vm => {
console.log(vm) //vm为vue的实例
console.log(‘组件路由勾子beforeRouteEnter的next‘)
})
},
beforeCreate() {
console.log(‘组件:my-component-two‘)
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(‘组件:my-component-two‘)
console.log(‘nextTick‘)
})
console.log(‘组件:my-component-two‘)
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(‘组件:my-component-two‘)
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(‘组件:my-component-two‘)
console.log(‘mounted‘)
},
beforeUpdate() {
console.log(‘组件:my-component-two‘)
console.log(‘beforeUpdate‘)
},
updated() {
console.log(‘组件:my-component-two‘)
console.log(‘updated‘)
},
beforeDestroy: function() {
console.log(‘组件:my-component-two‘)
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(‘组件:my-component-two‘)
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(‘组件:my-component-two‘)
console.log(this) //可以访问vue实例
console.log(‘组件路由勾子:beforeRouteLeave‘)
next()
}
})
var vm = new Vue({
el: "#app",
data: {
activeOne: false,
activeTwo: false,
},
methods: {
toggleOne() {
this.activeOne = !this.activeOne;
},
toggleTwo() {
this.activeTwo = !this.activeTwo;
}
},
})
</script>
</body>
</html>
这个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项目或者下载压缩包.并按步骤启动项目, 经过测试, 能顺利运行.
目录结构
或vue init webpck my-project
构建完成项目以后, cd my-project
,
npm install
或cnpm install
安装完成后, 先在src\assets
文件夹下创建了css
文件夹并在里面编写了需main.css
, 代码如下:
(先不要处理细节, copy代码先让项目能运行, 本文的主要关注点是生命周期和MVVM, 对于不知道怎么把html文件中<script>
引入vue完成的非脚手架项目移植到vue-cli
脚手架上去的同学, 可以参考代码结构)
main.css
:
[v-cloak] {
display: none;
}
table {
border: 1px solid #ccc;
padding: 0;
border-collapse: collapse;
table-layout: fixed;
margin-top: 10px;
width: 100%;
}
table td,
table th {
height: 30px;
border: 1px solid #ccc;
background: #fff;
font-size: 15px;
padding: 3px 3px 3px 8px;
overflow: hidden;
}
table th:first-child {
width: 30px;
}
.container,
.st {
width: 100%;
margin: 10px auto 0;
font-size: 13px;
font-family: "Microsoft YaHei";
}
.container .search {
font-size: 15px;
padding: 4px;
}
.container .add {
padding: 5px 15px;
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 6;
background: rgba(0, 0, 0, 0.7);
}
.overlay td:first-child {
width: 66px;
}
.overlay .con {
position: absolute;
width: 420px;
min-height: 300px;
background: #fff;
left: 50%;
top: 50%;
-webkit-transform: translate3d(-50%, -50%, 0);
transform: translate3d(-50%, -50%, 0);
/*margin-top: -150px;*/
padding: 20px;
}
然后修改App.vue并在src\component
目录下编写了组件Table.vue
和Crud.vue
, 代码如下:
App.vue
:
<template>
<div id="app">
<img src="./assets/logo.png">
<router-view/>
</div>
</template>
<script>
export default {
name: "App",
beforeRouteEnter(to, from, next) {
console.log("组件路由勾子:beforeRouteEnter, 下一行打印自身this");
console.log(this); //undefined,不能用this来获取vue实例
next(vm => {
console.log("组件路由勾子beforeRouteEnter的next");
console.log(vm); //vm为vue的实例
});
},
beforeCreate() {
console.log("beforeCreate");
},
created() {
this.$nextTick(() => {
console.log("nextTick");
});
console.log("created");
},
beforeMount() {
console.log("beforeMount");
},
mounted() {
console.log("mounted");
},
beforeUpdate() {
console.log("beforeUpdate");
},
updated() {
console.log("updated");
},
beforeDestroy: function() {
console.log("beforeDestroy 销毁前状态===============》");
},
destroyed: function() {
console.log("destroyed 销毁完成状态===============》");
},
beforeRouteLeave(to, from, next) {
console.log("组件路由勾子:beforeRouteLeave, 下一行打印自身this");
console.log(this); //可以访问vue实例
next();
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
Table.vue
:
<template>
<div class="container">
<input type="button" value="新增" class="add" @click="add">
<div>
<table>
<thead>
<tr>
<th v-for="head in header" v-bind:key="head.id" v-cloak>{{ head }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(student, index) in students" v-bind:key="student.index" v-cloak>
<td>{{index+1}}</td>
<td v-for="value in student" v-bind:key="value.id">{{ value.toString() }}</td>
<td><button v-on:click="edit(index)">修改</button><button @click="del(index)">删除</button></td>
</tr>
</tbody>
</table>
</div>
<!-- 外部组件,作为子组件,与Crud组件的关系是父子组件关系, modifylist用kebab-case(短横线分隔式命名),因为
HTML 特性是不区分大小写的。所以,当使用的不是字符串模板时,camelCase (驼峰式命名) 的 prop 需要转换为相对应的 kebab-case (短横线分隔式命名): -->
<crud class="just-try-something-and-press-f12" data-would-be-add-to-root-component-attr="true" v-bind:modify-list="selectedList" v-bind:is-active="isActive" v-on:edit="editOne" v-on:cancel="foobar = arguments[0];isActive=!isActive" v-cloak></crud>
<!-- class="just-try-something-and-press-f12" data-would-be-add-to-root-component-attr="true"
这两段话是为了验证组件标签上编写的的非prop特性会被添加到组件到组件的根元素上去, 和表单无关.
foobar = arguments[0] 是为了验证子组件通过$emit与父组件通信时接受参数的形式, 尝试直接把$on监听到的数据直接赋值给父组件的model, 打开F12 vue devtool可以看到能成功-->
</div>
</template>
<script>
// https://www.xiabingbao.com/vue/2017/07/10/vue-curd.html
import Crud from "./Crud";
export default {
name: "Table",
components: { Crud },
data() {
return {
foobar: 0,
selectedList: {},
isActive: false,
header: ["id", "用户名", "email", "性别", "省份", "爱好", "编辑"],
students: [
{
username: "李明",
email: "li@qq.com",
sex: "男",
province: "北京市",
hobby: ["篮球", "编程"]
},
{
username: "韩红",
email: "han@163.com",
sex: "女",
province: "河北省",
hobby: ["弹琴", "插画"]
}
]
};
},
filters: {
filter: function(array) {
// 数组的toString方法能输出
return array.toString();
}
},
methods: {
del(index) {
this.students.splice(index, 1);
},
add() {
var init = {
username: "",
email: "",
sex: "",
province: "",
hobby: []
};
this.selectedList = JSON.parse(JSON.stringify(init));
this.toggleEdit();
},
editOne(student) {
var isduplicated;
// 为了简单, 添加和编辑都是用这个方法, 判断保存的username是否重复, 如果重复则覆盖, 不重复则新建一行,通过isduplicated如果为true来判断.
isduplicated = this.students.some(aStudent => {
return aStudent.username === student.username;
});
console.log(isduplicated);
if (isduplicated) {
this.students.forEach((element, index) => {
if (student.username == element.username) {
this.students[index] = student;
}
});
} else {
this.students.push(student);
}
this.toggleEdit();
},
cancleOne() {
this.toggleEdit();
},
edit(index) {
// 一定要分别尝试这两断代码的区别!!! 两端代码要做同一件事就是传递表单的对象给子组件
// 下面这一句通过转换以后, 传递的selectedList对象是持有内存中新的引用的student
this.selectedList = JSON.parse(JSON.stringify(this.students[index]));
// 而下面这句持有的引用和表单中的student的引用是一致的, 即传递给子组件的student对象在进行属性编辑的时候, 也会实时改变父组件表单中的内容, 就算点击取消按钮也不会还原修改, 运行实践.
// this.selectedList = this.students[index]
this.toggleEdit();
},
toggleEdit() {
this.isActive = !this.isActive;
}
},
beforeRouteEnter(to, from, next) {
console.log("组件路由勾子:beforeRouteEnter, 下一行打印自身this");
console.log(this); //undefined,不能用this来获取vue实例
next(vm => {
console.log("组件路由勾子beforeRouteEnter的next");
console.log(vm); //vm为vue的实例
});
},
beforeCreate() {
console.log("beforeCreate");
},
created() {
this.$nextTick(() => {
console.log("nextTick");
});
console.log("created");
},
beforeMount() {
console.log("beforeMount");
},
mounted() {
console.log("mounted");
},
beforeUpdate() {
console.log("beforeUpdate");
},
updated() {
console.log("updated");
},
beforeDestroy: function() {
console.log("beforeDestroy 销毁前状态===============》");
},
destroyed: function() {
console.log("destroyed 销毁完成状态===============》");
},
beforeRouteLeave(to, from, next) {
console.log("组件路由勾子:beforeRouteLeave, 下一行打印自身this");
console.log(this); //可以访问vue实例
next();
}
};
</script>
<style>
@import "../assets/css/main.css";
</style>
Crud.vue
:
<template>
<div class="overlay" v-if="isActive">
<div class="con">
<h2 class="title">新增 | 修改</h2>
<div class="content">
<table>
<tr>
<td>用户名</td>
<td><input type="text" v-model="list.username"></td>
</tr>
<tr>
<td>邮箱</td>
<td><input type="text" v-model="list.email"></td>
</tr>
<tr>
<td>性别</td>
<td>
<label><input type="radio" name="sex" value="男" v-model="list.sex">男</label>
<label><input type="radio" name="sex" value="女" v-model="list.sex">女</label>
<label><input type="radio" name="sex" value="未知" v-model="list.sex">未知</label>
</td>
</tr>
<tr>
<td>省份</td>
<td>
<select name="" id="" v-model="list.province">
<option value="北京市">北京市</option>
<option value="河北省">河北省</option>
<option value="河南省">河南省</option>
<option value="重庆市">重庆市</option>
<option value="广东省">广东省</option>
<option value="辽宁省">辽宁省</option>
</select>
</td>
</tr>
<tr>
<td>爱好</td>
<td>
<label><input type="checkbox" v-model="list.hobby" value="篮球">篮球</label>
<label><input type="checkbox" v-model="list.hobby" value="读书">读书</label>
<label><input type="checkbox" v-model="list.hobby" value="插画">插画</label>
<label><input type="checkbox" v-model="list.hobby" value="编程">编程</label>
<label><input type="checkbox" v-model="list.hobby" value="弹琴">弹琴</label>
</td>
</tr>
</table>
<p>
<input type="button" @click="cancelModify" value="取消">
<input type="button" @click="modify" value="保存">
</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Crud",
data() {
return {
list: {}
};
},
methods: {
cancelModify() {
this.$emit("cancel", 123);
},
modify() {
this.$emit("edit", this.list);
}
},
props: ["modifyList", "isActive"],
computed: {
selectedList: function() {
return this.modifyList;
}
},
updated() {
console.log("updated");
this.list = this.modifyList;
},
beforeRouteEnter(to, from, next) {
console.log("组件路由勾子:beforeRouteEnter");
console.log(this); //undefined,不能用this来获取vue实例
next(vm => {
console.log("组件路由勾子beforeRouteEnter的next");
console.log(vm); //vm为vue的实例
});
},
beforeCreate() {
console.log("beforeCreate");
},
created() {
this.$nextTick(() => {
console.log("nextTick");
});
console.log("created");
},
beforeMount() {
console.log("beforeMount");
},
mounted() {
console.log("mounted");
},
beforeUpdate() {
console.log("beforeUpdate");
},
beforeDestroy: function() {
console.log("beforeDestroy 销毁前状态===============》");
},
destroyed: function() {
console.log("destroyed 销毁完成状态===============》");
},
beforeRouteLeave(to, from, next) {
console.log("组件路由勾子:beforeRouteLeave");
console.log(this); //可以访问vue实例
next();
}
};
</script>
<style>
</style>
最后后修改了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的组建实例结构中, 如下图所示.
<Root>组件是组件, 其子组件是<App>就是, <Table>表单组件是<App>的路由组件, <Crud>是编辑模态框存在表单中.
export default {
name: "Crud",
data() {
...
},
如上图所示, 由于Crud.vue
定义的name
是Crud所以在dev tools中显示的是<Crud>, 类似的<App>, <Table>等组件的组件名字都可以自定义, 最好使命名能有自描述性.
my-project
项目目录的根目录, 输入命令npm start
即可在本地8080
打开项目.
如上组件层级图所示, 这个项目一共三个组件来模拟表单操作。即:
App.vue
项目自带组件, 显示一张Vue的图片, 路由的根组件。Table.vue
是表单组件。如下图所示:
Crud.vue
是一个编辑模态框如下图所示:
现在我们应该对模块化表单组件结构有了一定理解了, 先无视代码细节, 来关注和第一章一样的v-if
声明位置对于组件生命周期的影响,以表单的编辑功能为例。
思考一下, 现在需要一个具有增删改查功能的表单, 有表格主体, 有一个能添加一列信息的表格, 有一个能添加新信息和编辑修改信息模态框, 还有删除按钮, 和搜索框。
为了编写可复用的组件, 把一个简易的表单分成两个组件, 一个是展示表单信息的表格Table.vue
, 还有一个模态框Crud.vue
, 当需要删除一行数据的时候删除对应行的数据(model), 搜索框忽略, 增加和编辑信息需要用到模态框, 那么就涉及组件之间的通信问题来传递数据(model), 那么问题来了。
这两个组件的组合方式是同级组件(非父子组件方式通信), 还是父子组件呢? 这你可以自行设计。
本项目采用的父子组件的方式来组合两个组件, 即在Table.vue
中有如下代码:
引入Crud.vue
<script>
import Crud from "./Crud";
export default {
...
components: { Crud },
...
}
</script>
声明该组件到模板中
<template>
<div class="container">
...
...
<crud ...></crud>
</div>
</template>
这样写Crud.vue就是Table.vue的子组件。
Table.vue初始渲染数据(假设是从数据库取出来的)如下:
<script>
export default {
name: "Table",
components: { Crud },
data() {
return {
selectedList: {},
isActive: false,
header: ["id", "用户名", "email", "性别", "省份", "爱好", "编辑"],
students: [
{
username: "李明",
email: "li@qq.com",
sex: "男",
province: "北京市",
hobby: ["篮球", "编程"]
},
{
username: "韩红",
email: "han@163.com",
sex: "女",
province: "河北省",
hobby: ["弹琴", "插画"]
}
]
};
},
...
...
}
</script>
打开项目显示如下:
情况1 v-if
声明在组件根元素上
<template>
<div ... v-if="isActive">
...
...
</div>
</template>
还记得之前的结论么, v-if
声明在组件根元素上, 组件会在页面渲染完成后处于mounted
状态!
让我们来看一看打开项目的初始生命周期:
页面的渲染顺序是, 所有组件先从最高父级组件至最低子组件都先经历beforeCreate => created => beforeMount =>
暂停, 下一个组件! (App.vue => Table.vue => Crud.vue
)
当最后一个子组件达到beforeMount
,然后全部组件以相反的顺序进入挂载状态mounted
。 (Crud.vue => Table.vue => App.vue
)
事件循环的顺序(nextTick)是父 => 子
现在我想要编辑第一行学生名字叫做李明的数据, 点击编辑按钮拿到李明的数据, 要怎么唤醒组件Crud
然后传递李明的数据进行编辑呢?
记住, Props向下传递, 事件向上传递。
Crud声明
<script>
export default {
...
props: ["modifyList", "isActive"],
...
}
</script>
Table通过v-bind命令传递数据给子组件
<template>
<div class="container">
...
...
<crud ...v-bind:modify-list="selectedList" v-bind:is-active="isActive"...></crud>
</div>
</template>
<script>
export default {
...
selectedList: {},
isActive: false
...
}
</script>
注意, 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
的数据。
<script>
export default {
name: "Crud",
data() {
return {
list: {}
};
},
...
...
}
</script>
子组件的isAcitve
布尔值转换为true
, 模态框显示出来。
重点来了, 假设我们不了解vue实例的生命周期, 不了解不同v-if
声明位置的生命周期, 那么Crud.vue
组件接收到的李明对象selectedList
和isActive
的数据该在什么哪个生命周期进行赋值呢, 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:
<template>
...
<crud ... v-on:edit="editOne"...></crud>
</template>
<script>
...
editOne(student) {
var isduplicated;
// 为了简单, 添加和编辑都是用这个方法, 判断保存的username是否重复, 如果重复则覆盖, 不重复则新建一行,通过isduplicated如果为true来判断.
isduplicated = this.students.some(aStudent => {
return aStudent.username === student.username;
});
console.log(isduplicated);
if (isduplicated) {
this.students.forEach((element, index) => {
if (student.username == element.username) {
this.students[index] = student;
}
});
} else {
this.students.push(student);
}
this.toggleEdit();
}
<script>
...
那么有人要问了, 可不可以把代码该成这样:
v-model="list.username" => v-model="modifyList.username"
v-model="list.email" => v-model="modifyList.email"
v-model="list.sex" => v-model="modifyList.sex"
v-model="list.province" => v-model="modifyList.province"
v-model="list.hobby" => v-model="modifyList.hobby"
这样就不用理会生命周期, 直接显示并修改prop
数据modifyList
, 并返回给父组件。 首先, 这样在技术上时可行的, 不会出现问题。 但是, Prop 是单向绑定的, 不应该在子组件内部改变 prop, 请遵守规范。
单向数据流
Prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是反过来不会。这是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。
另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop。如果你这么做了,Vue 会在控制台给出警告。
在两种情况下,我们很容易忍不住想去修改 prop 中数据:
Prop 作为初始值传入后,子组件想把它当作局部数据来用;
Prop 作为原始数据传入,由子组件处理成其它数据输出。
对这两种情况,正确的应对方式是:
......
......
以上单向数据流内容, 来自官方文档组件章节。
还有一个组件间传递的数据为对象的问题, 当点击编辑按钮时, 以这句代码this.selectedList = this.students[index]
传递李明的对象数据, 表单会出现一个问题, 点击修改李明行的按钮, 情况正常, 如下图:
然后随意修改数据,如下图
注意到, 表格中的用户名也跟着变化。 再来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
声明在组件上
<template>
<div class="container">
...
...
<crud ...v-if="isActive"...></crud>
</div>
</template>
还要删除组件Crud.vue
根组件, <div>
元素标签上的v-if="isActive"
。
那么还记得之前的结论么, v-if声明在组件上, 组件会在v-if
为true
后页面才开始渲染显示到DOM
节点树, 并显示相应的视图, 且组件关闭后后被销毁
让我们来看一看打开项目的初始生命周期:
可以看到没有组件Crud.vue
的控制台数据。
现在, 传递的数据给list
可以放在created beforeMount mounted
任一生命周期中, 项目就能顺利运行, 但是不能放在beforeUpdate updated
生命周期中, 因为数据动态绑定的关系, 组件Crud.vue
渲染到生命周期的created
状态时props
中定义的数据就已经被传递数据并赋值了, 所以点击编辑打开模态框并不会触发beforeUpdate updated
的生命周期, 在里面进行list
是赋值行为是没有意义的。
现在可以遵守规范, 让组件Table.vue可以传递李明的字符串, 再在Crud.vue中解析成对象, 因为上述原理, 解析的新对象不会递归出发组件beforeUpdate updated
的生命周期而进入死循环。 为了理解更深你不妨试试。
vue组件实例的生命周期还有需要探究的地方。