JavaScript和前端框架中的设计模式 | 青训营

62 阅读5分钟

JavaScript中的设计模式

  • 原型模式
  • 代理模式
  • 迭代器模式

javascript 中提供的api可以帮助实现这几种模式

原型模式

原型链中的原型就是原型模式

  1. 定义:复制已有对象来创建新的对象
  2. 应用场景:JS 中对象创建的基本模式

在某些特定的场景中,庞大对象的初始化,复制一个已有对象来创建一个新对象会有更好的性能,内存占用也会更少

在JavaScript中是常见的模式,但在其他语言中几乎不会用到

用原型模式创建上线订阅中的用户

javascript 提供Object.create(obj)基于obj创建一个对象

const baseUser: User = {
    name: "",
    status: "offline",
    followers: []

    subscribe(user: User, notify: Notify){
        user.followers.push({user, notify})
    },
    online(){
        this.status = "online"
        this.followers.forEach(({notify} => {
            notify(this)
        })
    }
}

// 复制已有对象创建新对象
export const createUser = (name: string) => {
    // 以baseUser为原型创建一个新的对象user
    const user: User = Object.create(baseUser)

    user.name = name
    user.followers = []
    return user
} 

代理模式

  1. 定义:可以自定义控制对象的访问方式,并且允许在更新前后做一些额外的处理
  2. 应用场景:监控,代理工具,前端框架实现等

使用代理模式实现用户状态订阅

单一原则,一个函数只做一件事情

这里的online既改变了status的状态,又通知所有notify函数的执行,实际上做了两件事情,不利于后续增加日志以及代码的添加和重构

type Notify = (user:User) => void    // 订阅者(订阅者也可以是一个函数)

export class User {
    name:string
    status: "offline" | "online"
    followers: {user: User; notify: Notify}[]    // 存储不同的user对应的回调函数(订阅)

    constructor(name: string){
        this.name = name
        this.status = "offline"
        this.followers = []
    }
    // 订阅
    subscribe(user: User, notify: Notify){
        user.followers.push({user, notify})
    }
    // 发布
    online(){
        this.status = "online"
        this.followers.forEach(({notify} => {
            notify(this)
        })
    }
}

用代理模式对online函数进行优化

  1. online只做一件事情
class User {
    //....
    online(){
        this.status = "online"
    }
}
  1. 代理

使用代理方式可以根据需要对代理对象进行配置和处理,使得函数更加可扩展和可复用。

采用createProxyUser将创建对象和代理行为包起来可以起到分支和隔离的作用

  1. 封装:createProxyUser函数充当了一个工厂函数的角色,负责创建带有代理功能的用户对象。
  2. 隔离性:可以限制代理对象的作用范围,仅在createProxyUser函数内部可见,可以避免代理对象的误用或直接修改,提高代码的安全性和可维护性。
export const createProxyUser = (name: string) => {
    const user = new User(name)

    const proxyUser = new Proxy(user, {
        set: (target, prop: keyof User, value) => {
            target[prop] = value
            // 如果是status的值发生变化则执行notifyStatusHandlers函数
            if(prop === 'status') {
                notifyStatusHandlers(target, value)
                // 如果要监听或status的值改变需要触发什么新的行为,都可以在继续加
                // ...         
            }
        }
    })

    const notifyStatusHandlers = (user: User, status: 'online' | 'offline') => {
        if(status === 'online') {
            user.followers.forEach({notify} => {
                notify(user)
            })
        }    
    }
    return prosyUser

    // 可以添加其他的新行为
    // ...
}

迭代器模式

  1. 定义:在不暴露数据类型的情况下访问集合中的数据
  2. 应用场景:数据结构中有多种数据类型,列表,树等,提供通用操作接口
const number = [1, 2, 3]

const map = new Map()
map.set('k1', 'v1')
map.set('k2', 'v2')

const set = new Set(['1', '2', '3'])
for(const number of numbers) {}
for(const [key, value] of map){}
for(const key of set) {}

for...of...

for...of 要求目标对象实现了迭代器接口,即对象必须具有 Symbol.iterator 属性,并且该属性是一个函数,返回一个迭代器对象。

特性:

  1. 遍历可迭代对象:for...of 可以用于遍历实现了迭代器(Iterator)接口的数据结构,包括数组、字符串、Set、Map 等。它提供了一种简洁的方式来访问可迭代对象中的每个元素。
  2. 遍历值而非索引:for...of 可以直接获取可迭代对象中的值,而不是索引
  3. 不支持普通对象:for...of 不适用于普通对象的遍历。如果需要遍历普通对象的属性,可以使用 for...in 循环。
  4. 对象不支持顺序遍历:for...of 在遍历集合时是按顺序进行的,但在遍历对象属性时并不保证顺序,因为对象属性之间没有固定的顺序。

性能:

  1. 性能较好:相比传统的 for 循环或 forEach 方法,for...of 通常具有更好的性能表现。这是因为它基于迭代器的机制,在每次迭代时都会调用迭代器的 next() 方法,无需在每次迭代中重新计算数组长度或手动管理索引
  2. 不支持修改:由于 for...of 是基于迭代器的机制,它并不支持修改可迭代对象的值。如果在循环过程中修改了可迭代对象,可能会导致意外的行为或错误结果。如果需要修改可迭代对象,建议使用传统的 for 循环或其他遍历方式。

用 for...of 迭代所有组件

//
class MyDomElement {
    tag: string
    children: MyDomElement[]
    constructor(tag: string) {
        this.tag = tag
        this.children = []
    }

    addChildren(component: MyDomElement) {
        this.children.push(component)
    }

    // 迭代方法,使该类实例化的对象可以用for...of进行迭代
    [Symbol.iterator]() {
        const list = [...this.children]
        let node

        return {
            next: () => {
                while((node = list.shift()){    // list 中还有元素
                    node.children.length > 0 && list.push(...node.children)

                    return { value: node, done: false }
                }
                return { value: null, done: true }
            }
        }
    }
}
// 测试用例
test("can iterate root element", () => {
    const body = new MyDomElement("body")
    const header = new MyDomElement("header")
    const main = new MyDomElement("main")
    const banner = new MyDomElement("banner")
    const content = new MyDomElement("content")
    const footer = new MyDomElement("footer")

    body.addChildren(header)
    body.addChildren(main)
    body.addChildren(footer)
    main.addChildren(banner)
    main.addChildren(content)

    const expectTags: string[] = []
    for (const element of body) {
        if (element) {
            expectTags.push(element.tag)
        }
    }
    expect(expectTags.length).toBe(5);
})

前端框架中的设计模式

代理模式

前端框架中对DOM操作的代理

在前端框架中操作的DOM 都是代理后的DOM,代理的DOM会和真正的DOM进行Diff操作,最后才进行视图的更新

这种代理在vue3中是用proxy实现的,在react中是用另外的方式,但原理都是当更改DOM时,真正改的都是代理后的虚拟DOM

image.png

组合模式

  1. 定义:可以多个对象组合使用,也可以单个对象独立使用
  2. 应用场景:DOM,前端组件,文件目录,部门

总结

  • 总结出抽象的模式相对简单,但是想要将抽象的模式套用到场景中却比较困难
  • 现代编程语言的多编程范式带来的更多可能性(函数式编程等)