0%

监听事件

可以用 v-on 指令监听 DOM 事件, 并在触发时运行一些 JavaScript 代码

1
2
3
4
5
6
7
8
9
10
11
12
<div id="app">
<button v-on:click="count += 1">+1</button>
<span>{{ count }}</span>
</div>
<script>
var vm = new Vue({
el: 'app',
data: {
count: 0
}
})
</script>

事件处理方法

大多时候, 我们的事件处理逻辑都是复杂的, 这时候直接写在 v-on 指令中是不可行的, 因此 v-on 还可以接受一个需要调用的方法名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="app">
<button v-on:click="add">+1</button>
<span>{{ count }}</span>
</div>
<script>
var vm = new Vue({
el: 'app',
data: {
count: 0
},
methods: {
add() {
// this 指向当前 vue 实例
this.count += 1
}
}
})
vm.add() // 也可以直接用实例调用此方法
</script>

内联处理器中的方法

除了在 v-on 指令后面绑定一个方法, 我们也可以在内联的 JavaScript 语句中调用方法
有时候我们需要在方法中访问原生的 DOM 事件, 可以用特殊的变量 $event 把它传入方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div id="app">
<button v-on:click="log('Hi')">+1</button>
</div>
<script>
var vm = new Vue({
el: 'app',
methods: {
log(msg) {
// this 指向当前 vue 实例
console.log(msg)
}
}
})
</script>

事件修饰符

在日常开发场景中, event.preventDefault()event.stopPropagation() 是非常常见的需求
为了解决此类场景, Vue 为 v-on 提供了事件修饰符

修饰符是由开头的指令后缀表示

  • .stop 阻止事件继续传播
  • .prevent 提交事件不再重载页面
  • .captrue 使用事件捕获模式
  • .self 只在 event.target 是当前元素资深时触发处理函数
  • .once 点击事件将只会触发一次
  • .passive 滚动事件的默认行为将会立即触发, 不会等待 onScroll 完成

按键修饰符

在监听键盘事件时, 我们经常要检查详细的按键, Vue 允许为 v-on 在监听键盘事件时添加按键修饰符

  • .enter 回车键
  • .tab tab 键
  • .delete 删除和退格键
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right
1
2
<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
<input type="text" v-on:keyup.enter="submit" />

Vue 允许我们将 keyboardEvent.key 暴露的任意有效按键名转换为 kebab-case 来作为修饰符
还可以通过全局 config.keyCodes 对象自定义按键修饰符别名 Vue.config.keyCodes.f1 = 112

系统修饰符

可以用以下修饰符来实现仅在按下相应按键时才出发鼠标或键盘事件的监听器

  • .ctrl
  • .alt
  • .shift
  • .meta
1
2
<!-- alt + c -->
<input @keyup.alt.67="clear" />

.exact 修饰符

.exact修饰符允许控制由精确的系统修饰符组合触发的事件

1
2
3
4
5
<!-- 即使 alt 或 shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">a</button>

<!-- 有且只有 ctrl 被按下时才触发 -->
<button @click.ctrl.exact="onClick">a</button>

鼠标按钮修饰符

  • .left
  • .right
  • .middle

v-for

我们可以用v-for指令基于一个数组来渲染一个列表. v-for指令需要使用 item in items形式的特殊语法, 其中items是源数据数组, 而item则是被迭代的数组元素的别名

也可以用of替代in作为分隔符
v-for 可以接受整数, 它会将模板重复对应次数
v-for 的对象也可以是一个计算属性或一个方法
可以在<template>元素上使用 v-for
不推荐在同一个元素上直接使用 v-for 与 v-if, v-for 的优先级高于 v-if

遍历数组

  • items 遍历源数据
  • item 遍历项
  • index 索引值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="hello">
<h1>滴滴搭</h1>
<ul>
<li v-for="(item, index) in items" :key="item">{{ item }}</li>
</ul>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data() {
return {
items: ['html', 'css', 'js', 'vue', 'react']
}
}
}
</script>

遍历对象

遍历对象会按照 Object.keys() 的结果遍历

  • objs 源数据对象
  • key 键
  • val 值
  • index 索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div class="hello">
<h1>滴滴</h1>
<ul>
<li v-for="(val, key, index) of objs" :key="key">
{{ index }}-{{ key }}-{{val}}
</li>
</ul>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data() {
return {
objs: {
name: 'Ginger',
age: 24
}
}
}
}
</script>

维护状态

Vue 在使用 v-for 渲染元素列表时, 如果数据项顺序发生改变, Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是就地更新每个元素, 并且确保他们每个索引位置正确渲染.
为了使 Vue 能跟踪每个节点的身份, 从而重用和重新排序现有元素, 我们需要为每项提供一个唯一的 key 属性

不推荐使用对象或数之类的非基本类型值作为 v-for 的 key, 推荐使用字符串或数值类型

1
2
3
<div v-for="item in items" v-bind:key="item.id">
<!-- 内容 -->
</div>

数组更新检测

变异方法

Vue 将被侦听的数组的编译方法进行了包裹, 所以他们也将会触发试图更新

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

替换数组

Vue 除了提供变异方法外, 还提供了非变异方法, 他们不会改变原始数组, 而总是返回一个数组

  • filter()
  • concat()
  • slice()

注意事项

由于 JavaScript 限制, Vue 不能检测以下数组变动

  • 利用索引直接修改数组某一项时
  • 修改数组长度时

Set 方法

为了解决无法检测到数组变动的问题, Vue 提供了以下两个方法, 或者使用其他变异方式等进行处理

  • Vue.set()
  • vm.$set()

对象变更检测

由于 JavaScript 限制, Vue 不能检测对象属性的添加或删除
对于已创建的 Vue 实例, Vue 不允许动态添加根级别的响应式属性, 但是可以用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var vm = new Vue({
el: 'app',
data: {
obj: {
name: 'ginger'
}
}
})
// 添加单个属性
vm.$set(vm.obj, 'age', 18)
// 添加多个属性
vm.obj = Object.assign({}, vm.obj, {
gender: 0
address: 'China'
})

在组件上使用 v-for

我们可以在任何自定义组件上使用 v-for 但是我们必须定义 key
如果要将遍历内容数据传递到组件内, 需要使用 props

v-if

v-if 指令用于条件性的渲染一块内容. 该内容只有在表达式结果返回 true 值的时候被渲染

1
<div v-if="true">v-if表达式结果为真时显示</div>

<template> 元素

<template> 元素是 Vue 给我们提供的内置元素, 它不会显示在页面中
如果我们要对多个元素使用v-if指令, 可以使用 <template> 元素包裹所有需要作用的元素

1
2
3
4
<template v-if="true">
<h3>title</h3>
<p>...content</p>
</template>

v-else

可以使用 v-else 指令来表示 v-if 的’else’块
v-else 元素必须紧跟在带 v-if 或者 v-else-if 的元素后面, 否则它将不会被识别

1
2
<div v-if="Math.random() > 0.5"></div>
<div v-else></div>

v-else-if

v-else-if, 可以充当v-if的’else-if’块, 可以连续使用, 也必须紧跟在带v-if或者v-else-if元素之后

1
2
3
4
<div v-if="type==='A'">A</div>
<div v-else-if="type==='B'">B</div>
<div v-else-if="type==='C'">C</div>
<div v-else="type==='D'">A</div>

key

Vue 会尽可能高效的渲染元素, 通常会复用已有元素而不是从头开始渲染, 这么做会使 Vue 变得非常快
但有时候, 我们并不希望使用此特性, Vue 为我们一种方式来表达不复用, 只需要添加一个具有唯一值的key属性即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<div class="hello">
<template v-if="loginType">
<label>Username</label>
<input placeholder="Enter your username" key="username" />
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address" key="pwd" />
</template>
<button @click="handle">toog</button>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
methods: {
handle() {
this.loginType = !this.loginType
}
},
data() {
return {
loginType: true
}
}
}
</script>

v-show

另一个根据条件展示元素的选项是v-show指令, 用法与v-if大致一样:

v-show 不支持 <template> 元素, 也不支持 v-else

1
<h1 v-show="true">Ginger</h1>

v-if vs v-show

  • v-if 是真正的条件渲染, 他会在切换过程中适当的将事件监听器和子组件适当地被销毁和重建
  • v-if 是惰性的, 如果在初始渲染时条件为假, 元素便不会渲染, 只到条件第一次为真时, 才会开始渲染
  • v-show 不管初始条件是什么, 元素总是会被渲染, 并且只是基于 CSS 进行切换

v-if 与 v-show 一起使用

v-ifv-for一起使用时, v-for具有比v-if更高的优先级

不推荐同时使用 v-ifv-else

绑定 HTML Class

Vue 对于 class 和 style 上使用v-bind做了专门的增强, 表达式结果的类型除了字符串之外, 还可以是对象或数组

对象语法

我们可以传给v-bind:class一个对象, 以动态的切换 class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<template>
<div id="app">
<!-- 传递一个对象 -->
<div :class="{ box: showBox, box1: showBox }"></div>
<!-- class 与 绑定对象共存 -->
<div class="box" :class="{ box1: showBox }"></div>
<!-- 接受一个对象 -->
<div :class="classObj"></div>
<!-- 接受一个计算属性 -->
<div :class="isShowBox"></div>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data() {
return {
showBox: true,
classObj: {
box: true,
box1: false
}
}
},
computed: {
isShowBox() {
return {
box: true,
box1: true
}
}
}
}
</script>

<style>
.box {
border: 1px solid red;
width: 100px;
height: 100px;
}
.box1 {
width: 60px;
height: 60px;
background-color: pink;
}
</style>

数组语法

我们还可以把一个数组传给 v-bind:class, 应用一个 class 列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<template>
<div id="app">
<!-- 接受一个数组 -->
<div :class="clsasArr"></div>
<!-- 使用表达式 -->
<div :class="[...clsasArr]"></div>
<!-- 使用三元表达式 -->
<div :class="[showBox ? 'box' : '']"></div>
<!-- 对象与数组同时使用 -->
<div :class="[{ box: showBox }, 'box1']"></div>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data() {
return {
showBox: true,
clsasArr: ['box', 'box1']
}
}
}
</script>

<style>
.box {
border: 1px solid red;
}
.box1 {
width: 60px;
height: 60px;
background-color: pink;
}
</style>

用于组件上

当我们在一个自定义组件上使用 class 属性时, 这些 class 将被添加到该组件的根元素上, 这个元素上已存在的 class 不会被覆盖

绑定内联样式

对象语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
<div id="app">
<!-- 内联样式 -->
<div
class="box"
:style="{ width: width + 'px', height: height + 'px' }"
></div>
<!-- 直接传入对象 -->
<div :style="style">box</div>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data() {
return {
width: 100,
height: 100,
style: {
backgroundColor: 'pink',
color: '#000'
}
}
}
}
</script>

<style>
div.box {
border: 1px solid red;
}
</style>

数组语法

v-bind:style 的数组语法可以将多个样式对象应用到同一个元素上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div id="app">
<div :style="arr">box</div>
</div>
</template>

<script>
export default {
name: "HelloWorld",
data() {
return {
arr: [{ color: "red" }, { border: "1px solid red" }]
};
}
};
</script>

<style>

自动添加前缀

v-bind:style 使用需要添加浏览器引擎前缀的 CSS 属性时,如 transform,Vue.js 会自动侦测并添加相应的前缀。

多重值

可以为 style 绑定中的属性提供一个包含多个值的数组,常用于提供多个带前缀的值,例如:
这样写只会渲染数组中最后一个被浏览器支持的值。在本例中,如果浏览器支持不带浏览器前缀的 flexbox,那么就只会渲染 display: flex。

1
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

计算属性

虽然我们可以在模板内使用表达式, 但是设计初衷是用于简单的运算, 如果在模板中放入太多的逻辑会让模板过重难以维护

1
<div>{{ message.split('').reverse().join() }}</div>

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div id="app">
<p>原始message: {{ message }}</p>
<p>计算属性message: {{ newMessage }}</p>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data() {
return {
message: 'Hello World'
}
},
computed: {
newMessage() {
return this.message
.split('')
.reverse()
.join('')
}
}
}
</script>

计算属性缓存 vs 方法

如果我们在methods中定义方法, 在模板中调用, 一样可以实现这个功能
不同的是, 计算属性是基于他们的响应式依赖进行缓存的, 只有在相关响应式依赖发生改变时他们才会重新计算求值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div id="app">
<p>原始message: {{ message }}</p>
<p>计算属性message: {{ reverseStr() }}</p>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data() {
return {
message: 'Hello World'
}
},
methods: {
reverseStr() {
return this.message
.split('')
.reverse()
.join('')
}
}
}
</script>

计算属性的 getter

计算属性默认只有 getter, 不过我们在需要时也可以提供一个 setter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div id="app">
<p>{{ firstName }}</p>
<p>{{ lastName }}</p>
<p v-if="fullNmae">{{ fullNmae }}</p>
<button @click="()=>{fullNmae = 'Hi,Ginger'}">update fullName</button>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data() {
return {
firstName: 'Hello',
lastName: 'World'
}
},
computed: {
fullNmae: {
get() {
return this.firstName + this.lastName
},
set(newVal) {
let arr = newVal.split(',')
this.firstName = arr[0]
this.lastName = arr[1]
}
}
}
}
</script>

侦听器

Vue 通过watch选项提供了一个可自定义的侦听器来响应数据的变化, 当需要在数据变化时执行异步或开销较大的操作时, 适合使用过

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<div id="app">
<p>{{ content }}</p>
<button @click="onClick">page++</button>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data() {
return {
content: '', // 页面内容
page: 1 // 页码
}
},
methods: {
onClick() {
this.page++
}
},
watch: {
page(newVal) {
// 根据页码发起数据请求 获取最新页数数据
...
}
}
}
</script>

计算属性 vs 侦听属性

Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动: 侦听属性.
不过官方更推荐使用计算属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<div id="app">
<p>{{ fullName}}</p>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data() {
return {
firstName: 'Hello',
lastName: 'World',
fullName: ''
}
},
created() {
this.firstName = 'Hello'
},
// 侦听器实现 重复代码
watch: {
firstName(val) {
return (this.fullName = val + this.lastName)
},
lastName(val) {
return (this.fullName = this.firstName + val)
}
},
// 计算属性实现 更简洁
computed: {
fullName() {
return this.firstName + this.lastName
}
}
}
</script>

模板语法

Vue.js 使用基于html的模板语法, 在底层上, Vue将模板编译成虚拟 DOM 渲染函数, 结合响应系统, 减少 DOM 操作次数

插值

文本

数据绑定最常用形式, 使用 Mustache 语法(双大括号)的文本插值
Mustache 标签会替换为对应数据对象上 msg 属性的值, 只要数据对象上 msg 属性发生了改变, 页面插值处内容也会更新

1
<div>Message: {{ msg }}</div>

v-once

通过使用 v-once 指令, 可以实现一次性的插值, 当数据改变时, 插值处内容不会更新

1
<div v-once>这个值不会改变: {{ msg }}</div>

原始 HTML

双大括号会将数据编译为普通文本, 如果想要输出 HTML, 需要使用 v-html 指令

1
2
<p>数据编译为普通文本 {{ msg }}</p>
<p>数据编译为HTML内容 <span v-html="html"></span></p>

Attribute

Mustache 语法不能作用在 HTML attribute 上, 但是 Vue 给我们提供了v-bind指令

1
<div v-bind:id="ID">绑定 id</div>

JavaScript 表达式

每个绑定只能包含单个表达式

1
<div v-bind:id="'this-' + ID"></div>

指令

指令是带有 v- 前缀的的特殊 attribute. 指令的作用是, 当表达式中的值发生变化时, 将其产生的连带影响, 响应式的作用于 DOM

1
<div v-if="seen">v-if绑定的值控制着此 div 元素的插入与移除</div>

参数

部分指令可以接受一些’参数’, 在指令名称之后以冒号表示. 例:

1
2
3
4
<!-- href是参数 表示将href的值与url进行绑定 -->
<a v-bind:href="url">...</a>
<!-- click是参数 表示监听的事件名与事件处理函数 -->
<a v-on:click="onClick">...</a>

动态参数

可以用方括号括起来的 JavaScript 表达式作为一个指令的参数

1
2
<!-- 此处的 attributeName 会被作为一个 JavaScript 表达式进行动态求值 求值结果将会作为最终的参数来使用 -->
<a v-bind:[attributeName]="url">...</a>

修饰符

修饰符是以.为后缀指明的, 用于指出一个指令应该以特殊方式绑定

1
2
3
4
<!-- 此处的 prevent 相当于在处理事件中添加 event.preventDetafult -->
<form v-on:submit.prevent="onSubmit">
<button>提交</button>
</form>

缩写

对于一些频繁使用的指令, Vuev-bindv-on这两个最常用的指令, 提供了特定简写:

  • v-bind
1
2
3
4
<!-- 完成写法 -->
<a v-bind:href="url">...</a>
<!-- 缩写 -->
<a :href="url">...</a>
  • v-on
1
2
3
4
5
6
7
<!-- 完整写法 -->
<button v-on:click="submit">
<button>
<!-- 缩写 -->
<button @click="submit"><button></button></button>
</button>
</button>

创建一个 Vue 实例

Vue 实例通过 new 一个 Vue 函数得到
在创建一个 Vue 实例的时候, 我们可以通过传入一个选项对象来构建我们需要的行为.

1
2
3
var vm = new Vue({
// 选项
})

数据与方法

当一个 Vue 实例被创建时, 它会将 data 对象中所有的属性加入到 Vue 的响应式系统中, 当他们发生改变时, 视图将会产生响应, 进行更新

data 中数据只有在实例创建时就已存在的属性才是响应式的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 数据对象
var data = { name: 'jack' }

// 将数据对象加入到 Vue 实例中
var vm = new Vue({
el: 'app'
data: data
})

// 获取实例上的属性 返回源数据中对应的字段
vm.name == data.a // true

// 设置属性也会影响到原始数据
vm.name = 'Ginger'
data.a // Ginger

Vue 实例暴露了一系列内置实例属性与方法, 他们都有**$** 前缀

1
2
vm.$data === data // true
vm.$el === document.getElementById('app') // true

实例生命周期

Vue 实例在创建时, 需要经过一系列的初始化过程, 在这个过程中, 会运行一些叫做生命周期钩子的函数, 这些钩子函数为用户提供了在实例创建过程中添加自己代码的机会

生命周期钩子中的this上下文指向调用他的vue实例
不要使用箭头函数

生命周期钩子

  • beforeCreate 实例创建前
  • created 实例创建后
  • beforeMount 挂载前
  • Mounted 挂载完毕
  • beforeupdate 实例数据更新前
  • updated 数据更新完毕
  • beforeDestroy 实例销毁前
  • destroyed 实例销毁完毕

生命周期图示

生命周期

Vue Devtools

Devtools 是一个 Vue 官方提供的浏览器插件, 他可以让我们在开发过程中国更直观更友好的的在浏览器中对 Vue 项目进行审查和调试

版本

Vue 分为开发版本和生产版本

  • 开发版本 - 包含完整的警告和调试模式
  • 生产版本 - 删除了警告

使用

直接引入(本地文件或 CDN)

在 html 文件中, 我们如果需要使用 Vue 可以直接使用 script 标签进行引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!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>
</head>
<body>
<!-- 最新版本 适合开发环境 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 指定版本 适合生产环境追溯问题 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
</body>
</html>

NPM

在使用 Vue 开发大型应用时, 推荐使用 NPM 安装, NPM 能够很好的与 webpack 配合使用.

1
$ npm install vue

命令行工具

1
vue create hello-word

Vue 提供了一个官方的 Cli, 为单页面应用(SPA)快速搭建繁杂的脚手架.

装饰器模式

装饰器模式是指在不改变原有对象的基础上, 对其进行包装扩展, 使其满足用户更复杂的需求.

复习下单例模式

使用单例模式维护一个全局的 Modal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!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" />
<style>
div {
width: 200px;
height: 200px;
border: 1px solid pink;
color: pink;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
</style>
<title>Document</title>
</head>
<body>
<button id="open">open</button>
<button id="hidden">hidden</button>
<script>
const Modal = (function() {
let modal = null
return function() {
if (!modal) {
modal = document.createElement('div')
modal.style.display = 'none'
modal.innerText = 'Modal'
document.body.appendChild(modal)
}
return modal
}
})()

document.getElementById('open').addEventListener(
'click',
function() {
const modal = new Modal()
console.log(modal)

modal.style.display = 'block'
},
false
)

document.getElementById('hidden').addEventListener(
'click',
function() {
const modal = new Modal()
modal.style.display = 'none'
},
false
)
</script>
</body>
</html>

问题来了

这时候产品经理说, 需要修改下这个按钮文字内容, 还要给点击按钮置灰, 不可点击, 如果我们在项目多处用到的话, 就需要一处处进行修改
如果封装成公共组件, 也会因为组件应用时的需求复杂度而产生同样的问题

装饰器模式改造案例

第一步 抽离原有旧逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 原有对象保持不变
const Modal = (function() {
let modal = null
return function() {
if (!modal) {
modal = document.createElement('div')
modal.style.display = 'none'
modal.innerText = 'Modal'
document.body.appendChild(modal)
}
return modal
}
})()

function openModal() {
// 封装显示模态框逻辑
const modal = new Modal()
if (modal) {
modal.style.display = 'block'
}
}

第二步 封装新需求逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function changeBtnText() {
// 封装按钮文字修改逻辑
const btn = document.getElementById('open')
btn.innerText = '快点我'
}

function disabledBtn() {
// 封装按钮可否点击逻辑
const btn = document.getElementById('open')
btn.setAttribute('disabled', true)
}

function changeBtnStatus() {
// 整和所有封装
changeBtnText()
disabledBtn()
}

第三步 添加需求到点击按钮上

1
2
3
4
5
6
7
8
document.getElementById('open').addEventListener(
'click',
function() {
openModal()
changeBtnStatus()
},
false
)

ES6 class 类实现装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// class 单例模式 modal
class Modal {
constructor() {
this.modal = null
}
static getModal() {
if (!this.modal) {
this.modal = document.createElement('div')
this.modal.innerText = '我是一个class单例模式下的div'
this.modal.style.display = 'none'
document.body.appendChild(this.modal)
}
return this.modal
}
}

// 装饰器模式
class openModal {
click() {
const modal = Modal.getModal()
modal.style.display = 'block'
}
}

class Decorator {
constructor(open_button) {
this.open_btn = open_button
}

onclick() {
// 职责单一
this.open_btn.click()
this.changeBtnStatus()
}

changeBtnStatus() {
this.changeBtnText()
this.disabledBtn()
}

changeBtnText() {
const btn = document.getElementById('open')
btn.innerText = '来点击我啊'
}

disabledBtn() {
const btn = document.getElementById('open')
btn.setAttor = '来点击我啊'
}
}

document.getElementById('open').addEventListener(
'click',
function() {
const openBtn = new openModal()
const btn = new Decorator(openBtn)
btn.onclick()
},
false
)

原型模式

在 JavaScript 中, Object.create() 是对原型模式的最好实现.

prototype 原型模式的应用

JavaScript 中不存在 class , ES6 中 class 也只是原型继承的语法糖, 在 js 中我们常用的是通过 prototype 模拟类来得到类的实例, 实现实例间数据、方法的共享.

JavaScript 的原型与原型链

在 JavaScript 中, 创建一个对象有多种方式

1
2
3
4
5
6
7
// 通过 Object 构造函数创建对象
const obj = new Object({})
// 通过 create 创建对象
const obj2 = Object.create({})
// 字面量方式创建对象
const obj3 = {name: 'Ginger'}
// ...
  • 在 JavaScript 中每一个构造函数都有一个 prototype 属性, 该原型对象指向构造函数的原型对象, 原型对象中 construtor 指回该构造函数
  • 每个实例对象, 都有一个 __proto__ 属性, 该属性指向创建该实例对象的构造函数的原型对象

通过 new 关键字, 获得 Fn 构造函数的实例对象, f 实例对象上并没有 hi 方法, 但是我们可以调用成功, 便是因为原型链的存在.

1
2
3
4
5
6
7
8
9
10
function Fn(name, age) {
this.name = name
this.age = age
}
Fn.prototype.hi = function() {
console.log('Hi')
}
const f = new Fn()
f.hi() // Hi
console.log(f.__proto__ === Fn.prototype) // true

一个简单的深拷贝

通过两个 api 对 obj 对象进行拷贝, 得到 obj2 改变 obj2 后, obj 内容未发生改变, 但是如果 obj 不是一个严格 json 格式的数据, 他的值是还是一个对象的时候, 这个就无法处理了

1
2
3
4
5
6
7
8
9
const obj = {
name:'Ginger',
age:24,
web:['html', 'js', 'css']
}
const obj2 = JSON.parse(JSON.stringify(obj))
obj2.web.splice(0,1)
console.log(obj.web) // ['html', 'js', 'css']
console.log(obj2.web) // ["js", "css"]

另外一个深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function DeepClone(obj) {
// 边界判断
if(typeof obj !== 'object' || obj === null) {
return obj
}

// 初始化结果值
let res = {}

// 如果是数组
if(obj.constructor === 'array') {
res = []
}

for(let i in obj) {
if(obj.hasOwnProperty(i)) {
// 如果值是对象 递归调用
res[i] = DeepClone(obj[i])
}
}

return res
}