「中高级前端面试」手写代码合集(二)

2,745 阅读15分钟

本合集旨在巩固前端工程师核心基础技能,从各大小厂高频手写题出发,对常见的模拟实现进行总结。更详尽的源代码放在 github 项目上,长期更新和维护

PS:文章如有误,请不吝指正。您的 「赞」 是笔者坚持创作的源动力。

接续上篇:「中高级前端面试」手写代码合集(一)

该篇着重梳理 设计模式框架/工具库,先放思维导图:

✨ 设计模式 ✨

设计模式,说白了就是用来解决某种特定问题的解决方案。

比如这样一个场景 👉:随着项目的迭代,接口的结构需要变动,为了不影响旧有业务,只能扩展而不能直接修改接口,于是我们很快想到了 适配器模式

那么,话不多说,正文开始。

23.适配器模式

适配器模式 的作用就是解决两个软件实体间接口不兼容情况,实体电器例如电源适配器、USB 转接口、各种转换器等。

需求来了,现在需要 对接多个快递平台 SDK 进行不同快递单的生成 功能。

// 顺丰
const sfOrderService = {
    create() {
    	console.log('顺丰订单已生成...')
    }
}
// 韵达
const ydOrderService = {
    create() {
    	console.log('韵达订单已生成...')
    }
}
// createOrder 提供给使用者调用
const createOrder = (express) => {
    if (express.create instanceof Function) express.create()
}
createOrder(sfOrderService)
createOrder(ydOrderService)

// 顺丰订单已生成...
// 韵达订单已生成...

现在新的需求来了,我们需要集成圆通 SDK,但是圆通 SDK 的方法是 generate,不是 create。为了满足开闭原则,我们想到了适配器模式。

// 圆通
const ytOrderService = {
    generate() {
    	console.log('圆通订单已生成...')
    }
}
// 适配器
const ytExpressAdapater = {
    create() {
    	return ytOrderService.generate()
    }
}
// 现在可以使用 createOrder 生成订单了,哈哈
createOrder(ytExpressAdapater)

// 圆通订单已生成...

另外一个常见的开发场景:数据格式变更

// 这是我们之前上传资源,后台给我们返回的文件信息
const responseUploadFile = {
    startTime: '',
    file: {
    	size: '100kb',
        type: 'text',
        ...
    },
    id: ''
}
// 某天后台将返回格式变动了,变为:
const changeResUploadFile = {
    size: '100kb',
    type: 'text',
    startTime: '',
    id: '',
    ...
}
// 为了不影响旧有业务,导致 BUG 和回归测试,写个适配器用来数据转换吧...
const responseUploadFileAdapter = (uploadFile) = > {
    const { startTime = '', size = '', type = '', id = '', ... } = uploadFile
    return {
    	startTime,
        file: {
            size,
            type,
            ...
        },
        id
    }
}
responseUploadFileAdapter(changeResUploadFile) // 转换成旧格式了

由此看出前后端分离开发,数据操作的 自由度 是很高的。

24.观察者模式

很多人常常会把 发布-订阅模式观察者模式 混淆在一起,其实他们是有区别的!

观察者模式 中,观察者是知道 Subject 的,Subject 一直保持对观察者进行记录。

然而,在 发布-订阅模式 中,发布者和订阅者不知道对方的存在。它们只是通过消息代理进行通信,同时是异步的,比如消息队列。在 发布-订阅模式 中,组件是松散耦合的,正好和 观察者模式 相反。

举个 观察者模式 的 🌰:我对企业很感兴趣(我作为观察者知道企业大名),企业维护我和其他面试者的简历,当职位空缺时主动通知我和其他竞争者。(这里观察者和观察对象是 互相知晓 的)

看下结构图,我们发现发布-订阅模式多了个中间层 Event Channel 用于调度:

下面是观察者模式的实现:

class Subject {
    constructor() {
        this.observers = [] // 观察者队列
    }
    add(observer) { // 没有事件通道
        this.observers.push(observer) // 必须将自己 observer 添加到观察者队列
        this.observers = [...new Set(this.observers)]
    }
    notify(...args) { // 亲自通知观察者
        this.observers.forEach(observer => observer.update(...args))
    }
    remove(observer) {
        let observers = this.observers
    	for (let i=0, len=observers.length; i<len; i++) {
            if (observers[i] === observer) observers.splice(i, 1)
        }
    }
}

class Observer {
    update(...args) {
    	console.log(...args)
    }
}

let observer_1 = new Observer() // 创建观察者1
let observer_2 = new Observer()
let sub = new Subject() // 创建目标对象
sub.add(observer_1) // 添加观察者1
sub.add(observer_2)
sub.notify('I changed !')

25.发布订阅模式

前面已经梳理过区别了,直接开始实现:

class PublicSubject { // 只有一个调度中心
    constructor() {
        this.subscribers = {}
    }
    subscribe(type, callback) { // 订阅
        let res = this.subscribers[type]
        if (!res) {
            this.subscribers[type] = [callback]
        } else {
            res.push(callback)
        }
    }
    publish(type, ...args) { // 发布
        let res = this.subscribers[type] || []
        res.forEach(callback => callback(...args))
    }
}

let pubSub = new PublicSubject()
pubSub.subscribe('blog', (arg) => console.log(`${arg} 更新了`)) // A 订阅 Keith
pubSub.subscribe('blog', (arg) => console.log(`${arg} 更新了`)) // B 订阅 Keith
pubSub.publish('blog', '掘金 Keith')

// 掘金 Keith 更新了
// 掘金 Keith 更新了

当然,这个版本功能还不够完善,实际上,发布-订阅模式 通常被用在事件监听和触发功能上,我们可能还需求移除订阅。比如常见的 Vue 父子组件通信 $emit、$on、$off、$once,Vue 的响应式,后文要介绍的 redux,以及 nodejs Event 模块 的 eventEmitter 类实现等均有应用。

那么,我们来实现下:

// once 参数表示是否只是触发一次
const wrapCallback = (fn, once=false) => ({ callback: fn, once })
class EventEmitter {
    constructor() {
        this.events = new Map()
    }
    on(type, fn, once=false) { // 监听订阅
        let handler = this.events.get(type)
        if (!handler) {
            this.events.set(type, wrapCallback(fn, once)) // 绑定回调
        } else if (handler && typeof handler.callback === 'function') {
            this.events.set(type, [handler, wrapCallback(fn, once)]) // 超过一个转为数组
        } else {
            handler.push(wrapCallback(fn, once))
        }
    }
    off(type, fn) { // 删除某个事件的回调,假如回调 <= 1,则等同 allOff 方法
        let handler = this.events.get(type)
        if (!handler) return;
        // 只有一个回调事件直接删除该订阅
        if (!Array.isArray(handler) && 
        handler.callback === fn.callback) this.events.delete(type)
        for (let i=0; i<handler.length; i++) {
            let item = handler[i]
            if (item.callback === fn.callback) {
                handler.splice(i, 1)
                i-- // 数组塌陷,i 往前一位
                if (handler.length === 1) this.events.set(type, handler[0])
            }
        }
    }
    // once:该订阅事件 type 只触发一次,之后自动移除
    once(type, fn) {
        this.on(type, fn, true)
    }
    emit(type, ...args) {
        let handler = this.events.get(type)
        if (!handler) return;
        if (Array.isArray(handler)) {
            handler.map(item => {
                item.callback.apply(this, args) // args 参数少,可以换成 call
                if (item.once) this.off(type, item) // 处理 once 的情况,off 移除
            })
        } else {
            handler.callback.apply(this, args) // 处理非数组
        }
    }
    allOff(type) {
        let handler = this.events.get(type)
        if (!handler) return;
        this.events.delete(type)
    }
}

let e = new EventEmitter()
e.on('eventA', () => {
  console.log('eventA 事件触发')
})
e.on('eventA', () => {
  console.log('✨ eventA 事件又触发了 ✨')
})

function f() { 
  console.log('eventA 事件我只触发一次');
}
e.once('type', f)
e.emit('type')
e.emit('type')
e.allOff('type')
e.emit('type')

// eventA 事件触发
// ✨ eventA 事件又触发了 ✨
// eventA 事件我只触发一次
// eventA 事件触发
// ✨ eventA 事件又触发了 ✨

26.策略模式

策略模式:简单理解就是定义一系列同级算法(功能的具体实现),在一个稳定的环境中使用,直接看代码。

// 定义一系列算法,看起来都是策略。
let levelOBJ = {
    A: (money) => money * 4,
    B: (money) => money * 3,
    C: (money) => money * 2,
    ...
}
// 一个稳定的环境 (函数) 用于调用算法
let calculateBouns = (level, money) => levelOBJ[level](money)
console.log(calculateBouns('A', 10000))

// 40000

27.代理模式

代理模式:为某个对象提供一种代理以控制对这个对象的访问(自定义方法,可以使用这个对象的资源)。

举个 🌰:双十一,小美有亿件快递到了,有些包裹太重了自己拿不动。于是,她拜托工具人小明帮忙,小明欣然前往快递点取件。这里,小明帮小美取快递就起到了代理的作用。注意:整个动作还是小美发起的,小明可以理解为一个透明的中间人,直接看代码。

let expressPoint = {
    pickUp() {
        console.log('取快递成功...')
    }
}
let Ming = {
    getMsg(target) {
        target.pickUp()
    }
}
let Mei = {
    getExpress(target) {
        Ming.getMsg(target) // 小明取件,可以配合定时器等逻辑做到延迟取件
    }
}
Mei.getExpress(expressPoint) // 小美取件,虽然是通过小明代理的。

// 取快递成功...

28.单例模式

单例模式:它保证一个类仅有一个实例,并提供一个访问它的全局访问点。

比如数据库:我们在访问网站,请求数据时,不管建立多少连接对数据读写,都是指向同一个数据库(这里不考虑数据库的集群、备份、缓存镜像等...)。

饿汉式单例

let ShopCar = (function() {
    let instance = init()
    function init() {
        return {
            buy(good) {
            	this.goods.push(good)
            },
            goods: []
        }
    }
    return {
        getInstance() {
            return instance
        }
    }
})()

let car1 = ShopCar.getInstance()
let car2 = ShopCar.getInstance()
car1.buy('橘子')
car2.buy('苹果')
console.log(car1.goods) // ['橘子', '苹果']
console.log(car1 === car2) // true

饿汉式在代码加载的时候就创建好了实例,理解起来就像不管一个人想不想吃东西都把吃的先买好,如同饿怕了一样。

如果一个对象使用频率不高,占用内存还特别大,明显就不合适用饿汉式了,这时就需要一种懒加载的思想,当程序需要这个实例的时候才去创建对象,就如同一个人懒到极致,饿到不行了才去吃东西。

懒汉式单例

let ShopCar = (function() {
    let instance
    function init() {
        return {
            buy(good) {
                this.goods.push(good)
            },
            goods: []
        }
    }
    return {
        getInstance() {
            if (!instance) instance = init() // 不要跟我比懒,我懒得跟你比。
            return instance
        }
    }
})()

29.工厂模式

工厂模式 细分为:

  • 简单工厂模式(工作中最常用 👍)
  • 工厂方法模式(很少用到)
  • 抽象工厂模式(基本不用...)

工厂方法的提出是为了将对象的创建和使用解耦,假如 A 想调用 B 的方法,C 也想…… 那么 B 可能会被实例化成多种,为了避免重复代码,我们使用工厂方法统一创建。

简单工厂模式

没什么神秘的,就是一个带有 静态 方法的类 (简单工厂模式又名 静态工厂模式),你只需给静态方法传入正确的参数,就可以获取到你所需要的对象,而无需知道其创建的具体细节。

以一个实际项目:用户权限 来说明,我们需要根据用户的权限来渲染不同的页面,低级权限用户看不到高级权限页面。

// 工厂类
class User {
    constructor(option) {
        this.name = options.name
        this.viewPage = options.viewPage
    }
    // 静态方法,可以在外部直接调用,不用实例化
    static getInstance(role) {
        let params;
        switch(role) {
            case 'superAdmin':
                // 在静态方法中返回实例
                params = { name: '超级管理员', viewPage: ['首页', '用户管理', '权限管理']}
                break;
            case 'admin':
                params = { name: '管理员', viewPage: ['首页', '用户管理']}
                break;
            case: 'user':
                params = { name: '普通用户', viewPage: ['首页']}
                break;
            default:
                throw new Error('参数错误,可选参数:superAdmin、admin、user')
        }
        return new User(params)
    }
}

let superAdmin = User.getInstance('superAdmin')
let admin = User.getInstance('admin')
let normalUser = User.getInstance('user')

工厂方法模式

工厂方法模式 的本意是 将实际创建对象的工作推迟到子类中,父类作为一个抽象类(抽象类不能实例化)。遗憾的是,ES6 暂时还没实现 abstract,但是我们可以使用 new.target 来模拟抽象类。

new.target 指向被 new 执行的构造函数。简单理解,只要函数/类被 new 调用了,new.target 就会有值。

class User {
    constructor(name = '', viewPage = []) {
        if (new.target === User) throw new Error('抽象类不能实例化!') // 注意这里
        this.name = name
        this.viewPage = viewPage
    }
}
// 工厂方法只做一件事,就是实例化对象
class UserFactory extends User {
    constructor(name, viewPage) {
        super(name, viewPage)
    }
    create(role) {
        let params;
        switch(role) {
            case 'superAdmin':
                params = { name: '超级管理员', viewPage: ['首页', '用户管理', '权限管理']}
                break;
            case 'admin':
                params = { name: '管理员', viewPage: ['首页', '用户管理']}
                break;
            case 'user':
                params = { name: '普通用户', viewPage: ['首页']}
                break;
            default:
                throw new Error('参数错误,可选参数:superAdmin、admin、user')
        }
        return new UserFactory(params)
    }
}

let userFactory = new UserFactory()
let superAdmin = userFactory.create('superAdmin')
let admin = userFactory.create('admin')
let normalUser = userFactory.create('user')

抽象工厂模式

上面介绍的两种工厂模式都是直接生成实例,但是抽象工厂模式不同。抽象工厂模式 是用于 对产品类簇 的创建。

让我们回顾下 简单工厂模式,假若随着迭代,用户权限越发复杂,增加 VIP 用户、临时管理员、中级用户等,他们的权限、职能都不同。按照这个思路,每出现一个用户权限就要增加新的 case 分支,那首先会造成这个工厂方法异常庞大,大到最终你不敢增加/修改任何地方,生怕导致工厂出现 BUG 影响现有系统逻辑,给测试人员和你自己带来额外的工作量。

而这一切的源头是没有遵守软件设计的 开放封闭原则

开放封闭原则:对扩展开放,对修改封闭。换句话说,软件实体可以扩展,但不能修改。

// 抽象用户工厂类
class UserFactory {
    constructor() {
        if (new.target === UserFactory) throw new Error('抽象类不能实例化!')
    }
    create() {
        throw new Error('抽象工厂类不允许直接调用方法,请重写实现!')
    }
}
// 具体用户类
class User extends UserFactory {
    create(role) {
        let params;
        switch(role) {
            case 'superAdmin':
                return new SuperAuthority()
                break;
            case 'admin':
                return new AdminAuthority()
                break;
            case 'user':
                return new UserAuthority()
                break;
            default:
                throw new Error('暂时没有这个用户权限!')
        }
    }
}
// 抽象类
class Authority {
    readWrite() {
        throw new Error('Authority 类不允许直接调用方法,请重新实现!')
    }
}
// 产品类簇
class SuperAuthority extends Authority {
    readWrite() {
        console.log('您可以随意浏览并修改网站内容。')
    }
}
class AdminAuthority extends Authority {
    readWrite() {
        console.log('您可以随意浏览并修改部分网站内容。')
    }
}
class UserAuthority extends Authority {
    readWrite() {
        console.log('您只能浏览部分网站内容。')
    }
}

const userAuthority = new User()
const myAuthority = userAuthority.create('superAdmin')
myAuthority.readWrite()

// 您可以随意浏览并修改网站内容。

抽象工厂模式 对原有系统不会造成任何潜在影响,所谓的 对扩展开放,对修改封闭 就比较圆满地实现了。

总结

上面说到的三种工厂模式和单例模式一样,都是属于创建型的设计模式。简单工厂模式 用来创建某一种产品对象的实例,用来创建单一对象;工厂方法模式 是将创建实例推迟到子类中进行;抽象工厂模式 是对类的工厂抽象用来创建产品类簇,不负责创建某一类产品的实例。

在实际业务中,需要根据业务复杂度来选择合适的模式。对于非大型的前端应用,简单工厂模式已经足够。

30.装饰器模式

装饰器模式 定义:在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法。简而言之就是对对象进行包装,返回一个新的对象描述(descriptor)。这个概念其实和 React 中的高阶组件、ES6 装饰器、TypeScript 装饰器-依赖注入 @Injectable 等类似。

不使用装饰器:

const log = (srcFun) => {
  if (typeof(srcFun) !== 'function') throw new Error(`the param must be a function`)
  return (...arguments) => {
    console.info(`${srcFun.name} invoke with ${arguments.join(',')}`)
    srcFun(...arguments)
  }
}

const plus = (a, b) => a + b
const logPlus = log(plus)
logPlus(1,2)

使用 装饰器

const log = (target, name, descriptor) => {
    var oldValue = descriptor.value
    descriptor.value = function() {
        console.log(`Calling ${name} with`, arguments)
        return oldValue.apply(this, arguments)
    }
    return descriptor
}

class Math {
    @log  // Decorator
    plus(a, b) {
        return a + b
    }
}
const math = new Math()
math.add(1, 2)

从上面的代码可以看出,如果有的时候我们并不需要关心函数的内部实现,仅仅是想调用它的话,装饰器 能够带来比较好的可读性,使用起来也是非常的方便。

现在让我们来用 JavaScript 实现一个:

/**
 * 装饰器函数
 * @param {Object} target 被装饰器的类的原型
 * @param {string} name 被装饰的类、属性、方法的名字
 * @param {Object} descriptor 被装饰的类、属性、方法的 descriptor
 * @returns {Object} result
 */
function Decorator(target, name, descriptor) {
    // 以此可以获取实例化的时候此属性的默认值
    let v = descriptor.initializer && descriptor.initializer.call(this)
    // 返回一个新的描述对象作为被修饰对象的descriptor,或者直接修改 descriptor 也可以
    return {
        enumerable: true,
    	configurable: true,
    	get() {
            return v
    	},
    	set(c) {
            v = c
    	}
    }
}

31.AJAX

简单实现一个 GET/POST 请求

// data 传入的参数也需要做兼容处理,对于中文还需要 encode 转码
function params(data) {
    if (typeof data === 'object') {
        var arr = [];
        for (var key in data) {
            arr.push(encodeURIComponent(key) + "=" + encodeURIComponent(data[key]));
        }
        return arr.join("&");
    }
    return data;
}
const myAjax = function(url, method='GET', data) {
    return new Promise((resolve, reject) => {
        // 兼容 xhr
        const xhr = XMLHttpRequest ? new XMLHttpRequest() : 
        new ActiveXObject('Microsoft.XMLHttp')
        const _data = params(data)
        
        if (method === 'GET') {
            // 打开请求,如果 url 已经有参数了,直接追加,没有从问号开始拼接
            if (url.indexOf('?') !== -1) {
                xhr.open(method, url + '&' + _data)
            } else {
                xhr.open(method, url + '?' + _data)
            }
            //发送请求,因为参数都跟在url后面,所以不用在send里面做任何处理
            xhr.send();
        }
        if (method === 'POST') {
            xhr.open(method, url, false)
            xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
            // 发送请求,post 请求的时候,会将 data 中的参数按照 & 拆分出来
            xhr.send(_data)
        }
            
        xhr.onreadystatechange = function() {
            if (xhr.readyState !== 4) return;
            if (xhr.status === 200 || xhr.status === 304) {
            	resolve(xhr.responseText)
            } else {
                reject(new Error(xhr.responseText))
            }
        }
    })
}

32.Vue 响应式

这个是 Object.defineProperty 版,后续计划更新 Vue 3 Proxy。当前源码响应式原理采用的是发布-订阅模式(回顾前文的模式篇) + Object.defineProperty 数据劫持 。

简单梳理:

  1. 实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。

  2. 实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到 通知,调用更新函数进行数据更新。

  3. 实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的 桥梁,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。

  4. 实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。

直接开造:

const Observer = function (data) {
  // for get/set
  for (let key in data) {
    defineReactive(data, key)
  }
}

const defineReactive = function (obj, key) {
  // 局部变量 dep,用于 get set 内部调用
  const dep = new Dep()
  let val = obj[key]
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log('in get')
      // 调用依赖收集器的 addSub,用于收集当前属性与 Watcher 中的依赖关系
      dep.depend()
      return val
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal
      // 当值发生变更时,通知依赖收集器,更新每个需要更新的 Watcher
      // 这里每个需要更新通过什么断定?dep.subs
      dep.notify()
    }
  })
}

const observe = function(data) {
  return new Observer(data)
}

const Vue = function (options) {
  const self = this
  // 将 data 赋值给 this._data
  if (options && typeof options.data === 'function') {
    this._data = options.data.apply(this)
  }
  // 挂载函数
  this.mount = function() {
    new Watcher(self, self.render)
  }
  // 渲染函数,里面决定了只有页面渲染才会 get 的值,当前只有 text
  // 没在 render 里的数据依旧能够 set 改变值,但不会触发 notify
  // 因为没有被 get Watcher,总要是为了避免毫无意义的渲染。
  this.render = function() {
    with(self) { // 限定 this
      _data.text
    }
  }
  // 监听 this._data
  observe(this._data)
}

const Watcher = function(vm, fn) {
  const self = this
  this.vm = vm
  // 将当前 Dep.target 指向自己
  Dep.target = this
  // 向 Dep 方法添加当前 Watcher
  this.addDep = function(dep) {
    dep.addSub(self)
  }
  // 更新方法,用于触发 vm._render
  this.update = function() {
    console.log('in watcher update')
    fn()
  }
  // 首次调用 vm._render,从而触发 text 的 get
  // 从而将当前的 Watcher 与 Dep 关联起来
  this.value = fn()
  // 这里清空了 Dep.target,为了防止 notify 触发时,不停地绑定 Watcher 与 Dep
  Dep.target = null
}

const Dep = function() {
  const self = this
  // 收集目标
  Dep.target = null
  // 存储收集器中需要通知的 Watcher
  this.subs = []
  // 当有目标时,绑定 Dep 与 Watcher 的关系
  this.depend = function() {
    if (Dep.target) {
      // 这里其实可以直接写成 self.addSub(Dep.target)
      // 没有这么写因为想还原源码的过程
      Dep.target.addDep(self)
    }
  }
  // 为当前收集器添加 Watcher
  this.addSub = function(watcher) {
    self.subs.push(watcher)
  }
  // 通知收集器中的所有 Watcher,调用其 update 方法
  this.notify = function() {
    for (let i=0; i<self.subs.length; i+=1) {
      self.subs[i].update()
    }
  }
}

const vue = new Vue({
  data() {
    return {
      text: 'hello world'
    }
  }
})

vue.mount() // in get
vue._data.text = '123'

// in watcher updata
// in get

refactor 版:

// Dep module
class Dep {
  static stack = []
  static target = null
  deps = null
  
  constructor() {
    this.deps = new Set()
  }

  depend() {
    if (Dep.target) {
      this.deps.add(Dep.target)
    }
  }

  notify() {
    this.deps.forEach(w => w.update())
  }

  static pushTarget(t) {
    if (this.target) {
      this.stack.push(this.target)
    }
    this.target = t
  }

  static popTarget() {
    this.target = this.stack.pop()
  }
}

// reactive/observe
function reactive(o) {
  if (o && typeof o === 'object') {
    Object.keys(o).forEach(k => {
      defineReactive(o, k, o[k])
    })
  }
  return o
}

function defineReactive(obj, k, val) {
  let dep = new Dep()
  Object.defineProperty(obj, k, {
    get() {
      dep.depend()
      return val
    },
    set(newVal) {
      val = newVal
      dep.notify()
    }
  })
  if (val && typeof val === 'object') {
    reactive(val)
  }
}

// watcher
class Watcher {
  constructor(effect) {
    this.effect = effect
    this.update()
  }

  update() {
    Dep.pushTarget(this)
    this.value = this.effect()
    Dep.popTarget()
    return this.value
  }
}

// 测试代码
const data = reactive({
  msg: 'aaa'
})

new Watcher(() => {
  console.log('===> effect', data.msg);
})

setTimeout(() => {
  data.msg = 'hello'
}, 1000)

33.将 Virtual Dom 转换为真实 Dom

render 部分:

// 转换为真实 Dom
function render(vnode, container) {
    container.appendChild(_render(vnode))
}
// vnode 的结构:{ tag, attrs, children, ... }
function _render(vnode) {
    // 如果是数字类型,转为字符串
    if (typeof vnode === 'number') vnode = String(vnode)
    // 字符串类型直接生成文本节点
    if (typeof vnode === 'string') return document.createTextNode(vnode)
    // 普通 dom
    const dom = document.createElement(vnode.tag)
    if (vnode.attrs) {
        Object.keys(vnode.attrs).forEach(key => {
            const value = vnode.attrs[key]
            dom.setAttribute(key, value)
        })
    }
    // 递归
    if (vnode.children) vnode.children.forEach(child => render(child, dom))
    return dom
}

34.Vue-Router

首先我们回顾下 Vue-Router 在 Vue 中的使用:

const routes = [
  { path: '/', component: Home },
  { path: '/page1', component: Page1 },
  ...
]

const router = new VueRouter({
  mode: 'history', // vue-router 有两种模式,默认为 hash 模式
  routes // 路由数组
})

<router-link><router-view> 标签应用:

<p>
  <!-- 使用 router-link 组件来导航,默认会被渲染成一个 `<a>` 标签 -->
  <router-link to="/">Go to Foo</router-link>
  <router-link to="/page2">Go to Bar</router-link>
</p>
<!-- 路由出口,路由匹配到的组件将渲染在这里 -->
<router-view></router-view>

实现思路:

  1. 绑定 hashchange 事件,实现前端路由
  2. 将传入的路由和组件做一个路由映射,切换哪个路由即可找到对应的组件显示
  3. 需要 new 一个 Vue 实例还做响应式通信,当路由改变的时候,router-view 会响应更新
  4. 注册 router-linkrouter-view 组件
class VueRouter {
  /**
   * 装饰器函数
   * @param {Object} Vue 构造函数
   * @param {Object} options 路由映射表,如前文变量 routes
   */
  constructor (Vue, options) {
    this.$options = options
    this.mode = options.mode || 'hash'
    this.routeMap = {}
    // new 一个 Vue 实例存储当前路由属性 current
    this.app = new Vue({
      data: {
        current: '#/' // 默认 `#/`
      }
    })
    // 初始化监听路由变化
    this.init()
    // 简单数据转换 this.routeMap = { '/': Home, '/page1': 'Page1' }
    this.createRouteMap(this.$options)
    // 组件注册
    this.initComponent(Vue, this.$options, this.app)
  }
  
  // 监听路由,一旦路由变化就会触发
  init () {
    if (this.mode === 'hash') {
      window.addEventListener('load', () => {
        this.app.current = window.location.hash.slice(1) || '/'
      }, false)
      window.addEventListener('hashchange', () => {
        this.app.current = window.location.hash.slice(1) || '/'
      }, false)
    } else {
      window.addEventListener('load', () => {
        this.app.current = window.location.pathname || '/'
      })
      // 通过 window.history.pushStateAPI 来添加浏览器历史记录
      // 然后通过监听 popState 事件,也就是监听历史记录的改变,来加载相应的内容
      window.addEventListener('popstate', () => {
        this.app.current = window.location.pathname || '/'
      })
    }
  }
  // 路由映射表
  createRouteMap (options) {
    options.routes.forEach(item => {
      this.routeMap[item.path] = item.component
    })
  }
  // 注册组件需要使用 Vue.component
  initComponent (Vue, options, app) {
    Vue.component('router-link', {
      props: {
        to: String
      },
      methods: { // 注册点击事件
        handleClick(event) {
          // 阻止 a 标签默认跳转
          event && event.preventDefault && event.preventDefault()
          let mode = options.mode
          let path = app.current
          if (mode === 'hash') {
            window.history.pushState(null, '', '#/' + path.slice(1))
          } else {
            window.history.pushState(null, '', path.slice(1))
          }
        }
      },
      template: '<a :href="to"><slot></slot></a>'
    })

    const _this = this;
    Vue.component('router-view', {
      render (h) {
        let component = _this.routeMap[_this.app.current]
        return h(component)
      }
    })
  }
}

最后,将 Vue 与 Hash 路由结合,监听了 hashchange 事件,再通过 Vue 的 响应机制 和组件,便有了上面实现好了一个 Vue-Router。

35.Redux

Redux 一个状态管理库。

注意:这里的 ReduxReact-Redux 看起来很像,但是他们的核心理念和关注点是不同的,Redux 其实只是一个单纯状态管理库,可以与任何框架一起用,没有界面相关的东西;React-Redux 关注的是怎么将 ReduxReact 结合起来,用到了一些 React 的 API。

简单梳理下 Redux

  • Store:一个数据仓库,用于存储所有的状态 State。
  • Action:一个动作,目的是更改仓库 Store 的状态,只停留在 的层面。
  • Reducers:根据接收的 Action 来真正改变 Store 中的状态,不是想了,而是 直接实施

可以看到 Redux 本身就是一个单纯的状态机,Store 存放了所有的状态,Action 是一个改变状态的通知,Reducer 接收到通知就更改 Store 中对应的状态。整个过程像这样:

图片来源,如有侵权请作者联系我删除。

举个例子,免税仓库里专门维护了一种 sku 阿玛尼口红:

import { createStore } from 'redux'
// 阿玛尼200 的库存
const initState = {
  lipstickArmani_200: 0
}
function reducer(state = initState, action) {
  switch (action.type) {
    case 'SUPPLY_GOODS':
      return {...state, lipstickArmani_200: state.lipstickArmani_200 + action.count}
    case 'REDUCE_GOODS':
      return {...state, lipstickArmani_200: state.lipstickArmani_200 - action.count}
    default:
      return state
  }
}

let store = createStore(reducer)
// subscribe 其实就是订阅 store 的变化,一旦 store 发生了变化,传入的回调函数就会被调用
// 如果是结合页面更新,更新的操作就是在这里执行
store.subscribe(() => console.log(store.getState()))
// 将action发出去要用dispatch
store.dispatch({ type: 'SUPPLY_GOODS' })    // lipstickArmani_200: 1
store.dispatch({ type: 'SUPPLY_GOODS' })    // lipstickArmani_200: 2
store.dispatch({ type: 'REDUCE_GOODS' })   // lipstickArmani_200: 1

分析下上面的代码主要涉及的方法:

  1. createStore:这个 Redux API 接受 reducer 方法作为参数,返回一个 store。
  2. store.subscribe:订阅 state 的变化,当 state 变化的时候执行回调,可以有多个 subscribe,里面的回调会依次执行。
  3. store.dispatch:触发 action 的方法,每次 dispatch action 都会执行reducer 生成新的 state,然后执行 subscribe 注册的回调。
  4. store.getState:一个简单的方法,返回当前的 state。

这里 subscribe 注册回调,dispatch 触发回调,这不就是前文介绍的 发布订阅模式 吗?直接开始实现:

function createStore(reducer, enhancer) {
    // 先处理 enhancer
    // 如果 enhancer 存在并且是函数,我们将 createStore 作为参数传给他
    // 返回一个新的 createStore
    // 再拿这个新的 createStore 执行,应该得到一个 Store,返回 Store
    if (enhancer && typeof enhancer === 'function') {
        const newCreateStore = enhancer(createStore)
        const newStore = newCreateStore(reducer)
        return newStore
    }
    
    let state,          // state记录所有状态
        listeners = []  // 保存所有注册的回调
    function subscribe(callback) {
        listeners.push(callback)
    }
    // 先执行 reducer 修改并返回新的 state,然后将所有的回调拿出来依次执行就行
    function dispatch(action) {
        state = reducer(state, action) // 这一步别忘
        
        for (let i=0; i<listeners.length; i++) {
            const listener = listeners[i]
            listener()
        }
    }
    function getState() {
        return state
    }
    // store 包装一下前面的方法直接返回
    const store = {
        subscribe,
        dispatch,
        getState
    }
    return store
}

❤️ 看这里 (* ̄︶ ̄)

文章到这里,暂时 就告一段落了,你可能会问:就这?

这,当然不够,该选题虽然足以应付工作中、面试中的大多数场景,但仍旧不足以体现一个程序员的真实素养,前端进阶不是靠简单的死记硬背就能轻松达成的。笔者始终坚信,程序员需要足够的时间沉淀,找准目标,深入学习,去专精几个技能,才能不断前进。

如果你觉得这篇内容对你挺有启发,记得点个 丫,让更多的人也能看到这篇内容,拜托啦,这对我真的很重要。

往期精选:

参考文献

工厂模式详解及项目实战

当面试官问你Vue响应式原理,你可以这么回答他

Vue reactive

redux 原理解析

redux 官方文档

reduxjs