前端学习记录
  • 前言及目录
  • 前端基础
    • HTML
    • CSS
      • CSS学习之布局
    • JavaScript
      • 跟着月影学JavaScript
      • JavaScript之对象、原型链及继承
      • JavaScript中的类
      • onclick与addEventListener区别
      • JS手撕题
    • HTTP与浏览器
      • HTTP实用指南
      • Web开发的安全之旅
    • 通用知识
      • 前端必须知道的开发调试知识
      • 前端设计模式应用
      • Web 标准与前端开发
  • 数据结构及算法
    • 数据结构
      • 1、线性表(List)
      • 2、堆栈(Stack)
      • 3、队列(Queue)
      • 4、二叉树(Binary Tree)
      • 5、二叉搜索树与平衡二叉树(BST & AVL)
      • 6、堆(Stack)& 哈夫曼树 & 并查集
      • 7、图(Graph)
        • 图论——解决最小生成树问题(Kruskal算法&Prim算法)
      • 8、排序(sort)
      • 9、散列表(hash)
      • 数据结构习题
        • 第一周:最大子列和算法、二分查找
        • 第二周:线性结构
        • 第三周:栽树(二叉树等)
        • 第四周:二叉搜索树&二叉平衡树
        • 第五周:堆&哈夫曼树&并查集
        • 第六周:图(上)连通集 、DFS&BFS
        • 第七周:图(中)Floyd算法求最短路
        • 第八周:图(下)
        • 第九周:排序(上)归并&堆排序
        • 第十周:排序(下)
        • 第十一周:散列查找 & KMP
    • CS基础
      • 编译原理 实验一 词法分析器设计
      • 编译原理 实验二 LL(1)分析法程序
    • LeetCode
      • 冲刺春招-精选笔面试 66 题大通关
        • day1:21. 合并两个有序链表、146. LRU 缓存、25. K 个一组翻转链表
        • day2:14. 最长公共前缀、3. 无重复字符的最长子串、124. 二叉树中的最大路径和
        • day3:206. 反转链表、199. 二叉树的右视图、bytedance-016最短移动距离
        • day4:1. 两数之和、15. 三数之和、42. 接雨水
        • day5:7. 整数反转、215. 数组中的第K个最大元素、23. 合并K个升序链表
        • day6:33. 搜索旋转排序数组、54. 螺旋矩阵、bytedance-006. 夏季特惠
        • day7:53. 最大子数组和、152. 乘积最大子数组、41. 缺失的第一个正数
        • day8:20. 有效的括号、200. 岛屿数量、76. 最小覆盖子串
        • day9:105. 从前序与中序遍历序列构造二叉树、103. 二叉树的锯齿形层序遍历、bytedance-010. 数组组成最大数
        • day10:94. 二叉树的中序遍历、102. 二叉树的层序遍历、394. 字符串解码
        • day11:415. 字符串相加、5. 最长回文子串、72. 编辑距离
        • day12:64. 最小路径和、300. 最长递增子序列、bytedance-004. 机器人跳跃问题
        • day13:88. 合并两个有序数组、31. 下一个排列、4. 寻找两个正序数组的中位数
        • day14:121. 买卖股票的最佳时机、56. 合并区间、135. 分发糖果
        • day15:232. 用栈实现队列、22. 括号生成、128. 最长连续序列
        • day16:bytedance-007. 化学公式解析、129. 求根节点到叶节点数字之和、239. 滑动窗口最大值
        • day17:141. 环形链表、236. 二叉树的最近公共祖先、92. 反转链表 II
        • day18:322. 零钱兑换、198. 打家劫舍、 bytedance-003. 古生物血缘远近判定
        • day19:160. 相交链表、143. 重排链表、142. 环形链表 II
        • day20:704. 二分查找、43. 字符串相乘、bytedance-002. 发下午茶
        • day21题目:69. x 的平方根、912. 排序数组、887. 鸡蛋掉落
        • day22:151. 颠倒字符串中的单词、46. 全排列、2. 两数相加
      • 剑指 Offer
        • 剑指offer day1 栈与队列(简单)
        • 剑指offer day2 链表(简单)
        • 剑指offer day3 字符串(简单)
        • 剑指offer day4 查找算法(简单)
        • 剑指offer day5 查找算法(中等)
        • 剑指offer day6 搜索与回溯算法(简单)
        • 剑指offer day7 搜索与回溯算法(简单)
        • 剑指offer day8 动态规划(简单)
        • 剑指offer day9 动态规划(中等)
        • 剑指offer day10 动态规划(中等)
        • 剑指offer day11 双指针(简单)
        • 剑指offer day12 双指针(简单)
        • 剑指offer day13 双指针(简单)
        • 剑指offer day14 搜索与回溯算法(中等)
        • 剑指offer day15 搜索与回溯算法(中等)
        • 剑指offer day16 排序(简单)
        • 剑指offer day17 排序(中等)
      • 剑指 Offer 专项突击版
  • 前端进阶
    • React
      • 响应式系统与 React
      • React学习小记
      • Redux学习之Redux三原则、createSore原理及实现
    • Vue
    • TypeScript
      • TypeScript入门
      • TypeScript 类型体操练习
        • Easy题(13/13)
        • Middle(20/72)
    • 前端工程化
      • Webpack知识体系
    • Node
    • 前端动画与绘图
      • WebGL基础
      • 前端动画简介
      • Floating UI 使用经验分享 - Popover
      • Floating UI 使用经验分享 - Dialog
      • Three.js 学习
        • 学习记录
        • 资源记录
    • 前端性能优化
    • 跨端
      • RN 学习小记之使用 Expo 创建项目
    • 开源
    • SEO 优化
      • 搜索引擎优化 (SEO) 新手指南笔记
  • 笔面试记录
    • 面经集锦
      • 2022春暑期实习MetaApp一二面面经
      • 2022春暑期实习字节前端一面凉经
    • 笔试复盘
      • 2022春暑期实习-美团前端-笔试
      • 2022春暑期实习-360前端-笔试(AK)
      • 2022春暑期实习-京东前端-笔试
      • 2022春暑期实习-网易雷火前端-笔试(AK)
      • 2022春暑期实习-网易互联网前端-暑期实习笔试
由 GitBook 提供支持
在本页
  • 什么是设计模式
  • 浏览器中的设计模式
  • 单例模式
  • 发布订阅模式(观察者模式)
  • JavaScript中的设计模式
  • 原型模式
  • 代理模式
  • 迭代器模式
  • 前端框架中的设计模式(React、Vue...)
  • 代理模式
  • 组合模式
  • 总结感想

这有帮助吗?

在GitHub上编辑
导出为 PDF
  1. 前端基础
  2. 通用知识

前端设计模式应用

【第二届青训营-寒假前端场】- 「前端设计模式应用」笔记

什么是设计模式

设计模式是软件设计中常见问题的解决方案模型,是历史经验的总结,与特定语言无关

设计模式大致分为23种设计模式

  • 创建型——如何高效灵活的创建一个对象

  • 结构型——如何灵活的将对象组装成较大的结构

  • 行为型——负责对象间的高效通信和职责划分

浏览器中的设计模式

单例模式

单例模式——存在一个全局访问对象,在任意地方访问和修改都会反映在这个对象上

最常用的其实就是浏览器中的window对象,提供了对浏览器的操作的封装,常用于缓存与全局状态管理等

单例模式实现请求缓存

用单例模式实现请求缓存:相同的url请求,希望第二次发送请求的时候可以复用之前的一些值

首先创建Request类,该类包含一个创建单例对象的静态方法getinstance,然后真正请求的操作为request方法,向url发送请求,若缓存中存在该url则直接返回,反之则缓存到该单例对象中。可以看到如下是上节课讲过的语法~

import {api} from './utils';
export class Request {
    static instance: Request;
    private cache: Record<string, string>;
    constructor() {
        this.cache = {};
    }
    static getinstance() {
        if(this.instance) {
            return this.instance;
        }
        this.instance = new Request();  // 之前还未有过请求,初始化该单例
        return this.instance;
    }
    public async request(url:string) {
        if(this.cache[url]) {
            return this.cache[url];
        }
        const response = await api(url);
        this.cache[url] = response;

        return response;
    }
}

实际中使用如下:利用getInstance静态方法创建该单例对象,并测试起执行时间进行对比。

// 不预先进行请求,测试其时间。
test('should response more than 500ms with class', async() => {
    const request = Request.getinstance();  //获取/创建一个单例对象(若之前未创建过则创建)
    const startTime = Date.now();
    await request.request('/user/1');
    const endTime = Date.now();

    const costTime = endTime-startTime;
    expect(costTime).toBeGreaterThanOrEqual(500);
});
// 先进行一次请求,在测试第二次请求的时间
test('should response quickly second time with class', async() => {
    const request1 = Request.getinstance();
    await request1.request('/user/1');

    const startTime = Date.now();   // 测试这一部分的时间
    const request2 = Request.getinstance();
    await request2.request('/user/1');
    const endTime = Date.now();		//

    const costTime = endTime-startTime;
    expect(costTime).toBeLessThan(50);
});

而在js中,我们也可以不用class写,这是因为传统的语言中无法export出来一个独立的方法等,只能export出来一个类

// 不用class?可以更简洁
import {api} from './utils';
const cache: Record<string,string> = {};
export const request = async (url:string) => {
    if(cache[url]) {    // 与class中一致
        return cache[url];
    }
    const response = await api(url);

    cache[url] = response;
    return response;
};
// 使用,可以看出来该方法也符合单例模式,但更加简洁。
test('should response quickly second time', async() => {
    await request('/user/1');
    const startTime = Date.now();   // 测试这一部分的时间
    await request('/user/1');
    const endTime = Date.now();

    const costTime = endTime-startTime;
    expect(costTime).toBeLessThan(50);
});

发布订阅模式(观察者模式)

应用非常广泛的一种模式,在被订阅对象发生变化时通知订阅者,常见场景很多,从系统架构之间的解耦到业务中的一些实现模式、邮件订阅等等。类似于添加事件

发布订阅模式实现用户上线订阅

举个实际应用的例子:通过该模式,我们可以实现用户的相互订阅,在该用户上线时调用相应的通知函数。

如图创建了一个User类, 构造器中初始状态置为离线,其拥有一个followers对象数组,包括了该用户订阅的所有{用户,调用函数},每次在该用户上线时,遍历其followers进行通知

type Notify = (user: User) => void;
export class User {
    name: string;
    status: "offline" | "online";// 状态 离线/在线
    followers: { user:User; notify: Notify }[]; // 订阅他人的数组,包括用户及其上线时的通知函数
    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);
        });
    }
}

测试函数:还是用jest,创建假的订阅函数进行测试(

test("should notify followers when user is online for multiple users", () => {
   const user1 = new User("user1");
   const user2 = new User("user2"); 
   const user3 = new User("user3"); 
   const mockNotifyUser1 = jest.fn();   // 通知user1的函数
   const mockNotifyUser2 = jest.fn();   // 通知user2的函数
   user1.subscribe(user3, mockNotifyUser1); // 1订阅了3
   user2.subscribe(user3, mockNotifyUser2); // 2订阅了3
   user3.online();  // 3上线,调用mockNotifyUser1和mockNotifyUser2
   expect(mockNotifyUser1).toBeCalledWith(user3);
   expect(mockNotifyUser2).toBeCalledWith(user3);
});

JavaScript中的设计模式

原型模式

可以想到javascript中的常见语言特性:原型链,原型模式指的其实就是复制一个已有的对象来创建新的对象,这在对象十分庞大的时候会有比较好的性能(相比起直接创建)。常用于js中对象的创建

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

首先,创建一个原型,可以看到这个原型相比起之前的来说没有定义构造器。

// 原型模式,当然要有原型啦
const baseUser:User = { 
    name: "",
    status: "offline",
    followers: [],
    subscribe(user, notify) {
        user.followers.push({user, notify});
    },
    online() { // 该用户上线 调用其订阅函数
        this.status = "online";
        this.followers.forEach( ({notify}) => {
            notify(this);
        });
    }
}
// 然后导出在该原型之上创建对象的函数
export const createUser = (name:string) => {
    const user:User = Object.create(baseUser);
    user.name = name;
    user.followers = [];
    return user;
};

实际使用:可以看到将new User变成了createUser

test("should notify followers when user is online for user prototypes", () => {
    const user1 = createUser("user1");
    const user2 = createUser("user2");
    const user3 = createUser("user3");
    const mockNotifyUser1 = jest.fn();   // 通知user1的函数
    const mockNotifyUser2 = jest.fn();   // 通知user2的函数
    user1.subscribe(user3, mockNotifyUser1); // 1订阅了3
    user2.subscribe(user3, mockNotifyUser2); // 2订阅了3
    user3.online();  // 3上线,调用mockNotifyUser1和mockNotifyUser2
    expect(mockNotifyUser1).toBeCalledWith(user3);
    expect(mockNotifyUser2).toBeCalledWith(user3);
});

代理模式

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

还是上述观察者模式的例子,可以使用代理模式对其进行优化,让他的online函数只做一件事:更改状态为上线。

type Notify = (user: User) => void;
export class User {
    name: string;
    status: "offline" | "online";// 状态 离线/在线
    followers: { user:User; notify: Notify }[]; // 订阅他人的数组,包括用户及其上线时的通知函数
    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);
        // });
    }
}

创建User的一个代理:ProxyUser

Proxy函数说明

target

要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

handler

一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

// 创建代理,监听其上线状态的变化
export const createProxyUser = (name:string) => {
    const user = new User(name); //正常的user
    // 代理的对象
    const proxyUser = new Proxy(user, { 
        set: (target, prop: keyof User, value) => {
            target[prop] = value;
            if(prop === 'status') {
                notifyStatusHandlers(target, value);
            }
            return true;
        }
    })
    const notifyStatusHandlers = (user: User, status: "online" | "offline") => {
        if(status === "online") {
            user.followers.forEach(({notify}) => {
                notify(user);
            });
        }
    };
    return proxyUser;
}

迭代器模式

在不暴露数据类型的情况下访问集合中的数据,常用于数据结构中拥有多种数据类型(列表、

树等),提供通用的操作接口。

用for of 迭代所有组件

定义一个list 队列,每次从队首取个节点出来,如果这个节点有孩子结点将其全部添加到队尾,每次调用next都返回一个结点~详见代码

class MyDomElement {
    tag: string;
    children: MyDomElement[];
    constructor(tag:string) {
        this.tag = tag;
        this.children = [];
    }
    addChildren(component: MyDomElement) {
        this.children.push(component);
    }
    [Symbol.iterator]() {
        const list = [...this.children];
        let node;
        return {
            next: () => {
                while((node = list.shift())) { // 每次从队首取个节点出来,如果有孩子结点将其添加到队尾
                    node.children.length > 0 && list.push(...node.children);
                    return { value: node, done: false };
                }
                return { value:null, done:true };
            },
        };
    }
}

使用场景如下:通过for of迭代body中的所有子元素

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) {	// 迭代body的所有元素,需要包含main中的子元素
        if(element) {
            expectTags.push(element.tag);
        }
    }
    
    expect(expectTags.length).toBe(5);
});

前端框架中的设计模式(React、Vue...)

代理模式

与之前讲的Proxy不太一样

Vue组件实现计数器

<template>
	<button @click="count++">count is:{{ count }}</button>
</template>
<script setup lang="ts">
import {ref} from "vue";
const count = ref(0);
</script>

上述代码,为什么count能随点击而变化?这就要说到前端框架中对DOM操作的代理了:

更改DOM属性 -> 视图更新

更改DOM属性 -> 更新虚拟DOM -Diff-> 视图更新

如下就是前端框架对DOM的一个代理,通过其提供的钩子可以在更新前后进行操作:

<template>
	<button @click="count++">count is:{{ count }}</button>
</template>
<script setup lang="ts">
import { ref, onBeforeUpdate, onUpdated } from "vue";
const count = ref(0);
const dom = ref<HTMLButtonElement>();
onBeforeUpdate(() => {
    console.log("Dom before update", dom.value?.innerText);
});
onUpdated(() => {
    console.log("Dom after update", dom.value?.innerText);
});
</script>

组合模式

可以多个对象组合使用,也可以单个对象独立使用,常应用于前端组件,最经典的就是React的组件结构:

React组件结构

还是计数器的例子~

export const Count = () => {
    const [count, setCount] = useState(0);
    return (
    	<button onClick={() => setCount((count) => count+1)}>
        	count is: {count}
        </button>
    );
};

该Count,既可以独立渲染,也可以渲染在App中,后者就是一种组合

function App() {
    return (
        <div className = "App">
        	<Header />
            <Count />
            <Footer />
        </div>
    );
}

总结感想

下面是老师的一些总结:

设计模式不是银弹,总结出抽象的模式听起来比较简单,但是想要将抽象的模式套用到实际的场景中却非常困难,现代编程语言的多编程范式带来了更多的可能性,我们要在真正优秀的开源项目中学习设计模式并不断实践

这节课讲了浏览器和js中常用的设计模式,包括单例模式、观察者模式、原型模式、代理模式、迭代器模式等,还讲了设计模式究竟有什么用~在我看来,从实际的项目中学习设计模式确实是一种比较好的方法。

本文引用的大部分内容来自吴立宁老师的课

上一页前端必须知道的开发调试知识下一页Web 标准与前端开发

最后更新于3年前

这有帮助吗?

ps: 这里的测试是使用进行的,其中用到了部分的api,可以通过文档了解其用途

导出在该原型之上创建对象的函数,该函数接受一个name参数,利用 使用原型来创建一个新对象,并在其基础上进行增加或修改

可自定义控制队员对象的访问方式,并且允许在更新前后做一些额外处理,常用于监控、代理工具、前端框架等等。JS中有自带的代理对象: ,在红宝书中代理那章也有详细阐述。

用到了 该迭代器可以被 循环使用。

Jest
expect
Object.create()
Proxy()
Symbol.iterator
for...of