0%

学习笔记 2020 11 10

学习笔记 2020-11-10

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

DOM

MutationObserver 接口

基本用法
  1. 回调与 MutationRecord

    每个回调都会收到一个 MutationRecord 实例的数组。包含发生了什么变化,DOM 的哪一部分受到了影响。因为回调执行之前可能同时发生多个满足观察条件的事件,所以每次执行回调都会传入一个包含按顺序入队的 MutationRecord 实例的数组。

    let observer = new MutationObserver( 
     (mutationRecords) => console.log(mutationRecords));
    observer.observe(document.body, { attributes: true }); 
    document.body.setAttribute('foo', 'bar');
    // [MutationRecord]:
    //  [
    //   0: {
    //     MutationRecord,
    //     addedNodes: NodeList [],
    //     attributeName: "foo",
    //     attributeNamespace: null,
    //     nextSibling: null,
    //     oldValue: null,
    //     previousSibling: null,
    //     removedNodes: NodeList [],
    //     target: body,
    //     type: "attributes",
    //     __proto__: MutationRecord,
    //     length: 1
    //   },
    //   __proto__: Array(0),
    //  ]
    
    let observer = new MutationObserver( 
     (mutationRecords) => console.log(mutationRecords)); 
    observer.observe(document.body, { attributes: true }); 
    document.body.setAttributeNS('baz', 'foo', 'bar'); 
    // [ 
    //   { 
    //     addedNodes: NodeList [], 
    //     attributeName: "foo", 
    //     attributeNamespace: "baz", 
    //     nextSibling: null, 
    //     oldValue: null, 
    //     previousSibling: null 
    //     removedNodes: NodeList [], 
    //     target: body 
    //     type: "attributes" 
    //   } 
    // ]
    
    let observer = new MutationObserver(mutationRecords => console.log(mutationRecords));
    observer.observe(document.body, { attributes: true });
    document.body.className = 'foo';
    document.body.className = 'bar';
    document.body.className = 'baz';
    // [MutationRecord, MutationRecord, MutationRecord]

    MutationRecord 实例的属性如下表:

    属性 说明
    target 被修改影响的目标节点
    type 字符串,表示变化的类型:”attributes”、”characterData”或”childList”
    oldValue 如果在 MutationObserverInit 对象中启用(attributeOldValue 或 characterData OldValue 为 true),”attributes”或”characterData”的变化事件会设置这个属性为被替代的值 “childList”类型的变化始终将这个属性设置为 null。
    attributeName 对于”attributes”类型的变化,这里保存被修改属性的名字。其他变化事件会将这个属性设置为 null
    attributeNamespace 对于使用了命名空间的”attributes”类型的变化,这里保存被修改属性的名字。对于使用了命名空间的”attributes”类型的变化,这里保存被修改属性的名字
    addedNodes 对于”childList”类型的变化,返回包含变化中添加节点的 NodeList。默认为空 NodeList。
    removedNodes 对于”childList”类型的变化,返回包含变化中删除节点的 NodeList。默认为空 NodeList。
    previousSibling 对于”childList”类型的变化,返回变化节点的前一个同胞 Node。默认为 null。
    nextSibling 对于”childList”类型的变化,返回变化节点的后一个同胞 Node。默认为 null。

    传给回调函数的第二个参数是观察变化的 MutationObserver 的实例。

    let observer = new MutationObserver( 
     (mutationRecords, mutationObserver) => console.log(mutationRecords,
    mutationObserver)); 
    observer.observe(document.body, { attributes: true }); 
    document.body.className = 'foo'; 
    // [MutationRecord], MutationObserver
  2. disconnect() 方法

    默认情况下,只要被观察的元素不被垃圾回收,回调就不会被终止。调用 disconnect() 方法提前终止执行回调。

    let observer = new MutationObserver(() => console.log('<body> attributes changed'));
    observer.observe(document.body, { attributes: true });
    document.body.className = 'foo';
    observer.disconnect();
    document.body.className = 'bar';
    //(没有日志输出)

    调用后,已经加入任务队列的回调也会被抛弃。

    如果想执行已经入列的回调,可以使用 setTimeout() 。

    let observer = new MutationObserver(() => console.log('<body> attributes changed'));
    observer.observe(document.body, { attributes: true });
    document.body.className = 'foo';
    setTimeout(() => {
      observer.disconnect();
      document.body.className = 'bar';
    }, 0);
    // <body> attributes changed
  3. 复用 MutationObserver

    多次调用 observe() 方法,可以复用一个 MutationObserver 对象观察多个不同的目标节点。此时, MutationRecord 的 target 属性可以标识发生变化事件的目标节点。

    let observer = new MutationObserver(mutationRecords =>
      console.log(mutationRecords.map(x => x.target))
    );
    // 向页面主体添加两个子节点
    let childA = document.createElement('div'),
      childB = document.createElement('span');
    document.body.appendChild(childA);
    document.body.appendChild(childB);
    // 观察两个子节点
    observer.observe(childA, { attributes: true });
    observer.observe(childB, { attributes: true });
    // 修改两个子节点的属性
    childA.setAttribute('foo', 'bar');
    childB.setAttribute('foo', 'bar');
    // [<div>, <span>]

    此时调用 disconnect() 方法会停止观察所有目标。

  4. 重用 MutationObserver

    调用过 disconnect() 的 MutationObserver 还可以重新使用,只需要关联到新的目标节点。

MutationObserverInit 与观察范围

MutationObserverInit 对象用于控制对目标节点的观察范围。

属性 说明
subtree 布尔值,表示除了目标节点,是否观察目标节点的子树(后代)。如果是 false,则只观察目标节点的变化;如果是 true,则观察目标节点及其整个子树。默认为 false。
attributes 布尔值,表示是否观察目标节点的属性变化。默认为 false
attributeFilter 字符串数组,表示要观察哪些属性的变化。把这个值设置为 true 也会将 attributes 的值转换为 true。默认为观察所有属性
attributeOldValue 布尔值,表示 MutationRecord 是否记录变化之前的属性值。把这个值设置为 true 也会将 attributes 的值转换为 true。默认为 false。
characterData 布尔值,表示修改字符数据是否触发变化事件。默认为 false。
characterDataOldValue 布尔值,表示 MutationRecord 是否记录变化之前的字符数据。把这个值设置为 true 也会将 characterData 的值转换为 true。默认为 false。
childList 布尔值,表示修改目标节点的子节点是否触发变化事件。默认为 false。

默认情况下,观察范围限定为一个元素及其子节点的变化。设置 subtree 可以将观察范围拓展到这个元素的子树,即所有后代节点。被观察子树的节点被移出子树之后仍然能够触发变化事件。

异步回调与记录队列

MutationObserver 接口出于性能考虑而涉及,核心是异步回调与记录队列模型。为了在大量变化事件发生时不影响性能,每次变化的信息会保存在 MutationRecord 实例中,然后添加到记录队列。这个队列对每个 MutationObserver 实例都是唯一的,是所有 DOM 变化事件的有序列表。

  1. 记录队列

    每次 MutationRecord 被添加到 MutationObserver 的记录队列时,仅当之前没有已排期的微任务回调时,才会将观察者注册的回调作为微任务调度到任务队列上。

    不过在回调的微任务异步执行期间,有可能又会发生更多变化事件。因此被调用的回调会接收到一个 MutationRecord 实例的数组,顺序为它们进入记录队列的顺序。回调要负责处理这个数组的每一个实例,因为函数退出之后这些实现就不存在了。回调执行后,这些 MutationRecord 就用不着了。因此记录队列会被清空。

  2. takeRecords() 方法

    调用 MutationObserver 实例的 takeRecords() 方法可以清空记录队列,取出并返回其中的所有 MutationRecord 实例。

性能、内存与垃圾回收

将变化回调委托给微任务来执行可以保证事件同步触发,同时避免随之而来的混乱。为 MutationObserver 而实现的记录队列,可以保证即使变化事件被爆发式地触发,也不会显著地拖慢浏览器。

  1. MutationObserver 的引用

    MutationObserver 实例与目标节点之间的引用关系是非对称的。MutationObserver 拥有对要观察的目标节点的弱引用。因为是弱引用,所以不会妨碍垃圾回收程序回收目标节点。

    然而,目标节点却拥有对 MutationObserver 的强引用。如果目标节点从 DOM 中被移除,随后被垃圾回收,则关联的 MutationObserver 也会被垃圾回收。

  2. MutationRecord 的引用

    记录队列中的每个 MutationRecord 的实例至少包含对已有 DOM 节点的一个引用。如果变化是 childList 类型,则会包含多个节点的引用。记录队列和回调处理的默认行为是耗尽这个队列,处理每个 MutationRecord ,然后让它们超出作用域并被垃圾回收。

    有时候可能需要保存某个观察者的完整变化记录。保存这些 MutationRecord 实例,也就会保存它们引用的节点,因而会妨碍这些节点被回收。如果需要尽快地释放内存,建议从每个 MutationRecord 中抽取出最有用的信息,然后保存到一个新对象中,最后抛弃 MutationRecord。

现代 JavaScript 教程

任务

  1. 创建日期

    创建一个 Date 对象,日期是:Feb 20, 2012, 3:12am。时区是当地时区。

    使用 alert 显示结果。

    let time = new Date('2012-02-20 3:12');
    console.log(time);
  2. 显示星期数

    编写一个函数 getWeekDay(date) 以短格式来显示一个日期的星期数:‘MO’,‘TU’,‘WE’,‘TH’,‘FR’,‘SA’,‘SU’。

    例如:

    let date = new Date(2012, 0, 3);  // 3 Jan 2012
    alert( getWeekDay(date) );        // 应该输出 "TU"
    function getWeekDay(date) {
      let day = date.getDay();
      switch (day) {
        case 1:
          return 'MO';
        case 2:
          return 'TU';
        case 3:
          return 'WE';
        case 4:
          return 'TH';
        case 5:
          return 'FR';
        case 6:
          return 'SA';
        case 0:
          return 'SU';
      }
    }
  3. 欧洲的星期表示方法

    欧洲国家的星期计算是从星期一(数字 1)开始的,然后是星期二(数字 2),直到星期日(数字 7)。编写一个函数 getLocalDay(date),并返回日期的欧洲式星期数。

    let date = new Date(2012, 0, 3);  // 3 Jan 2012
    alert( getLocalDay(date) );       // 星期二,应该显示 2
    function getLocalDay(date) {
      let day = date.getDay();
      return day === 0 ? 7 : day;
    }
  4. 许多天之前是哪个月几号?

    写一个函数 getDateAgo(date, days),返回特定日期 date 往前 days 天是哪个月的哪一天。

    例如,假设今天是 20 号,那么 getDateAgo(new Date(), 1) 的结果应该是 19 号,getDateAgo(new Date(), 2) 的结果应该是 18 号。

    跨月、年也应该是正确输出:

    let date = new Date(2015, 0, 2);
    
    alert( getDateAgo(date, 1) ); // 1, (1 Jan 2015)
    alert( getDateAgo(date, 2) ); // 31, (31 Dec 2014)
    alert( getDateAgo(date, 365) ); // 2, (2 Jan 2014)

    P.S. 函数不应该修改给定的 date 值。

    function getDateAgo(date, num) {
      let result = date.getTime() - num * 24 * 3600 * 1000;
      return new Date(result).getDate();
    }
    
    // 参考解决方案
    function getDateAgo(date, days) {
      let dateCopy = new Date(date);
    
      dateCopy.setDate(date.getDate() - days);
      return dateCopy.getDate();
    }
  5. 某月的最后一天

    写一个函数 getLastDayOfMonth(year, month) 返回 month 月的最后一天。有时候是 30,有时是 31,甚至在二月的时候会是 28/29。

    参数:

    • year —— 四位数的年份,比如 2012。
    • month —— 月份,从 0 到 11。

    举个例子,getLastDayOfMonth(2012, 1) = 29(闰年,二月)

    function getLastDayOfMonth(year, month) {
      let date = new Date(year, month + 1, 0);
      return date.getDate();
    }
  6. 今天过去了多少秒?

    写一个函数 getSecondsToday(),返回今天已经过去了多少秒?

    例如:如果现在是 10:00 am,并且没有夏令时转换,那么:

    getSecondsToday() == 36000 // (3600 * 10)

    该函数应该在任意一天都能正确运行。那意味着,它不应具有“今天”的硬编码值。

    function getSecondsToday() {
      const day = new Date();
      const hour = day.getHours();
      const minute = day.getMinutes();
      const second = day.getSeconds();
      return hour * 3600 + minute * 60 + second;
    }
    
    // 参考解决方案
    function getSecondsToday() {
      let now = new Date();
    
      // 使用当前的 day/month/year 创建一个对象
      let today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    
      let diff = now - today; // ms difference
      return Math.round(diff / 1000); // make seconds
    }
  7. 距离明天还有多少秒?

    写一个函数 getSecondsToTomorrow(),返回距离明天的秒数。

    例如,现在是 23:00,那么:

    getSecondsToTomorrow() == 3600

    P.S. 该函数应该在任意一天都能正确运行。那意味着,它不应具有“今天”的硬编码值。

    function getSecondsToTomorrow() {
      const now = new Date();
    
      const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
    
      const diff = tomorrow - now;
      return Math.random(diff / 1000);
    }
  8. 格式化相对日期

    写一个函数 formatDate(date),能够对 date 进行如下格式化:

    • 如果 date 距离现在不到 1 秒,输出 "right now"
    • 否则,如果 date 距离现在不到 1 分钟,输出 "n sec. ago"
    • 否则,如果不到 1 小时,输出 "m min. ago"
    • 否则,以 "DD.MM.YY HH:mm" 格式输出完整日期。即:"day.month.year hours:minutes",全部以两位数格式表示,例如:31.12.16 10:00

    举个例子:

    alert( formatDate(new Date(new Date - 1)) ); // "right now"
    
    alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago"
    
    alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago"
    
    // 昨天的日期,例如 31.12.16 20:00
    alert( formatDate(new Date(new Date - 86400 * 1000)) );
    function formatDate(date) {
      const now = new Date();
      const diff = Math.round((now - date) / 1000);
    
      const hour = Math.floor(diff / 3600);
      const minute = Math.floor((diff - hour * 3600) / 60);
      const second = Math.floor(diff % 60);
    
      if (hour) {
        return `${format(date.getDate())}.${format(date.getMonth() + 1)}.${format(
          date.getFullYear()
        )} ${format(date.getHours())}:${format(date.getMinutes())}`;
      } else if (minute) {
        return `${minute} min. ago`;
      } else if (second) {
        return `${second} sec. ago`;
      } else {
        return `right now`;
      }
    }
    function format(num) {
      num = '0' + num;
      return num.slice(-2);
    }