【代码片段】JS

2022/07/21 14:31:44

工具方法

并发限制器

同时执行大量任务会对 cpu 造成压力,进行流操作时会导致所有任务进度缓慢。

class TaskParallelLimiter {
  taskList = []; // 待办事件列表
  activeThreadAmount = 0;
  maxThreadAmount = 5;
  host = null;

  constructor(host = this, limit = 5) {
    this.host = host;
    this.maxThreadAmount = limit;
  }

  // 添加事件
  push(fnName, ...args) {
    let task = fnName.bind(this.host, ...args);
    let _t = this;
    function packingTask() {
      _t.activeThreadAmount++;
      return task().finally(() => {
        _t.activeThreadAmount--;
        nextTask();
      });
    }
    function nextTask() {
      if (_t.activeThreadAmount < _t.maxThreadAmount) {
        let next = _t.taskList.shift();
        if (next) {
          next().finally(() => {
            nextTask();
          });
        }
      }
    }
    if (this.activeThreadAmount < this.maxThreadAmount) {
      packingTask();
    } else {
      this.taskList.push(packingTask);
    }
  }
}

TaskParallelLimiter(host, limit) 构造函数接收两个参数:

  • host:添加任务时会将方法的 this 指向 host
  • limit:最大并发数。

taskLimiter.push() 推入任务队列的方法必须返回一个 Promise 对象。

let taskLimiter = new TaskParallelLimiter(this, 3);

function task() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(1);
      resolve();
    }, 1000);
  });
}

let len = 10;
while (len-- > 0) {
  taskLimiter.push(task);
}

防抖节流

防抖函数

在延迟范围内重复调用只执行最后一次。

  • 一参:待执行函数。
  • 二参:延迟多久后执行。
function antiShake(fn, interval = 300) {
  let timmer;
  return function (...args) {
    clearTimeout(timmer);
    timmer = setTimeout(fn, interval, ...args);
  };
}

window.onresize = antiShake(function () {
  console.log(1);
});

节流函数

在延迟时间后才可再次执行函数。

  • 一参:待执行函数。
  • 二参:延迟多久后可再次执行。
function throttle(fn, interval = 300) {
  let canExecute = false;
  let timmer = null;
  return function (...args) {
    if (!canExecute) {
      fn.apply(null, ...args);
      canExecute = true;
      timmer = setTimeout(() => {
        canExecute = false;
        timmer = null;
      }, interval);
    }
  };
}

window.onresize = throttle(function () {
  console.log(1);
});

ip 排序

// ip排序
function ipsSort(ips) {
  // ips =  ["10.10.15.130", "10.10.16.40", "127.0.0.1", "192.168.1.123", "192.168.1.38", "192.168.1.39"];
  // 升序
  ips.sort(function (a, b) {
    a = a.trim(); // 空格会影响排序
    b = b.trim();
    var arr1 = a.split(".");
    var arr2 = b.split(".");
    for (var i = 0; i < 4; i++) {
      if (arr1[i] > arr2[i]) {
        return 1;
      } else if (arr1[i] < arr2[i]) {
        return -1;
      }
    }
  });
  return ips;
}

获取浏览器信息

// 获取浏览器信息
function getBrowserInfo() {
  var inBrowser = typeof window !== "undefined";
  var inWeex = typeof WXEnvironment !== "undefined" && !!WXEnvironment.platform;
  var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase();
  var UA = inBrowser && window.navigator.userAgent.toLowerCase();
  var isIE = UA && /msie|trident/.test(UA);
  var isIE9 = UA && UA.indexOf("msie 9.0") > 0;
  var isEdge = UA && UA.indexOf("edge/") > 0;
  var isAndroid =
    (UA && UA.indexOf("android") > 0) || weexPlatform === "android";
  var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || weexPlatform === "ios";
  var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge;
  var isPhantomJS = UA && /phantomjs/.test(UA);
  var isFF = UA && UA.match(/firefox\/(\d+)/);
  return {
    inBrowser,
    inWeex,
    weexPlatform,
    UA,
    isIE,
    isIE9,
    isEdge,
    isAndroid,
    isIOS,
    isChrome,
    isPhantomJS,
    isFF,
  };
}

生成范围内随机数

// 生成范围内的随机数
function getRandom(min = 0, max = 255) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

获取图片原始宽高

// 获取图片原始宽高
function loadImageAsync(url) {
  return new Promise(function (resolve, reject) {
    var image = new Image();

    image.onload = function () {
      var obj = {
        w: image.naturalWidth,
        h: image.naturalHeight,
      };
      resolve(obj);
    };

    image.onerror = function () {
      reject(new Error("Could not load image at " + url));
    };
    image.src = url;
  });
}

调度器

**题目描述:**实现一个带并发限制的异步调度器,保证同时运行的任务最多有两个。完善下面代码中的 Scheduler 类,使得程序能正常输出。

class Scheduler {
  add(promiseCreator) {}
  // ...
}
const timeout = (time) =>
  new Promise((resolve) => {
    setTimeout(resolve, time);
  });
const scheduler = new Scheduler();
const addTask = (time, order) => {
  scheduler.add(() => timeout(time)).then(() => console.log(order));
};
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
// 1200ms时,4完成,输出4

代码实现:

class Scheduler {
  constructor(maxNum) {
    this.maxNum = maxNum;
    this.taskList = []; // 待执行任务队列
    this.count = 0; // 当前执行数
  }

  async add(promiseCreator) {
    // 入栈函数
    if (this.count >= this.maxNum) {
      await new Promise((resolve) => {
        this.taskList.push(resolve); // 当resolve被执行时才会接着执行
      });
    }
    this.count++;
    const result = await promiseCreator(); // 执行定时器,即() => timeout(time), 返回值也是一个promise对象
    this.count--;
    if (this.taskList.length) {
      this.taskList.shift()(); // 待执行队列中有值的话取出第一个并执行(这里取出来的是上面的resolve,执行之后该resolve对应的函数就开始执行)
    }
    return result;
  }
}

获取页面性能指标

原文链接:前端性能监控指标open in new window

function getPerformanceTiming() {
  var performance = window.performance;

  if (!performance) {
    // 当前浏览器不支持
    console.log("你的浏览器不支持 performance 接口");
    return;
  }

  var t = performance.timing;
  var times = {};

  //【重要】页面加载完成的时间
  //【原因】这几乎代表了用户等待页面可用的时间
  times.loadPage = t.loadEventEnd - t.navigationStart;

  //【重要】解析 DOM 树结构的时间
  //【原因】反省下你的 DOM 树嵌套是不是太多了!
  times.domReady = t.domComplete - t.responseEnd;

  //【重要】重定向的时间
  //【原因】拒绝重定向!比如,http://example.com/ 就不该写成 http://example.com
  times.redirect = t.redirectEnd - t.redirectStart;

  //【重要】DNS 查询时间
  //【原因】DNS 预加载做了么?页面内是不是使用了太多不同的域名导致域名查询的时间太长?
  // 可使用 HTML5 Prefetch 预查询 DNS ,见:[HTML5 prefetch](http://segmentfault.com/a/1190000000633364)
  times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;

  //【重要】读取页面第一个字节的时间
  //【原因】这可以理解为用户拿到你的资源占用的时间,加异地机房了么,加CDN 处理了么?加带宽了么?加 CPU 运算速度了么?
  // TTFB 即 Time To First Byte 的意思
  // 维基百科:https://en.wikipedia.org/wiki/Time_To_First_Byte
  times.ttfb = t.responseStart - t.navigationStart;

  //【重要】内容加载完成的时间
  //【原因】页面内容经过 gzip 压缩了么,静态资源 css/js 等压缩了么?
  times.request = t.responseEnd - t.requestStart;

  //【重要】执行 onload 回调函数的时间
  //【原因】是否太多不必要的操作都放到 onload 回调函数里执行了,考虑过延迟加载、按需加载的策略么?
  times.loadEvent = t.loadEventEnd - t.loadEventStart;

  // DNS 缓存时间
  times.appcache = t.domainLookupStart - t.fetchStart;

  // 卸载页面的时间
  times.unloadEvent = t.unloadEventEnd - t.unloadEventStart;

  // TCP 建立连接完成握手的时间
  times.connect = t.connectEnd - t.connectStart;

  return times;
}

日期

获取时间日期字符串

function getFullTime() {
  let date = new Date(), //时间戳为10位需*1000,时间戳为13位的话不需乘1000
    Y = date.getFullYear() + "",
    M =
      date.getMonth() + 1 < 10
        ? "0" + (date.getMonth() + 1)
        : date.getMonth() + 1,
    D = date.getDate() < 10 ? "0" + date.getDate() : date.getDate(),
    h = date.getHours() < 10 ? "0" + date.getHours() : date.getHours(),
    m = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes(),
    s = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();
  return `${Y}-${M}-${D} ${h}:${m}:${s}`;
}

数组

数组按指定顺序排序

  • 一参:目标数组。
  • 二参:排序规则,一参中的数组按照这个数组元素顺序排序。
  • 三参:被排序元素相对其他元素的位置,默认放在其他元素后面。
function arrLaggingSort(target, sortList, insertHead = false) {
  if (insertHead) {
    sortList.reverse();
  }
  sortList.forEach((ele) => {
    let index = target.indexOf(ele);
    if (index !== -1) {
      target.splice(index, 1);
      if (insertHead) {
        target.unshift(ele);
      } else {
        target.push(ele);
      }
    }
  });
  return target;
}
  • 示例
let arr = [1, 2, 3, 4, 5, 6];
let sortList = [3, 5];
arrLaggingSort(arr, sortList, true); // [3, 5, 1, 2, 4, 6]
arrLaggingSort(arr, sortList); //[(1, 2, 4, 6, 3, 5)];

多层数组扁平化

reduce 函数不接收二参时,回调函数的一参是数组的第一位,二参是第二位

接收二参时,reduce 回调函数的第一个参数就是输入的参数,二参是数组的第一位

function flattenDeep(arr) {
  return arr.reduce((acc, cur) => {
    const _val = Array.isArray(cur) ? flattenDeep(cur) : [cur];
    return [...acc, ..._val];
  }, []);
}
flattenDeep([[1, [3, 2]], { name: 234 }, 3, [2, 3, 4, [342, [2]], 999], 0]);

字符串

将字符串按照数字分隔

function strListGenerator(str) {
  let reg = /\d+/g;
  let matchList = str.match(reg);
  let result = [];
  if (matchList) {
    matchList.forEach((numberStr, index) => {
      if (index > 0) {
        result.pop();
      }
      let arr = str.split(numberStr);
      result.push(
        createStrNode(arr[0]),
        createStrNode(numberStr, true),
        createStrNode(arr[1])
      );
      str = arr[1];
    });
  } else {
    result = [createStrNode(str)];
  }
  return result;
}

let str =
  "您的订单已派送,请保持您的电话畅通。查询配送进sss度,可拨打:10010。";
strListGenerator(str); // [{string: '...', isNumber: false},{string: '10010', isNumber: true}]

前置零补至指定位数

/*
 *功能: {在字符串前以0补全指定位数}
 *输入: {number}    (原数据,指定位数)
 *输出: {string}	"04"
 */
function supNumber(num, x) {
  if ((num + "").length < x) {
    return "0" + num;
  } else {
    return num;
  }
}

交换字符串中的两个单词

var re = /(\w+)\s(\w+)/;
var str = "John Smith";
var newstr = str.replace(re, "$2, $1");
// Smith, John

千分位

千分位(三位一分):2,123,456,789

// 1.千分位(三位一分):2,123,456,789
// 思路:只要字符串长度大于三就裁剪后三位并更新字符串,通过累加实现
function toThousands(num) {
  var num = (num || 0).toString(),
    result = "";
  // 将传入数据转换为字符串
  while (num.length > 3) {
    // 只要字符串长度大于三就执行
    // -----------------------------------------------------------------------
    result = "," + num.slice(-3) + result; // ',' + 后三位值 + 之前累加的值
    // -----------------------------------------------------------------------
    // 将字符串后三位裁剪并加入最终的字符串
    num = num.slice(0, num.length - 3);
    // 更新字符串长度
  }
  if (num) {
    result = num + result;
  }
  return result;
}
function setCookie(cname, cvalue, exdays) {
  var d = new Date();
  d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
  var expires = "expires=" + d.toGMTString();
  document.cookie = cname + "=" + cvalue + "; " + expires;
}
function getCookie(cname) {
  var name = cname + "=";
  var ca = document.cookie.split(";");
  for (var i = 0; i < ca.length; i++) {
    var c = ca[i].trim();
    if (c.indexOf(name) == 0) return c.substring(name.length, c.length);
  }
  return "";
}
function checkCookie() {
  var username = getCookie("username");
  if (username != "") {
    alert("Welcome again " + username);
  } else {
    username = prompt("Please enter your name:", "");
    if (username != "" && username != null) {
      setCookie("username", username, 365);
    }
  }
}

DOM

获取元素相对于页面的位置

元素相对于页面的位置 = 元素的相对位置与它所有父元素的相对位置的累加之和。

function getOffsetByDom(elem) {
  var left = elem.offsetLeft,
    top = elem.offsetTop;
  while ((elem = elem.offsetParent)) {
    left += elem.offsetLeft;
    top += elem.offsetTop;
  }
  return {
    left,
    top,
  };
}

数据

blob 转 base64

/** blob转base64 */
function blobToBase64(blob) {
  if (blob instanceof Blob) {
    // 判断是否为blob类型
    return new Promise((resolve, reject) => {
      const fileReader = new FileReader();
      fileReader.onload = (e) => {
        resolve(e.target.result);
      };
      fileReader.readAsDataURL(blob);
      fileReader.onerror = () => {
        reject(new Error("blobToBase64 error"));
      };
    });
  }
}

业务功能

录制屏幕

思路

  1. 用 navigator.mediaDevices.getDisplayMedia 获取到录制权限后得到 MediaStream。
  2. 用 new MediaRecorder(MediaStream) 创建一个媒体录制的容器。
  3. 使用 MediaRecorder 的 API 完成录制后保存数据(blob 格式)。
  4. 用 URL.createObjectURL(blob) 创建 blob 链接,然后利用 a 标签实现下载。

代码实现

var mediaRecorder;
var blobData;
function getMediaAuth() {
  // 获取录制权限
  navigator.mediaDevices
    .getDisplayMedia()
    .then((res) => {
      var options = { mimeType: "video/webm; codecs=vp9" };
      mediaRecorder = new MediaRecorder(res, options);
      startRecord();
      eventListenerStopRecord();
    })
    .catch((err) => {
      console.log(err);
    });
}
function eventListenerStopRecord() {
  // 录制停止时保存录制的blob数据
  mediaRecorder.ondataavailable = function (e) {
    blobData = e.data;
  };
}
function startRecord() {
  // 开始录制
  mediaRecorder.start();
}
function stopRecord() {
  // 停止录制
  mediaRecorder.stop();
}
function downLoad() {
  // 下载文件
  var url = URL.createObjectURL(blobData);
  var a = document.createElement("a");
  document.body.appendChild(a);
  a.style = "display: none";
  a.href = url;
  a.download = "screen-record.webm";
  a.click();
  window.URL.revokeObjectURL(url);
}

获取客户端 IP 地址

前端获取

前端无法获取用户 IP,但是能通过后端接口实现,这里借助搜狐的 api 获取 ip 信息。

async function getIp() {
  return new Promise((resolve, reject) => {
    let script = document.createElement("script");
    script.src = "http://pv.sohu.com/cityjson?ie=utf-8";
    script.onload = function () {
      resolve(returnCitySN);
      document.body.removeChild(script);
    };
    script.onerror = function (err) {
      reject(err);
    };
    document.body.appendChild(script);
  });
}

getIp().then((res) => {
  console.log(res);
});

NodeJS 获取

const os = require("os");
function getIPAdress() {
  var interfaces = os.networkInterfaces();
  for (var devName in interfaces) {
    var iface = interfaces[devName];
    for (var i = 0; i < iface.length; i++) {
      var alias = iface[i];
      if (
        alias.family === "IPv4" &&
        alias.address !== "127.0.0.1" &&
        !alias.internal
      ) {
        return alias.address;
      }
    }
  }
}

App 分享链接

  • URL,分享网址
  • TITLE,标题
  • ORIGIN,分享 @ 相关 twitter 账号
  • SOURCE,来源(QQ 空间会用到)
  • DESCRIPTION,描述
  • IMAGE,图片
  • SUMMARY,摘要
var templates = {
  qzone:
    "http://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey?url={{URL}}&title={{TITLE}}&desc={{DESCRIPTION}}&summary={{SUMMARY}}&site={{SOURCE}}&pics={{IMAGE}}",
  qq: 'http://connect.qq.com/widget/shareqq/index.html?url={{URL}}&title={{TITLE}}&source={{SOURCE}}&desc={{DESCRIPTION}}&pics={{IMAGE}}&summary="{{SUMMARY}}"',
  weibo:
    "https://service.weibo.com/share/share.php?url={{URL}}&title={{TITLE}}&pic={{IMAGE}}&appkey={{WEIBOKEY}}",
  wechat: "javascript:",
  douban:
    "http://shuo.douban.com/!service/share?href={{URL}}&name={{TITLE}}&text={{DESCRIPTION}}&image={{IMAGE}}&starid=0&aid=0&style=11",
  linkedin:
    "http://www.linkedin.com/shareArticle?mini=true&ro=true&title={{TITLE}}&url={{URL}}&summary={{SUMMARY}}&source={{SOURCE}}&armin=armin",
  facebook: "https://www.facebook.com/sharer/sharer.php?u={{URL}}",
  twitter:
    "https://twitter.com/intent/tweet?text={{TITLE}}&url={{URL}}&via={{ORIGIN}}",
  google: "https://plus.google.com/share?url={{URL}}",
};

剪贴板

复制内容到剪贴板

function setClipboarData(data) {
  let oInput = document.createElement("input");
  oInput.value = data;
  document.body.appendChild(oInput);
  oInput.select();
  document.execCommand("Copy");
  oInput.style.display = "none";
  document.body.removeChild(oInput);
}

获取剪贴板内容

/** 获取剪贴板内容 */
function getClipboard() {
  document.addEventListener(
    "paste",
    async (e) => {
      const cbd = e.clipboardData;
      const ua = window.navigator.userAgent;
      // 如果是 Safari 直接 return
      if (!(e.clipboardData && e.clipboardData.items)) {
        return;
      }
      // Mac平台下Chrome49版本以下 复制Finder中的文件的Bug Hack掉
      if (
        cbd.items &&
        cbd.items.length === 2 &&
        cbd.items[0].kind === "string" &&
        cbd.items[1].kind === "file" &&
        cbd.types &&
        cbd.types.length === 2 &&
        cbd.types[0] === "text/plain" &&
        cbd.types[1] === "Files" &&
        ua.match(/Macintosh/i) &&
        Number(ua.match(/Chrome\/(\d{2})/i)[1]) < 49
      ) {
        return;
      }
      let blob: File;
      for (let i = 0; i < cbd.items.length; i++) {
        const item = cbd.items[i];
        if (item.kind === "file") {
          blob = item.getAsFile();
          if (blob) {
            this.sendMessage(blob);
          }
          if (blob.size === 0) {
            return;
          }
        }
      }
      return blob;
    },
    false
  );
}

跳转到微信

  • 跳转到微信首页
<a href="weixin://">跳转到微信</a>
  • 打开微信公众号页面

注意该链接只能在微信内置浏览器中打开

需要将链接中的 __biz 值替换为对应公众号的 uin_base64,该值可在微信公众号首页(登录后)在控制台打印 wx.data.uin_base64 得到。

<a
  href="https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=MzI4ODEyNTE2Nw==#wechat_redirect"
  >打开微信公众号页面</a
>
  • 外部浏览器跳转至微信公众号

可以通过微信小程序提供的 URL Scheme 先跳转至小程序(URL Scheme 文档open in new window),再在小程序中引导关注公众号。