《Javascript设计模式核心原理与应用实践》笔记整理二

1,062 阅读7分钟

一、结构型1 - 装饰器模式

定义: 不改变原对象的基础上,包装拓展,使其可满足更复杂需求。

装饰器应用场景:

比如这样一个场景:①要给所有按钮加上弹窗校验。②校验未通过时将按钮文本修改并置灰。需要满足不同按钮的自定义需求。如果第一步已经由别人完成,第二步是之后的扩展,扩展时不想关注原来的逻辑,只想进行扩充,这时候就用到装饰器模式了。

装饰器第一步: 将旧逻辑先抽出去。(用到单例模式的案例)

// 弹窗逻辑的抽取
function openModal() {
    const modal = new Modal()
    modal.style.display = 'block'
}

装饰器第二步: 写新逻辑

// 按钮文本修改逻辑
function changeBtnText() {
    const btn = document.querySelector('#open')
    btn.innerText = '去登录'
}

// 按钮置灰逻辑
function btnGray() {
    const btn = document.querySelector('#open')
    btn.setAttribute('disabled', true)
}

// 新功能整合
function changeBtn() {
    changeBtnText()
    btnGray()
}

然后把处理逻辑放到按钮中

openBtn.addEventListener('click', ()=>{
    openModal()
    changeBtn()
})

这样实现了只添加不修改。

ES6 中用面向对象方式来写上面的代码:

class OpenModal {
    onClick: function(){
        const modal = new Modal()
        modal.style.display = 'block'
    }
}

class BtnDecorator {
    // 将按钮实例传入
    constructor(btn){
        this.btn = btn
    }
    onClick: function(){
        this.btn.onClick()
        // 包装一层新逻辑
        this.btnChange()
    }
    btnChange(){
        this.changeBtnText()
        this.makeBtnDisabled()
    }
    changeBtnText(){
        const btn = document.querySelector('#open')
        btn.innerText = '先登录'
    }
    makeBtnDiabled() {
        const btn = document.querySelector('#open')
        btn.setAttribute('disabled', true)
    }
}

const openModal = new OpenModal()
const btnDecor = new BtnDecorator()

btn.addEventListener('click',function(){
    // 下面这两种点击,一个是只有弹窗,一个是补充了新功能
    // openModal.onclick()
    btnDecor.onclick()
})

在ES7中Decorator模式直接可以拿来用。这是一个语法糖。

装饰器的原理:

装饰器定义的方式: 定义装饰器函数,把被装饰者传给装饰器函数。

ES7中的@decorator装饰器就是一个语法糖,他帮我们做了一些工作:①函数传参及调用;②将“属性装饰器”交到你手里。

给类添加装饰器

function classDecorator(target){
    target.hasDecorator = true
    return target
}

// 将装饰器安装到Button上
@classDecorator
class Button {
    // ...
}

给方法添加装饰器

看不懂下面一段代码的意思

function funcDecorator(target, name, descriptor) {
    let originalMethod = descorator.value
    descorator.value = function() {
        console.log('我是func的装饰器逻辑')
        return originalMethod.apply(this, arguments)
    }
    return descriptor
}

class Button {
    @funcDecorator
    onClick() {
        console.log('我是func的原有逻辑')
    }
}

这时候target变成了Button.prototype,就是类的原型对象。这是因为onClick方法总是要依附其实例存在的,修饰onClick其实是修饰他的实例。

装饰器函数是在编译阶段执行的,实例是在运行阶段执行。所以装饰器函数真正触及的就是在类层面,不在实例层面。

???

在类装饰器中传入一个target就行了,但是在上例中方法装饰器有三个参数。第一个参数target:是类的原型。第二个参数:是修饰的目标属性名。第三个参数经常用:属性描述对象。就跟Object.defineProperty的第三个参数一样,由多个属性描述符组成。分为数据描述符(比如value、writteble)和存取描述符(get、set)。拿到这个值,就拿到了目标方法的属性控制权。

用例:React中的高阶组件

高阶组件就是一个函数,接收一个组件作为参数,返回一个新组件。

他是装饰器模式在react中的实践。

下例:把一个组件丢到有红色边框的容器里。

import React, { Component } from 'react'

const HOCdemo = (WrapperComponent)=> class extends Component {
    render() {
        return <div style={{border: '2px solid #FF00'}}>
            <WrapperComponent/>
        </div>
    }
}
export default HOCdemo

咋装饰呢

import React, { Component } form 'react'
import HocDemo from './HOCdemo'

@HocDemo
class TargetDemo extends React.Component {
    render(){
        // 处理
    }
}
export default TargetDemo

使用装饰器改写Redux的connect

在React中用Redux的时候,一般需要用connect将状态和组件绑起来。

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import action from './action.js'

class App extends Component {
    render() {//App的业务逻辑}
}

function mapStateToProps(state){
    // 假设App的状态对应状态树上的app节点
    return state.app
}

function mapDispatchToProps(dispatch){
    return bindActionCreators(action, dispatch)
}

// 将APP和Redux绑定
export default connect(mapStateToProps, mapDispatchToProps)(App)

connect的俩参数: 第一个是一个函数,建立组件和状态之间的映射关系,第二个参数也是函数,建立组件和store.dispatch之间的的关系,使组件具有dispatch派发状态的能力。

调用connect就返回一个具有装饰作用的函数,此函数可接收一个函数作为参数,使这个目标组件和Redux结合,具有Redux提供的数据和能力。

可以改写为这样,把connect抽取出来:

import { connect } from 'connect'
import { bindActionCreators } from 'react-redux'
import action from './action.js'

function mapStateToProps(state) {
    return state.app
}

function mapDispatchToProps(dispatch) {
    return bindActionCreators(action, dispatch)
}
// 将connect调用的结果作为一个装饰器输出
export default connect(mapStateToProps, mapDispatchToProps)

给组件加上装饰器

import React, {Component} from 'react'
import connect from './connect.js'

@connect
export default class App extends React.Component {
     render(){
    }
}

装饰器模式的优点:

灵活、复用性高,不依托任何逻辑存在。

装饰器库

code-decorators封装了一些常用的装饰器。

二、结构型2 - 适配器模式

适配器想要达到的一个目标就是: 兼容与统一

axios就用到了这种模式,实际上派发请求的dispatchRequest方法中,做了两件事:1是数据转换-将请求体、响应体里的数据格式化;2是调用适配器。

三、结构型3 - 代理模式

代理的应用挺多的,比如VPN、Nginx接口转发、等等。用到代理是因为不能直接访问。

ES6中的proxy

const proxy = new Proxy(obj, handler)

第一个参数是目标对象,handler也是一个对象,用来定义代理的行为。当我们通过proxy去访问目标对象时,handler会对我们的行为做一层拦截,也就是说每次行为都要经过handler。

例如婚介所:

const girl = {
    name: '小妹',
    age: 22,
    introduce: 'xxxxx',
    avator: 'xxxx',
    tmpAvator: 'cccc',
    mobile: '24354'
}

有些信息是私密信息,需要平台一次过滤,如果是VIP才能看

// 普通信息
const baseInfo = ['name', 'introduce', 'tmpAvator']
// 最私密
const privateInfo = ['avator', 'mobile']

// 某用户为例
const user = {
    ...// 一些必要信息
    isValidated: true,
    isVIP: false
}

// 婚介所
const hunjie = new Proxy(girl, {
    get: function(girl, key){
        if(baseInfo.indexOf(key) !==-1 && !user.isValidate) {
            alert('你还没有完成验证')
            return
        }
        if(user.isValidated && privateInfo.indexOf(key) && !user.isVIP) {
            alert('只有VIP才能查看这个信息')
        }
    }
})

代理模式的应用

四种: 事件代理、虚拟代理、缓存代理、保护代理。

1. 事件代理

很常见的一种代理。

比如一个ul>li*n的组合,每点击一个li都要有有些操作,那么需要给每个li添加一个监听,li的个数也多,开销越大。

但事件本身是可以冒泡的,从li上冒到ul上,所以我们在ul上绑定一次监听事件就可以监听到所有li的事件。

2. 虚拟代理

懒加载: 用来处理图片的加载时机,比如先用div占位,等滚动到位置后再加载。

图片预加载: 为避免网络不好时白屏,常见的操作是先给img标签弄一个占位图,创建一个image实例,让这个Image实例的src指向实际的图片,观察image实例的加载情况--当对应的真实图片加载完成,即已经有了该图片的缓存内容,再将img的src指向真实的图片。

class PreLoadImage {
    constructor(imgNode) {
        // 获取真实的dom节点
        this.imgNode = imgNode
    }
    setSrc(imgUrl){
        this.imgNode.src = imgUrl
    }
}

class ProxyImage {
    static Loading_url = 'xxx'
    constructor(targetImage) {
        this.targetImage = targetImage
    }
    setSrc(targetUrl){
        this.targetImage.setSrc(ProxyImage(Loading_url))
        const virtualImage = new Image()
        virtualImage.onload = ()=>{
            this.targetImage.setSrc(targetUrl)
        }
        virtualImage.src = targetUrl
    }
}

3. 缓存代理

计算量大的时候,如果我们已经算过某个值不想再算第二次,就从内存中取结果,这时候需要一个缓存代理,在我们计算结果的同时将计算结果缓存下来。

比如对传入的值求和

const addAll = function(){
    console.log('进行一次新计算')
    let result = 0 
    const len= arguments.length
    for(let i = 0; i<len;i++) {
        result += argument[i]
    }
    return result
}

const proxyAddAll = (function() {
    const resultCache = {}
    return function(){
        // 将入参转化为一个字符串
        const args = Array.prototype.join.call(arguments, ',')
        // 检查入参是否有对应的计算结果
        if(args in resultCache) {
            return resultCache[args]
        }
        return resultCache[args] = addAll(...arguments)
    }
})()

保护代理

就上面婚介所那个例子。