前言
我们知道在编译器技术中都需要通过有限状态机(Finite State Machine,简称FSM)来分词,所以我们想把编译技术学通透,就必须掌握有限状态机的实现原理。有限状态机是一种基于有限个状态以及在这些状态之间的转移和动作的计算模型,在生活、软件、数学等领域均有广泛的应用。
在编程实践中,当需要实现具有复杂状态转换逻辑的功能时,通常会使用状态模式来组织代码,以提高代码的可维护性和可读性,而状态模式是有限状态机的一种实现方式。所以我们在学习有限状态机之前先让我们学习状态模式吧,通过掌握状态模式进而循序渐进地学好有限状态机的原理。
状态机的经典案例
状态模式(State Pattern)是一种行为设计模式,用于在对象的内部状态发生变化时改变其行为。
我们知道红绿灯有三种状态:红灯、黄灯和绿灯,当红灯亮的时候,车辆停止;当黄灯亮的时候,车辆准备;当绿灯亮的时候,车辆通行。我们可以看到不同的状态,车辆的表现行为是不一样的,我们先通过不使用状态模式的红绿灯模拟程序的实现。
实现代码如下:
javascript
// 初始化红绿灯状态
let state = 'red';
// 定义红绿灯状态切换的函数
function trafficLight() {
if (state === 'red') {
console.log('红灯亮,车辆停止')
// 假设绿灯持续 5 秒
setTimeout(function() {
state = 'green'
trafficLight()
}, 5000)
} else if (state === 'green') {
console.log('绿灯亮,车辆通行')
// 假设黄灯持续 2 秒
setTimeout(function() {
state = 'yellow'
trafficLight()
}, 2000)
} else if (state === 'yellow') {
console.log('黄灯亮,车辆准备')
// 假设红灯再次亮起需要等待一段时间,例如 3 秒
setTimeout(function() {
state = 'red'
trafficLight()
}, 3000)
}
}
// 启动红绿灯状态切换
trafficLight()
在上述代码例子中我们使用了 setTimeout 来模拟红绿灯的持续时间,并在每个状态结束后调用 trafficLight 函数来改变状态。这只是一个简单的示例,没有使用状态模式,但可以有效地模拟红绿灯的状态变化。
实际上面类似上述这种拥有大量 if else 语句的代码,我们已经编写过无数遍了。而上面的代码例子其实就是一个最简单的有限状态机的实现,那么它有什么特点呢,跟我们平时写的大量 if else 、swith case 的代码有什么不同呢,为什么上面的代码就是一个有限状态机的实现呢?因为对于状态机而言,核心就是拥有多个状态,当前只会处于其中一种状态,状态之间可以切换,并且下一个状态是由当前状态决定的,当前处在哪种状态下,就有对应的函数或者类进行处理。而我们上述实现的红绿灯状态变化的代码例子中就很好验证了有限状态机的定义。首先拥有红、绿、黄三种状态,当前只会处于其中一种状态,或是红灯状态、或是绿灯状态、或是黄灯状态,且下一个状态是由当前状态决定的,如果当前是红灯状态,那么下一个状态就是绿灯状态,而不可能是黄灯或其他状态。
在实际应用中,我们可能需要在 DOM 中显示红绿灯的倒计时,这里只是简单地打印到控制台。
迭代
我们继续实现在 DOM 中显示红绿灯的倒计时。代码迭代如下:
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>使用状态模式和状态机进行红绿灯演示</title>
</head>
<body>
<div id="lightText"></div>
<script>
const el = document.getElementById('lightText')
// 初始化红绿灯状态
let state = 'red';
let count = null
let timer = null
// 定义红绿灯状态切换的函数
function trafficLight() {
if (state === 'red') {
if (count) {
count--
} else {
// 假设绿灯持续 5 秒
count = 5
}
el.textContent = `红灯倒计时:${count}`
if (!timer) {
timer = setTimeout(function() {
state = 'green'
timer = null
count = null
}, 5000)
}
} else if (state === 'green') {
if (count) {
count--
} else {
// 假设绿灯持续 3 秒
count = 3
}
el.textContent = `绿灯倒计时:${count}`
if (!timer) {
isTimerCleared = true
timer = setTimeout(function() {
state = 'yellow'
timer = null
count = null
}, 3000)
}
} else if (state === 'yellow') {
if (count) {
count--
} else {
// 假设黄灯持续 2 秒
count = 2
}
el.textContent = `黄灯倒计时:${count}`
if (!timer) {
timer = setTimeout(function() {
state = 'red'
timer = null
count = null
}, 2000)
}
}
}
// 启动红绿灯状态切换
trafficLight()
// 每秒执行一次,以实现倒计时
setInterval(trafficLight, 1000)
</script>
<body>
</html>
我们上述代码虽然实现了在 DOM 中现实红绿灯的倒计时,但还存在缺点,state、count、isTimerCleared 和 el 都是全局变量,这可能导致命名冲突和其他潜在的问题。为了解决这个问题,我们使用 OOP 进行封装。通过创建一个 TrafficLight 类,在类中将数据(属性)和处理数据的操作(方法)封装在一起,形成一个不可分割的整体,从而避免全局变量污染。
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>使用状态模式和状态机进行红绿灯演示</title>
</head>
<body>
<div id="lightText"></div>
<script>
class TrafficLight {
state = 'red'
count = null
timer = null
el = null
constructor(el) {
this.el = el
this.onTick()
setInterval(() => this.onTick(), 1000)
}
onTick() {
if (this.state === 'red') {
if (this.count) {
this.count--
} else {
// 假设绿灯持续 5 秒
this.count = 5
}
this.el.textContent = `红灯倒计时:${this.count}`
if (!this.timer) {
this.timer = setTimeout(() => {
this.state = 'green'
this.timer = null
this.count = null
}, 5000)
}
} else if (this.state === 'green') {
if (this.count) {
this.count--
} else {
// 假设绿灯持续 3 秒
this.count = 3
}
this.el.textContent = `绿灯倒计时:${this.count}`
if (!this.timer) {
this.timer = setTimeout(() => {
this.state = 'yellow'
this.timer = null
this.count = null
}, 3000)
}
} else if (this.state === 'yellow') {
if (this.count) {
this.count--
} else {
// 假设黄灯持续 5 秒
this.count = 2
}
this.el.textContent = `黄灯倒计时:${this.count}`
if (!this.timer) {
this.timer = setTimeout(() => {
this.state = 'red'
this.timer = null
this.count = null
}, 2000)
}
}
}
}
const el = document.getElementById('lightText')
// 启动红绿灯状态切换
new TrafficLight(el)
</script>
<body>
</html>
我们上面虽然通过类来实现红绿灯程序的封装,但 TrafficLight 类中 onTick 方法整个代码结构显得非常松散,阅读性差,而软件应该是"自描述"的,代码除了给机器看之外,也要给人看。我们希望写的代码更易读,让代码可以更好地表达自己的意图。
我们可以通过提炼函数,通过函数名称来知道我们的程序的业务结构,而提炼函数这个方法是《重构》这本书中介绍的一种代码重构手段。其次各种状态的持续时间(如红灯5秒、绿灯3秒、黄灯2秒)是硬编码在onTick方法中的,这意味着如果需要修改这些值,你需要直接编辑代码。可以将红绿灯的持续时间作为类的参数或配置选项来传递,而不是硬编码在代码中。这使得代码更加灵活和可配置。
重新迭代的代码如下:
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>使用状态模式和状态机进行红绿灯演示</title>
</head>
<body>
<div id="lightText"></div>
<script>
class TrafficLight {
state = 'red'
count = null
timer = false
el = null
constructor(el, redDuration, greenDuration, yellowDuration) {
this.el = el
this.redDuration = redDuration
this.greenDuration = greenDuration
this.yellowDuration = yellowDuration
this.onTick()
setInterval(() => this.onTick(), 1000)
}
setState(state) {
this.state = state
}
setCount(initCount) {
if (this.count) {
this.count--
} else {
this.count = initCount
}
}
updateTextContent(text) {
this.el.textContent = `${text}:${this.count}`
}
reset() {
this.timer = null
this.count = null
}
onTick() {
if (this.state === 'red') {
// 假设绿灯持续 x 秒
this.setCount(this.redDuration)
this.updateTextContent('红灯倒计时')
if (!this.timer) {
this.timer = setTimeout(() => {
this.setState('green')
this.reset()
}, this.redDuration * 1000)
}
} else if (this.state === 'green') {
// 假设绿灯持续 x 秒
this.setCount(this.greenDuration)
this.updateTextContent('绿灯倒计时')
if (!this.timer) {
this.timer = setTimeout(() => {
this.setState('yellow')
this.reset()
}, this.greenDuration * 1000)
}
} else if (this.state === 'yellow') {
// 假设黄灯持续 2 秒
this.setCount(this.yellowDuration)
this.updateTextContent('黄灯倒计时')
if (!this.timer) {
this.timer = setTimeout(() => {
this.setState('red')
this.reset()
}, this.yellowDuration * 1000)
}
}
}
}
const el = document.getElementById('lightText')
// 启动红绿灯状态切换
new TrafficLight(el, 5, 3, 2)
</script>
<body>
</html>
经过上述重构后我们达到了以下目的:
-
提高可配置性:可以将红绿灯的持续时间作为类的参数或配置选项来传递,而不是硬编码在代码中。这使得代码更加灵活和可配置。
-
减少重复代码:通过封装公共的逻辑到类中,可以避免在不同地方重复编写相同的代码。
-
提炼函数:我们可以通过提炼函数,通过函数名称来知道我们的程序的业务结构,让代码可以更好地表达自己的意图。
对于前端而言 OOP 更重要的是理解它的模块化的封装特性,也就是在类中将数据(属性)和处理数据的操作(方法)封装在一起,形成一个不可分割的整体,从而避免全局变量污染。其次就是使用 OOP 可以更好地支持各种设计模式,这些模式是解决常见问题的最佳实践。通过使用设计模式,你可以更高效地构建健壮、可维护且可扩展的 JavaScript 应用程序。
红绿灯实现程序经过上述的迭代,已经变得非常优秀了,但它仍然有缺点,具体就是 onTick 方法中存在大量的 if else 语句,那么要消除 if else 语句通常会使用策略模式,除了策略模式,还可以使用状态模式。那么下面就让我们使用状态模式来改进我们的红绿灯程序吧。
使用状态模式消除 if else
我们在现实生活中红绿灯可能还存在一种情况,就是高道路使用效率,当直行绿灯亮起时,车辆可以进入红绿灯等待区。所以我们还需要加一个红绿灯的状态,那么这个红绿灯的状态就是在红灯之后发生的。我们现在必须改造上面的代码来完成红绿灯的状态。
代码修改如下:
diff
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>使用状态模式和状态机进行红绿灯演示</title>
</head>
<body>
<div id="lightText"></div>
<script>
class TrafficLight {
state = 'red'
count = null
timer = false
el = null
- constructor(el, redDuration, greenDuration, yellowDuration) {
+ constructor(el, redDuration, greenDuration, yellowDuration, redGreenDuration) {
this.el = el
this.redDuration = redDuration
this.greenDuration = greenDuration
this.yellowDuration = yellowDuration
+ this.redGreenDuration = redGreenDuration
this.onTick()
setInterval(() => this.onTick(), 1000)
}
setState(state) {
this.state = state
}
setCount(initCount) {
if (this.count) {
this.count--
} else {
this.count = initCount
}
}
updateTextContent(text) {
this.el.textContent = `${text}:${this.count}`
}
reset() {
this.timer = null
this.count = null
}
onTick() {
if (this.state === 'red') {
// 假设绿灯持续 x 秒
this.setCount(this.redDuration)
this.updateTextContent('红灯倒计时')
if (!this.timer) {
this.timer = setTimeout(() => {
- this.setState('green')
+ this.setState('redGreen')
this.reset()
}, this.redDuration * 1000)
}
+ } else if (this.state === 'redGreen') {
+ // 假设红绿灯持续 x 秒
+ this.setCount(this.redGreenDuration)
+ this.updateTextContent('红绿灯倒计时')
+ if (!this.timer) {
+ this.timer = setTimeout(() => {
+ this.setState('green')
+ this.reset()
+ }, this.redGreenDuration * 1000)
+ }
} else if (this.state === 'green') {
// 假设绿灯持续 x 秒
this.setCount(this.greenDuration)
this.updateTextContent('绿灯倒计时')
if (!this.timer) {
this.timer = setTimeout(() => {
this.setState('yellow')
this.reset()
}, this.greenDuration * 1000)
}
} else if (this.state === 'yellow') {
// 假设黄灯持续 2 秒
this.setCount(this.yellowDuration)
this.updateTextContent('黄灯倒计时')
if (!this.timer) {
this.timer = setTimeout(() => {
this.setState('red')
this.reset()
}, this.yellowDuration * 1000)
}
}
}
}
const el = document.getElementById('lightText')
// 启动红绿灯状态切换
- new TrafficLight(el, 5, 3, 2)
+ new TrafficLight(el, 5, 3, 2, 4)
</script>
<body>
</html>
现在上述这个例子先告一段落,我们来考虑一下上述程序的缺点。
很明显 onTick 方法是违反 开放/封闭 原则的,每次新增或者修改状态都需要改动到 onTick 方法中的代码,这使得 onTick 成为一个非常不稳定的方法。而且所有跟状态有关的行为,都被封装在了 onTick 方法中,如果以后继续增加状态,那么这个方法将会变得非常臃肿。
此外 onTick 方法中堆砌大量 if... ellse 语句来做状态判断来进行不同情况的处理,但是状态一多就显得代码逻辑特别的乱,随着增加新的状态或者修改一个状态,if else 语句就要相应的的增多或者修改,这样程序的可读性,扩展性就会变得很弱,也会增加维护的成本。
使用状态模式改进程序
现在我们通过使用状态模式改进红绿灯的程序。状态模式允许一个对象在其内部状态改变时改变它的行为,这个对象看起来好像修改了它的类,这通常是一些 OOP 语言对状态模式的定义,例如 Java,而我们上述例子也通过 OOP 方式实现了我们的红绿灯演示程序。在 OOP 编程中状态模式的关键是将事物的每种状态都封装成单独的类,然后把跟此种状态相关的行为都封装到这个类的里面。
那么根据红绿灯的特点,我们可以定义RedLightState(红灯状态)、GreenLightState(绿灯状态) 和 YellowLightState(黄灯状态) 这三个类,那么我们就可以让原来的 TrafficLight 对象的行为(即灯的颜色和倒计时)随着其内部状态(RedLightState、GreenLightState、YellowLightState)的改变而改变。
同时由于所有状态类都共有相同的属性和方法,如 count(倒计时计数器)、timer(定时器)、setCount(设置倒计时)、setState(改变状态)、reset(重置定时器和计数器)和 updateTextContent(更新页面上的文本内容),我们可以把这些都封装到一个基础状态类中,让 RedLightState、GreenLightState 和 YellowLightState 类进行继承。
具体代码实现如下:
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>使用状态模式和状态机进行红绿灯演示</title>
</head>
<body>
<div id="lightText"></div>
<script>
// 公共基础类
class TrafficLightState {
count = null
timer = null
constructor(lightContext) {
this.lightContext = lightContext;
}
setCount(initCount) {
if (this.count) {
this.count--
} else {
this.count = initCount
}
}
setState(state) {
this.lightContext.setState(state)
}
reset() {
this.timer = null
this.count = null
}
updateTextContent(text) {
this.lightContext.updateTextContent(text, this.count)
}
// 声明每个状态可以响应的事件
onTick() {
throw new Error(`该方法必须通过子类实现!`);
}
}
class RedLightState extends TrafficLightState {
onTick() {
// 假设红灯持续 x 秒
this.setCount(this.lightContext.redDuration)
this.updateTextContent('红灯倒计时')
if (!this.timer) {
this.timer = setTimeout(() => {
this.setState(new GreenLightState(this.lightContext))
this.reset()
}, this.lightContext.redDuration * 1000)
}
}
}
class GreenLightState extends TrafficLightState {
onTick() {
// 假设绿灯持续 x 秒后变为黄灯
this.setCount(this.lightContext.greenDuration)
this.updateTextContent('绿灯倒计时')
if (!this.timer) {
this.timer = setTimeout(() => {
this.setState(new YellowLightState(this.lightContext))
this.reset()
}, this.lightContext.greenDuration * 1000)
}
}
}
class YellowLightState extends TrafficLightState {
onTick() {
// 假设黄灯持续 x 秒后变为红灯
this.setCount(this.lightContext.yellowDuration)
this.updateTextContent('黄灯倒计时')
if (!this.timer) {
this.timer = setTimeout(() => {
this.setState(new RedLightState(this.lightContext))
this.reset()
}, this.lightContext.yellowDuration * 1000)
}
}
}
class TrafficLight {
constructor(el, redDuration, greenDuration, yellowDuration) {
this.el = el
this.redDuration = redDuration
this.greenDuration = greenDuration
this.yellowDuration = yellowDuration
this.state = new RedLightState(this); // 初始状态为红灯
// 启动红绿灯循环
this.tick();
}
setState(state) {
this.state = state
}
updateTextContent(text, count) {
this.el.textContent = `${text}:${count}`
}
tick() {
this.state.onTick()
setTimeout(() => this.tick(), 1000)
}
}
const el = document.getElementById('lightText')
// 启动红绿灯状态切换
new TrafficLight(el, 5, 3, 2)
</script>
<body>
</html>
我们可以看到经过使用状态模式迭代升级后的代码已经完全消除了原来需要通过 if else 进行状态转换了,这就是状态模式的魅力,同时这也是 OOP 的多态特性。多态性是指子类对象可以替代父类对象,而程序在运行时可以根据对象的实际类型来调用相应的方法。在上述这个例子中,TrafficLight 类中的 state 属性可以是 RedLightState、GreenLightState 或 YellowLightState 中的任何一个实例。当调用 state.onTick() 方法时,程序会根据 state 的实际类型来调用相应状态类的 onTick 方法,这就是多态性的体现。
这同时也是 OOP 的拓展性,即继承允许子类扩展父类的功能。在这个例子中,RedLightState、GreenLightState 和 YellowLightState 都实现了自己的 onTick 方法,这些方法扩展了父类 TrafficLightState 的功能,定义了在不同状态下每秒应该执行的操作。
使用对象参数进行优化传参
当函数的参数过多时,代码的可读性和维护性会受到影响,我们可以使用对象参数进行优化传参,同时也方便后续添加或删除参数。
具体代码实现如下:
diff
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>使用状态模式和状态机进行红绿灯演示</title>
</head>
<body>
<div id="lightText"></div>
<script>
// 公共基础类
class TrafficLightState {
count = null
timer = null
constructor(lightContext) {
this.lightContext = lightContext
+ this.options = lightContext.options
}
setCount(initCount) {
if (this.count) {
this.count--
} else {
this.count = initCount
}
}
setState(state) {
this.lightContext.setState(state)
}
reset() {
this.timer = null
this.count = null
}
updateTextContent(text) {
this.lightContext.updateTextContent(text, this.count)
}
// 声明每个状态可以响应的事件
onTick() {
throw new Error(`该方法必须通过子类实现!`);
}
}
class RedLightState extends TrafficLightState {
onTick() {
// 假设红灯持续 x 秒
- this.setCount(this.lightContext.redDuration)
+ this.setCount(this.options.redDuration)
this.updateTextContent('红灯倒计时')
if (!this.timer) {
this.timer = setTimeout(() => {
this.setState(new GreenLightState(this.lightContext))
this.reset()
- }, this.lightContext.redDuration * 1000)
+ }, this.options.redDuration * 1000)
}
}
}
class GreenLightState extends TrafficLightState {
onTick() {
// 假设绿灯持续 x 秒后变为黄灯
- this.setCount(this.lightContext.greenDuration)
+ this.setCount(this.options.greenDuration)
this.updateTextContent('绿灯倒计时')
if (!this.timer) {
this.timer = setTimeout(() => {
this.setState(new YellowLightState(this.lightContext))
this.reset()
- }, this.lightContext.greenDuration * 1000)
+ }, this.options.greenDuration * 1000)
}
}
}
class YellowLightState extends TrafficLightState {
onTick() {
// 假设黄灯持续 x 秒后变为红灯
- this.setCount(this.lightContext.yellowDuration)
+ this.setCount(this.options.yellowDuration)
this.updateTextContent('黄灯倒计时')
if (!this.timer) {
this.timer = setTimeout(() => {
this.setState(new RedLightState(this.lightContext))
this.reset()
- }, this.lightContext.yellowDuration * 1000)
+ }, this.options.yellowDuration * 1000)
}
}
}
class TrafficLight {
- constructor(el, redDuration, greenDuration, yellowDuration) {
+ constructor(el, options) {
this.el = el
- this.redDuration = redDuration
- this.greenDuration = greenDuration
- this.yellowDuration = yellowDuration
+ this.options = options
this.state = new RedLightState(this); // 初始状态为红灯
// 启动红绿灯循环
this.tick();
}
setState(state) {
this.state = state
}
updateTextContent(text, count) {
this.el.textContent = `${text}:${count}`
}
tick() {
this.state.onTick()
setTimeout(() => this.tick(), 1000)
}
}
const el = document.getElementById('lightText')
// 启动红绿灯状态切换
- new TrafficLight(el, 5, 3, 2)
+ new TrafficLight(el, { redDuration: 5, greenDuration: 3, yellowDuration: 2 })
</script>
<body>
</html>
经过我们上述对传参的优化以及使用状态模式的修改,使得红绿灯的行为和状态分离,易于扩展和维护。如果需要添加新的状态或修改现有状态的行为,只需要添加或修改具体的状态类即可,而不需要修改上下文类或其他状态类。
那么我们再来添加一个红绿灯的状态,当红绿灯亮起时,车辆可以进入红绿灯等待区。所以我们还需要加一个红绿灯的状态类,代码如下:
javascript
class RedGreenLightState extends TrafficLightState {
onTick() {
// 假设红灯持续 x 秒
this.setCount(this.options.redGreenDuration)
this.updateTextContent('红绿灯倒计时')
if (!this.timer) {
this.timer = setTimeout(() => {
this.setState(new GreenLightState(this.lightContext))
this.reset()
}, this.options.redGreenDuration * 1000)
}
}
}
红绿灯状态是在红灯类里面切换的,所以我们还需要修改一下 RedLightState,代码修改如下:
diff
class RedLightState extends TrafficLightState {
onTick() {
// 省略 ...
if (!this.timer) {
this.timer = setTimeout(() => {
- this.setState(new GreenLightState(this.lightContext))
+ this.setState(new RedGreenLightState(this.lightContext))
this.reset()
}, this.options.redDuration * 1000)
}
}
}
接着我们就可以重新启动我们的红绿灯程序了:
javascript
const el = document.getElementById('lightText')
// 启动红绿灯状态切换
new TrafficLight(el, { redDuration: 5, redGreenDuration: 4, greenDuration: 3, yellowDuration: 2 })
我们发现现在可以非常方便地添加新的状态和行为了,不再需要大量修改其他类或方法中的代码了。 所以经过 OOP 实现状态模式既可以给我们的程序提供 OOP 编程的优点:可重用性、可扩展性、封装、继承、多态性以及易于理解和维护的特性,同时又拥有状态模式的优点,状态模式通过将复杂的状态转换逻辑分散到各个状态类中,使得每个状态类的逻辑相对简单,易于理解和使用。同时,上下文环境类也变得相对简单,只需维护对当前状态的引用即可,此外状态模式使得状态的切换变得简单明了,只需改变上下文环境类中的当前状态引用即可实现状态的切换,无需编写复杂的条件判断语句,而这些都极大地提高了软件开发的效率和质量。
状态模式与策略模式的区别
如果有了解过策略模式的同学就知道策略模式就是为了消除 if else 语句而生的,而我们上文中也讲到了状态模式也可以消除 if else 语句,那么它们之间到底有什么区别呢?
在 Vue 技术栈中所用到的 VueRouter 他的实现就使用到了策略模式,那么下面我们来简单实现一个路由。VueRouter 的两种主要路由模式包括 Hash 模式和 History 模式。我们在本篇当中只简单实现 Hash 模式的路由。
简单的实现代码如下:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple Router</title>
</head>
<body>
<ul>
<li><a href="#/about">关于我</a></li>
<li><a href="#/contact">联系我</a></li>
</ul>
<!-- 渲染路由对应的 UI -->
<div id="routeView"></div>
<script>
// 获取路由视图元素的引用
const routerView = document.getElementById('routeView')
// 处理哈希变化
function updateView() {
// 获取当前 hash 值并去掉 # 符号
const currPath = window.location.hash.slice(1)
if (currPath === '/about') {
routerView.textContent = '我是掘金签约作者'
} else if (currPath === '/contact') {
routerView.textContent = '我的微信号是:iamcobyte'
}
}
// 监听DOM加载完成事件
window.addEventListener('DOMContentLoaded', updateView)
// 监听哈希值变化事件
window.addEventListener('hashchange', updateView)
</script>
</body>
</html>
通过代码我们可以看到路由的 Hash 模式的实现原理是监听 hashchange 事件 ,当 URL 的 hash 值发生改变时,浏览器会触发 hashchange 事件,VueRouter 会根据新的 hash 值找到对应的路由,并导航到该路由,由于 hash 值的变化并不会导致浏览器向服务器发送请求,因此页面不会重新加载。
通过上文我们知道通过 OOP 编程可以避免我们的变量收到全局污染,也就是在类中将数据(属性)和处理数据的操作(方法)封装在一起,形成一个不可分割的整体,从而避免全局变量污染。
所以我们通过 OOP 重新实现一遍,代码如下:
javascript
// Router 类
class Router {
constructor(viewId) {
this.routerView = document.getElementById(viewId)
// 监听哈希值变化事件
window.addEventListener('hashchange', this.updateView.bind(this))
// 监听DOM加载完成事件
window.addEventListener('DOMContentLoaded', this.updateView.bind(this))
}
// 更新视图的方法
updateView() {
// 获取当前 hash 值并去掉 # 符号
const currPath = window.location.hash.slice(1)
if (currPath === '/about') {
this.routerView.textContent = '我是掘金签约作者'
} else if (currPath === '/contact') {
this.routerView.textContent = '我的微信号是:iamcobyte'
}
}
}
// 创建 Router 实例并传入视图元素的 ID
new Router('routeView')
虽然我们通过 OOP 的方式迭代了我们路由功能,但上述代码中 Router 类中的 updateView 方法还明显存在缺点:硬编码的路由处理。updateView 方法内直接通过 if else 条件语句来判断 currPath 是否等于某个具体的路由字符串,并据此设置 routerView 的内容。这种方式在路由数量增加时会导致 updateView 方法变得冗长和难以维护。
我们可以通过策略模式来消除上述 updateView 方法中的 if else 语句。创建一个路由映射表(或对象)的策略对象,将路由路径映射到对应的内容策略。这样,当需要添加新的路由时,只需在映射表中添加新的策略配置,而无需修改 updateView 方法了。
迭代后的代码如下:
javascript
// Router 类
class Router {
constructor(viewId, routes) {
this.routerView = document.getElementById(viewId)
this.routes = routes
// 监听哈希值变化事件
window.addEventListener('hashchange', this.updateView.bind(this))
// 监听DOM加载完成事件
window.addEventListener('DOMContentLoaded', this.updateView.bind(this))
}
// 更新视图的方法
updateView() {
// 获取当前 hash 值并去掉 # 符号
const currPath = window.location.hash.slice(1)
this.routerView.textContent = this.routes[currPath]
}
}
// 路由内容映射策略
const routes = {
'/about': '我是掘金签约作者', // about 的内容策略
'/contact': '我的微信号是:iamcobyte' // contact 的内容策略
};
// 创建 Router 实例并传入视图元素的 ID以及路由配置策略
new Router('routeView', routes)
经过使用策略模式进行迭代我们的代码后,updateView 方法中的 if else 语句消除了。至此我们可以总结一下状态模式与策略模式的区别了。
状态模式通常通过将对象的状态封装到不同的状态类中来实现,状态之间的切换是由状态对象隐式改变的。策略模式则是通过将算法或行为封装到独立的策略类中来实现,而策略的更改则是由客户端或者用户显式地选择所需的策略或算法来改变对象的行为。通常一轮执行中状态模式会切换多种状态行为,而策略模式只会切换一种策略行为。此外状态模式中状态之间的切换规则也在编写程序的时候规定好了,而策略模式的切换则可以由客户端或者用户随时更改。所以状态模式与策略模式还是有本质的区别的。
总结
状态模式通过将每个状态封装为独立类,彻底消除了冗长的条件分支,使代码符合"开放/封闭"原则。每个状态类只需专注自身行为与转移逻辑,上下文对象仅委托当前状态执行操作,这种结构精准对应了有限状态机的"状态决定行为,行为触发转移"的本质。
该模式显著提升了系统的可维护性和可扩展性。新增或修改状态只需增减相应的状态子类,无需改动现有代码,降低了变更风险;同时状态逻辑内聚,便于单元测试,使系统演进更加稳健。
需注意状态模式与策略模式的根本区别:策略模式由客户端主动选择算法,策略之间相互独立;而状态模式的状态转换由内部规则自动触发,外界无法干预。理解这一差异,有助于在合适场景中应用设计模式,并为深入理解编译原理中的状态机技术奠定基础。