前言:
当前朋友们对“js中手写ajax”都比较关怀,姐妹们都需要知道一些“js中手写ajax”的相关资讯。那么小编同时在网上搜集了一些对于“js中手写ajax””的相关知识,希望兄弟们能喜欢,咱们快快来了解一下吧!接续上篇:「中高级前端面试」手写代码合集
✨ 设计模式 ✨
设计模式,说白了就是用来解决某种特定问题的解决方案。
比如这样一个场景 :随着项目的迭代,接口的结构需要变动,为了不影响旧有业务,只能扩展而不能直接修改接口,于是我们很快想到了 适配器模式。
那么,话不多说,正文开始。
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() // 创建观察者1let observer_2 = new Observer()let sub = new Subject() // 创建目标对象sub.add(observer_1) // 添加观察者1sub.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 订阅 KeithpubSub.subscribe('blog', (arg) => console.log(`${arg} 更新了`)) // B 订阅 KeithpubSub.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 { bug(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.工厂模式
工厂模式 细分为:
简单工厂模式(工作中最常用 )工厂方法模式(很少用到)抽象工厂模式(基本不用...)简单工厂模式
没什么神秘的,就是一个带有 静态 方法的类 (简单工厂模式又名 静态工厂模式),你只需给静态方法传入正确的参数,就可以获取到你所需要的对象,而无需知道其创建的具体细节。
以一个实际项目:用户权限 来说明,我们需要根据用户的权限来渲染不同的页面,低级权限用户看不到高级权限页面。
// 工厂类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 + bconst 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 */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 getvue._data.text = '123'// in watcher updata// in get复制代码
refactor 版:
// Dep moduleclass 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/observefunction 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) }}// watcherclass 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 部分:
// 转换为真实 Domfunction 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发出去要用dispatchstore.dispatch({ type: 'SUPPLY_GOODS' }) // lipstickArmani_200: 1store.dispatch({ type: 'SUPPLY_GOODS' }) // lipstickArmani_200: 2store.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}
标签: #js中手写ajax