一、Vue指令(了解)
1、深入响应式原理
Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。Vue里面是怎么做到的的呢?其实就是使用了Object.defineProperty
把Vue内的属性全部转成 getter/setter
。Object.defineProperty
是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
Object.defineProperty
实现了对象劫持这个功能
https://github.com/vuejs/vue/blob/1.0/src/observer/index.js
https://github.com/vuejs/vue/blob/2.6/src/core/observer/index.js
语法:
Object.defineProperty(obj, prop, desc)
数据属性:
通过Object.defineProperty()为对象定义属性,有两种形式,分别为数据描述符,存取描述符,下面分别描述两者的区别:
writable
如果为true标识可以被修改,如果为false标识不能被修改(默认值为false)
configurable
描述属性是否配置,以及可否删除,可以认为是总开关 默认值 false(不可删除)
enumerable
描述属性是否出现在for in 或者 Object.keys()的遍历中 默认值false(不能遍历)
let obj = {};
Object.defineProperty(obj, 'name', {
value: '张三'
})
obj.name = '李四'
console.log(obj.name) // 张三
let obj = {};
Object.defineProperty(obj, 'name', {
value: '张三',
writable: true
})
obj.name = '李四'
console.log(obj.name)
let obj = {};
Object.defineProperty(obj, 'name', {
value: '张三',
writable: true,
configurable: true,
enumerable: true
})
obj.name = '李四'
// delete obj.name
console.log(obj.name) // 李四
console.log(Object.keys(obj)) // ['name']
存取属性:
let obj = {};
let temp = null;
Object.defineProperty(obj, 'name', {
get() {
return temp
},
set(val) {
temp = val
}
})
obj.name = '李四'
console.log(obj.name)
面试题回答:
vue的双向数据绑定原理是什么?
vue数据双向绑定是通过数据劫持结合“发布者-订阅者模式”的方式来实现的。 vue是通过Object.defineProperty()来实现数据劫持,其中会有getter()和setter方法;当读取属性值时,就会触发getter()方法,在view中如果数据发生了变化,就会通过Object.defineProperty()对属性设置一个setter函数,当数据改变了就会来触发这个函数;
参考:https://segmentfault.com/a/1190000014274840
参考:https://zhuanlan.zhihu.com/p/51357583
2、自定义指令
除了一些内置的制定(v-model和v-show...),Vue也允许注册自定义指令。
// 注册一个全局自定义指令 v-focus
Vue.directive('demo', {
inserted: function (el, binding, vnode) {
//
console.log(el, binding, vnode);
}
})
// 组件中注册局部指令
new Vue({
el: '#app',
data: {},
directives: {
demo: {
inserted: function (el, binding, vnode) {
cosnole.log(el, binding, vnode);
}
}
}
})
// 在模板中使用自定义指令
<div v-demo>
</div>
钩子函数:
bind
: 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update
:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
componentUpdated
:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
接下来我们来看一下钩子函数的参数 (即 el
、binding
、vnode
和 oldVnode
)。
钩子函数的参数:
el
:指令所绑定的元素,可以用来直接操作 DOM 。
binding
:一个对象,包含以下属性:
value
:指令的绑定值,例如:v-my-directive="1 + 1"
中,绑定值为 2
。
oldValue
:指令绑定的前一个值,仅在 update
和 componentUpdated
钩子中可用。无论值是否改变都可用。
expression
:字符串形式的指令表达式。例如 v-my-directive="1 + 1"
中,表达式为 "1 + 1"
。
modifiers
:一个包含修饰符的对象。例如:v-my-directive.foo.bar
中,修饰符对象为 { foo: true, bar: true }
。
oldVnode
:上一个虚拟节点,仅在 update
和 componentUpdated
钩子中可用。
实现类似v-show的自定义指令
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<p v-demo="status">12</p>
<button @click="status = !status">取反</button>
</div>
</body>
<script>
const vm = new Vue({
el: '#app',
data: {
status: true
},
directives: {
demo: {
inserted(el, binding, vnode) {
console.log(el, binding)
if (binding.value) {
el.style.display = 'block'
} else {
el.style.display = 'none'
}
},
update(el, binding, vnode, oldVnode){
console.log(el, binding)
if (binding.value) {
el.style.display = 'block'
} else {
el.style.display = 'none'
}
}
}
}
})
</script>
</html>
二、Vue组件化开发(掌握)
1、什么是组件化?
面对复杂问题的处理方式,把问题拆解成很多个能处理的小问题,再将其放在整体中,会发现大的问题也会迎刃而解。
而组件化的思想也类似:
如果我们实现一个页面结构和逻辑非常复杂的页面时,如果全部一起实现会变得非常复杂,而且也不利于后续的维护和迭代功能。
但如果我们这时候把页面分成一个个小的功能块,每个功能块能完成属于自己这部分独立的功能,那么整个页面之后的维护和迭代也会变得非常容易。
2、Vue组件化思想
组件化是Vue重要的思想
它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。
组件化思想的应用开发:
有了组件化的思想,我们在之后的开发中就要充分的利用它。
这样让我们的代码更加方便组织和管理,并且扩展性也更强。
3、全局组件
通过Vue.component('组件名称', {})
,通过这个方法注册的都是全局组件,也就是他们再注册之后可以用在任何新创建的Vue
实例挂载的区域内。
<body>
<div id="app">
<my-con></my-con>
<div>
<my-con></my-con>
</div>
</div>
<my-con></my-con>
</body>
<script>
Vue.component('my-con', {
template: '<section><h3>组件标题</h3><p>组件内容</p></section>'
})
const vm = new Vue({
el: '#app'
})
</script>
上面案例中,在<div id="app">...</div>
外的组件 my-con
没有替换成组件真正的页面结构,是因为 new Vue()
挂载在 id=app
的节点上,不在这个节点上标签,不会受到Vue的影响。
4、局部组件
通过 Vue.component
方式注册的组件,称之为全局组件。任何地方都可以使用。全局注册往往是不够理想的。比如,如果你使用一个像 webpack 这样的构建系统,全局注册所有的组件意味着即便你已经不再使用一个组件了,它仍然会被包含在你最终的构建结果中。这造成了用户下载的 JavaScript 的无谓的增加。
注册局部组件
<body>
<div id="app">
<my-con></my-con>
<div>
<my-con></my-con>
</div>
</div>
<div id="app1">
<my-con1></my-con1>
</div>
</body>
<template id="template1">
<section>
<h3>组件标题</h3>
<p>组件内容</p>
</section>
</template>
<template id="template2">
<section>
<h3>组件标题B</h3>
<p>组件内容B</p>
</section>
</template>
<script>
var componentA = {
template: '#template1'
}
var componentB = {
template: '#template2'
}
const vm = new Vue({
el: '#app',
components: {
'my-con': componentA
}
})
const vm1 = new Vue({
el: '#app1',
components: {
'my-con1': componentB
}
})
</script>
父组件和子组件
组件和组件之间存在层级关系,而其中一种最重要的关系就是父子组件关系。
5、组件可以访问Vue实例数据吗?
那组件如果要使用data定义自己属性保存数据要怎么做呢?
组件对象也有一个data的属性(也有methods等属性,下面我们有用到)
只是这个data属性必须是一个函数,而且函数返回一个对象 ,对象保存着数据
<body>
<div id="app">
<my-con></my-con>
<div>
<my-con></my-con>
</div>
</div>
<div id="app1">
<my-con1></my-con1>
</div>
</body>
<template id="template1">
<section>
<h3>{{title}}</h3>
<p>组件内容</p>
</section>
</template>
<template id="template2">
<section>
<h3>{{title}}B</h3>
<p>组件内容B</p>
<aa></aa>
</section>
</template>
<script>
var componentA = {
template: '#template1',
data() {
return {
title: 'zujianbiaoti'
}
}
}
var componentB = {
template: '#template2',
data() {
return {
title: 'zj'
}
},
components: {
'aa': {
template: '<div>aa</div>'
}
}
}
const vm = new Vue({
el: '#app',
data: {title: '组件标题'},
components: {
'my-con': componentA
}
})
const vm1 = new Vue({
el: '#app1',
components: {
'my-con1': componentB
}
})
</script>
为什么data在组件中必须是一个函数呢?
原因是在于Vue让每个组件对象都返回一个新的对象,因为如果是同一个对象的,组件在多次使用后会相互影响。
6、父子组件间的通讯
父级向子级传递
在组件中,使用选项props来声明需要从父级接收到的数据。
props的值有两种方式:
对象,对象可以设置传递时的类型(String,Number,Boolean,Array, Object,Date,Function,Symbol),也可以设置默认值等。
<body>
<div id="app1">
<my-con1></my-con1>
</div>
</body>
<template id="template2">
<section>
<h3>{{title}}B</h3>
<p>组件内容B</p>
<!-- my-con1组件内的aa组件 -->
<aa v-bind:parent-txt="childtxt"></aa>
</section>
</template>
<script>
var componentB = {
template: '#template2',
data() {
return {
title: 'zj',
childtxt: 'child text'
}
},
components: {
'aa': {
template: '<div>{{parentTxt}}</div>',
props: ['parentTxt']
}
}
}
const vm1 = new Vue({
el: '#app1',
components: {
'my-con1': componentB
}
})
</script>
子级向父级传递
父组件向子组件传递数据,通过自定义事件
<body>
<div id="app1">
<my-con1></my-con1>
</div>
</body>
<template id="template2">
<section>
<h3>{{title}}B</h3>
<p>组件内容B</p>
<aa v-bind:parent-txt="childtxt" v-on:changetitle="changeTitle"></aa>
</section>
</template>
<script>
var componentB = {
template: '#template2',
data() {
return {
title: 'zj',
childtxt: 'child text'
}
},
components: {
'aa': {
template: '<div v-on:click="change">{{parentTxt}}</div>',
props: ['parentTxt'],
methods: {
change() {
this.$emit('changetitle', {
a: 1
})
}
}
}
},
methods: {
changeTitle(obj) {
console.log(obj)
this.title = obj.a
}
}
}
const vm1 = new Vue({
el: '#app1',
components: {
'my-con1': componentB
}
})
</script>
案例分析:
现在的需求是点击子组件aa
然后把父组件my-con1
上的标题给改变;
首先,在父组件的具体页面结构找到子组件aa
,在子组件aa
上使用v-on:changetitle="changeTitle"
, changetitle
是子组件的自定义事件名称,changeTitle
是父组件my-con1
里的methods
属性定义的方法;
其次,在子组件aa
里为div绑定点击事件 v-on:click="change"
, 在子组件aa
里的methods定义change方法,change方法里使用this.$emit('changetitle')
,使用$emit方法来触发绑定在子组件上的自定义事件,$emit第一个参数就是上一步定义的自定义事件changetitle
,第二个参数就是传递到父组件的参数,可以不传。
弹窗例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
html,body, #app1{
width: 100%;
height: 100%;
}
.wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background-color: rgba(0,0,0, 0.3);
}
.content {
width: 220px;
height: 160px;
background-color: #fff;
}
.title {
height: 40px;
line-height: 40px;
text-align: center;
}
.msg {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 5px 10px;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
height: 80px;
text-align: center;
color: gray;
font-size: 14px;
}
.bottom {
display: flex;
height: 40px;
line-height: 40px;
text-align: center;
}
.bottom div {
flex: 1;
color: green;
}
.bottom div:nth-of-type(1) {
border-right: 1px solid #eee;
color: red;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app1">
<my-con v-show="showCon" :txt-obj="textObj" @btn-handle="handleBtnClick"></my-con>
</div>
</body>
<template id="template1">
<div class="wrapper">
<div class="content">
<p class="title">{{txtObj.title}}</p>
<div class="msg">{{txtObj.msg}}</div>
<div class="bottom">
<div @click="cancel">{{txtObj.cancelTxt}}</div>
<div @click="submit">{{txtObj.submitTxt}}</div>
</div>
</div>
</div>
</template>
<script>
var componentA = {
template: '#template1',
props: {
txtObj: {
type: Object,
default: {}
}
},
methods: {
cancel() {
// 自定义事件名称建议全小写
this.$emit('btn-handle', 'cancel')
},
submit() {
// 自定义事件名称建议全小写
this.$emit('btn-handle', 'submit')
},
}
}
const vm1 = new Vue({
el: '#app1',
data: {
textObj: {
title: 'bug提示',
msg: '亲,你还有53633个bug,是否要处理?',
cancelTxt: '忽略,下班',
submitTxt: '加班处理'
},
showCon: true
},
components: {
myCon: componentA
},
methods: {
handleBtnClick(type) {
console.log(type)
if (type === 'cancel') {
this.showCon = false
}
}
}
})
</script>
</html>
非父子组件通讯
实际工作中如果遇到跨组件或者非父组件间的传递数据,那该怎么办?第一个可以使用中央事件总线,也就是一个中介来完成。第二个就是使用 Vuex
提供的功能,这里先暂时不讨论这种方案,后续专门学习Vuex。
**案例:**点击按钮1,改变按钮2的背景颜色
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app1">
<my-con></my-con>
<my-con1></my-con1>
</div>
</body>
<template id="template1">
<button @click="click1">按钮1</button>
</template>
<template id="template2">
<button @click="click2" :style="{backgroundColor: fontColor}">按钮2</button>
</template>
<script>
const bus = new Vue();
const componentA = {
template: '#template1',
methods: {
click1() {
// 点击按钮1
bus.$emit('xxx', this.getRandomColor());
},
getRandomColor() {
return `rgb(${this.getRandomNum()},${this.getRandomNum()},${this.getRandomNum()})`
},
getRandomNum() {
return Math.floor(Math.random() * 256)
}
}
}
const componentB = {
template: '#template2',
data() {
return {
fontColor: ''
}
},
methods: {
click2() {
// 点击按钮2
}
},
mounted() {
bus.$on('xxx', (color) => {
console.log(color)
this.fontColor = color
})
}
}
const vm1 = new Vue({
el: '#app1',
components: {
myCon: componentA,
myCon1: componentB
}
})
</script>
</html>
7、编译作用域(掌握)
父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。
也就是说父组件上的数据,只能在父组件上修改,不能传递到子组件里修改。
三、插槽(slot)
1. 为什么使用slot
slot翻译为插槽,组件的插槽:
组件的插槽也是为了让我们封装的组件更加具有扩展性。
京东头部导航栏例子:
2. 如何在组件中使用slot呢?
如何去封装这类的组件呢?
如果,我们每一个单独去封装一个组件,显然不合适:比如每个页面都返回,这部分内容我们就要重复去封装。
但是,如果我们封装成一个,好像也不合理:有些左侧是菜单,有些是返回,有些中间是搜索,有些是文字,等等
如何封装合适呢?抽取共性,保留不同
最好的封装方式就是将共性抽取到组件中,将不同暴露为插槽。
一旦我们预留了插槽,就可以让使用者根据自己的需求,决定插槽中插入什么内容。
是搜索框,还是文字,还是菜单。由调用者自己来决定。
3. slot的基本使用(匿名插槽)
了解了为什么用slot,我们再来谈谈如何使用slot?
在子组件中,使用特殊的元素<slot>
就可以为子组件开启一个插槽。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
</style>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app1">
<my-con></my-con>
<my-con>
<h2>我是h2标签的内容</h2>
<p>我是p标签的内容</p>
</my-con>
</div>
</body>
<template id="template1">
<div>
<slot>我是插槽中的默认内容!!</slot>
</div>
</template>
<script>
var componentA = {
template: '#template1',
}
const vm1 = new Vue({
el: '#app1',
data: {
},
components: {
myCon: componentA
}
})
</script>
</html>
4. 具名插槽
当子组件的功能复杂时,子组件的插槽可能并非是一个。
比如我们封装一个导航栏的子组件,可能就需要三个插槽,分别代表左边、中间、右边。
那么,外面在给插槽插入内容时,如何区分插入的是哪一个呢?
如何使用具名插槽呢?
<slot name='myslot'></slot>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
</style>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app1">
<my-con>
<div slot="left">左侧</div>
<div slot="right">右侧</div>
<div slot="center">中间</div>
</my-con>
</div>
</body>
<template id="template1">
<div>
<slot name="left">我是左侧插槽中的默认内容!!</slot>
<slot name="center">我是中间侧插槽中的默认内容!!</slot>
<slot name="right">我是右侧插槽中的默认内容!!</slot>
</div>
</template>
<script>
var componentA = {
template: '#template1',
}
const vm1 = new Vue({
el: '#app1',
data: {
},
components: {
myCon: componentA
}
})
</script>
</html>
5、课堂练习
京东导航栏
6. 作用域插槽
默认情况下,父组件使用子组件,插槽数据默认是拿父组件的数据,而不是子组件拿数据。
作用域插槽在父组件使用我们的子组件时, 插槽的数据从子组件中拿到数据,而不是从父组件拿到。
7. 作用域插槽的多种写法
// 1、基本写法
<one-comp>
<button slot="btn" slot-scope="scope">按钮{{scope.msg}}</button>
</one-comp>
// 2、基本写法之模板写法
<one-comp>
<template slot="btn" slot-scope="scope">
<button>按钮{{scope.msg}}</button>
</template>
</one-comp>
// 3、指令写法
<one-comp v-slot:btn="scope">
<button>按钮{{scope.msg}}</button>
</one-comp>
// 4、指令写法之模板写法
<one-comp>
<template v-slot:btn="scope">
<button>按钮{{scope.msg}}</button>
</template>
</one-comp>
四、作业
作业一:
完成百度首页(https://baidu.com):
作业要求:页面刷新输入框自动聚焦
作业二:
点击登录按钮,打开登录弹框:
作业要求:登录弹框必须是组件话实现,而且点击x按钮,可以关闭登录弹框