本合集旨在巩固前端工程师核心基础技能,从各大小厂高频手写题出发,对常见的模拟实现进行总结。更详尽的源代码放在 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 数据劫持 。
简单梳理:
-
实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
-
实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到 通知,调用更新函数进行数据更新。
-
实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的 桥梁,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
-
实现一个订阅器 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>
实现思路:
- 绑定
hashchange
事件,实现前端路由 - 将传入的路由和组件做一个路由映射,切换哪个路由即可找到对应的组件显示
- 需要 new 一个 Vue 实例还做响应式通信,当路由改变的时候,router-view 会响应更新
- 注册
router-link
和router-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
一个状态管理库。
注意:这里的 Redux 和 React-Redux 看起来很像,但是他们的核心理念和关注点是不同的,Redux 其实只是一个单纯状态管理库,可以与任何框架一起用,没有界面相关的东西;React-Redux 关注的是怎么将 Redux 跟 React 结合起来,用到了一些 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
分析下上面的代码主要涉及的方法:
- createStore:这个 Redux API 接受 reducer 方法作为参数,返回一个 store。
- store.subscribe:订阅 state 的变化,当 state 变化的时候执行回调,可以有多个 subscribe,里面的回调会依次执行。
- store.dispatch:触发 action 的方法,每次 dispatch action 都会执行reducer 生成新的 state,然后执行 subscribe 注册的回调。
- 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
}
❤️ 看这里 (* ̄︶ ̄)
文章到这里,暂时 就告一段落了,你可能会问:就这?
这,当然不够,该选题虽然足以应付工作中、面试中的大多数场景,但仍旧不足以体现一个程序员的真实素养,前端进阶不是靠简单的死记硬背就能轻松达成的。笔者始终坚信,程序员需要足够的时间沉淀,找准目标,深入学习,去专精几个技能,才能不断前进。
如果你觉得这篇内容对你挺有启发,记得点个 赞
丫,让更多的人也能看到这篇内容,拜托啦,这对我真的很重要。
往期精选: