1.实现一个 sleep 函数,比如 sleep(1000) 意味着等待1000毫秒
js
// 使用 promise来实现 sleep
const sleep = (time) => {
return new Promise(resolve => setTimeout(resolve, time))
}
sleep(1000).then(() => {
// 这里写你的骚操作
})
2. 异步并发数限制
js
/**
* 关键点
* 1. new promise 一经创建,立即执行
* 2. 使用 Promise.resolve().then 可以把任务加到微任务队列,防止立即执行迭代方法
* 3. 微任务处理过程中,产生的新的微任务,会在同一事件循环内,追加到微任务队列里
* 4. 使用 race 在某个任务完成时,继续添加任务,保持任务按照最大并发数进行执行
* 5. 任务完成后,需要从 doingTasks 中移出
*/
function limit(count, array, iterateFunc) {
const tasks = []
const doingTasks = []
let i = 0
const enqueue = () => {
if (i === array.length) {
return Promise.resolve()
}
const task = Promise.resolve().then(() => iterateFunc(array[i++]))
tasks.push(task)
const doing = task.then(() => doingTasks.splice(doingTasks.indexOf(doing), 1))
doingTasks.push(doing)
const res = doingTasks.length >= count ? Promise.race(doingTasks) : Promise.resolve()
return res.then(enqueue)
};
return enqueue().then(() => Promise.all(tasks))
}
// test
const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i))
limit(2, [1000, 1000, 1000, 1000], timeout).then((res) => {
console.log(res)
})
3. 异步串行 | 异步并行
js
// 实现一个异步加法
function asyncAdd(a, b, callback) {
setTimeout(function () {
callback(null, a + b);
}, 500);
}
// 解决方案
// 1. promisify
const promiseAdd = (a, b) => new Promise((resolve, reject) => {
asyncAdd(a, b, (err, res) => {
if (err) {
reject(err)
} else {
resolve(res)
}
})
})
// 2. 串行处理
async function serialSum(...args) {
return args.reduce((task, now) => task.then(res => promiseAdd(res, now)), Promise.resolve(0))
}
// 3. 并行处理
async function parallelSum(...args) {
if (args.length === 1) return args[0]
const tasks = []
for (let i = 0; i < args.length; i += 2) {
tasks.push(promiseAdd(args[i], args[i + 1] || 0))
}
const results = await Promise.all(tasks)
return parallelSum(...results)
}
// 测试
(async () => {
console.log('Running...');
const res1 = await serialSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
console.log(res1)
const res2 = await parallelSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
console.log(res2)
console.log('Done');
})()
4.实现有并行限制的 Promise 调度器
js
实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有两个
addTask(1000,"1");
addTask(500,"2");
addTask(300,"3");
addTask(400,"4");
的输出顺序是:2 3 1 4
整个的完整执行流程:
一开始1、2两个任务开始执行
500ms时,2任务执行完毕,输出2,任务3开始执行
800ms时,3任务执行完毕,输出3,任务4开始执行
1000ms时,1任务执行完毕,输出1,此时只剩下4任务在执行
1200ms时,4任务执行完毕,输出4
js
class Scheduler {
constructor(limit) {
this.queue = [];
this.maxCount = limit;
this.runCounts = 0;
}
add(time, order) {
const promiseCreator = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(order);
resolve();
}, time);
});
};
this.queue.push(promiseCreator);
}
taskStart() {
for (let i = 0; i < this.maxCount; i++) {
this.request();
}
}
request() {
if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {
return;
}
this.runCounts++;
this.queue
.shift()()
.then(() => {
this.runCounts--;
this.request();
});
}
}
const scheduler = new Scheduler(2);
const addTask = (time, order) => {
scheduler.add(time, order);
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
scheduler.taskStart();
5.图片懒加载
js
// <img src="default.png" src="https://xxxx/real.png">
function isVisible(el) {
const position = el.getBoundingClientRect()
const windowHeight = document.documentElement.clientHeight
// 顶部边缘可见
const topVisible = position.top > 0 && position.top < windowHeight;
// 底部边缘可见
const bottomVisible = position.bottom < windowHeight && position.bottom > 0;
return topVisible || bottomVisible;
}
function imageLazyLoad() {
const images = document.querySelectorAll('img')
for (let img of images) {
const realSrc = img.dataset.src
if (!realSrc) continue
if (isVisible(img)) {
img.src = realSrc
img.dataset.src = ''
}
}
}
// 测试
window.addEventListener('load', imageLazyLoad)
window.addEventListener('scroll', imageLazyLoad)
// or
window.addEventListener('scroll', throttle(imageLazyLoad, 1000))
6.版本号排序的方法
js
// 题目描述:有一组版本号如下 ['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']。现在需要对其进行排序,排序的结果为 ['4.3.5','4.3.4.5','2.3.3','0.302.1','0.1.1']
arr.sort((a, b) => {
let i = 0;
const arr1 = a.split(".");
const arr2 = b.split(".");
while (true) {
const s1 = arr1[i];
const s2 = arr2[i];
i++;
if (s1 === undefined || s2 === undefined) {
return arr2.length - arr1.length;
}
if (s1 === s2) continue;
return s2 - s1;
}
});
console.log(arr);
7.请实现 DOM2JSON 一个函数,可以把一个 DOM 节点输出 JSON 的格式
js
<div>
<span>
<a></a>
</span>
<span>
<a></a>
<a></a>
</span>
</div>
把上面dom结构转成下面的JSON格式
{
tag: 'DIV',
children: [
{
tag: 'SPAN',
children: [
{ tag: 'A', children: [] }
]
},
{
tag: 'SPAN',
children: [
{ tag: 'A', children: [] },
{ tag: 'A', children: [] }
]
}
]
}
js
// 代码实现
function dom2Json(domtree) {
let obj = {};
obj.name = domtree.tagName;
obj.children = [];
domtree.childNodes.forEach((child) => obj.children.push(dom2Json(child)));
return obj;
}
8.分片思想解决大数据量渲染问题
js
let ul = document.getElementById("container");
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total / once;
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal, curIndex) {
if (curTotal <= 0) {
return false;
}
//每页多少条
let pageCount = Math.min(curTotal, once);
window.requestAnimationFrame(function () {
for (let i = 0; i < pageCount; i++) {
let li = document.createElement("li");
li.innerText = curIndex + i + " : " + ~~(Math.random() * total);
ul.appendChild(li);
}
loop(curTotal - pageCount, curIndex + pageCount);
});
}
loop(total, index);
扩展思考 :对于大数据量的简单 dom 结构渲染可以用分片思想解决 如果是复杂的 dom 结构渲染如何处理?这时候就需要使用虚拟列表了,虚拟列表和虚拟表格在日常项目使用还是很多的
9.查找数组公共前缀
js
示例 1:
输入:strs = ["flower","flow","flight"]
输出:"fl"
示例 2:
输入:strs = ["dog","racecar","car"]
输出:""
js
const longestCommonPrefix = function (strs) {
const str = strs[0];
let index = 0;
while (index < str.length) {
const strCur = str.slice(0, index + 1);
for (let i = 0; i < strs.length; i++) {
if (!strs[i] || !strs[i].startsWith(strCur)) {
return str.slice(0, index);
}
}
index++;
}
return str;
};
10.判断括号字符串是否有效
js
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
js
const isValid = function (s) {
if (s.length % 2 === 1) {
return false;
}
const regObj = {
"{": "}",
"(": ")",
"[": "]",
};
let stack = [];
for (let i = 0; i < s.length; i++) {
if (s[i] === "{" || s[i] === "(" || s[i] === "[") {
stack.push(s[i]);
} else {
const cur = stack.pop();
if (s[i] !== regObj[cur]) {
return false;
}
}
}
if (stack.length) {
return false;
}
return true;
};
11.实现一个padStart()或padEnd()的polyfil
js
String.prototype.padStart 和 String.prototype.padEnd是ES8中新增的方法,允许将空字符串或其他字符串添加到原始字符串的开头或结尾
js
String.prototype.myPadStart = function (targetLen, padString = " ") {
if (!targetLen) {
throw new Error('请输入需要填充到的长度');
}
let originStr = String(this); // 获取到调用的字符串, 因为this原本是String{},所以需要用String转为字符串
let originLen = originStr.length; // 调用的字符串原本的长度
if (originLen >= targetLen) return originStr; // 若是 原本 > 目标 则返回原本字符串
let diffNum = targetLen - originLen; // 10 - 6 // 差值
for (let i = 0; i < diffNum; i++) { // 要添加几个成员
for (let j = 0; j < padString.length; j++) { // 输入的padString的长度可能不为1
if (originStr.length === targetLen) break; // 判断每一次添加之后是否到了目标长度
originStr = `${padString[j]}${originStr}`;
}
if (originStr.length === targetLen) break;
}
return originStr;
}
console.log('xxx'.myPadStart(16))
console.log('xxx'.padStart(16))
12.将虚拟 Dom 转化为真实 Dom
js
// 真正的渲染函数
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);
});
}
// 子数组进行递归操作
vnode.children.forEach((child) => dom.appendChild(_render(child)));
return dom;
}
13.实现一个双向绑定
js
// defineProperty 版本
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 数据劫持
Object.defineProperty(data, 'text', {
// 数据变化 --> 修改视图
set(newVal) {
input.value = newVal;
span.innerHTML = newVal;
}
});
// 视图更改 --> 数据变化
input.addEventListener('keyup', function(e) {
data.text = e.target.value;
});
js
// proxy 版本
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 数据劫持
const handler = {
set(target, key, value) {
target[key] = value;
// 数据变化 --> 修改视图
input.value = value;
span.innerHTML = value;
return value;
}
};
const proxy = new Proxy(data, handler);
// 视图更改 --> 数据变化
input.addEventListener('keyup', function(e) {
proxy.text = e.target.value;
});
14. 实现Vue reactive响应式
js
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
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)
15.实现redux中间件
js
// 简单版本
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());
js
// redux 的迷你版本
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
},{})
}
16.实现redux-thunk
redux-thunk 可以利用 redux 中间件让 redux 支持异步的 action
js
// 如果 action 是个函数,就调用这个函数
// 如果 action 不是函数,就传给下一个中间件
// 发现 action 是函数就调用
const thunk = ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
export default thunk