# 手写圣杯布局
flex 实现
bfc 实现
# 手写 css 实现抽奖圆盘
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.wrapper {
width: 300px;
height: 300px;
margin: 100px;
}
.pointer {
position: absolute;
top: 88px;
left: 243px;
width: 0;
border-top: 30px solid red;
border-left: 15px solid transparent;
border-right: 15px solid transparent;
}
.wheel {
height: 100%;
width: 100%;
border-radius: 50%;
position: relative;
overflow: hidden;
transform: rotate(0deg);
transition: transform 4s ease-out;
}
.part {
position: absolute;
width: 0;
height: 0;
color: #fff;
border-left: 150px solid transparent;
border-right: 0 solid transparent;
border-top: 150px solid transparent;
border-bottom: 150px solid transparent;
border-top-color: pink;
transform-origin: 100% 50%; // 这个决定了旋转的顶点
}
.part1 {
transform: rotate(0deg);
border-top-color: pink;
}
.part2 {
transform: rotate(45deg);
border-top-color: rgb(177, 18, 44);
}
.part3 {
transform: rotate(90deg);
border-top-color: rgb(76, 18, 177);
}
.part4 {
transform: rotate(135deg);
border-top-color: rgb(18, 129, 177);
}
.part5 {
transform: rotate(180deg);
border-top-color: rgb(66, 177, 18);
}
.part6 {
transform: rotate(225deg);
border-top-color: rgb(177, 124, 18);
}
.part7 {
transform: rotate(270deg);
border-top-color: rgb(230, 54, 34);
}
.part8 {
transform: rotate(315deg);
border-top-color: rgb(29, 18, 177);
}
.part-text {
position: absolute;
top: -100px;
left: -76px;
width: 80px;
transform: rotate(65deg);
}
button {
background-color: rgb(99, 141, 209);
cursor: pointer;
margin: 40px 120px;
width: 64px;
height: 32px;
border: none;
color: #fff;
border-radius: 4px;
}
button:hover {
background-color: rgba(99, 141, 209, 0.8);
}
</style>
</head>
<body>
<div class="wrapper">
<div id="wheel" class="wheel">
<div class="part part1">
<div class="part-text">奖品1</div>
</div>
<div class="part part2">
<div class="part-text">奖品2</div>
</div>
<div class="part part3">
<div class="part-text">奖品3</div>
</div>
<div class="part part4">
<div class="part-text">奖品4</div>
</div>
<div class="part part5">
<div class="part-text">奖品5</div>
</div>
<div class="part part6">
<div class="part-text">奖品6</div>
</div>
<div class="part part7">
<div class="part-text">奖品7</div>
</div>
<div class="part part8">
<div class="part-text">奖品8</div>
</div>
</div>
<div class="pointer"></div>
<button id="start">抽奖</button>
</div>
</body>
<script>
const wheel = document.getElementById('wheel');
const button = document.getElementById('start');
button.addEventListener('click', () => {
const oldDeg = wheel.style.transform
? wheel.style.transform.split('(')[1].split('deg')[0]
: 0;
const newDeg = Number(oldDeg) + Math.floor(Math.random() * 360) + 360 * 4;
wheel.style.transform = `rotate(${newDeg}deg)`;
});
</script>
</html>
# 手写 css 实现 3d 立方体(魔方)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.wrapper {
width: 500px;
height: 500px;
margin: 100px auto;
border: 1px solid #ccc;
perspective: 8000px; /** 核心属性 相当于摄像机的远近,设置远一点的话正方体更标准 */
position: relative;
}
.cube {
width: 200px;
height: 200px;
position: absolute;
left: 150px;
top: 150px;
transform: rotateX(25deg) rotateY(25deg);
transform-style: preserve-3d; /** 核心属性 不设置没办法展示 3d 效果 */
}
.box {
position: absolute;
width: 200px;
height: 200px;
border: 1px solid #ccc;
}
/** 前 */
.box1 {
background-color: rgba(202, 68, 34, 0.5);
transform: translateZ(100px);
}
/** 后 */
.box2 {
background-color: rgba(191, 202, 34, 0.5);
transform: translateZ(-100px);
}
/** 上 */
.box3 {
background-color: rgba(34, 118, 202, 0.5);
transform: rotateY(90deg) translateZ(100px);
}
/** 下 */
.box4 {
background-color: rgba(34, 87, 202, 0.5);
transform: rotateY(-90deg) translateZ(100px);
}
/** 左 */
.box5 {
background-color: rgba(202, 34, 202, 0.5);
transform: rotateX(90deg) translateZ(100px);
}
/** 右 */
.box6 {
background-color: rgba(202, 34, 42, 0.5);
transform: rotateX(-90deg) translateZ(100px);
}
</style>
</head>
<body>
<div class="wrapper">
<div class="cube">
<div class="box box1"></div>
<div class="box box2"></div>
<div class="box box3"></div>
<div class="box box4"></div>
<div class="box box5"></div>
<div class="box box6"></div>
</div>
</div>
</body>
</html>
# 树扁平化成数组,根据 parentId 将 tree 转成 array
function treeToArray(tree, parentId = null) {
let result = [];
tree.forEach((item) => {
const { id, children, ...rest } = item;
// 当前节点 先进 数组
result = [...result, { ...rest, id, parentId }];
// 如果有子节点,递归
if (children && children.length > 0) {
result = [...result, ...treeToArray(children, id)];
}
});
return result;
}
// 示例树结构
const tree = [
{
id: 1,
name: 'A',
children: [
{
id: 2,
name: 'B',
children: [{ id: 4, name: 'D', children: [] }],
},
{
id: 3,
name: 'C',
children: [{ id: 5, name: 'E', children: [] }],
},
],
},
{
id: 6,
name: 'F',
children: [],
},
];
// 转换为数组
const array = treeToArray(tree);
console.log(array);
# 扁平数组树形化,根据 parentId 将数组组装成 tree
function arrayToTree(arr) {
let rootItems = [];
let arrMap = {};
arr.forEach((item) => {
arrMap[item.id] = { ...item, children: [] };
});
arr.forEach((item) => {
const treeItem = arrMap[item.id];
// 如果当前item不存在parentId 说明是根节点
if (!item.parentId) {
rootItems.push(treeItem);
} else {
// 如果当前item存在parentId 说明是子节点,那么子节点就要放到其父节点的children里面
if (arrMap[item.parentId]) {
arrMap[item.parentId].children.push(treeItem);
}
}
});
return rootItems;
}
const items = [
{ id: 1, name: 'A', parentId: null },
{ id: 2, name: 'B', parentId: 1 },
{ id: 3, name: 'C', parentId: 1 },
{ id: 4, name: 'D', parentId: 2 },
{ id: 5, name: 'E', parentId: 3 },
{ id: 6, name: 'F', parentId: null },
];
const tree = arrayToTree(items);
console.log(JSON.stringify(tree, null, 2));
# 手写实现一个 2000 毫秒的延时器
function delay(time) {
return new Promise((resolve) =>
setTimeout(() => {
resolve();
}, time),
);
}
# 手写 promise.all
Promise.myPromiseAll = (promises) => {
return new Promise((resolve, reject) => {
const promisesArr = [...promises];
const len = promisesArr.length;
if (len === 0) {
resolve([]);
}
let successCount = 0;
let result = [];
for (let [idx, prom] of promisesArr.entries()) {
Promise.resolve(prom)
.then((res) => {
successCount++;
result[idx] = res;
if (successCount === len) {
resolve(result);
}
})
.catch((e) => reject(e));
}
});
};
Promise.myPromiseAll([1, 2, 3]).then((results) => {
console.log(results);
});
# 手写 promise.map 和 并发控制
const delay = (time) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, time);
});
};
// PromiseMap 的定义:批量执行所有的异步任务
Promise.myMap = (list, asyncFn) => {
return Promise.all(list.map(asyncFn));
};
// 加上并发控制
Promise.myMapWithLimit = function (list, asyncFn, limit) {
// 最终结果
let result = [];
// 当前执行的任务的index
let currRunIdx = 0;
// 当前运行中的线程数量
let running = 0;
return new Promise((resolve, reject) => {
const next = () => {
// 如果当前执行的任务 已经溢出,并且没有任务在运行了;说明已经全部运行完了
if (currRunIdx >= list.length && running === 0) {
resolve(result);
}
// 如果 线程有空余,并且当前运行的任务index没有溢出;那么执行任务
if (running < limit && currRunIdx < list.length) {
const idx = currRunIdx;
running++;
currRunIdx++;
Promise.resolve(asyncFn(list[idx]))
.then((res) => {
// 当前任务运行成功:进行result赋值操作、可执行的线程空了一个出来、递归继续执行新的任务
result[idx] = res;
running--;
next();
})
.catch((e) => reject(e));
}
};
// 启动任务;按线程数量启动
for (let i = 0; i < limit; i++) {
next();
}
});
};
Promise.myMapWithLimit(
[1, 1, 2, 3, 9, 3, 2, 1, 1.9],
(x) =>
Promise.resolve(x).then(async (res) => {
await delay(1000 * res);
console.log(res);
}),
2,
);
# 手写 hooks :带锁机制的 useLockFn
const useLockFn = (callback) => {
const lockRef = useRef(false);
return async (...args) => {
if (lockRef.current) {
// 抛出 locked 正在执行
} else {
lockRef.current = true;
try {
await callback(...args);
} catch (e) {
// 抛出 error
} finally {
lockRef.current = false;
}
}
};
};
# 手写 大数 处理
function bigNumAdd(num1, num2) {
const maxLength = Math.max(num1.length, num2.length);
const num1Arr = num1.split('').reverse();
const num2Arr = num2.split('').reverse();
let result = [];
let step = 0;
for (let i = 0; i < maxLength; i++) {
const tempSum = Number(num1Arr[i] || 0) + Number(num2Arr[i] || 0) + step;
if (tempSum >= 10) {
result[i] = tempSum - 10;
step = 1;
} else {
result[i] = tempSum;
step = 0;
}
}
// !!!!!! 最后还要处理一次进位,这个很重要!不然无法处理 99+99 的场景
if (step > 0) {
result.push(step);
}
return result.reverse().join('');
}
const num1 = '12345678987654';
const num2 = '3456789298765';
const num3 = '99';
const num4 = '99';
console.log(`${num1} + ${num2} = ${bigNumAdd(num1, num2)}`);
console.log(`${num3} + ${num4} = ${bigNumAdd(num3, num4)}`);
# LocalStorage 的监听 storage 事件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button onclick="handleAdd()">点击加1</button>
</body>
<script>
// 通过广播实现
const channel = new BroadcastChannel('localStroageChannel');
channel.addEventListener('message', (e) => {
const { key, newValue, oldValue } = e.data;
console.log('BroadcastChannel: ', key, newValue, oldValue);
});
// 重写 localStoregae.setItem
const originLocalSetItemForChannel = window.localStorage.setItem;
localStorage.setItem = (key, value) => {
const oldValue = localStorage.getItem(key);
// 使用原始的方式赋值
originLocalSetItemForChannel.call(window.localStorage, key, value);
// 派发 广播
channel.postMessage({
key,
newValue: value,
oldValue,
});
};
</script>
<script>
// 重写 localStoregae.setItem
const originLocalSetItem = window.localStorage.setItem;
localStorage.setItem = (key, value) => {
const oldValue = localStorage.getItem(key);
// 使用原始的方式赋值
originLocalSetItem.call(window.localStorage, key, value);
// 派发自定义事件
const event = new Event('localStorageChange');
event.key = key;
event.newValue = value;
event.oldValue = oldValue;
window.dispatchEvent(event);
};
let handleAdd = () => {
window.localStorage.setItem(
'test',
+window.localStorage.getItem('test') + 1,
);
};
window.addEventListener('storage', (e) => {
const { key, newValue, oldValue } = e;
console.log('storage: ', key, newValue, oldValue);
});
window.addEventListener('localStorageChange', (e) => {
const { key, newValue, oldValue } = e;
console.log('localStorageChange', key, newValue, oldValue);
});
// storage 事件可以跨标签的原理
// 当其中一个标签页修改 localstorage 浏览器的渲染进程会将此修改通知给浏览器的主进程
// 浏览器的主进程通过 IPC 进程间通信机制 将这些变化广播给其他同源标签页的渲染进程
// 其他同源标签页的渲染进程会接收到这些变化并触发 storage 事件
</script>
</html>
# react 的 compose 是怎么实现的
// 假设有多个函数 f, g, h,
// 我们可以通过 compose(f, g, h) 来实现:f(g(h(x)))。
// 也就是说,compose 从右到左依次执行每个函数。
function compose(...fns) {
if (fns.length === 0) {
return (args) => args;
}
if (fns.length === 1) {
return fns[0];
}
return function (...args) {
// 使用 reduceRight 从右向左 执行
return fns.reduceRight((preFn, curFn) => {
// curFn(preFn); 这一步很巧妙,上次的结果(初始的时候是传入的参数)是这一次函数的参数
return curFn(preFn);
}, args);
};
}
const addOne = (x) => x + 1;
const multiplyByTwo = (x) => x * 2;
const square = (x) => x * x;
const composedFunction = compose(addOne, multiplyByTwo, square);
// 执行 composedFunction
console.log(composedFunction(5)); // Output: 51
# 手写全排列递归+回溯
function getAllPermutions(str) {
let result = [];
// 递归函数,用来生成排列
function _getAllPermutions(currPermution, otherChars) {
// 如果剩余的字符为空,说明该条递归路径已经走到底了,输出该条排列路径
if (otherChars.length === 0) {
result.push(currPermution);
}
// 遍历剩余字符
for (let i = 0; i < otherChars.length; i++) {
// 新的排列
const newPermution = `${currPermution}${otherChars[i]}`;
// 新的剩余字符数组,删掉当前字符的其他字符
const newOtherChars = otherChars
.split('')
.filter((__, idx) => idx !== i)
.join('');
// 继续递归
_getAllPermutions(newPermution, newOtherChars);
}
}
// 启动
_getAllPermutions('', str);
return result;
}
console.log(getAllPermutions('abcd'));
# 最长无重复子字符串-滑动窗口
function getMaxLengthSubStr(str) {
let arr = [];
let max = 0;
for (let right = 0; right < str.length; right++) {
const curChar = str[right];
const curIndex = arr.indexOf(curChar);
if (curIndex >= 0) {
arr = arr.slice(curIndex + 1);
}
arr.push(curChar);
max = Math.max(max, arr.length);
}
return max;
}
function getMaxLengthSubStr2(str) {
let arrMap = {};
let left = 0;
let max = 0;
for (let right = 0; right < str.length; right++) {
const curChar = str[right];
if (arrMap[curChar] >= 0) {
left = arrMap[curChar] + 1;
}
arrMap[curChar] = right;
max = Math.max(max, right - left + 1);
}
return max;
}
console.log(getMaxLengthSubStr('pwwkew')); // 3
console.log(getMaxLengthSubStr('abcabcbb')); // 3
console.log(getMaxLengthSubStr('bbbbbbbbb')); // 1
console.log(getMaxLengthSubStr2('pwwkew')); // 3
console.log(getMaxLengthSubStr2('abcabcbb')); // 3
console.log(getMaxLengthSubStr2('bbbbbbbbb')); // 1
# 手写从数组 array 中随机取出 n 个元素-洗牌算法
function sampleSize(array, n) {
let result = [...array];
// 这里需要注意洗盘算法需要从数组末尾开始,不然的话,i为0开始的时候,j也为0
// 而且还有一个问题是 正序洗盘的话,已经洗过的元素会被重复选,所以导致不均匀
// 倒序可以保证每个元素都是均匀的被选取
// 洗牌算法进行 arr.length - 1 次有效交换,最后一次 i = 0 是无效交换
for (let i = array.length - 1; i > 0; i--) {
const j = (Math.floor(Math.random() * (i + 1))[(result[i], result[j])] = [
result[j],
result[i],
]);
}
return result.slice(0, n);
}
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(sampleSize(arr, 3)); // 可能输出 [6, 3, 8]