实现一个compose函数
组合多个函数,从右到左,比如:compose(f, g, h)
最终得到这个结果(...args) => f(g(h(...args))).
题目描述:实现一个 compose
函数
// 用法如下: | |
function fn1(x) { | |
return x + 1; | |
} | |
function fn2(x) { | |
return x + 2; | |
} | |
function fn3(x) { | |
return x + 3; | |
} | |
function fn4(x) { | |
return x + 4; | |
} | |
const a = compose(fn1, fn2, fn3, fn4); | |
console.log(a(1)); // 1+4+3+2+1=11 |
实现代码如下
function compose(...funcs) { | |
if (!funcs.length) return (v) => v; | |
if (funcs.length === 1) { | |
return funcs[0] | |
} | |
return funcs.reduce((a, b) => { | |
return (...args) => a(b(...args))) | |
} | |
} |
compose
创建了一个从右向左执行的数据流。如果要实现从左到右的数据流,可以直接更改compose
的部分代码即可实现
- 更换
Api
接口:把reduce
改为reduceRight
- 交互包裹位置:把
a(b(...args))
改为b(a(...args))
实现迭代器生成函数
我们说迭代器对象全凭迭代器生成函数帮我们生成。在ES6
中,实现一个迭代器生成函数并不是什么难事儿,因为ES6早帮我们考虑好了全套的解决方案,内置了贴心的 生成器 (Generator
)供我们使用:
// 编写一个迭代器生成函数 | |
function *iteratorGenerator() { | |
yield '1号选手' | |
yield '2号选手' | |
yield '3号选手' | |
} | |
const iterator = iteratorGenerator() | |
iterator.next() | |
iterator.next() | |
iterator.next() |
丢进控制台,不负众望:
写一个生成器函数并没有什么难度,但在面试的过程中,面试官往往对生成器这种语法糖背后的实现逻辑更感兴趣。下面我们要做的,不仅仅是写一个迭代器对象,而是用ES5
去写一个能够生成迭代器对象的迭代器生成函数(解析在注释里):
// 定义生成器函数,入参是任意集合 | |
function iteratorGenerator(list) { | |
// idx记录当前访问的索引 | |
var idx = 0 | |
// len记录传入集合的长度 | |
var len = list.length | |
return { | |
// 自定义next方法 | |
next: function() { | |
// 如果索引还没有超出集合长度,done为false | |
var done = idx >= len | |
// 如果done为false,则可以继续取值 | |
var value = !done ? list[idx++] : undefined | |
// 将当前值与遍历是否完毕(done)返回 | |
return { | |
done: done, | |
value: value | |
} | |
} | |
} | |
} | |
var iterator = iteratorGenerator(['1号选手', '2号选手', '3号选手']) | |
iterator.next() | |
iterator.next() | |
iterator.next() |
此处为了记录每次遍历的位置,我们实现了一个闭包,借助自由变量来做我们的迭代过程中的“游标”。
运行一下我们自定义的迭代器,结果符合预期:
实现单例模式
核心要点: 用闭包和Proxy
属性拦截
function proxy(func) { | |
let instance; | |
let handler = { | |
constructor(target, args) { | |
if(!instance) { | |
instance = Reflect.constructor(fun, args); | |
} | |
return instance; | |
} | |
} | |
return new Proxy(func, handler); | |
} |
实现reduce方法
- 初始值不传怎么处理
- 回调函数的参数有哪些,返回值如何处理。
Array.prototype.myReduce = function(fn, initialValue) { | |
var arr = Array.prototype.slice.call(this); | |
var res, startIndex; | |
res = initialValue ? initialValue : arr[0]; // 不传默认取数组第一项 | |
startIndex = initialValue ? 0 : 1; | |
for(var i = startIndex; i < arr.length; i++) { | |
// 把初始值、当前值、索引、当前数组返回去。调用的时候传到函数参数中 [1,2,3,4].reduce((initVal,curr,index,arr)) | |
res = fn.call(null, res, arr[i], i, this); | |
} | |
return res; | |
} |
实现redux中间件
简单实现
function createStore(reducer) { | |
let currentState | |
let listeners = [] | |
function getState() { | |
return currentState | |
} | |
function dispatch(action) { | |
currentState = reducer(currentState, action) | |
listeners.map(listener => { | |
listener() | |
}) | |
return action | |
} | |
function subscribe(cb) { | |
listeners.push(cb) | |
return () => {} | |
} | |
dispatch({type: 'ZZZZZZZZZZ'}) | |
return { | |
getState, | |
dispatch, | |
subscribe | |
} | |
} | |
// 应用实例如下: | |
function reducer(state = 0, action) { | |
switch (action.type) { | |
case 'ADD': | |
return state + 1 | |
case 'MINUS': | |
return state - 1 | |
default: | |
return state | |
} | |
} | |
const store = createStore(reducer) | |
console.log(store); | |
store.subscribe(() => { | |
console.log('change'); | |
}) | |
console.log(store.getState()); | |
console.log(store.dispatch({type: 'ADD'})); | |
console.log(store.getState()); |
2. 迷你版
export const createStore = (reducer,enhancer)=>{ | |
if(enhancer) { | |
return enhancer(createStore)(reducer) | |
} | |
let currentState = {} | |
let currentListeners = [] | |
const getState = ()=>currentState | |
const subscribe = (listener)=>{ | |
currentListeners.push(listener) | |
} | |
const dispatch = action=>{ | |
currentState = reducer(currentState, action) | |
currentListeners.forEach(v=>v()) | |
return action | |
} | |
dispatch({type:'@@INIT'}) | |
return {getState,subscribe,dispatch} | |
} | |
//中间件实现 | |
export applyMiddleWare(...middlewares){ | |
return createStore=>...args=>{ | |
const store = createStore(...args) | |
let dispatch = store.dispatch | |
const midApi = { | |
getState:store.getState, | |
dispatch:...args=>dispatch(...args) | |
} | |
const middlewaresChain = middlewares.map(middleware=>middleware(midApi)) | |
dispatch = compose(...middlewaresChain)(store.dispatch) | |
return { | |
...store, | |
dispatch | |
} | |
} | |
// fn1(fn2(fn3())) 把函数嵌套依次调用 | |
export function compose(...funcs){ | |
if(funcs.length===0){ | |
return arg=>arg | |
} | |
if(funs.length===1){ | |
return funs[0] | |
} | |
return funcs.reduce((ret,item)=>(...args)=>ret(item(...args))) | |
} | |
//bindActionCreator实现 | |
function bindActionCreator(creator,dispatch){ | |
return ...args=>dispatch(creator(...args)) | |
} | |
function bindActionCreators(creators,didpatch){ | |
//let bound = {} | |
//Object.keys(creators).forEach(v=>{ | |
// let creator = creator[v] | |
// bound[v] = bindActionCreator(creator,dispatch) | |
//}) | |
//return bound | |
return Object.keys(creators).reduce((ret,item)=>{ | |
ret[item] = bindActionCreator(creators[item],dispatch) | |
return ret | |
},{}) | |
} |
实现数组扁平化flat方法
题目描述: 实现一个方法使多维数组变成一维数组
let ary = [1, [2, [3, [4, 5]]], 6]; | |
let str = JSON.stringify(ary); |
第0种处理:直接的调用
arr_flat = arr.flat(Infinity);
第一种处理
ary = str.replace(/(\[|\])/g, '').split(',');
第二种处理
str = str.replace(/(\[\]))/g, ''); | |
str = '[' + str + ']'; | |
ary = JSON.parse(str); |
第三种处理:递归处理
let result = []; | |
let fn = function(ary) { | |
for(let i = 0; i < ary.length; i++) }{ | |
let item = ary[i]; | |
if (Array.isArray(ary[i])){ | |
fn(item); | |
} else { | |
result.push(item); | |
} | |
} | |
} |
第四种处理:用 reduce 实现数组的 flat 方法
function flatten(ary) { | |
return ary.reduce((pre, cur) => { | |
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur); | |
}, []); | |
} | |
let ary = [1, 2, [3, 4], [5, [6, 7]]] | |
console.log(flatten(ary)) |
第五种处理:能用迭代的思路去实现
function flatten(arr) { | |
if (!arr.length) return; | |
while (arr.some((item) => Array.isArray(item))) { | |
arr = [].concat(...arr); | |
} | |
return arr; | |
} | |
// console.log(flatten([1, 2, [1, [2, 3, [4, 5, [6]]]]])); |
第六种处理:扩展运算符
while (ary.some(Array.isArray)) { | |
ary = [].concat(...ary); | |
} |
参考 前端进阶面试题详细解答
实现深拷贝
简洁版本
简单版:
const newObj = JSON.parse(JSON.stringify(oldObj));
局限性:
- 他无法实现对函数 、RegExp等特殊对象的克隆
- 会抛弃对象的
constructo
r,所有的构造函数会指向Object
- 对象有循环引用,会报错
面试简版
function deepClone(obj) { | |
// 如果是 值类型 或 null,则直接return | |
if(typeof obj !== 'object' || obj === null) { | |
return obj | |
} | |
// 定义结果对象 | |
let copy = {} | |
// 如果对象是数组,则定义结果数组 | |
if(obj.constructor === Array) { | |
copy = [] | |
} | |
// 遍历对象的key | |
for(let key in obj) { | |
// 如果key是对象的自有属性 | |
if(obj.hasOwnProperty(key)) { | |
// 递归调用深拷贝方法 | |
copy[key] = deepClone(obj[key]) | |
} | |
} | |
return copy | |
} |
调用深拷贝方法,若属性为值类型,则直接返回;若属性为引用类型,则递归遍历。这就是我们在解这一类题时的核心的方法。
进阶版
- 解决拷贝循环引用问题
- 解决拷贝对应原型问题
// 递归拷贝 (类型判断) | |
function deepClone(value,hash = new WeakMap){ // 弱引用,不用map,weakMap更合适一点 | |
// null 和 undefiend 是不需要拷贝的 | |
if(value == null){ return value;} | |
if(value instanceof RegExp) { return new RegExp(value) } | |
if(value instanceof Date) { return new Date(value) } | |
// 函数是不需要拷贝 | |
if(typeof value != 'object') return value; | |
let obj = new value.constructor(); // [] {} | |
// 说明是一个对象类型 | |
if(hash.get(value)){ | |
return hash.get(value) | |
} | |
hash.set(value,obj); | |
for(let key in value){ // in 会遍历当前对象上的属性 和 __proto__指代的属性 | |
// 补拷贝 对象的__proto__上的属性 | |
if(value.hasOwnProperty(key)){ | |
// 如果值还有可能是对象 就继续拷贝 | |
obj[key] = deepClone(value[key],hash); | |
} | |
} | |
return obj | |
// 区分对象和数组 Object.prototype.toString.call | |
} | |
// test | |
var o = {}; | |
o.x = o; | |
var o1 = deepClone(o); // 如果这个对象拷贝过了 就返回那个拷贝的结果就可以了 | |
console.log(o1); |
实现完整的深拷贝
1. 简易版及问题
JSON.parse(JSON.stringify());
估计这个api能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:
- 无法解决
循环引用
的问题。举个例子:
const a = {val:2}; | |
a.target = a; |
拷贝a
会出现系统栈溢出,因为出现了无限递归的情况。
- 无法拷贝一些特殊的对象,诸如
RegExp, Date, Set, Map
等 - 无法拷贝
函数
(划重点)。
因此这个api先pass掉,我们重新写一个深拷贝,简易版如下:
const deepClone = (target) => { | |
if (typeof target === 'object' && target !== null) { | |
const cloneTarget = Array.isArray(target) ? []: {}; | |
for (let prop in target) { | |
if (target.hasOwnProperty(prop)) { | |
cloneTarget[prop] = deepClone(target[prop]); | |
} | |
} | |
return cloneTarget; | |
} else { | |
return target; | |
} | |
} |
现在,我们以刚刚发现的三个问题为导向,一步步来完善、优化我们的深拷贝代码。
2. 解决循环引用
现在问题如下:
let obj = {val : 100}; | |
obj.target = obj; | |
deepClone(obj);//报错: RangeError: Maximum call stack size exceeded |
这就是循环引用。我们怎么来解决这个问题呢?
创建一个Map。记录下已经拷贝过的对象,如果说已经拷贝过,那直接返回它行了。
const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null; | |
const deepClone = (target, map = new Map()) => { | |
if(map.get(target)) | |
return target; | |
if (isObject(target)) { | |
map.set(target, true); | |
const cloneTarget = Array.isArray(target) ? []: {}; | |
for (let prop in target) { | |
if (target.hasOwnProperty(prop)) { | |
cloneTarget[prop] = deepClone(target[prop],map); | |
} | |
} | |
return cloneTarget; | |
} else { | |
return target; | |
} | |
} |
现在来试一试:
const a = {val:2}; | |
a.target = a; | |
let newA = deepClone(a); | |
console.log(newA)//{ val: 2, target: { val: 2, target: [Circular] } } |
好像是没有问题了, 拷贝也完成了。但还是有一个潜在的坑, 就是map 上的 key 和 map 构成了强引用关系,这是相当危险的。我给你解释一下与之相对的弱引用的概念你就明白了
在计算机程序设计中,弱引用与强引用相对,
被弱引用的对象可以在任何时候被回收,而对于强引用来说,只要这个强引用还在,那么对象无法被回收。拿上面的例子说,map 和 a一直是强引用的关系, 在程序结束之前,a 所占的内存空间一直不会被释放。
怎么解决这个问题?
很简单,让 map 的 key 和 map 构成弱引用即可。ES6给我们提供了这样的数据结构,它的名字叫WeakMap,它是一种特殊的Map, 其中的键是弱引用的。其键必须是对象,而值可以是任意的
稍微改造一下即可:
const deepClone = (target, map = new WeakMap()) => { | |
//... | |
} |
3. 拷贝特殊对象
可继续遍历
对于特殊的对象,我们使用以下方式来鉴别:
Object.prototype.toString.call(obj);
梳理一下对于可遍历对象会有什么结果:
["object Map"] | |
["object Set"] | |
["object Array"] | |
["object Object"] | |
["object Arguments"] |
以这些不同的字符串为依据,我们就可以成功地鉴别这些对象。
const getType = Object.prototype.toString.call(obj); | |
const canTraverse = { | |
'[object Map]': true, | |
'[object Set]': true, | |
'[object Array]': true, | |
'[object Object]': true, | |
'[object Arguments]': true, | |
}; | |
const deepClone = (target, map = new Map()) => { | |
if(!isObject(target)) | |
return target; | |
let type = getType(target); | |
let cloneTarget; | |
if(!canTraverse[type]) { | |
// 处理不能遍历的对象 | |
return; | |
}else { | |
// 这波操作相当关键,可以保证对象的原型不丢失! | |
let ctor = target.prototype; | |
cloneTarget = new ctor(); | |
} | |
if(map.get(target)) | |
return target; | |
map.put(target, true); | |
if(type === mapTag) { | |
//处理Map | |
target.forEach((item, key) => { | |
cloneTarget.set(deepClone(key), deepClone(item)); | |
}) | |
} | |
if(type === setTag) { | |
//处理Set | |
target.forEach(item => { | |
target.add(deepClone(item)); | |
}) | |
} | |
// 处理数组和对象 | |
for (let prop in target) { | |
if (target.hasOwnProperty(prop)) { | |
cloneTarget[prop] = deepClone(target[prop]); | |
} | |
} | |
return cloneTarget; | |
} |
不可遍历的对象
const boolTag = '[object Boolean]'; | |
const numberTag = '[object Number]'; | |
const stringTag = '[object String]'; | |
const dateTag = '[object Date]'; | |
const errorTag = '[object Error]'; | |
const regexpTag = '[object RegExp]'; | |
const funcTag = '[object Function]'; |
对于不可遍历的对象,不同的对象有不同的处理。
const handleRegExp = (target) => { | |
const { source, flags } = target; | |
return new target.constructor(source, flags); | |
} | |
const handleFunc = (target) => { | |
// 待会的重点部分 | |
} | |
const handleNotTraverse = (target, tag) => { | |
const Ctor = targe.constructor; | |
switch(tag) { | |
case boolTag: | |
case numberTag: | |
case stringTag: | |
case errorTag: | |
case dateTag: | |
return new Ctor(target); | |
case regexpTag: | |
return handleRegExp(target); | |
case funcTag: | |
return handleFunc(target); | |
default: | |
return new Ctor(target); | |
} | |
} |
4. 拷贝函数
- 虽然函数也是对象,但是它过于特殊,我们单独把它拿出来拆解。
- 提到函数,在JS种有两种函数,一种是普通函数,另一种是箭头函数。每个普通函数都是
- Function的实例,而箭头函数不是任何类的实例,每次调用都是不一样的引用。那我们只需要
- 处理普通函数的情况,箭头函数直接返回它本身就好了。
那么如何来区分两者呢?
答案是: 利用原型。箭头函数是不存在原型的。
const handleFunc = (func) => { | |
// 箭头函数直接返回自身 | |
if(!func.prototype) return func; | |
const bodyReg = /(?<={)(.|\n)+(?=})/m; | |
const paramReg = /(?<=\().+(?=\)\s+{)/; | |
const funcString = func.toString(); | |
// 分别匹配 函数参数 和 函数体 | |
const param = paramReg.exec(funcString); | |
const body = bodyReg.exec(funcString); | |
if(!body) return null; | |
if (param) { | |
const paramArr = param[0].split(','); | |
return new Function(...paramArr, body[0]); | |
} else { | |
return new Function(body[0]); | |
} | |
} |
5. 完整代码展示
const getType = obj => Object.prototype.toString.call(obj); | |
const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null; | |
const canTraverse = { | |
'[object Map]': true, | |
'[object Set]': true, | |
'[object Array]': true, | |
'[object Object]': true, | |
'[object Arguments]': true, | |
}; | |
const mapTag = '[object Map]'; | |
const setTag = '[object Set]'; | |
const boolTag = '[object Boolean]'; | |
const numberTag = '[object Number]'; | |
const stringTag = '[object String]'; | |
const symbolTag = '[object Symbol]'; | |
const dateTag = '[object Date]'; | |
const errorTag = '[object Error]'; | |
const regexpTag = '[object RegExp]'; | |
const funcTag = '[object Function]'; | |
const handleRegExp = (target) => { | |
const { source, flags } = target; | |
return new target.constructor(source, flags); | |
} | |
const handleFunc = (func) => { | |
// 箭头函数直接返回自身 | |
if(!func.prototype) return func; | |
const bodyReg = /(?<={)(.|\n)+(?=})/m; | |
const paramReg = /(?<=\().+(?=\)\s+{)/; | |
const funcString = func.toString(); | |
// 分别匹配 函数参数 和 函数体 | |
const param = paramReg.exec(funcString); | |
const body = bodyReg.exec(funcString); | |
if(!body) return null; | |
if (param) { | |
const paramArr = param[0].split(','); | |
return new Function(...paramArr, body[0]); | |
} else { | |
return new Function(body[0]); | |
} | |
} | |
const handleNotTraverse = (target, tag) => { | |
const Ctor = target.constructor; | |
switch(tag) { | |
case boolTag: | |
return new Object(Boolean.prototype.valueOf.call(target)); | |
case numberTag: | |
return new Object(Number.prototype.valueOf.call(target)); | |
case stringTag: | |
return new Object(String.prototype.valueOf.call(target)); | |
case symbolTag: | |
return new Object(Symbol.prototype.valueOf.call(target)); | |
case errorTag: | |
case dateTag: | |
return new Ctor(target); | |
case regexpTag: | |
return handleRegExp(target); | |
case funcTag: | |
return handleFunc(target); | |
default: | |
return new Ctor(target); | |
} | |
} | |
const deepClone = (target, map = new WeakMap()) => { | |
if(!isObject(target)) | |
return target; | |
let type = getType(target); | |
let cloneTarget; | |
if(!canTraverse[type]) { | |
// 处理不能遍历的对象 | |
return handleNotTraverse(target, type); | |
}else { | |
// 这波操作相当关键,可以保证对象的原型不丢失! | |
let ctor = target.constructor; | |
cloneTarget = new ctor(); | |
} | |
if(map.get(target)) | |
return target; | |
map.set(target, true); | |
if(type === mapTag) { | |
//处理Map | |
target.forEach((item, key) => { | |
cloneTarget.set(deepClone(key, map), deepClone(item, map)); | |
}) | |
} | |
if(type === setTag) { | |
//处理Set | |
target.forEach(item => { | |
cloneTarget.add(deepClone(item, map)); | |
}) | |
} | |
// 处理数组和对象 | |
for (let prop in target) { | |
if (target.hasOwnProperty(prop)) { | |
cloneTarget[prop] = deepClone(target[prop], map); | |
} | |
} | |
return cloneTarget; | |
} |
实现every方法
Array.prototype.myEvery=function(callback, context = window){ | |
var len=this.length, | |
flag=true, | |
i = 0; | |
for(;i < len; i++){ | |
if(!callback.apply(context,[this[i], i , this])){ | |
flag=false; | |
break; | |
} | |
} | |
return flag; | |
} | |
// var obj = {num: 1} | |
// var aa=arr.myEvery(function(v,index,arr){ | |
// return v.num>=12; | |
// },obj) | |
// console.log(aa) |
实现map方法
- 回调函数的参数有哪些,返回值如何处理
- 不修改原来的数组
Array.prototype.myMap = function(callback, context){ | |
// 转换类数组 | |
var arr = Array.prototype.slice.call(this),//由于是ES5所以就不用...展开符了 | |
mappedArr = [], | |
i = 0; | |
for (; i < arr.length; i++ ){ | |
// 把当前值、索引、当前数组返回去。调用的时候传到函数参数中 [1,2,3,4].map((curr,index,arr)) | |
mappedArr.push(callback.call(context, arr[i], i, this)); | |
} | |
return mappedArr; | |
} |
实现一个迷你版的vue
入口
// js/vue.js | |
class Vue { | |
constructor (options) { | |
// 1. 通过属性保存选项的数据 | |
this.$options = options || {} | |
this.$data = options.data || {} | |
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el | |
// 2. 把data中的成员转换成getter和setter,注入到vue实例中 | |
this._proxyData(this.$data) | |
// 3. 调用observer对象,监听数据的变化 | |
new Observer(this.$data) | |
// 4. 调用compiler对象,解析指令和差值表达式 | |
new Compiler(this) | |
} | |
_proxyData (data) { | |
// 遍历data中的所有属性 | |
Object.keys(data).forEach(key => { | |
// 把data的属性注入到vue实例中 | |
Object.defineProperty(this, key, { | |
enumerable: true, | |
configurable: true, | |
get () { | |
return data[key] | |
}, | |
set (newValue) { | |
if (newValue === data[key]) { | |
return | |
} | |
data[key] = newValue | |
} | |
}) | |
}) | |
} | |
} |
实现Dep
class Dep { | |
constructor () { | |
// 存储所有的观察者 | |
this.subs = [] | |
} | |
// 添加观察者 | |
addSub (sub) { | |
if (sub && sub.update) { | |
this.subs.push(sub) | |
} | |
} | |
// 发送通知 | |
notify () { | |
this.subs.forEach(sub => { | |
sub.update() | |
}) | |
} | |
} |
实现watcher
class Watcher { | |
constructor (vm, key, cb) { | |
this.vm = vm | |
// data中的属性名称 | |
this.key = key | |
// 回调函数负责更新视图 | |
this.cb = cb | |
// 把watcher对象记录到Dep类的静态属性target | |
Dep.target = this | |
// 触发get方法,在get方法中会调用addSub | |
this.oldValue = vm[key] | |
Dep.target = null | |
} | |
// 当数据发生变化的时候更新视图 | |
update () { | |
let newValue = this.vm[this.key] | |
if (this.oldValue === newValue) { | |
return | |
} | |
this.cb(newValue) | |
} | |
} |
实现compiler
class Compiler { | |
constructor (vm) { | |
this.el = vm.$el | |
this.vm = vm | |
this.compile(this.el) | |
} | |
// 编译模板,处理文本节点和元素节点 | |
compile (el) { | |
let childNodes = el.childNodes | |
Array.from(childNodes).forEach(node => { | |
// 处理文本节点 | |
if (this.isTextNode(node)) { | |
this.compileText(node) | |
} else if (this.isElementNode(node)) { | |
// 处理元素节点 | |
this.compileElement(node) | |
} | |
// 判断node节点,是否有子节点,如果有子节点,要递归调用compile | |
if (node.childNodes && node.childNodes.length) { | |
this.compile(node) | |
} | |
}) | |
} | |
// 编译元素节点,处理指令 | |
compileElement (node) { | |
// console.log(node.attributes) | |
// 遍历所有的属性节点 | |
Array.from(node.attributes).forEach(attr => { | |
// 判断是否是指令 | |
let attrName = attr.name | |
if (this.isDirective(attrName)) { | |
// v-text --> text | |
attrName = attrName.substr(2) | |
let key = attr.value | |
this.update(node, key, attrName) | |
} | |
}) | |
} | |
update (node, key, attrName) { | |
let updateFn = this[attrName + 'Updater'] | |
updateFn && updateFn.call(this, node, this.vm[key], key) | |
} | |
// 处理 v-text 指令 | |
textUpdater (node, value, key) { | |
node.textContent = value | |
new Watcher(this.vm, key, (newValue) => { | |
node.textContent = newValue | |
}) | |
} | |
// v-model | |
modelUpdater (node, value, key) { | |
node.value = value | |
new Watcher(this.vm, key, (newValue) => { | |
node.value = newValue | |
}) | |
// 双向绑定 | |
node.addEventListener('input', () => { | |
this.vm[key] = node.value | |
}) | |
} | |
// 编译文本节点,处理差值表达式 | |
compileText (node) { | |
// console.dir(node) | |
// {{ msg }} | |
let reg = /\{\{(.+?)\}\}/ | |
let value = node.textContent | |
if (reg.test(value)) { | |
let key = RegExp.$1.trim() | |
node.textContent = value.replace(reg, this.vm[key]) | |
// 创建watcher对象,当数据改变更新视图 | |
new Watcher(this.vm, key, (newValue) => { | |
node.textContent = newValue | |
}) | |
} | |
} | |
// 判断元素属性是否是指令 | |
isDirective (attrName) { | |
return attrName.startsWith('v-') | |
} | |
// 判断节点是否是文本节点 | |
isTextNode (node) { | |
return node.nodeType === 3 | |
} | |
// 判断节点是否是元素节点 | |
isElementNode (node) { | |
return node.nodeType === 1 | |
} | |
} |
实现Observer
class Observer { | |
constructor (data) { | |
this.walk(data) | |
} | |
walk (data) { | |
// 1. 判断data是否是对象 | |
if (!data || typeof data !== 'object') { | |
return | |
} | |
// 2. 遍历data对象的所有属性 | |
Object.keys(data).forEach(key => { | |
this.defineReactive(data, key, data[key]) | |
}) | |
} | |
defineReactive (obj, key, val) { | |
let that = this | |
// 负责收集依赖,并发送通知 | |
let dep = new Dep() | |
// 如果val是对象,把val内部的属性转换成响应式数据 | |
this.walk(val) | |
Object.defineProperty(obj, key, { | |
enumerable: true, | |
configurable: true, | |
get () { | |
// 收集依赖 | |
Dep.target && dep.addSub(Dep.target) | |
return val | |
}, | |
set (newValue) { | |
if (newValue === val) { | |
return | |
} | |
val = newValue | |
that.walk(newValue) | |
// 发送通知 | |
dep.notify() | |
} | |
}) | |
} | |
} |
使用
<html lang="cn"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge"> | |
<title>Mini Vue</title> | |
</head> | |
<body> | |
<div id="app"> | |
<h1>差值表达式</h1> | |
<h3>{{ msg }}</h3> | |
<h3>{{ count }}</h3> | |
<h1>v-text</h1> | |
<div v-text="msg"></div> | |
<h1>v-model</h1> | |
<input type="text" v-model="msg"> | |
<input type="text" v-model="count"> | |
</div> | |
<script src="./js/dep.js"></script> | |
<script src="./js/watcher.js"></script> | |
<script src="./js/compiler.js"></script> | |
<script src="./js/observer.js"></script> | |
<script src="./js/vue.js"></script> | |
<script> | |
let vm = new Vue({ | |
el: '#app', | |
data: { | |
msg: 'Hello Vue', | |
count: 100, | |
person: { name: 'zs' } | |
} | |
}) | |
console.log(vm.msg) | |
// vm.msg = { test: 'Hello' } | |
vm.test = 'abc' | |
</script> | |
</body> | |
</html> |
实现ES6的extends
function B(name){ | |
this.name = name; | |
}; | |
function A(name,age){ | |
//1.将A的原型指向B | |
Object.setPrototypeOf(A,B); | |
//2.用A的实例作为this调用B,得到继承B之后的实例,这一步相当于调用super | |
Object.getPrototypeOf(A).call(this, name) | |
//3.将A原有的属性添加到新实例上 | |
this.age = age; | |
//4.返回新实例对象 | |
return this; | |
}; | |
var a = new A('poetry',22); | |
console.log(a); |
实现ES6的const
由于ES5环境没有block
的概念,所以是无法百分百实现const
,只能是挂载到某个对象下,要么是全局的windo
w,要么就是自定义一个object
来当容器
var __const = function __const (data, value) { | |
window.data = value // 把要定义的data挂载到window下,并赋值value | |
Object.defineProperty(window, data, { // 利用Object.defineProperty的能力劫持当前对象,并修改其属性描述符 | |
enumerable: false, | |
configurable: false, | |
get: function () { | |
return value | |
}, | |
set: function (data) { | |
if (data !== value) { // 当要对当前属性进行赋值时,则抛出错误! | |
throw new TypeError('Assignment to constant variable.') | |
} else { | |
return value | |
} | |
} | |
}) | |
} | |
__const('a', 10) | |
console.log(a) | |
delete a | |
console.log(a) | |
for (let item in window) { // 因为const定义的属性在global下也是不存在的,所以用到了enumerable: false来模拟这一功能 | |
if (item === 'a') { // 因为不可枚举,所以不执行 | |
console.log(window[item]) | |
} | |
} | |
a = 20 // 报错 |
Vue
目前双向绑定的核心实现思路就是利用Object.defineProperty
对get
跟set
进行劫持,监听用户对属性进行调用以及赋值时的具体情况,从而实现的双向绑定
实现节流函数(throttle)
节流函数原理:指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。总结起来就是: 事件,按照一段时间的间隔来进行触发 。
像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多
手写简版
使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行
时间戳方式:
// func是用户传入需要防抖的函数 | |
// wait是等待时间 | |
const throttle = (func, wait = 50) => { | |
// 上一次执行该函数的时间 | |
let lastTime = 0 | |
return function(...args) { | |
// 当前时间 | |
let now = +new Date() | |
// 将当前时间和上一次执行函数时间对比 | |
// 如果差值大于设置的等待时间就执行函数 | |
if (now - lastTime > wait) { | |
lastTime = now | |
func.apply(this, args) | |
} | |
} | |
} | |
setInterval( | |
throttle(() => { | |
console.log(1) | |
}, 500), | |
1 | |
) |
定时器方式:
使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数
function throttle(func, delay){ | |
var timer = null; | |
returnfunction(){ | |
var context = this; | |
var args = arguments; | |
if(!timer){ | |
timer = setTimeout(function(){ | |
func.apply(context, args); | |
timer = null; | |
},delay); | |
} | |
} | |
} |
适用场景:
DOM
元素的拖拽功能实现(mousemove
)- 搜索联想(
keyup
) - 计算鼠标移动的距离(
mousemove
) Canvas
模拟画板功能(mousemove
)- 监听滚动事件判断是否到页面底部自动加载更多
- 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
- 缩放场景:监控浏览器
resize
- 动画场景:避免短时间内多次触发动画引起性能问题
总结
- 函数防抖 :将几次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
- 函数节流 :使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。
实现模板字符串解析功能
let template = '我是{{name}},年龄{{age}},性别{{sex}}'; | |
let data = { | |
name: '姓名', | |
age: 18 | |
} | |
render(template, data); // 我是姓名,年龄18,性别undefined | |
function render(template, data) { | |
const reg = /\{\{(\w+)\}\}/; // 模板字符串正则 | |
if (reg.test(template)) { // 判断模板里是否有模板字符串 | |
const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段 | |
template = template.replace(reg, data[name]); // 将第一个模板字符串渲染 | |
return render(template, data); // 递归的渲染并返回渲染后的结构 | |
} | |
return template; // 如果模板没有模板字符串直接返回 | |
} |
实现一个简易的MVVM
实现一个简易的MVVM
我会分为这么几步来:
- 首先我会定义一个类
Vue
,这个类接收的是一个options
,那么其中可能有需要挂载的根元素的id
,也就是el
属性;然后应该还有一个data
属性,表示需要双向绑定的数据 - 其次我会定义一个
Dep
类,这个类产生的实例对象中会定义一个subs
数组用来存放所依赖这个属性的依赖,已经添加依赖的方法addSub
,删除方法removeSub
,还有一个notify
方法用来遍历更新它subs
中的所有依赖,同时Dep类有一个静态属性target
它用来表示当前的观察者,当后续进行依赖收集的时候可以将它添加到dep.subs
中。 - 然后设计一个
observe
方法,这个方法接收的是传进来的data
,也就是options.data
,里面会遍历data
中的每一个属性,并使用Object.defineProperty()
来重写它的get
和set
,那么这里面呢可以使用new Dep()
实例化一个dep
对象,在get
的时候调用其addSub
方法添加当前的观察者Dep.target
完成依赖收集,并且在set
的时候调用dep.notify
方法来通知每一个依赖它的观察者进行更新 - 完成这些之后,我们还需要一个
compile
方法来将HTML模版和数据结合起来。在这个方法中首先传入的是一个node
节点,然后遍历它的所有子级,判断是否有firstElmentChild
,有的话则进行递归调用compile方法,没有firstElementChild
的话且该child.innderHTML
用正则匹配满足有/\{\{(.*)\}\}/
项的话则表示有需要双向绑定的数据,那么就将用正则new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')
替换掉是其为msg
变量。 - 完成变量替换的同时,还需要将
Dep.target
指向当前的这个child
,且调用一下this.opt.data[key]
,也就是为了触发这个数据的get
来对当前的child
进行依赖收集,这样下次数据变化的时候就能通知child
进行视图更新了,不过在最后要记得将Dep.target
指为null
哦(其实在Vue
中是有一个targetStack
栈用来存放target
的指向的) - 那么最后我们只需要监听
document
的DOMContentLoaded
然后在回调函数中实例化这个Vue
对象就可以了
coding :
需要注意的点:
childNodes
会获取到所有的子节点以及文本节点(包括元素标签中的空白节点)firstElementChild
表示获取元素的第一个字元素节点,以此来区分是不是元素节点,如果是的话则调用compile
进行递归调用,否则用正则匹配- 这里面的正则真的不难,大家可以看一下
完整代码如下:
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> | |
<title>MVVM</title> | |
</head> | |
<body> | |
<div id="app"> | |
<h3>姓名</h3> | |
<p>{{name}}</p> | |
<h3>年龄</h3> | |
<p>{{age}}</p> | |
</div> | |
</body> | |
</html> | |
<script> | |
document.addEventListener( | |
"DOMContentLoaded", | |
function () { | |
let opt = { el: "#app", data: { name: "等待修改...", age: 20 } }; | |
let vm = new Vue(opt); | |
setTimeout(() => { | |
opt.data.name = "jing"; | |
}, 2000); | |
}, | |
false | |
); | |
class Vue { | |
constructor(opt) { | |
this.opt = opt; | |
this.observer(opt.data); | |
let root = document.querySelector(opt.el); | |
this.compile(root); | |
} | |
observer(data) { | |
Object.keys(data).forEach((key) => { | |
let obv = new Dep(); | |
data["_" + key] = data[key]; | |
Object.defineProperty(data, key, { | |
get() { | |
Dep.target && obv.addSubNode(Dep.target); | |
return data["_" + key]; | |
}, | |
set(newVal) { | |
obv.update(newVal); | |
data["_" + key] = newVal; | |
}, | |
}); | |
}); | |
} | |
compile(node) { | |
[].forEach.call(node.childNodes, (child) => { | |
if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) { | |
let key = RegExp.$1.trim(); | |
child.innerHTML = child.innerHTML.replace( | |
new RegExp("\\{\\{\\s*" + key + "\\s*\\}\\}", "gm"), | |
this.opt.data[key] | |
); | |
Dep.target = child; | |
this.opt.data[key]; | |
Dep.target = null; | |
} else if (child.firstElementChild) this.compile(child); | |
}); | |
} | |
} | |
class Dep { | |
constructor() { | |
this.subNode = []; | |
} | |
addSubNode(node) { | |
this.subNode.push(node); | |
} | |
update(newVal) { | |
this.subNode.forEach((node) => { | |
node.innerHTML = newVal; | |
}); | |
} | |
} | |
</script> |
简化版2
function update(){ | |
console.log('数据变化~~~ mock update view') | |
} | |
let obj = [1,2,3] | |
// 变异方法 push shift unshfit reverse sort splice pop | |
// Object.defineProperty | |
let oldProto = Array.prototype; | |
let proto = Object.create(oldProto); // 克隆了一分 | |
['push','shift'].forEach(item=>{ | |
proto[item] = function(){ | |
update(); | |
oldProto[item].apply(this,arguments); | |
} | |
}) | |
function observer(value){ // proxy reflect | |
if(Array.isArray(value)){ | |
// AOP | |
return value.__proto__ = proto; | |
// 重写 这个数组里的push shift unshfit reverse sort splice pop | |
} | |
if(typeof value !== 'object'){ | |
return value; | |
} | |
for(let key in value){ | |
defineReactive(value,key,value[key]); | |
} | |
} | |
function defineReactive(obj,key,value){ | |
observer(value); // 如果是对象 继续增加getter和setter | |
Object.defineProperty(obj,key,{ | |
get(){ | |
return value; | |
}, | |
set(newValue){ | |
if(newValue !== value){ | |
observer(newValue); | |
value = newValue; | |
update(); | |
} | |
} | |
}) | |
} | |
observer(obj); | |
// AOP | |
// obj.name = {n:200}; // 数据变了 需要更新视图 深度监控 | |
// obj.name.n = 100; | |
obj.push(123); | |
obj.push(456); | |
console.log(obj); |
实现instanceOf
思路:
- 步骤1:先取得当前类的原型,当前实例对象的原型链
- 步骤2:一直循环(执行原型链的查找机制)
- 取得当前实例对象原型链的原型链(
proto = proto.__proto__
,沿着原型链一直向上查找) - 如果 当前实例的原型链
__proto__
上找到了当前类的原型prototype
,则返回true
- 如果 一直找到
Object.prototype.__proto__ == null
,Object
的基类(null
)上面都没找到,则返回false
// 实例.__ptoto__ === 类.prototype | |
function _instanceof(example, classFunc) { | |
// 由于instance要检测的是某对象,需要有一个前置判断条件 | |
//基本数据类型直接返回false | |
if(typeof example !== 'object' || example === null) return false; | |
let proto = Object.getPrototypeOf(example); | |
while(true) { | |
if(proto == null) return false; | |
// 在当前实例对象的原型链上,找到了当前类 | |
if(proto == classFunc.prototype) return true; | |
// 沿着原型链__ptoto__一层一层向上查 | |
proto = Object.getPrototypeof(proto); // 等于proto.__ptoto__ | |
} | |
} | |
console.log('test', _instanceof(null, Array)) // false | |
console.log('test', _instanceof([], Array)) // true | |
console.log('test', _instanceof('', Array)) // false | |
console.log('test', _instanceof({}, Object)) // true |
实现bind方法
bind
的实现对比其他两个函数略微地复杂了一点,涉及到参数合并(类似函数柯里化),因为bind
需要返回一个函数,需要判断一些边界问题,以下是bind
的实现
bind
返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过new
的方式,我们先来说直接调用的方式- 对于直接调用来说,这里选择了
apply
的方式实现,但是对于参数需要注意以下情况:因为bind
可以实现类似这样的代码f.bind(obj, 1)(2)
,所以我们需要将两边的参数拼接起来 - 最后来说通过
new
的方式,对于new
的情况来说,不会被任何方式改变this
,所以对于这种情况我们需要忽略传入的this
简洁版本
- 对于普通函数,绑定
this
指向 - 对于构造函数,要保证原函数的原型对象上的属性不能丢失
Function.prototype.myBind = function(context = window, ...args) { | |
// this表示调用bind的函数 | |
let self = this; | |
//返回了一个函数,...innerArgs为实际调用时传入的参数 | |
let fBound = function(...innerArgs) { | |
//this instanceof fBound为true表示构造函数的情况。如new func.bind(obj) | |
// 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true,可以让实例获得来自绑定函数的值 | |
// 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context | |
return self.apply( | |
this instanceof fBound ? this : context, | |
args.concat(innerArgs) | |
); | |
} | |
// 如果绑定的是构造函数,那么需要继承构造函数原型属性和方法:保证原函数的原型对象上的属性不丢失 | |
// 实现继承的方式: 使用Object.create | |
fBound.prototype = Object.create(this.prototype); | |
return fBound; | |
} | |
// 测试用例 | |
function Person(name, age) { | |
console.log('Person name:', name); | |
console.log('Person age:', age); | |
console.log('Person this:', this); // 构造函数this指向实例对象 | |
} | |
// 构造函数原型的方法 | |
Person.prototype.say = function() { | |
console.log('person say'); | |
} | |
// 普通函数 | |
function normalFun(name, age) { | |
console.log('普通函数 name:', name); | |
console.log('普通函数 age:', age); | |
console.log('普通函数 this:', this); // 普通函数this指向绑定bind的第一个参数 也就是例子中的obj | |
} | |
var obj = { | |
name: 'poetries', | |
age: 18 | |
} | |
// 先测试作为构造函数调用 | |
var bindFun = Person.myBind(obj, 'poetry1') // undefined | |
var a = new bindFun(10) // Person name: poetry1、Person age: 10、Person this: fBound {} | |
a.say() // person say | |
// 再测试作为普通函数调用 | |
var bindNormalFun = normalFun.myBind(obj, 'poetry2') // undefined | |
bindNormalFun(12) // 普通函数name: poetry2 普通函数 age: 12 普通函数 this: {name: 'poetries', age: 18} |
注意:bind
之后不能再次修改this
的指向,bind
多次后执行,函数this
还是指向第一次bind
的对象
数组中的数据根据key去重
给定一个任意数组,实现一个通用函数,让数组中的数据根据 key 排重:
const dedup = (data, getKey = () => {} ) => { | |
// todo | |
} | |
let data = [ | |
{ id: 1, v: 1 }, | |
{ id: 2, v: 2 }, | |
{ id: 1, v: 1 }, | |
]; | |
// 以 id 作为排重 key,执行函数得到结果 | |
// data = [ | |
// { id: 1, v: 1 }, | |
// { id: 2, v: 2 }, | |
// ]; |
实现
const dedup = (data, getKey = () => { }) => { | |
const dateMap = data.reduce((pre, cur) => { | |
const key = getKey(cur) | |
if (!pre[key]) { | |
pre[key] = cur | |
} | |
return pre | |
}, {}) | |
return Object.values(dateMap) | |
} |
使用
let data = [ | |
{ id: 1, v: 1 }, | |
{ id: 2, v: 2 }, | |
{ id: 1, v: 1 }, | |
]; | |
console.log(dedup(data, (item) => item.id)) | |
// 以 id 作为排重 key,执行函数得到结果 | |
// data = [ | |
// { id: 1, v: 1 }, | |
// { id: 2, v: 2 }, | |
// ]; |
实现find方法
find
接收一个方法作为参数,方法内部返回一个条件find
会遍历所有的元素,执行你给定的带有条件返回值的函数- 符合该条件的元素会作为
find
方法的返回值 - 如果遍历结束还没有符合该条件的元素,则返回
undefined
var users = [ | |
{id: 1, name: '张三'}, | |
{id: 2, name: '张三'}, | |
{id: 3, name: '张三'}, | |
{id: 4, name: '张三'} | |
] | |
Array.prototype.myFind = function (callback) { | |
// var callback = function (item, index) { return item.id === 4 } | |
for (var i = 0; i < this.length; i++) { | |
if (callback(this[i], i)) { | |
return this[i] | |
} | |
} | |
} | |
var ret = users.myFind(function (item, index) { | |
return item.id === 2 | |
}) | |
console.log(ret) |
实现事件总线结合Vue应用
Event Bus
(Vue、Flutter 等前端框架中有出镜)和Event Emitter
(Node中有出镜)出场的“剧组”不同,但是它们都对应一个共同的角色—— 全局事件总线 。
全局事件总线,严格来说不能说是观察者模式,而是发布-订阅模式。它在我们日常的业务开发中应用非常广。
如果只能选一道题,那这道题一定是 Event Bus/Event Emitter
的代码实现——我都说这么清楚了,这个知识点到底要不要掌握、需要掌握到什么程度,就看各位自己的了。
在Vue中使用Event Bus来实现组件间的通讯
Event Bus/Event Emitter
作为全局事件总线,它起到的是一个沟通桥梁的作用。我们可以把它理解为一个事件中心,我们所有事件的订阅/发布都不能由订阅方和发布方“私下沟通”,必须要委托这个事件中心帮我们实现。
在Vue中,有时候 A 组件和 B 组件中间隔了很远,看似没什么关系,但我们希望它们之间能够通信。这种情况下除了求助于 Vuex
之外,我们还可以通过 Event Bus
来实现我们的需求。
创建一个 Event Bus
(本质上也是 Vue 实例)并导出:
const EventBus = new Vue() | |
export default EventBus |
在主文件里引入EventBus
,并挂载到全局:
import bus from 'EventBus的文件路径' | |
Vue.prototype.bus = bus |
订阅事件:
// 这里func指someEvent这个事件的监听函数 | |
this.bus.$on('someEvent', func) |
发布(触发)事件:
// 这里params指someEvent这个事件被触发时回调函数接收的入参 | |
this.bus.$emit('someEvent', params) |
大家会发现,整个调用过程中,没有出现具体的发布者和订阅者(比如上面的PrdPublisher
和DeveloperObserver
),全程只有bus
这个东西一个人在疯狂刷存在感。这就是全局事件总线的特点——所有事件的发布/订阅操作,必须经由事件中心,禁止一切“私下交易”!
下面,我们就一起来实现一个Event Bus
(注意看注释里的解析):
class EventEmitter { | |
constructor() { | |
// handlers是一个map,用于存储事件与回调之间的对应关系 | |
this.handlers = {} | |
} | |
// on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数 | |
on(eventName, cb) { | |
// 先检查一下目标事件名有没有对应的监听函数队列 | |
if (!this.handlers[eventName]) { | |
// 如果没有,那么首先初始化一个监听函数队列 | |
this.handlers[eventName] = [] | |
} | |
// 把回调函数推入目标事件的监听函数队列里去 | |
this.handlers[eventName].push(cb) | |
} | |
// emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数 | |
emit(eventName, ...args) { | |
// 检查目标事件是否有监听函数队列 | |
if (this.handlers[eventName]) { | |
// 如果有,则逐个调用队列里的回调函数 | |
this.handlers[eventName].forEach((callback) => { | |
callback(...args) | |
}) | |
} | |
} | |
// 移除某个事件回调队列里的指定回调函数 | |
off(eventName, cb) { | |
const callbacks = this.handlers[eventName] | |
const index = callbacks.indexOf(cb) | |
if (index !== -1) { | |
callbacks.splice(index, 1) | |
} | |
} | |
// 为事件注册单次监听器 | |
once(eventName, cb) { | |
// 对回调函数进行包装,使其执行完毕自动被移除 | |
const wrapper = (...args) => { | |
cb.apply(...args) | |
this.off(eventName, wrapper) | |
} | |
this.on(eventName, wrapper) | |
} | |
} |
在日常的开发中,大家用到EventBus/EventEmitter
往往提供比这五个方法多的多的多的方法。但在面试过程中,如果大家能够完整地实现出这五个方法,已经非常可以说明问题了,因此楼上这个EventBus
希望大家可以熟练掌握。学有余力的同学