欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JavaScript高级程序设计第四版学习--第二十章

程序员文章站 2022-07-07 18:54:47
...

title: JavaScript高级程序设计第四版学习–第二十章
date: 2021-5-29 20:59:12
author: Xilong88
tags: JavaScript

本章内容
Atomics与SharedArrayBuffer
跨上下文消息
Encoding API
File API与Blob API
拖放
Notifications API
Page Visibility API
Streams API
计时API
Web组件
Web Cryptography API
可能出现的面试题:
1.如何跨文档传递信息,了解过XDM吗?
2.文件拖拽如何实现?
3.拖放事件的组成?
4.通知API了解过吗?
5.如何测试加载性能?(高精度时间戳怎么获取)
6.了解过影子DOM吗?

知识点:

Atomics与SharedArrayBuffer 略
Streams API 略
自定义元素 略
Web Cryptography API 略

1.跨上下文消息

跨文档消息 ,有时候也简称为XDM(cross-document messaging),是一种在不同执行上下文(如不同工作线程或不同源的页面)间传递信息的能力。

let iframeWindow = document.getElementById("myframe").contentWindow;
iframeWindow.postMessage("A secret", "http://www.wrox.com");

如果不想限制接收目标,则可以给postMessage() 的第二个参数传"*" ,但不推荐这么做。

传给onmessage 事件处理程序的event 对象包含以下3方面重要信息。

data :作为第一个参数传递给postMessage() 的字符串数据。
origin :发送消息的文档源,例如"http://www.wrox.com" 。
source :发送消息的文档中window 对象的代理。这个代理对象主
要用于在发送上一条消息的窗口中执行postMessage() 方法。如
果发送窗口有相同的源,那么这个对象应该就是window 对象。
window.addEventListener("message", (event) => {
  // 确保来自预期发送者
  if (event.origin == "http://www.wrox.com") {
    // 对数据进行一些处理
    processMessage(event.data);
    // 可选:向来源窗口发送一条消息
    event.source.postMessage("Received!", "http://p2p.wrox.com");
  }
});

2.Encoding API

批量编码是通过TextEncoder的实例完成

const textEncoder = new TextEncoder();
const decodedText = 'foo';
const encodedText = textEncoder.encode(decodedText);
// f的UTF-8编码是0x66(即十进制102)
// o的UTF-8编码是0x6F(即二进制111)
console.log(encodedText); // Uint8Array(3) [102, 111, 111]

编码器是用于处理字符的,有些字符(如表情符号)在最终返回的数组中可能会占多个索引:

const textEncoder = new TextEncoder();
const decodedText = '☺';
const encodedText = textEncoder.encode(decodedText);
// ☺的UTF-8编码是0xF0 0x9F 0x98 0x8A(即十进制240、159、152、138)
console.log(encodedText); // Uint8Array(4) [240, 159, 152, 138]

解码

const textDecoder = new TextDecoder();
// f的UTF-8编码是0x66(即十进制102)
// o的UTF-8编码是0x6F(即二进制111)
const encodedText = Uint8Array.of(102, 111, 111);
const decodedText = textDecoder.decode(encodedText);
console.log(decodedText); // foo

3.File API与Blob API

name :本地系统中的文件名。
size :以字节计的文件大小。
type :包含文件MIME类型的字符串。
lastModifiedDate :表示文件最后修改时间的字符串。这个属性只有Chome实现了。
let filesList = document.getElementById("files-list");
filesList.addEventListener("change", (event) => {
  let files = event.target.files,
      i = 0,
      len = files.length;
  while (i < len) {
    const f = files[i];
    console.log(`${f.name} (${f.type}, ${f.size} bytes)`);

    i++;
  }
});

FileReader 类型

FileReader 类型提供了几个读取文件数据的方法。

readAsText(file, encoding) :从文件中读取纯文本内容并保
存在result 属性中。第二个参数表示编码,是可选的。
readAsDataURL(file) :读取文件并将内容的数据URI保存
在result 属性中。
readAsBinaryString(file) :读取文件并将每个字符的二进制
数据保存在result 属性中。
readAsArrayBuffer(file) :读取文件并将文件内容以
ArrayBuffer 形式保存在result 属性。

因为这些读取方法是异步的,所以每个FileReader 会发布几个事件,其中3个最有用的事件是progress 、error 和load ,分别表示还有更多数据、发生了错误和读取完成。

progress 事件每50毫秒就会触发一次,其与XHR的progress 事件具有相同的信息:lengthComputable 、loaded 和total 。

在progress 事件中可以读取FileReader 的result 属性,即使其中尚未包含全部数据。

触发error 事件时,FileReader 的error 属性会包含错误信息。这个属性是一个对象,只包含一个属性:code 。这个错误码的值可能是1(未找到文件)、2(安全错误)、3(读取被中断)、4(文件不可读)或5(编码错误)。

let filesList = document.getElementById("files-list");
filesList.addEventListener("change", (event) => {
  let info = "",
      output = document.getElementById("output"),
      progress = document.getElementById("progress"),
      files = event.target.files,
      type = "default",
      reader = new FileReader();
  if (/image/.test(files[0].type)) {
    reader.readAsDataURL(files[0]);
    type = "image";
  } else {
    reader.readAsText(files[0]);
    type = "text";
  }
  reader.onerror = function() {
    output.innerHTML = "Could not read file, error code is " +
        reader.error.code;
  };
  reader.onprogress = function(event) {
    if (event.lengthComputable) {
      progress.innerHTML = `${event.loaded}/${event.total}`;
    }
  };
  reader.onload = function() {
    let html = "";
    switch(type) {
      case "image":
        html = `<img src="${reader.result}">`;
        break;
      case "text":

        html = reader.result;
        break;
    }
    output.innerHTML = html;
  };
});

FileReaderSync 类型

FileReaderSync 类型就是FileReader 的同步 版本

// worker.js
self.omessage = (messageEvent) => {
  const syncReader = new FileReaderSync();
  console.log(syncReader); // FileReaderSync {}
  // 读取文件时阻塞工作线程
  const result = syncReader.readAsDataUrl(messageEvent.data);
  // PDF文件的示例响应

  console.log(result); // data:application/pdf;base64,JVBERi0xLjQK...
  // 把URL发回去
  self.postMessage(result);
};

Blob 与部分读取

Blob 对象有一个size 属性和一个type 属性,还有一个slice() 方法用于进一步切分数据。另外也可以使用FileReader 从Blob 中读取数据。下面的例子只会读取文件的前32字节:

let filesList = document.getElementById("files-list");
filesList.addEventListener("change", (event) => {
  let info = "",
    output = document.getElementById("output"),
    progress = document.getElementById("progress"),
    files = event.target.files,
    reader = new FileReader(),
    blob = blobSlice(files[0], 0, 32);
  if (blob) {

    reader.readAsText(blob);
    reader.onerror = function() {
      output.innerHTML = "Could not read file, error code is " +
                 reader.error.code;
    };
    reader.onload = function() {
      output.innerHTML = reader.result;
    };
  } else {
    console.log("Your browser doesn't support slice().");
  }
});

window.URL.createObjectURL() 方法并传入File 或Blob 对象。

返回的URL就可以直接用

let filesList = document.getElementById("files-list");
filesList.addEventListener("change", (event) => {
  let info = "",
    output = document.getElementById("output"),
    progress = document.getElementById("progress"),
    files = event.target.files,
    reader = new FileReader(),
    url = window.URL.createObjectURL(files[0]);
  if (url) {
    if (/image/.test(files[0].type)) {
      output.innerHTML = `<img src="${url}">`;
    } else {
      output.innerHTML = "Not an image.";
    }
  } else {

    output.innerHTML = "Your browser doesn't support object URLs.";
  }
});

4.拖文件

event.dataTransfer.files可以读取到拖到浏览器里面的文件

let droptarget = document.getElementById("droptarget");
function handleEvent(event) {
  let info = "",
    output = document.getElementById("output"),
    files, i, len;
  event.preventDefault();
  if (event.type == "drop") {
    files = event.dataTransfer.files;
    i = 0;
    len = files.length;
    while (i < len) {
      info += `${files[i].name} (${files[i].type}, ${files[i].size} bytes)<br>`;
      i++;
    }
    output.innerHTML = info;

  }
}
droptarget.addEventListener("dragenter", handleEvent);
droptarget.addEventListener("dragover", handleEvent);
droptarget.addEventListener("drop", handleEvent);

要记得屏蔽掉拖进来和拖放结束的默认事件,不然会显示文件下载到了浏览器。

5.媒体元素
<audio><video>

省略了,要用再看

6.原生拖放
在某个元素被拖动时,会(按顺序)触发以下事
件:
(1) dragstart
(2) drag
(3) dragend

在按住鼠标键不放并开始移动鼠标的那一刻,被拖动元素上会触发dragstart 事件。此时光标会变成非放置符号(圆环中间一条斜杠),表示元素不能放到自身上。拖动开始时,可以在ondragstart 事件处理程序中通过JavaScript执行某些操作。
dragstart 事件触发后,只要目标还被拖动就会持续触发drag 事件。这个事件类似于mousemove ,即随着鼠标移动而不断触发。当拖动停止时(把元素放到有效或无效的放置目标上),会触发dragend 事件。

dataTransfer 对象

getData() 和setData()

// 传递文本
event.dataTransfer.setData("text", "some text");
let text = event.dataTransfer.getData("text");
// 传递URL
event.dataTransfer.setData("URL", "http://www.wrox.com/");
let url = event.dataTransfer.getData("URL");

dropEffect 与effectAllowed

dropEffect 属性可以告诉浏览器允许哪种放置行为。这个属性有以下4种可能的值。

"none" :被拖动元素不能放到这里。这是除文本框之外所有元素
的默认值。
"move" :被拖动元素应该移动到放置目标。
"copy" :被拖动元素应该复制到放置目标。
"link" :表示放置目标会导航到被拖动元素(仅在它是URL的情
况下)。

effectAllowed 属性表示对被拖动元素是否允许dropEffect 。这个属性有如下几个可能的值。

"uninitialized" :没有给被拖动元素设置动作。
"none" :被拖动元素上没有允许的操作。
"copy" :只允许"copy" 这种dropEffect 。
"link" :只允许"link" 这种dropEffect 。

"move" :只允许"move" 这种dropEffect 。
"copyLink" :允许"copy""link" 两种dropEffect 。
"copyMove" :允许"copy""move" 两种dropEffect 。
"linkMove" :允许"link""move" 两种dropEffect 。
"all" :允许所有dropEffect 。

可拖动能力

<!-- 禁止拖动图片 -->
<img src="smile.gif" draggable="false" alt="Smiley face">
<!-- 让元素可以拖动 -->
<div draggable="true">...</div>

7.显示和隐藏通知

Notification.requestPermission()
    .then((permission) => {
      console.log('User responded to permission request:', permission);
    });
new Notification('Title text!', {
  body: 'Body text!',
  image: 'path/to/image.png',
  vibrate: true
});

Notifications API
提供了4个用于添加回调的生命周期方法:

onshow 在通知显示时触发;
onclick 在通知被点击时触发;
onclose 在通知消失或通过close() 关闭时触发;
onerror 在发生错误阻止通知显示时触发。
const n = new Notification('foo');
n.onshow = () => console.log('Notification was shown!');
n.onclick = () => console.log('Notification was clicked!');
n.onclose = () => console.log('Notification was closed!');
n.onerror = () => console.log('Notification experienced an error!');

8.Page Visibility API

document.visibilityState 值,表示下面4种状态之一。

页面在后台标签页或浏览器中最小化了。
页面在前台标签页中。
实际页面隐藏了,但对页面的预览是可见的(例如在Windows7上,用户鼠标移到任务栏图标上会显示网页预览)。
页面在屏外预渲染。

visibilitychange 事件,该事件会在文档从隐藏变可见(或反之)时触发。document.hidden 布尔值,表示页面是否隐藏。这可能意味着页面在后台标签页或浏览器中被最小化了。这个值是为了向后兼容才继续被浏览器支持的,应该优先使用document.visibilityState

检测页面可见性。要想在页面从可见变为隐藏或从隐藏变为可见时得到通知,需要监听visibilitychange 事件。document.visibilityState 的值是以下三个字符串之一:
“hidden”
“visible”
“prerender”

9.计时API

window.performance上有很多计时API,相对于Date对象来说,更精准。

Performance 接口由多个API构成:

High Resolution Time API
Performance Timeline API
Navigation Timing API
User Timing API
Resource Timing API
Paint Timing API
const t0 = performance.now();
const t1 = performance.now();
console.log(t0);       // 1768.625000026077
console.log(t1);       // 1768.6300000059418
const duration = t1 – t0;
console.log(duration); // 0.004999979864805937

performance.timeOrigin 属性返回计时器初始化时全局系统时钟的值。

const relativeTimestamp = performance.now();
const absoluteTimestamp = performance.timeOrigin + relativeTimestamp;
console.log(relativeTimestamp); // 244.43500000052154
console.log(absoluteTimestamp); // 1561926208892.4001

每个Entry对象都有name 、entryType 、startTime 和duration 属性
记录着性能相关的东西。

console.log(performance.getEntries());
const entry = performance.getEntries()[0];
console.log(entry.name);      // "https://foo.com"
console.log(entry.entryType); // navigation
console.log(entry.startTime); // 0
console.log(entry.duration);  // 182.36500001512468

User Timing API
可以自定义标记,然后获取标记上的信息

performance.mark('foo');
console.log(performance.getEntriesByType('mark')[0]);
// PerformanceMark {
//   name: "foo",
//   entryType: "mark",
//   startTime: 269.8800000362098,
//   duration: 0
// }
performance.mark('foo');
for (let i = 0; i < 1E6; ++i) {}
performance.mark('bar');
const [endMark, startMark] = performance.getEntriesByType('mark');
console.log(startMark.startTime - endMark.startTime); // 1.3299999991431832

除了自定义性能条目,还可以生成PerformanceMeasure (性能度量)条目,对应由名字作为标识的两个标记之间的持续时间。PerformanceMeasure 的实例由performance.measure() 方法生成:

performance.mark('foo');
for (let i = 0; i < 1E6; ++i) {}
performance.mark('bar');
performance.measure('baz', 'foo', 'bar');
const [differenceMark] = performance.getEntriesByType('measure');
console.log(differenceMark);
// PerformanceMeasure {

//   name: "baz",
//   entryType: "measure",
//   startTime: 298.9800000214018,
//   duration: 1.349999976810068
// }

Navigation Timing API

Navigation Timing API提供了高精度时间戳,用于度量当前页面加载速度。浏览器会在导航事件发生时自动记录PerformanceNavigationTiming 条目。这个对象会捕获大量时间戳,用于描述页面是何时以及如何加载的

下面的例子计算了loadEventStart 和loadEventEnd 时间戳之间的差:

const [performanceNavigationTimingEntry] = performance.getEntriesByType('navigation');
console.log(performanceNavigationTimingEntry);
// PerformanceNavigationTiming {
//   connectEnd: 2.259999979287386
//   connectStart: 2.259999979287386
//   decodedBodySize: 122314
//   domComplete: 631.9899999652989
//   domContentLoadedEventEnd: 300.92499998863786
//   domContentLoadedEventStart: 298.8950000144541
//   domInteractive: 298.88499999651685
//   domainLookupEnd: 2.259999979287386
//   domainLookupStart: 2.259999979287386
//   duration: 632.819999998901
//   encodedBodySize: 21107
//   entryType: "navigation"
//   fetchStart: 2.259999979287386
//   initiatorType: "navigation"
//   loadEventEnd: 632.819999998901
//   loadEventStart: 632.0149999810383
//   name: " https://foo.com "
//   nextHopProtocol: "h2"
//   redirectCount: 0
//   redirectEnd: 0
//   redirectStart: 0

//   requestStart: 7.7099999762140214
//   responseEnd: 130.50999998813495
//   responseStart: 127.16999999247491
//   secureConnectionStart: 0
//   serverTiming: []
//   startTime: 0
//   transferSize: 21806
//   type: "navigate"
//   unloadEventEnd: 132.73999997181818
//   unloadEventStart: 132.41999997990206
//   workerStart: 0
// }
console.log(performanceNavigationTimingEntry.loadEventEnd –
            performanceNavigationTimingEntry.loadEventStart);
// 0.805000017862767

Resource Timing API

面的例子计算了加载一个特定资源所花的时间:

const performanceResourceTimingEntry = performance.getEntriesByType('resource')[0];
console.log(performanceResourceTimingEntry);
// PerformanceResourceTiming {
//   connectEnd: 138.11499997973442
//   connectStart: 138.11499997973442
//   decodedBodySize: 33808
//   domainLookupEnd: 138.11499997973442
//   domainLookupStart: 138.11499997973442
//   duration: 0
//   encodedBodySize: 33808
//   entryType: "resource"
//   fetchStart: 138.11499997973442
//   initiatorType: "link"
//   name: "https://static.foo.com/bar.png",

//   nextHopProtocol: "h2"
//   redirectEnd: 0
//   redirectStart: 0
//   requestStart: 138.11499997973442
//   responseEnd: 138.11499997973442
//   responseStart: 138.11499997973442
//   secureConnectionStart: 0
//   serverTiming: []
//   startTime: 138.11499997973442
//   transferSize: 0
//   workerStart: 0
// }
console.log(performanceResourceTimingEntry.responseEnd –
            performanceResourceTimingEntry.requestStart);
// 493.9600000507198

10.Web组件

HTML模板

<template id="foo">
  <p>I'm inside a template!</p>
</template>
const fragment = document.querySelector('#foo').content;
console.log(document.querySelector('p')); // null
console.log(fragment.querySelector('p')); // <p>...<p>

通过<template> 元素的content 属性可以取得这
个DocumentFragment 的引用:

console.log(document.querySelector('#foo').content); // #document-fragment

DocumentFragment 也是批量向HTML中添加元素的高效工具。因为只需要把操作一次DOM

// 开始状态:
// <div id="foo"></div>
//
// 期待的最终状态:
// <div id="foo">
//   <p></p>
//   <p></p>
//   <p></p>
// </div>
// 也可以使用document.createDocumentFragment()
const fragment = new DocumentFragment();
const foo = document.querySelector('#foo');
// 为DocumentFragment添加子元素不会导致布局重排
fragment.appendChild(document.createElement('p'));
fragment.appendChild(document.createElement('p'));
fragment.appendChild(document.createElement('p'));
console.log(fragment.children.length); // 3
foo.appendChild(fragment);
console.log(fragment.children.length); // 0
console.log(document.body.innerHTML);
// <div id="foo">
//   <p></p>

//   <p></p>
//   <p></p>
// </div>

影子DOM

attachShadow() 方法需要一个shadowRootInit 对象,返回影子DOM的实例。shadowRootInit 对象必须包含一个mode 属性,值为"open" 或"closed" 。对"open" 影子DOM的引用可以通过shadowRoot 属性在HTML元素上获得,而对"closed" 影子DOM的引用无法这样获取

document.body.innerHTML = `
  <div id="foo"></div>
  <div id="bar"></div>
`;
const foo = document.querySelector('#foo');
const bar = document.querySelector('#bar');
const openShadowDOM = foo.attachShadow({ mode: 'open' });
const closedShadowDOM = bar.attachShadow({ mode: 'closed' });

console.log(openShadowDOM);   // #shadow-root (open)
console.log(closedShadowDOM); // #shadow-root (closed)
console.log(foo.shadowRoot);  // #shadow-root (open)
console.log(bar.shadowRoot);  // null
for (let color of ['red', 'green', 'blue']) {
  const div = document.createElement('div');
  const shadowDOM = div.attachShadow({ mode: 'open' });
  document.body.appendChild(div);
  shadowDOM.innerHTML = `
    <p>Make me ${color}</p>
    <style>
    p {
      color: ${color};
    }
    </style>
  `;
}

合成与影子DOM槽位

document.body.innerHTML = `
<div id="foo">
  <p>Foo</p>
</div>
`;
document.querySelector('div')
    .attachShadow({ mode: 'open' })
    .innerHTML = `
      <div id="bar">
        <slot></slot>
      </div>`
console.log(document.querySelector('p').parentElement);
// <div id="foo"></div>

宿主的元素会放到槽位里面去

槽位可以命名

document.body.innerHTML = `
<div>
  <p slot="foo">Foo</p>
  <p slot="bar">Bar</p>
</div>
`;
document.querySelector('div')
    .attachShadow({ mode: 'open' })
    .innerHTML = `
    <slot name="bar"></slot>
    <slot name="foo"></slot>
    `;
// Renders:
// Bar
// Foo

事件会逃出影子DOM

// 创建一个元素作为影子宿主
document.body.innerHTML = `
<div onclick="console.log('Handled outside:', event.target)"></div>
`;
// 添加影子DOM并向其中插入HTML
document.querySelector('div')
  .attachShadow({ mode: 'open' })
  .innerHTML = `
<button onclick="console.log('Handled inside:', event.target)">Foo</button>
`;
// 点击按钮时:
// Handled inside:  <button οnclick="..."></button>
// Handled outside: <div οnclick="..."></div>