# 手写圣杯布局

  • 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]