LaySent's Site

Dropped Frame Count in Video🔗

• JavaScript

如果在哔哩哔哩播放视频的过程中,出现了比较明显的卡顿,播放器的左下角会出现一个 Toast,提示“丢帧严重,已自动关闭防挡弹幕”。那么,播放器是如何检测出当前的丢帧是否严重的呢?

通过阅读哔哩哔哩播放器的部分源码,不难发现核心实现如下:

i.prototype.autoVisible = function() {
  var i = this;
  i.danmakuMask.maskIsShow() && (i.dropFramesTimer = setTimeout((() => {
    // 默认检测间隔是 600ms
    i.dropFramesTime = 600;
    // 当前总丢失帧数
    var e = i.droppedFrameCount();
    // i.dropFrames 是上次检查时总丢失帧数
    // 丢失超过 14 帧就认为丢帧严重
    if (e - i.dropFrames > 14)
      return i.player.toast.addTopHinter("丢帧严重,已自动关闭防挡弹幕", 2e3),
      i.maskDanmaku.value(!1),
      void i.danmakuMask.setting("dmask", !1);
    // 更新总丢失帧数
    i.dropFrames = e,
    i.autoVisible()
  }
  ), i.dropFramesTime))
}
,
i.prototype.droppedFrameCount = function() {
  return this.player.video && this.player.video.webkitDroppedFrameCount || 0
}

简单来说,代码判断了 600 毫秒内,是否有超过 14 帧丢失,如果有的话,就认为丢帧严重,需要做一些计算的降级。而这里判断丢帧数量的核心代码,是获取了 video 上的 webkitDroppedFrameCount 属性。因为是 webkit 开头,可以知道这个是一个非标准的 API。

通过查询 MDN 相关文档,不难发现,在正式的标准(草稿阶段)中,对应的应该是 VideoPlaybackQuality 中的 droppedVideoFrames 属性。

要获取当前 video 的丢失情况,可以使用下面的代码:

const quality = video.getVideoPlaybackQuality();
const droppedVideoFrames = quality.droppedVideoFrames;

这里 droppedVideoFrames 和 webkitDroppedFrameCount 是一致的。

如果想知道丢帧的百分比,可以配合 totalVideoFrames 一起判断:

const quality = video.getVideoPlaybackQuality();
const percent = quality.droppedVideoFrames / quality.totalVideoFrames;

注:视频解码前后都有可能发生丢帧。只要浏览器判断认为当前帧已经不可能在正确的时间被播放,就会触发丢帧。


Digit Display🔗

• CSS

在计时展示的时候,经常会遇到这样的问题:因为不同数字实际的“宽度”是不同的,因此在数字跳动的时候,整体的宽度会时常变化,无法对齐。

一个常见的方案,是将数字的字体设置成等宽字体(monospace):

.digits {
  font-family: monospace;
}

但是这样的方案会导致数字部分的字体和其他地方的字体出现出入,视觉上可能出现不统一的情况。

CSS 提供了一些属性来微调字体展示,从而让数字展示的时候不出现宽度变化:

  1. 第一个方案是使用 font-variant-numeric 属性,并设置值为 tabular-nums。根据 MDN 介绍,tabular-nums 会将字体设置成“等宽”的样子(占用的空间是等宽的,但是每个字本身并不一定是等宽的)。对应到 OpenType 中就是 tnum,相关的介绍可以参考这里;
  2. 第二个方案是使用 font-feature-settings 属性,并设置值为 tnum。根据 MDN 介绍,这是一个更底层的属性。效果其实和 font-variant-numeric 是一样的,且因为本身底层,并不建议直接使用。

需要注意的一点是,这一属性仅针对数字生效,对于小数点或是英文数字,并不生效。下面是一些实际的效果:

  • 3.14 <= 正常数字的展示效果
  • 3.14 <= tabular-nums 展示效果
  • 2.72 <= 同上,数字部分可以保持对齐
  • 1234 <= 对小数点无效,并没有对齐
  • wave <= 对英文字母无效,并没有对齐

Check Initial Commit🔗

• Git

在阅读开源项目代码的时候,一个常见的操作是,通过查看最开始的 commits 来了解整个项目/框架最初的设计理念。对一个发展了一段时间的大型项目来说,想要直接通过 git log 找到最初的 commits 还是比较花时间的。一个取巧的办法是通过 --reverse 参数来倒序排列所有的 commits,从而快速定位最开始的那一些。同样,可以通过 -n 参数来进一步过滤展示的条数,比如通过 -n 1 来仅展示一条 commit。

最终的命令如下:

git log --reverse -n 1

Google vs Trick Tool🔗

• Open Source Code

当用户试图比较两样东西的时候,经常会在 Google 中输入 xxx vs xxx 来进行搜索。而 Google 的搜索关键词自动填充功能可以很好得展示用户常见的输入内容。因此,当已知一个关键词 A,就很容易在 Google 中通过 A vs 这样的关键词去触发自动填充功能,从而找到和 A 近似的其他关键词 B,C,D。

比如,查找和 Webpack 相关的关键词:

desmos-graph

既然可以通过这个方法知道 A 相近的关键词 B,那么通过同样的方法,递归一下,就可以知道 B 的关键词 C。如此往复,就可以生成一张相近关键词的无向图(之所以是“无向图”是因为默认 vs 是满足交换律的)。

这里提供了一个可视化的工具,只要输入一个关键词,就可以递归的调用 Google 的接口,最终生成一个无向图。工具的源代码在这里。其中,具体查询 Google Suggestion API 的代码在这里可以找到。

关于这个 Google Suggestion API 的用法介绍(包括 Google vs 的技巧介绍),可以参考另外一篇文章。


Multiple Object Destructuring Assignment🔗

• JavaScript

在 JavaScript 中,可以通过 destructuring assignment 来对对象内的属性进行赋值。比如:

const a = { b: 1 };
const { b: c } = a;
console.log(c); // => 1

有意思的是,同一个属性可以被多次使用,比如:

const obj = { inner: { value: 1 } };
const { inner: wholeObj, inner: { value } } = obj;

console.log(wholeObj); // => { value: 1 }
console.log(value); // => value

甚至,将一个属性赋值给多个变量也是可行的:

const obj = { key: 'value' };
const { key: key1, key: key2 } = obj;

console.log(key1); // => 'value'
console.log(key2); // => 'value'

Git Stash Files🔗

• Git

一般情况下,使用 git stash 命令只会将当前工作区内的改动缓存起来。对于新生成的文件,默认是不会缓存的。然而在切分支的时候,难免会遇到这样的情况:当前分支的新文件在另一个分支下是已经提交的内容。此时如果切分支,可能会因为冲突而失败。如果希望将新文件也全部都缓存起来,可以两步走:

git add -A
git stash

以上两部可以合并成一步完成:git stash -u 或者 git stash --include-untracked。

如果希望连那些被 git ignore 的文件也一起缓存起来,可以使用 git stash -a 或者 git stash --all 命令。当然,一般不建议这样做。比如在 JavaScript 项目中,如果使用 git stash -a 命令,很有可能会将整个 node_modules 目录都缓存起来。


.bind multiple times🔗

• JavaScript

在 JavaScript 注册事件回调的时候,经常会用到 .bind 方法绑定 this 的指向。(关于 .bind 方法的使用,可以参考 MDN)。

这里有一个关于 .bind 使用的细节:根据 ECMAScript 规范的描述,.bind 只能修改 this 指向一次。

具体的规范如下:

NOTE 2: If Target is an arrow function or a bound function exotic object then the thisArg passed to this method will not be used by subsequent calls to F.

这里规范明确了两个事情:

  1. .bind 函数对已经 bind 过的函数是无效的;
  2. .bind 函数对箭头函数是无效的。

可以简单通过下面的代码验证这个行为:

function test(...args) {
  console.log(this.name, ...args);
}

const first = test.bind({ name: 'first' }, 1);
const second = first.bind({ name: 'second' }, 2);

first();  // output: "first 1"
second(); // output: "first 1 2"

可见,.bind 函数可以继续增加预置的函数参数,但是对 this 的指向无法继续改变了。

另外,在规范里还提到了一个细节:经过 .bind 之后的函数不再有 prototype 了。具体的规范如下:

NOTE 1: Function objects created using Function.prototype.bind are exotic objects. They also do not have a “prototype” property.

验证代码:

function Animal() { }
Animal.prototype.say = function () { console.log(this.name); }

const Cat = Animal.bind({ name: 'cat' });
console.log(Cat.prototype); // output: undefined

Mnemonic Device of Array.sort🔗

• JavaScript

在 JavaScript 中进行数组的排序,需要写一个比较函数,通过函数返回的结果来判断元素的顺序:如果返回的是负数,那么第一个参数在前;如果返回的是正数,那么第一个参数在后;如果返回 0,则表示两个值相等。(MDN 文档见这里)

如果觉得上面提到的这种行为不容易记忆的话,可以考虑下面的这种“直观”的写法:

const list = [8, 6, 7, 5, 3, 0, 9, 2, 4, 1];

// Ascending: 最终的数组排序结果类似从 a 排序到 z
list.sort((a, z) => a - z);

// Descending: 最终的数组排序结果类似从 z 排序到 a
list.sort((a, z) => z - a);

通过 a - z 这样的“形式”来表达从 a 排序到 z(即从小到大排序);z - a 来表达从 z 排序到 a(即从大到小排序)。最终的效果非常直观。Credit: David K. 🎹

其他类似的“直观”写法,比如说:

let i = 3;
while (i --> 0) {
  console.log(i);
}

上述表达式中,通过 --> 来创建一个类似“趋向于”的表达方式(当然,其实常见的写法应该是 i-- > 0)。

这样的写法可以在语法允许的范围内创造一些更“直观”的表达方式,至于好坏,可能见仁见智。


Latest Props in React Fiber🔗

• React

ReactDOM 在创建 DOM 元素的时候,会将 Fiber 信息也存储到 DOM 节点上(参考之前的文章)。但是需要注意的一点是,从 Fiber 上拿到的 Props 不一定是最新的数据。特别是对于使用 React Hooks 的情况来说,需要额外小心。举例来说:

const App = () => {
  const btn = React.useRef(null);
  const [value, setValue] = React.useState("");
  function onChange(event) {
    setValue(event.target.value);
  }
  function onClick() {
    if (!btn.current) return;
    const fiberKey = Object.keys(btn.current).filter((key) =>
      key.startsWith("__reactInternalInstance")
    )[0];
    if (!fiberKey) return;
    const fiber = btn.current[fiberKey];
    console.log(fiber.memoizedProps.onClick.valueInContext);
  }
  onClick.valueInContext = value;
  return (
    <form>
      <input onChange={onChange} value={value}>
      <button
        ref={btn}
        type="submit"
        onClick={onClick}
      >
        输入内容后点我
      </button>
    </form>
  );
};

在输入框输入一段文字之后点击按钮,可以看到最终在 console 输出的内容并不是完整的输入内容。比如,输入 12345 但是在 console 只输出了 1234。通过阅读 React 中对于事件处理的代码可以发现,在获取当前监听事件的时候(这里的 getListener 函数),使用了 getFiberCurrentPropsFromNode 这个函数,而这个函数的定义方法在这里。可以看到,使用的是另一个 DOM 节点上的字段:'__reactEventHandlers$' + randomKey。

因此,上面的代码只需要稍作调整,就可以正常工作了:

const App = () => {
  const btn = React.useRef(null);
  const [value, setValue] = React.useState("");
  function onChange(event) {
    setValue(event.target.value);
  }
  function onClick() {
    if (!btn.current) return;
    const propsKey = Object.keys(btn.current).filter((key) =>
      // ** 主要改动点 **
      key.startsWith("__reactEventHandlers")
    )[0];
    if (!propsKey) return;
    const props = btn.current[propsKey];
    console.log(props.onClick.valueInContext);
  }
  onClick.valueInContext = value;
  return (
    <form>
      <input onChange={onChange} value={value}>
      <button
        ref={btn}
        type="submit"
        onClick={onClick}
      >
        输入内容后点我
      </button>
    </form>
  );
};

对于 __reactEventHandlers 字段的更新,可以参考 ReactDOM 给 Reconciler 配置的两个 API:createInstance 和 commitUpdate(代码在这里)。

当然,对于 Class Component 来说,因为方法一般都是固定的成员方法,因此 __reactEventHandlers 和 __reactInternalInstance 记录下的内容没有什么差别。

另外需要注意的一点是,在 React 最新版本中,__reactInternalInstance$xxx 改名叫 __reactFiber$xxx;而 __reactEventHandlers$xxx 改名叫 __reactEvents$xxx。


isFrozen🔗

• JavaScript

在 JavaScript 中,可以通过 Object.freeze API 将一个对象“冻结”起来。操作之后,对象就无法被修改,添加或删除属性。

如果需要判断一个对象是否被执行过 Object.freeze 操作,最简单的方法,就是使用 Object.isFrozen API:

const obj = { key: 'value' };
const frozen = Object.freeze(obj);

console.log(Object.isFrozen(obj));    // => false;
console.log(Object.isFrozen(frozen)); // => true;

具体来说,Object.isFrozen API 主要是针对对象做了以下几点判断:

  1. 对象是否是不可扩展的;
  2. 对象的所有属性是否都是不可配置的(configurable = false);
  3. 对象的数据属性(没有 getter/setter 访问器属性)都是不可写的(writable = false)。

除了 Object.freeze 后的对象 isFrozen 一定为 true 外,根据上面的规则还可以构造出一些特殊的情况也返回 true:

const empty = { };
Object.preventExtensions(empty);
// 满足第一条;且因为没有其他属性,因此满足第二,第三条
console.log(Object.isFrozen(empty)); // => true

const obj = { key: 'value' };
// 满足第一条
Object.preventExtensions(obj);
// 满足第二条 & 第三条
Object.defineProperty(obj, 'key', { configurable: false, writable: false });
console.log(Object.isFrozen(obj)); // => true

const accessor = { get key() { return 'value' } };
// 满足第一条
Object.preventExtensions(accessor);
// 满足第二条
Object.defineProperty(accessor, 'key', { configurable: false });
// 因为没有数据属性,因此满足第三条
console.log(Object.isFrozen(accessor)); // => true

与 Object.freeze 类似,当 Object.isFrozen API 传入值类型/null/undefined 的时候,在 ES5 和 ES2015 环境下的行为略有不同:在 ES5 下,API 会直接报错(同 Object.freeze);在 ES2015 中则会直接返回 true。