一、什么是設計模式?
設計模式是一套被反復使用、多數(shù)人知曉、經過分類編目的、代碼設計經驗的總結。它是為了可重用代碼,讓代碼更容易的被他人理解并保證代碼的可靠性。
設計模式實際上是“拿來主義”在軟件領域的貫徹實踐,它是一套現(xiàn)成的工具,拿來即用。下面來看一下設計模式的設計原則。
二、設計幾個原則
單一職責原則、開放封閉原則、里式替換原則、接口隔離原則 、依賴反轉原則 、最少知識原則。
三、常見的設計模式及實際案例
單例模式、工廠模式、策略模式、代理模式、適配器模式、觀察者模式/發(fā)布-訂閱模式
【1】單例模式
1. 什么是單例模式?
單例模式 (Singleton Pattern)又稱為單體模式,保證一個類只有一個實例,并提供一個訪問它的全局訪問點。也就是說,第二次使用同一個類創(chuàng)建新對象的時候,應該得到與第一次創(chuàng)建的對象完全相同的對象。
2.Vue中的單例模式
(1)Element UI
Element UI是使用Vue開發(fā)的一個前端UI框架。ElementUI 中的全屏 Loading 蒙層調用有兩種形式:
●指令形式:Vue.use(Loading.directive)
=>使用方式 :<div :v-loading.fullscreen="true">...</div>;
●服務形式:Vue.prototype.$loading = service
=>使用方式 :this.$loading({ fullscreen: true });
用服務方式使用全屏 Loading 是單例的,即在前一個全屏 Loading 關閉前再次調用全屏 Loading,并不會創(chuàng)建一個新的 Loading 實例,而是返回現(xiàn)有全屏 Loading 的實例。下面是 ElementUI 實現(xiàn)全屏 Loading 的源碼:
import Vue from 'vue'
import loadingVue from './loading.vue'
const LoadingConstructor = Vue.extend(loadingVue)
let fullscreenLoading
const Loading = (options = {}) => {
if (options.fullscreen && fullscreenLoading) {
return fullscreenLoading
}
let instance = new LoadingConstructor({
el: document.createElement('div'),
data: options
})
if (options.fullscreen) {
fullscreenLoading = instance
}
return instance
}
export default Loading
這里的單例是 fullscreenLoading,是存放在閉包中的,如果用戶傳的 options 的 fullscreen 為 true 且已經創(chuàng)建了單例,則直接返回之前創(chuàng)建的單例,如果之前沒有創(chuàng)建過,則創(chuàng)建單例并賦值給閉包中的 fullscreenLoading 后返回新創(chuàng)建的單例實例。
(2)Vuex
Vuex 是一個專為 Vue.js 應用程序開發(fā)的狀態(tài)管理模式。Vuex,它們都實現(xiàn)了一個全局的 Store 用于存儲應用的所有狀態(tài)。這個 Store 的實現(xiàn),正是單例模式的典型應用。Vuex 使用單一狀態(tài)樹,用一個對象就包含了全部的應用層級狀態(tài)。至此它便作為一個“唯一數(shù)據(jù)源 (SSOT)”而存在。這也意味著,每個應用將僅僅包含一個 store 實例。單一狀態(tài)樹讓我們能夠直接地定位任一特定的狀態(tài)片段,在調試的過程中也能輕易地取得整個當前應用狀態(tài)的快照。——Vuex官方文檔
// 安裝vuex插件
Vue.use(Vuex)
// 將store注入到Vue實例中
new Vue({
el: '#app',
store
})
通過調用Vue.use()方法,安裝了 Vuex 插件。Vuex 插件是一個對象,它在內部實現(xiàn)了一個 install 方法,這個方法會在插件安裝時被調用,從而把 Store 注入到Vue實例里去。也就是說每 install 一次,都會嘗試給 Vue 實例注入一個 Store。
let Vue // 這個Vue的作用和樓上的instance作用一樣
...
export function install (_Vue) {
// 判斷傳入的Vue實例對象是否已經被install過Vuex插件(是否有了唯一的state)
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
// 若沒有,則為這個Vue實例對象install一個唯一的Vuex
Vue = _Vue
// 將Vuex的初始化邏輯寫進Vue的鉤子函數(shù)里
applyMixin(Vue)
}
可以保證一個 Vue 實例(即一個 Vue 應用)只會被 install 一次 Vuex 插件,所以每個 Vue 實例只會擁有一個全局的 Store。
【2】工廠模式
1. 什么是工廠模式?
工廠模式就是根據(jù)不用的輸入返回不同的實例,一般用來創(chuàng)建同一類對象,它的主要思想就是將對象的創(chuàng)建與對象的實現(xiàn)分離。
在創(chuàng)建對象時,不暴露具體的邏輯,而是將邏輯封裝在函數(shù)中,那么這個函數(shù)就可以被視為一個工廠。工廠模式根據(jù)抽象程度的不同可以分為:簡單工廠、工廠方法、抽象工廠。
2.Vue中的工廠模式
(1)VNode
和原生的 document.createElement 類似,Vue 這種具有虛擬 DOM 樹(Virtual Dom Tree)機制的框架在生成虛擬 DOM 的時候,提供了 createElement 方法用來生成 VNode,用來作為真實 DOM 節(jié)點的映射:
createElement('h3', { class: 'main-title' }, [
createElement('img', { class: 'avatar', attrs: { src: '../avatar.jpg' } }),
createElement('p', { class: 'user-desc' }, 'hello world')
])
createElement 函數(shù)結構大概如下:
class Vnode (tag, data, children) { ... }
function createElement(tag, data, children) {
return new Vnode(tag, data, children)
}
(2)vue-route
在Vue在路由創(chuàng)建模式中,也多次用到了工廠模式:
export default class VueRouter {
constructor(options) {
this.mode = mode // 路由模式
switch (mode) { // 簡單工廠
case 'history': // history 方式
this.history = new HTML5History(this, options.base)
break
case 'hash': // hash 方式
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract': // abstract 方式
this.history = new AbstractHistory(this, options.base)
break
default:
// ... 初始化失敗報錯
}
}
}
mode 是路由創(chuàng)建的模式,這里有三種 History、Hash、Abstract,其中,History 是 H5 的路由方式,Hash 是路由中帶 # 的路由方式,Abstract 代表非瀏覽器環(huán)境中路由方式,比如 Node、weex 等;this.history 用來保存路由實例,vue-router 中使用了工廠模式的思想來獲得響應路由控制類的實例。
【3】策略模式
1. 什么是策略模式?
策略模式 (Strategy Pattern)又稱政策模式,其定義一系列的算法,把它們一個個封裝起來,并且使它們可以互相替換。封裝的策略算法一般是獨立的,策略模式根據(jù)輸入來調整采用哪個算法。關鍵是策略的實現(xiàn)和使用分離。
2.策略模式的實際應用
(1)表格 formatter
Element UI 的表格控件的 Column 接受一個 formatter 參數(shù),用來格式化內容,其類型為函數(shù),并且還可以接受幾個特定參數(shù),像這樣:Function(row, column, cellValue, index)。
以文件大小轉化為例,后端經常會直接傳 bit 單位的文件大小,那么前端需要根據(jù)后端的數(shù)據(jù),根據(jù)需求轉化為自己需要的單位的文件大小,比如 KB/MB。
首先實現(xiàn)文件計算的算法:
export const StrategyMap = {
// Strategy 1: 將文件大?。╞it)轉化為 KB
bitToKB: val => {
const num = Number(val)
return isNaN(num) ? val : (num / 1024).toFixed(0) + 'KB'
},
// Strategy 2: 將文件大小(bit)轉化為 MB
bitToMB: val => {
const num = Number(val)
return isNaN(num) ? val : (num / 1024 / 1024).toFixed(1) + 'MB'
}
}
// Context: 生成el表單 formatter
const strategyContext = function(type, rowKey){
return function(row, column, cellValue, index){
StrategyMap[type](row[rowKey])
}
}
export default strategyContext
在組件中直接使用:
<template>
<el-table :data="tableData">
<el-table-column prop="date" label="日期"></el-table-column>
<el-table-column prop="name" label="文件名"></el-table-column>
<!-- 直接調用 strategyContext -->
<el-table-column prop="sizeKb" label="文件大小(KB)"
:formatter='strategyContext("bitToKB", "sizeKb")'>
</el-table-column>
<el-table-column prop="sizeMb" label="附件大小(MB)"
:formatter='strategyContext("bitToMB", "sizeMb")'>
</el-table-column>
</el-table>
</template>
<script type='text/javascript'>
import strategyContext from './strategyContext.js'
export default {
name: 'ElTableDemo',
data() {
return {
strategyContext,
tableData: [
{ date: '2019-05-02', name: '文件1', sizeKb: 1234, sizeMb: 1234426 },
{ date: '2019-05-04', name: '文件2', sizeKb: 4213, sizeMb: 8636152 }]
}
}
}
</script>
<style scoped></style>
運行結果如下圖
(2)表單驗證
除了表格中的 formatter 之外,策略模式也經常用在表單驗證的場景。Element UI 的 Form 表單 具有表單驗證功能,用來校驗用戶輸入的表單內容。實際需求中表單驗證項一般會比較復雜,所以需要給每個表單項增加 validator 自定義校驗方法。
實現(xiàn)通用的表單驗證方法:
// src/utils/validates.js
// 姓名校驗 由2-10位漢字組成
export function validateUsername(str) {
const reg = /^[\u4e00-\u9fa5]{2,10}$/
return reg.test(str)
}
// 手機號校驗 由以1開頭的11位數(shù)字組成
export function validateMobile(str) {
const reg = /^1\d{10}$/
return reg.test(str)
}
// 郵箱校驗
export function validateEmail(str) {
const reg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/
return reg.test(str)
}
增加一個柯里化方法,用來生成表單驗證函數(shù):
// src/utils/index.js
import * as Validates from './validates.js'
// 生成表格自定義校驗函數(shù)
export const formValidateGene = (key, msg) => (rule, value, cb) => {
if (Validates[key](value)) {
cb()
} else {
cb(new Error(msg))
}
}
具體使用:
<template>
<el-form ref="ruleForm"
label-width="100px"
class="demo-ruleForm"
:rules="rules"
:model="ruleForm">
<el-form-item label="用戶名" prop="username">
<el-input v-model="ruleForm.username"></el-input>
</el-form-item>
<el-form-item label="手機號" prop="mobile">
<el-input v-model="ruleForm.mobile"></el-input>
</el-form-item>
<el-form-item label="郵箱" prop="email">
<el-input v-model="ruleForm.email"></el-input>
</el-form-item>
</el-form>
</template>
<script type='text/javascript'>
import * as Utils from '../utils'
export default {
name: 'ElTableDemo',
data() {
return {
ruleForm: { pass: '', checkPass: '', age: '' },
rules: {
username: [{
validator: Utils.formValidateGene('validateUsername', '姓名由2-10位漢字組成'),
trigger: 'blur'
}],
mobile: [{
validator: Utils.formValidateGene('validateMobile', '手機號由以1開頭的11位數(shù)字組成'),
trigger: 'blur'
}],
email: [{
validator: Utils.formValidateGene('validateEmail', '不是正確的郵箱格式'),
trigger: 'blur'
}]
}
}
}
}
</script>
效果如圖:
【4】代理模式
1. 什么是代理模式?
代理模式 (Proxy Pattern)又稱委托模式,它為目標對象創(chuàng)造了一個代理對象,以控制對目標對象的訪問。
代理模式把代理對象插入到訪問者和目標對象之間,從而為訪問者對目標對象的訪問引入一定的間接性。正是這種間接性,給了代理對象很多操作空間,比如在調用目標對象前和調用后進行一些預操作和后操作,從而實現(xiàn)新的功能或者擴展目標的功能。
2.代理模式在實戰(zhàn)中的應用
(1)攔截器
在項目中經常使用 Axios 的實例來進行 HTTP 的請求,使用攔截器 interceptor 可以提前對 request 請求和 response 返回進行一些預處理,比如:
1、request 請求頭的設置,和 Cookie 信息的設置;
2、權限信息的預處理,常見的比如驗權操作或者 Token 驗證;
3、數(shù)據(jù)格式的格式化,比如對組件綁定的 Date 類型的數(shù)據(jù)在請求前進行一些格式約定好的序列化操作;
4、空字段的格式預處理,根據(jù)后端進行一些過濾操作;
5、response 的一些通用報錯處理,比如使用 Message 控件拋出錯誤;
除了 HTTP 相關的攔截器之外,還有路由跳轉的攔截器,可以進行一些路由跳轉的預處理等操作。
(2)前端框架的數(shù)據(jù)響應式化
Vue 2.x 中通過 Object.defineProperty 來劫持各個屬性的 setter/getter,在數(shù)據(jù)變動時,通過發(fā)布-訂閱模式發(fā)布消息給訂閱者,觸發(fā)相應的監(jiān)聽回調,從而實現(xiàn)數(shù)據(jù)的響應式化,也就是數(shù)據(jù)到視圖的雙向綁定。
為什么 Vue 2.x 到 3.x 要從 Object.defineProperty 改用 Proxy 呢,是因為前者的一些局限性,導致的以下缺陷
:
1、無法監(jiān)聽利用索引直接設置數(shù)組的一個項,例如:vm.items[indexOfItem] = newValue;
2、無法監(jiān)聽數(shù)組的長度的修改,例如:vm.items.length = newLength;
3、無法監(jiān)聽 ES6 的 Set、WeakSet、Map、WeakMap 的變化;
4、無法監(jiān)聽 Class 類型的數(shù)據(jù);
5、無法監(jiān)聽對象屬性的新加或者刪除;
【5】適配器模式
1. 什么是適配器模式?
適配器模式(Adapter Pattern)又稱包裝器模式,將一個類(對象)的接口(方法、屬性)轉化為用戶需要的另一個接口,解決類(對象)之間接口不兼容的問題。
主要功能是進行轉換匹配,目的是復用已有的功能,而不是來實現(xiàn)新的接口。也就是說,訪問者需要的功能應該是已經實現(xiàn)好了的,不需要適配器模式來實現(xiàn),適配器模式主要是負責把不兼容的接口轉換成訪問者期望的格式而已。
2.適配器的實際案例
(1)Vue 計算屬性
Vue 中的計算屬性也是一個適配器模式的實例,以官網(wǎng)的例子為例:
<template>
<div id="example">
<p>Original message: "{{ message }}"</p> <!-- Hello -->
<p>Computed reversed message: "{{ reversedMessage }}"</p> <!-- olleH -->
</div>
</template>
<script type='text/javascript'>
export default {
name: 'demo',
data() {
return {
message: 'Hello'
}
},
computed: {
reversedMessage: function() {
return this.message.split('').reverse().join('')
}
}
}
</script>
對原有數(shù)據(jù)并沒有改變,只改變了原有數(shù)據(jù)的表現(xiàn)形式。
(2) 源碼中的適配器模式
Axios 的用來發(fā)送請求的 adapter 本質上是封裝瀏覽器提供的 API XMLHttpRequest。
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data
var requestHeaders = config.headers
var request = new XMLHttpRequest()
// 初始化一個請求
request.open(config.method.toUpperCase(),
buildURL(config.url, config.params, config.paramsSerializer), true)
// 設置最大超時時間
request.timeout = config.timeout
// readyState 屬性發(fā)生變化時的回調
request.onreadystatechange = function handleLoad() { ... }
// 瀏覽器請求退出時的回調
request.onabort = function handleAbort() { ... }
// 當請求報錯時的回調
request.onerror = function handleError() { ... }
// 當請求超時調用的回調
request.ontimeout = function handleTimeout() { ... }
// 設置HTTP請求頭的值
if ('setRequestHeader' in request) {
request.setRequestHeader(key, val)
}
// 跨域的請求是否應該使用證書
if (config.withCredentials) {
request.withCredentials = true
}
// 響應類型
if (config.responseType) {
request.responseType = config.responseType
}
// 發(fā)送請求
request.send(requestData)
})
}
這個模塊主要是對請求頭、請求配置和一些回調的設置,并沒有對原生的 API 有改動,所以也可以在其他地方正常使用。這個適配器可以看作是對 XMLHttpRequest 的適配,是用戶對 Axios 調用層到原生 XMLHttpRequest 這個 API 之間的適配層。
【5】觀察者模式/發(fā)布-訂閱模式
1. 什么是觀察者模式?
觀察者模式(Observer Pattern)定義了一種一對多的關系,讓多個訂閱者對象同時監(jiān)聽某一個發(fā)布者,或者叫主題對象,這個主題對象的狀態(tài)發(fā)生變化時就會通知所有訂閱自己的訂閱者對象,使得它們能夠自動更新自己。
2. 什么是發(fā)布-訂閱模式?
其實它是發(fā)布訂閱模式的一個別名,但兩者又有所不同。這個別名非常形象地詮釋了觀察者模式里兩個核心的角色要素——發(fā)布者和訂閱者。
發(fā)布-訂閱模式有一個調度中心
觀察者模式是由具體目標調度的,而發(fā)布-訂閱模式是統(tǒng)一由調度中心調的
3.Vue中的發(fā)布-訂閱模式
(1)EventBus
在Vue中有一套事件機制,其中一個用法是 EventBus??梢允褂?EventBus 來解決組件間的數(shù)據(jù)通信問題。
1.創(chuàng)建事件中心管理組件之間的通信
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
2.發(fā)送事件
<template>
<div>
<first-com></first-com>
<second-com></second-com>
</div>
</template>
<script>
import firstCom from './firstCom.vue'
import secondCom from './secondCom.vue'
export default {
components: { firstCom, secondCom }
}
</script>
firstCom組件中發(fā)送事件:
<template>
<div>
<button @click="add">加法</button>
</div>
</template>
<script>
import {EventBus} from './event-bus.js' // 引入事件中心
export default {
data(){
return{
num:0
}
},
methods:{
add(){
EventBus.$emit('addition', {
num:this.num++
})
}
}
}
</script>
3.接收事件
在secondCom組件中發(fā)送事件:
<template>
<div>求和: {{count}}</div>
</template>
<script>
import { EventBus } from './event-bus.js'
export default {
data() {
return {
count: 0
}
},
mounted() {
EventBus.$on('addition', param => {
this.count = this.count + param.num;
})
}
}
</script>
(2)Vue源碼
發(fā)布-訂閱模式在源碼中應用很多,比如雙向綁定機制的場景
響應式化大致就是使用 Object.defineProperty 把數(shù)據(jù)轉為 getter/setter,并為每個數(shù)據(jù)添加一個訂閱者列表的過程。這個列表是 getter 閉包中的屬性,將會記錄所有依賴這個數(shù)據(jù)的組件。也就是說,響應式化后的數(shù)據(jù)相當于發(fā)布者。
每個組件都對應一個 Watcher 訂閱者。當每個組件的渲染函數(shù)被執(zhí)行時,都會將本組件的 Watcher 放到自己所依賴的響應式數(shù)據(jù)的訂閱者列表里,這就相當于完成了訂閱,一般這個過程被稱為依賴收集(Dependency Collect)。
組件渲染函數(shù)執(zhí)行的結果是生成虛擬 DOM 樹(Virtual DOM Tree),這個樹生成后將被映射為瀏覽器上的真實的 DOM樹,也就是用戶所看到的頁面視圖。
當響應式數(shù)據(jù)發(fā)生變化的時候,也就是觸發(fā)了 setter 時,setter 會負責通知(Notify)該數(shù)據(jù)的訂閱者列表里的 Watcher,Watcher 會觸發(fā)組件重渲染(Trigger re-render)來更新(update)視圖。文章來源:http://www.zghlxwxcb.cn/news/detail-429272.html
Vue 的源碼:文章來源地址http://www.zghlxwxcb.cn/news/detail-429272.html
// src/core/observer/index.js 響應式化過程
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// ...
const value = getter ? getter.call(obj) : val // 如果原本對象擁有getter方法則執(zhí)行
dep.depend() // 進行依賴收集,dep.addSub
return value
},
set: function reactiveSetter(newVal) {
// ...
if (setter) { setter.call(obj, newVal) } // 如果原本對象擁有setter方法則執(zhí)行
dep.notify() // 如果發(fā)生變更,則通知更新
}
})
到了這里,關于【Vue】Vue 前端設計模式梳理的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!