学了点技术,我要开始装X了(三)

简介: 接上文。

第九式:浏览器也会摸鱼 🐟 ?


浏览器一帧都会干些什么?


我们都知道,页面的内容都是一帧一帧绘制出来的,浏览器刷新率代表浏览器一秒绘制多少帧。原则上说 1s 内绘制的帧数也多,画面表现就也细腻。


电影放映的标准是每秒放映 24 帧,也就是说电影每秒放映 24 幅画面,以达到动画的效果,超过 24 帧/s 连续的变化,视觉暂留就会将静态的画“动”起来。研究表明,人眼承受的极限为每秒 55 帧,还有研究表明,每秒 60 帧以上可以明显提升观众的观影感受。每秒 120 帧是每秒 24 帧的 5 倍,采用这样的拍摄技术可以让画面更加栩栩如生,让观众仿佛置身其中,给人一种似真似幻的感觉。


目前浏览器大多是 60Hz(60 帧/s),每一帧耗时也就是在 16.6ms 左右。那么在这一帧的(16.6ms) 过程中浏览器又干了些什么呢?


66e3cce3d76110bcbce5a9d13c0d3606.png


通过上面这张图可以清楚的知道,浏览器一帧会经过下面这几个过程:


  1. 接受输入事件,处理用户的交互,如点击、触碰、滚动等事件


  1. 执行事件回调


  1. 开始一帧


  1. 执行 RAF (RequestAnimationFrame)


  1. 页面布局,样式计算


  1. 绘制渲染


  1. 执行 RIC (RequestIdelCallback)


第七步的 RIC 事件不是每一帧结束都会执行,只有在一帧的 16.6ms 中做完了前面 6 件事儿且还有剩余时间,才会执行。如果一帧执行结束后还有时间执行 RIC 事件,那么下一帧需要在事件执行结束才能继续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时。


requestIdleCallback 的启示


我们以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器有剩余时间时通知我们。


requestIdleCallback((deadline) => {
  // deadline 有两个参数
  // timeRemaining(): 当前帧还剩下多少时间,最大值50ms
  // didTimeout: 是否超时
  // 另外 requestIdleCallback 后如果跟上第二个参数 {timeout: ...} 则会强制浏览器在当前帧执行完后执行。
  if (deadline.timeRemaining() > 0) {
    // TODO
  } else {
    requestIdleCallback(otherTasks);
  }
});
// 用法示例
let tasksNum = 10000;
requestIdleCallback(unImportWork);
function unImportWork(deadline) {
  while (deadline.timeRemaining() && tasksNum > 0) {
    console.log(`执行了 ${10000 - tasksNum + 1}个任务`);
    tasksNum--;
  }
  if (tasksNum > 0) {
    // 在未来的帧中继续执行
    requestIdleCallback(unImportWork);
  }
}


其实部分浏览器已经实现了这个 API,这就是 requestIdleCallback。但是由于以下因素,Facebook 在 React 的重构升级中, 抛弃了 requestIdleCallback 的原生 API,而实现了功能更完备的 requestIdleCallbackpolyfill,这就是Scheduler。除了在空闲时触发回调的功能外,Scheduler 还提供了多种调度优先级供任务设置:


  • 浏览器兼容性;


  • 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换 tab 后,之前 tab 注册的 requestIdleCallback 触发的频率会变得很低。


requestIdleCallback 的 callback 会在浏览器的空闲时间运行,而在w3c 文档[23]里,空闲时间分两种:


  • 在执行一段连续的动画时,将给定帧提交到屏幕与开始处理下一帧之间的时间,这段时间内属于空闲时间。在连续动画和屏幕更新期间,此类空闲时间会频繁发生,但通常会非常短(即,如果我们的屏幕是 60hz(1s 内屏幕刷新 60 次)的设备,小于 16 毫秒),如下图所示。


41fb153c75c66d216bf2d79808de9dbf.png


  • 另外一种空闲时间,当用户属于空闲状态(没有与网页进行任何交互),并且屏幕中也没有动画执行。此时空闲时间理论上是无限长的。但为了避免在不可预测的任务(例如处理用户输入)引起用户可察觉的延迟,这些空闲时间段的长度应限制为最大值 50ms,一旦空闲期结束,浏览器可以安排另一个空闲期。


c6b1ba78c8041474ba070a903f8bff30.png


也就是说,即使浏览器一直处于空闲状态的话,deadline.timeRemaining可以得到的最长时间,也是 50ms,这是 w3c 标准[24] 规定的。一些低优先级的任务可使用 requestIdleCallback 等浏览器不忙的时候来执行,同时因为时间有限,它所执行的任务应该尽量是能够量化,细分的微小任务。


50 ms 的最大截止时间来自一个 RESPONSETIME研究,该研究表明,对 100 毫秒内用户输入的响应通常被人类感知为瞬时的。将空闲期限限制为 50 ms 意味着即使用户输入在空闲任务开始后立即发生,用户代理仍有剩余的 50 ms 时间来响应用户输入,而不会产生用户可察觉的延迟。


当设备的性能越来越好,浏览器支持的效果越来越炫,浏览器的开发者开始越来越多的考虑使用原生 API 来处理一些之前特别占用性能的功能,自从最初的 requestAnimationFrameInsterSectionObserver,到requestIdleCallback,对于前端的将来,充满希望,没错,我们都会有“光明的未来”,哈哈 😄,关于浏览器的更多细节,可以参考我之前的两篇文章:


  • 浏览器是如何工作的:Chrome V8 让你更懂 JavaScript[25]


  • 47 张图带你走进浏览器的世界[26]


拓展阅读与参考


  • requestIdleCallback-后台任务调度[27]


  • 浏览器帧原理剖析[28]


  • Accurately measuring layout on the web[29]


  • Cooperative Scheduling of Background Tasks[30]


第十式:自制hash生成器


项目中,也许我们会遇到需要使用 JS 生成特定长度随机字符串的需求,比如用来做 Hash 值、uuid、随机码等,除了可以借助一些库和插件之外,其实部分场景下,我们完全可以自定义函数实现指定长度随机字符串的生成。


简洁版函数只需要两行代码:


/**
 * 生成长度为len的包含a-z、A-Z、0-9的随机字符串
 */
function generateStr(len = 18) {
  // 一行代码生成0-9、A-Z、a-z、总长度为62的字符数组
  var arr = [...new Array(62)].map((item, i) =>
    String.fromCharCode(i + (i < 10 ? 0 : i < 36 ? 7 : 13) + 48)
  );
  return [...new Array(len)]
    .map(() => arr[Math.floor(Math.random() * arr.length)])
    .join('');
}
generateStr(18);


如果担心重复,则可以添加一个Map来缓存已经生成的字符串,每次返回时判断一下:


/**
 * 生成长度为len的包含a-z、A-Z、0-9的随机字符串
 */
const cacheMap = new Map(); // 缓存已经生成过了的字符串
// 一行代码生成0-9、A-Z、a-z、总长度为62的字符数组
const arr = [...new Array(62)].map((item, i) =>
  String.fromCharCode(i + (i < 10 ? 0 : i < 36 ? 7 : 13) + 48)
);
function generateStr(len = 18) {
  const str = [...new Array(len)]
    .map(() => arr[Math.floor(Math.random() * arr.length)])
    .join('');
  if (cacheMap.has(str)) {
    // 这里会有死循环的问题,比如下面的for循环,i设置的大于62
    console.log(cacheMap, str);
    // i 值越大,len越小,重复的概率越大
    return generateStr(len);
  } else {
    cacheMap.set(str, true);
    return str;
  }
}
for (let i = 0; i < 20; i++) {
  // 长度选小一点,测试20次
  // i设置的大于62会出现死循环,可先算出排列组合数进行预防
  // i 值越大,len越小,重复的概率越大,执行时间越长
  generateStr(1);
}
console.log(cacheMap);


1 行代码生成指定长度数字:这种方法有缺点,低概率会出现位数不足的问题(原因是 0.00566 * 100000 = 566,会丢失前面的 0),不推荐使用。


// len 最多16,可能出现
function generateNum(len = 16) {
  return Math.floor(Math.random() * Math.pow(10, len));
}


  • 2 行代码生成包含大小写字母和数字的随机字符串[31]


第十一式:如何在离开页面时发送请求?


用户卸载网页的时候(关闭浏览器、刷新浏览器或者跳转其他页面时),有时需要向服务器发送一些统计数据;同时,前端在做异常监控、统计页面访问时长时,也会需要在页面崩溃、关闭的时候发送请求。很自然的做法是在 unload 事件或 beforeunload 事件的监听函数里面,使用 XMLHttpRequest 对象发送数据。但是,这样做不是很可靠,因为 XMLHttpRequest 对象是异步发送,很可能在它即将发送的时候,页面和相关资源已经卸载,会引起 function not found 的错误,从而导致发送取消或者发送失败


解决方法就是 AJAX 通信改成同步发送,即只有发送完成,页面才能卸载。但是,很多浏览器已经不支持同步的 XMLHttpRequest 对象了(即 open()方法的第三个参数为 false):


window.addEventListener('unload', logData, false);
function logData() {
  var client = new XMLHttpRequest();
  // 第三个参数表示同步发送
  client.open('POST', '/log', false);
  client.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8');
  client.send(analyticsData);
}


同步通信有几种变通的方法:


  • 一种做法是新建一个<img>元素,数据放在 src 属性,作为 URL 的查询字符串,这时浏览器会等待图片加载完成(服务器回应),再进行卸载。


  • 另一种做法是创建一个循环,规定执行时间为几秒钟,在这几秒钟内把数据发出去,然后再卸载页面。


通过在 unload 事件处理器中,创建一个图片元素并设置它的 src 属性的方法来延迟卸载以保证数据的发送。因为绝大多数浏览器会延迟卸载以保证图片的载入,所以数据可以在卸载事件中发送


const reportData = (url, data) => {
  let img = document.createElement('img');
  const params = [];
  Object.keys(data).forEach((key) => {
    params.push(`${key}=${encodeURIComponent(data[key])}`);
  });
  img.onload = () => (img = null);
  img.src = `${url}?${params.join('&')}`;
};


这些做法的共同问题是,卸载的时间被硬生生拖长了,后面页面的加载被推迟了,用户体验不好。


Navigator.sendBeacon 就是天生用来解决“网页离开时的请求发送”问题的,该方法可用于通过 HTTP 将少量数据异步传输到 Web 服务器。可以发现,同样是采用异步的方式,但是Navigator.sendBeacon发出的异步请求是作为浏览器任务执行的,与当前页面是脱钩的。因此该方法不会阻塞页面卸载流程和延迟后面页面的加载。当用户代理成功把数据加入浏览器传输队列时,sendBeacon() 方法将会返回 true,如果受到队列总数、数据大小的限制后,会返回 false。返回true后,只是表示进入了发送队列,浏览器会尽力保证发送成功,但是否成功了,无法判断。


目前 Google Analytics 使用 Navigator.sendBeacon 来上报数据。

Navigator.sendBeacon方法接受两个参数,第一个参数是目标服务器的 URL,第二个参数是所要发送的数据(可选),可以是任意类型(字符串、表单对象、二进制对象等等)。这个方法的返回值是一个布尔值,成功发送数据为 true,否则为 false。该方法发送数据的 HTTP 方法是 POST,可以跨域,类似于表单提交数据。它不能指定回调函数。


window.addEventListener('unload', analytics, false);
function analytics(state) {
  if (!navigator.sendBeacon) return;
  var URL = 'http://example.com/analytics';
  var data = 'state=' + state + '&location=' + window.location;
  navigator.sendBeacon(URL, data);
}


sendBeacon方法具有如下特点:


  • 发出的是异步请求,并且是 POST 请求,后端解析参数时,需要注意处理方式;


  • 发出的请求,是放到的浏览器任务队列执行的,脱离了当前页面,所以不会阻塞当前页面的卸载和后面页面的加载过程,用户体验较好;


  • 只能判断出是否放入浏览器任务队列,不能判断是否发送成功;


  • Beacon API不提供相应的回调,因此后端返回最好省略response body


参考资料


  • Google Analytics added sendBeacon functionality to Universal Analytics JavaScript API[32]


  • Navigator.sendBeacon 无阻塞发送统计数据[33]


  • Navigator.sendBeacon() —— MDN[34]


第十二式:让 VSCode、浏览器和你心有灵犀


在刚接手比较大型项目时,也许你会经常碰到这样的问题:需要修改一个页面,却苦于对项目结构不熟悉、文件夹结构不规范等,不知道文件在哪个目录下;要改一个 bug,却难以迅速定位到问题所在文件,这时候你是否幻想过,如果可以点击页面上的组件,在 VSCode 中自动跳转到对应文件,并定位到对应行号岂不美哉?


react-dev-inspector [35]就是满足你这些幻想的梦中女神,这个插件允许用户通过简单的点击直接从浏览器 React 组件跳转到本地 IDE 代码。TA 不仅能满足你的幻想,使用起来也是非常简单方便,看完这张动图,懂得都懂 😜:


ba36d115f5bd07fff7255f1bc7a0f43a.jpg


如果看完图,你还不放心的话,不妨现在就先在在线预览体验[36]地址体验一下(在线体验地址里,激活点击跳转的快捷键是四个按键的组合,不过你完全不用担心,因为这个组合是可以自定义的,你完全可以改成两个按键的组合)。


前面说了,使用方式非常简单,只需三步:


  • 首先,保证你的命令行本身可以通过 code 命令打开 VSCode 编辑器,比如code .,用 VSCode 打开当前文件夹下的文件;如果没有配置这个,可以参考以下步骤:


  • 首先打开 VSCode。


  • 使用 command + shift + p (注意 window 下使用 ctrl + shift + p) 然后搜索 code,选择 install 'code' command in path


  • 安装react-dev-inspector,修改babelrc.jswebpack.config.ts文件:


// babelrc.js
export default {
  plugins: [
    // plugin options docs see:
    // https://github.com/zthxxx/react-dev-inspector#inspector-babel-plugin-options
    'react-dev-inspector/plugins/babel',
  ],
};
// webpack.config.ts
import type { Configuration } from 'webpack';
import { launchEditorMiddleware } from 'react-dev-inspector/plugins/webpack';
const config: Configuration = {
  /**
   * [server side] webpack dev server side middleware for launch IDE app
   */
  devServer: {
    before: (app) => {
      app.use(launchEditorMiddleware);
    },
  },
};


  • 对项目入口文件进行以下修改:


import React from 'react';
import { Inspector, InspectParams } from 'react-dev-inspector';
const InspectorWrapper =
  process.env.NODE_ENV === 'development' ? Inspector : React.Fragment;
export const Layout = () => {
  // ...
  return (
    <InspectorWrapper
      // props docs see:
      // https://github.com/zthxxx/react-dev-inspector#inspector-component-props
      // 这里可以随便配置快捷键,你可以改成两个按键的组合
      keys={['control', 'shift', 'command', 'c']}
      disableLaunchEditor={false}
      onHoverElement={(params: InspectParams) => {}}
      onClickElement={(params: InspectParams) => {}}
    >
      {/*这里是你原来的入口组件jsx*/}
      <YourComponent>...</YourComponent>
    </InspectorWrapper>
  );
};


当然,这个插件目前也支持在Vite2create-react-appUmi中使用,接入也都很简单,可以参考react-dev-inspector GitHub 仓库及使用[37]文档。


这个插件的原理,简单说也分为三步:


  • 构建时:添加一个 webpack loader遍历编译前的 AST 节点,在 DOM 节点上加上文件路径、名称等相关的信息。使用 DefinePlugin 注入项目运行时的根路径,以便后续用来拼接文件路径,打开 VSCode 相应的文件。


  • 运行时:需要在项目的最外层包裹 Inspector 组件,用于在浏览器端监听快捷键,弹出 debug 的遮罩层,在点击遮罩层的时候,利用 fetch 向本机服务发送一个打开 VSCode 的请求。


  • 本地服务:需要启动 react-dev-utils 里的一个中间件,监听一个特定的路径,在本机服务端执行打开 VSCode 的指令,如code src/pages/index.ts


如果你对其中的原理很感兴趣,可以参考字节跳动 Web Infra 团队的文章——开发提效——我点了页面上的元素,VSCode 乖乖打开了对应的组件?原理揭秘[38]


相关文章
|
6月前
1003 我要通过
1003 我要通过
38 0
|
6月前
|
人工智能 物联网 大数据
还记得当初自己为什么选择计算机?
还记得当初自己为什么选择计算机?
49 0
|
3月前
|
算法 安全
关于我用半个月过了软件设计师这件事
这篇文章分享了作者在半个月内通过软件设计师考试的经验,包括快速刷视频了解知识体系、针对性地刷历年真题并根据错题加强知识点巩固、进行模拟考试和总结,以及使用笔记软件记录重要知识点的方法。
关于我用半个月过了软件设计师这件事
|
3月前
|
安全 Linux Windows
网工小白,一定要焊死在电脑上的6款工具!
网工小白,一定要焊死在电脑上的6款工具!
|
存储 编解码 数据处理
还在为搞不懂笔记本电脑参数而苦恼么?一篇文章就够啦
还在为搞不懂笔记本电脑参数而苦恼么?一篇文章就够啦
297 4
|
数据安全/隐私保护
小白也能重装系统?写给小白的一封信--重装纯净版Win10系统
小白也能重装系统?写给小白的一封信--重装纯净版Win10系统
292 0
|
测试技术 定位技术
1424:我要送人头
题目描述: 废话他呆了,打了那么久DOTA 2 ,没见过这样的局。队友手了炸弹人和小娜迦,这局是要拖到什么时候啊。因此,废话决定要不停地送人头,让对面赶紧结束这场令他煎熬的比赛。 废话现在有一个软件,可以让他的英雄按照输入的矩阵行走。已知DOTA 2的地图是一个正方形,他只要输入单位转置矩阵就可以无限送人头了。
84 0
|
Windows
上才艺——win32搞一个拼图游戏
上才艺——win32搞一个拼图游戏
184 0
上才艺——win32搞一个拼图游戏
|
移动开发 JavaScript 物联网
不装了、摊牌了,我们要搞事情
不装了、摊牌了,我们要搞事情
不装了、摊牌了,我们要搞事情
|
Web App开发 移动开发 前端开发