龙空技术网

「中高级前端面试」手写代码续集

前端十亿少女的梦 98

前言:

当前朋友们对“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