前段时间在项目使用div
元素模拟body
时使用了will-change
时触发了一个奇特的问题,最终以删除will-change
而解决。那么这一期中咱们先来聊聊will-change
、transform
和层有关的事情,而后再和你们一块儿分享几个在Web中的JavaScript相关的API,好比全屏API、MediaStream API、MediaRecorder API、scrollIntoView
API 和 分享 API等。感兴趣的同窗请继续往下阅读。javascript
在一些浏览器的调试工具中提供一些调试工具,能够查看当前面的三维视图模式,在该模式中,页面中的HTML嵌套结构,会以图形化的方式,由外到内,从页面底部一级一级凸显出来。这种视图可让你很容易的看清楚页面的嵌套结构。css
而在CSS中有关于渲染层的控制一样要以借助一些CSS的属性来作相应的控制,好比transform
和z-index
等。html
浏览器渲染一个Web页面可能会经历下面这样的几个过程:html5
简单地了解一下有关于样式计算,布局和元素绘制等几个方面。java
浏览器下载并解析HTML只会生成一个DOM树,这个时候DOM还不足以获知页面的具体样式,浏览器主进程还会基于CSS选择器解析CSS获取每个节点的最终的计算样式值。即便不提供任何CSS,浏览器对每一个元素也会有一个默认的样式:css3
若是想要让页面有完整的样式效果,除了获取每一个节点的具体样式,还须要获知每个节点在页面上的位置,布局实际上是找到全部元素的几何关系的进程。其具体过程大体以下:git
经过遍历DOM及相关的元素的计算样式,主线程会构建出包含每一个元素的坐标信息及盒子大小的布局树。布局树和DOM树相似,可是其中只包含页面可见的元素,若是一个元素设置了display: none
,这个元素不会出如今布局树上,伪元素虽然在DOM树上不可见,可是在布局树上是可见的。github
即便知道了不一样元素的位置及样式信息,咱们还须要知道不一样元素的绘制前后顺序才能正确绘制出整个页面。在绘制阶段,主线程会遍历布局树以建立绘制记录。绘制记录能够看作是记录各元素绘制前后顺序的笔记。web
页面的渲染还会经历一个层的合成。正如文章开头所示,页面分割了不一样的层,并相互嵌套。而复合是将页面分割为不一样的层,并单独栅格化,随后组合为帧的技术。不一样层的组合由compositor线程(合成器线程)完成。shell
主线程会遍历布局树来建立层树(Layer tree),添加了will-change
属性的元素,会被看作单独的一层。
有关于浏览器渲染原理方面更多的内容能够阅读:
你可能会想给每个元素都添加will-change
,不过组合过多的层也许会在每一帧都栅格化页面中的某些小部分更慢。那么怎么合理的使用will-change
或者说怎么更合理的使用层呢?
@dassurma在他的博客中对这方面作过相应的阐述:
除非你要更改
transform
,不然不要使用will-change: transform
。使用will-change: opacity
或backface-visibility:hidden
的反作用不会那么使人困扰。
来看看文章中一些有意思的东西,也值得咱们去关注和掌握的东西。
在CSS中会常用animation
或transition
来实现一些动效效果。在使用这两个属性时,浏览器会自动将动画元素放到一个层上。它将主画布保留到下一帧,并将额外的工做保持在尽量低的位置。咱们能够在浏览器的调试工具中的Rendering选项卡中启用Layers borders或Layers选项卡能够看到位于单独图层上的元素周围的橙色边框和当前页面上全部层的实时交互视图:
理想状况下呢,都但愿浏览器知道什么是合适的,而后去作什么。遗憾的是,事实并不是如此。例如,当你使用requestAnimationFrame()
在逐帧的基础上处理动画元素。这是很难的,不可能让浏览告诉元素将有一个新值转换每一个帧。除非你本身将动画元素放到它本身的层中,不然就会有性能问题,由于浏览器将从新绘制整个文档的每一帧。
在过去,大都会设置transform: translateZ(0)
来开启GPU的渲染。由于它将使用GPU计算透视(perspective
)失真(即便它最终没有失真)。若是使用translateX()
或translateY()
,则不须要透视图失真,浏览器将使用指定的偏移量将元素绘制到主画布中。
早期这样使用,在Chrome和Safari浏览器会让元素闪烁,因此被建议在样式中设置backface-visibility: hidden
,甚至直到今天还这么的使用。
自2016看开始,浏览器对will-change
属性的支持度愈来愈高,而该属性告诉浏览某个CSS属性将会改变。若是你在元素上设置will-change: transform
,会告诉浏览器transform
属性将在不久的未来发生更改。所以,浏览器能够推测地应用优化来适应这些将来的更改。在转换的状况下,这意味着它将强制元素到它本身的层上。
在某些场景之下,使用will-change:transform
会对性能有提升,好比:
能够避免元素重绘。
虽然某些场景对性能有所提升,但也会有相关的反作用。
先来看backface-visibility:hidden
带来的反作用 —— 隐藏元素的背面。一般这面不是面向用户的,可是当你在3D空间中旋转你的元素时,它会发生。以下图所示:
对于will-change: transform
,前面提到过,该属性会告诉浏览器未来会发生什么。因为这个语义,规范规定设置will-change:<something>
必须具备与该<something>
属性的任何非初始值相同的反作用。
这彷佛颇有道理,但当你使用position: fixed
或position: absolute
时,就会出错:
若是为transform
设置一个值,将会建立一个新的包含块。任何具备position: fixed
或position: absolute
的子元素都会相对于这个新的包含块。这个在一些容器中使用position: fixed
的元素将不会再相对于视窗定位,会相对于容器(具备新包含块)定位。
而transform: translateZ(0)
和will-change: transform
具备相同的反作用,可是也可 人会干扰使用transform
的其余样式,由于这些属性根据级联相互覆盖。
再看一下will-change: opacity
,从行为上看上去和前面示例中演示的效果同样,但这并不意味着它没有副做做。设置will-change:opacity
会建立一个新的叠加上下文。意味着它能够影响元素渲染的顺序。若是有重叠的元素,它能够更改哪一个元素位于顶部。即便出现这种状况,z-index
也能帮助你恢复你想要的层叠顺序。
也就是说,使用will-change
的时候千万要注意,这也为何在某些场景下使用will-change
会带来反作用。正如上面所述,除了will-change
以外,transform: translateZ(0)
、backface-visibility:hidden
或will-change: opacity
多少会带来一些反作用。直到目前为止,使用will-change: opacity
或 backface-visibility: hidden
能够将一个元素强制放到它本身的层上(由于它的反作用彷佛最不可能产生问题)。另外,只有当你真正要改变transform
时,才应用使用will-change: transform
。
扩展阅读:
Chrome 61开始为Android系统引入了Web Share API以来彷佛并无引发太多的关注。但在移动端的分享功能却又是不可或缺乏的一部分。Web Share API本质上它提供了一种方法,能够Web页面或Web应用程序中提供分享的能力。该API的引入容许开发人员利用用户设备的本地内容共享功能向应用程序或网站添加共享功能。
咱们能够先使用navigator.share
来作判断,检测用户的浏览器是否支持Web Share API:
if (navigator.share) {
// Web Share API is supported
} else {
// Fallback
}
复制代码
支持Web Share API的浏览器能够调用navigator.share()
方法并传递如下这些字段:
url
:分享的URL
字符串title
:分享的标题,一般是document.title
text
:分享的描述内容来看一个简单的示例:
shareButton.addEventListener('click', event => {
if (navigator.share) {
navigator.share({
title: '来自W3cplus的分享',
url: 'https://www.w3cplus.com'
}).then(() => {
console.log('Thanks for sharing!');
})
.catch(console.error);
} else {
// fallback
}
});
复制代码
对于不支持Web Share API的浏览器,咱们能够作了个降级方案,好比在Web页面弹出一个Modal框:
shareButton.addEventListener('click', event => {
if (navigator.share) {
navigator.share({
title: 'Web技巧11',
url: 'https://www.w3cplus.com'
}).then(() => {
console.log('Thanks for sharing!');
})
.catch(console.error);
} else {
shareDialog.classList.add('is-open');
}
});
复制代码
有关于Web Share API更多的内容能够阅读:
在Web页面或Web应用程序上,咱们能够从用户的相机、麦克风等来捕获媒体流。咱们可使用这些媒体流在WebRTC上进行实时的视频聊天。经过MediaRecorder API还能够直接在Web浏览器中记录和保存用户的音频或视频。
要使用MediaRecorder API就先须要一个MediaStream。能够从<video>
或<audio>
元素中获取一个,也能够经过调用getUserMedia
来捕获用户的相机和麦克风。一旦你有一个流,就能够用它初始化MediaRecorder
,也就能够开始录制了。
在记录期间,MediaRecorder
对象将发出dataavailable
事件,并将记录的数据做为事件的一部分。咱们将侦听这些事件并整理数组中的数据块。一旦记录完成,咱们将把块数组从新绑定到一个Blob
对象中。咱们能够经过调用MediaRecorder
对象上的start
和stop
来控制录制的开始和结束。
和Web Share API相似,能够经过下面的方式来检测浏览器是否支持MediaRecorder
:
if ('MediaRecorder' in window) {
// everything is good, let's go ahead
} else {
renderError ('Sorry, your browser doesn't support the MediaRecorder API, so this demo will not work')
}
复制代码
对于renderError
方法,咱们将用错误消息替换某个指定的元素,好比<main>
来显示相应的错误信息。能够在事件侦听器以后添加此方法:
function renderError(message) {
const main = document.querySelector('main');
main.innerHTML = `<div class="error"><p>${message}</p></div>`;
}
复制代码
若是浏览器支持MediaRecorder
,那咱们就能够访问麦克风来记录。为此,咱们将使用getUserMedia
这个API。另外,咱们不会要求直接访问麦克风,由于这对任何用户来讲都是一个糟糕的体验。咱们可让用户主动点击用户上面的某个按钮来访问麦克风,而后再向用户发起询问,肯定是否开启麦克风来记录。
if ('MediaRecorder' in window) {
getMic.addEventListener('click', async () => {
getMic.setAttribute('hidden', 'hidden');
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: false
});
console.log(stream);
} catch {
renderError(
'You denied access to the microphone so this demo will not work.'
);
}
});
} else {
renderError ('Sorry, your browser doesn't support the MediaRecorder API, so this demo will not work')
}
复制代码
还能够调用navigator.mediaDevices.getUserMedia
返回一个promise
,若是用户容许访问该媒体,则该promise
将成功解析。由于咱们使用的是现代JavaScript,因此可使用async/wait
使这个promise
看起来是同步的。咱们声明的click
事件是一个异步函数,而后当调用getUserMedia
时,咱们等待结果,而后继续。
固然,用户也有可能会拒绝访问麦克风,咱们能够在try/catch
语句中封装调用来处理这个问题。若是用户拒绝访问麦克风将致使catch
块中代码执行,那么会调用renderError
函数。
通过上面的处理,咱们可使用麦克风了,能够准备录音了。但咱们还须要将录下来的这些东西存放起来。
首先,咱们将使用的是MIME
类型,即audio/webm
,另外还须要建立一个变量,好比chunks
(它是一个数组),用来存储录下来的内容。
MediaRecorder
使用从用户麦克风捕获的媒体流和选对象初始化,将传递前面定义的MIME
类型:
if ('MediaRecorder' in window) {
getMic.addEventListener('click', async () => {
getMic.setAttribute('hidden', 'hidden');
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: false
});
const mimeType = 'audio/webm';
let chunks = [];
const recorder = new MediaRecorder(stream, { type: mimeType });
} catch {
renderError(
'You denied access to the microphone so this demo will not work.'
);
}
});
} else {
renderError ('Sorry, your browser doesn't support the MediaRecorder API, so this demo will not work')
}
复制代码
如今咱们已经建立了MediaRecorder
,咱们须要为它设置一些事件监听器。记录器出于许多不一样的缘由发出事件。这些都和记录器自己的交互有关,所以能够在记录器开始记录、暂停、继续和中止时侦听事件。最重要的事件是dataavailable
事件,它在记录器积极记录时按期发出。这些事件包含一个记录块,咱们将把它推到刚刚建立的chunks
数组上。
if ('MediaRecorder' in window) {
getMic.addEventListener('click', async () => {
getMic.setAttribute('hidden', 'hidden');
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: false
});
const mimeType = 'audio/webm';
let chunks = [];
const recorder = new MediaRecorder(stream, { type: mimeType });
recorder.addEventListener('dataavailable', event => {
if (typeof event.data === 'undefined') return;
if (event.data.size === 0) return;
chunks.push(event.data);
});
recorder.addEventListener('stop', () => {
const recording = new Blob(chunks, {
type: mimeType
});
renderRecording(recording, list);
chunks = [];
});
} catch {
renderError(
'You denied access to the microphone so this demo will not work.'
);
}
});
} else {
renderError ('Sorry, your browser doesn't support the MediaRecorder API, so this demo will not work')
}
复制代码
另外,为了方便用户能够把录下来的音保存起来,还能够建立一个函数:
function renderRecording(blob, list) {
const blobUrl = URL.createObjectURL(blob);
const li = document.createElement('li');
const audio = document.createElement('audio');
const anchor = document.createElement('a');
anchor.setAttribute('href', blobUrl);
const now = new Date();
anchor.setAttribute(
'download',
`recording-${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDay().toString().padStart(2, '0')}--${now.getHours().toString().padStart(2, '0')}-${now.getMinutes().toString().padStart(2, '0')}-${now.getSeconds().toString().padStart(2, '0')}.webm`
);
anchor.innerText = 'Download';
audio.setAttribute('src', blobUrl);
audio.setAttribute('controls', 'controls');
li.appendChild(audio);
li.appendChild(anchor);
list.appendChild(li);
}
复制代码
最终示例代码以下:
<!-- HTML -->
<div class="controls">
<button type="button" id="mic">Get Microphone</button>
<button type="button" id="record" hidden>Record</button>
</div>
<ul id="recordings"></ul>
// JavaScript
window.addEventListener('DOMContentLoaded', () => {
const getMic = document.getElementById('mic');
const recordButton = document.getElementById('record');
const list = document.getElementById('recordings');
if ('MediaRecorder' in window) {
getMic.addEventListener('click', async () => {
getMic.setAttribute('hidden', 'hidden');
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: false
});
const mimeType = 'audio/webm';
let chunks = [];
const recorder = new MediaRecorder(stream, { type: mimeType });
recorder.addEventListener('dataavailable', event => {
if (typeof event.data === 'undefined') return;
if (event.data.size === 0) return;
chunks.push(event.data);
});
recorder.addEventListener('stop', () => {
const recording = new Blob(chunks, {
type: mimeType
});
renderRecording(recording, list);
chunks = [];
});
recordButton.removeAttribute('hidden');
recordButton.addEventListener('click', () => {
if (recorder.state === 'inactive') {
recorder.start();
recordButton.innerText = 'Stop';
} else {
recorder.stop();
recordButton.innerText = 'Record';
}
});
} catch {
renderError(
'You denied access to the microphone so this demo will not work.'
);
}
});
} else {
renderError(
"Sorry, your browser doesn't support the MediaRecorder API, so this demo will not work."
);
}
});
function renderError(message) {
const main = document.querySelector('main');
main.innerHTML = `<div class="error"><p>${message}</p></div>`;
}
function renderRecording(blob, list) {
const blobUrl = URL.createObjectURL(blob);
const li = document.createElement('li');
const audio = document.createElement('audio');
const anchor = document.createElement('a');
anchor.setAttribute('href', blobUrl);
const now = new Date();
anchor.setAttribute(
'download',
`recording-${now.getFullYear()}-${(now.getMonth() + 1)
.toString()
.padStart(2, '0')}-${now
.getDay()
.toString()
.padStart(2, '0')}--${now
.getHours()
.toString()
.padStart(2, '0')}-${now
.getMinutes()
.toString()
.padStart(2, '0')}-${now
.getSeconds()
.toString()
.padStart(2, '0')}.webm`
);
anchor.innerText = 'Download';
audio.setAttribute('src', blobUrl);
audio.setAttribute('controls', 'controls');
li.appendChild(audio);
li.appendChild(anchor);
list.appendChild(li);
}
复制代码
示例代码来自@Phil nash《An introduction to the MediaRecorder API》一文。
扩展阅读:
MediaStream API 也能够用来帮助你建立来自用户输入设备的数据流,好比视频(摄像机)或音频(麦克风)。该API有两个核心方法:.getUserMedia()
和navigator.mediaDevices
。
请注意,此对象仅在HTTPS-secured上下文中才可用。
async function getMedia() {
const constraints = {
// ...
};
let stream;
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
// use the stream
} catch (err) {
// handle the error - user's rejection or no media available
}
}
getMedia();
复制代码
该方法接受所谓的constraints
对象,并返回一个promise
,该promise
解析为一个新的MediaStream
实例。这样的接口是当前流媒体的表示。它由零个或多个独立的MediaStreamTrack
组成,每一个MediaStreamTrack
表示音频或视频流,而音频轨道由左右通道组成(用于立体声和其余东西)。若是须要进一步控制,这些轨道还提供了一些特殊的方法和事件。
constraints
对象是较为重要的部分。它用于配置.getUserMedia()
的请求和生成的流。此对象能够具备布尔值或对象值的两个属性:audio
和video
。
const constraints = {
video: true,
audio: true
};
复制代码
经过将它们都设置为true
,咱们请求访问用户的默认视频和音频的输入设备,并应用默认设置。要知道,为了让.getUserMedia()
能正常工做,必须至少设置这两个属性中的一个。
若是但愿进一步配置媒体设备的设置,须要传递一个对象。这里提供的属性列表很是长,而且根据应用于视频或音频的曲目类型不一样而有所不一样。你能够在这里看到完整的列表,并使用.getSupportedConstraints()
方法检查可用的列表。
假设此次咱们想更具体一些,为视频轨道指定一些额外的配置。
async function getConstraints() {
const supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
const video = {};
if (supportedConstraints.width) {
video.width = 1920;
}
if (supportedConstraints.height) {
video.height = 1080;
}
if (supportedConstraints.deviceId) {
const devices = await navigator.mediaDevices.enumerateDevices();
const device = devices.find(device => {
return device.kind == "videoinput";
});
video.deviceId = device.deviceId;
}
return { video };
}
复制代码
另外咱们可使用.enumerateDevices()
方法检查可用的输入设备,同时设置视频的分辨率。它返回一个promise
,该promise
解析为一个MediaDeviceInfo
对象数组。来看一个简单的小示例:
<!-- HTML -->
<video autoplay></video>
<audio autoplay></audio>
// JavaScript
async function getConstraints() {
const supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
const video = {};
if (supportedConstraints.width) {
video.width = 1920;
}
if (supportedConstraints.height) {
video.height = 1080;
}
if (supportedConstraints.deviceId) {
const devices = await navigator.mediaDevices.enumerateDevices();
const device = devices.find(device => {
return device.kind == "videoinput";
});
video.deviceId = device.deviceId;
}
return { video, audio: true };
}
async function getMedia() {
const constraints = await getConstraints();
const video = document.querySelector("video");
const audio = document.querySelector("audio");
let stream = null;
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
console.log(stream, video.srcObject)
video.srcObject = stream;
audio.srcObject = stream;
// use the stream
} catch (err) {
// handle the error - user's rejection or no media available
}
}
getMedia();
复制代码
扩展阅读:
有的时候须要将页面全屏显示。CSS中有一些伪元素,能够实现该效果:
:-webkit-full-screen body,
:-moz-full-screen body,
:-ms-fullscreen body {
/* properties */
width: 100vw;
height: 100vh;
}
:full-screen body {
/*pre-spec */
/* properties */
width: 100vw;
height: 100vh;
}
:fullscreen body {
/* spec */
/* properties */
width: 100vw;
height: 100vh;
}
/* deeper elements */
:-webkit-full-screen body {
width: 100vw;
height: 100vh;
}
/* styling the backdrop*/
::backdrop,
::-ms-backdrop {
/* Custom styles */
}
复制代码
而苹果去年秋天在iPad Safari上推出了对全屏API的支持。这意味着开发人员如今能够在iPad上为用户建立彻底沉浸式的Web应用程序。它可靠地消除了屏幕上的全部干扰,帮助用户专一于手头的任务。就像一个本地应用程序同样,能够全屏显示。
下面的代码就是使用原生JavaScript来实现全屏效果的。首先建立了一个名为_toggleFullScreen
的函数,让你在全屏和url
模式之间切换:
const _toggleFullScreen = function _toggleFullScreen() {
if (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement) {
if (document.cancelFullScreen) {
document.cancelFullScreen();
} else {
if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else {
if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
}
}
}
} else {
const _element = document.documentElement;
if (_element.requestFullscreen) {
_element.requestFullscreen();
} else {
if (_element.mozRequestFullScreen) {
_element.mozRequestFullScreen();
} else {
if (_element.webkitRequestFullscreen) {
_element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
}
}
}
}
};
复制代码
这个_toggleFullScreen
函数处理全部浏览器的前缀和特性。如今咱们只须要确认用户使用的设备、浏览器和iOS版本,这样就能够启用全屏功能了。
不过咱们能够采用window.navigator.userAgent
来作检测:
const userAgent = window.navigator.userAgent;
const iPadSafari =
!!userAgent.match(/iPad/i) && // Detect iPad first.
!!userAgent.match(/WebKit/i) && // Filter browsers with webkit engine only
!userAgent.match(/CriOS/i) && // Eliminate Chrome & Brave
!userAgent.match(/OPiOS/i) && // Rule out Opera
!userAgent.match(/FxiOS/i) && // Rule out Firefox
!userAgent.match(/FocusiOS/i); // Eliminate Firefox Focus as well!
const element = document.getElementById('fullScreenButton');
function iOS() {
if (userAgent.match(/ipad|iphone|ipod/i)) {
const iOS = {};
iOS.majorReleaseNumber = +userAgent.match(/OS (\d)?\d_\d(_\d)?/i)[0].split('_')[0].replace('OS ', '');
return iOS;
}
}
if (element !== null) {
if (userAgent.match(/iPhone/i) || userAgent.match(/iPod/i)) {
element.className += ' hidden';
} else if (userAgent.match(/iPad/i) && iOS().majorReleaseNumber < 12) {
element.className += ' hidden';
} else if (userAgent.match(/iPad/i) && !iPadSafari) {
element.className += ' hidden';
} else {
element.addEventListener('click', _toggleFullScreen, false);
}
}
复制代码
示例代码来自于@Marvin Danig的《How to go fullscreen on iPad Safari》一文。
来看个示例:
扩展阅读:
在《滚动的特性》和《改变用户体验的滚动新特性》中咱们都提到了CSS的overscroll-behavior
能够控制一个容器或页面body容器滚动时发生的默认行为。可使用这个属性取消滚动连接、禁用、自定义下拉刷新,禁用在iOS上的回弹效果等。并且使用overscroll-behavior不会对页面有性能影响。
在JavaScript中提供了一个scrollIntoView
API,能够指示浏览器将一个元素添加到视窗端口。经过在scrollIntoViewOption
对象上添加行为属性,能够指示scrollIntoView
API使用滚动部分具备动画效果。
element.scrollIntoView({ behavior: 'smooth' });
复制代码
好比使用JavaScript来自动检测对锚点的点击,这样浏览器就会跳到锚点目标。这种跳转可能会让用户迷失方向(由于它会一闪就跳过去了),因此让这个动画有一个过程化将会大大改善用户体验。
// Everytime someone clicks on something
document.body.addEventListener('click', e => {
const href = e.target.href;
// no href attribute, no need to continue then
if (!href) return;
const id = href.split('#').pop();
const target = document.getElementById(id);
// no target to scroll to, bail out
if (!target) return;
// prevent the default quick jump to the target
e.preventDefault();
// set hash to window location so history is kept correctly
history.pushState({}, document.title, href);
// smooooooth scroll to the target!
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
});
复制代码
扩展阅读:
scrollIntoView
与 scrollIntoViewIfNeeded
API 介绍scrollIntoView
is the best thing since sliced breadWeb Animations API已经出来好久了,并且很是的棒。除了能支持日常动画以外,还可使用Promise
、rAF
和CSS的transition
来从新建立它们,会让动画效果更接近人体工程学。
使用element.animation
能够很是轻易的让任何元素根据动画序列帧播放,相似CSS的@keyframes
动画:
element.animate([
{transform: 'translateX(0px)', backgroundColor: 'red'},
{transform: 'translateX(100px)', backgroundColor: 'blue'},
{transform: 'translateX(50px)', backgroundColor: 'green'},
{transform: 'translateX(0px)', backgroundColor: 'red'},
//...
], {
duration: 3000,
iterations: 3,
delay: 0
}).finish.then(_ => console.log('I’m done animating!'));
复制代码
然而,平常开发要像上面那样使用动画系列声明的方式来定义动画,须要使用Web Animation Polyfill,它包含了咱们实际所须要的更多功能:
Object.assign(element.style,
{
transition: 'transform 1s, background-color 1s',
backgroundColor: 'red',
transform: 'translateX(0px)',
}
);
requestAnimationFramePromise()
.then(_ => animate(element,
{transform: 'translateX(100px)', backgroundColor: 'blue'}))
.then(_ => animate(element,
{transform: 'translateX(50px)', backgroundColor: 'green'}))
.then(_ => animate(element,
{transform: 'translateX(0px)', backgroundColor: 'red'}))
.then(_ => console.log('I’m done animating!'));
复制代码
当你使用CSS Transition让一个元素具备一个动画效果,其实他有一个transitioned
事件:
function transitionEndPromise(element) {
return new Promise(resolve => {
element.addEventListener('transitionend', function f() {
element.removeEventListener('transitionend', f);
resolve();
});
});
}
复制代码
这样使用后不会注册咱们的侦听器,也不会泄露内存!有了这个,我可使用Promises替代回调,等待动画的结束。
咱们也能够对requestAnimationFrame
进行包装,让其变得更简单:
function requestAnimationFramePromise() {
return new Promise(resolve => requestAnimationFrame(resolve));
}
复制代码
有了这个,咱们可使用Promise而不是回调来等待下一帧。
咱们能够将它合并到咱们本身的element.animate()
中:
function animate(element, stylz) {
Object.assign(element.style, stylz);
return transitionEndPromise(element)
.then(_ => requestAnimationFramePromise());
}
复制代码
这是对animation
和transition
很是轻量的抽象处理,会给不少开发人员带来不少的便利。
若是您在两个元素上使用这个技术,而其中一个元素是另外一个元素的祖先,那么从继承者的传递结束事件将使前面的动画链向前推动。幸运的是,经过检查事件,能够很容易的解决这样的现象,好比event.target
:
function transitionEndPromise(element) {
return new Promise(resolve => {
element.addEventListener('transitionend', function f(event) {
if (event.target !== element) return;
element.removeEventListener('transitionend', f);
resolve();
});
});
}
复制代码
上面的的示例代码来自于@Surma的《DIY Web Animations: Promises + rAF + Transitions》一文
扩展阅读:
这一期中咱们先来聊聊will-change、transform和层有关的事情,而后再和你们一块儿分享几个在Web中的JavaScript相关的API,好比全屏API、MediaStream API、MediaRecorder API、scrollIntoView API 和 分享 API等。