0%

学习笔记 2020 11 20

学习笔记 2020-11-20

JavaScript 高级程序设计(第4版) 阅读记录

事件

模拟事件

DOM 事件模拟

createEvent( eventType: string ) => event: object

  1. 模拟键盘事件

    • eventType: “MouseEvent”
    • event.initMouseEvent()
  2. 模拟键盘事件

    • eventType: “KeyboardEvent”
    • event.initKeyboardEvent()
    • event createEvent(“Events”)
    • event.initEvent()
  3. 模拟其他事件

    • eventType: “HTMLEvents”
    • event.initEvent()
  4. 自定义 DOM 事件

    • eventType: “CustomEvents”

    • event.initCustomEvent()

IE 事件模拟
  • createEventObject() => event: object
  • fireEvent( name:string, event: object)

动画与 Canvas 图形

使用 requestAnimationFrame

该方法让浏览器通过最优方式确定重绘的时序。

早期定时动画

一般计算机显示器的屏幕刷新率都是 60Hz,意味着每秒需要重绘 60 次。实现平滑动画最佳的重绘间隔为 1000 毫秒 / 60,大约 17 毫秒。

时间间隔的问题

知道何时绘制下一帧是创造平滑动画的关键。浏览器自身计时器存在精度问题。

requestAnimationFrame

接收一个参数,一个 DOMHighResTimeStamp 的实例。表示下次重绘的时间。

cancelAnimationFrame
通过 requestAnimationFrame 节流

调用该方法会将回调函数推入一个重绘之前调用的钩子队列中,可用于节流。

基本的画布功能

<canvas id="drawing" width="200" height="200">
  A drawing of something
</canvas>
let drawing = document.getElementById('drawing');
// 确保浏览器支持<canvas>
if (drawing.getContext) {
  let context = drawing.getContext('2d');
  // 其他代码
}

可以通过 toDataURL() 方法导出 <canvas> 元素上的图像。接收参数,要生成图像的 MIME 类型。

2D 绘图上下文

2D 上下文的坐标原点位于画布左上角。

填充和描边
  • fillStyle
  • strokeStyle

这两个属性可以是字符串、渐变对象或图案对象。

现代 JavaScript 教程

装饰器模式和转发,call/apply

任务

  1. 间谍装饰器

    创建一个装饰器 spy(func),它应该返回一个包装器,该包装器将所有对函数的调用保存在其 calls 属性中。

    每个调用都保存为一个参数数组。

    例如:

    function work(a, b) {
      alert( a + b ); // work 是一个任意的函数或方法
    }
    
    work = spy(work);
    
    work(1, 2); // 3
    work(4, 5); // 9
    
    for (let args of work.calls) {
      alert( 'call:' + args.join() ); // "call:1,2", "call:4,5"
    }

    P.S. 该装饰器有时对于单元测试很有用。它的高级形式是 Sinon.JS 库中的 sinon.spy

    function spy(func) {
      function it(...args) {
        it.calls.push(args);
        return func.apply(this, args);
      }
      it.calls = [];
      return it;
    }
  2. 延时装饰器

    创建一个装饰器 delay(f, ms),该装饰器将 f 的每次调用延时 ms 毫秒。

    例如:

    function f(x) {
      alert(x);
    }
    
    // create wrappers
    let f1000 = delay(f, 1000);
    let f1500 = delay(f, 1500);
    
    f1000("test"); // 在 1000ms 后显示 "test"
    f1500("test"); // 在 1500ms 后显示 "test"

    换句话说,delay(f, ms) 返回的是延迟 ms 后的 f 的变体。

    在上面的代码中,f 是单个参数的函数,但是你的解决方案应该传递所有参数和上下文 this

    function delay(func, time) {
     return function (...args) {
       setTimeout(() => {
         func.apply(this, args);
       }, time);
     };
    }
  3. 防抖装饰器

    debounce(f, ms) 装饰器的结果是一个包装器,该包装器将暂停对 f 的调用,直到经过 ms 毫秒的非活动状态(没有函数调用,“冷却期”),然后使用最新的参数调用 f 一次。

    换句话说,debounce 就像一个“接听电话”的秘书,并一直等到 ms 毫秒的安静时间之后,才将最新的呼叫信息传达给“老板”(调用实际的 f)。

    举个例子,我们有一个函数 f,并将其替换为 f = debounce(f, 1000)

    然后,如果包装函数非别在 0ms、200ms 和 500ms 时被调用了,之后没有其他调用,那么实际的 f 只会在 1500ms 时被调用一次。也就是说:从最后一次调用开始经过 1000ms 的冷却期之后。

    ……并且,它将获得最后一个调用的所有参数,其他调用的参数将被忽略。

    以下是其实现代码(使用了 Lodash library 中的防抖装饰器 ):

    let f = _.debounce(alert, 1000);
    
    f("a");
    setTimeout( () => f("b"), 200);
    setTimeout( () => f("c"), 500);
    // 防抖函数从最后一次函数调用以后等待 1000ms,然后执行:alert("c")

    现在我们举一个实际中的例子。假设用户输入了一些内容,我们想要在用户输入完成时向服务器发送一个请求。

    我们没有必要为每一个字符的输入都发送请求。相反,我们想要等一段时间,然后处理整个结果。

    在 Web 浏览器中,我们可以设置一个事件处理程序 —— 一个在每次输入内容发生改动时都会调用的函数。通常,监听所有按键输入的事件的处理程序会被调用的非常频繁。但如果我们为这个处理程序做一个 1000ms 的 debounce 处理,它仅会在最后一次输入后的 1000ms 后被调用一次。

    在这个实时演示的示例中,处理程序将结果显示在了下面的方框中,试试看:

    看到了吗?第二个输入框调用了防抖函数,所以它的内容是在最后一次输入的 1000ms 后被处理的。

    因此,debounce 是一个处理一系列事件的好方法:无论是系列键盘输入,鼠标移动还是其他类似的事件。

    它在最后一次调用之后等待给定的时间,然后运行其可以处理结果的函数。

    任务是实现一个 debounce 装饰器。

    提示:如果你好好想想,实现它只需要几行代码 :)

    function debounce(func, time) {
      let timeId = null;
      return function (...args) {
        if (timeId) {
          clearTimeout(timeId);
          timeId = null;
        }
        timeId = setTimeout(() => {
          func.apply(this, args);
        }, time);
      };
    }
  4. 节流装饰器

    创建一个“节流”装饰器 throttle(f, ms) —— 返回一个包装器。

    当被多次调用时,它会在每 ms 毫秒最多将调用传递给 f 一次。

    与去抖的不同是,它是个完全不同的装饰器:

    • debounce 会在“冷却(cooldown)”期后运行函数一次。适用于处理最终结果。
    • throttle 运行函数的频率不会大于所给定的时间 ms 毫秒。适用于不应该经常进行的定期更新。

    换句话说,throttle 就像接电话的秘书,但是打扰老板(实际调用 f)的频率不能超过每 ms 毫秒一次。

    让我们看看现实生活中的应用程序,以便更好地理解这个需求,并了解它的来源。

    例如,我们想要跟踪鼠标移动。

    在浏览器中,我们可以设置一个函数,使其在每次鼠标移动时运行,并获取鼠标移动时的指针位置。在使用鼠标的过程中,此函数通常会执行地非常频繁,大概每秒 100 次(每 10 毫秒)。

    我们想要在鼠标指针移动时,更新网页上的某些信息。

    ……但是更新函数 update() 太重了,无法在每个微小移动上都执行。高于每 100ms 更新一次的更新频次也没有意义。

    因此,我们将其包装到装饰器中:使用 throttle(update, 100) 作为在每次鼠标移动时运行的函数,而不是原始的 update()。装饰器会被频繁地调用,但是最多每 100ms 将调用转发给 update() 一次。

    在视觉上,它看起来像这样:

    1. 对于第一个鼠标移动,装饰的变体立即将调用传递给 update。这很重要,用户会立即看到我们对其动作的反应。
    2. 然后,随着鼠标移动,直到 100ms 没有任何反应。装饰的变体忽略了调用。
    3. 100ms 结束时 —— 最后一个坐标又发生了一次 update
    4. 然后,最后,鼠标停在某处。装饰的变体会等到 100ms 到期,然后用最后一个坐标运行一次 update。因此,非常重要的是,处理最终的鼠标坐标。

    一个代码示例:

    function f(a) {
      console.log(a);
    }
    
    // f1000 最多每 1000ms 将调用传递给 f 一次
    let f1000 = throttle(f, 1000);
    
    f1000(1); // 显示 1
    f1000(2); // (节流,尚未到 1000ms)
    f1000(3); // (节流,尚未到 1000ms)
    
    // 当 1000ms 时间到...
    // ...输出 3,中间值 2 被忽略

    P.S. 参数(arguments)和传递给 f1000 的上下文 this 应该被传递给原始的 f

    function throttle(func, time) {
      let flag = false,
        savedArgs = null;
    
      function wrapper() {
        if (flag) {
          savedArgs = arguments;
          return;
        }
        func.apply(this, arguments);
        flag = true;
        setTimeout(() => {
          flag = false;
          if (savedArgs) {
            wrapper.apply(this, savedArgs);
            savedArgs = null;
          }
        }, time);
      }
      return wrapper;
    }