本文测试使用环境:
系统:macOS Mojave 10.14.2
CPU:4 核 2.3 GHz
Node: 10.15.1
一般人理解 Node 是单线程的,所以 Node 启动后线程数应该为 1,我们做实验看一下。
1 | setInterval(() => { |
可以看到 Node 进程占用了 7 个线程。为什么会有 7 个线程呢?
我们都知道,Node 中最核心的是 v8 引擎,在 Node 启动后,会创建 v8 的实例,这个实例是多线程的。
所以大家常说的 Node 是单线程的指的是 JavaScript 的执行是单线程的,但 Javascript 的宿主环境,无论是 Node 还是浏览器都是多线程的。
Node 有两个编译器:
full-codegen:简单快速地将 js 编译成简单但是很慢的机械码。
Crankshaft:比较复杂的实时优化编译器,编译高性能的可执行代码。
还是上面那个例子,我们在定时器执行的同时,去读一个文件:
1 | const fs = require('fs') |
线程数量变成了 11 个,这是因为在 Node 中有一些 IO 操作(DNS,FS)和一些 CPU 密集计算(Zlib,Crypto)会启用 Node 的线程池,而线程池默认大小为 4,因为线程数变成了 11。
我们可以手动更改线程池默认大小:
1 | process.env.UV_THREADPOOL_SIZE = 64 |
一行代码轻松把线程变成 71 😊
Node 的单线程也带来了一些问题,比如对 cpu 利用不足,某个未捕获的异常可能会导致整个程序的退出等等。因为 Node 中提供了 cluster 模块,cluster 实现了对 child_process 的封装,通过 fork 方法创建子进程的方式实现了多进程模型。比如我们最常用到的 pm2 就是其中最优秀的代表。
我们看一个 cluster 的 demo:
1 | const cluster = require('cluster'); |
这个时候看下活动监视器:
一共有 9 个进程,其中一个主进程,cpu 个数 * cpu 核数 = 2 * 4 = 8 个 子进程。
所以无论 child_process 还是 cluster,都不是多线程模型,而是多进程模型。虽然开发者意识到了单线程模型的问题,但是没有从根本上解决问题,而且提供了一个多进程的方式来模拟多线程。从前面的实验可以看出,虽然 Node (V8)本身是具有多线程的能力的,但是开发者并不能很好的利用这个能力,更多的是由 Node 底层提供的一些方式来使用多线程。Node 官方说:
You can use the built-in Node Worker Pool by developing a C++ addon. On older versions of Node, build your C++ addon using NAN, and on newer versions use N-API. node-webworker-threads offers a JavaScript-only way to access Node’s Worker Pool.
但是对于 JavaScript 开发者,一直没有一个标准的、好用的方式来使用 Node 的多线程能力。
直到 Node 10.5.0 的发布,官方才给出了一个实验性质的模块 worker_threads 给 Node 提供真正的多线程能力。
先看下简单的 demo:
1 | const { |
上述代码在主线程中开启五个子线程,并且主线程向子线程发送简单的消息。
由于 worker_thread 目前仍然处于实验阶段,所以启动时需要增加 --experimental-worker
flag,运行后观察活动监视器:
不多不少,正好多了五个子线程。😊
worker_thread 核心代码
worker_thread 模块中有 4 个对象和 2 个类。
threadId === 0
进行判断的。来看一个进程通信的例子:
1 | const assert = require('assert'); |
更多详细用法可以查看官方文档。
根据大学课本上的说法:“进程是资源分配的最小单位,线程是CPU调度的最小单位”,这句话应付考试就够了,但是在实际工作中,我们还是要根据需求合理选择。
下面对比一下多线程与多进程:
属性 | 多进程 | 多线程 | 比较 |
---|---|---|---|
数据 | 数据共享复杂,需要用IPC;数据是分开的,同步简单 | 因为共享进程数据,数据共享简单,同步复杂 | 各有千秋 |
CPU、内存 | 占用内存多,切换复杂,CPU利用率低 | 占用内存少,切换简单,CPU利用率高 | 多线程更好 |
销毁、切换 | 创建销毁、切换复杂,速度慢 | 创建销毁、切换简单,速度很快 | 多线程更好 |
coding | 编码简单、调试方便 | 编码、调试复杂 | 多进程更好 |
可靠性 | 进程独立运行,不会相互影响 | 线程同呼吸共命运 | 多进程更好 |
分布式 | 可用于多机多核分布式,易于扩展 | 只能用于多核分布式 | 多进程更好 |
上述比较仅表示一般情况,并不绝对。
work_thread 让 Node 有了真正的多线程能力,算是不小的进步。
]]>React Hooks 是 16.7.0-alpha 版本的新特性,安装即可享用。
React Hooks 是对 React function 组件的一种扩展,通过一些特殊的函数,让无状态组件拥有状态组件才拥有的能力。
Hooks 是 React 函数组件中的一类特殊函数,通常以 use 开头,比如 useRef,useState,useReducer 等。通常在我们写 React 组件的时候,如果这个组件比较复杂,拥有自己的生命周期或者 state,就将其写成 class 组件;如果这个组件仅仅用来展示,就将其写成 function 组件。
React Hooks 使用 function 组件的写法,通过 useState 这样的 API 解决了 function 组件没有 state 的问题,通过 useEffect 来解决生命周期的问题,通过自定义 hooks 来复用业务逻辑。
Hooks 主要分三种:
看一下官方给出的 demo
1 | import { useState } from 'react'; |
这里的 useState 就是一个 hook,返回一个数组,第一个 count 表示一个 state,默认值为 0;第二个 setCount 相当于 class function 中的 setState,表示对 count 的更新操作。
这样写的好处是每个 state 独立管理,避免状态复杂的时候 state 臃肿。
基本用法描述如下:
1 | const [state, setState] = useState(initialState); |
useState 返回一个数组,第一个值是一个 stateful(有状态)的值,第二个值是更新这个状态值的函数。在初始渲染的时候,返回的 state 与 initialState 相同,在后续重新渲染时,useState 返回的第一个值将始终是应用更新后的最新 state(状态) 。
setState 函数用于更新 state(状态) ,它接受一个新的 state(状态) 值,并将组件排入重新渲染的队列。
由于 setState 使用函数式的更新方式,所以可以传递函数给 setState,该函数将接收先前的值,并返回更新的值。
1 | function Counter({initialCount}) { |
上述代码可以使用上次的 state 来计算新的 state。与 React 类组件中的 setState 不同,useState 不会自动合并更新对象。所以如果要更新的 state 依赖前一个 state 的时候,需要使用对象扩展的方式:
1 | setState(prevState => { |
initialState 参数既可以是一个值,也可以是一个函数,如果初始状态是高开销的计算结果,则可以改为提供函数,该函数仅在初始渲染时执行:
1 | const [state, setState] = useState(() => { |
initialState 参数只有在初始渲染期间才会使用,在随后的渲染中,它会被忽略。
Effect Hooks 允许在组件中执行副作用(side effects),类似于类中的生命周期方法。通常我们需要在 componentDidMount 和 componentDidUpdate 写一些操作,可能是更新数据,也可能是更新 Dom。除此之外,我们还会在 componentWillUnmount 的时候解绑一些事件监听防止内存泄露。这些都导致了组件维护成本的增大。而在 function 组件中,又没有这些生命周期,因此 Hooks 使用 Effect Hooks 来取代这些生命周期,完成一部分能力。
看一下官方给出的动态更改 title 的 demo:
1 | import { useState, useEffect } from 'react'; |
在 useEffect 之前,我们需要在 componentDidMount 和 componentDidUpdate 中同时去调用更改 title 的方法,以完成组件初始化的状态和数据更新的状态。useEffect 传递一个函数给 React,React 在组件渲染完成后和更新后调用这个函数来完成上述功能。默认情况下,它在第一次渲染之后和每次更新之后都运行。
可以将 useEffect Hook 视为 componentDidMount,componentDidUpdate 和 componentWillUnmount 的组合。
那 useEffect 什么时候执行 componentWillUnmount 的操作呢?
如果 useEffect 中返回一个函数,在 React 卸载当前的组件的时候,会执行这个函数,用于清理 effect。
对比需要清理 effect 和不需要清理 effect 的两种写法:
1 | function FriendStatusWithCounter(props) { |
通过跳过 Effect 来优化性能。
通常,每次组件渲染或者更新都去执行某些逻辑会带来无谓的消耗,所以我们经常会写这样的代码:
1 | componentDidUpdate(prevProps, prevState) { |
只有组件更新前后的 state.count 发生变化的时候,才去更新 title。
用 Hooks 可以更简单地处理这个问题
1 | useEffect(() => { |
给 useEffect 传入第二个参数,这个参数是一个数组。如果组件重新渲染,只有这个 count 发生变化的时候 React 才会执行函数 中的内容,否则会直接跳过这个 effect。如果数组中是多个参数,那么只要其中一个发生变化,React 都会执行函数中的内容。
这也适用于具有清理阶段的 effect :
1 | useEffect(() => { |
如果希望 effect 只在组件 componentDidMount 和 componentWillUnmount 的时候执行,则只需要给第二个参数传一个空数组即可。传入一个空数组 [] 输入告诉 React 你的 effect 不依赖于组件中的任何值,因此该 effect 仅在 mount 时运行,并且在 unmount 时执行清理,从不在更新时运行。
React Hooks 其实不仅仅是功能层面的增强,也给 React 注入了新的软件思想。这就是最近几年开始流行的 “约定大于配置”,比如 Hooks 函数必须使用 use 开头,还有接下来要讲的规则。前面在我的文章 webpack4 新特性 也提到了这个内容。
Hooks 只能在顶层调用,不要在循环,条件或嵌套函数中调用 Hook。原因是 React 需要保证每次组件渲染的时候都以相同的顺序调用 Hooks。
假如一个组件中有多个 Hooks,React 如何知道哪个 state(状态) 对应于哪个 useState 调用呢?答案是 React 依赖于调用 Hooks 的顺序。本质上来说 Hooks 就是数组(React hooks: not magic, just arrays)。每次执行 useState 都会改变下标,如果 useState 被包裹在 condition 中,那每次执行的下标就可能对不上,导致 useState 更新错数据。
Hooks 只能在 React function 组件中调用,或者在自定义 Hooks 中调用。通过遵循此规则,可以确保组件中的所有 stateful (有状态)逻辑在其源代码中清晰可见。
eslint-plugin-react-hooks 可以保证强制执行上述两个规则。
1 | npm install eslint-plugin-react-hooks@next |
1 | // Your ESLint configuration |
自定义 Hooks 就是将组件之间需要共有的逻辑抽出来写成单独的函数。与一般的函数的区别是,自定义 Hooks 是一个以 use 开头的函数,内部可以调用其它的 Hooks。
1 | import { useState, useEffect } from 'react'; |
在另外一个组件中,将其引入后,就可以使用了
1 | import {useFriendStatus} from 'hooks/xxx.js'; |
可以看出,自定义 Hooks 就是一个 JavaScript 函数而已,并没有什么特别。不过需要注意的是,自定义 Hooks 函数也必须以 use 开头(规约优先)。
1 | const context = useContext(Context); |
接受一个 context(上下文)对象(从 React.createContext 返回的值)并返回当前 context 值,当提供程序更新时,此 Hook 将使用最新的 context 值触发重新渲染。
1 | const [state, dispatch] = useReducer(reducer, initialState); |
useReducer 可以理解为 Redux 的 Hooks,接受的第一个参数是 (state, action) => newState
的 reducer,并返回与 dispatch 方法配对的当前状态。
1 | const initialState = {count: 0}; |
useReducer 接受可选的第三个参数 initialAction,表示在组件初始化期间执行的操作。比如利用 props 传递的值来初始化 state 的操作。
1 |
|
1 | const refContainer = useRef(initialValue); |
useRef 返回一个可变的 ref 对象,通过 .current
属性对其进行访问,返回的对象将存留在整个组件的生命周期中。
1 | function TextInputWithFocusButton() { |
1 | useImperativeMethods(ref, createInstance, [inputs]); |
useImperativeMethods 与 forwardRef 共同使用,表示强制方法。通过 ref 将子组件的某个方法暴露给父组件。
子组件:
1 | function FancyInput(props, ref) { |
父组件:
1 |
|
用法与 useEffect 相同,但在所有 DOM 变化后同步触发。使用它来从 DOM 读取布局并同步重新渲染。 在浏览器绘制之前 useLayoutEffect 将同步刷新。
useEffect 中的函数会在 layout(布局) 和 paint(绘制) 后触发。 这使得它适用于许多常见的 side effects ,例如设置订阅和事件处理程序,因为大多数类型的工作不应阻止浏览器更新屏幕。
但是如果 effect 不能够推迟,比如要 DOM 改变必须在下一次绘制之前同步触发,使用 useLayoutEffect 会更加合适。
Hooks 通过设定某些特殊函数,在 React 组件内部“钩住”其生命周期和 state,帮助开发者解决一些逻辑复用的问题,通过自定义的 Hooks 对代码进行抽象,让我们写出更加符合函数式编程的规范,同时也减少了层层嵌套带来的问题。
Koa 是一个非常轻量的 web 开发框架,由 Express 团队打造。相较于 Express,Koa 使用 async 函数解决异步的问题,并且完全脱离中间件,非常优雅,而且 Koa 代码简洁友好,很适合初学者阅读。
可以看到 Koa 的结构非常简单,lib 文件夹下面放着 koa 的核心文件:
application 是 koa 的入口文件,export 出一个 Application 的类(继承自 events.Emitter)。application 有以下几个主要(public)的 api:
listen: 实现对 http.createServer() 的封装,传入的参数 callback 中完成中间件合并,错误监听以及上下文的创建和 request 的处理。
use: 我们通常使用 app.use(function) 将中间件添加到应用程序。use 方法中,koa 将中间件(函数)添加到 this.middleware 数组中。
callback: koa-compose 将中间件组合在一起, 然后返回一个 request 回调函数,同时给 listen 作为回调。
toJSON: 返回一个去除私有属性(_
开头)的对象。
1 | module.exports = class Application extends Emitter { |
context 是我们在使用 koa 中最常接触到的 ctx,就是一个暴露出来的对象。context 中实现了对 cookie 的 get set 操作,这也是我们可以直接使用 ctx 对 cookie 操作的原理。除此之外,ctx 中最重要的是 delegate,也就是委托。我们简单看一下代码:
1 | delegate(proto, 'response') |
以上的 proto 就是 ctx,实现了对 response 对象的代理,比如我们可以通过使用 ctx.status 来访问 ctx.response.status。
同样的,request 上面的属性和方法也被代理到了 ctx 中:
1 |
|
ctx.hostname 即是 ctx.request.hostname。
request.js 和 response.js 中完成对 Koa Request/Response 对象的封装,可以通过 request.xxx/response.xxx 对其进行操作。其中使用了很多 get 和 set 方法。
SRI 全称 Subresource Integrity - 子资源完整性,是指浏览器通过验证资源的完整性(通常从 CDN 获取)来判断其是否被篡改的安全特性。
通过给 link 标签或者 script 标签增加 integrity 属性即可开启 SRI 功能,比如:
1 | <script type="text/javascript" src="//s.url.cn/xxxx/xxx.js?_offline=1" integrity="sha256-mY9nzNMPPf8oL3CJss7THIEoXAC2ToW1tEX0NBhMvuw= sha384-ncIKElSEk2OR3YfjNLRSY35mzt0CUwrpNDVS//iD3dF9vxrWeZ7WPlAPJTqGkSai" crossorigin="anonymous"></script> |
integrity 值分成两个部分,第一部分指定哈希值的生成算法(sha256、sha384 及 sha512),第二部分是经过 base64 编码的实际哈希值,两者之间通过一个短横(-)分割。integrity 值可以包含多个由空格分隔的哈希值,只要文件匹配其中任意一个哈希值,就可以通过校验并加载该资源。上述例子中我使用了 sha256 和 sha384 两张 hash 方案。
备注:
crossorigin="anonymous"
的作用是引入跨域脚本,在 HTML5 中有一种方式可以获取到跨域脚本的错误信息,首先跨域脚本的服务器必须通过 Access-Controll-Allow-Origin 头信息允许当前域名可以获取错误信息,然后是当前域名的 script 标签也必须声明支持跨域,也就是 crossorigin 属性。link、img 等标签均支持跨域脚本。如果上述两个条件无法满足的话, 可以使用try catch
方案。
在 Web 开发中,使用 CDN 资源可以有效减少网络请求时间,但是使用 CDN 资源也存在一个问题,CDN 资源存在于第三方服务器,在安全性上并不完全可控。
CDN 劫持是一种非常难以定位的问题,首先劫持者会利用某种算法或者随机的方式进行劫持(狡猾大大滴),所以非常难以复现,很多用户出现后刷新页面就不再出现了。之前公司有同事做游戏的下载器就遇到这个问题,用户下载游戏后解压不能玩,后面通过文件逐一对比找到原因,原来是 CDN 劫持导致的。怎么解决的呢?听说是找 xx 交了保护费,后面也是利用文件 hash 的方式,想必原理上也是跟 SRI 相同的。
所幸的是,目前大多数的 CDN 劫持只是为了做一些夹带,比如通过 iframe 插入一些贴片广告,如果劫持者别有用心,比如 xss 注入之类的,还是非常危险的。
开启 SRI 能有效保证页面引用资源的完整性,避免恶意代码执行。
通过使用 webpack 的 html-webpack-plugin 和 webpack-subresource-integrity 可以生成包含 integrity 属性 script 标签。
1 | import SriPlugin from 'webpack-subresource-integrity'; |
那么当 script 或者 link 资源 SRI 校验失败的时候应该怎么做呢?
比较好的方式是通过 script 的 onerror 事件,当遇到 onerror 的时候重新 load 静态文件服务器之间的资源:
1 | <script type="text/javascript" src="//11.url.cn/aaa.js" |
loadjs:
1 | function loadjs (event) { |
这种方式的缺点是目前 onerror 中的 event 参数无法区分究竟是什么原因导致的错误,可能是资源不存在,也可能是 SRI 校验失败,不过目前来看,除非有统计需求,无差别对待并没有多大问题。
除此之外,我们还需要使用 script-ext-html-webpack-plugin 将 onerror 事件注入进去:
1 | const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin'); |
然后将 loadjs 和 loadSuccess 两个方法注入到 html 中,可以使用 inline 的方式。
还在知乎上看到一位大神另辟蹊径,通过 jsonp 的方式解决 CDN 劫持。个人感觉这种方式目前能够完美应对 CDN 劫持的主要原因是运营商通过文件名匹配的方式进行劫持,作者的方式就是通过 onerror 检测拦截,并且去掉资源文件的 js 后缀以应对 CDN 劫持。
]]>Kong 是一款基于 OpenResty 的 API 网关平台,在客户端和(微)服务之间转发 API 通信。Kong 通过插件的方式扩展自己的功能,其中包括身份验证、安全控制、流量控制、熔断机制、日志、黑名单、API 分发等等众多功能。下图是官网给出的传统项目架构和使用 Kong 的架构:
Next-Generation API Platform for Modern Architectures。
可以看到,使用 Kong 之后,内部服务开发者只需要 focus 具体业务的实现,网关层提供 API 分发、管理、维护等功能,开发者只需要简单的配置就可以把自己开发的服务发布出去,同时置于网关的保护之下。
OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。
Kong 可以安装运行在大部分 Linux 分布式平台和 macOS 上。全部安装方式请查看 安装 Kong 社区版。
(1) 安装 Kong
1 | $ brew tap kong/kong |
(2) 准备数据库
安装 PostgresSQL,在 Kong 启动之前指定数据库和用户。
1 | $ CREATE USER kong; CREATE DATABASE kong OWNER kong; |
由于对 Postgres 并不熟悉,我使用 GUI 工具 pgAdmin4 完成 User 和 Database 的创建。
(3) 准备 kong 配置文件
kong 默认使用 /etc/kong/kong.conf
作为启动的配置文件,因此我们在 /etc/kong/
目录下创建 kong.conf 文件,内容如下:
1 | database = postgres |
全部 kong 的配置文件你可以查看 kong.conf.default。
(4) 启动 kong
1 | $ kong migrations up |
这个时候 kong 就启动起来了。然后我们可以通过下面的命令测试:
1 | $ curl -i http://localhost:8001/ |
(5) 更多 kong 的命令
1 | $ kong check /etc/kong/kong.conf # 检验 kong 配置文件是否正确 |
(6) kong 启动后监听了 4 个端口
(1) 创建一个名为 kong-net 的 network
1 | $ docker network create kong-net |
(2) 启动数据库(PostgreSQL)
1 | $ docker run -d --name kong-database \ |
这个时候命令行会显示 Unable to find image 'postgres:9.6' locally
,然后会自动帮我买下载 postgres 的 image。
(3) 准备数据库
1 | $ docker run --rm \ |
(4) 启动 Kong
1 | $ docker run -d --name kong \ |
(5) 使用 Kong
1 | $ curl -i http://localhost:8001/ |
更详细的内容可以查看 5 分钟快速开始
Kong dashboard 是一个基于 node 实现的管理 Kong 网关设置的 GUI 工具。
使用 npm:
1 | # Install Kong Dashboard |
使用 Docker:
1 | # Start Kong Dashboard |
本质上 Kong 是作用于请求和响应之间的一层代理,我们可以通过 RESTful 的形式管理 API。
使用 curl 命令行:
1 | $ curl -i -X POST \ |
或者使用 kong-dashboard
,在 http://localhost:8080/#!/apis 编辑查看:
这时,Kong 已经做好了对 HOST 是 example.com 的 api 的代理请求,并且将其代理到 https://lz5z.com 上。
$ curl -i -X GET \ --url http://localhost:8000/ \ --header 'Host: example.com'
以上只是 Kong 简单的安装和工具的使用,由于之前对 docker、PostgresSQL 等周边工具并不熟悉,所以学习起来需要扩展的东西比较多,暂时先写到这里吧。关于 Kong 插件的使用已经编写,用户操作、授权、负载均衡、熔断等信息,这里先埋坑,后面有时间再补上吧。
]]>在手机端网页开发过程中,我们经常会遇到滚动点停误触的问题,最开始想到的解决办法就是判断当前页面(DOM)是否在滚动,如果在滚动,就取消点击或者其他事件。但是在判断页面是否在滚动的时候出现了一些问题,最常见的就 uiwebview scroll 事件延迟,导致我们无法准确判断当前页面(DOM)是否还在滚动。于是想到了使用 requestAnimationFrame 判断某个元素的位置是否发生变化来标识当前页面(DOM)是否在滚动。
这是移动端的前端开发中实际遇到的一个问题,当我们的页面出现滚动条的时候,用手滑动屏幕,屏幕上页面内容会快速滚动,不会因为手已经离开了屏幕而滚动停止。当我们想要停止滚动的时候,轻轻点击屏幕,让屏幕停止。但是这个时候有个问题,如果屏幕上点击的位置恰好可以点击,这个时候就会误触。还有一种常见的情况是,滚动已经停止了,点击屏幕发生在其之后,但是感觉像是发生了误触。
最先想到的解决办法当然是加锁,当页面在滚动的时候,就禁止元素的点击或者 touch 事件。但是这里存在一个问题,有些情况下,我们并不能正确的获得当前页面是否正在发生滚动。比如在 iOS UIWebViews 中, 在视图的滚动过程中,scroll 事件不会被触发;在滚动结束后,scroll 才会触发,参见 Bootstrap issue #16202 。不能正确获取 scroll 事件就无法正确判断当前页面是否正在滚动。看起来我们陷入了僵局。
我们放弃 scroll 事件,使用别的方式判断页面是否滚动。最先想到的就是通过获取某个元素的相对位置,如果在两帧之内位置没有发生变化,那不就证明了当前页面已经不滚动了吗。
我们首先给 window 上绑定 touch 事件:
1 | window.addEventListener('touchmove', this.onWindowTouchMove.bind(this)) |
如果发生 touchmove,就认为用户滑动了,在 touchend 的时候通过 getBoundingClientRect() 获取元素位置,再使用 requestAnimationFrame() 判断在两帧之间元素的位置是否发生变化,以此来标识页面滚动是否停止。
1 | let element = e.target |
完整代码 scrolling-observer:
包已经发布在 npm 上了,可以 npm 或者 yarn 使用:
1 | $ npm install scrolling-observer --save |
使用方式:
1 | import scroll from 'scrolling-observer' |
需要使用 ssr 的同学请注意不要在 node 端初始化,因为构造函数中使用了 window 对象。
简单通过判断两帧之间元素的相对位置是否发生变化来判断页面是否正在滚动。使用 requestAnimationFrame 并且只在 touchend 后触发检查机制,对页面性能也不会造成太大的影响。目前来看是不错的解决方案。
]]>关于 webpack 入门的文章可以参考 webpack 从入门到放弃。
关于 webpack 性能优化的内容可以参考 webpack 打包优化。
关于 webpack4 全部新的特性可以查看官方的 releases。
学习一项新知识最好能站在巨人的肩膀上,其中 angular-cli、create-react-app 和 vue-cli 中对 webpack4 中的使用都是我们学习和模仿的对象。
使用 npx 创建 react-demo,创建之后 npm run eject
就可以看到它详细的 webpack 配置了。
1 | $ npx create-react-app react-demo |
不过比较遗憾的是正式版本的 create-react-app 暂时还不支持 webpack4,我们可以使用 react-scripts@2.0.0-next.3e165448
来体验 webpack4 的特性。
1 | $ # Create a new application |
其中 config 目录下的与 webpack 相关的三个文件是非常好的学习和借鉴的对象,可以说适应于绝大多数中小型项目。
Vue CLI3 简直可以说是学习和使用 vue 中一个无敌的存在,其中 @vue/cli-service 中集成了 webpack 的默认配置,带来开箱即用的快感;不过 Vue CLI 没有像 angular-cli 和 create-react-app 那样提供 eject 命令,而是通过 vue.config.js 进行包括 webpack 在内的全局配置。其可视化工具 vue ui 中的 inspect 可以查看 webpack 参数,非常强大。
Vue CLI3 内部的 webpack 配置是通过 webpack-chain 维护的,这个库提供了一个 webpack 原始配置的上层抽象,使其可以定义具名的 loader 规则和具名插件,并有机会在后期进入这些规则并对它们的选项进行修改。
如果你的项目也有链式访问特定的 loader 的需求的话,不妨参考一下 Vue CLI3。
如果不希望使用 webpack-chain 的话,可以参考其它比较成熟的 vue 项目,比如 vue-element-admin 也非常具有借鉴意义。
npm outdated
查看与 webpack 相关的 loader 和 plugin 是否需要升级。thrownewError('Cyclic dependency'+nodeRep)
的错误的话,可以使用 Alpha 版本 npm i--save-dev html-webpack-plugin@next
。由于 webpack4 以后对 css 模块支持的逐步完善和 commonChunk 插件的移除,在处理 css 文件提取的计算方式上也做了些调整。所以之前一直使用的 extract-text-webpack-plugin 也完成了它的历史使命,将让位于 mini-css-extract-plugin。
extract-text-webpack-plugin 会将 css 内联在 js 中,这样带来的问题是:css 或者 js 的改动都会影响整个 bundle 的缓存。而 mini-css-extract-plugin 在 code Splitting 的时候会将原先内联写在每一个 js chunk bundle 的 css,单独拆成了一个个 css 文件。然后再通过 optimize-css-assets-webpack-plugin 这个插件对 css 进行压缩和优化。
备注:optimize-css-assets-webpack-plugin 默认使用 cssnano 进行 css 代码优化,但是也会导致一些问题,比如我之前遇到的 z-index 重新计算的问题和 keyframes 重命名的问题:解决 webpack 打包后 z-index 重新计算的问题。
可能是受到 parcel(一款号称快速,零配置的 Web 应用程序打包器)的影响,webpack4 也引入了零配置的概念,遵从软件行业更先进的『规约大于配置』的理念。
mode 有三个值:
选项 | 描述 |
---|---|
development | 会将 process.env.NODE_ENV 的值设为 development。启用 NamedChunksPlugin 和 NamedModulesPlugin。 |
production | 会将 process.env.NODE_ENV 的值设为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin。 |
none | 不选用任何默认优化选项 |
(1)可以在启动命令后加入参数使用:
1 | "scripts": { |
(2)也可以在配置文件中加入 mode 属性:
1 | // webpack.development.config.js |
development 模式默认开启了 NamedChunksPlugin 和 NamedModulesPlugin 方便调试,提供了更完整的错误信息,更快的重新编译的速度。
1 | // webpack.production.config.js |
production 模式提供代码压缩和代码分割,同时 webpack 也会自动进行 Scopehoisting 和 Tree-shaking。
可以看出 mode 本质上是提供了一些默认的配置,以此来简化 webpack 的使用门槛。
optimization 是 webpack4 中最大的改进,其中包括代码压缩,分割,优化等功能。
webpack4 移除 CommonsChunkPlugin,取而代之的是两个新的配置项(optimization.splitChunks 和 optimization.runtimeChunk)来进行分包。
我们来看下 create-react-app 生成的关于分包的配置:
1 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); |
在分包功能上主要使用 splitChunks 和 runtimeChunk 两个参数。
默认情况下 splitChunks 的配置就适用于大多数用户。webpack4 将会按照以下规则自动进行分包:
为了满足后面两个条件,webpack 有可能受限于包的最大数量值,生成的代码体积往上增加。
默认配置对应的参数如下:
1 | optimization: { |
(1) splitChunks.chunks
表示哪些 chunks 会被分割,可以提供字符串或者 function 作为参数。如果传字符串的话,值可以是 “all”、“async”、“initial”。“all” 表示无论 chunk 是 async 还是 non-async 都可以被共享。
(2) splitChunks.cacheGroups
默认模式会将所有来自 node_modules 的模块分配到 一个叫 vendors 的缓存组;所有重复引用至少两次的代码,会被分配到 default 的缓存组。
一个模块可以被分配到多个缓存组,优化策略会将模块分配至跟高优先级别(priority)的缓存组,或者会分配至可以形成更大体积代码块的组里。
默认来说,缓存组会继承 splitChunks 的配置。所有上面列出的选择都是可以用在缓存组里的:chunks, minSize, minChunks, maxAsyncRequests, maxInitialRequests, name。
可以通过 optimization.splitChunks.cacheGroups.default: false
禁用 default 缓存组。
可以使用如下的方式提取公共代码:
1 | cacheGroups: { |
(3) minSize: 形成一个新代码块最小的体积,默认是 30 kb。
(4) minChunks: 在分割之前,这个代码块最小应该被引用的次数(保证代码块复用性,默认值为 1 ,即不需要多次引用也可以被分割)。
(5) maxInitialRequests: 一个入口最大的并行请求数,默认是 3。
(6) maxAsyncRequests: 按需加载时候最大的并行请求数,默认是 5。
(7) name: 要控制代码块的命名,可以用 name 参数来配置,当不同分割代码块被赋予相同名称时候,他们会被合并在一起。如果赋予一个神奇的值 true,webpack 会基于代码块和缓存组的 key 自动选择一个名称。
webpack4 提供了 runtimeChunk 能让我们方便的提取 manifest,以前我们需要这样配置
1 | new webpack.optimize.CommonsChunkPlugin({ |
webpack4 中则只需要
1 | runtimeChunk: true, |
通过 optimization.runtimeChunk: true
选项,webpack 会添加一个只包含运行时(runtime)额外代码块到每一个入口。
这个需要看场景使用,会导致每个入口都加载多一份运行时代码。其实打包生成的 runtime.js 非常的小,gzip 之后一般只有几 kb,但这个文件又经常会改变,导致我们每次都需要重新请求它,它的 http 耗时远大于它的执行时间了,所以建议不要将它单独拆包,而是将它内联到 index.html 之中。
1 | const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin') // 支持 prefetch preload |
在使用 webpack 构建的应用程序中,主要包含三种类型的代码:
runtime 以及伴随的 manifest 数据,主要是指:在浏览器运行时,webpack 用来连接模块化的应用程序的所有代码。
(1)runtime
在模块交互时,连接模块所需的加载和解析逻辑。包括浏览器中的已加载模块的连接,以及懒加载模块的执行逻辑。
(2)manifest
当编译器(compiler)开始执行、解析和映射应用程序时,它会保留所有模块的摘要信息。这个摘要的数据集合称为 “Manifest”,当完成打包并发送到浏览器时,在运行时通过 Manifest 来解析和加载模块。
无论选择哪种模块语法,那些 import 或 require 语句现在都已经转换为 __webpack_require__
方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够查询模块标识符,检索出背后对应的模块。
可以理解为在应用程序运行时,编译器通过 manifest 中的数据来查找相应的模块,管理模块的加载和执行。
根据业务的复杂程度,一般在我们的代码中存在以下几种类型的代码:
基础组件库:react/vue; redux/vuex/mobx; react-router/vue-router; axios;
UI 组件库:Ant Design/Element;
必要组件/公共组件:Nav; Footer; Header; 全局配置等
非必要组件/代码:自己封装的组件和函数
低频组件:富文本; Markdown-Editor; Echarts 等
业务代码:业务组件; 业务模块; 业务页面等
它是构成我们项目必不可少的一些基础类库,比如 vue 全家桶或者 reat 全家桶,它们的升级频率都不高,但每个页面都需要它们,还有一些全局被共用的,体积不大的第三方库也可以放在其中:比如 nprogress、js-cookie、clipboard 等。
也可以使用 webpack 的 dll 技术将这些代码抽取为动态链接库。
可以考虑将 UI 组件库也打包在 libs 中,不过相比于 chunk-libs,它的升级频率更高,并且体积更大,因此单独打包是更好的选择。
自定义组件可以选择单独打包成 bundle,也可以与业务代码打包在一起,还是要结合具体情况来看。
低频组件和 chunk-commons 最大的区别是,它们只会在一些特定业务场景下使用,比如富文本编辑器、js-xlsx 等。webpack4 会根据这些库的大小(30kb)选择将其打包成独立的 bundle 或者 直接打包到具体的页面 bundle 中。
一般按照页面来划分打包。
webpack 插件是一个具有 apply 方法的 JavaScript 对象。apply 属性会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问。
1 | const pluginName = 'BasicPlugin' |
使用这个插件
1 | const BasicPlugin = require('./BasicPlugin.js') |
webpack 基于插件的运行模式非常强大,也是其能够迅速占领市场,社区活跃的主要原因。如果把 webpack 比作流水线,插件就是流水线上一个个工人。webpack 通过 Tapable 来组织这条复杂的流水线。
webpack 在运行过程中会广播事件,每个插件只需要监听它所关心的事件,就能加入到这条生产线中,从而改变生产线的运作。webpack 中基于观察者模式的事件流机制保证了其运行的有序性。
插件的核心是两个继承于 Tapable 的对象: Compiler 和 Compilation,它们是连接插件与 webpack 之间的桥梁。在插件代码的编写中,只要拿到了这两个对象,就可以实现广播和监听事件。
Compiler 和 Compilation 的区别在于:Compiler 代表了整个 webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。
webpack4 插件的编写方式与之前发生了变化,主要表现在 Compiler 和 Compilation 中事件监听和广播的表现形式。
webpack3:
1 | /** |
webpack4:
html-webpack-plugin 中在 compilation.hooks 上添加了 htmlWebpackPluginBeforeHtmlGeneration 对象:
来看下 html-webpack-include-assets-plugin 的兼容写法。
在金山的最后一天了,从昨天早上就开始了划水摸鱼的工作模式,主要是把项目的代码结构给几位同事讲一下,还有一些比较容易让人困惑的点,才发现我对项目是如此熟悉,大部分代码如数家珍。
从 2017 年 2 月 14 号情人节入职到明天 2018 年 8 月 24 正式离职,差不多在这里呆了一年半的时间,自己的进步自己看得到,周围的同事也看得到。老大曾经对我说,感觉入职的时候对我的评级评低了。听到以后很开心,可能不是评低了,是我确实进步了不少。跟去年要离开 OOCL 的时候感觉不同,那个时候正好赶上过年并且自己做的项目没有东西可以做,所以非常轻松,走之前差不多很长一段时间都在摸鱼。而这次从金山离职,从提出离职申请到昨天,基本上每天都有事情要做,赶需求,改 Bug,招新人,讲代码,交接功能,一直忙个不停。可见自己的重要性提高了,所以非常谨慎地又将一些可能留下的问题改了又改,争取不给后面的同事埋坑。
当然是为了多赚钱啦。说什么个人提升,开阔视野,年轻就要多吃苦之类的,本质上还是为了当前或者以后多赚钱。以三分兴趣,七分为了钱去奋斗,我觉得 。当然跳槽也是有成本的,现在已经快要九月份了,跳槽相当于亏了几个月的年终奖,而且去深圳还要租房,每周往返深圳珠海还要不菲的路费,这些都是代价。粗略一算,可能未来几个月会过得更穷。再加上今年投资亏了一些,P2P 暴雷亏了一些,内推奖金没拿到,我感觉今年我就是跟钱过不去 😢。
虽然在别的厂能拿到更好的待遇,但是选择腾讯一个是离家近,一个是大厂光环。所以就算是去镀金了吧,希望以后能有更好的发展。
金山给了我很多的成长,因此内心对其还是非常感激的,唯一觉得公司有点亏待我的就是给的钱有点少,涨薪也没有谈判余地,等了很久给了我一个数字,内心当然是不满意的。因为那个时候已经有猎头联系并且开出接近 double 的待遇了,所以差不多知道涨薪后就有离开的意向了。但是当时我们组有新的产品即将上线,大家都处于加班加点的状态,个人觉得那个时候离开有些不厚道,就拖延了几个月。
到五六月份开始准备,陆陆续续面了头条,阿里,腾讯。头条是一面挂,后面就没有消息了;阿里非常顺利,一到三面都过了,后来栽到 HR 面,各种原因不说了。腾讯也是一面挂,但是没过多久又有电话打来,换个部门接着面,那个时候差不多已经算是拿到阿里 offer 了,想着反正面面也不吃亏,没想到这个却成了最终的归宿。腾讯的部门是 SNG,前前后后总共面了 7 面,简直是体力劳动,每次半小时到一小时不等,聊完都是口干舌燥、面红耳赤。中间又穿插了一个阿里的部门继续面。。。终于到了 7 月 24 号,拿到了腾讯 SNG 的 offer,没怎么犹豫就接受了。收到腾讯 offer 一周多以后,又接到阿里的电话,总监面。。。(这个效率有点低啊)
今日头条是第一次面试,而且只面了一面,视频面试加在线笔试(别人看着你写代码),因为表现不好,差不多面完就知道没戏了。今日头条比较好的是,面试前有声音很好听的 hr 小妹妹打电话提前预约,预约后有邮件通知,面试挂了还有婉拒的邮件。这点比很多大厂都要友好一些。
腾讯的面试很细节,个人感觉是需要提前查漏补缺的,从 api 到设计到原理还有算法都有涉猎。面 alloyteam 的时候,面试官的问题种种都是 “你觉得 a 比 b 相比怎么样”,“使用 a 技术开发某某可能会遇到什么问题”,“某某某为什么选择这样的设计” 这样很开放性的问题,感觉自己回答的都不是很好。后面通过同事咨询面试官,得到的答复是他还在考虑,然后就没有然后了。毕竟是腾讯的明星组,想进入还是有难度的。
阿里的面试差异化比较大,没有明确的风格,每个面试官都不同,有一个面试官聊完技术以后又跟我一起聊了大半个小时中国互联网环境(估计有创业的想法),还有一个面试官从头到尾没问技术问题,一直问我目前做什么,平时怎么学习,项目中遇到了哪些问题,后面又要了我的博客。当然大部分面试官还是常规性地问一些技术问题,不过我能够明显的感受到阿里的面试氛围更加活泼一些。还有就是阿里的 HR 给了我较为不愉快的体验,这里不多说了。
具体的面试问题我觉得没有啥参考性,就不贴了。
遗憾的事有几件,一个是之前想重构一下当前项目的打包,升级一下 webpack4 的,一直没有时间,希望后面的小伙伴能够再接再厉帮我完成心愿。一个是现在的前端负责人技术很好,感觉在他身上还有一些东西可以学习。还有一个是要离开珠海安逸的环境去深圳奋斗了,珠海真是太适合生活了,环境优雅,人人都很随和,交通也没有那么拥挤,而深圳会给人无形的压力。
虽然最近腾讯股价大跌,但是对我们底层员工来说应该是没有影响的吧。早就知道腾讯会有比较苦逼的加班,所以内心也并不是很担心。目前在金山我也会经常性的留下来加班,有时候是做项目,有时候是留下来学习,所以对加班也没有那么排斥。对要进的部门其实也没有太多的了解,后面熟悉了以后再说吧。
还是像以前一样,希望技术快快提升,money 快快来。
]]>Symbol.iterator
属性和 Symbol.asyncIterator
属性为数据提供 for...of
和 for...await...of
访问机制外,它还有什么功能呢?或者说,ES6 中增加 Symbol 数据类型主要面对什么场景呢?Symbol() 函数返回 symbol 类型的值,该类型具有静态属性和静态方法,并且不支持 new Symbol()
语法。每个从 Symbol() 函数中返回的 symbol 值都是唯一的。一个 symbol 值能作为对象属性的标识符,这是该数据类型最大的目的。
我们可以直接使用 Symbol() 函数创建 symbol 类型,并且用一个字符串作为其描述,每次都会创建一个新的 symbol 类型。
1 | let a = Symbol() |
Symbol() 函数不能使用 new 操作符。因为 JavaScript 中 new 操作符用来创建对象,Symbol 生成的是一个原始类型的值,并不是对象。通过原始数据类型创建一个显式包装器对象的方式从 ECMAScript 6 开始不再被支持。 然而现有的原始包装器对象,如 new Boolean()
、new String()
以及 new Number()
因为遗留原因仍可被创建。
1 | new Symbol() // TypeError: Symbol is not a constructor at new Symbol |
Symbol 可以接收字符串或者对象作为参数,如果参数是对象的话,Symbol 会调用该对象的 toString() 方法,将其转换为字符串,再生成 symbol 值。
1 | let a = { |
Symbol 值不能与其它数据类型的值进行运算,但是 Symbol 值可以显式转换为字符串或者 Boolean,其它类型的转换都会报 TypeError 错误。
1 | let a = Symbol('World') |
每一个 Symbol 函数生成的值都不相等,因此 Symbol 可以作为标识符,当做对象属性名,这样就可以保证不会出现相同的属性名。这可以有效避免属性被覆盖。
1 | let a = Symbol() |
注意,Symbol 值作为对象属性名时,不能用点运算符,因为点运算符后面是字符串,而 symbol 值并不是字符串。
1 | let a = Symbol() |
在对象内部使用 Symbol 的时候,必须放在方括号中。
1 | let a = Symbol() |
1 | Symbol.length // 0 |
返回对象默认迭代器方法,使用 for...of
进行迭代。
1 | const iterable = { |
返回对象默认的异步迭代器的方法,使用 for await of
进行迭代。
1 | const myAsyncIterator = { |
指定了匹配的是正则表达式而不是字符串。String.prototype.match()
方法会调用此函数。此函数还用于标识对象是否具有正则表达式的行为。比如: String.prototype.startsWith()
,String.prototype.endsWith()
和 String.prototype.includes()
这些方法会检查其第一个参数是否是正则表达式,是正则表达式就抛出一个 TypeError。现在,如果 match symbol 设置为 false(或者一个 假值),就表示该对象不打算用作正则表达式对象。
1 | var re = /foo/ |
Symbol.replace,Symbol.search、Symbol.split 使用方法都与 Symbol.match 比较类似,这里就不赘述了。
用于判断某对象是否为某构造器的实例,因此你可以用它自定义 instanceof 操作符在某个类上的行为。
1 | class MyArray { |
用于配置某对象作为 Array.prototype.concat()
方法的参数时是否展开其数组元素。
对于数组对象,默认情况下,用于 concat 时,会按数组元素展开然后进行连接(数组元素作为新数组的元素)。
1 | var arr1 = ['a', 'b', 'c'] |
重置 Symbol.isConcatSpreadable
可以改变默认行为。
1 | var arr1 = ['a', 'b', 'c'] |
对于类似数组的对象,默认是不展开的,如果期望使用 concat 时,展开其元素用于连接,重置 Symbol.isConcatSpreadable
为 true。
1 | var arr1 = [1, 2, 3] |
使用给定的 key 搜索现有的 symbol,如果找到则返回该 symbol。否则将使用给定的 key 在全局 symbol 注册表中创建一个新的 symbol。
1 | Symbol.for('foo') // 创建一个 symbol 并放入 symbol 注册表中,键为 'foo' |
用来获取 symbol 注册表中与某个 symbol 关联的键。
1 | // 创建一个 symbol 并放入 Symbol 注册表,key 为 'foo' |
Transfer-Encoding (传输编码) 是常见的 HTTP 头 字段,表示将实体安全传递给用户所采用的编码形式。与另外一个更为常见的 Content-Encoding 不同,Content-Encoding 表示内容编码,通常用于对实体内容进行压缩编码,比如 gzip,deflate 等。而 Transfer-Encoding 不会减少实体内容传输大小,但是会改变实体传输的形式。Content-Encoding 和 Transfer-Encoding 二者是相辅相成的,对于一个 HTTP 报文,很可能同时进行了内容编码和传输编码。
在 HTTP 请求头中,Transfer-Encoding 被称为 TE,表示浏览器预期接受的传输编码方式,可使用 Response 头 Transfer-Encoding 字段中的值,比如 chunked;另外还可用 trailers 这个值来表明浏览器希望在最后一个大小为 0 的块之后还接收到一些额外的字段。
HTTP/1.0 后期引入长连接的概念,通过 Connection: keep-alive
实现,服务端和客户端通过这个头部告诉对方发送完数据后不需要断开 TCP 连接,后面可以继续使用。HTTP/1.1 则将其变为默认规则,只要不发送 Connection: close
,所有的连接均保持为长连接。
持久链接需要服务器在开始发送消息体前发送 Content-Length 消息头字段,但是对于动态生成的内容来说,在内容创建完之前是不可知的。在 HTTP 协议中的 Transfer-Encoding 这篇文章中,作者举了两个例子来阐述长连接存在的问题。使用 node 创建 server。
1 | require('net').createServer(function(sock) { |
使用 sock.destroy()
,则每次发送完请求后,就关闭 TCP 连接,假如去掉 sock.destroy()
,服务变成长连接,但是请求的状态一直在 pending,因此浏览器无法确认数据是否传输完成,只能一直等待。通过设置 Content-Length
来解决这个问题。
1 | require('net').createServer(function(sock) { |
这样浏览器能正常接收响应数据,通过 Content-Length
判断实体已经结束,但是如果 Content-Length
计算错误会导致数据异常,并且对于动态生成的内容来说,在内容创建完之前其长度是不可知的。
Transfer-Encoding 的出现正是为了解决这个问题。如果一个 HTTP 消息(请求消息或应答消息)的 Transfer-Encoding 消息头的值为 chunked,那么,消息体由数量未定的块组成,并以最后一个大小为 0 的块为结束。分块传输编码只在 HTTP/1.1 中提供。
使用方式也很简单,在响应头部加上 Transfer-Encoding: chunked
后,就表示这个报文采用分块编码。每一个非空的块都以该块包含数据的字节数(字节数以十六进制表示)开始,跟随一个 CRLF (回车及换行),然后是数据本身,最后以一个大小为 0 的块 + CRLF 结束。
1 | require('net').createServer(function(sock) { |
用浏览器访问 http://localhost:8080/
可以看到 “This is the data in the first chunk and this is the second one consequence”。
Transfer-Encoding: chunked
:数据以一系列分块的形式进行发送。 Content-Length 首部在这种情况下不被发送。Transfer-Encoding: compress
:采用 Lempel-Ziv-Welch (LZW) 压缩算法,这种内容编码方式已经被大部分浏览器弃用。Transfer-Encoding: deflate
:采用 zlib 结构 (在 RFC 1950 中规定),和 deflate 压缩算法(在 RFC 1951 中规定)。Transfer-Encoding: gzip
:表示采用 Lempel-Ziv coding (LZ77) 压缩算法,以及 32 位 CRC 校验的编码方式。这个编码方式最初由 UNIX 平台上的 gzip 程序采用。Transfer-Encoding: identity
:用于指代自身(例如:未经过压缩和修改)。除非特别指明,这个标记始终可以被接受。301 Moved Permanently,永久重定向。被请求资源已永久移动到新位置,并且将来任何对该资源的引用都使用本响应返回的若干个 URI 之一。301 资源除非额外指定,否则都是可缓存的。
注意:对于某些使用 HTTP/1.0 协议的浏览器,当它们发送的 POST 请求得到了一个 301 响应的话,接下来的重定向请求将会变成 GET 方式。
302 Found 表示临时重定向 Moved Temporarily。由于这样的重定向是临时的,客户端应继续向原有地址发送以后的请求,只有在 Cache-Control 或 Expires 中进行了指定的情况下,这个响应才是可缓存的。
注意:虽然 RFC1945 和 RFC 2068 规范不允许客户端在重定向时改变请求的方法,但是很多现存的浏览器将 302 响应视作为 303 响应,并且使用 GET 方式访问在 Location 中规定的 URI,而无视原先请求的方法。因此状态码 303 和 307 被添加了进来,用以明确服务器期待客户端进行何种反应。
来看一个常见的 301 状态码的演示。访问本网页的时候,由于使用 https 协议,并且设置 http 自动重定向到 https,所以假如直接使用 http 协议http://lz5z.com进行访问,会有一次 301 重定向。
浏览器获得响应结果后,根据 Location 中的值进行重定向,打开页面 https://lz5z.com。
我们常用的短链接就是 302 跳转,比如我使用 sina 的短链接服务生成本页面的地址: http://t.cn/RdC6GHq。对其进行访问的时候就首先发生了 302 重定向。
由于 301 重定向是永久的重定向,搜索引擎在抓取新内容的同时也将旧的网址替换为重定向之后的网址。302 重定向是临时的重定向,搜索引擎会抓取新的内容而保留旧的网址。因为服务器返回 302 代码,搜索引擎认为新的网址只是暂时的。
所以 301 是对搜索引擎更加友好的重定向,建议只要不是资源临时转移,都可以使用 301 的方式。
关于 ES7/8/9 全部特性可以查看 tc39 官方的 proposals,这些都是最后进入 stage 4 的特性。
ES9 的新特性:
s
(dotAll) flag for regular expressions (正则表达式 dotAll 模式)dotAll 是一个新的正则表达式修饰符,目前 JS 拥有的修饰符有:
正则表达式中的 .
用来匹配任何单个字符,但是有 2 个除外:多字节 emoji 字符和行终结符。
1 | let regex = /^.$/ |
通过设置 u 表示 unicode
1 | let regex = /^.$/u |
行终止符包括
还有一些其它字符,也可以作为一行的开始:
目前 .
只能匹配其中的一部分:
1 | let regex = /./ |
标记 s
表示 dotAll,用来改变 .
不能匹配行终止符的行为:
1 | /hello.world/.test('hello\nworld') // false |
或者用 \s
来匹配空白符:
1 | /hello.world/.test('hello\nworld') // false |
dotAll 表示 .
可以匹配任意字符:
1 | const re = /hello.world/s // 等价于 const re = new RegExp('hello.world', 's') |
捕获组就是把正则表达式中匹配到的内容,保存到内存中以数字编号或者显式命名的数组里,方便后面使用。这种引用既可以在正则表达式内部,也可以是在正则表达式外部。
捕获组有两种形式,一种是普通捕获组,另一种是命名捕获组。
1 | const regex = /(\d{4})-(\d{2})-(\d{2})/ |
使用数字捕获组的一个缺点是对于引用不太直观,以上面的例子,我们很难分清楚哪个组代表的是年,哪个组代表的是月。而命名捕获组就是为了解决这个问题。
ES2018 允许命名捕获组可以使用 (?<name>...)
语法给每个组起一个名字。
1 | const regex = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/ |
每个捕获组的名字必须唯一,否则会抛出异常。
1 | const regex = /(?<foo>\d)-(?<foo>\d)/ |
任何匹配失败的命名组都将返回 undefined。
1 | let re = /^(?<optional>\d+)?$/ |
1 | let re = /^(?<one>.*):(?<two>.*)$/ |
1 | const reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/ |
String.prototype.replace
第 2 个参数可以接受一个函数。这时 命名捕获组的引用会作为 groups 参数传递进去:
1 | let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/ |
当需要在正则表达式里面引用命名捕获组时,使用 \k<name>
语法。
1 | let duplicate = /^(?<half>.*).\k<half>$/ |
/(?<name>)/
和 /\k<foo>/
只有在命名捕获组中才有意义。如果正则表达式没有命名捕获组,那么 /\k<foo>/
仅仅是字符串字面量 “k
1 | /\k<foo>/.test('k<foo>') // true |
断言 (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)当前位置后面的字符串应该满足断言,但是并不捕获,在当前的 JavaScript 正则表达式只支持正向断言。
1 | const regex = /li(?=zhen)/ |
正向否定断言正好相反
1 | const regex = /li(?!zhen)/ |
反向断言和正向断言的行为一样,只是方向相反。反向肯定断言使用语法 (?<=...)
。
比如我们想获取所有的人民币金额,但是不获取其它货币(比如美元):
1 | const regex = /(?<=\D)\d+(\.\d*)?/ |
Unicode 标准为每个符号分配各种属性和属性值,比如希腊字母 π
在 Unicode 中有独特的属性和属性值。目前版本的 ECMAScript 中正则表达式是无法匹配这些 Unicode 的,通常开发人员有两种选择。
(1) 在运行时使用类似于 xregexp 这样的库创建增强的正则表达式:
1 | const regexGreekSymbol = XRegExp('\\p{Greek}', 'A') |
缺点是 xregexp 是一个运行时依赖,对性能要求较高的 web 应用来说不是很理想。而且其压缩文件 xregexp-all-min.js.gz
也有 35k,并且每当 Unicode 标准更新时,必须要更新 xregexp 才能使用新数据。
(2) 在编译时的时候使用 regenerate 生成正则表达式。
1 | const regenerate = require('regenerate') |
虽然这种方法所生成的正则表达式相当大,但是能够得到最佳的运行时性能。最大的缺点是它需要一个构建脚本,每当 Unicode 标准更新时,必须更新生成脚本。
ES2018 中使用 \p{…}
和 \P{…}
进行 Unicode 的属性转义,在正则表达式中使用 u
进行标记。在 \p{…}
内,可以以键值对的方式设置需要匹配的属性,而非具体内容。比如要匹配希腊字母 π
:
1 | const reGreekSymbol = /\p{Script=Greek}/u |
解决了以下几个问题:
ECMAScript 6 中增加了数组的 Rest 解构赋值和 Spread 语法,比如:
1 | var a, b, rest |
1 | function sum(x, y, z) { |
ES2018 中增加了对象的 Rest 属性和 Spread 语法。
1 | let {x, y, ...z} = {x:1, y:2, a:3, b:4} |
1 | let n = {x, y, ...z} |
Promise.prototype.finally 早就有很多实现,以至于我一直都认为它是原生对象的原型属性。常见的实现有:
1 | Promise.resolve(2).finally(() => {}) // will be resolved with 2 |
关于 JavaScript 的异步循环,我在之前的文章JavaScript 循环与异步有过探索。如今 ECMAScript 中有了对异步迭代的原生支持。
ES6 中引入迭代器来遍历数组,JavaScript 中的迭代器是一个对象,提供 next() 方法,用来返回序列中的下一项,这个方法包含两个属性:done 和 value。
迭代器对象一旦被创建,就可以反复调用 next()。
1 | function makeIterator (array) { |
常见的可迭代对象有:Array、String、TypedArray、Map、Set。这些对象都内置可迭代的对象,在其原型中有一个 Symbol.iterator
方法。
我们也可以定义可迭代对象。
1 | var myIterable = {} |
当我们定义了可迭代对象后,就可以在 Array.from
、for...of
中使用这个对象。
一个异步迭代器就像一个迭代器,除了它的 next() 方法返回一个 { value, done } 的 promise。如上所述,我们必须返回迭代器结果的 promise,因为在迭代器方法返回时,迭代器的下一个值和 done 状态可能未知。
1 | const myAsyncIterator = { |
对于异步迭代器,使用 for await of
进行迭代。
1 | (async function () { |
异步生成器函数与生成器函数类似,但有以下区别:
yield*
的行为以支持异步迭代。1 | async function* myAsyncGenerator() { |
函数返回一个异步生成器(async generator)对象,可以用在 for-await-of 语句中使用。
Node 基于 V8 引擎构建,采用单线程模型,所有的 JavaScript 将会运行在单个进程的单个线程上,它带来的好处是:没有多线程中常见的锁以及线程同步的问题,操作系统在调度时也能减少上下文切换,提高 CPU 使用率。但是如今 CPU 基本均是多核的,真正的服务器往往还有多个 CPU,一个 Node 进程只能利用一个核,这带来硬件资源的浪费。另外,Node 运行在单线程之上,一个单线程抛出异常而没有被捕获,将会导致进程的崩溃。
严格来说,Node 并非真正的单线程,Node 自身中还有 I/O 线程存在,这些 I/O 线程由底层 libuv 处理,这部分线程对于 JavaScript 而言是透明的,只有 C++ 扩展时才会关注到,JavaScript 代码运行在 V8 上,是单线程的。
Node 提供 child_process 模块来实现多核 CPU 的利用。child_process.fork() 函数来实现进程的复制。
worker.js 代码如下:
1 | var http = require('http') |
通过 node worker.js
启动它,会监听 1000 到 2000 之间的一个随机端口。
master.js 代码如下:
1 | var fork = require('child_process').fork |
这段代码根据 CPU 数量复制出对应的 Node 进程数,Linux 系统下通过 ps aux | grep worker.js 查看进程的数量。
1 | ps aux | grep worker.js |
这种通过 Master 启多个 Worker 的模式就是主从模式,进程被分为主进程和工作进程。主进程不负责具体的业务,而是负责调度和管理工作进程,它是趋于稳定的。
通过 fork() 复制的进程都是独立的进程,这个进程中有着独立的 V8 实例,它需要至少 30ms 的启动时间和至少 10MB 的内存。因此 fork 依然是昂贵的。
child_process 模块给予 Node 可以随意创建子进程的能力,详细的使用方法可以参考这篇文章:Node.js 中 child_procss 模块。
1 | var cp = require('child_process') |
首先来看一个示例:
parent.js
1 | var cp = require('child_process') |
sub.js
1 | process.on('message', function (m) { |
通过 fork() 或者其它 API,创建子进程之后,为了实父子程之间的通信,父进程与子进
程之间会创建 IPC 通道。通过过 IPC 通道,父子进程之间才能通过 message 和 send() 传递信息。
进程间通信原理:
IPC 全称是 Inter-Process Communication,即进程间通信,Node 实现 IPC 使用管道(pipe)技术,具体实现细节由 libuv 提供。在 Windows 下由命名管道(named pipe)实现,Linux 下采用 Unix Domain Socket 实现。表现在应用层上的进程间通信只有简单的 message 事件和 send() 方法。父进程在实际创建子进程之前,会创建 IPC 通道并监听它,然后才真正创建出子进程,并且通过环境变量 NODE_CHANNEL_FD 告诉子进程这个 IPC 通道的文件描述符。子进程通过这个文件描述符去连接这个已存在的 IPC 通道,从而完成父子进程之间的连接。
由于 IPC 管道是用命名管道或者 Domain Socket 创建的,与网络 socket 比较类似,属于双向通行。不同的是它们在系统内核中就完成了进程间的通信,而不是通过网络层,非常高效。
通常我们启用多个 Node 进程的时候,假如每个进程都监听 80 端口,会导致 EADDRINUSE 异常,解决方案是让每个进程监听不同的端口,其中主进程监听 80,对外接收所有的网络请求,再将这些请求代理到不同的端口的进程上。
通过代理不仅能解决端口重复监听的问题,还能适当的做负载均衡。由于进程每接收一个连接都会用掉一个文件描述符,因此代理方案中客户端连接到代理进程,代理进程连接到工作进程的过程需要用掉两个文件描述符,操作系统的文件描述符是有限的,代理方式需要一倍数量的文件描述符影响了系统的扩展能力。
为了解决上述问题,Node 引入了进程间传递句柄的功能。
1 | child.send(message, [sendHandle]) |
句柄是一种可以用来标识资源的引用,比如句柄可以标识一个服务器端的 socket 对象,一个客户端的 socket 对象,一个 UDP scoket,一个管道等。
发送句柄意味着主进程接收到 socket 请求后,直接将 socket 发送给工作进程,而不是重新与工作进程之间建立新的 socket 连接来转发数据。
主进程 parent.js:
1 | var child = require('child_process').fork('child.js') |
子进程 child.js:
1 | process.on('message', function(m, server) { |
通过 node 启动查看效果:
1 | // 先启动服务器 |
可以看出父子进程都有可能处理客户端请求。
尝试将服务发送给多个子进程。
1 | // parent.js |
子进程将进程 ID 打印出来。
1 | // child.js |
通过 curl 测试依然是相同的结果,请求可能被父进程处理,也可能被不同的子进程处理。并且这些都是在 TCP 层面完成的事情。我们尝试将其改成 HTTP 层来处理。
1 | // parent.js |
对子进程进行改动
1 | // child.js |
重新启动 parent.js 后,再次测试,所有的请求都是由子进程处理了。整个过程中,服务的过程发生了一次改变:
主进程发送完句柄并且关闭监听之后,成了下图的结构:
WebSocket 与 Node 之间的配合可以说是天作之合:WebSocket 客户端基于事件的编程模型与 Node 中自定义事件相差无几;WebSocket 实现了客户端与服务器之间的长连接,而 Node 在与大量客户端之间保持高并发连接方面非常擅长。
WebSocket 有以下好处:
WebSocket 在客户端的应用示例:
1 | var ws = new WebSocket("wss://127.0.0.1:12010/updates") |
上述客户端代码与服务器建立 WebSocket 连接后,每 50 毫秒向服务器发送一次数据。并且通过 onmessage 接受服务端传来的数据。
在 WebSocket 之前,服务器与客户端通信最高效的是 Comet 技术,实现原理依赖于长轮询或 iframe 流。长轮询是客户端向服务器发起请求,服务器只有在超时或者数据响应时断开连接(res.end()),客户端在收到数据或者超时后重新发起请求,这个请求拖着长长的尾巴,所以用彗星命名。
使用 WebSocket 技术,客户端只需要保持一个 TCP 连接即可完成双向通信,无需频繁断开连接和重发请求。
WebSocket 协议主要分两个部分:握手和数据传输。
客户端建立连接时,通过 HTTP 发起报文请求:
1 | GET /chat HTTP/1.1 |
其中 Upgrade 表示请求服务器升级协议为 WebSocket;Sec-WebSocket-Protocol 和 Sec-WebSocket-Version 表示协议和版本号;Sec-WebSocket-Key 用于安全校验,是一个随机生成的 Base64 编码的字符串,与服务器响应首部的 Sec-WebSocket-Accept 是配套使用的,为 WebSocket 提供基本防护。其对应的算法如下:
将 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
拼接,通过 SHA1 计算出摘要,并转成 base64 字符串。
1 | var crypto = require('crypto') |
服务器处理完请求后,响应的报文如下:
1 | HTTP/1.1 101 Switching Protocols |
客户端收到响应后,会校验 Sec-WebSocket-Accept 的值,如果成功,就开始接下来的数据传输。
握手顺利完成后,就开始 WebSocket 数据帧协议,协议升级过程如下图:
握手完成后,客户端的 onopen() 将会被触发。服务器端没有 onopen() 方法,为了完成 TCP socket 事件到 WebSocket 事件的封装,需要在接收数据时进行处理,WebSocket 的数据帧协议在底层的 data 事件上封装完成的:
1 | WebSocket.prototype.setSocket = function (socket) { |
客户端调用 send() 发送数据时,服务端出发 onmessage();当服务器调用 send() 发送数据时,客户端的 onmessage() 触发。send() 发送的数据会被协议封装为一帧或者多帧,然后逐帧发送。
为了安全考虑,客户端需要对发送的数据帧进行掩码处理,服务器一旦收到无掩码帧的数据,连接将关闭;而服务器的数据则不需要掩码处理。
(1) WebSocket 对象作为构造函数,用于新建 WebSocket 实例。
1 | var ws = new WebSocket('ws://localhost:8080') |
(2) readyState
(3) 事件
1 | ws.onopen = function () {} |
1 | var app = require('express')() |
在所有的 WebSocket 服务器实现中,Node 最贴近 WebSocket 的使用方式:
另外,Node 基于事件驱动的方式使得它应对 WebSocket 这类长连接的应用场景时可以轻松处理大量并发请求。
]]>Node 中提供了 net,dgram,http,https 四个模块,分别用来处理 TCP,UDP,HTTP,HTTPS,适用于客户端和服务器。
TCP 传输控制协议,在 OSI 模型中属于传输层,许多应用层的协议基于 TCP 构建,比如 HTTP,SMTP,IMAP 等。回顾一下 OSI 模型。
TCP 是面向连接的协议,其显著特征是在传输之前需要 3 次握手。只有建立会话,服务端与客户端才能互相发送数据,在建立会话的过程中,服务端和客户端分别提供一个 socket,这两个 socket 共同形成连接。服务端与客户端通过 socket 实现两者之间连接的操作。
1 | var net = require('net') |
使用 telnet 工具作为客户端对刚才创建的服务器进行连接。
1 | telnet 127.0.0.1 8124 |
同样的,我们也可以对 Domain Socket 进行监听
1 | server.listen('/tmp/echo.sock') |
通过 net 模块自行构建客户端进行会话 client.js:
1 | var net = require('net') |
注意,如果是 Domain Socket,在填写选项时,填写 path 即可。
1 | var client = net.connect({path: '/tmp/echo.sock'}) |
上述代码分为服务端事件和连接事件。
(1) 服务端事件
对于 net.createServer() 创建的服务器而言,它是一个 EventEmitter 实例,它的自定义事件有如下几种。
(2) 连接事件
服务器可以与多个客户端保存连接,每个连接都是典型的可读可写的 Stream 对象。它的自定义事件有如下几种。
TCP socket 为可读可写 Stream 对象,可以用 pipe() 实现管道操作。如下代码实现 echo 服务器。
1 | var net = require('net') |
TCP 对网络中的小数据包有一定的优化策略:Nagle 算法,用来减少网络中小数据包。Nagle 算法针对这种情况,要求缓冲区数据达到一定数量或者一定时间后才将其发出,并且 Nagle 算法合并小数据包,一次优化网络。但是可能造成数据延迟发送。
Node 中默认开启 Nagle 算法,可以调用 socket.setNoDelay(true) 关闭 Nagle 算法,使得 write() 可以立即发送数据到网络中。
UDP 又称为用户数据包服务,与 TCP 一样属于网络传输层。UDP 不是面向连接的,TCP 中一旦建立连接,所有的会话都是基于连接完成,客户端如果要与另一个 TCP 服务同学,需要另创建一个 socket 处理。在 UDP 中,一个 socket 可以与多个 UDP 服务通信。
UDP 提供面向事物的不可靠传输服务,在网络差的情况下存在丢包的问题,但是它无须连接,资源消耗低,处理快速且灵活,fico适用于那些偶尔丢一两个数据包也不会产生问题的场景,比如音频、视频等。DNS 服务基于 UDP 实现。
UDP socket 既可以作为服务端,又可以作为客户端。
1 | var dgram = require('dgram') |
(1) 创建 UDP 服务器
通过调用 dgram.bind(port, [address]) 方法创建 UDP 服务器,接收网路消息。
1 | var dgram = require('dgram') |
(2) 创建 UDP 客户端
1 | var dgram = require('dgram') |
客户端执行后,服务端输出:
1 | node main.js |
当 socket 在客户端时,可以调用 send() 方法发生消息到网络。
1 | socket.send(buf, offset, length, port, address, [callback]) |
(3) UDP socket 事件
UDP 相对于 TCP 更简单,它只是一个 EventEmitter 的实例,而非 Stream 的实例。它自定义事件如下:
TCP 与 UDP 都属于网络传输层协议,如果要构造高效的网络应用,就应该从传输层进行着手。但是一般使用应用层协议就能满足我们大部分开发需求。Node 提供基本的 http 和 https 模块用于 HTTP 和 HTTPS 的封装。
1 | var http = require('http') |
HTTP 构建于 TCP 之上,属于应用层协议。
使用 curl 查看网络通信的报文信息。
1 | curl -v http://127.0.0.1:1337 |
报文解析:
(1) TCP 三次握手
1 | * About to connect() to 127.0.0.1 port 1337 (#0) |
(2) 客户端向服务端发送请求报文
1 | GET / HTTP/1.1 |
(3) 服务器响应客户端内容
1 | < HTTP/1.1 200 OK |
(4) 结束会话
1 | * Connection #0 to host 127.0.0.1 left intact |
从上述报文信息中可以看出 HTTP 的特点:基于请求响应式的,以一问一答的方式实现服务,虽然基于 TCP 会话,但是本身并无会话的特点。
Node 的 http 模块包含对 HTTP 处理的封装,在 Node 中,HTTP 服务继承自 TCP 服务(net 模块),它能够与多个客户端保持连接,采用事件驱动的形式,并不为每一个连接创建额外的线程或者进程,占用很低的内存,并且实现高并发。
HTTP 服务与 TCP 服务的区别在于,开启 keepalive 后,一个 TCP 会话可以用于多次请求和响应,TCP 以 connection 为单位进行服务,HTTP 服务以 request 为单位进行服务。http 模块即是将 connection 到 request 的过程进行了封装。
除此之外,http 模块将连接所用的 socket 的读写抽象为 ServerRequest 和 ServerResponse 对象,它们分别对应请求和响应操作。在请求产生的过程中,http 模块拿到连接中传来的数据,调用二进制模块 http_parser 进行解析,在解析完请求报文的报头后,触发 request 事件,调用用户的业务逻辑。
(1) HTTP 请求
对于 TCP 连接的读操作,http 模块将其封装为 ServerRequest 对象。报头通过 http_parser 进行解析。
1 | GET / HTTP/1.1 |
1 | headers: { |
报文体部分则抽象为一个只读流对象,如果业务逻辑需要读取报文体中的数据,则要在这个数据流结束后才能进行操作。
1 | function (req, res) { |
(2) HTTP 响应
HTTP 响应对象封装了底层连接的写操作,可以将其看作一个可写的流对象,通过 res.setHeader() 和 res.writeHead() 响应报文头部信息。
1 | res.writeHead(200, {'Content-Type': 'text/plain'}) |
转化为报文如下:
1 | < HTTP/1.1 200 OK |
setHeader 可以进行多次调用,但只有调用 writeHead 后,报文才会写入到连接中,此外,http 模块还会自动设置一些头信息。
1 | < Date: Mon, 04 Jun 2018 15:34:30 GMT |
报文体则是通过调用 res.write() 和 res.end() 方法实现,区别在于 res.end() 会调用 write() 发送数据,然后发送信号告知服务器这次响应结束。
响应结束后,HTTP 服务器可能将当期连接用于下一次请求,或者关闭连接。另外,无法服务器在处理业务逻辑时是否发生异常,务必在结束时调用 res.end() 结束请求,否则客户端将一直处于等待的状态。当然也可以通过延迟 res.end() 的方式实现客户端与服务器之间的长连接,但结束时务必关闭连接。
(3) HTTP 服务的事件
HTTP 服务器抽象了一些事件,供应用层使用,服务器也是一个 EventEmitter 实例。
Expect: 100-continue
的请求到服务器,服务器将触发 checkContinue 事件。如果服务器没有监听这个事件,则会自动响应客户端 100 Continue 的状态码,表示接受数据上传。如果不接受,或者客户端数据较多时,响应 400 Bad Request 拒绝客户端继续发送数据。(4) HTTP 客户端
http 模块通过调用 http.request(options, connect) 构造客户端。与上文的 curl 大致相同:
1 | var options = { |
执行:
1 | node client.js |
options 中选项有如下这些:
(5) HTTP 代理
http 提供的 ClientRequest 对象也是基于 TCP 层实现的,在 keepalive 的情况下,一个底层的会话连接可以用于多次请求。为了重用 TCP 连接,http 模块包含一个默认的客户端代理对象 http.globalAgent。
http.globalAgent 对每个服务器端(host + port)创建的连接进行管理,默认情况下,每个请求最多可以创建 5 个连接,它的实质是一个连接池。
调用 HTTP 客户端对一个服务器发起 10 次 HTTP 请求时,其实质只有 5 个请求处于并发状态,后续的请求需要等待某个请求完成后才真正发出,与浏览器对同一域名的并发限制相同。
1 | var agent = new http.Agent({ |
也可以设置 agent 选项为 false,以脱离连接池管理,使请求不受并发限制。
(6) HTTP 客户端事件
Buffer 是一个像 Array 的对象,主要用来操作字节。Buffer 是一个典型的 JavaScript 与 C++ 结合的模块,它将性能相关的部分用 C++ 实现,将非性能相关的部分用 JavaScript 实现。
Buffer 所占用的内存不是通过 V8 分配的,而是堆外内存。由于 V8 垃圾回收性能的影响,将 Buffer 对象用更高效的专有内存分配回收策略来管理。
Buffer 在 Node 进程启动的时候已经载入了,并将其放在全局对象 global 上,因此无需 require() 就能使用。
Buffer 的元素为 16 进制的两位数,即 0 到 255 的数值。
1 | var str = '深入浅出node.js' |
不同编码的字符串占用的元素个数各不相同,中文在 UTF-8 编码下占用 3 个元素,字母和半角标点占用 1 个元素。
Buffer 可以通过 length 属性得到长度,也可以通过下标访问元素。
1 | var buf = new Buffer(100) |
如果给元素赋值不是 0 到 255 的整数而是小数,Buffer 通过不断 +256 或者不断 -256 得到一个位于 0 - 255 之间的整数。如果是小数,则直接舍弃小数部分,只保留整数部分。
1 | buf[10] = -100 |
Buffer 对象的内存不是在 V8 堆内存中,而且 Node 的 C++ 层面实现的内存申请。因为处理大量的字节数据不能采用需要一点内存就像操作系统申请一点内存的方式,这可能造成大量内存申请的系统调用,对操作系统有一定压力。Node 使用的策略是在 C++ 层面申请内存,在 JavaScript 中分配内存。
Node 操作 Buffer 使用 slab 内存分配策略。slab 是一种动态内存管理机制,最早出现于 SunOS,目前广泛应用于 Linux。
slab 是一块申请好的固定大小的内存区域。一共有三种状态: full:完全分配状态,partial:部分分配状态;empty:没有分配状态。
当我们需要一个 Buffer 对象,可以通过传入 size 来指定 Buffer 对象大小:
1 | new Buffer(size) |
Node 以 8kb 为界限来区分 Buffer 是大对象还是小对象。这个 8kb 也就是每个 slab 的值,在 JavaScript 层面,以它作为单位进行内存分配。
(1) 小 Buffer 对象
如果指定 Buffer 的大小小于 8kb,Node会按照小对象的方式进行分配。
(2) 大 Buffer 对象
如果是超过 8kb 的对象,将会直接分配一个 SlowBuffer 对象作为 slab 单元,这个 slab 单元将被这个大 Buffer 对象独占。
Buffer 对象可以与字符串直接互相转换,目前支持的字符串编码类型有:ASCII、UTF-8、UTF-16LE/USC-2、Base64、Binary、Hex。
字符串可以通过 Buffer 构造函数转换为 Buffer 对象,存储的只能说一种编码类型。encoding 参数不传递时,默认按照 UTF-8 编码进行转码和存储。一个 Buffer 对象可以存储不同编码类型的字符串转码的值,调用 write() 可以实现。
1 | new Buffer(str, [encoding]) |
由于可以不断写内容到 Buffer 对象中,并且每次都可以指定编码,所以 Buffer 对象中可以存在多种编码转化后的内容,需要注意的是,每种编码所用的字节长度不同,反转 Buffer 回字符串时需要谨慎处理。
1 | buf.toString([encoding], [start], [end]) |
可以设置 encoding,start,end 这 3 个参数实现整体或者局部的转化。
由于 Node 中 Buffer 对象只支持上述几种类型的编码,因此可以用 isEncoding() 函数判断编码是否支持转化。
1 | Buffer.isEncoding(encoding) |
如果需要转化其它类型的编码,可以借助 iconv 和 iconv-lite 两个模块。
iconv-lite 由纯 JavaScript 实现,iconv 则是通过 C++ 调用 libiconv 库实现,前者比后者更轻量,无需编译和处理环境依赖。
1 | var iconv = require('iconv-lite') |
Buffer 常用于从输入流中读取内容
1 | var fs = require('fs') |
上述代码在英文环境中一般不会出现问题,但是在中文环境中,经常会看到乱码。data 事件中获取的 chunk 对象为 Buffer 对象,上述代码将其当做字符串处理:data += chunk
本质上是 data = data.toString() + chunk.toString()
。在英文环境中,toString() 不会造成任何问题,但是对于宽字节的中文,却会形成问题。
我们创建 test.md,内容为李白的《静夜思》,修改刚才的代码。
1 | var rs = fs.createReadStream('./test.md' { highWaterMark: 11 }) |
输出结果如下:
1 | 窗前明��光,疑���地上霜,举头��明月,���头思故乡。 |
下面我们来分析乱码是怎么来的。
上面传的参数 highWaterMark 的作用是限制 Buffer 对象的长度为 11。前面说到中文 UTF-8 为 3 个字节,所以前 3 个字“床前明”能够正常输出,后面 11 - 3 * 3 = 2 个字节无法正常解析为 UTF-8 的中文字符串,所以输出乱码。在调用 toString() 的时候,默认使用 UTF-8 编码。后面的乱码都是相同的道理。
1 | var rs = fs.createReadStream('./test.md', { highWaterMark: 11 }) |
setEncoding() 的作用是让 data 事件中传递的不再是一个 Buffer 对象,而是编码后的字符串。改进后重新执行,得到正确的输出。
1 | 窗前明月光,疑是地上霜,举头望明月,低头思故乡。 |
在调用 setEncoding() 的时候,可读流对象在内部设置了一个 decoder 对象,每次 data 事件都是通过 decoder 对象进行 Buffer 到字符串的解析。
Buffer 在文件 I/O 和网络 I/O 中运用广泛,在应用中,通常操作字符串,但一旦在网络中传输,都需要转换为 Buffer,以二进制数据进行传输。
构造一个 10kb 大小的字符串,通过纯字符串的方式向客户端发送:
1 | var http = require('http') |
使用 ab 进行性能测试,发起 200 个并发客户端:
1 | ab -c 200 -t 100 http://127.0.0.1:8001 |
在我的腾讯云上单核 1G CPU,1G 内存的服务器上测试结果如下:
1 | Server Software: |
测试的 QPS(每秒查询次数)为 3815.61,传输率为 38435.54。
去掉 helloworld = new Buffer(helloworld) 前面的注释,再次测试:
1 | Server Software: |
测试的 QPS(每秒查询次数)为 6886.98,传输率为 69374.22。性能提升了近一倍。
通过预先转换静态内容为 Buffer 对象,可以有效减少 CPU 重复使用,节省服务器资源。在 Node 构建的 Web 应用中,可以选择将页面中的动态内容和静态内容分类,静态内容预先转换为 Buffer 对象,使性能得到提升。由于文件本身是二进制数据,所以在不需要改变内容的场景中,设置 Buffer 为只读,不做额外的转换能达到更好的效果。
通过 fs.createReadStream(path, opts) 创建文件读流,其中可以传入的参数为:
1 | { |
opts 可以包括 start 和 end 值,使其可以从文件读取一定范围的字节而不是整个文件。例如从 100 个字节的文件中读取最后 10 个字节:
1 | fs.createReadStream('sample.txt', { start: 90, end: 99 }) |
fs.createReadStream() 的工作方式是在内存中准备一段 Buffer,然后在 fs.read() 读取时逐步从磁盘中将字节复制到 Buffer,完成一次读取后,从这个 Buffer 中通过 slice() 方法取出部分数据作为一个小 Buffer 对象,再通过 data 事件传递给调用方。如果 Buffer 用完,则重新分配一个,如果还有剩余则继续使用。
1 | var pool |
理想状况下,每次读取的长度就是用户指定的 highWaterMark,但是假如读到文件最后,剩下的内容不到 highWaterMark 那么大,这是预先指定的 Buffer 对象将会有剩余,不过这部分内存可以分配给下次读取时用。
highWaterMark 的大小对性能有以下两个影响:
本章学习 V8 的垃圾回收机制以及如何高效使用内存,内存泄漏以及如何排查内存泄漏。
关于 JavaScript 中常用的垃圾回收机制,可以参考这篇文章 JavaScript 垃圾回收。
一般后端开发语言中,在基本的内存使用上都没有什么限制,而 Node 中将 JavaScript 的使用内存做出如下限制:64 位操作系统约为 1.4G,32 位操作系统约为 0.7G。在这样的限制下,Node 无法直接操作大内存对象,比如将一个 2GB 文件读取到内存中进行字符串分析,即使物理内存有 32 GB。
在 V8 中,所有的 JavaScript 对象都是通过堆来进行内存分配的,Node 中可以通过 process.memoryUsage() 查看内存使用情况。
1 | node |
其中 heapTotal 和 heapUsed 是 V8 堆内存使用情况,前者是已经申请到的堆内存,后者是当前内存使用量。external 代表 V8 管理的,绑定到 Javascript 的 C++ 对象的内存使用情况。rss 代表进程常驻内存部分, 是给这个进程分配了多少物理内存(占总分配内存的一部分) 这些物理内存中包含堆,栈,和代码段。进程中的内存总共有几部分,一部分是 rss,其余部分在交换区(swap)或者文件系统(filesystem)中。
当我们在代码中声明变量并且赋值的时候,使用的对象就分配在堆中,如果已经申请到的堆内存不够分配时,就继续申请,直到超过 V8 的限制为止。
在 Node 环境中使用下面两个参数可以调整启动时内存限制的大小:
1 | node --max-nex-space-size=1024 app.js // 单位为KB |
V8 采用分代式的垃圾回收机制,主要将内存分为新生代和老生代。新生代中对象存活时间较短,老生代中对象存活时间较长或者常驻内存。 --max-old-space-size
和 --max-new-space-size
就是用于设置老生代和新生代内存大小。
(1) Scavenge 算法
新生代对象主要通过 Scavenge 算法进行垃圾回收,在 Scavenge 的具体实现中,主要采取 Cheney 算法。
Cheney 算法是一种采用复制的方式实现的垃圾回收算法,它将堆内存一分为二,每份空间称为 semispace,两份堆内存一个处于使用中,一个处于闲置状态。处于使用状态的的空间称为 From 空间,处于闲置状态的空间称为 To 空间。当我们分配对象时,首先在 From 空间分配,当开始进行垃圾回收时,会检查 From 中存活的对象,将其复制到 To 空间中,非存活对象占用的空间将被释放。
Scavenge 的缺点是只能使用一半的堆内存,但是由于 Scavenge 只复制存活的对象,所以在面对声明周期较短的场景时,非常有优势。因此在 V8 新生代内存中垃圾回收使用 Scavenge 算法。
在 V8 分代式垃圾回收机制下,From 空间中存活的对象在复制到 To 空间之前要进行检查,将一些满足条件的对象移动到老生代内存中。
(2) Mark-Sweep & Mark-Compact
V8 在老生代内存中,主要采用标记清除法和标记紧缩法进行垃圾回收。
Mark-Sweep 在标记阶段遍历堆中所有的对象,并标记活着的对象,在随后的清除阶段,只清除没有被标记的对象。Mark-Sweep 存在的问题是进行一次标记清除回收后,内存会出现不连续的状态。
为了解决 Mark-Sweep 中内存碎片的问题,Mark-Compact 被提出来了。Mark-Compact 是标记整理或者标记紧缩的意思。 Mark-Compact 在 Mark-Sweep 的基础上演变而来,它们的差别在于,清除完标记对象后,在整理的过程中,将活着的对象向一端移动,移动完成后,直接清理掉边界的内存。
(3) Incremental Marking
为了避免出现 JavaScript 应用逻辑与垃圾回收器中看到的不一致的情况,垃圾回收的 3 种算法都要将应用逻辑暂停下路,待执行完垃圾回收后再恢复执行逻辑。
增量标记是在 V8 为了降低垃圾回收时带来的停顿时间,V8 从停顿阶段入手,将原来要一口气完成的动作拆分为许多部分,每完成一部分,让 JavaScript 应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成。
通过在启动参数中添加 --trace_gc
,当进行垃圾回收时,会打印出垃圾回收的信息。
通过在启动参数中添加 --prof
,可以得到 V8 执行时的性能分析数据,其中包含垃圾回收执行所占用的时间。
在 JavaScript 中能形成作用域的有函数调用,with 以及 全局作用域。比如在下面代码中:
1 | var foo = function () { |
foo() 在每次被调用的时候都会创建对于的作用域,执行完后作用域销毁,作用域内声明的局部变量也随之销毁。在这个示例中,local 对象会分配在新生代内存 From 中,作用域释放后,local 被垃圾回收。
(1) 标识符查找
标识符可以理解为变量名,在 JavaScript 执行时,它会首先查找当前作用域,如果找不到,将会向上级作用域查找,直到查到为止。这种不断向上级作用域查找的方式也叫做作用域链。
(2) 变量主动释放
全局变量如果不主动删除,可能会导致对象常驻内存(老生代),可以通过 delete 操作符来删除引用关系。或者将变量重新赋值,让旧的对象脱离引用关系。
1 | global.foo = 'I am a global object' |
闭包是一种反作用域链的方式,通过高阶函数,实现外部作用域访问内部作用域中的变量的方法。
1 | var foo = function () { |
一般来说,bar() 函数执行完毕后,局部变量 local 就会被垃圾回收,但是 bar() 函数返回了一个匿名函数,而且匿名函数还具备访问 local 的条件,所以只要执行匿名函数的 baz 存在,local 就不会被垃圾回收。
在正常的 JavaScript 执行中,无法立即回收的内存有闭包和全局变量,因此在使用的时候要多加小心,避免老生代内存不断增多的现象。
(1) process.memoryUsage()
(2) os 模块的 totalmem() 和 freemem() 可以查看操作系统总内存和闲置内存。
通过 process.memoryUsage() 可以发现堆中的内存使用量总是小于进程的常驻内存使用量的,这就意味着 Node 中内存的使用并非全部通过 V8 进行分配。那些不通过 V8 进行分配的内存成为堆外内存。比如 Buffer 对象使用的就是堆外内存。
造成内存泄漏的主要原因有:缓存,队列消费不及时,作用域未释放。
在 Node 中,一旦一个对象被当做缓存用,那就意味着它将会常驻老生代内存,老生代内存的堆积会导致垃圾回收在进行扫描时,对这些对象做无用功。
下面是我们经常都会写的代码:
1 | var cache = {} |
上述代码十分容易理解,创建缓存以内存换取 CPU 执行时间,但是要注意一定要限定缓存对象的大小,再加上完善的过期策略防止内存无限制增长。
直接将内存作为缓存的方案要十分慎重,除了要限制缓存大小外,还需要考虑的事情是进程直接无法共享内存。解决方案是使用进程外缓存,比如 Redis 和 Memcached。
Node 通过生产者-消费者模式构建消息队列,假如队列的消费速度低于队列的生成速度,很容易造成堆积。举一个例子,有的应用会收集日志,假如采用数据库来记录日志,由于数据库构于文件系统之上,写入的效率低于文件直接写入,于是会形成数据库写入操作的堆积,而 JavaScript 中相关的作用域得不到释放,从而导致内存泄漏。
解决方法:
node-heapdump 允许对 V8 堆内存抓取快照,用于事后分析。
1 | var memwatch = require('memwatch') |
在进程中使用 node-memwatch 之后,每次进行垃圾回收的时候,都会触发一次 stats 事件,这个事件将会传递内存的统计信息。
1 | { |
leak 事件记录 Node 中存在的内存泄漏。如果经过 5 次垃圾回收,内存仍然没有释放,这意味着可能存在内存泄漏,node-memwatch 会发出一个 leak 事件。
1 | { start: Fri, 29 Jun 2012 14:12:13 GMT, |
growth 显示了 5 次垃圾回收的过程中内存增长了多少。
Node 中使用 Stream 模块处于处理大文件
Stream 模块是 Node 的原生模块,继承自 EventEmitter,具备基本自定义事件功能和标准的事件和方法。Stream 分为读和写两种,Node 中很多模块依赖于 Stream 模块,比如 fs.createReadStream() 和 fs.createWriteStream() 分别用来创建文件的可读流和可写流。
1 | var reader = fs.createReadStream('in.txt') |
或者使用管道方法
1 | var reader = fs.createReadStream('in.txt') |
通过流的方式进行文件的读写,不会受 V8 内存限制,如果不需要进行字符串层面的操作,可以借助 Buffer 操作,但是大片使用内存的情况依然需要消息,即使 V8 不限制内存,物理内存依然有限制。
]]>异步的概念首先在 Web2.0 中火起来,是因为浏览器中 JavaScript 在单线程上执行,而且它还与 UI 渲染共用一个线程。这意味着 JavaScript 在执行的时候 UI 渲染和响应是处于停滞状态的。前端通过异步的方式来消除 UI 阻塞的现象。假如业务场景中有一组互不相关的任务需要完成,可以采用下面两种方式。
如果创建多线程的开销小于并行执行,那么多线程的方式是首选的。多线程的代价在于创建线程和执行期间线程上下文切换的开销较大。另外,在复杂业务中,多线程编程经常面临锁、状态同步等问题。但是多线程能有效利用 CPU。
单线程顺序执行比较符合编程人员按照顺序思考的思维方式,也是最主流的编程方式。缺点在于执行性能,任何一个略慢的任务都会导致后续执行代码被阻塞。
Node 在两者之间给出了它的方案:利用单线程,远离多线程死锁,状态同步问题;利用异步 I/O,让单线程远离阻塞,更好地利用 CPU。
异步 I/O 就是 I/O 的调用不再阻塞后续计算,将原有等待 I/O 完成这段时间分配给其它需要的业务去执行。
从计算机内核 I/O 而言,同步/异步和阻塞/非阻塞实际上是不同的。操作系统内核对 I/O 只有两种方式,阻塞和非阻塞。在调用阻塞 I/O 时,应用程序需要等待 I/O 完成才返回结果。阻塞 I/O 造成 CPU 等待 I/O,CPU 的处理能力得不到充分利用。为了提高性能,内核提供了非阻塞 I/O。非阻塞 I/O 在调用之后立马返回,但是数据并不在返回结果中,返回结果中只有当前调用的状态。为了获取完整的数据,应用程序需要重复调用 I/O 操作来确认是否完成。这种方式叫做轮询。
非阻塞 I/O 技术虽然不会让 CPU 等待造成浪费,但是却需要轮询去确认是否完成数据获取,其实也是对 CPU 资源的浪费。
主要轮询技术:
(1) read。反复调用来检查 I/O 的状态。
(2) select。通过文件描述符上的事件状态进行判断,select 轮询采用 1024 长度数组存储状态。
(3) poll。使用链表,减少不必要的检查。
(4) epoll。该方案是 Linux 下效率最高的 I/O 事件通知机制。在进入轮询的时候如果没有检查到 I/O 事件,将会进行休眠,知道事件发生将它唤醒。
事件循环是 Node 自身的执行模型,正是它使得回调函数十分普遍。
在进程启动时,Node 便会创建一个类似于 while(true) 的循环,每执行一次循环体成为 Tick。每个 Tick 的过程就是查看是否有事件待处理,如果有,就取出事件及其相关的回调函数。如果存在关联的回调函数,就执行它们,然后进入下个循环,直到没有事件处理,就退出进程。
在每个 Tick 的过程中,如何判断是否有事件需要处理呢?Node 在每个事件循环中都有一个或多个观察者,而判断是否有事件需要处理的过程就是向这些观察者询问是否有要处理的事件。
在 Node 中,事件主要来源于网络请求,文件 I/O 等。事件循环是一个典型的生产者/消费者模型。异步 I/O,网络请求等则是事件的生产者,源源不断为 Node 提供不同类型的事件,这些事件被传递到对应的观察者哪里,事件循环则从观察者那里取出事件并处理。
对于 Node 中的异步 I/O 而言,回调函数究竟是谁在调用呢?比如下述代码,当文件打开成功后,后面的回调的执行过程是怎样的呢?
1 | const fs = require('fs') |
从 JavaScript 调用 Node 核心模块,核心模块调用 C++ 内建模块,内建模块通过 libuv 进行系统调用。libuv 作为封装层,有平台各自的实现,本质上是调用 uv_fs_open() 方法。在调用 uv_fs_open() 的过程中,我们创建了一个 FSReqWrap 请求对象。从 JavaScript 层传入的参数和当前方法都封装在这个请求对象中,回调函数也是这个请求对象的一个属性。而操作系统拿到这个对象后,将 FSReqWrap 对象推入线程池中等待执行。
至此,JavaScript 调用立即返回,异步调用第一阶段完成,JavaScript 线程可以继续执行后续任务。当前的 I/O 操作在线程池中等待执行,不管它是否阻塞,都不会影响 JavaScript 后续的执行。
线程池中的请求对象在得到 CPU 资源后调用操作系统底层的函数完成 I/O 操作,线程池调用 PostQueuedCompletionStatus() 方法提交状态,然后将结果存储在请求对象的 req-> result
属性上,并且释放线程回归线程池。I/O 观察者在每次 Tick 的时候通过调用 GetQueuedCompletionStatus() 方法去检查线程池中是否有执行完的请求,如果存在,会将请求对象加入到 I/O 观察者队列中,然后将其当做事件处理。
I/O 观察者取出请求对象的 result 属性作为参数,取出绑定在上面的回调函数,然后执行,以此达到调用 JavaScript 回调函数的目的。至此,整个异步 I/O 完成。
事件循环、观察者、请求对象、I/O 线程池这四者共同构成了 Node 异步 I/O 模型的基本要素。Windows 主要通过 IOCP 来向系统内核发送 I/O 调用和从系统内核获取 I/O 状态,配以事件循环,完成异步 I/O 的过程,Linux 下通过 epoll 实现这个过程。不同的是,线程池在 Windows 上由内核 IOCP 实现,Linux 下由 libuv 实现。
最后回答上面提到的问题,回调函数究竟由谁来执行?答案是:I/O 观察者。
Node 中还存在一些与 I/O 无关的 API:setTimeout()、setInterval()、setImmediate() 和 process.nextTick()。
(1) setTimeout 和 setInterval 的实现原理与异步 I/O 比较类似,只是不需要线程池参与。调用 setTimeout/setInterval 创建的定时器会被插入定时器观察者内部的红黑树中,每次 Tick 执行时,会从该红黑树中迭代选出定时器对象,检查是否超过时间,如果超过,它的回调函数立即执行。
执行回调函数的是定时器观察者。
定时器的问题在于,它并非精确的,尽管事件循环非常快,但是如果每一次循环占用时间较多,那么下次循环时,它可能已经超时很久了。比如 setTimeout 设定一个任务在 10 毫秒后执行,但是在 9 毫秒时,有一个任务占用了 5 毫秒的 CPU 时间片,再次轮到定时器执行时,时间已经超过 4 毫秒了。
(2) process.nextTick() 的出现正是为了解决定时器精度不高,并且需要红黑树(性能浪费)的问题。它的作用是定义一个动作,在下次事件轮询的时间点上执行这个动作。
比如:
1 | function foo () { |
终端上的输出结果是:
1 | bbb |
使用 setTimeout 也能达到同样的效果:
1 | function foo () { |
每次调用 process.nextTick() 方法,只会将回调函数放入队列中,在下一轮 Tick 时取出执行。
定时器中采用红黑树的操作时间复杂度为 O(lg(n)),nextTick() 的时时复杂度为 O(1)。相比之下,
process.nextTick() 更高效。
(3) setImmediate() 与 process.nextTick() 方法十分类似,都是将回调函数延迟执行。
1 | process.nextTick(function () { |
两者的输出结果是一样的:
1 | 正常执行 |
process.nextTick 的优先级要高于 setImmediate。原因是事件循环对观察者的检查是有先后顺序的。process.nextTick 属于 idle 观察者,setImmediate 属于 check 观察者。在每一个轮询检查中,idle 观察者优先于 I/O 观察者,I/O 观察者优先于 check 观察者。
还有一个主要的区别是,process.nextTick() 的回调函数保存在数组中,setImmediate() 的回调函数保存在链表中。在行为上,process.nextTick() 在每次轮询中会将数组内全部回调函数执行完,setImmediate() 在每次循环中只执行链表的第一个回调函数。
事件驱动的实质就是通过主循环和事件触发的方式来运行程序,Node 采用的事件驱动的方式,无需为每个请求简历额外的线程,可以省去线程创建切换和销毁带来的开销,使得服务器能有条不紊地处理消息,这是 Node 高性能的一个主要原因。
事件驱动带来的高效也被 Nginx 采用,不同之处在于 Nginx 由纯 C 编写,性能极其强大,非常适合做 Web 服务器。
异步 I/O 的核心是事件循环,Node 使用了和浏览器中一样的执行模型,让 JavaScript 在服务端发挥巨大的能量。
]]>不知不觉 Node 已经更新到第十个版本了,本人使用 Node 也有两年多时间,之前学习的东西一直零零散散,没有形成系统的知识体系,于是最近又抽时间回顾这本经典的 《深入浅出Node.js》,阅读的过程中,难免有些东西不易理解或者容易忘记,因此选择博客的形式记录。
作者书写这本书的时候,Node 的稳定版本为 v0.10.13,当前最高版本为 v10.1.0,不过整个 Node 的核心体系在当时已经形成,因此对更高版本的理解问题不大。
Node 诞生于 2009 年 3 月,作者为 Ryan Dahl。作者选择 JavaScript 作为 Node 的实现语言主要因为:JavaScript 高性能(V8),符合事件驱动,没有后端历史包袱。
除了 HTML、WebKit 和显卡这些与 UI 相关技术没有支持外,整个 Node 的结构与 Chrome 非常相似,它们都是基于事件驱动的异步架构,浏览器通过事件驱动来服务界面上的交互,Node 通过事件驱动来服务 I/O。
(1) 异步 I/O。在 Node 中,绝大多数的操作都是以异步的方式进行调用,从文件操作到网络请求都是如此。
(2) 事件与回调函数。Node 将前端浏览器中应用广泛的事件机制引入后端,配合异步 I/O。优点是事件编程轻量,低耦合,只用关注事务点等,缺点是多个事件之间的协作是一个问题。
(3) 单线程。Node 保持了 JS 单线程的特点,在 Node 中,JS 与其余线程无法共享状态。单线程好处了不用处理多线程之间的状态同步与通信,没有死锁的存在,也没有线程切换带来的性能开销。缺点是无法利用多核 CPU;错误会引起整个应用退出,应用健壮性值得考验;对大规模高 CPU 计算不友好。
在浏览器中,HTML5 制定了 Web Worker 标准来解决 JS 大规模计算导致的阻塞 UI 渲染的问题。而 Node 中,使用 child_process 创建子进程来应对单线程带来的问题。
(4) 跨平台。
(1) I/O 密集型。I/O 密集的优势˞要在于 Node 利用事件循环的能力,而不是启动每一个线程为每一个请求服务,资源占用极少。
(2) Node 是否适用于 CPU 密集型应用?首先 Node 的计算性能并不差,但是由于 JavaScript 单线程的原因,如果有长时间运算,将导致 CPU 不能释放,使后续 I/O 无法发起。
(3) 与遗留系统和平共处。比如和 Java 配合,Node 完成 Web 端的开发,Java 提供稳定的接口。
(4) 分布式应用。
Node 的模块化采用 CommonJS 规范,关于 JavaScript 模块化的各种规范,可以参考 前端模块化-CommonJS,AMD,CMD,ES6。
CommonJS 规范涵盖了模块,二进制,Buffer,字符集编码,I/O 流,进程环境,文件系统,socket,单元测试,Web服务器接口,包管理等。
(1) 模块引用
通过 require() 方法引入一个模块的 API 到当前上下文中。
1 | var math = require('math') |
(2) 模块定义
在模块中,上下文提供 exports 对象用于导出当前模块的变量或者方法,并且它是唯一导出的出口。在模块中,还存在一个 module 对象,代表模块自身,而 exports 是 module 的属性。
1 | exports.add = function () { |
(3) 模块标识
模块标识就是传递给 require() 的参数,它必须是符合小驼峰命名的字符串,或者以 .
和 ..
开头的相对路径,或者绝对路径。
CommonJS 构建的这套模块导出和引入机制使得用户完全不必考虑变量污染,命名空间等方案相形见绌。
Node 引入模块,需要经历三个步骤:路径分析,文件定位,编译执行。
Node 中的模块分为核心模块和文件模块。
(1) 核心模块在 Node 源码编译过程中,编译成为二进制文件,在 Node 启动阶段部分核心模块就被加载进内存,所以省去了文件定位和编译的时间,加载速度最快。
(2) 文件模块则是在运行时动态加载。
(3) 自定义模块是指非核心模块,也不是路径形式的文件模块。以文件或者包的形式存在,这类模块的查找是最费时的。
模块路径:Node 在定位文件模块的时候采用的一种查找策略。具体表现为一个路径组成的数组。比如我在自己的电脑 /Users/lizhen/WorkSpaces/test
目录下面创建文件 index.js:
内容如下:
1 | console.log(module.paths) |
运行脚本输出结果如下:
1 | [ '/Users/lizhen/WorkSpaces/test/node_modules', |
其路径寻址规则如下:从当前目录的 node_modules 中寻找 -> 父目录的 node_modules 中寻找 -> 递归一直到根目录的 node_modules。
它的生成方式与 JavaScript 原型链或者作用域链的查找方式十分类似。Node 会逐个尝试模块路径,直到找到模块或者查找到根目录位置。可以看出,当文件路径比较深的时候,模块查找会比较耗时。
Node 对引入过的模块都会进行缓存,无论是核心模块还是文件模块,require() 方法都采用缓存优先的方式进行加载,并且核心模块的优先级高于文件模块。
require() 在分析标识符的过程中,如果标识符不包括扩展名,Node 会按照 .js
, .json
, .node
的次序补足扩展名,依次尝试。
在尝试的过程中,需要调用 fs 模块同步阻塞式地判断文件是否存在,所以会引起性能问题。解决的办法是:1. .node
和 .json
文件标识符中带上扩展名。2. 同步配合缓存,可以大幅缓解 Node 单线程中阻塞调用的缺陷。
在 Node 中,每个文件都是一个对象,它的定义如下:
1 | function Module (id, parent) { |
编译和执行是引入文件模块的最后一个阶段。定位到文件后,Node 会新建一个模块对象,然后根据路径载人并编译。不同文件载入方式不同:
.js
文件,通过 fs 模块同步读取文件后编译执行。.node
文件,由 C/C++ 编写,通过 dlopen() 加载最后编译生成的文件。.json
文件,通过 fs 模块同步读取后,用 JSON.parse() 解析。.js
文件载入。每个编译成功的模块都会将其文件路径作为索引缓存在 Module._cache
对象上,以提高二次引入的性能。
根据不同的文件扩展名,Node 会调用不同的读取方式,如 .json
文件:
1 | Module._extensions['.json'] = function (module, filename) { |
其中,Module._extensions
会赋值给 require 的 extensions 属性。
1 | console.log(require.extensions) // { '.js': [Function], '.json': [Function], '.node': [Function] } |
也可以通过扩展 require.extensions['.ext']
的方式对自定义扩展名进行特殊的加载,但是 Node 官方并不鼓励这种行为。
在编译 JavaScript 的过程中,Node 对获取的 JavaScript 文件进行包装:模块包装器
1 | (function(exports, require, module, __filename, __dirname) { |
这样每个模块文件之间都进行了作用域隔离,包装之后的代码会通过 vm 原生模块的 runInThisContext() 方法执行(类似 eval,只是有明确的上下文,不污染全局)。
exports vs module.exports
exports 对象本质上来说只是 Node 模块包装器的一个形参,直接对其进行赋值,只会改变形参的引用,但并不能改变作用域外的值。
1 | var change = function (a) { |
所以如果要实现 require 引入一个类的效果,请赋值给 module.exports 对象。
更详细的解释,可以查看 exports 快捷方式。
我个人的理解是:module 对象在 Node 执行时创建,并且自带 exports 属性,而 exports 对象是对 module.exports 的值引用,当 module.exports 改变的时候, exports 不会被改变,而模块导出的时候,真正导出的是 module.exports,而不是 exports。
看这个例子:
math.js
1 | exports.add = function () { |
test.js
1 | var math = require('./math') |
可以看出,exports 上赋的值,在 module.exports 被重写后无效。
Node 的核心模块分为 C/C++ 编写和 JavaScript 编写的两部分。其中 C/C++ 文件在 src 目录下,JavaScript 文件在 lib 目录下。
(1) JavaScript 核心模块编译过程
在编译所有的 C/C++ 文件之前,编译程序需要将所有的 JavaScript 模块文件编译为 C/C++ 代码。
src/node.js
和 lib/*.js
)转换为 C++ 里面的数组,生成 node_natives.h
头文件。process.binding('natives')
取出,编译成功后模块缓存在 NativeModule._cache
,文件模块则缓存在 Module._cache
。1 | function NativeModule (id) { |
(2) C/C++ 核心模块的编译过程
Node 的高性能,很大程度是因为核心模型,多数有 C/C++ 编写,C++ 模块主内完成核心,JS 模块主外实现封装模块,充分利用了脚本语言易编写,C/C++ 高效执行的优点。Node 中常见的 buffer、crypto、evals、fs、os 等模块都是 C/C++ 编写的。
(3) 核心模块引入流程
(4) 模块调用栈
(5) 包与 NPM
在 Node 中,包和 NPM 是将模块联系起来的一种机制。CommonJS 规范中包目录应该包含如下这些文件。
NPM 全局安装:
通过执行命令 npm install express -g
将 express 安装为全局可用的可执行命令,但并不意味着可以从任何地方通过 require() 都可以引入它。
实际上,全局安装的包都被安装在一个统一的目录下,这个目录为:
path.resolve(process.execPath, '..', '..', 'lib', 'node_modules')
这个路径是 Node 可执行文件的路径,比如,Node 可执行文件的路径为 /usr/local/bin/node
,那么模块目录就是 /usr/local/lib/node_modules
。
关于更多 JavaScript 模块的规范可以参考 前端模块化-CommonJS,AMD,CMD,ES6。
]]>http://www.google.com
,出来的是百度的页面。超文本传输安全协议(英文:Hypertext Transfer Protocol Secure 缩写为:HTTPS,常称为 HTTP over TLS/SSL 或 HTTP Secure)是一种通过计算机网络进行安全通信的传输协议。HTTPS 经由 HTTP 进行通信,但利用 SSL/TLS 来加密数据包。关于 TLS/SSL 的详细内容,可以查看传输层安全性协议。
传统的 HTTP 协议基于 TCP/IP 协议来传递数据,客户端通过三次握手与服务端建立连接,HTTPS 在传输数据之前需要客户端与服务端之间进行一次握手,在握手的过程中确立双方加密传输数据的密码信息。TLS/SSL 使用非对称加密、对称加密以及 HASH 算法。握手过程可以简单描述如下:
(1) 浏览器向服务器发送自己所支持的加密规则。
(2) 服务器从中选取一组加密算法与 HASH 算法,将自己的身份信息以证书(CA)的形式返回给浏览器。证书里面包含网站地址,加密公钥 S_PuKey 以及证书的颁发机构等信息。
(3) 客户端确认其颁发的证书的有效性,如果证书有效浏览器会生成一串随机数的密码 C_Key,并用证书中提供的公钥 S_PuKey 加密。然后客户端使用约定好的 HASH 计算握手消息,并使用生成的随机数 C_Key 对消息进行加密,最后将之前生成的所有信息发送给服务器。
(4) 服务器使用自己的私钥将信息解密取出密码 C_Key,使用 C_Key 解密浏览器发来的握手消息,并验证 HASH 是否与浏览器发来的一致。然后服务器使用密码加密一段握手消息,发送给浏览器。
(5) 浏览器解密并计算握手消息的 HASH,如果与服务端发来的 HASH 一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码 C_Key 并利用对称加密算法进行加密。
HTTPS 一般使用的加密与 HASH 算法如下:
由于浏览器生成的密码是整个数据加密的关键,因此在传输的时候使用了非对称加密算法对其加密。非对称加密算法会生成公钥和私钥,公钥只能用于加密数据,因此可以随意传输,而网站的私钥用于对数据进行解密,所以网站都会非常小心的保管自己的私钥,防止泄漏。
TLS握手过程中如果有任何错误,都会使加密连接断开,从而阻止了隐私信息的传输。
SSL 证书验证失败有以下三点原因:
HTTPS 是否安全,是一个相对的概念,从服务器身份认证,保护交换数据的隐私性和完整性方面来说,它是安全的,但是它也有自己的局限。
TLS/SSL 协议依赖浏览器和服务器所支持的加密算法。
HTTPS 也不能防止网站被爬虫抓取,攻击者可以根据某些手段推测加密后的密文,从而使选择密文攻击成为可能。
Charles 抓 HTTPS 包的过程可以理解为中间人攻击。
浏览器和服务器每次新建会话时都使用非对称密钥交换算法协商出对称密钥,也就是上文所说的 C_Key,使用 C_Key 完成应用数据的加解密和验证,整个会话过程中的密钥只在内存中生成和保存,而且每个会话的 C_Key 都不相同,并且无法窃取。
所以整个加密过程中,至关重要的就是客户端生成的对称秘钥 C_Key,中间人攻击是先伪装服务器向浏览器发送伪造的公钥,从而取得浏览器的私钥。这样就完成的浏览器端和服务器端的解密。
中间人攻击:是指攻击者与通讯的两端分别建立独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制。
整个过程可以总结为以下:
(1) 首先代理软件截获浏览器发送给服务端的 HTTPS 请求,然后代理软件假装自己是浏览器向服务器发送请求进行握手。
(2) 代理软件获取服务器 CA 证书,验证 CA 证书并且解密后获取公钥 S_PuKey。
(3) 代理软件伪造 CA 证书,冒充服务器传递给客户端浏览器。浏览器解析 CA 证书,使用代理软件伪造的 S_PubKey 生成 C_Key。浏览器根据 C_Key 加密消息,并且计算 HASH,传递给代理软件。
(4) 代理软件根据私钥解析密文计算出浏览器的 C_Key,然后再使用服务端的公钥 S_PuKey 进行加密后返回给服务器。服务器用自己的私钥解开密文后与代理软件建立信任,握手完成。
(5) 代理软件获取服务器发送的密文,用对称秘钥解开,计算出服务器的明文(Charles 为什么能抓 HTTPS 的包?),再次加密后返回给浏览器。
(6) 整个通信过程中,代理软件一直拥有对称秘钥,因此整个 HTTPS 的过程中,信息始终对其透明。
综述:代理软件能够抓取 HTTPS 的核心是让浏览器信任代理软件的证书,使用这个证书代替要访问网站的证书。由于 CA 证书只能有特定的信任机构才能颁发,所以一般来说,中间人是无法欺骗浏览器获取信任的。而我们自己抓包则是主动信任了代理软件的证书,因此达到了使用代理软件可以抓取 HTTPS 的功能。
白屏时间是指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间。
首屏时间是指浏览器从响应用户输入网络地址,到首屏内容渲染完成的时间。
白屏时间 = 地址栏输入网址后回车 - 浏览器出现第一个元素
首屏时间 = 地址栏输入网址后回车 - 浏览器第一屏渲染完成
影响白屏时间的因素:网络,服务端性能,前端页面结构设计。
影响首屏时间的因素:白屏时间,资源下载执行时间。
以百度为例,将 chrome 网速调为 Fast 3G,然后打开 Performance 工具,点击 “Start profiling and reload page” 按钮,查看 Screenshots 如下图:
通常认为浏览器开始渲染 <body>
或者解析完 <head>
的时间是白屏结束的时间点。
1 |
|
白屏时间 = firstPaint - performance.timing.navigationStart || pageStartTime
关于首屏时间是否包含图片加载网上有不同的说法,个人认为,只要首屏中的图片加载完成,即是首屏完成,不在首屏中的图片可以不考虑。
计算首屏时间常用的方法有:
(1) 首屏模块标签标记法
由于浏览器解析 HTML 是按照顺序解析的,当解析到某个元素的时候,你觉得首屏完成了,就在此元素后面加入 script
计算首屏完成时间。
1 |
|
(2) 统计首屏内加载最慢的图片/iframe
通常首屏内容中加载最慢的就是图片或者 iframe 资源,因此可以理解为当图片或者 iframe 都加载出来了,首屏肯定已经完成了。
由于浏览器对每个页面的 TCP 连接数有限制,使得并不是所有图片都能立刻开始下载和显示。我们只需要监听首屏内所有的图片的 onload 事件,获取图片 onload 时间最大值,并用这个最大值减去 navigationStart 即可获得近似的首屏时间。
1 |
|
Performance 接口可以获取到当前页面与性能相关的信息。
(1) Performance.timing
在 chrome 中查看 performance.timing 对象:
与浏览器对应的状态如下图:
左边红线代表的是网络传输层面的过程,右边红线代表了服务器传输回字节后浏览器的各种事件状态,这个阶段包含了浏览器对文档的解析,DOM 树构建,布局,绘制等等。
1 | // 计算加载时间 |
(2) Performance.navigation
window.location.reload()
刷新页面;(3) Performance.memory
(1) DOMContentLoaded 是指页面元素加载完毕,但是一些资源比如图片还无法看到,但是这个时候页面是可以正常交互的,比如滚动,输入字符等。 jQuery 中经常使用的 $(document).ready()
其实监听的就是 DOMContentLoaded 事件。
(2) load 是指页面上所有的资源(图片,音频,视频等)加载完成。jQuery 中 $(document).load()
监听的是 load 事件。
1 | // load |
HTTP 缓存通常要配合客户端(浏览器)使用才能发挥效果,所以又被称之为浏览器缓存,是 Web 性能优化的一大利器。
浏览器缓存分为强缓存和协商缓存。
(1) 强缓存:浏览器在加载资源的时候,根据资源的 HTTP Header 判断它是否命中强缓存,如果命中,浏览器直接从自己的缓存中读取资源,不会发请求到服务器。
(2) 协商缓存:当强缓存没有命中的时候,浏览器向服务器发送请求,服务器端依据资源的另外一些 HTTP Header 验证这个资源是否命中协商缓存,如果协商缓存命中,服务器会将这个请求返回 304,浏览器从缓存中加载这个资源;若未命中请求,服务端返回 200 并将资源返回客户端,浏览器更新本地缓存数据。
另外一种分类方式,可以将浏览器缓存分成 HTTP 协议缓存和非 HTTP 协议缓存。
(1) 非 HTTP 协议缓存:使用 HTML Meta 标签,开发者可以告诉浏览器是否缓存当前页面。
1 | <META HTTP-EQUIV="Pragma" CONTENT="no-cache"> |
上述代码告诉浏览器当前页面不能被缓存,每次访问都要去服务端拉取。只有部分浏览器支持,缓存代理服务器不支持。
(2) HTTP 协议缓存:通过在 HTTP 协议头里面定义一些字段来告诉浏览器当前资源是否缓存,比如 Cache-Control, Expires, Last-Modified, Etag 等。
(1) Pragma:设置资源是否缓存,no-cache 表示不缓存。在 HTTP/1.1 中被 Cache-Control 替代,所以优先级低于 Cache-Control。
(2) Expires:设置资源过期时间,Expires 的值对应一个 GMT(格林尼治时间) 来告诉浏览器资源什么时间过期。缺点是如果客户端与服务端时间相差很大,会导致时间计算不精确,在 HTTP/1.1 中被 max-age 取代。
(1) Cache-Control:设置一个相对的时间,在缓存判定的时候,由浏览器进行判断。Cache-Control 的值可以是 public, private, no-cache, no-store, no-transform 等。
max-age(单位为 s) 设定缓存最大的有效时间,Cache-Control: max-age=3600
表示该资源在浏览器端一个小时内均有效。
s-maxage(单位是 s) 设定共享缓存时间,比如 CDN 或者代理。
no-store 网络资源不缓存,每次都到服务器上拉取。
no-cache 表示网络资源可以缓存一份,但使用前必须询问服务器此资源是不是最新的。
public 表明响应可以被任何对象(客户端,代理服务器等)缓存。
private 表明响应只能被单个用户缓存,其它用户或者代理服务器不能缓存这些数据。
(2) Last-Modified/If-Modified-Since:
Last-Modified 表示响应资源最后修改时间,需要与 Cache-Control 共同使用,是检查服务端资源更新的一种方式。
If-Modified-Since 表示资源过期时(超过 max-age),发现资源具有 Last-Modified 声明,则再次向web服务器请求时带上头 If-Modified-Since,表示请求时间。web 服务器收到请求后发现 Header 中有 If-Modified-Since 则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),HTTP 200;若最后修改时间较旧,说明资源无新修改,则响应HTTP 304 (无需包体,节省浏览),告知浏览器继续使用所保存的cache。
(3) Etag/If-None-Match:
Etag 是根据资源内容生成的一段 hash 字符串,标识资源的状态,由服务端产生。浏览器将这串字符串传回服务器,验证资源是否发生修改。
If-None-Match 表示当资源过期时(超过 max-age),发现资源有 Etag 声明,向 web 服务器发送请求时带上 If-None-Match (Etag 值)。web 服务器收到请求后发现 Header 中带有 If-None-Match 则与被请求资源的相应校验串进行对比,决定返回 200 或者 304。
Etag 可以解决 Last-Modified 存在的一些问题:
(1) F5 刷新页面时,会跳过强缓存,检查协商缓存。
(2) ctrl + F5 强制刷新页面时,之间从服务端加载数据,跳过强缓存和协商缓存。
缓存技术几乎存在于网络技术发展的各个角落,从数据库到服务器,从服务器到网络,再从网络到客户端,缓存随处可见。跟前端有关的缓存技术主要有:DNS 缓存,HTTP 缓存,浏览器缓存,HTML5 缓存(localhost/manifest)和 service worker 中的 cache api。
当用户在浏览器中输入网址的地址后,浏览器要做的第一件事就是解析 DNS:
(1) 浏览器检查缓存中是否有域名对应的 IP,如果有就结束 DNS 解析过程。浏览器中的 DNS 缓存有时间和大小双重限制,时间一般为几分钟到几个小时不等。DNS 缓存时间过长会导致如果 IP 地址发生变化,无法解析到正确的 IP 地址;时间过短会导致浏览器重复解析域名。
(2) 如果浏览器缓存中没有对应的 IP 地址,浏览器会继续查找操作系统缓存中是否有域名对应的 DNS 解析结果。我们可以通过在操作系统中设置 hosts 文件来设置 IP 与域名的关系。
(3) 如果还没有拿到解析结果,操作系统就会把域名发送给本地区的域名服务器(LDNS),LDNS 通常由互联网服务提供商(ISP)提供,比如电信或者联通。这个域名服务器一般在城市某个角落,并且性能较好,当拿到域名后,首先也是从缓存中查找,看是否有匹配的结果。一般来说,大多数的 DNS 解析到这里就结束了,所以 LDNS/ISP DNS 承担了大部分的域名解析工作。如果缓存中有 IP 地址,就直接返回,并且会被标记为非权威服务器应答。
第三步有一点需要注意的是,如果用户在自己电脑里设置了 DNS,比如 Google 的
8.8.8.8
或者 CloudFlare 新出的1.1.1.1
,将不会通过 ISP DNS 服务器解析。
(4) 如果前面三步还没有命中 DNS 缓存,那只能到 Root Server 域名服务器中请求解析了。根域名服务器拿到请求后,首先判断域名是哪个顶级域名下的,比如 .com
, .cn
, .org
等,全球一共 13 台顶级域名服务器。根域名服务器返回对应的顶级域名服务器(gTLD Server)地址。
(5) 本地域名服务器(LDNS)拿到地址后,向 gTLD Server 发送请求,gTLD 服务器查找并且返回此域名对应的 Name Server 域名服务器地址。这个 Name Server 通常就是用户注册的域名服务器,例如用户在某个域名服务提供商申请的域名,那么这个域名解析任务就由这个域名提供商的服务器来完成。
这个过程的解析方式为递归搜索。比如:
https://movie.lz5z.com
,本地域名服务器首先向顶级域名服务器(com 域)发送请求,com 域名服务器将域名中的二级域lz5z
的 IP 地址返回给 LDNS,LDNS 再向二级域名服务器发送请求进行查询,之后不断重复直到 LDNS 得到最终的查询结果。
(6) Name Server 域名服务器会查询存储的域名和 IP 的映射关系表,在正常情况下都根据域名得到目标 IP 地址,连同一个 TTL 值返回给 LDNS。LDNS 会缓存这个域名和 IP 的对应关系,缓存时间由 TTL 值控制。LDNS 会把解析结果返回给用户,DNS 解析结束。
(1) chrome: chrome://net-internals/#dns
(2) 本地 DNS :Windows: ipconfig /flushdns
; Linux 和 mac 根据不同的版本有不同的方式
(1) 减少 DNS 查询,避免重定向。
(2) DNS 预解析:
1 | <meta http-equiv="x-dns-prefetch-control" content="on" /> |
<link rel="dns-prefetch" href="https://lz5z.com" />
(3) 域名发散/域名收敛
PC 端因为浏览器有域名并发请求限制(chrome 为 6 个),也就是同一时间,浏览器最多向同一个域名发送 6 个请求,因此 PC 端使用域名发散策略,将 http 静态资源放入多个域名/子域名中,以保证资源更快加载。常见的办法为使用 cdn。
将静态资源放在同一个域名下,减少 DNS 解析的开销。域名收敛是移动互联网时代的产物,在 LDNS 没有缓存的情况下,DNS 解析占据一个请求的大多数时间,因此,采用尽可能少的域名对整个页面加载速度有显著的提高。
(4) HttpDNS
DNS 请求使用的是 UDP 协议,虽然没有 TCP 三次握手的开销,但是可能导致弱网环境下(2G,3G)数据丢失的问题。还记得之前Web 性能优化-页面重绘和回流(重排)中提到的 Google 1s 终端首屏渲染标准,假如 DNS 解析出现问题,那可能几秒甚至几十秒都首屏不了了。而且国内牛 X 的运营商的品质你也是知道的,随便劫持一下 DNS 就让你的 web 应用不见天日。
为了应对以上两个问题,HttpDNS 应运而生,原理也非常简单,将 DNS 这种容易被劫持的协议,转而使用 HTTP 协议请求 Domain 与 IP 地址之间的映射。获得正确的 IP 地址后,就不用担心 ISP 篡改数据了。
国内腾讯云和阿里云都有相应的解决方案
Google 的方案则更近一步,使用 https 协议。
上一篇文章学习了重绘和回流对页面性能的影响,是从比较宏观的角度去优化 Web 性能,本篇文章从每一帧的微观角度进行分析,来学习 CSS3 硬件加速的知识。
CSS3 硬件加速又叫做 GPU 加速,是利用 GPU 进行渲染,减少 CPU 操作的一种优化方案。由于 GPU 中的 transform 等 CSS 属性不会触发 repaint,所以能大大提高网页的性能。
我做了一个页面,左边元素的动画通过 left/top 操作位置实现,右边元素的动画通过 transform: translate
实现,你可以打开 chrome 的 “Paint flashing” 查看,绿色部分是正在 repaint 的内容。
从 demo 中可以看到左边的图形在运动时外层有一圈绿色的边框,表示元素不停地 repaint,并且可以看到其运动过程中有丢帧现象,具体表现为运动不连贯,有轻微闪动。
之前学习 flash 的时候,就知道动画是由一帧一帧的图片组成,在浏览器中也是如此。我们首先看一下,浏览器每一帧都做了什么。
- JavaScript:JavaScript 实现动画效果,DOM 元素操作等。
- Style(计算样式):确定每个 DOM 元素应该应用什么 CSS 规则。
- Layout(布局):计算每个 DOM 元素在最终屏幕上显示的大小和位置。由于 web 页面的元素布局是相对的,所以其中任意一个元素的位置发生变化,都会联动的引起其他元素发生变化,这个过程叫 reflow。
- Paint(绘制):在多个层上绘制 DOM 元素的的文字、颜色、图像、边框和阴影等。
- Composite(渲染层合并):按照合理的顺序合并图层然后显示到屏幕上。
浏览器在获取 render tree(详细知识可以查看Web性能优化-页面重绘和回流(重排))后,渲染树中包含了大量的渲染元素,每一个渲染元素会被分到一个图层中,每个图层又会被加载到 GPU 形成渲染纹理。GPU 中 transform 是不会触发 repaint 的,这一点非常类似 3D 绘图功能,最终这些使用 transform 的图层都会由独立的合成器进程进行处理。
过程如下:
render tree -> 渲染元素 -> 图层 -> GPU 渲染 -> 浏览器复合图层 -> 生成最终的屏幕图像。
TIPS: chrome devtools 中可以开启 Rendering 中的 Layer borders 查看图层纹理。
其中黄色边框表示该元素有 3d 变换,表示放到一个新的复合层(composited layer)中渲染,蓝色栅格表示正常的 render layer。
在文章开始给出的例子中,我们也可以开启 Layer borders,可以观察到,使用 transform: translate
动画的元素,外围有一个黄色的边框,可知其为复合层。
在 GPU 渲染的过程中,一些元素会因为符合了某些规则,而被提升为独立的层(黄色边框部分),一旦独立出来,就不会影响其它 DOM 的布局,所以我们可以利用这些规则,将经常变换的 DOM 主动提升到独立的层,那么在浏览器的一帧运行中,就可以减少 Layout 和 Paint 的时间了。
哪些规则能让浏览器主动帮我们创建独立的层呢?
关于 z-index 导致的硬件加速的问题,可以查看这篇文章 CSS3硬件加速也有坑!!
CSS 中的以下几个属性能触发硬件加速:
如果有一些元素不需要用到上述属性,但是需要触发硬件加速效果,可以使用一些小技巧来诱导浏览器开启硬件加速。
1 | .element { |
注意:我在不同的资料中查到的 transform 是否能触发硬件加速的结果不同,自己测试后,发现结果是可以。
(1)过多地开启硬件加速可能会耗费较多的内存,因此什么时候开启硬件加速,给多少元素开启硬件加速,需要用测试结果说话。
(2)GPU 渲染会影响字体的抗锯齿效果。这是因为 GPU 和 CPU 具有不同的渲染机制,即使最终硬件加速停止了,文本还是会在动画期间显示得很模糊。
早在五年前,Google 就提出了 1s 完成终端页面的首屏渲染的标准。
常见的优化网络请求的方法有:DNS Lookup,减少重定向,避免 JS、CSS 阻塞,并行请求,代码压缩,缓存,按需加载,前端模块化…
虽然相较于网络方面的优化,前端渲染的优化显得杯水车薪,而且随着浏览器和硬件性能的增长,再加上主流前端框架(react、vue、angular)的已经帮我们解决了大多数的性能问题,但是前端渲染性能优化依然值得学习,除去网络方面的消耗,留给前端渲染的时间已经不多了。本文主要学习前端渲染相关的问题。
- 浏览器把获取到的 HTML 代码解析成1个 DOM 树,HTML 中的每个 tag 都是 DOM 树中的1个节点,根节点是 document 对象。DOM 树里包含了所有 HTML 标签,包括
display:none
隐藏的标签,还有用 JS 动态添加的元素等。- 浏览器把所有样式解析成样式结构体,在解析的过程中会去掉浏览器不能识别的样式,比如 IE 会去掉 -moz 开头的样式。
- DOM Tree 和样式结构体组合后构建 render tree, render tree 类似于 DOM tree,但区别很大,render tree 能识别样式,render tree 中每个 NODE 都有自己的 style,而且 render tree 不包含隐藏的节点 (比如
display:none
的节点,还有 head 节点),因为这些节点不会用于呈现,而且不会影响呈现的节点,所以就不会包含到 render tree 中。注意visibility:hidden
隐藏的元素还是会包含到 render tree 中的,因为visibility:hidden
会影响布局(layout),会占有空间。根据 CSS2 的标准,render tree 中的每个节点都称为 Box (Box dimensions),理解页面元素为一个具有填充、边距、边框和位置的盒子。- 一旦 render tree 构建完毕后,浏览器就可以根据 render tree 来绘制页面了。
总结为下图:
图片来自 浏览器渲染页面过程与页面优化
在此过程中,前端工程师主要的敌人为:
在回流的时候,浏览器会使 render tree 中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。因此回流必将引起重绘,而重绘不一定会引起回流。
Reflow 的成本比 Repaint 高得多的多。DOM Tree 里的每个结点都会有 reflow 方法,一个结点的 reflow 很有可能导致子结点,甚至父点以及同级结点的 reflow。
F12 打开控制台 -> DevTools -> Show console drawer -> Rendering -> 勾选 Paint flashing。
当一个元素的外观的可见性 visibility 发生改变的时候,但是不影响布局。类似的例子包括:outline, visibility, background color。
1 | var s = document.body.style |
如果向上述代码中那样,浏览器不停地回流+重绘,很可能性能开销非常大,实际上浏览器会优化这些操作,将所有引起回流和重绘的操作放入一个队列中,等待队列达到一定的数量或者时间间隔,就 flush 这个队列,一次性处理所有的回流和重绘。
虽然有浏览器优化,但是当我们向浏览器请求一些 style 信息的时候,浏览器为了确保我们能拿到精确的值,就会提前 flush 队列。
requestAnimationFrame:能保证浏览器在正确的时间进行渲染。
保持 DOM 操作“原子性”:
1 | // bad |
className 只要赋值,就一定出现一次 rendering 计算;classList 的 add 和 remove,浏览器会进行样式名是否存在的判断,以减少重复的 rendering。
1 | ele.className += 'something' |
1 | // bad |
display: none
,完成后再将其显示出来,这样只会触发一次回流和重绘。假如需要在下面的 html 中添加两个 li 节点:
1 | <ul id=""> |
使用 JavaScript:
1 | let ul = document.getElementByTagName('ul') |
上述代码会发生两次回流,假如使用 display: none
的方案,虽然能够减少回流次数,但是会发生一次闪烁,这时候使用 DocumentFragment 的优势就体现出来了。
DocumentFragment 有两大特点:
1 | let fragment = document.createDocumentFragment() |
可见 DocumentFragment 是一个孤儿节点,没爹就能出生,但是在需要它的时候,它又无私地把孩子奉献给文档树,然后自己默默离开。是不是有点像《银翼杀手2049》?
随着 JavaScript 工程越来越大,团队协作不可避免,为了更好地对代码进行管理和测试,模块化的概念逐渐引入前端。模块化可以降低协同开发的成本,减少代码量,同时也是“高内聚,低耦合”的基础。
模块化主要解决两个问题:
在各种模块化规范出来之前,人们使用匿名闭包函数解决模块化的问题。
1 | var num0 = 2; // 注意这里的分号 |
这样做的好处是,你可以在函数内部使用全局变量和局部变量,并且不用担心局部变量污染全局变量。这种用括号把匿名函数包起来的方式,也叫做立即执行函数(IIFE)。所有函数内部代码都在闭包(closure)内。它提供了整个应用生命周期的私有和状态。
CommonJS 将每个文件都视为一个模块,在每个模块中变量默认都是私有变量,通过 module.exports 定义当前模块对外输出的接口,通过 require 加载模块。
(1) 使用方法:
circle.js
1 | const { PI } = Math |
app.js
1 | const circle = require('./circle.js') |
(2) 原理:node 在编译 js 文件的过程中,会使用一个如下的函数包装器将其包装模块包装器:
1 | (function (exports, require, module, __filename, __dirname) { |
这也是为什么在 node 环境中可以使用这几个没有显式定义的变量的原因。其中 __filename
和 __dirname
在查找文件路径的过程中分析得到后传入的。module 变量是这个模块对象自身,exports 是在 module 的构造函数中初始化的一个空对象。
更详细的内容可以参考 node modules
关于什么时候使用 exports、什么时候使用 module.exports,可以参考 exports shutcut
(3) 优点 vs 缺点
CommonJS 能够避免全局命名空间污染,并且明确代码之间的依赖关系。但是 CommonJS 的模块加载是同步的,假如一个模块引用三个其它模块,那么这三个模块需要被完全加载后这个模块才能运行。这在服务端不是什么问题(node),但是在浏览器端就不是那么高效了,毕竟读取网络文件比本地文件要耗时的多。
AMD 全称异步模块化定义规范(Asynchronous Module Definition),采用异步加载模块的方式,模块的加载不影响后面语句的执行,并且使用 callback 回调函数的方式来运行模块加载完成后的代码。
(1) 使用方式
定义一个 myModule 的模块,它依赖 jQuery 模块:
1 | define('myModule', ['jQuery'], function ($) { |
第一个参数表示模块 id,为可选参数,第二个参数表示模块依赖,也是可选参数。
使用 myModule 模块:
1 | require(['myModule', function (myModule) {}]) |
requirejs 是 AMD 规范的一个实现,详细的使用方法可以查看官方文档。
CMD 规范来源于 seajs,CMD 总体于 AMD 使用起来非常接近,AMD 与 CMD 的区别,可以查看 与 RequireJS 的异同](https://github.com/seajs/seajs/issues/277)
(1) 使用方式:
1 | // CMD |
CMD 推崇依赖就近,可以把依赖写进你的代码中的任意一行,AMD 是依赖前置的,在解析和执行当前模块之前,模块必须指明当前模块所依赖的模块。
UMD(Universal Module Definition)并不是一种规范,而是结合 AMD 和 CommonJS 的一种更为通用的 JS 模块解决方案。
在打包模块的时候经常会见到这样的写法:
1 | output: { |
表示打包出来的模块为 umd 模块,既能在服务端(node)运行,又能在浏览器端运行。我们来看 vue 打包后的源码 vue.js
1 | (function (global, factory) { |
代码翻译过来就是:
module.exports = factory()
把 vue 导出 (通过 require(‘vue’) 进行引用)。终于到了 ES6 的时代,JS 开始从语言层面支持模块化,从 node8.5 版本开始支持原生 ES 模块。不过有两点限制:
--experimental-modules
假如有 a.mjs 如下:
1 | export default { |
在 b.mjs 中可以引用:
1 | import a from './a.mjs' |
chrome61 开始也支持 JS module,只需要在 script 属性中添加 type="module"
即可。
1 | <script type="module" src="module.js"></script> |
ES6 module 主要由两个命令组成:export 和 import。
(1) export 命令
1 | // 输出变量 |
需要注意的是,export 命令只能对外输出接口,以下的输出方式均为错误的:
1 | // 报错 |
export 输出的值是动态绑定的,这点与 CommonJS 不同,CommonJS 输出的是值的缓存,不存在动态更新。
如何删除 node 缓存?
1 | let config |
export 命令必须处于模块顶层,如果处于块级作用域内,就会报错。
(2) import 命令
1 | // import 的变量是只读的 |
(3) export default
1 | // a.js |
export default 与 export 输出的模块在引用的时候,差别仅仅是是否用 {}
将变量包起来。
1 | function add (x, y) { |
(4) export 与 import 复合写法
1 | export { foo, bar } from 'a.js' |
JavaScript 具有自动垃圾回收机制,这种垃圾回收机制原理其实很简单:找出那些不再继续使用的变量,然后释放其所占用的内存,垃圾回收器会按照固定的时间间隔周期性地执行这一操作。局部变量只有在函数执行的过程中存在,在这个过程中,会为局部变量在栈(或者堆)内存上分配空间,然后在函数中使用这些变量,直至函数执行结束。垃圾回收器必须追踪哪个变量有用哪个没用,对于不再有用的变量打上标记,以备将来回收其占用的内存,用于标识无用变量的策略主要有标记清除法和引用计数法。
JavaScript 在定义变量时就完成了内存分配,还可以通过函数调用分配内存:
1 | /** |
使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。
JavaScript 中最常用的垃圾回收方式就是标记清除(mark-and-sweep),当变量进入环境时,就将这个变量标记“进入环境”,当变量离开环境时,就将其标记为“离开环境”。至于怎么标记有很多种方式,比如特殊位的反转、维护一个列表等。
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后它会去掉环境中的变量已经被环境中变量被标记为引用的变量,在此之后再被标记的变量将被视为准备删除的变量。最后垃圾回收器清除标记的变量,回收它们所占用的内存空间。
目前主流浏览器都是使用标记清除式的垃圾回收策略,只不过收集的间隔有所不同。
引用计数跟踪几个每个值被引用的次数,当声明一个引用类型值赋给该变量时,则这个值的引用次数就是 1,如果同一个值被赋给另外一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,就可以将其内存空间回收。当垃圾回收器再次运行时,它就会释放哪些引用次数为 0 的值所占用的内存。
Netscape Navigator 3.0 是最早使用引用计数策略的浏览器,它很快就遇到一个严重的问题:循环引用。
1 | function problem () { |
在这个例子中,obj1 和 obj2 通过各自的属性相互引用,也就是说,这两个对象的引用次数都是 2。在采用标记清除策略的现实中,由于函数执行后,两个对象都离开了作用域,因此相互引用不存在问题。
但是在引用计数策略中,当函数执行完毕后,obj1 和 obj2 还得继续存在,因为它们的引用次数永远不会是 0,导致内存无法回收。
Netscape Navigator 4.0 中放弃了引用计数,转而使用标记清除来实现垃圾回收。
IE 存在的问题:
在 IE9 之前,IE 中有一部分对象并不是原生 JavaScript 对象。例如,BOM 和 DOM 中的对象就是 C++ 实现的 COM 对象,而 COM 对象的垃圾收集机制采用的是引用计数策略。因此,即使 IE 中的 JavaScript 引擎使用标记清除策略实现,但是 JS 访问的 COM 对象依然是基于引用计数策略的。可以在 IE 中涉及到 COM 对象,就会存在循环引用的问题。
1 | var ele = document.getElementById('some_element') |
在这个例子中一个 DOM 元素与一个原生 JS 对象之间创建了循环引用,由于 COM 的引用计数的垃圾回收策略,导致例子中的 DOM 从页面删除,也不会被垃圾回收。
解决办法:
1 | obj.ele = null |
将变量设置为 null 意味着切断变量和它此前引用值之间的连接。当垃圾回收器下次运行时,就能删除这些值并回收它们占用的内存。
IE9 之后,DOM 和 BOM 对象都被转换成立真正的 JS 对象,这样就避免了两种垃圾回收算法并存导致的问题。
垃圾收集器是周期性运行,因此其运行时间间隔是一个非常重要的问题。IE7 之前的垃圾收集器是根据内存分配量运行的,达到某一个临界值(256 个变量,4096 个对象、或者 64 KB 字符串)就是启动垃圾回收器,这导致了一个问题:如果该脚本在其生命周期需要一直保持这么多变量,垃圾回收器就不得不频繁运行。
事实上,浏览器中一般可以主动触发垃圾收集过程。在 IE 中,调用 window.CollectGarbage()
方法会立即执行垃圾收集,在 Opera7 之后的版本中,调用 window.opera.collect()
也会启动垃圾收集。
比较好的办法就是执行代码中只保留必要的数据,一旦数据不再有用,通过设置为 null 来释放其引用(dereferencing),适用于大多数全局变量和全局对象的属性。
V8 引擎会限制 JavaScript 所能使用的内存大小,64 位系统是 1.4GB,32 位系统是 0.7GB。在 Node 环境中使用下面两个参数可以调整启动时内存限制的大小:
1 | node --max-nex-space-size=1024 app.js // 单位为KB |
这两条命令分别对应 Node 内存堆中的「新生代」和「老生代」
V8 将堆分为了几个不同的区域:
脚本中,绝大多数对象的生存期很短,只有某些对象的生存期较长。为利用这一特点,V8将堆进行了分代。对象起初会被分配在新生区(通常很小,只有 1-8 MB,具体根据行为来进行启发)。在新生区的内存分配非常容易:我们只需保有一个指向内存区的指针,不断根据新对象的大小对其进行递增即可。当该指针达到了新生区的末尾,就会有一次清理(小周期),清理掉新生区中不活跃的死对象。对于活跃超过 2 个小周期的对象,则需将其移动至老生区。老生区在标记-清除或标记-紧缩(大周期)的过程中进行回收。大周期进行的并不频繁。一次大周期通常是在移动足够多的对象至老生区后才会发生。至于足够多到底是多少,则根据老生区自身的大小和程序的动向来定。
JavaScript 是一种垃圾回收语言,垃圾回收语言通过周期性地检查之前被分配的内存是否可以从应用的其它部分访问来帮助开发者管理内存。内存泄露是指当一块内存不再被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或者内存池的现象。内存泄漏可能会导致应用程序卡顿或者崩溃。
在 chrome 中可以通过 performance 中的 Memory record 来查看,选中 Memory 后点击左边的 Record,然后模拟用户的操作,一段时间后点击 stop,在面板上查看这段时间的内存占用情况。如果内存基本平稳,则无内存泄漏情况;如果内存占用不断飙升,内可能出现内存泄漏的情况。
在 Node 环境中,可以输入 process.memoryUsage()
查看 Node 进程的内存占用情况。
判断内存泄漏,以 heapUsed 字段为准。
《JavaScript高级程序设计》中提到了一种内存泄漏:由于 IE9 之前的版本对 JS 对象和 DOM 对象中使用的垃圾回收机制,会导致如果闭包的作用域链中保存着一个 HTML 元素,那该元素将无法销毁。
1 | function assignHandler () { |
以上代码创建了一个作为 element 元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用,匿名函数中保存了一个对 element 对象的引用,因此无法减少 element 的引用数。只要匿名函数在,element 的引用数至少是 1,因此它所占用的内存就永远无法回收。
解决办法:
1 | function assignHandler () { |
注意: 上述问题在现代浏览器上并不会出现
在 JavaScript 非严格模式中,未定义的变量会自动绑定在全局对象上(window/global),比如:
1 | function foo () { |
foo 执行的时候,由于内部变量没有定义,所以相当于 window.bar = 'something'
,函数执行完毕,本应该被销毁的变量 bar 却永久的保留在内存中了。
解决办法,使用严格模式。
虽然全局变量上绑定的变量无法被垃圾回收,但是有时需要使用全局变量去存储临时信息,这个时候要格外小心,并在变量使用完毕后设置为 null,以回收内存。
1 | window.bar = null |
下面写一个 demo:
1 | function test() { |
将这段脚本放置于浏览器中,打开 chrome performance,记录一段时间后,发现内存线条如下:
同时打开 chrome 任务管理器,会看到代表当前页面的标签页所占用的内存不断飙升。
1 | var nodes = ''; |
这里的 dom 元素虽然已经从页面上移除了,但是 js 中仍然保存这对该 dom 元素的引用,导致内存不能释放。
打开 chrome 控制台 Memory,点击 Take snapshot
:
点击生成的 Snapshot,通过关键字 str
进行 filter:
从上图可知,代码运行结束后,内存中的长字符串依然没有被垃圾回收。
闭包是指函数能够访问父环境中定义的变量。
1 | (function() { |
上面代码中的 unused 是一个闭包,因为其内部引用了父环境中的变量 originalBar,虽然它被没有使用,但 v8 引擎并不会把它优化掉,因为 JavaScript 里存在 eval 函数,所以 v8 引擎并不会随便优化掉暂时没有使用的函数。
需要注意的一点是: 闭包的作用域一旦创建,它们有同样的父级作用域,作用域是共享的。
bar 引用了someMethod,someMethod 这个函数与 unused 这个闭包共享一个闭包上下文。所以 someMethod 也引用了 originalBar 这个变量。
因此引用链如下:
GCHandler -> foo -> bar -> someMethod -> originalBar -> someMethod(old) -> originalBar(older)-> someMethod(older)
造成了闭包的循环引用。
Vim 是一款由 Vi 派生出来的命令行编辑器,具有语法高亮、代码折叠、多语言支持、多视图等强大的功能,并且支持插件扩展和调用脚本语言。Vim 有多种模式,其中最常用的为插入和执行模式,仅仅通过键盘来在这些模式之中切换,大大提高了程序开发效率。
1 | 要移动光标使用 h、j、k、l 键 |
通过 Vim + 文件名进去文件后,默认为普通模式。注意进入普通模式后请勿开启 Shift-Lock(大小写锁定键)。
退出 Vim,按 :q!
<回车>。 这种方式退出编辑器会丢弃进入编辑器以来所做的改动。
在普通模式下,按 x 键来删除光标所在位置的字符。
在普通模式下,按 i 键来插入文本。
按 a 键来添加文本。
插入与添加直接的区别:
插入是在光标前插入文本,添加光标字母后面添加。
使用 :wq
以保存文件并退出
输入 dw
可以从光标处删除至一个单词的末。
输入 d$
从当前光标删除到行末。
输入 de
从当前光标当前位置直到单词末尾,包括最后一个字符。
输入 dd
删除整行。
输入 2dd
删除两行。
输入 2w
使光标向后移动两个单词。
输入 3e
使光标向后移动到第三个单词的末尾。
比如之前的光标位置为:
—> |this is a demo.
输入 2w
:
—> this is |a demo.
输入 2e
:
—> this i|s a demo.
—> |this is a demo
d2w
: —> |a demo.
d2e
: —> | a demo.
输入 u
来撤消最后执行的命令。
输入 U
来撤消对整行的修改。
使用 CTRL-R
(先按 CTRL 再按 R)撤销撤销命令。
删除操作后,输入 p
将最后一次删除的内容置入光标之后。
输入 r
加字符替换光标后一个字符。
要改变文本直到一个单词的末尾,请输入 ce
。
ce
命令相当于删除一个单词的同时,进入插入模式。
使用 c2w
删除两个单词并且进入插入模式。
使用 c$
删除光标后所有内容并且进入插入模式。
输入 CTRL-G
显示当前编辑文件中当前光标所在行位置以及文件状态信息。
输入行号 + G (注意是大写) 可以直接将光标定位于行数。
输入 /
加上字符串,可以在当前文件中查找该字符串。
要查找同上一次的字符串,只需要按 n
键。要向相反方向查找同上一次的字符串,请输入大写 N
即可。
回到之前的位置按 CTRL-O
,重复按可以回退更多步。CTRL-I 会跳转到较新的位置。
提示:如果查找已经到达文件末尾,查找会自动从文件头部继续查找,除非 ‘wrapscan’ 选项被复位。
把光标置于有括号( (、[ 或 { )的地方,按下 %
光标会自动定位到与其配对的括号处。
在一行内替换头一个字符串 old 为新的字符串 new,输入 :s/old/new
。
在一行内替换所有的字符串 old 为新的字符串 new,输入 :s/old/new/g
。
在两行内替换所有的字符串 old 为新的字符串 new,输入 :#,#s/old/new/g
,其中 #, # 代表的是替换操作的若干行中首尾两行的行号。
在文件内替换所有的字符串 old 为新的字符串 new,输入 :%s/old/new/g
进行全文替换时询问用户确认每个替换需添加 c 标志 :%s/old/new/gc
输入 :!
然后紧接着输入一个外部命令可以执行该外部命令,比如 :!ls
可以在 Vim 中查看当前目录。
要将对文件的改动保存到文件中,请输入 :w FILENAME
。 该命令会以 FILENAME 为文件名保存整个文件。
移动光标至某一行,按下 v
键进入可视模式,移动光标选中内容,然后按 :
,屏幕底部会出现 :'<,'>
,再输入 w FILENAME
可将选中的内容报错到 FILENAME 中。
提示:按 v 键使 Vim 进入可视模式进行选取。可以四处移动光标使选取区域变大或变小。接着可以使用一个操作符对选中文本进行操作。例如,按 d 键会删除选中的文本内容。
要向当前文件中插入另外的文件的内容,请输入 :r FILENAME
。
:r FILENAME
可提取磁盘文件 FILENAME 并将其插入到当前文件的光标位置后面。
我们来实现一个最简单的需求,将一个元素从屏幕左边均匀地移动到屏幕右边。
下面是效果:
(1)css animation
用 css 实现是最合理也是最高效的。
1 | @keyframes move_animation1 { |
注:
transform:translateZ(0);
用来开启 chrome GPU 加速,解决动画”卡顿”。
在动画中使用 transform 比 left/top 性能更好,能减少浏览器 repaint。
(2)假如用 JS 实现呢
首先想到的是 setInterval/setTimeout,原理就是利用人眼的视觉残留和电脑屏幕的刷新,让元素以连贯平滑的方式逐步改变位置,最终实现动画的效果。
常用的屏幕刷新频率为 60Hz,一些电竞屏幕则为 144Hz。我们以常用的刷新频率为例,60Hz 意味着屏幕每 1000 / 60 ≈ 16.7ms 刷新一次,所以我们设置 setInterval 的间隔为 16.7ms:
1 | const animateDiv = document.querySelector('.animate-div') |
setInterval/setTimeout 存在两个问题:
- setTimeout 的执行时间并不是确定的。在 Javascript 中, setTimeout 任务被放进了异步队列中,只有当主线程上的任务执行完以后,才会去检查该队列里的任务是否需要开始执行,因此 setTimeout 的实际执行时间一般要比其设定的时间晚一些。
- 刷新频率受屏幕分辨率和屏幕尺寸的影响,因此不同设备的屏幕刷新频率可能会不同,而 setTimeout 只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相同。
以上两种情况都会导致 setTimeout 的执行步调和屏幕的刷新步调不一致,从而引起丢帧现象。 虽然在上述代码中我们将时间间隔设置为 16.7ms,但是还是不能完全避免丢帧的现象。
(3)requestAnimationFrame
requestAnimationFrame 与 setTimeout/setInterval 最大的区别是由系统自己的刷新机制来决定什么时候调用动画函数,开发者只需要定义好动画函数,这个函数会在浏览器重绘之前调用。
requestAnimationFrame 接收一个回调函数作为参数,DOMHighResTimeStamp,指示当前被 requestAnimationFrame() 排序的回调函数被触发的时间。回调函数中传入时间戳作为参数,该时间戳是一个十进制数,单位毫秒,最小精度为 1ms。
1 | const animateDiv = document.querySelector('.animate-div') |
除了精准控制调用时机以外,requestAnimationFrame 还有两大优点:
取消一个先前通过调用 window.requestAnimationFrame()方法返回的动画帧请求。
1 | const animateDiv = document.querySelector('.animate-div') |
requestAnimationFrame 目前还存在兼容性问题,使用 requestAnimationFrame polyfill 来进行优雅降级。
1 | if (!Date.now) |
JS 中有多种方式实现循环:for; for in; for of; while; do while; forEach; map
等等。假如循环里面的内容是异步并且 await 的,那异步代码究竟是像 Promise.all
一样将循环中的代码一起执行,还是每次等待上一次循环执行完毕再执行呢?
forEach 和 map, some, every 循环是并行执行的,相当于 Promise.all,其它 for, for in, for of, while, do while 都是串行执行的。
先定义异步函数 foo 和可遍历数组 arr:
1 | const arr = Array.from({ length: 5 }, (v, k) => k) |
并行执行:
1 | /** |
串行执行:
1 | /** |
首先查看 forEach 的 polyfill,简化后可以理解为以下代码:
1 | Array.prototype.forEach = function(callback, thisArg) { |
可以看到本质上 forEach 还是通过 while 循环来实现的,假如我们想要一个异步的 forEach 的话,只需要将 callback 的调用改成 await 即可:
1 | Array.prototype.forEachAsync = async function(callback, thisArg) { |
npm 上有一个更为完备的解决方案:forEachAsync
1 | const forEachAsync = require('forEachAsync').forEachAsync |
就这么多。
]]>1 | const url = 'https://lz5z.com/000/?a=123&b=456&c=%E4%B8%AD%E6%96%87' |
下次面试官问你的时候,你能答上来吗?😉😉😉
下面是 《JavaScript高级程序设计》 中给出的方案:
1 | function getQueryStringArgs () { |
1 | let obj1 = { x: 1, y: 2 } |
以上的拷贝方式就是浅拷贝,当 obj2 的值改变时,obj1 的值也随之发生改变。
1 | let arr1 = [0, 1, ['a', 'b']] |
Array.prototype.concat(), Array.prototype.slice(), Array.from() 只能实现对一维数组的深拷贝。
1 | let obj1 = { x: 1, y: 2 } |
使用 JSON.parse() + JSON.stringify() 实现深拷贝
1 | let obj1 = { |
JSON.parse 和 JSON.stringify 看起来不错,不过存在一些问题:
1 | JSON.stringify({a: function add (){}}) // '{}' |
1 | function deepClone(o) { |
测试代码:
1 | let obj1 = { |
注意:由于使用 for in
循环,所以只能深度拷贝对象自身属性(非原型链上的属性),并且属性为 enumerable。
使用递归拷贝对象的方法,在目标非常大,层级关系非常深的时候会出现性能问题,具体解决方案可以参考我之前写的 JavaScript递归优化 使用栈代替递归的方式解决。
lodash 中提供 4 个对象拷贝相关的方法:
1 | _.clone() // 提供浅拷贝 |
demo
1 | function customizer(value) { |
相信上述几种方法已经能够满足我们平时大部分的需求了,如果有额外的需求,只能自己定义实现深/浅拷贝的方式了。
]]>unset variable_name
set -u
调用未声明变量报错(默认无提示)1 | x=123 |
1 | x=123 |
Shell 字符串可以用单引号,也可以用双引号,也可以不用引号。
1 | str='Hello World' |
其中双引号中可以出现变量和转义符。
1 | string="abcd" |
提取子字符串
以下实例从字符串第 2 个字符开始截取 4 个字符:
1 | string="Hello World" |
查找字符串
1 | string="Hello World" |
Shell 中只支持一维数组
1 | names=('leo' 'jack' 'tim') |
Shell 没有多行注释
1 | -------------------------------------------- |
创建脚本 test.sh
1 | !/bin/bash |
为脚本设置执行权限,并执行
1 | chmod +x test.sh |
原生 bash 不支持数学运算符,但是可以通过其他命令实现,比如 expr。
1 | val=`expr 2 + 2` # 注意空格 |
1 | [ $a -eq $b ] # -eq 相等 |
1 | a=$1 |
1 | ./test.sh 10 20 |
1 | !/bin/bash |
1 | a=10 |
1 | a="abc" |
1 | echo "\"Are you OK?\"" # 转义字符 |
1 | printf format-string [arguments...] |
%s %c %d %f都是格式替代符
%-10s 指一个宽度为10个字符(-表示左对齐,没有则表示右对齐),任何字符都会被显示在10个字符宽的字符内,如果不足则自动以空格填充,超过也会将内容全部显示出来。
%-4.2f 指格式化为小数,其中.2指保留2位小数。
test 命令用于检查某个条件是否成立,它可以进行数值、字符和文件三个方面的测试。
1 | a=10 |
代码中的 [] 表示执行基本的算数运算。
1 | a=10 |
1 | a="abc" |
1 | -e 文件名 如果文件存在则为真 |
1 | if |
1 | for var in item1 item2 ... itemN |
1 | while condition |
1 | while true |
1 | case 值 in |
case工作方式如上所示。取值后面必须为单词in,每一模式必须以右括号结束。取值可以为变量或常数。匹配发现取值符合某一模式后,其间所有命令开始执行直至 ;;。
取值将检测匹配的每一个模式。一旦模式匹配,则执行完匹配模式相应命令后不再继续其他模式。如果无一匹配模式,使用星号 * 捕获该值,再执行后面的命令。
1 | while : |
1 | while : |
esac
case的语法和C family语言差别很大,它需要一个esac(就是case反过来)作为结束标记,每个case分支用右圆括号,用两个分号表示break。
1 | funWithReturn(){ |
函数返回值在调用该函数后通过 $? 来获得。
注意:所有函数在使用前必须定义。这意味着必须将函数放在脚本开始部分,直至shell解释器首次发现它时,才可以使用。调用函数仅使用其函数名即可。
1 | funWithParam(){ |
1 | command > file # 将输出重定向到 file |
1 | command > /dev/null |
/dev/null 是一个特殊的文件,写入到它的内容都会被丢弃;如果尝试从该文件读取内容,那么什么也读不到。但是 /dev/null 文件非常有用,将命令的输出重定向到它,会起到"禁止输出"的效果。
如果希望屏蔽 stdout 和 stderr,可以这样写:
1 | command > /dev/null 2>&1 |
注意:0 是标准输入(STDIN),1 是标准输出(STDOUT),2 是标准错误输出(STDERR)
1 | a="abc" |
1 | 使用 . 号来引用test1.sh 文件 |
接下来,我们为 test2.sh 添加可执行权限并执行:
1 | chmod +x test2.sh |
注:被包含的文件 test1.sh 不需要可执行权限。
-p 输入提示信息
-t 等待时间(单位是秒)
-n 字符数,read只
-s 输入隐藏数据
1 | !/bin/bash |
Akamai http2 demo 这个 Akamai 公司建立的官方 demo,左右两边分别为 HTTP/1.1 和 HTTP/2,两边都同时请求 300 多张图片,从加载时间可以看出 HTTP/2 在速度上的绝对优势。
chrome 商店中有一个工具 HTTP/2 and SPDY indicator 用来查看当前网站是否基于 HTTP/2,添加到 chrome 后如果蓝色闪电亮了说明支持 HTTP/2。
HTTP/2 所有性能增强的核心在于新的二进制分帧层,它定义了如何封装 HTTP 消息并在客户端与服务器之间传输。HTTP/1.x 协议解析基于纯文本,而 HTTP/2 将所有传输的信息分割为更小的消息和帧,并采用二进制格式对它们编码。二进制只有 0 和 1 的组合实现起来方便且健壮。
有别于 HTTP/1.1 在连接中的明文请求,HTTP/2 将一个 TCP 连接分为若干个流(Stream),每个流中可以传输若干消息(Message),每个消息由若干最小的二进制帧(Frame)组成。这也是 HTTP/1.1 与 HTTP/2 最大的区别。 HTTP/2 中,每个用户的操作行为被分配了一个流编号(stream ID),这意味着用户与服务端之间创建了一个 TCP 通道;协议将每个请求分区为二进制的控制帧与数据帧部分,以便解析。
在 HTTP/1.1 协议中 「浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞」这也是我们在站点中使用 CDN 的主要原因。
多路复用原理上还是基于以上 TCP 连接通道,通过单一的 TCP 连接发起和响应多重请求机制。
在 HTTP/1.x 中,header 中带有大量信息,而且每次都要重复发送,HTTP/2 中引入 HPACK 算法用于对 HTTP 头部做压缩。其原理在于:
HTTP/2 引入了服务器推送,可以在客户端请求资源之前发送数据,这允许服务器直接提供浏览器渲染页面所需资源,而无须浏览器在收到、解析页面后再提起一轮请求,节约了加载时间。除此之外,服务器还能够缓存数据,在同源策略下,不同页面共享缓存资源成为可能。
HTTP/1.1 的有一个缺点是:当一个含有确切值的 Content-Lengt h的 HTTP 消息被送出之后,你就很难中断它了。当然,通常你可以断开整个 TCP 链接(但也不总是可以这样),但这样导致的代价就是需要通过三次握手来重新建立一个新的TCP连接。
一个更好的方案是只终止当前传输的消息并重新发送一个新的。在 HTTP/2 里面,我们可以通过发送 RST_STREAM 帧来实现这种需求,从而避免浪费带宽和中断已有的连接。
XSS(Cross Site Script) 跨站脚本攻击,是攻击者利用网站漏洞在网站上注入恶意客户端代码,以获取访问权限,冒充用户,修改 HTML 内容等。恶意内容一般包括 JavaScript,主要方式是获取用户的隐私数据,例如 cookie,session 等。
XSS 攻击可以分为 3 类:存储型、反射型、基于 DOM。
存储型 XSS 是指恶意脚本永久存储在目标服务器上,当客户端请求数据时,脚本从服务器上传回并且执行。存储型 XSS 一般存在于 form 表单提交等交互功能,比如发帖留言,提交文本信息等。攻击者将内容经正常的功能提交于数据库存储,当前端页面获得后端从数据库中读取的注入代码时,将其渲染并且执行。
存储型 XSS 需要满足以下 3 个条件:
因此防止存储型 XSS 需要前端和后端共同努力。
服务器接受客户端的请求包,不会存储请求包的内容,只是简单的把用户数据 “反射” 给客户端造成反射型 XSS。常见的有用户搜索,错误信息的处理,这种攻击方式具有一次性。
反射型 XSS 有以下特征:
下面写一个简单的示例:
1 | <html lang="en"> |
在页面 input 中输入 <img src="" onerror="alert(document.cookie)">
,可以看到页面弹出警告框,并且显示用户 cookie。
基于 DOM 的 XSS 是指恶意脚本修改页面结构,比如一些免费 wifi 用来植入悬浮广告。
现阶段很多开源的库都专门针对 XSS 进行转义处理,默认抵御大多数 XSS 攻击,但是还是有很多方法绕过转义规则。假如页面不设置字符集的话,浏览器有自动识别编码的机制,所以黑客通过使用非常规字符集来达到 XSS 注入的功能。
常用的有以下几种,你也可以根据页面 DOM 结果对其进行修改
1 | ><script>alert(document.cookie)</script> |
XSS 之所以会发生,是因为用户输入的数据变成了代码。所以我们需要对数据进行 HTML Encode 处理,将其中的特殊字符进行编码。
HTML character | HTML Encoded |
---|---|
< | < |
> | > |
& | & |
’ | ' |
" | " |
空格 | |
<script>
,<iframe>
,<img>
等。onclick
,onerror
,onfocus
等。CSRF(Cross-Site Request Forgery) 跨站请求伪造攻击,是指攻击者通过盗用用户登录信息,模拟发送各种请求。攻击者借助聊天软件、论坛、微博等发送链接(有些伪装成短域名),迫使用户去执行攻击者预设的操作。如果当前用户具有管理员权限的话,CSRF 攻击将危及到整个 Web 应用程序。与 XSS 相比,XSS 是利用用户对指定网站的信任,CSRF 是利用网站对用户浏览器的信任。
要完成一次 CSRF 攻击,用户必须依次完成两个步骤:
假如一家银行的转账操作的 URL 地址是:http://lz5z.com/withdraw?account=AccountName&amount=1000&for=PayeeName
,恶意网站 B 中放置一段代码:<img src=http://lz5z.com/withdraw?account=lizhen&amount=1000&for=BadGuy>
。由于 img、script、iframe 标签不受同源策略现在,假如用户在未登出 A 的情况下打开了 B 网站,在 Cookie 未过期的情况下,用户就会损失 1000 块。
DDos(Distributed Denial of Service) 分布式拒绝服务。原理是利用大量的请求造成资源过载,导致服务不可用。DDoS 攻击从层次上可以分为网络层攻击和应用层攻击。
网络层 DDoS 攻击包括 SYN Flood、ACK Flood、UDP Flood、ICMP Flood 等。
应用层 DDoS 攻击不是发生在网络层,是发生在 TCP 建立握手成功之后,应用程序处理请求的时候,现在很多常见的 DDoS 攻击都是应用层攻击。
好久没有更新博客了。
2017年过得真快,转眼已经快触不到2017的尾巴了,如果算农历年的话,留给它的时间也已经不多了。
2017年对于我来说发生的最重要的事情就是跳槽了,从 OOCL 离职,到入职 WPS 正好一年了。这一年可以说是我职业发展最为重要的一年,以后应该都会在这个方向前行了。这一年差不多是我从门外汉逐步入门的过程,虽然之前也有一两年的工作经验,但大多时候是打酱油,在一个大的项目中缝缝补补。而经过 WPS 一年的训练,如今我可以写一些小的项目,也完全看懂了部门的大项目的整个架构。
第二件事就是今年八月份买了人生第一辆小车车-日产骐达。这辆小车车如今已经是我们家中第三重要的成员了,给生活提升了极大的幸福感。尤其是搬家的时候,身心俱疲,但是当进入小车的一瞬间,知道自己无论如何有落脚的地方,就觉得很安心。
第三件事就是公司搬家,下面是我离开旧金山时候拍的照片。
旧金山大楼:
这是旧金山的工位,呆了差不多10个月:
下面是刚搬到新办公室的照片,一台 PC,一台 mac mini,两台电脑都很卡。
后面自己买了 2k 32 寸的显示器,公司又给升级了最新的 PC - DELL 的高配 7050 + 三星 SSD,现在不再抱怨电脑卡了。新升级设备后给自己加了好多天的班,用流畅舒服的设备敲代码让人欲罢不能啊。(老板听到后请给我加薪)
还有一件事就是部门给了优秀员工奖,虽然没有实质性的奖励,但是可以看到 leader 予以的器重,所以还是很开心的。年终考评时,前端负责人也给了很不错的评价,说以后会让我负责更多的东西,多给我成长的机会。
2018年又长了一岁了,最重要的当然是结婚的事情提上了日程,以前别人问怎么还不结婚的时候,总是答,人家还小呢。但是这几年越发觉得自己其实已是一个油腻的中年人了,上学时本来就比周围的人大,一直被称为 “振哥”,工作以后发现同等年纪的人已经工作三四年了,越发焦急,觉得自己一事无成,马齿徒增。大学毕业的时候选择工作而没有像别的同学那样读研也是同样的原因。而明年的结婚算是给自己和家人一个交代,完成了人生的一件大事。
2018年当然是希望自己技术越来好,能承担越来越多的责任。当然最重要的是,要赚更多的钱,家人都健康快乐。
]]>最近项目不算忙,抽时间重构了一下项目的打包,先说一下成就。
在我的开发电脑上:
OS: macOS High Sierra
CPU: 2.6 GHz Intel Core i5
内存: 8G 1600 DDR3
硬盘: 1 TB SATA磁盘
代码全量编译时间从 4 分 51 秒优化到 2 分 08 - 20 秒左右。
在项目编译电脑上:
OS: Ubuntu 16.04.3 LTS
CPU: Intel® Core™ i5-7500 CPU @ 3.40GHz
内存: 64G 2133 DDR4
硬盘: 1 TB SSD
代码全量编译时间从 4 分 08 秒优化到 1 分 10 - 20 秒左右。
升级 SSD 可能是提升效果最明显的吧,从上面两组数据中就可以看出。相同的优化在 SSD 中表现要明显很多。
如果你的项目还在用 webpack2 的话,强烈建议你升级到 webpack3。webpack3 向下兼容,只不过有一些插件需要同时升级,注意看控制台给出的日志,把需要升级的一起升级了就好了。
webpack 打包的时候,每个模块都被一个闭包函数包裹,过多的闭包函数降低了浏览器中 JS 执行效率,Scope Hoisting 的作用是减少闭包函数的数量,将有关联的模块放到同一个闭包函数中。
启用方法
1 | module.exports = { |
Scope Hoisting 是基于 ECMAScript Module syntax ,对于 Commonjs 和 AMD 的模块不适用。
上面升级的算是副本,下面才是正文。
现在开发的项目算是比较大的项目,严格来说,是多个 SPA 组成的多项目。这样做的好处是能减少架构师的工作,同一份架构给多个项目使用,能保证项目稳定性。坏处也比较明显,就是会额外引入无用的依赖,比如共用的 helper 模块,很多项目都引用了,但是并不是每个项目都使用里面的每个函数。这点 tree-shaking 可以给出解决方案,但是实际开发过程中,由于同事们代码质量参差不齐,有些没用到的函数和模块也都引用了,所以导致 tree-shaking 的效果并不是很好。比如在大项目中,同事把几个 helper 里面函数全部封装到 vue-filter 中,当然里面的内容主要项目大多数都引用到了,但是后面同事在初始化一个小项目的同时,无论是否需要也都用了相同的代码(copy and paste)。
1 | import * as helpers from 'helpers' |
于是 helper 中每个 function 都挂载在 Vue-filter 中,所以完美的避开了 tree-shaking。
另外 tree-shaking 虽然能够一定程度的减少打包后代码的体积,但是开发和编译的速度还是会受到一定的影响。
下面是代码打包速度优化的一些思路,多数来源于网上的资料。
抽取公共代码有两个好处,一个是能减少编译代码的数量,一个是能够充分利用浏览器缓存,比如遇到项目切换的情况,使用 service-worker 中缓存共用的 common 代码能够减少请求的数量。
以下是 vue-cli 中给出的解决方案
1 | // split vendor js into its own file |
DLL 预编译的作用是将项目中稳定的依赖单独打包编译生成动态链接库,在业务代码中引用。这点在开发过程中优势比较明显,每次更新代码重新编译的时候都能够省去 DLL 库的编译,有不小的速度提升。
DLL 需要有一个额外的打包过程,新建一个 webpck.dll.conf.js 用来打包 DLL,并且在 package.json 中添加打包过程。
package.json
1 | { |
webpack.dll.conf.js
1 | const webpack = require('webpack') |
通过运行 npm run dll
在 dist 目录下生成了两个文件 vendor.dll.js 和 vendor.manifest.json。其中 vendor.dll.js 中是打包压缩后的 vendor 代码,vendor.manifest.json 是 vendor 文件的 node_modle 路径和 webpack 打包 id 的映射。
然后通过 DllReferencePlugin 将 vendor 引入业务代码。
1 | // 这里将生成的 vendor.dll.js 文件 copy 到 你需要的目录 |
最后还需要在 html 中引入生成的 DLL,网上有一些教程是直接把 script 标签写入 html 中的,但是由于我们多个项目同时依赖同一份 html 模板,其中某一些项目并不需要引入 DLL,比如一些静态页面。于是使用 html-webpack-include-assets-plugin 实现按需加载。
1 | ...pkgs.reduce((pre, current) => { |
在 pkgs 中控制 HtmlWebpackPlugin 的参数,和是否需要引入 vendor.dll.js。
pkg 模板如下:
1 | const extChunks = IS_PROD ? ['manifest'] : [] |
打包后可以明显看到 app.js 和 vendor.js 体积缩小,但是项目总体积略有增大。因为通过 DLL 的方式,额外存储了外部依赖的路径和 ID。
这点想必大家都知道
1 | module.exports = { |
(1) uglifyjs-webpack-plugin 多线程提示 JS 压缩效率
使用 uglifyjs-webpack-plugin 不仅可以加速 webpack 压缩 js 代码的速度,还能与 webpack tree-shaking 配合,减少代码体积。webpack 本身并不会执行 tree-shaking。它需要依赖于像 UglifyJS 这样的第三方工具来执行实际的未引用代码(dead code)删除工作。
1 | new UglifyJsParallelPlugin({ |
记得开启缓存,能有效提升打包效率。
(2) happypack 多线程提升 loader 执行效率。
使用 happypack 之前,你可以先去 Loader Compatibility List 看一下 happypack 的兼容性列表。
1 | const os = require('os') |
使用 happypack 后,性能比较差的 mac mini 速度反而降低了一些,但是性能比较强的编译机速度有不少的提升,所以 happypack 可以酌情使用,测试后发现速度有提升再加入,没有提升就果断弃用。
hard-source-webpack-plugin 也是利用缓存效果提升打包速度。
HardSourceWebpackPlugin is a plugin for webpack to provide an intermediate caching step for modules.
用法很简单
1 | const HardSourceWebpackPlugin = require('hard-source-webpack-plugin') |
开发的时候,使用 koa-webpack-middleware 的 devMiddleware, hotMiddleware 两个中间件是提供 dev 服务和代码热新服务,devMiddleware 本质上是对 webpack-dev-middleware 的一层封装,而 hotMiddleware 是对 webpack-hot-middleware 的一层封装。
开发过程中,所有的代码均被载入两个 webpack 服务中,因此有一丁点的代码改动都需要重新编译所有的 buddle,这对开发过程是极其不好的体验,因此划分代码依赖,通过 npm 参数编译不同的项目,来达到加速开发的效果。
比如使用 npm run dev project1
来开发项目 project1,而其它代码并不加载到 webpack 中。
拿到 project1 参数可以通过 node.js 的 process 对象
1 | let projects = process.argv.slice(2) |
以上打包优化都是参考网上的一些东西, 在实际使用过程中,发现有些文章内容是写了,但是并没有亲身实践,有些错误或者不完善的地方甚至都是一模一样的,所以自己结合实际项目走了一遍流程后,还是决定把东西写出来,希望对看到的人有帮助。
]]>linux
1 | 查看端口占用 |
windows
1 | 查看端口占用 |
linux
1 | shutdown |
windows
1 | shutdown |
windows
1 | ipconfig /flushdns |
1 | 显示当前的 Git 配置 |
1 | git add -A (stage all files: new, modified, deleted) |
1 | git commit -m [message] |
1 | 取回远程仓库的变化,并与本地分支合并 |
1 | 上传本地指定分支到远程仓库 |
1 | 列出所有本地分支 |
1 | 选择一个commit,合并进当前分支 |
1 | 显示所有远程仓库 |
在 CSS 伪元素基本用法一文中讲述了伪元素的基础功能,本章学习一些进阶功能,看看伪元素能实现哪些方便好用的功能。
如果一个元素内部的子元素全部都是浮动的话,那么这个元素会出现高度塌陷,这个时候就需要清除浮动。高度塌陷的负面作用主要有:不能正确显示背景,边框不能撑开,margin 和 padding 不能正确显示。
假设有代码如下:
html:
1 | <div class="outer"> |
css:
1 | .outer { |
使用伪元素清除浮动的办法:
1 | .outer { |
其它清除浮动的办法:
(1)给父元素设置高度。
(2)clear: both
清除浮动。
常见的用法是在父元素结束之前,统一引入一个元素 clear: both
用来清除浮动。
html:
1 | <div class="outer"> |
css:
1 | .clear { |
这种方法实现起来很简单,不过缺点也很明显,引入了额外的 DOM 元素。
clear 属性可以对应的属性值有:
(3)给父级元素定义 overflow: auto
或者 overflow: hidden
1 | .outer { |
使用 overflow 属性来清除浮动只可以使用 hiddent 和 auto 不能使用 visible。 为了兼容 IE 最好用 overflow:hidden
,缺点是元素会被截断。
总结清除浮动最佳方案
1 | // 全浏览器通用的 clearfix 方案 |
这点在移动端开发显得尤为重要,可以增强用户体验。
html:
1 | <button class="btn">click</button> |
css:
1 | .btn { |
还有一种不使用伪元素扩大可点击范围的方式是使用 border + background-clip
1 | .btn { |
我是分割线
实现方式
html:
1 | <p class="divide">我是分割线</p> |
css:
1 | .divide { |
通过在 content 中使用 attr 函数可以调用元素的属性。
1 | a:after { |
1 | a:before { |
html:
1 | <ol class="sites"> |
css:
1 | .sites { |
html:
1 | <ol id="sites"> |
css
1 | li { |
在网上还有很多关于伪元素的用法,非常有趣,既能减少 DOM 元素数量,还能用 CSS 实现一部分 JS 的功能,非常酷炫,后面见到有趣的用法会不断记录。
]]>CSS 中可以利用伪元素给 DOM 元素添加特殊的样式。比如说,我们可以通过 :before
在一个元素前增加一些文本,并为这些文本添加样式。虽然用户可以看到这些文本,但是这些文本实际上不在文档树中。
CSS3 规范中要求使用双冒号(::)添加伪元素,用以区分伪元素和伪类,比如 ::before
是伪元素,:hover
是伪类。但是大部分伪元素依然支持单冒号的形式,::before
写成 :before
也可以,为了向后兼容,一般推荐使用单冒号的形式。
支持单双冒号的伪元素有: :before/::before
,:after/::after
,:first-letter/::first-letter
,:first-line/::first-line
。
仅支持双冒号的伪元素有: ::selection
,::placeholder
,::backdrop
。
:before
& :after
:before
和 :after
可以在元素前面或者后面插入内容,用 content 属性表示要插入的内容,这个虚拟元素默认是行内元素,可以配合其它样式使用。
html:
1 | <p> </p> |
css:
1 | p:before { |
p 元素会显示 Hello World,但是被插入的内容实际上不在文档树中。
:first-letter
:first-letter
用来获取元素中文本的首字母,被修饰的首字母不在文档树中。注意没有 :last-letter
。
首行只在 block-container box 内部才有意义, 因此 :first-letter
伪元素 只在 display 属性值为 block, inline-block, table-cell, list-item 或者 table-caption 的元素上才起作用。 其他情况下 :first-letter
毫无意义。
:first-letter
的优先级低于 :before
,也就是如果元素用 :before
先插入文本,会获取 before 伪元素中的内容。
html:
1 | <p>World</p> |
css:
1 | p:before { |
这时,:first-letter
实际获取的元素是 :before
中的 H。
注意: 在一个使用了 :first-letter
伪元素的选择器中,只有很小的一部分 css 属性能被使用 ::first-letter
:first-line
:first-line
用来获取 块状元素 中的第一行文本,不能用于内联元素。
html:
1 | <h1>Hello</br>World</h1> |
css:
1 | h1:first-line { |
在一个使用了 ::first-line 伪元素的选择器中,只有很小的一部分css属性能被使用 ::first-line
::selection
::selection
伪元素应用于文档中被用户高亮的部分(比如使用鼠标或其他选择设备选中的部分),该伪元素只支持双冒号的形式。
只有 Gecko 引擎需要加前缀(-moz)
1 | ::-moz-selection { |
注意: 只有一小部分 CSS 属性可以用于 ::selection
选择器: color, background-color, cursor, outline, text-decoration, text-emphasis-color 和 text-shadow。要特别注意的是,background-image 会如同其他属性一样被忽略。
::placeholder
(试验性质):placeholder
匹配占位符的文本,只有元素设置了 placeholder 属性时,该伪元素才能生效。在一些浏览器中(IE10 和 Firefox18 及其以下版本)会使用单冒号的形式。
1 | input::-moz-placeholder { |
::backdrop
(试验性质)用于改变全屏模式下背景色,全屏模式默认背景色为黑色。
1 | h1:fullscreen::backdrop { |
最近做的一个关于电影的网站 IMDB Top250,想对其进行 SEO 优化,用到 meta 信息的时候,很多知识都是 『似乎』、『好像』、『可能』 的感觉,回想自己一直没有系统的学习过 meta 相关的知识,这些东西虽然简单,但是很多时候能发挥出意想不到的效果,尤其对于 SEO 有非常重要的作用。
meta 标签位于文档的头部,可提供有关页面的元信息(meta-information)。 meta 标签本身不包含任何内容,通过其属性定义了与文档相关联的内容。
meta 标签一共有五个属性值: charset、content、http-equiv、name、scheme。 其中 http-equiv 和 name
必须与 content 配合组成键值对使用, charset 为 HTML5 属性, scheme 属性 HTML5 不支持。
定义 HTML 文档编码方式,一般使用世界通用语言编码 UTF-8。
1 | <meta charset="UTF-8"> |
在 HTML4 中的写法是
1 | <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
http-equiv 为枚举属性,与 content 属性组成键值对,一般用于服务器向浏览器传回一些特定的信息,以帮助浏览器编译和显示页面内容。虽然有些服务器会发送许多这种键值对,但是所有服务器都至少要发送一个:content-type:text/html
。这将告诉浏览器准备接收一个 HTML 文档。
http-equiv 可枚举的值有: content-type, default-style, refresh。
1 | <meta http-equiv="refresh" content="3;URL=https://lz5z.com"> |
以上表示页面 3 秒后自动跳转。
name 属性是用的最多的属性,常用的有 description,keywords,author,viewport,generator 等等。
其中 keywords 对应 content 用逗号分隔,description 为搜索引擎显示网页时候的简介。
viewport 用于指定视窗的属性,在移动端开发时显得尤为重要。
1 | <meta name="keywords" content="HTML5,meta"> |
还有一些属性值,比如 referrer,robots,renderer。
(1) referrer 控制所有从该文档发出的 HTTP 请求中 HTTP Referer 头的内容:
1 | <meta name="referer" content="always"> |
referrer 对应的 content 属性可取的值:
no-referrer
不要发送 HTTP Referer 首部。origin
发送当前文档的 origin。no-referrer-when-downgrade
当目的地是先验安全的(https->https)则发送 origin 作为 referrer ,但是当目的地是较不安全的 (https->http)时则不发送 referrer 。这个是默认的行为。origin-when-crossorigin
在同源请求下,发送完整的URL (不含查询参数) ,其他情况下则仅发送当前文档的 origin。unsafe-URL
在同源请求下,发送完整的URL (不含查询参数)。HTTP Referer 头:
Referer 请求头字段允许由客户端指定资源的 URI 来自于哪一个请求地址,这对服务器有好处。Referer 请求头让服务器能够拿到请求资源的来源,可以用于分析用户的兴趣爱好、收集日志、优化缓存等等。同时也让服务器能够发现过时的和错误的链接并及时维护。
注意:动态地插入 <meta name="referrer">
(通过 document.write 或者 appendChild) 是不起作用的。同样注意如果同时有多个彼此冲突的策略被定义,那么 no-referrer 策略会生效。
(2) robots 用来告诉搜索引擎的爬虫哪些页面需要索引,哪些不需要索引。
1 | <meta name="robots" content="all"> |
robots 对应的 content 可取的值:
还有一些只有固定的搜索引擎支持的参数,比如 noodp,noarchive 等,这里就不说明了。
(3) renderer
renderer 并不是 w3c 标准,但却经常见于一些网页中,这个属性主要用于双核或者多核浏览器(猎豹浏览器,360浏览器)使用指定的内核处理自己的网页。目前大多数 「双核」 浏览器内部的两个内核分别是 IE 内核和 WebKit 内核,IE 内核主要用于兼容「老一辈」的网页,使其能够正常显示;WebKit 内核则用于渲染「新一代」的网页,从而发挥出更快的显示速度、更好的显示效果、更优异的脚本执行性能。
作为用户来说并不关心你使用哪个内核,简单易用才是王道,因此在网页中设置首选内核会让网页有更好的效果。
1 | <meta name="renderer" content="webkit"> |
renderer
对应的 content 用于指定浏览器内核,
webkit(WebKit 内核)、ie-stand(IE 内核-标准模式)、ie-comp(IE 内核-兼容模式)。我们也可以同时指定多个内核名称,之间以符号"|"进行分隔,此时浏览器将会按照从左到右的先后顺序选择其具备的渲染内核来处理当前网页。
IE8 有自己独特的写法 X-UA-Compatible 对于 IE8 之外的浏览器是不识别的。
1 | // Edge 模式通知 IE 以最高级别的可用模式显示内容 |
注: 如果设置浏览器内核为 Webkit (极速模式),打开网页后却为 IE (兼容模式),尝试刷新浏览器则会自动切换模式。
通常是这样设置的
1 | <meta name="renderer" content="webkit"> |
(4) format-detection
防止 ios 把数字/字符串识别为电话/邮件/日期/地址
1 | <meta name="format-detection" content="telephone=no"> |
首先直接从 github 把 blog 项目导入到 coding,项目名称命名为 [name].coding.me,相当于 github 上面的 [name].github.io。
进入项目代码,点击左侧 『代码 -> Pages 服务』,选择静态 Pages 服务,coding 部署来源仅支持 coding-pages 分支和 master 分支,所以选择 master 分支。
这时,通过 [name].coding.me 就能够访问页面了,但是这还远远不够,我们还需要添加自定义域名和开启 SSL 服务。
首先确保项目根目录中有 CNAME 文件,里面是自己的域名,比如我的域名 lz5z.com,然后在 coding 页面自定义域名中输入此域名,并且开启强制 HTTPS 访问。
然后去自己域名服务商那里修改 DNS Server,我的域名在万网购买,于是在万网控制台添加一个 CNAME 记录和一个 A 记录,加上之前 github pages 添加的主机记录,截图如下。
红色部分为新添加的记录,如果不知道 coding.net 的 ip 地址的话,可以手动 ping 一下。
由于之前使用 cloudflare 的免费 SSL 服务而将 DNS Server 的地址指向了 cloudflare,这个时候把地址改回万网默认配置即可。
经过漫长的等待,DNS 解析生效,此时通过 https://lz5z.com 访问,发现域名已经生效了,但是存在两个问题:
国外地址访问网站报 SSL 不合法主要是因为这个原因:
注意:申请 SSL/TLS 证书需要通过 Let’s Encrypt 的 HTTP 方式验证域名所有权。如果您的域名在境外无法访问 Coding Pages 的服务器,将导致 SSL/TLS 证书申请失败。
查阅资料发现大家的解决方式都是设置双线解析,也就是国外访问通过 github pages,国内访问通过 coding.net,因此要为域名设置解析路线,如果域名服务商自定义解析路线,可以选择免费的 DNSPod 做 DNS 解析。
DNSPod 提供双线解析的原理我不是很明白,而且比较困惑的是 github pages 自定义域名原生是不资辞 SSL 的,之前的做法是使用 cloudflare 的 SSL 服务进行重定向,假如使用双线解析的话,那国外地址为什么能够看到合法的 SSL 呢?
而且按照网上的做法改了 DNS 解析后,并没有发生双线解析,无论是国外还是国内都是解析到 coding.net,但是解决了国外地址访问报 SSL 证书错误的问题。着实很奇怪,以下是我的做法。
注册 -> 登录 -> 实名认证 -> 进入控制台 -> 添加域名
添加域名的时候 DNSPod 会自动监测域名之前的解析情况,然后用 DNSPod 服务器提供的 DNS 地址替代万网提供的地址。
DNSPod DNS 记录如下:
更改万网 DNS Server 为 DNSPod:
再次经过漫长的等待,DNS 生效后,无论国内国外访问网站都是合法的 SSL,excited!
每次新建隐私窗口打开网站都是先看 coding 的广告,然后再重定向到之前的地址,这是极差的用户体验,不过 coding 官方提供了解决办法,简单的就是购买 coding 的会员,免费的办法就是在网站首页任意位置放置「Hosted by Coding Pages」的文字版或图片版,具体办法参考 coding pages 服务的说明。添加之后勾选 已放置 Hosted by Coding Pages,等待一天或者两天就生效了。
这次切换 github pages 到 coding.net 真的费时费力,不过好在现在网页能够正常访问,而且速度也比之前快很多,所以还是比较满意的。
]]>与 PC 端共同开发一个页面,页面由 PC 端提供,内部 iframe 则由我们前端提供。开发时候遇到了一个问题,webpack 打包后 css 的 z-index 值与原始值不符,导致 iframe 里面的 toast 被外面 z-index 较小的 dialog 覆盖。更改 toast 的 z-index,发现没起作用,页面上的 z-index 依然是之前的值,而不是 css 中赋予的值。给 z-index 加上 !important 后依然无效,查资料发现是 OptimizeCssAssetsPlugin 调用 cssProcessor cssnano 对 z-index 进行了重新计算导致的。
这本来是 webpack 插件的一个善举(让 z-index 数值更加合理),但是具体情况来看,这里显然不需要这个 “善举”。
解决方案按照网上的资料,可以在 OptimizeCssAssetsPlugin 插件中关掉 cssnano 对 z-index 的重新计算(cssnano 称为 rebase)。
1 | new OptimizeCSSPlugin({ |
cssnano 将 z-index rebase 归类为 unsafe,只有在单个网页的 css 全部写入一个 css 文件,并且不通过 JavaScript 进行改动时是 safe。
参考: http://cssnano.co/optimisations/zindex/
cssnano 默认进行 z-index rebase。
unsafe (potential bug) 优化项默认不开启应该比较友好。
以上是网上提供的方案,而且亲测有效,但是由于项目太大,因为其中一个小功能改了整个项目的 css 处理策略,难免有些担心会影响到其它页面。思考再三,决定不改 webpack 配置。
观察之前项目中使用的框架,在生成 dialog 或者 toast 的时候,即使在 webpack 插件对 css 进行处理之后,其 z-index 依然是很大的。
比如 element-ui 下 的 popup-manager.js 中首先设置 zIndex 为 2000,然后在 openModal 的时候动态添加 css 到 DOM 中,并且改变 zIndex 的值,而在浏览器中观察弹框的 z-index,果然是没有经过 cssnano rebase 的。
于是仿照 element-ui 的做法,把 z-index 相关的 css 用 js 动态插入到 DOM 中,就完美地解决了这个问题,并且没有对其它项目产生影响。
1 | // 改变 toast 的 z-index |
webpack 在对代码进行打包之前,会扫描所有的模块,建立模块之间的依赖树,而插件的运作时机也是相对于此时的静态代码,因此用 js 动态插入 css,webpack 显然不会知道要插入的 css 是什么样的,因此动态插入的 css 内容就不会经过插件的处理,也就避免了 OptimizeCssAssetsPlugin 的 “善举”。
]]>linux 中用户相对于文件有三种身份:owner、group、others,每种身份各有 read、write、execute 三种权限。
使用 ls -l
命令可以查看与文件权限相关的信息:
1 | ls -l |
其中第一个字符表示文件类型:d 表示文件为一个目录,- 表示文件为普通文件,l 表示链接, b 表示设备文件。
接下来的字符中,以三个为一组,且均为 r(read)、 w(write)、 x(execute) 三个参数的组合,首先三个字符表示文件所有者权限,后面三个字符表示用户组权限,最后三个表示其他人对文件的权限。这三个权限的位置不会改变,如果没有权限,就会出现减号[ - ]。
后面的字段分别代表:硬链接个数,所有者,所在组,文件或者目录大小,最后访问/修改时间,文件或者目录名。
chgrp:改变文件所属群组 change group
chown:改变文件拥有者 change owner
chmod:改变文件的权限 change mod
首先使用 groups 命令查看当前用户在哪些分组中,然后使用 chgrp 命令改变文件所属用户组
1 | chgrp -R admin foo |
-R 表示递归更改文件属组,就是在更改某个目录文件的属组时,如果加上 -R 参数,那么该目录下的所有文件的属组都会更改。可以通过 /etc/group
查看当前系统所有的分组。
可以看到文件分组由 staff 变成了 admin。
语法
1 | chown [–R] 属主名 文件名 |
chown 可以更改文件的 owner,也可以同时更改文件属组。假如当前系统中有一个名为 test 的用户。
1 | sudo chown -R test foo |
此时 foo 的 owner 变成了 test。可以通过 /etc/passwd
文件查看当前系统所有的用户。
chown 还可以用户修改文件所在的分组。
1 | sudo chown [-R] lizhen:staff foo |
文件属性又变回去了。
chmod 用来更改文件属性,权限可以使用符号或数字来表示。
使用符号表示权限:
[ + ]为文件或目录增加权限
[ - ]删除文件或目录的权限
[ = ]设置指定的权限
通过使用 u(owner)、g(group)、o(other) 来代表三种身份的权限,此外 a 代表 all,即全部身份。
语法
1 | chmod u/g/o/a +/-/= r/w/x filename |
1 | ls -l test.txt |
使用数字改变权限:
x: 1
w: 2
r: 4
所以权限 rwx
就等于 4 + 2 + 1 = 7
,也就是 chmod a=rwx file
相当于 chmod 777 file
。
-rw——- (600) 只有所有者才有读和写的权限
-rw-r–r– (644) 只有所有者才有读和写的权限,组群和其他人只有读的权限
-rwx—— (700) 只有所有者才有读,写,执行的权限
-rwxr-xr-x (755) 只有所有者才有读,写,执行的权限,组群和其他人只有读和执行的权限
-rwx–x–x (711) 只有所有者才有读,写,执行的权限,组群和其他人只有执行的权限
-rw-rw-rw- (666) 每个人都有读写的权限
-rwxrwxrwx (777) 每个人都有读写和执行的权限
1 | chmod 711 test.txt |
ES2016 只有两个新特性
includes 查找一个值是否在数组中
1 | [1, 2, 3].includes(3) //true |
includes 还可以接收两个参数,第一个表示要查找的值,第二个表示从数组第 N 个元素开始查找。
1 | [1, 2, 3].includes(2) // true |
注意上面 [1, 2, NaN].includes(NaN)
的返回值为 true,虽然 NaN === NaN
的结果为 false,所以『包含』和『相等』还是有区别的。
1 | const tt = [-0, 1, NaN] |
测试发现 includes 和 indexOf 在 node 8 / chrome 61 下速度差异不大,因此在使用的时候不用考虑性能的问题。
在 ES2015 中,String 对象也有 includes 方法,String.prototype.includes,但是只能用于 String,不能用于 characters。
ES2016 新增幂运算符改进语法
1 | 3 ** 3 // 27 |
幂运算符的优先级高于二元运算符,低于一元运算符。
1 | 2 * 5 ** 2 // 50 |
主要新特性:
小改款
关于 async/await 很早以前就写过了,而且现在基本上已经成了异步代码必备了,这里就不赘述了。
详情参考JavaScript异步解决方案async/await
共享内存和原子内容比较多,后面会单独写一篇文章,暂时留坑。
(1) Object.entries()
该方法将一个对象中所有可枚举的属性与值按照二维数组的方式返回,如果对象是数组,则数组的下标作为键值。
1 | Object.entries({ one: 1, two: 2}) // [['one', 1], ['two', 2]] |
返回数组的顺序与 Object.keys() 一致。
1 | let obj = {3: 'a', 2: 'b', 1: 'c'} |
Object.entries() 会忽略对象中 key 为 Symbol 的键值对。
1 | Object.entries({ [Symbol()]: 123, foo: 'abc' }) // [ [ 'foo', 'abc' ] ] |
通过 Object.entries() 设置一个 Map 对象。
1 | let map = new Map(Object.entries({ |
通过 Object.entries() 遍历对象。
1 | let obj = { one: 1, two: 2 } |
(2) Object.values()
该方法返回对象可枚举键值对中所有的 value。
1 | Object.values({ one: 1, two: 2 }) // [ 1, 2 ] |
(1) String.prototype.padStart
padStart 函数通过填充字符串首部使字符串达到一定的长度。该方法接受两个参数,第一个表示目标字符串长度,第二个表示填充内容,默认填充内容为空格。
1 | 'abc'.padStart(10) // " abc" |
(2) String.prototype.padEnd
padEnd 填充字符串的时候从尾部开始填充,其它均与 padStart 相同。
1 | 'abc'.padEnd(10) // "abc " |
该方法获取目标对象所有属性的属性描述符,该属性必须是自己定义的,不能是通过原型链继承来的。
关于属性描述符的作用,可以查看使用 Object.defineProperty 为对象定义属性
1 | const obj = { |
使用 Object.assign() copy 一个对象/属性 的时候,不能正确 copy 属性的 get 和 set,而通过 getOwnPropertyDescriptors() 能够实现正确 copy 一个对象/对象属性。
1 | let Leo = Object.defineProperty({}, 'name', { |
我们发现通过 Object.assign() copy 后的 ‘name’ 属性,其 ‘get’, ‘set’ 属性不见了
1 | const result2 = {} |
使用 Object.getOwnPropertyDescriptors 配合 Object.defineProperties 就可以实现正确 copy 了。
这个新特性很简单,就是允许我们在定义或者调用函数的时候参数后面多加一个逗号而不报错。
1 | function foo (a, b,) {} // correct |
在数组和对象中这样的写法也没有问题。
1 | let arr = ['red', 'green', 'blue',] |
新加入这个特性的好处就是当我们调整参数或者代码结构的时候,不需要再额外地添加或者删除逗号了,尤其是对代码进行注释的时候会方便很多。在版本管理上,不会因为出现一个逗号,导致原本只有一行的修改变成两行。
dispatch
和 broadcast
两个方法,之前在 vue2 组件通信 也提到过 vue2 中取消了 $dispatch
和 $broadcast
两个重要的事件,而 Element 重新实现了这两个函数。代码地址放在 element-ui/lib/mixins/emitter
emitter.js
1 | ; |
dispatch
和 broadcast
方法都需要 3 个参数,componentName
组件名称, eventName
事件名称, params
传递的参数。
dispatch
方法会寻找所有的父组件,直到找到名称为 componentName
的组件,调用其 $emit()
事件。broadcast
方法则是遍历当前组件的所有子组件,找到名称为 componentName
的子组件,然后调用其 $emit()
事件。
这里也看出了 Element 中的 dispatch
与 broadcast
的不同,vue1 中的 $dispatch
和 $broadcast
会将事件通知给所有的 父/子 组件,只要其监听了相关事件,都能够(能够,不是一定)触发;而 Element 则更像是定向爆破,指哪打哪,其实更符合我们日常的需求。
兄弟组件之间的通信可以很好的诠释上述两个事件。假设父组件 App.vue 中引入了两个子组件 Hello.vue 和 Fuck.vue。
如果你的项目中巧合使用了 Element,那可以按照下面的方式将其引入进来,如果没有用 Element 也不用担心,复制上面的 emitter.js
,通过 mixins 的方式引入即可。
在 App.vue 中监听 message
事件,收到事件后,通过 broadcast
和接收到的参数,将事件定向传播给相关组件。
1 | <template> |
Fuck.vue 与 Hello.vue 的内容基本相同,下面只列出 Fuck.vue
1 | import Emitter from 'element-ui/lib/mixins/emitter' |
mixins/event.js
1 | import Emitter from 'element-ui/lib/mixins/emitter' |
Fuck.vue 中监听了 message
事件,当收到消息时,向 tableData
中加入新的值。而 summit
方法则调用 event.js
中的 communicate
方法,通过 dispatch
方法将事件传播给 ROOT
组件。
Lazyload 可以加快网页访问速度,减少请求,实现思路就是判断图片元素是否可见来决定是否加载图片。当图片位于浏览器视口 (viewport) 中时,动态设置 <img>
标签的 src 属性,浏览器会根据 src 属性发送请求加载图片。
首先不设置 src 属性,将图片真正的 url 放在另外一个属性 data-src 中,在图片即将进入浏览器可视区域之前,将 url 取出放到 src 中。
懒加载的关键是如何判断图片处于浏览器可视范围内,通常有三种方法:
通过对比屏幕可视窗口高度和浏览器滚动距离与元素相对文档顶部的距离之间的关系,判断元素是否可见。
示意图如下:
代码如下:
1 | function isInSight(el) { |
通过 getBoundingClientRect() 获取图片相对于浏览器视窗的位置
示意图如下:
getBoundingClientRect() 方法返回一个 ClientRect 对象,里面包含元素的位置和大小的信息
1 | ClientRect { |
其中位置是相对于浏览器视图左上角而言。代码如下:
1 | function isInSight1(el) { |
使用 IntersectionObserver API,观察元素是否可见。“可见”的本质是目标元素与 viewport 是否有交叉区,所以这个 API 叫做“交叉观察器”。
实现方式
1 | function loadImg(el) { |
IntersectionObserver 的作用就是检测一个元素是否可见,以及元素什么时候进入或者离开浏览器视口。
WICG 提供了一个 polyfill
1 | const io = new IntersectionObserver(callback, option) |
IntersectionObserver 是一个构造函数,接受两个参数,第一个参数是可见性变化时的回调函数,第二个参数定制了一些关于可见性的参数(可选),IntersectionObserver 实例化后返回一个观察器,可以指定观察哪些 DOM 节点。
下面是一个最简单的应用:
1 | // 1. 获取 img |
(1) callback
回调 callback 接受一个数组作为参数,数组元素是 IntersectionObserverEntry 对象。IntersectionObserverEntry 对象上有7个属性,
1 | IntersectionObserverEntry { |
(2) option
假如我们需要特殊的触发条件,比如元素可见性为一半的时候触发,或者我们需要更改根元素,这时就需要配置第二个参数 option 了。
通过设置 option 的 threshold 改变回调函数的触发条件,threshold 是一个范围为0到1数组,默认值是[0],也就是在元素可见高度变为0时就会触发。如果赋值为 [0, 0.5, 1],那回调就会在元素可见高度是0%,50%,100%时,各触发一次回调。
1 | const observer = new IntersectionObserver((changes) => { |
root 参数默认是 null,也就是浏览器的 viewport,可以设置为其它元素,rootMargin 参数可以给 root 元素添加一个 margin,如 rootMargin: '20px'
时,回调会在元素出现前 20px 提前调用,消失后延迟 20px 调用回调。
(3) 观察器
1 | // 开始观察 |
使用前两种方式实现 lazyload 都需要监听浏览器 scroll 事件,而且要对每个目标元素执行 getBoundingClientRect() 方法以获取所需信息,这些代码都在主线程上运行,所以可能造成性能问题。
Intersection Observer API 会注册一个回调方法,每当期望被监视的元素进入或者退出另外一个元素的时候(或者浏览器的视口)该回调方法将会被执行,或者两个元素的交集部分大小发生变化的时候回调方法也会被执行。通过这种方式,网站将不需要为了监听两个元素的交集变化而在主线程里面做任何操作,并且浏览器可以帮助我们优化和管理两个元素的交集变化。
service worker 的功能和特性可以总结为以下几点:
1 | if ('serviceWorker' in navigator) { |
每次页面加载成功后,就会调用 register() 方法,浏览器将会判断 service worker 线程是否已注册并做出相应的处理。
scope 参数是可选的,默认值为 sw.js
所在的文件目录。
打开 chrome 浏览器, 输入 chrome://inspect/#service-workers 可以可以用 DevTools 查看 Service workers 的工作情况。
service worker 注册后,浏览器就会尝试安装并激活它,并且在这里完成静态资源的缓存。
所以我们在 sw.js
中添加 install 事件
1 | this.addEventListener('install', function (event) { |
install 事件一般是被用来完成浏览器的离线缓存功能,service worker 的缓存机制是依赖 cache API 实现的。cache API 为绑定在 service worker 上的全局对象,可以用来存储网络响应发来的资源,这些资源只在站点域名内有效,并且一直存在,直到你告诉它不再存储。
每次任何被 service worker 控制的资源被请求到时,都会触发 fetch 事件,因此我们可以利用 fetch 事件对资源响应做一些拦截操作
1 | this.addEventListener('fetch', function (event) { |
这样看来,其实可以把 service worker 理解为一个浏览器端的代理服务器,这个代理服务器通过 scope 和 fetch 事件来 hook 站点的请求,来达到资源缓存的功能。
注意:request 和 response 不能直接使用而是通过 clone 的方式使用是因为他们是 stream,因此只能使用一次。
/sw.js
控制着页面资源和请求的缓存,如果 /sw.js
需要更新应该怎么办呢?
sw.js
文件,当浏览器获取到了新的文件,发现 sw.js
文件发生更新,就会安装新的文件并触发 install 事件。1 | // 安装阶段跳过等待,直接进入 activate |
注意:如果 sw.js
文件被浏览器缓存,则可能导致更新得不到响应。如遇到该问题,可尝试这么做:在 webserver 上添加对该文件的过滤规则,不缓存或设置较短的有效期。
也可以借助 Registration.update()
手动更新
1 | var version = '1.0.1'; |
除了浏览器触发更新之外,service worker 还有一个特殊的缓存策略: 如果该文件已 24 小时没有更新,当 Update 触发时会强制更新。这意味着最坏情况下 service worker 会每天更新一次。
可以单独设置调试时 service worker 安装后立即激活:
1 | self.addEventListener('install', function() { |
service worker 基于注册、安装、激活等步骤在浏览器 js 主线程中独立分担缓存任务。
下面是一个使用 service worker 的 postMessage API 做的一个简单计算器,其中计算部分在 service worker 线程中完成。假如有一些比较耗时的工作,比如大量计算,或者 fetch 数据,可以将其放入 service worker 线程中,以达到提高页面响应的目的。
这个网站记录了很多 service worker demo。
很简单,换一个 markdown 引擎,然后再增加 emoji 插件即可。😊
1 | npm un hexo-renderer-marked --save |
据说 hexo-renderer-markdown-it 的速度要比 Hexo 原装插件要快,而且功能更多:
Main Features
- Support for Markdown, GFM and CommonMark
- Extensive configuration
- Faster than the default renderer |
hexo-renderer-marked
- Safe ID for headings
- Anchors for headings with ID
- Footnotes
<sub>
H2O<sup>
x2<ins>
Inserted
然后编辑 _config.yml
:
1 | markdown: |
:smile:
就可以。1 | const Benchmark = require('benchmark') |
系统:macOS Sierra
CPU:2.6 GHz Intel Core i5
内存:8 GB 1600 MHz DDR3
Node: 8.1.0
1 | apply x 951,707 ops/sec ±0.46% (87 runs sampled) |
可见虽然 call 比 apply 要快一些,但是差别并不是很大,那么在浏览器上面表现如何呢?
你也可以点击下面的 button 在自己的浏览器上查看运行效果。
可以看到几个浏览器中都是 call 的速度要快于 apply,不过都没有特别明显。其中 Safari 的速度让我大吃一惊,直接比其它几个浏览器快了一个数量级。看来 WWDC 2017 发布会上苹果吹的牛没有那么大啊,不过也可能 mac 从硬件层面对 Safari 进行优化。
SO 上面解释的比较详细,在语言设计的时候,apply 需要执行的步数就比 call 要多:无论 call 还是 apply,最终都是调用一个叫做 [[Call]]
的内部函数,而 apply 相对于 call 多做了一些参数处理,如参数判断、格式化等。
SO 上面提到 call 的性能是 apply 的 4 倍甚至 30 倍,为什么在我这里的测试只有一丁点差距呢?
突然想到是否参数问题,于是去掉参数和增加参数,分别于 node 环境中测试,发现变化并不大,差距依然很小。那么猜想可能是 ES5 与 ES6 的差距导致的。
对比 ES5 和 ES6 中对这两个函数的定义,发现 Function.prototype.call 的变化并不大,主要变化发生在 Function.prototype.apply 上,从 ES5 的 9 步变成了 ES6 的 6步。主要变化发生在对参数处理的部分,其它关于内部函数调用的部分,看起来并没有太多差异。
通过测试发现随着 ECMAScript 语言和 JavaScript 解释器性能不断增强,call vs apply 在性能上的差距越来越小, SO 上面提到的数倍甚至几十倍的差距,目前已经不存在了,因此在使用上可以随心所欲了。
]]>项目路径
1 | ├── README.md |
把文件 clone 下来后,安装依赖,然后就可以运行了
1 | git clone https://github.com/Leo555/css_modules_study.git |
浏览器会自动打开静态文件,方便查看效果。
首先配置 webpack 环境(本文使用webpack2),给 css-loader 增加一个 modules 查询参数,表示打开 CSS Modules 功能。
简单的示例如下:
1 | module: { |
如果需要给 CSS Modules 传递一些参数,可以用对象的方式:
1 | module: { |
开启 CSS Modules 后,所有的 CSS 选择器都是局部作用域,除非声明它为全局的。
1 | /*Scoped Selectors*/ |
以上两个 CSS class 通过如下方法被 JS 引用
1 | import styles from "./style.css"; |
后面的引用方式都相同,因此略去,具体可查看 index.js。
查看构建后的 CSS,发现局部变量的名字被编译成 hash (localIdentName: '[path][name]__[local]--[hash:base64:5]'
),而全局变量的名字不变。
原来 CSS Modules 就做了这么一点微小的工作。
CSS Modules 通过组合的方式进行集成,以达成代码复用的效果。
1 | /*Class Composes*/ |
otherClassName 继承 className,因为拥有了 color 和 margin 属性,而 background 继承 otherClassName,却重写了 border-style。
在 animation.css 中,定义了动画 tada:
1 | @keyframes tada { |
在 style 中的引用方式如下:
1 | /*Scoped Animations*/ |
上面第二个 composes 也展示了如何从其它 CSS 模块中引用选择器。
通过 @value 定义变量和引用变量
colors.css
1 | @value color: black; |
引用方式
1 | /*Define variables*/ |
vue-loader 中集成了 CSS Modules,可以作为模拟 CSS 作用域的替代方案。
在 <style>
上添加 module 属性即可开启 CSS Modules 模式:
1 | <style module> |
css-loader 会自动将生成的 CSS 对象注入到 $style 中,只需要在 <template>
中使用动态 class 绑定:
1 | <template> |
由于它是一个计算属性,它也适用于 :class
的 object/array 语法:
1 | <template> |
在 JavaScript 访问它:
1 | <script> |
在 .vue 中可以定义不止一个 <style>
,为了避免被覆盖,你可以通过设置 module 属性来为它们定义注入后计算属性的名称。
1 | <style module="a"> |
在前端模块化的道路上,CSS 显然落后 JS 非常多。ES2015/ES2016 的快速普及和 Babel/webpack 等工具的发展,让 JS 在大型项目工程化中越发强大,最终成为一流语言 。
CSS Modules 解决了哪些问题呢?
CSS 模块化的解决方案有很多,但主要有两类。一类是彻底抛弃 CSS,使用 JS 或 JSON 来写样式。Radium,jsxstyle,react-style 属于这一类。优点是能给 CSS 提供 JS 同样强大的模块化能力;缺点是不能利用成熟的 CSS 预处理器(或后处理器) Sass/Less/PostCSS,:hover 和 :active 伪类处理起来复杂。另一类是依旧使用 CSS,但使用 JS 来管理样式依赖,代表是 CSS Modules。CSS Modules 能最大化地结合现有 CSS 生态和 JS 模块化能力,API 简洁到几乎零学习成本。发布时依旧编译出单独的 JS 和 CSS。它并不依赖于 React,只要你使用 Webpack,可以在 Vue/Angular/jQuery 中使用。是我认为目前最好的 CSS 模块化解决方案。
animation 是复合属性,其子属性有:
(1) animation-delay 动画延时
(2) animation-direction 动画在每次运行完后是反向运行还是重新回到开始位置重复运行
(3) animation-duration 动画一个周期的时长
(4) animation-iteration-count 动画重复次数,infinite无限次重复动画
(5) animation-name 指定由 @keyframes
(6) animation-timing-function 设置动画速度曲线,默认是 “ease”
(7) animation-fill-mode 指定动画执行后跳回到初始状态还是保留在结束状态
此外,还有 animation-play-state 属性,但是不能简写到 animation 属性中,该属性允许暂停和恢复动画。
基本语法
1 | animation-name: first_animation; |
animation: name duration timing-function delay iteration-count direction;
@keyframes 用于规定动画如何从一种样式逐渐变化为另一种样式,其基本用法如下:
1 | @keyframes first_animation { |
关键词 “from” 和 “to”,等同于 0% 和 100%,表示动画开始状态和结束状态。中间状态由浏览器自动推算。
animation-iteration-count 指定动画播放的次数,默认值为 1。可以指定具体的次数,也可以使用关键字 infinite 让动画无限次播放。
1 | animation-name: first_animation; |
animation-fill-mode 指定动画执行后跳回到初始状态还是保留在结束状态。
animation-fill-mode : none | forwards | backwards | both;
none: 不改变默认行为
forwards:当动画完成后,保持最后一个属性值(在最后一个关键帧中定义)
backwards:让动画回到第一帧的状态(在第一个关键帧中定义)
both:根据 animation-direction 轮流应用 forwards 和 backwards 规则
animation-direction 指定对象动画运动的方向,有以下四种取值:
normal:正常方向,默认
reverse:动画反向运行,方向始终与normal相反
alternate:动画会循环正反方向交替运动
alternate-reverse:动画从反向开始,再正反方向交替运动
animation-play-state 用于手动控制动画的状态,有 paused 和 running 两种取值:
running:默认值,表示动画正常运动
paused:表示暂停动画
目前各大浏览器都支持 transition,所以不加浏览器前缀即可使用。
CSS3 transition 规范定义了以下四个 CSS 属性:
transition-delay(过渡延迟时间)
transition-duration(过渡持续时间)
transition-property(过渡属性)
transition-timing-function(过渡效果的时间曲线)
1 | /* transition: 1s 1s width ease; */ |
默认值为 all,表示浏览器所有能接受的可过渡属性,可以使用单个值或以逗号隔开的多个值。
1 | transition-property: width,height; |
可以在 这里 和 这里 查看哪些 CSS 属性支持 transition。
transition-delay 属性规定了在执行一个过渡之前的等待时间。IE 和 Opera 不接受 transition-duration 在-10ms和10ms之间的值。默认值0表示不过渡直接看到执行后的结果。单位是秒s,也可以是毫秒ms。
1 | transition-delay: 1s; |
动画的执行时间,默认值0表示不过渡。单位是秒s,也可以是毫秒ms。
1 | transition-duration: 1s; |
ease:默认值,缓解效果,变化速度逐渐放慢
linear:线性效果,匀速变化
ease-in:渐显效果,加速变化
ease-out:渐隐效果,减速变化
ease-in-out:渐显渐隐效果
cubic-bezier: 自定义变化速度,可以使用 cubic-bezier 定制想要的效果。
1 | transition: width cubic-bezier(.14,.78,.92,.36) 1s; |
transition 是一个复合属性,可以同时定义
transition-property、transition-duration、transition-timing-function、transition-delay 子属性值。
1 | /* property name | duration | timing function | delay */ |
写复合属性的时候,四个属性是可以改变顺序的,不过两个时间属性若同时出现,第一个代表 duration,第二个代表 delay,如果只出现一个时间属性,则表示 duration。
使用 transition 结合 transform 能够完成一些简单的动画效果
使用 transition 做动画简单易用,不过也存在一些缺点:
(1) 动画需要事件触发
(2) 动画只能执行一次
(3) transition 只能定义开始状态和结束状态,不能定义中间状态
因此如果想要完成比较复杂的动画,还是要用 css3 中的 animation 属性。
本章学习 CSS3 中的 transform 属性。
transform 属性目前还存在浏览器兼容性问题,建议使用 PostCSS 或手动添加浏览器前缀。
使用 transform,元素可以被转换(translate)、旋转(rotate)、缩放(scale)、倾斜(skew)。
transform 属性只对 block 元素生效。
transform: translate(x, y); 表示使元素在 X 轴和 Y 轴移动,y 可以省略,表示不移动。如果参数为负,则表示往相反的方向移动。同时还可以使用 translateX、translateY 和 translateZ 表示在某一个方向移动。Z 轴移动的前提是元素本身或者元素的父元素设定了透视值。
1 | transform: translate(12px, 50%); |
旋转 transform: rotate(angle) angle 取值有:角度值deg,弧度值rad,梯度gard,转/圈turn,正数值代表顺时针旋转,反之逆时针。
rotateX、rotateY、rotateZ 表示分别在 X、Y、Z 轴上旋转。rotate3d(x, y, z, angle) 表示在3维空间旋转。
1 | transform: rotate(-30deg); |
缩放 transform: scale(x, y) 表示使元素在 X 轴和 Y 轴缩放。
1 | transform: scale(2, 0.5); |
倾斜 transform: skew(x, y) 表示 X 轴和 Y 轴倾斜的角度,取值类型为角度值deg。
1 | transform: skew(30deg, 20deg); |
矩阵变形transform: matrix(a,c,e,b,d,f) 相当于直接应用一个[a c e b d f]变换矩阵。
1 | transform: matrix(a, c, b, d, tx, ty) |
transform-origin 用来定义转换元素的位置,在没有重置 transform-origin 改变元素原点位置的情况下,CSS 的变形操作都是以元素自己中心位置进行。
1 | transform-origin: left; |
下面是 B 站上的一则影评,建议高清食用。
]]>公司组织的一个机器学习的小比赛, 数据下载地址 。大意是根据用户所安装的 APP (加密)预测用户的性别,训练数据标记 label (性别),典型的监督学习方案。
下载之后,解压成为文本文件。 数据格式如下:
每一行代表一个用户的数据,一共120万个样本用户数据
每一行都有5列,每一列以制表符 tab 分割(\t)。
第一列是用户编号(已经脱敏,转化成1 ~1,200,000的编号)
第二列是用户的性别 (male/female)
第三列是用户的移动设备类型
第四列是用户的 APP 列表,每个 APP 已经脱敏,以数字编号代替 APP 名称。多个 APP 之间以逗号(,)作为分隔符
第五列是用户所在区域。
其中移动设备类型/APP 列表/区域是特征数据。性别是结果数据。
首先分析数据,一共有机型、APP、区域三个维度。性别可能对 APP 和机型有偏好,但是不能对区域有偏好,而是不同的区域可能对 APP 有不同的偏好,比如某省用户偏爱直播,某省用户偏爱交友等等。
建模方案,把 APP 和 机型(数值化)作为两个维度对数据进行训练,分区域建模,不同的区域使用不同的模型。然后使用全部数据或随机部分数据建模形成公共数据模型,公共模型用来分析用户区域数据不足或者来自未建模区域的数据。
技术方案:Python + scikit-learn + pandas + numpy
环境搭建使用 Anaconda
项目困难主要出现在 APP 降维,也就是判断哪些 APP 与性别相关,这是一个相关性分析的问题。网上找了很多资料,算法描述也有,不过没有找到合适的 Python 实现。Spark 版本的倒是很多,可是不想在一个小项目里面使用两种技术栈。
目前使用上海数据建模,只使用 APP 信息,未加入机型信息,预测准确度大约为79%。
后面会加入机型信息,并使用特征提取对 APP 信息进行降维,希望能提高准确率。
由于公司政策原因,代码不能放入 github,后续会把思路和核心代码写出来。
]]>块状元素居中是一个老生常谈的话题,之前面试的时候考官也曾问到过这个。下面写几种常见的块状元素居中的方式。
假如想要 con 在 box 中居中
1 | <div class="box"> |
1 | .box { |
1 | .box { |
1 | .box { |
1 | .box { |
1 | .box { |
使用 flex 布局的优势不可谓不明显:
传统布局的核心是盒子模型,依赖 display 属性 + position 属性 + float 属性。可以看出来传统布局非常容易实现像 word 左对齐,右对齐这样的功能,可以说,传统布局更适合于文字排版。
flex 是 flexible Box 的缩写,可以看做弹性的盒子模型。
使用 flex 首先要设置父元素 display: flex
。任何元素都可以指定为 flex 布局:
块状元素:
1 | .box { |
行内元素
1 | .box { |
设为 flex 布局以后,子元素的 float、clear 和 vertical-align 属性将失效。
flex 的核心的概念就是 容器 和 轴。容器包括外层的 父容器 和内层的 子容器,轴包括 主轴 和 交叉轴,如下图所示:
容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis)。主轴的开始位置叫做 main start,结束位置叫做 main end;交叉轴同理,
子容器默认沿主轴排列。单个子容器占据的主轴空间叫做 main size,占据的交叉轴空间叫做 cross size。
容器具有这样的特点:父容器可以统一设置子容器的排列方式,子容器也可以单独设置自身的排列方式,如果两者同时设置,以子容器的设置为准。
父容器一共有6个属性: flex-direction, flex-wrap, flex-flow, justify-content, align-items, align-content
属性 | 描述 | 效果 |
---|---|---|
flex-direction: row | (默认值)主轴为水平方向,起点在左端 | |
flex-direction: row-reverse | 主轴为水平方向,起点在右端 | |
flex-direction: column | 主轴为垂直方向,起点在上沿 | |
flex-direction: column-reverse | 主轴为垂直方向,起点在下沿 |
属性 | 描述 | 效果 |
---|---|---|
flex-wrap: nowrap | (默认)不换行 | |
flex-wrap: wrap | 换行,第一行在上方 | |
flex-wrap: wrap-reverse | 换行,第一行在下方 |
flex-direction 属性和 flex-wrap 属性的简写形式,默认值为 row nowrap
属性 | 描述 | 效果 |
---|---|---|
justify-content: flex-start | (默认)起始端对齐 | |
justify-content: flex-end | 末尾段对齐 | |
justify-content: center | 居中对齐 | |
justify-content: space-around | 子容器沿主轴均匀分布,位于首尾两端的子容器到父容器的距离是子容器间距的一半。 | |
justify-content: space-between | 子容器沿主轴均匀分布,位于首尾两端的子容器与父容器相切。 |
属性 | 描述 | 效果 |
---|---|---|
align-items: flex-start | 交叉轴的起点对齐 | |
align-items: flex-end | 交叉轴的终点对齐 | |
align-items: center | 交叉轴的中点对齐 | |
align-items: baseline | 基线对齐(首行文字对齐)所有子容器向基线对齐,交叉轴起点到元素基线距离最大的子容器将会与交叉轴起始端相切以确定基线。 | |
align-items: stretch | (默认)如果子容器未设置高度或设为auto,子容器沿交叉轴方向的尺寸拉伸至与父容器一致 |
子容器一共有6个属性: order, flex-grow, flex-shrink, flex-basis, flex, align-self
默认值为 0,可以为负值,数值越小排列越靠前。order 只能为整数。
属性 | 效果 |
---|---|
order: -1 |
默认值为 0,就是即使存在剩余空间,也不瓜分。如果定义了非 0 值,则按照比例瓜分。flex-grow 只能为整数。
属性 | 效果 |
---|---|
flex-grow: 1 |
默认为1,即如果空间不足,则子容器将缩小。如果所有子容器的 flex-shrink 都为1,当空间不足时,都将等比例缩小。如果某个子容器的 flex-shrink 为0,其他子容器都为1,则空间不足时,前者不缩小。
属性 | 效果 |
---|---|
flex-shrink: 0 |
表示在不伸缩的情况下子容器占据主轴空间的大小,默认为 auto,表示子容器本来的大小。
flex-grow, flex-shrink 和 flex-basis 的简写,默认值为 0 1 auto
align-self 属性允许单个子容器有与其他子容器不一样的对齐方式,默认值为auto,表示继承父元素的 align-items 属性,如果没有父元素,则等同于 stretch。改属性的取值与 align-items 相同。
属性 | 效果 |
---|---|
align-self: flex-end |
1 | var a = 'Hello'; |
猜猜弹框中会输出 ‘Hello’ 还是 ‘World’。揭晓答案: ‘undefined’。这里是一个 JavaScript 的小陷阱–JavaScript 变量提升(Hoisting)。
在 ES6 之前,JavaScript 没有块状作用域(block-level scope),只有函数级作用域(function-level scope)。
1 | // 块级作用域 |
如果在声明一个变量的时候没有使用 var 关键字,那么变量将成为一个全局变量。
1 | (function() { |
在 setTimeout 中的函数是在全局作用域中执行的。
1 | var a = 1 |
为了避免对全局作用域的污染, 所以一般情况下我们尽可能少的声明全局变量。
关于 ES6 中 使用 let 和 const 声明块级作用域的内容,可以参考 JavaScript 中的 let 和 const。
关于 ES5 中严格模式的内容可以参考 JavaScript 严格模式。
关于 JavaScript 中 this 的详细用法可以参考 JavaScript 中 的this。
在 JavaScript 中,函数、变量的声明都会被提升(hoisting)到该函数或变量所在的 scope 的顶部。
1 | var a |
在 JavaScript 中,如果声明一个变量,但是为对其进行赋值,那么 JS 引擎会默认让其等于 undefined。所以上述例子中可以看到变量 b 在声明后,被提升到作用域顶部,和 a 一样,获得了 undefined 的值。
除了变量声明会提升,函数声明也会提升。
1 | console.log(add(1, 2, 3)) // 6 |
值得注意的是:函数声明可以提升,但是函数表达式不能提升。
函数声明: function fun(arguments) {}
函数表达式: var fun = function (arguments) {}
1 | add(1, 2) // 报错:Uncaught TypeError: add is not a function |
函数声明会覆盖变量声明。
1 | var test |
如果变量已经赋值,则无法别覆盖:
1 | var test = 'test' |
在 JavaScript 中,一个变量以四种方式进入作用域 scope:
function foo() {}
;var bar
;函数声明和变量声明总是会被移动(即 hoisting)到它们所在的作用域的顶部。而变量的解析顺序(优先级),与变量进入作用域的 4 种方式的顺序一致,如果一个变量的名字与函数的名字相同,那么函数的名字会覆盖变量的名字,无论其在代码中的顺序如何,但是名字的初始化却是按其在代码中书写的顺序进行的,不受以上优先级的影响。
而变量的解析顺序(优先级),与变量进入作用域的 4 种方式的顺序一致。
1 | // 1. var 声明并且赋值高于函数声明 |
变量声明(赋值) > 形参 > 语言内置变量 > 变量声明不赋值 > 函数外部作用域的其他所有声明
总结变量优先级正好验证了作用域链式查找,局部作用域 -> 上一级局部作用域 -> 全局作用域 -> TypeError。
最后看一个例子:
1 | function test(arguments) { |
今天在吃早饭的时候就被同事@,说有一块页面效果在测试服务器的部署效果跟本地不一样:代码在本地运行没有问题,部署后发现有一个分割线的位置明显不对。来到公司后看了同事的演示,觉得可能是 css 代码压缩时出现了问题。
通过 chrome 查看相关 css,发现了问题所在,有一段代码是这样写的:
1 | .clz_editor_container { |
压缩后在 chrome 中代码变成了这样的:
1 | .clz_editor_container[data-v-5fd4dedf]{ |
然而实际浏览器中前两句都没有生效。
因为在代码压缩时,相同的代码会默认选择比较靠后的,因此 display: -ms-flexbox; -ms-flex-direction: column;
,而 -ms-flexbox
和 -ms-flex-direction
是为了兼容 IE 浏览器而存在的, 所以这两句 css 都没有生效。
而没有压缩的代码在浏览器中运行时,浏览器自动选择了合适的 css 语句所以没有出现问题。
解决方案很简单啦,这应该是同事写代码粗心导致的,直接把 display: flex; flex-direction: column;
加上就行了。而且 idea 里面自动代码兼容性补全功能,所以用 idea 写出的代码应该不会出现这个问题。
然后有同事说应该有一些工具能够自动补全的,于是 google 了一下,发现这种问题早就有非常好的解决方案,那就是 PostCSS 的插件 autoprefixer。
首先安装 webpack 插件 postcss-loader 和 autoprefixer
1 | $ npm i autoprefixer postcss-loader --save-dev |
然后修改 webpack 配置文件,在插件系统中更改 LoaderOptionsPlugin,在 options 中增加 postcss
1 | new webpack.LoaderOptionsPlugin({ |
然后在所有 css 相关的 loader 中增加 postcss-loader
1 | { |
注意 postcss-loader 应该放在 less-loader 和 css-loader 之间,处理顺序为:
less-loader -> postcss-loader -> css-loader -> style-loader
修改前面出问题的 css 为原生
1 | .clz_editor_container { |
重新打包压缩后的 css 如下
重新打开查看效果,问题解决。
注意如果你在 css 中使用 @import
引入其它 css 文件,而被引入的文件在 webpack 打包后又没有加入浏览器前缀的话,建议在 css-loader 中加入 importLoaders=1
参数
1 | { |
PostCSS 是什么?官方给出的定义是: PostCSS 是一个用 JavaScript 转化 CSS 的工具。准确的说,PostCSS 是一个平台,通过一些插件,能做很多事情:
(1) 增加代码可读性
比如刚才我们用的 autoprefixer,通过给 css 添加供应商前缀,让我们的 css 代码更加优雅。
(2) 使用未来 CSS 的语法特性
通过使用 cssnext 插件,可以允许我们使用最新的 css 语法,而不用等待浏览器支持。
(3)global css 终结者
PostCSS 通过 CSS Modules 对 css 命名做模块化处理,一般为添加前缀和后缀,让我们写 css 的时候不必担心命名太通用,只要觉得有意义即可。
(4)避免 css errors
通过使用 stylelint 来避免 css errors。
(4)更强大的栅格系统
LostGrid 通过 calc() 轻松创建强大的栅格系统。
(5)更多插件 更多功能
在 webpack 中使用 PostCSS 的一般方式
1 | $ npm install postcss-loader --save-dev |
1 | module.exports = { |
可以通过在不同路径下创建不同的 config 来实现配置覆盖的功能,在根目录下创建的 postcss.config.js 会被子目录中的配置文件覆盖。
1 | module.exports = { |
1 | module.exports = { |
假如有 style.css 如下
1 | :root { |
webpack 配置文件下
1 | var cssnext = require('cssnext'); |
运行 webpack 命令后,dist 文件夹下面的 style.css 如下
1 | div { |
这里一共使用了三个插件,cssnext 解析 css 自定义属性和 val() 函数,autoprefixer 添加浏览器前缀,postcss-px2rem 完成 px 到 rem 单位的转化。
Webpack + ES6 已经成为目前最流行的前端解决方案,本文是 Webpack2 学习教程。
在 「What is webpack」一文中作者讲述了自己为什么要开发出 webpack。
新建 webpack-demo 文件夹,安装 webpack 到 dev
1 | mkdir webpack-demo |
新建一个 hello.js 文件
1 | function hello () { |
在命令行中输入下面内容进行打包
1 | webpack hello.js hello.bundle.js |
打开打包后的文件发现里面注入了很多 webpack 所需的一些内置函数,比如 __webpack_require__
,除此之外,webpack 还对我们写的代码进行编号,比如刚才我们写的 hello function 在 hello.bundle.js 中的编号就是 /* 0 */
。
新建 style.css 文件
1 | body { |
在 hello.js 中引入该文件
1 | require('./style.css') |
再次使用刚才的命令打包,发现命令行报错
1 | ERROR in ./style.css |
错误提示很明显:模块解析错误,你可能需要一个合适的 loader 去处理这种类型的文件。
webpack 默认不支持 css 文件类型,所以我们来安装 css-loader 和 style-loader
1 | npm i css-loader style-loader --save-dev |
css-loader 是使 webpack 可以处理 css 文件;style-loader 把 css-loader 处理完的代码,新建一个 style 标签,插入到 HTML 代码中。
然后将这两个 loader 引入 hello.js
1 | require('style-loader!css-loader!./style.css') |
再次运行打包命令就可以在 hello.bundle.js 中找到下面这句话
1 | exports.push([module.i, "body {\r\n\tbackground-color: gray;\r\n}", ""]); |
为了查看效果,我们新建 index.html
1 |
|
打开 index.html 查看效果,发现 style-loader 在 head 中插入了一个 style 标签将 css 插入 html 中。
在命令行中输入 webpack
命令,webpack 会自动寻找 webpack.config.js 文件,并按照里面的配置对项目进行打包。还可以通过 --config
参数指定 webpack 配置文件。
webpack.config.js 使用 CommonJS 规范,下面是一个最基础的配置文件
1 | module.exports = { |
entry
参数表明我们的打包是从哪个文件开始的,output
参数定义打包后的文件如何存储。
如果需要使用一些 webpack 的参数,可以使用 npm 脚本来实现,比如
1 | "scripts": { |
上面是我们分析 webpack 打包后文件常用的方式,把每个 modules 显示出来,并且按照文件大小排序。
webpack 根据 entry 创建所有应用程序依赖图表,entry 告诉 webpack 从哪里开始,并遵循着依赖关系图打包。
entry 有以下几种写法
1 | entry: './src/app.js' |
指定多入口主要为了解决两种场景,一个是将业务代码和框架代码分割,一个是为了处理多页面应用。使用 CommonsChunkPlugin 插件可以将公共的类库代码打包成一个 common 模块。这样在多页面程序中可以把共用代码缓存起来,方便其他页面使用。
output 参数告诉 webpack 如何把编译后的文件写入到磁盘里,无论有多少个 entry 都只有一个 output 配置。一般形式的写法如下:
1 | output: { |
output.path 是一个绝对路径,filename 指生产打包文件后的名称
假如 entry 为多入口,使用上述写法只会生产一个 bundle.js,不符合我们代码分割的需求,那么我们可以用一些占位符来表示输出的结果。一共有四种占位符:[id], [name], [hash], [chunkhash]。注意 [hash] 指的是本次打包的 hash,这个 hash 在 webpack 打包时日志的第一行显示。而 [chunkhash] 是每一个 chunk 自己的 hash 值。
1 | entry: { |
hash 值由 md5 算法生成,可以当做每个文件的版本号,这点对于我们管理产品时每次只上线被更改的文件非常有用。如果觉得默认 hash 值太长了,可以通过 [chunkhash:8] 来指定 hash 位数。
通常我们上线产品会使用 cdn 加速静态资源文件的获取,我们可以把 cdn 写入到 output.publicPath 中。publicPath 表示如果产品上线,js 的路径就会自动加上 publicPath。
1 | output: { |
webpack 中把所有的资源都当做一个模块,无论这个文件是代码文件,还是图片文件,只要有对应的 loader 均可以在 webpack 中转换使用,这也是 webpack 最大的优势所在。
前面「引入css文件」中已经展示了如何使用 loader,通常配置方式如下:
1 | module: { |
test 说明了当前 loader 能处理那些类型的文件的正则匹配,use 则指定了 loader 的类型。
注:这里说一下 webpack1 与 webpack2 的区别,在 webpack1 中,使用 module.loaders 声明 loader,而 webpack2 中使用功能更为强大的 module.rules。 为了兼容旧版,module.loaders 语法仍然有效,旧的属性名依然可以被解析。
loader 还可以在使用的时候传入相关的参数,比如我们使用 css-loader 时
1 | module: { |
注:在 webpack 1 中,loader 可以链式调用,上一个 loader 的输出被作为输入传给下一个 loader,通常被用 ! 连写,如 loader: "style-loader!css-loader!less-loader"
。这一写法在 webpack 2 中只在使用旧的选项 module.loaders 时才有效。使用 rule.use 配置选项,use 可以设置为一个 loader 数组。使用 module.rules 时,如果只有一个 loader,既可以用 loader 又可以用 use,但是如果是多 loader,则只能使用 use。
首先安装 babel
1 | npm install babel-loader babel-core --save-dev |
修改 webpack 配置文件
1 | module: { |
注意这里一定要加上 exclude 或者 include,因为 babel-loader 处理的速度非常慢。
然后还需要指定所用 ECMAScript 的版本,假如使用 ES6 语法
1 | npm install babel-preset-es2015 --save-dev |
告诉 webpack babel 使用哪个版本的 preset 有三种方式
(1) 在 webpack 中声明
1 | module: { |
注: 如果 loader 需要传参数的话,既可以写成 query 的形式,也可以写成像 url 传参一样的形式:
1 | rules: [{ |
但是如果为多 loader 的话,只能用 use + options 的形式。
(2) 在根目录创建 .babelrc 文件,文件内容如下
1 | { |
(3) 在 package.json 中指定 preset
1 | "babel": { |
插件是 wepback 的支柱功能。在你使用 webpack 配置时,webpack 自身也构建于同样的插件系统上!插件目的在于解决 loader 无法实现的其他事,在这个页面你可以看到一些 webpack 常用的插件。
由于 plugin 可以传递参数,你必须在 wepback 配置中,向 plugins 属性传入 new 实例。
1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); //通过 npm 安装 |
下面我们以最常用的 html-webpack-plugin 为例,讲解插件的用法。
首先使用 npm 安装插件
1 | npm i html-webpack-plugin --save-dev |
然后在 webpack.config.js 配置文件中使用。
1 | const htmlWebpackPlugin = require('html-webpack-plugin') |
html-webpack-plugin 插件还能接受一些其它参数,比如title
、inject: (true | 'head' | 'body' | false)
、favicon
、minify
、hash
、cache
等。
还可以设置一些自定义的参数,在 html 文件中通过类似 js 模板语言的方式进行引用。
比如在 webpack 配置文件中
1 | plugins: [ |
然后在 index.html 中使用 <%= htmlWebpackPlugin.options.date %>
对 date 进行引用,这样就给了我们更大的自由度,用相同的 html 模板生成不同的 html 文件。
通过加上 minify 来实现对 html 文件的压缩,minify 传入一个 html-minify 对象。
1 | plugins: [ |
对于一个多页面应用程序,需要生成多少个页面,就 new 多少个 htmlWebpackPlugin 实例。假如不同的页面依赖不同的 chunks, 那么我们可以使用 chunks 参数指定当前页面所使用的 chunks。也可以使用 excludeChunks 来指定排除了某些 chunks 以后的全部 chunks。
1 | const htmlWebpackPlugin = require('html-webpack-plugin') |
对应的 html 模板文件为:
1 |
|
运行npm run webpack
后生成 3 个 html 文件,分别引入其所需要的依赖。
假如有这么一段 less
1 | .layer { |
首先安装 less 和 less-loader
1 | npm install less less-loader --save-dev |
在 webpack 配置文件中加入 less-loader
1 | module: { |
loader 的执行顺序为从后往前执行,所以其顺序为 less-loader -> css-loader -> style-loader。 如果需要引入 postcss-loader 的话,应该放在 less-loader 和 css-loader 中间。
图片文件一般使用 file-loader 配合 url-loader,如果有压缩需求的话,可以使用 image-webpack-loader
安装两个 loader
1 | npm i file-loader url-loader --save-dev |
url-loader 的功能与 file-loader 十分相似,不同的是 url-loader 可以指定一个 limit 参数, 当图片或者文件的大小大于 limit 的时候,url-loader 把资源直接交给 file-loader 处理,而当资源小于 limit 的时候,url-loader 会把图片转为 base64 的编码,并直接打包到引用的文件中。
file-loader 打包的文件通过 http 请求获取,url-loader 打包的文件通过 base64 的方式获取,这两个方法各有各的优势。通过 http 载入的图片可以享受到浏览器的图片缓存,当图片重复使用次数比较多的时候具有一定的便利。base64 的方式引入图片可以降低 http 请求的次数,但是也会带来一定程度的代码冗余。
(1) 使用 file-loader
1 | module: { |
(2) 使用 url-loader
1 | module: { |
(3) image-webpack-loader 可以对图片文件进行压缩,并且配合 url-loader 和 file-loader 共同使用
1 | module: { |
loader 的参数也可以通过 options 传递
1 | { |
image-webpack-loader 可以针对不同的图片类型就行压缩,详细的信息可以在官网里面查询。
注:在 image-webpack-loader 实际使用过程中,必须传入一个 options 参数,否则会报错,使用的时候注意一下。
ERROR in Error: Child compilation failed:
Module build failed: TypeError: Cannot read property ‘bypassOnDebug’ of null
本文只是 webpack 打包的一些知识,只涉及到一些基本使用,关于 webpack 在项目中的实际应用,以及打包的一些技巧和优化,会在下一节中讲起。
]]>px 是 css 中最常用的字体大小单位。
px 就是表示 pixel,像素,是屏幕上显示数据的最基本的点;还有一个看起来很像的单位 pt,pt 就是 point,是印刷行业常用单位,等于1/72英寸,一般在打印的时候使用。
像素 px 是相对于显示器屏幕分辨率而言的,所以一般把它看做一个基础单位,很多其它单位都是以 px 为参照的。
em 指的是相对于当前对象内文本的字体大小,比如设置 body 的字体大小(font-size)为 14px,而对 body 内所有的 div 设置字体大小为 1.5em,那么 div 内字体大小就是 14px * 1.5 = 21px
通常写 html 的时候会发生很多嵌套,每个节点都从父节点继承字体大小,这样很难控制每个层级的字体大小。rem (roo em) 应运而生,rem 是指相对于根节点字体大小,通常根节点是指 html 元素。
1 | html { font-size: 14px; } |
这样所有 div 中字体的大小都是 21px 了。
css 中的百分比是一种相对值,使用百分比的关键是找到它的参照物。
属性 | 参照 |
---|---|
width & height | 宽和高在使用百分比值时,其参照一般都是父元素的 content 的宽和高。 |
margin & padding | margin 和 padding,其任意方向的百分比值,参照都是包含块的宽度。 |
border-radius | 为一个元素的border-radius定义的百分比值,参照物是这个元素自身的尺寸。border-radius:50%; |
font-size | 参照是直接父元素的 font-size。 |
line-height | 参照是元素自身的font-size |
vertical-align | 参照是元素自身的line-height |
bottom、left、right、top | 参照是元素的包含块。left和right是参照包含块的宽度,bottom和top是参照包含块的高度。 |
transform: translate | 参照是元素自己的边界框的尺寸 |
移动互联网时代各种设备大小不一,响应式的布局变得更加流行,而响应式布局很大程度上依赖比例规则。
vh 和 vm 也是相对长度,不过其参照是显示窗口的宽度或高度,一般来说 100 vh = viewport 的高度,100vm = viewport 的宽度。
下面一段话是响应式的,你可以缩放浏览器大小来查看效果。
vmin 和 vmax 的出现主要是为了移动设备横竖屏切换。vmax 是相对于 viewport 宽度或者高度中比较大的一个,vmin 则是比较小的那个。比如手机屏幕宽度为1100px,高度为700px,那么 100vmin = 700px, 100vmax = 1100px。
]]>Vue 父子组件之间通信主要采取两种方式,通常可以总结为 props down、events up,父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息,这点跟 React 一模一样。
Vue2.0 废除了 events
、$dispatch
、$broadcast
几个事件,官方推荐使用 全局事件驱动 或者 vuex代替,目前只剩下 vm.$on
、vm.$once
、vm.$off
、vm.$emit
几个事件。
Vue 组件之间的作用域是相互隔离的,父组件向子组件传值只能通过 props 的方式,子组件不能直接调用父组件的数据。在子组件中,如果需要调用父组件传来的参数,必须显式的声明 props。
1 | Vue.component('child', { |
父组件向子组件传值
1 | <child message="hello!"></child> |
props 传递值只能父组件向子组件传递,不能反回来,每当父组件更新时,子组件中的 props 会自动更新。如果在子组件中更改 props,Vue 控制台会给出 warning。因此如果需要在子组件中更改 props 通常会把其作为初始值赋值给某个变量,然后变量的值,或者在计算属性中定义一个基于 props 的值。
如果子组件需要把信息传达给父组件,可以使用 v-on
绑定自定义事件
1 | <div id="counter-event-example"> |
我们给 button-counter 绑定了一个自定义事件 increment
,v-on 绑定事件还可以简写为 @increment=""
。
1 | Vue.component('button-counter', { |
button-counter 组件的模板中包含一个 button,其 click 事件会触发($emit)自定义事件 increment
,因此每次在子组件中点击一次 button,父组件中都会调用 incrementTotal() 方法。
上面讲的两种方法都父子组件之间的通信,有时候非父子关系的组件也需要通信。在 Vue1.0 时代,可以通过 $dispatch 和 $broadcast 来解决,首先 dispatch 到根组件,然后再 broadcast 到子组件。Vue2.0 中官方推荐用 event bus 或者 vuex 解决,event bus 的本质是一个发布者订阅者模式。
var bus = new Vue()
bus.$emit('id-selected', 1)
bus.$on('id-selected', function (id) {})
下面是 stackoverflow 上面的一个例子
1 | <div id="example"> |
1 | var bus = new Vue() |
Vuex 是 Vue 组件的一个状态管理器,相当于一个只为 Vue 服务的 Redux。下面一个图能很好的反映出 Vuex 是如何让组件之间通信的。
下面是 Vuex 官网上给出的一个 计数器的例子
1 | <div id="app"> |
1 | const store = new Vuex.Store({ |
点击查看效果
{{ count }}
在 Vuex 中,store 是组件状态的一个容器,上面的 store 中定义了一个初始的 state 对象,和两个 mutations 函数。我们可以通过 store.state 来获取状态对象,以及通过 store.commit 方法触发状态变更。要注意的是,我们不能直接更改 store 中的状态,改变 store 中的状态的唯一途径就是显式地提交(commit) mutations。
如果想要在 vue2 中使用 dispatch 和 broadcast,可以参考 vue2 组件通信——使用 dispatch 和 broadcast
]]>一共去金山面了三次试,一次是后台开发,两次是前台开发。去年11月份的时候第一次去金山面试服务器开发的职位,很多问题都没有答上来,就惨淡离去了。后来又有一个前端开发的岗位,抱着试一试的态度就去了。
刚开始跟第一个面试官聊得挺不错,技术问题讨论完了,面试官问我能否带项目,我表示自己暂时没有那个能力。面试官又问及学习能力,我表示自己平时一直坚持学习新知识,也有读一些书,然后这个面试官就出去了。出去以后听到外面有人在讨论,我隐约听到刚才的面试官说:「他完全不符合这个岗位的要求』。我想,看来这次又没戏了,然后安慰自己:没关系,回去继续学习就好了,反正知识都是自己的。
过了一会,进来了两个人,一个是项目组的经理,另外一个不知道是什么职位。经理首先跟我说,他们不是进来面试的,就是想跟我聊一下。那就聊呗。原来他们是想招一个有三五年经验的可以直接带项目的人,目前有一个项目的重构工作要做,不过看我表现还行,就想看下我有没有这个能力在短时间内成长为这样的角色。这个时候我内心是窃喜的,对于一个只有一年多工作经验的新人来说,能有这样一个锻炼的机会是多么宝贵,可是又觉得压力挺大,毕竟之前都是在 leader 手下工作,只是做一下 task,框架设计类的工作都是 leader 做好的,我只在里面添加功能而已。
后面一直聊,关于能否胜任这个职位,我始终都没有给经理一个准确的答复,只表示这对我是一个很大的挑战,也是我努力的方向。我想,后来没有拿到这个职位主要也是因为这个原因吧。不过职位是双向选择的,即使我愿意,他们也肯定会把我跟其他应聘者比较,最终选出合适的人选。
跟这个经理聊完已经快要六点了,这个时候我又累又渴,聊了这么久口干舌燥。经理出去后,HR 助理过来说 HR 还在面试别人,等下会过来,让我再稍等一会。那天好像有很多实习生过来,所以就把我排在了最后。
终于等到了 HR,HR 看起来也很疲倦,我就首先向她慰问,表示辛苦了。然后 HR 简单地聊了几句,就结束了,整个过程不到五分钟。
第二天 HR 助理打来电话,说我跟职位要求不是特别符合,不过有另外一个部门的职位问我愿不愿意试一下。我说愿意啊,然后答应过几天再去面试。感觉好累啊。
再一次去面试就轻松多了,跟第一个面试官简单聊了几个技术问题,写了几行代码他就出去了。第二个面试官是经理,也没有多聊,问了一些 HTTP 的问题,就 OK 了,两个面试加一起半个小时不到。
然后 HR 助理过来说之前也跟 HR 聊过了,让我先回去等通知,我就回去了。
第二天收到了 offer,工资比我要的低了一点,不过总体还算满意,就答应了。
首先我的老东家 OOCL 真是一家挺不错的公司,尤其在培养人方面非常舍得投入。而且各种分享,演讲都非常多。入职的时候跟 OOCL 签的培训协议,如果干不满两年,需要赔偿 X 美金(按月递减)的违约金。违约金还挺多的,不过个人觉得非常值,比在社会上报那些乱七八糟的培训班强多了。工作前三个月半封闭脱产培训,请的中科院的老师,讲课深入浅出,在那几个月内我也进步很大。最后离职的时候由于我只工作了一年半,赔偿了一部分钱。除了培训以外,OOCL 整个福利待遇在珠海来说也都算不错,每年两次调薪,竞争力还是比较大。
OOCL 这么好你为啥要走呢?
OOCL 是一个船运公司,有非常复杂的业务。在 OOCL 的时间大部分都在搞业务,而且做的东西很杂,感觉学了很多东西,又样样都不精通。去面试后端的时候,关于缓存,高并发之类的问题都没能答上来。而在互联网公司会觉得自己跟用户更近一些,业务方面也会很好理解。专职做前端或者后端会让自己在某个领域更精通一些,我还是希望成为某个领域的砖家,在金山这个理想实现起来会更快一些。
今天是入职第二天,说实话给不了太多的评价,只能从外部简单对比一下 OOCL 和金山。
OOCL 是香港的公司,感觉大家更 formal 一些,邮件、IM 基本都是英文,穿着也不会太随意。金山是典型的互联网公司,穿拖鞋短裤是家常便饭。
OOCL 没有食堂,不过培训的时候有中餐可以吃,质量参差不齐,好坏看运气。金山的食堂真心不错,荤素搭配,米面汤粉都有,而且三餐免费,赞。
办公环境的话 OOCL 就略胜一筹了,安静整洁,空调全年恒温。金山要乱一些,平时很多人说话,甚至还有人戴着耳机听歌的时候跟着吹口哨。还有一个要吐槽金山的是,新到职,没有新电脑用,为什么大家都有 mac 用,我只能用 win?
OOCL 是不加班的,即使有项目特别紧的时候加班,也会把加班的时间补给你,也就是可以在项目没那么紧的时候多休几天假,这点很人性化。金山据说加班挺严重的,目前还不知道如何。
其实这次跳槽,没有涨多少工资,感觉待遇差不太多。不知道这边工资上升空间大不大。不过金山住房公积金交 12%,这点不错。
技术上其实 OOCL 算是一个比较敢尝新的公司,除了公司主要业务网站一直坚持 JavaEE 的技术栈以外,很多新的业务都是用比较新的技术做的。比如我去年就接触了 MEAN、Hadoop、Scala、Spark、HBase、Hive、Impala、机器学习等很多比较新潮的技术,虽然很多都是浅尝辄止。也是前面几个月维护一个 Node.JS 的项目让我对 JavaScript 生态产生了兴趣,最终转入前端行业的。金山这边似乎也是比较愿意接受吸纳新技术,就我在的项目组而言,已经开始完全用 ES6 开发产品,对 React 和 Vue 也采取乐观的态度。
今年我的目标是丰富自己前端技术栈,提高自己独立开发的能力,弥补自己 CSS 方面的劣势。无论如何,希望今后能过得更好,技术要来越好,钱越来越多。
]]>使用循环的方式创建组件列表
1 | const numbers = [1, 2, 3, 4, 5]; |
使用参数
1 | function NumberList(props) { |
注意上面代码中的 key
,它是一个 string 类型的属性,在创建 lists 元素的时候,你需要添加这个属性,如果不添加会有 warning。
React 元素可以具有一个特殊的属性 key,这个属性不是给用户用的,而是给 React 自己用的。如果我们动态地创建 React 元素,而且 React 元素内包含数量或顺序不确定的子元素时,我们就需要提供 key 这个特殊的属性。
为什么需要给每一个元素一个标识呢?我们知道当组件的属性发生了变化,其 render 方法会被重新调用,组件会被重新渲染。比如元素里面 [{name: 'Leo'}] => [{name: 'Jack'}]
那么有可能是删除了 Leo,然后为 Jack 新建了一个,也有可能是更改了 name 属性,因此为数组中的元素传一个唯一的 key(比如用户的 ID),就很好地解决了这个问题。React 比较更新前后的元素 key 值,如果相同则更新,如果不同则销毁之前的,重新创建一个元素。
Keys 只能被定义在循环里面
以下用法都是错误的
1 | function ListItem(props) { |
下面是正确的用法:
1 | function ListItem(props) { |
在上一章学习 React 组件的时候,想增加 React 对 Ajax 支持的内容,却发现网上的教程竟然用 jQuery 完成 Ajax 请求,个人觉得为了发送一个简单的请求引入 jQuery 库杀鸡焉用宰牛刀啊。其实 W3C 已经有了更好的替代品,那就是: Fetch API。
Fetch API 的出现与 JavaScript 异步编程模型 Promise 息息相关,在 Fetch API 出现之前,JavaScript 通过 XMLHttpRequest(XHR) 来执行异步请求,XHR 将输入、输出和事件模型混杂在一个对象里,这种设计并不符合职责分离的原则。而且,基于事件的模型与 Promise 以及基于 Generator 的异步编程模型不太搭。
Fetch API 提供了对 Headers,Request,Response 三个对象的封装,以及一个 fetch() 函数用来获取网络资源,并且在离线用户体验方面,由于 ServiceWorkers 的介入,Fetch API 也能提供强大的支持。
fetch() 方法被定义在 window 对象中,你可以直接在控制台中输入 fetch() 查看浏览器是否支持,gitHub 上有基于低版本浏览器的兼容实现。
fetch() 方法接受一个参数——资源的路径。无论请求成功与否,它都返回一个 promise 对象,resolve 对应请求的 Response 对象。
1 | let myImage = document.querySelector('.my-image'); |
点击查看效果
在获取请求的 Response 对象后,通过该对象的 json() 方法可以将结果作为 JSON 对象返回,response.json() 同样会返回一个 Promise 对象,因此可以继续链接一个 then() 方法。相比传统的 XHR 的基于事件类型的编程方式,四不四简单很多哈。
Fetch API 引入了3个接口,它们分别是 Headers,Request 以及 Response 。他们直接对应了相应的 HTTP 概念,但是基于安全考虑,有些区别,例如支持CORS规则以及保证 cookies 不能被第三方获取。
通过 Request 构造器函数创建一个新的请求对象,这也是建议标准的一部分。 第一个参数是请求的 url,第二个参数是一个选项对象,用于配置请求。然后将 Request 对象传递给 fetch() 方法,用于替代默认的 url 字符串。
1 | //不缓存响应结果, 方法为 GET |
除此之外,还可以基于 Request 对象创建新对象,比如将一个 GET 请求创建成为一个 POST 请求
1 | let postReq = new Request(req, {method: 'POST'}); |
每个 Request 对象都有一个 header 属性,在 Fetch API 中它对应了一个 Headers 对象。 我们可以使用 Headers 对象构建 Request 对象。而在 Response 对象中也有一个 header 属性,但是响应头是只读的。
Headers 接口是一个简单的多映射的名-值表
1 | let headers = new Headers(); |
也可以传一个多维数组或者 json:
1 | reqHeaders = new Headers({ |
构建 Respondse 对象有什么用呢?通常 Response 的内容在服务端生成,但是 Fetch API 是浏览器里面的内容啊。
对了,就是为了离线应用,通过 Service Worker 浏览器能够获取请求头的内容,然后通过在浏览器中构建响应头来替换来自服务器的响应头以达到构建离线应用的目的(这方面内容以后再说)。
构建方法
1 | let response = new Response( |
Request 和 Response 对象中的 body 只能被读取一次,它们有一个属性叫 bodyUsed,读取一次之后设置为 true,就不能再读取了。
1 | let res = new Response("one time use"); |
这样设计的目的是为了之后兼容基于流的 API,让应用只能消费一次 data,这样就允许了 JavaScript 处理大文件例如视频,并且可以支持实时压缩和编辑。
如何让 body 能经得起多次读取呢?Fetch API 提供了一个 clone() 方法。调用这个方法可以得到一个克隆对象。不过要记得,clone() 必须要在读取之前调用,也就是先 clone() 再读取。
1 | let res = new Response("many times use"); |
虽然 Fetch API 提供了更加简洁的接口,Promise 形式的编程体验,但是它也不是完美的,最大的问题就是不能中断一个请求,并且无法检测一个请求的进度,这些在 XHR 中早就有很好的解决方案。也行 Fetch API 需要更多的时间。
]]>React 提倡组件化的开发方式,每个组件只关心自己部分的逻辑,使得应用更加容易维护和复用。
React 还有一个很大的优势是基于组件的状态更新视图,对于测试非常友好。
React 每一个组件的实质是状态机(State Machines),在 React 的每一个组件里,通过更新 this.state,再调用 render() 方法进行渲染,React 会自动把最新的状态渲染到网页上。
1 | class HelloMessage extends React.Component { |
通过在组件的 constructor 中给 this.state 赋值,来设置 state 的初始值,每当 state 的值发生变化, React 重新渲染页面。
注意:
(1) 请不要直接编辑 this.state,因为这样会导致页面不重新渲染
1 | // Wrong |
使用 this.setState() 方法来改变它的值
1 | // Correct |
(2) this.state 的更新可能是异步的(this.props 也是如此)
React 可能会批量地调用 this.setState() 方法,this.state 和 this.props 也可能会异步地更新,所以你不能依赖它们目前的值去计算它们下一个状态。
比如下面更新计数器的方法会失败:
1 | // Wrong |
第二种形式的 setState() 方法接收的参数为一个函数而不是一个对象。函数的第一个参数为 previous state,第二个参数为当前的 props
1 | // Correct |
实现一个计数器
1 | class HelloMessage extends React.Component { |
React 的数据流是单向的,是自上向下的层级传递的,props 可以对固定的数据进行传递。
1 | class Welcome extends React.Component { |
state 和 props 看起来很相似,其实是完全不同的东西。
一般来说,this.props 表示那些一旦定义,就不再改变的特性,比如购物车里的商品名称、价格,而 this.state 是会随着用户互动而产生变化的特性,比如用户购买商品的个数。
在 React 中,我们可以通过 this.refs 方便地获取 DOM:
1 | class HelloMessage extends React.Component { |
React 组件的生命周期分为三类:
(1) 挂载(Mounting): 已插入真实 DOM
componentWillMount(): 在初次渲染之前执行一次,最早的执行点
componentDidMount(): 在初次渲染之后执行
getInitialState() –> componentWillMount() –> render() –> componentDidMount()
(2) 更新(Updating): 正在被重新渲染
componentWillReceiveProps(): 在组件接收到新的 props 的时候调用。在初始化渲染的时候,该方法不会调用。
shouldComponentUpdate(): 在接收到新的 props 或者 state,将要渲染之前调用。
componentWillUpdate(): 在接收到新的 props 或者 state 之前立刻调用。
componentDidUpdate(): 在组件的更新已经同步到 DOM 中之后立刻被调用。
componentWillReceiveProps() –> shouldComponentUpdate() –> componentWillUpdate –> render() –> componentDidUpdate()
(3) 移除(Unmounting): 已移出真实 DOM
componentWillUnmount(): 在组件从 DOM 中移除的时候立刻被调用。
下面举 React 官网的一个输出时间的例子,在 Clock 渲染之前设置一个定时器,每隔一秒更新一下 this.state.date 的值,并在组件移除的时候清除定时器。
1 | class Clock extends React.Component { |
React 内建的跨浏览器的事件系统,我们可以在组件里添加属性来绑定事件和相应的处理函数。这种事件绑定方法极大的方便了事件操作,不用再像以前先定位到 DOM 节点,再通过 addEventListener 绑定事件,还要用 removeEventListener 解绑。当组件注销时,React 会自动帮我们解绑事件。
React 处理事件与 DOM 处理事件非常相似,有以下两点不同:
1 | class LoggingButton extends React.Component { |
另外一个不同的是 React 不支持向事件处理函数 return false
,一般 HTML 事件函数中,可以通过 return false
来阻止默认行为,比如
1 | <a href="#" onclick="console.log('The link was clicked.'); return false"> |
Vue 阻止浏览器默认行为的方式最简单,用一个装饰符就可以搞定 <form v-on:submit.prevent="onSubmit"></form>
。
而在 React 中,必须调用 preventDefault 方法才能完成以上功能。
1 | function ActionLink() { |
在这里的 e
是 React 封装过后的,因此不用担心游览器差异带来的影响。☺
假设 Greeting 组件根据状态选择渲染 UserGreeting 和 GuestGreeting 中的一个。
1 | function UserGreeting(props) { |
1 | function Mailbox(props) { |
其它类型的逻辑判断,像三元运算符,if else
React 也均支持。
1 | render() { |
1 | render() { |
通过在组件内部 return null
可以达到阻止组件渲染的
1 | function WarningBanner(props) { |
第一章 React 入门 和本章 React 组件都是比较基础的内容,后面会学习全新的程序设计模式 Flux 和 Redux 来管理应用的状态,很多函数式编程的思想正好努力学习一下。
]]>React 首次被提出是在2014年的 F8 大会上,当期的主题为 “Rethinking Web App Development at Facebook”,这也是 React 名字的由来。
React 以组件化的开发方式,专注于 MVC 架构中的 View,即视图, 这使得React很容易和开发者已有的开发栈进行融合。React 推荐将 UI 上每一个功能相对独立的模块定义成组件,然后将小的组件通过组合或者嵌套的方式构成大的组件,最终完成整体 UI 的构建。
Web 开发的最终目的是把数据反映到 UI 上,这时就需要对 DOM 进行操作,复杂或者频繁的 DOM 操作通常是性能瓶颈产生的原因。React 为此引入了虚拟 DOM(Virtual DOM) 的机制:开发者操作虚拟 DOM,React 在必要的时候将它们渲染到真正的 DOM 上。
基于 React 进行开发时所有的 DOM 构造都是通过虚拟 DOM 进行,每当数据变化时,React 都会重新构建整个 DOM 树,然后 React 将当前整个 DOM 树和上一次的 DOM 树进行对比,得到 DOM 结构的区别,然后仅仅将需要变化的部分更新到实际的浏览器。
同时 React 能够批处理虚拟 DOM 的刷新,在一个事件循环(Event Loop)内的两次数据变化会被合并,例如你连续的先将节点内容从 A 变成 B,然后又从 B 变成 A,React 会认为 UI 不发生任何变化。尽管每一次都需要构造完整的虚拟 DOM 树,但是因为虚拟 DOM 是内存数据,性能是极高的,而对实际 DOM 进行操作的仅仅是 Diff 部分,因而能达到提高性能的目的。
1 |
|
上面的 Hello World 的例子中,引入了三个库文件,react.js,react-dom.js 和 babel.js,它们必须首先加载。在之前的版本中,需要加载 “JSXTransformer.js”,后来 React 官方不再维护这个库,由 babel 对 JSX 语法进行编译。
ReactDOM.render 是 React 的最基本方法,用于将模板转为 HTML 语言,并插入指定的 DOM 节点。
一般我们启动一个 React 项目会使用 React 脚手架工具 create-react-app,它会帮助你创建一个基于 webpack、Babel 和 ESLint 的单页面项目。
1 | yarn global add create-react-app |
项目启动后会有一个 “Welcome to React” 的页面自动打开。
打开 package.json 文件,发现并没有找到 webpack、Babel 等 package 相关的依赖,所有的工作都是 “react-scripts” 帮助我们做的,这样极大地降低了初学者入门学习 React 的成本。
HTML 语言直接写在 JavaScript 语言之中,不加任何引号,这就是 JSX 的语法,它允许 HTML 与 JavaScript 的混写。
例如:
1 | let names = ['Leo', 'Jack', 'John']; |
上面代码体现了 JSX 的基本语法规则:遇到 HTML 标签(以 < 开头),就用 HTML 规则解析;遇到代码块(以 { 开头),就用 JavaScript 规则解析。
JSX 允许直接在模板插入 JavaScript 变量。如果这个变量是一个数组,则会展开这个数组的所有成员,代码如下:
1 | let arr = [ |
ReactDOM.render 方法也可以写在函数中,例如:
1 | let t0 = new Date().getTime(); |
定义 React 组件有三种方法,第一种是 JavaScript 函数,第二种是用 ES6 classes 的方式,一个是用 React.createClass(已经过时)
1 | function HelloMessage(props) { |
注意这里调用属性的时候没有 this。
1 | class HelloMessage extends React.Component { |
React.createClass(meta) 方法用于生成组件类,参数 meta 是一个实现预定义接口的 JavaScript 对象,用来对 React 组件原型进行扩展。
在 meta 中,至少需要实现一个 render() 方法,而这个方法, 必须而且只能返回一个有效的 React 元素。这意味着,如果你的组件是由多个元素构成的,那么你必须在外边包一个顶层元素,然后返回这个顶层元素。
1 | const HelloMessage = React.createClass({ |
1 | const HelloMessage = React.createClass({ |
内联 css 的写法与用 JavaScript 直接操作样式相同:
1 | document.getElementById('root').style.paddingLeft='104px'; |
1 | //组合组件 |
ReactJS 入门暂时就到这里,后面会有更加详细的内容。
]]>Yarn 是 Facebook 开发的一款新的 JavaScript 包管理工具, 作为 NPM 的替代产品,主要是为了解决下面两个问题:
相比于 NPM,Yarn 的速度更快,Yarn 会把使用过的模块在本地缓存一份,如果下次还要用到相同版本的模块,那么将会直接使用本地的而不是访问网络重新获取一份。而 NPM 使用的时候,如果不全局安装那么每个项目都要重新下载一次包,浪费时间和资源。
Yarn 在安装模块之前会验证文件完整性。
每当 NPM 或 Yarn 需要安装一个包时,它会进行一系列的任务。在 NPM 中这些任务是按包的顺序一个个执行,这意味着必须等待上一个包被完整安装才会进入下一个;Yarn 则并行的执行这些任务,提高了性能。
NPM 安装包的时候输出惨不忍睹,而 Yarn 的输出就清晰多了。
作用 | NPM 命令 | Yarn 命令 |
---|---|---|
初始化 | npm init | yarn init |
安装 package.json 中的包 | npm install | yarn |
安装某个包 | npm install xxx --save | yarn add xxx |
删除某个包 | npm uninstall xxx --save | yarn remove xxx |
开发模式下安装某个包 | npm install xxx --save-dev | yarn add xxx -dev |
更新 | npm update --save | yarn upgrade |
全局安装 | npm install xxx –global | yarn global add xxx |
清除缓存 | npm cache clean | yarn cache clean |
查看模块信息 | npm info xxx | yarn info xxx |
运行script | npm run | yarn run |
测试 | npm test | yarn test |
在使用 NPM 管理 JavaScript 模块的时候,可以用比较宽松的方式定义某个模块的版本信息,如
1 | *: 任意版本 |
理想状态下使用语义化版本发布补丁不会包含大的变化,但不幸的很多时候并非如此。NPM 的这种策略可能导致两台拥有相同 package.json 文件的电脑安装了不同版本的包,这可能导致一些错误。很多模块的安装错误和环境问题都是由于这个原因导致。
为了避免包版本的错误匹配,一个确定的安装版本被固定在一个锁文件中。每次模块被添加时,Yarn 就会创建(或更新) yarn.lock 文件,这样你就可以保证其它电脑也安装相同版本的包,同时包含了 package.json 中定义的一系列允许的版本。
在 npm 中同样可以使用 npm shrinkwrap 命令来生成一个锁文件,这样在使用 npm install 时会在读取 package.json 前先读取这个文件,就像 Yarn 会先读取 yarn.lock 一样。这里的区别是 Yarn 总会自动更新 yarn.lock,而 npm 需要你重新操作。
npm install 命令会根据 package.json 安装依赖以及允许你添加新的模块; yarn install 仅会按照 yarn.lock 或 package.json 里面的依赖顺序来安装模块。
与 npm install 类似,yarn add 允许你添加与安装模块,添加依赖的同时也会将依赖写入 package.json,类似 npm 的 --save 参数;Yarn 的 --dev 参数则是添加开发依赖,类似 npm 的 --save-dev 参数。
不像 npm 添加 -g 或 --global 可以进行全局安装,Yarn 使用的是 global 前缀(yarn global add xxx)。global 前缀只能用于 yarn add, yarn bin, yarn ls 和 yarn remove。
该命令会查找依赖关系并找出为什么会将某些包安装在你的项目中。也许你知道为什么添加,也许它只是你安装包中的一个依赖,yarn why 可以帮你找出。
相比 NPM,Yarn 可以方便生成锁文件,安装模块时非常迅速并且会将依赖自动添加进 package.json,模块可以并行安装。不过个人认为,Yarn 的优势不是绝对的,毕竟 NPM 久经考验,或许不久的将来,NPM 也会拥有这些特性。
]]>简单的讲,gulp 是一个构建工具,一个基于流的构建工具,一个 nodejs 写的构建工具,使用 gulp 的目的就是为了自动化构建,提高程序员工作效率😂。
1 | npm install --global gulp |
1 | npm install --save-dev gulp |
1 | var gulp = require('gulp'); |
1 | gulp |
默认的名为 default 的任务(task)将会被运行。
想要单独执行特定的任务(task),请输入
1 | gulp <task> <othertask>。 |
1 | var gulp = require('gulp'); |
输出顺序为:
task1
Hello World
task2
(1) 在项目根目录下创建 src 文件目录,里面创建 index.js
(2) 在项目根目录下创建 dist 文件目录
(3) 安装 gulp-uglify
1 | npm install gulp-uglify --save-dev |
(4) 使用 gulp 压缩 index.js 并将结果输出
1 | var gulp = require('gulp'); |
(5) 运行 “gulp” 命令后发现在 dist 目录下生产了压缩后的 index.js
(6) 解释
gulp.src 是输入; gulp.dest 是输出
pipe 是管道的意思,也是 stream 里核心概念,pipe 将上一个的输出作为下一个的输入。src 里所有 js,经过处理1,处理2,变成输出结果,中间的处理 pipe 可以1步,也可以是n步。第一步处理的结果是第二步的输入,以此类推,就像生产线一样,每一步都是一个 task 是不是很好理解呢?
每个独立操作单元都是一个 task,使用 pipe 来组装 tasks,于是 gulp 就变成了基于 task 的组装工具。
在上面的例子中,gulp.src() 函数用字符串匹配一个文件或者文件的编号(被称为“glob”),然后创建一个对象流来代表这些文件,接着传递给 uglify() 函数,它接受文件对象之后返回有新压缩源文件的文件对象,最后那些输出的文件被输入 gulp.dest()函数,并保存下来。
gulp.src() 可以接收以下类型的参数:
js/app.js 精确匹配文件
js/.js 仅匹配 js 目录下的所有后缀为 .js 的文件
js//.js 匹配 js 目录及其子目录下所有后缀为 .js 的文件
!js/app.js 从匹配结果中排除 js/app.js,这种方法在你想要匹配除了特殊文件之外的所有文件时非常好用
*.+(js|css) 匹配根目录下所有后缀为 .js 或者 .css 的文件
假如 js 目录下包含了压缩和未压缩的 JavaScript 文件,现在我们想要创建一个任务来压缩还没有被压缩的文件,我们需要先匹配目录下所有的 JavaScript 文件,然后排除后缀为 .min.js 的文件:
1 | gulp.src(['js/**/*.js', '!js/**/*.min.js']) |
babel 用于转化 JavaScript 代码,比如将 ES6 的语法转化成 ES5,或者将 JSX 语法转化为 JavaScript 语法。
假如上文中提到的 index.js 里面的内容如下:
1 | ; |
使用 babel 转化为 ES5 语法:
(1) 安装 babel-core babel-preset-es2015
1 | npm install --save-dev babel-core babel-preset-es2015 |
(2) 创建 .babelrc 文件, 配置如下
{
“presets”: [“es2015”]
}
(3) 手动使用 babel 转译:
1 | babel src -d lib |
(4) 安装 gulp-babel
1 | npm install --save-dev gulp-babel |
(5) 编写 gulpfile
在根目录新建一个 gulpfile.babel.js 文件。
gulp 原生并不支持 ES6 语法,但是我们可以告诉 gulp 使用 babel 将 gulpfile 转换为 ES5,方法就是将 gulpfile 命名为 gulpfile.babel.js。
(6) 使用 ES6 编写 gulpfile.babel.js
1 | import gulp from 'gulp'; |
打开 lib 目录下的 index.js 文件,就可以查看 babel 编译后的 ES5 语法的文件了。
开始工作以后,每次改动 index.js 都要手动 gulp 一下实在太麻烦了,使用 gulp-watch 可以监听文件变化,当文件被修改之后,自动将文件转换。
(1) 安装 gulp-watch
1 | npm install gulp-watch --save-dev |
(2) 新增 task
1 | gulp.task('watch', () => { |
(3) 启动 watch task
1 | gulp watch |
修改 index.js 后 lib/index.js 也会随之改变。(≧∀≦)ゞ
1 | gulp -T |
默认的,task 将以最大的并发数执行,也就是说,gulp 会一次性运行所有的 task 并且不做任何等待。如果你想要创建一个序列化的 task 队列,并以特定的顺序执行,需要做两件事:
假如我想要 task1 执行完成后再执行 task2, 可以用以下三种方式:
1 | gulp.task('task1', function () { |
1 | gulp.task('task1', function () { |
task 的执行函数其实都有个回调,我们只需要在异步队列完成的时候调用它就好了。
1 | gulp.task('task1', function (cb) { |
所以只要依赖的任务是上面三种情况之一,就能保证当前任务在依赖任务执行完成后再执行。这边需要注意的是依赖的任务相互之间还是并行的。需要他们按顺序的话。记得给每个依赖的任务也配置好依赖关系。
1 | var gulp = require('gulp'); |
随机森林(Random Forest,简称RF),通过集成学习的思想将多棵决策树集成的一种算法,它的基本单元是决策树。从直观角度来解释,每棵决策树都是一个分类器(假设现在针对的是分类问题),那么对于一个输入样本,N棵树会有N个分类结果。而随机森林集成了所有的分类投票结果,将投票次数最多的类别指定为最终的输出。
首先是两个随机采样的过程,random forest 对输入的数据要进行行、列的采样。
对于行采样,采用有放回的方式,也就是在采样得到的样本集合中,可能有重复的样本。假设输入样本为 N 个,那么采样的样本也为 N 个,这选择好了的 N 个样本用来训练一个决策树,作为决策树根节点处的样本,同时使得在训练的时候,每一棵树的输入样本都不是全部的样本,使得相对不容易出现 over-fitting。
对于列采样,从 M 个 feature 中,选择 m 个 (m << M),即:当每个样本有M个属性时,在决策树的每个节点需要分裂时,随机从这 M 个属性中选取出 m 个属性,满足条件 m << M。
对采样之后的数据使用完全分裂的方式建立出决策树,这样决策树的某一个叶子节点要么是无法继续分裂的,要么里面的所有样本的都是指向的同一个分类。分裂的办法是:采用上面说的列采样的过程从这m个属性中采用某种策略(比如说信息增益)来选择1个属性作为该节点的分裂属性。
决策树形成过程中每个节点都要按完全分裂的方式来分裂,一直到不能够再分裂为止(如果下一次该节点选出来的那一个属性是刚刚其父节点分裂时用过的属性,则该节点已经达到了叶子节点,无须继续分裂了)。
假设有一组相亲网站提供的数据,抽取特征后发现是否相亲有四个因素组成: 年龄,是否有房,收入,是否公务员
age, house, income, governor, go_date
30, 1, 80, 1, 1
28, 0, 30, 0, 0
29, 0, 80, 1, 1
32, 1, 40, 1, 1
32, 0, 100, 1, 1
40, 1, 30, 1, 0
28, 1, 40, 1, 1
57, 0, 80, 1, 0
45, 0, 78, 0, 0
34, 0, 70, 1, 0
…
那么假如有一个新的会员注册后,填写了信息如下,
年龄: 33
是否有房: 无
收入: 80
是否公务员: 是
那么请问这位会员是否能得到相亲的机会?
1 | from numpy import genfromtxt |
决策树是一个非参数的监督式学习方法,主要用于分类和回归,算法的目标是通过推断数据特征,学习决策规则从而创建一个预测目标变量的模型。决策树(decision tree)是一个树结构(可以是二叉树或非二叉树)。其每个非叶节点表示一个特征属性上的测试,每个分支代表这个特征属性在某个值域上的输出,而每个叶节点存放一个类别。使用决策树进行决策的过程就是从根节点开始,测试待分类项中相应的特征属性,并按照其值选择输出分支,直到到达叶子节点,将叶子节点存放的类别作为决策结果。
决策树(Decision Tree)是一种简单但是广泛使用的分类器。通过训练数据构建决策树,可以高效的对未知的数据进行分类。决策数有两大优点:
决策树既可以做分类,也可以做回归。
以文章开始的图片为例子,假设银行贷款前需要审查用户信息,来确定是否批准贷款,构造数据 data.scv 如下:
house, married, income, give_loan
1, 1, 80, 1
1, 0, 30, 1
1, 1, 30, 1
0, 1, 30, 1
0, 1, 40, 1
0, 0, 80, 1
0, 0, 78, 0
0, 0, 70, 1
0, 0, 88, 1
0, 0, 45, 0
0, 1, 87, 1
0, 0, 89, 1
0, 0, 100, 1
1 | from numpy import genfromtxt |
回归和分类不同的是向量 y 可以是浮点数。
1 | from sklearn import tree |
scikit-learn 官网给出的例子是:
1 | import numpy as np |
首先,逻辑回归是一个分类算法而不是一个回归算法,该算法可根据已知的一系列因变量估计离散数值(比方说二进制数值 0 或 1 ,是或否,真或假),它通过将数据拟合进一个 逻辑函数 来预估一个事件出现的概率。因为它预估的是概率,所以它的输出值大小在 0 和 1 之间(正如所预计的一样)。
[比利时的人口增长数量图]逻辑函数由于它的S形,有时也被称为sigmoid函数。
通过一个简单的例子来理解这个算法。
假设你的朋友让你解开一个谜题。这只会有两个结果:你解开了或是你没有解开(离散值)。想象你要解答很多道题来找出你所擅长的主题。这个研究的结果就会像是这样:假设题目是一道十年级的三角函数题,你有 70% 的可能会解开这道题。然而,若题目是个五年级的历史题,你只有 30% 的可能性回答正确。这就是逻辑回归能提供给你的信息。
逻辑回归主要用于分类,比如邮件分类,是否肿瘤、癌症诊断,用户性别判断,预测用户购买产品类别,判断评论是正面还是负面等。
逻辑回归的数学模型和求解都相对比较简洁,实现相对简单。通过对特征做离散化和其他映射,逻辑回归也可以处理非线性问题,是一个非常强大的分类器。因此在实际应用中,当我们能够拿到许多低层次的特征时,可以考虑使用逻辑回归来解决我们的问题。
我们假设输入是一个特征矩阵或者 csv 文件,我们使用 NumPy 来载入 csv 文件。
以下是从 UCI 机器学习数据仓库中下载的数据。
1 | import numpy as np |
数据归一化是指把数字变成(0,1)之间的小数。
数据的标准化是将数据按比例缩放,使之落入一个小的特定区间。
大多数机器学习算法中的梯度方法对于数据的缩放和尺度都是很敏感的,在开始跑算法之前,我们应该进行归一化或者标准化的过程,这使得特征数据缩放到 0-1 范围中。scikit-learn 提供了归一化和标准化的方法:
1 | from sklearn import preprocessing |
在解决一个实际问题的过程中,选择合适的特征或者构建特征的能力特别重要。这成为特征选择或者特征工程。
特征选择时一个很需要创造力的过程,更多的依赖于直觉和专业知识,并且有很多现成的算法来进行特征的选择。
下面的树算法(Tree algorithms)计算特征的信息量:
1 | from sklearn import metrics |
机器学习是一个过程,这样的过程包括数据处理 + 模型训练,而特征提取是数据处理中不可或缺的一环。
比如预测什么样的生活方式特征是引发冠心病 (CHD) 的危险因素?给定具有吸烟状态、饮食、锻炼、饮酒和 CHD 状态度量的患者样本,可以使用这四个生活方式变量建立一个模型,用于预测患者样本中 CHD 的存在性。然后可使用此模型为每个因子推导几率比估计值,从而获知某些信息,例如吸烟者比非吸烟者在何种程度上更易患 CHD。
大多数问题都可以归结为二元分类问题。这个算法的优点是可以给出数据所在类别的概率。
1 | from sklearn import metrics |
以上 加载数据 -> 数据归一化 -> 特征选择 -> 算法选择 既是机器学习的一般代码逻辑。如果选择其它算法,只需要更改最后一步算法选择即可。
上次的 ITA 项目开始接触机器学习相关的知识,从本文开始,我将学习并介绍机器学习最常用的几种算法,并使用 scikit-learn 相关模型完成相关算法的 demo。
线性回归,是利用数理统计中回归分析,来确定两种或两种以上变量间相互依赖的定量关系的一种统计分析方法。我们通过拟合最佳直线来建立自变量和因变量的关系,这条最佳直线叫做回归线,并且用 Y= a*x + b
这条线性等式来表示。
理解线性回归可以想象一下一般人身高与体重之间的关系,在不能准确测试体重的情况下,按照身高进行排序,也能大体得出体重的大小。这是现实生活中使用线性回归的例子。
在这个例子中,Y 是体重(因变量),x 是身高(自变量),a 和 b 分别为斜率和截距,可以通过最小二乘法获得。
自己伪造了一些数据
1 | import matplotlib.pyplot as plt |
1 | from sklearn.linear_model import LinearRegression |
上述代码中 sklearn.linear_model.LinearRegression 类是一个估计器(estimator)。估计器依据观测值来预测结果。在 scikit-learn 里面,所有的估计器都带有:
fit() 用来分析模型参数,predict() 是通过 fit()算出的模型参数构成的模型,对解释变量进行预测获得的值。
因为所有的估计器都有这两种方法,所有 scikit-learn 很容易实现不同的模型。
线性回归的两种主要类型是一元线性回归和多元线性回归。一元线性回归的特点是只有一个自变量。多元线性回归则存在多个自变量。找最佳拟合直线的时候,你可以拟合到多项或者曲线回归。这些就被叫做多项或曲线回归。
一元线性回归模型是 Y= a*x + b
,求解一元线性回归模型的本质就是求解参数 a 和 b 的过程,最常用的方法为最小二乘法。
模型的残差是训练样本点与线性回归模型的纵向距离
1 | # 残差预测值 |
如图所示:
我们可以通过残差之和最小化实现最佳拟合,也就是说模型预测的值与训练集的数据最接近就是最佳拟合。对模型的拟合度进行评估的函数称为残差平方和(residual sum of squares)成本函数。就是让所有训练数据与模型的残差的平方之和最小化,如下所示:
其中, yi 是观测值, f(xi)f(xi) 是预测值。
1 | import numpy as np |
残差平方和: 2.05
使用线性回归得出模型后,我们可以用 R 方(r-squared)评估模型的效果。R方也叫确定系数(coefficient of determination),表示模型对现实数据拟合的程度。
一元线性回归中R方等于皮尔逊积矩相关系数(Pearson product moment correlation coefficient或Pearson’s r)的平方。这种方法计算的R方一定介于0~1之间的正数。其他计算方法,包括scikit-learn中的方法,不是用皮尔逊积矩相关系数的平方计算的,因此当模型拟合效果很差的时候R方会是负值。
LinearRegression的score方法可以计算R方
1 | ## 测试集 |
R 方: 0.898422638707
R 方是 0.898 说明测试集里面大多数的数据都可以通过模型解释
多元回归即存在多个自变量,比如影响体重的因素不仅仅有身高,还有胸围,假设 x 中的第一个参数为身高,第二个参数为胸围。
1 | from sklearn.linear_model import LinearRegression |
Predicted: 56.05, Target: [56]
Predicted: 60.03, Target: [63]
Predicted: 61.30, Target: [63]
Predicted: 65.56, Target: [72]
Predicted: 82.42, Target: [80]
R-squared: 0.83
上面两个例中,都假设自变量和响应变量的关系是线性的。真实情况未必如此,现实世界中的曲线关系都是通过增加多项式实现的,其实现方式和多元线性回归类似。在 scikit-learn 中,我们使用 PolynomialFeatures 构建多项式回归模型。下面比较多项式回归和线性回归的区别。
1 | from sklearn.preprocessing import PolynomialFeatures |
我们不断改变 polynomial_featurizer = PolynomialFeatures(degree=3) 中 degree 的参数,当 degree = 5 的时候曲线经过所有的点,这种情况就成为拟合过度(over-fitting)。当模型出现拟合过度的时候,并没有从输入和输出中推导出一般的规律,而是记忆训练集的结果,这样在测试集的测试效果就不好了。
]]>1 | var message // 变量声明之后默认取得了 undefined 值 |
Null 类型只有一个值 null,null 表示一个空指针对象。
1 | typeof null // "objec" |
如果定义变量准备在将来保存对象,最好讲该变量初始化为 null,这样可以通过检查 null 来判断是否已经保存了一个对象的引用。
实际上,undefined 值派生自 null
1 | null == undefined // true |
null vs undefined
尽管 null 和 undefined 之间的相等操作符(==)返回 true,不过它们的用途完全不同,如前所述,无论什么情况下,没有必要把一个变量的值设为 undefined,而如果一个变量将来要保存对象,应该将其显式地设为 null。
对于任何数据类型,调用 Boolean() 函数,总是会返回一个 Boolean 值。
1 | Boolean(0) // false |
(1) 整数:
1 | var intNum = 55 |
(2) 浮点数:
1 | 3e-17 // 0.000...03 |
ECMAScript 最小数:Number.MIN_VALUE,在大多数浏览器中为 5e-324。
ECMAScript 最大数:Number.MAX_VALUE,在大多数浏览器中为 1.7976931348623157e+308。
如果计算超过 JavaScript 数值范围,会自动转为特殊的 Infinity 值,负数则为 -Infinity。Infinity 不能参与数值计算。通过 isFinite() 函数判断参数是否位于最大值和最小值之间。
1 | 1 / 0 // Infinity |
(3) NaN (Not a Number)
NaN 用来表示本来要返回数值的操作数未返回数值的情况,避免抛出错误。
NaN 的设计有两个特点:
1.任何涉及 NaN 的操作都返回 NaN
2.NaN与任何值都不相等,包括 NaN 本身
1 | 0/0 // NaN |
针对这两个特点,ECMAScript 设计了 isNaN() 函数。这个函数帮助我们判断参数是否 “不是数值”。isNaN() 接受参数后,会尝试将这个值转换为数值,如果这个值不能被转换为数值,则返回 true。
1 | isNaN(NaN) // true |
(4) 数值转换
Number() 函数转换规则如下:
1.如果是 Boolean 值,返回 1 或者 0。
2.数字直接返回。
3.null 返回 0。
4.undefined 返回 NaN。
5.字符串:如果是十进制整数,八进制整数或者十六进制整数返回十进制整数,空字符串返回 0,其它均返回 NaN。
6.如果是对象,调用对象的 valueOf() 方法,然后按照前面的转换规则转换,如果转换值为 NaN,则调用对象的 toString() 方法。
parseInt()
1 | parseInt('1234blue') // 1234 |
parseInt() 解析八进制字面量的字符串时,ES3 和 ES5 存在区别,在 ES3 中 ‘070’ 被当做八进制字面量,ES5 则当做 ‘70’。
因此 parseInt 可以接收第二个参数,表示以多少进制解析第一个参数。
1 | parseInt('0xAF', 16) // 175 |
Symbol 是 ES6 新增的数据类型,用来解决对象中属性名重复的问题,Symbol 表示独一无二的值,通过 Symbol 函数生成。
1 | Symbol("foo") !== Symbol("foo") // true |
Object 对象是一组数据和功能的集合。
1 | var o = new Object() |
关于 Object 对象的详细内容,可以参考 深入学习JavaScript——Object对象 和 使用 Object.defineProperty 为对象定义属性。
(1) typeof 操作符
typeof 操作符返回值一共有7种:number,boolean,symbol,string,object,undefined,function。
1 | typeof '' // string 有效 |
(2) instanceof
instanceof 用来判断 A 是否为 B 的实例,需要注意的是,instanceof 检测的是原型。
可以理解为:
1 | instanceof (A, B) { |
1 | [] instanceof Array // true |
[] 的 __proto__
指向了 Array.prototype,而 Array.prototype.__proto__
又指向了 Object.prototype,而 Object.prototype.__proto__
指向了 null,因此 []、Array、Object 在内部形成了一条原型链。instanceof 只能用来判断两个对象是否属于实例关系,而不能判断一个对象实例具体属于哪种类型。
(3) constructor
当一个函数 F 被定义的时候,JS 引擎会自动帮其添加 prototype,并在 prototype 上添加一个 constructor 属性,并让其指向 F 的引用。
当实例化 F 的时候,var f = new F()
,F 原型上的 constructor 传递到了 f 上,因此 f.constructor === F
。
F 利用原型对象上的 constructor 引用了自身,当 F 作为构造函数来创建对象时,原型上的 constructor 就被遗传到了新创建的对象上, 从原型链角度讲,构造函数 F 就是新对象的类型。这样做的意义是,让新对象在诞生以后,就具有可追溯的数据类型。
1 | ''.constructor === String |
利用 constructor 判断数据类型存在的问题:
(4) toString
toString() 是 Object 的原型方式,调用该方法,默认返回当前对象的 [[CLass]]
,其格式为 [object Xxx],其中 Xxx 就是对象的类型。
1 | Object.prototype.toString.call('') // [object String] |
基本数据类型复制相当于在内存中新开辟一块内存,引用数据类型的复制相当于在内存中创建了一个新的指针,指向存储在堆中的一个对象。
ECMAScript 中所有的函数都是 按值传递参数 的。也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另外一个变量一样。
在向参数传递基本数据类型的值时,被传递的值会被复制给一个局部变量(即命名参数,也就是 arguments 对象中的一个元素)。在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数外部。
1 | function setName (obj) { |
<script>
元素定义了6个属性:要注意的是,带有 src 的 <script>
元素中不应该再包含额外的代码,如果包含了嵌入的代码,则只会下载外部文件,嵌入的代码不会执行。
按照惯例,所有的 <script>
都应该放入 <head>
中,但是这就意味着必须要等所有的 JavaScript 代码下载解析和执行完毕后才能开始呈现页面内容(浏览器在遇到 body 标签时,才开始呈现页面内容)。假如有很多 JavaScript 代码需要执行的话,就会导致浏览器窗口出现空白,因此比较好的做法是把 JavaScript 代码放在 <body>
的最后。
HTML4.01 中为 <script>
增加了 defer 属性,这个属性用来表明脚本执行的时候不会影响页面结构,也就是说脚本会延迟到整页面解析完毕后再运行。因此在 <script>
中设置 defer 属性,相当于告诉浏览器,立即下载,但延迟执行。
1 |
|
在这个例子中,虽然 <script>
放在了 head 中,但是其中包含的脚本将延迟到浏览器解析到 </html>
标签才会开始执行。HTML5 规范要求脚本按照他们出现的先后顺序执行,因此第一个延迟脚本 a.js 会优先于 b.js 执行,而这两个脚本会先于 DOMContentLoaded 事件执行。在现实中,延迟脚本不一定会按照顺序执行,也不一定会在 DOMContentLoaded 事件触发之前执行,因此最好只包含一个延迟脚本。
defer 属性只适用于外部脚本文件,因此嵌入脚本的 defer 属性会被浏览器忽略,而且各个浏览器对 defer 属性的处理不尽相同,因此把延迟脚本放在页面底部仍是最佳选择。
在 XHTML 文档中,要把 defer 属性设置为 defer=“defer”
HTML5 为 <script>
元素定义了 async 属性。async 只适用于外部脚本文件,并且告诉浏览器立即下载文件。但与 defer 不同的是,标记为 async 的脚本并不能保证按照指定它们的先后顺序执行。例如
1 | <html> |
在上述代码中,b.js 可能会在 a.js 之前执行,因此,确保两者之间互不依赖非常重要,指定 async 属性的目的是不让页面等待两个脚本下载和执行,从而异步脚在页面其它内容。因此,建议异步脚本不要在加载期间修改 DOM。
异步脚本一定会在页面 load 事件之前执行,但可能会在 DOMContentLoaded 事件触发之前或之后执行。
下面这张图能很好地说明 defer 与 async 之间的关系:
从图中我们可以得出以下几点:
上面提到的只是规范,但是各个厂商的实现可能有所不同,chrome 浏览器首先会请求 HTML 文档,然后对其中的各种资源(图片、CSS、视频等)调用相应的资源加载器进行异步网络请求,同时进行 DOM 渲染,直到遇到 <script>
标签的时候,主进程才会停止渲染等待此资源加载完毕然后调用 V8 引擎对 js 解析,继而继续进行 DOM 解析。可以理解为如果加了 async 属性就相当于单独开了一个进程去独立加载和执行,而 defer 是和将 <script>
放到 body 底部一样的效果。
为验证我们设计测试代码如下:
1 |
|
可以看到几个资源是异步加载并且执行后才开始出现首屏效果,首屏时间接近 1000ms,还是比较慢的。
放在 body 底部的时候,首屏出现的时间快了很多,大约在 500ms 左右,资源文件在 HTML 解析后按顺序加载执行。
defer 为延迟执行,但是下载是可以异步下载的,首屏时间不到 600ms,但是慢于 script 放于 body 底部。
async 为异步代码,所有的代码都是在页面解析完成后执行,但是执行顺序并非按照代码书写顺序。
两个放在一起更能看出效果
事件捕获:当事件发生时(onclick, onmouseover……),浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件。(IE10 及以下浏览器不支持捕获型事件)
事件冒泡:与事件捕获恰恰相反,事件冒泡顺序是由内到外进行事件传播,直到根节点。
(1) onlick -->事件冒泡,重写 onlick 会覆盖之前属性,没有兼容性问题。DOM0 级事件处理程序,每个元素都有自己的事件处理程序属性,这些属性通常全部小写,将这些属性的值全部设置为一个函数,就可以指定事件处理程序。
1 | var el = document.getElementById('myBtn') |
(2) addEventListener(event, listener, useCapture)
参数定义:event—(事件名称,如 click,不带 on),listener—事件监听函数,useCapture—是否采用事件捕获进行事件捕捉,默认为 false,即采用事件冒泡方式。 IE8 及以下不支持,属于 DOM2 级的方法,可添加多个方法不被覆盖。
1 | // 事件类型没有 on,false 表示在事件第三阶段(冒泡)触发,true表示在事件第一阶段(捕获)触发。 如果handle是同一个方法,只执行一次。 |
(3) attachEvent(event.type, handle) IE 特有,兼容 IE8 及以下,可添加多个事件处理程序,与 DOM 方法不同的是,多个事件的执行顺序与添加顺序相反,attachEvent 只支持冒泡阶段。
1 | // 如果 handle 是同一个方法,绑定几次执行几次,这点和 addEventListener 不同,事件类型要加 on, 例如 onclick 而不是 click |
使用 attachEvent() 添加的事件可以通过 detachEvent() 来移除,条件是必须提供相同的参数,与 DOM 方法一样,这也意味着添加的匿名函数不能被移除。
(4) 默认事件行为:href=’’,submit表单提交等
1 | ele.onclick = function() { |
1 | element.addEventListener('click', function(e) { |
1 | element.attachEvent("onclick", function(e) { |
属性/方法 | 类型 | 读写 | 说明 |
---|---|---|---|
Event.bubbles | Boolean | 只读 | 表示该事件是否在 DOM 中冒泡 |
Event.cancelable | Boolean | 只读 | 表示这个事件是否可以取消 |
Event.currentTarget | Element | 只读 | 当前注册事件的对象的引用,当前事件需要传递到的对象 |
Event.defaultPrevented | Boolean | 只读 | 表示了是否已经执行过了 event.preventDefault() |
Event.eventPhase | Integer | 只读 | 指示事件流正在处理哪个阶段: 1.捕获 2.目标 3. 冒泡 |
Event.target | Element | 只读 | 对事件起源目标的引用 |
Event.timeStamp | Number | 只读 | 事件创建时的时间戳,毫秒级别 |
Event.type | String | 只读 | 事件的类型(‘click’) |
event.preventDefault | Function | 只读 | 取消事件的默认行为,如果 cancelable 是 true,则可以使用这个方法 |
event.stopImmediatePropagation | Function | 只读 | 取消事件的进一步捕获或者冒泡,同时阻止任何事件处理程序被调用(DOM3) |
event.stopPropagation | Function | 只读 | 取消事件的默认行为,如果 bubbles 是 true,则可以使用这个方法 |
在事件处理程序的内部,对象的 this 始终等于 currentTarget 的值,而 target 则只包含事件的实际目标。如果直接将事件处理程序指定给了目标元素,则 this, currentTarget 和 target 包含相同的值。
1 | var btn = document.getElementById('myBtn') |
由于 click 事件的目标是按钮,因此这三个值是相等的,但如果事件处理程序存在于按钮的父节点中,结果就不一样了。
1 | document.body.onclick = function (e) { |
单击这个按钮时,this 和 currentTarget 都等于 document.body,因为事件处理程序是注册到这个元素上的。然而 target 元素却等于按钮元素,因为它是 click 事件的真正目标。
访问 IE 中的 event 对象时,如果使用 DOM0 级方法添加事件处理程序,event 对象作为 window 对象的一个属性存在。使用 attachEvent() 添加事件处理程序时,event 会作为参数传入,也可以从 window 对象中访问 event 对象,就像 DOM0 级方法一样。
1 | // onclick |
属性/方法 | 类型 | 读写 | 说明 |
---|---|---|---|
Event.cancelable | Boolean | 读/写 | 默认为 false,但将其设置为 true 就可以取消事件冒泡(与 DOM0 级的 stopPropagation()方法的作用相同) |
Event.returnValue | Boolean | 读/写 | 默认为 false,但将其设置为 true 就可以取消事件的默认行为(与 DOM 中的 preventDefault()方法的作用相同) |
Event.srcElement | Element | 只读 | 事件的目标(DOM 中的 target) |
Event.type | String | 只读 | 事件的类型 |
事件处理程序的作用域是根据指定它的方式来确定的,所以其 this 也会有所不同,比较好的办法是用 event.srcElement。
1 | var btn = document.getElementById('myBtn') |
IE 事件中的 returnValue 相当于 DOM 中的 preventDefault() 方法,它们的作用都是取消事件的默认行为,不过这里不能确定事件的默认行为是否已经被取消。
cancelBubble 属性与 DOM 中的 stopPropagation() 方法作用相同,用来阻止事件冒泡,由于 IE 不支持事件捕获,因此只能阻止事件冒泡,而 stopPropagation() 同时可以取消事件冒泡和捕获。
1 | var btn = document.getElementById('myBtn') |
JavaScript 中实现事件绑定主要使用两个方法: addEventListener、attachEvent。
为了兼容浏览器,按照网上通用的方案对事件进行封装
1 | // 事件绑定 |
1 | // 事件解绑 |
1 | // 阻止默认事件 |
HTML内容:
1 | <body> |
css
1 | #parent{ |
JavaScript
1 | let parent = document.getElementById('parent') |
通过 ‘addEventListener’ 方法,采用事件冒泡方式给 DOM 元素注册 click 事件,点击“子元素”,控制台依次输出 “click-child” --> “click-parent” --> “click-body”。
事件触发顺序是由内到外的,这就是事件冒泡,虽然只点击子元素,但是它的父元素也会触发相应的事件。
如果点击子元素不想触发父元素的事件怎么办?
那就是停止事件传播—event.stopPropagation()
1 | child.addEventListener('click', function(e){ |
修改上面事件冒泡的例子
1 | let parent = document.getElementById('parent') |
父元素通过事件捕获的方式注册了 click 事件,所以在事件捕获阶段就会触发,然后到了目标阶段,即事件源,之后进行事件传播,parent 同时也用冒泡方式注册了 click 事件,所以这里会触发冒泡事件,最后到根节点。这就是整个事件流程。
事件委托(事件代理):利用事件冒泡的特性,将里层的事件委托给外层事件,根据 event 对象的属性进行事件委托,改善性能。
使用事件委托能够避免对特定的每个节点添加事件监听器;事件监听器是被添加到它们的父元素上。事件监听器会分析从子元素冒泡上来的事件,找到是哪个子元素的事件。
委托在 JQuery 中已经得到了实现,即通过 $(selector).on(event,childSelector,data,function,map) 实现委托,一般用于动态生成的元素,当然 JQuery 也是通过原生的 js 去实现的,下面举一个简单的栗子,如果要单独点击 table 里面的 td,普通做法是 for 循环给每个 td 绑定事件,td 少的话性能什么差别,td 如果多了,就不行了,我们使用事件委托:
HTML
1 | <table id="outside" border="1" style="cursor: pointer;"> |
JavaScript
1 | let out = document.getElementById('outside') |
table01 | table02 | table03 | table04 | table05 | table06 | table07 | table08 | table09 | table10 |
事件的三个阶段分别为:捕获,目标和冒泡,低版本 IE 不支持捕获。绑定事件的方法为 addEventListener 和 attachEvent。addEventListener 方法的第三个 boolean 型参数表示添加的事件为捕获或者冒泡,true 代表捕获,false 代表冒泡。
事件冒泡的优点为:
JavaScript = ECMAScript + DOM + BOM
1.ECMAScript 为 JavaScript 提供核心语言功能,是由欧洲计算机制造商协会(ECMA)39号技术委员会(TC39)制定的一种通用、跨平台、供应商中立的脚本语言和语义。ECMAScript 是一种由 ECMA 组织通过 ECMA-262 标准化的脚本程序设计语言。ECMA-262 标准没有参考 Web 浏览器,它规定了语言的语法、类型、语句、关键字、保留字、操作符、对象。
2.DOM (文档对象模型) 是针对 XML 但是经过扩展用于 HTML 的应用程序编程接口(API)。DOM 把 HTML 页面映射为一个多层节点结构,开发人员借助 DOM 提供的 API,可以轻松地删除,添加,替换或者修改节点。
3.BOM(浏览器对象模型)指的是由 Web 浏览器暴露的所有对象组成的表示模型。从根本上将 BOM 只处理浏览器窗口和框架,但是人们习惯把针对浏览器的 JavaScript 扩展也算作 BOM 的一部分,例如:浏览器弹出新窗口的功能;移动、缩放和关闭浏览器窗口的功能;navigator 对象;location 对象; screen 对象;cookies 支持;XMLHttpRequest 和 IE 的 ActiveXObject 对象。BOM 直到 HTML5 才有了规范可以遵守,在此之前每个浏览器都有自己不同的实现。
DOM1 级由两个模块组成,DOM 核心(DOM Core)和 DOM HTML。其中,DOM Core 规定如何映射基于 XML 的文档结构,DOM HTML 模块则在 DOM Core 基础上加以扩展,添加了针对 HTML 的对象和方法。
DOM2 在原有的 DOM 基础上又扩充了鼠标和用户界面事件、范围、遍历(迭代 DOM 文档的方法)等细分模块,并且通过对象接口增加了对 CSS 的支持。DOM2 级引入的模块有:
- DOM 视图(DOM Views):定义了追踪不同文档的视图接口。
- DOM 事件(DOM Events):定义了事件和事件处理的接口。
- DOM 样式(DOM Style):定义了基于 CSS 为元素样式的接口。
- DOM 遍历和范围(DOM Traversal and Range):定义了遍历和操作文档树的接口。
DOM3 级进一步扩展 DOM,引入了以统一方式加载和保存文档的方法——在 DOM 加载和保存(DOM Load and Save)模块中定义,新增了 DOM 验证(DOM Validation)。DOM3 级也对 DOM Core 进行了扩展,开始支持 XML 1.0 规范。
DOM0 级,DOM0 级标准本质上不存在,所谓 DOM0 只是 DOM 历史坐标中的一个参照点,具体来说,DOM0 级是指 Internet Explorer 4.0 和 Netscape Navigator 4.0 最初支持的 DHTML。
可以通过以下代码确定浏览器是否支持 DOM 模块:
1 | var supportsDOM2Core = document.implementation.hasFeature('core', '2.0') |
用 Angular + socket.io 做了一个聊天 demo,消息通信没有问题,在 Angular 数据绑定的地方却栽了跟头:明明 model 已经发生了改变,在视图上就是看不到更新。
后来仔细研究,通过使用 “$scope.$apply()” 解决了这个问题。
之前对 Angular 数据双向绑定只有一个大概的印象,并没有深入地了解,正好趁这个机会好好学习一下数据绑定的过程。
服务端代码:
1 | ; |
客户端代码:
1 |
|
CSS 代码略。
JavaScript 代码:
1 | ; |
socket.io 通过 socket.emit() 发送事件,通过 socket.on() 监听事件。
上面代码似乎没有什么问题,可是运行的时候总是发生视图不更新的情况。
debug 发现 $scope.chatMessage 的值已经发生改变了,按理说 Angular 的 model 与 view 是双向绑定的,model 改变 view 也应该随之更新才对啊,为什么会出现这种情况呢?
$scope.chatMessage 发生变化后,没有强制 $digest 循环,监视 chatMessage 的 $watch 没有执行,而我们自己执行一次 $apply,那么这些 $watch 就会看见这些变化,然后根据需要更新 DOM。
]]>最近在 ITA 写了一个聊天机器人的 Flask 服务,自己写了一些 node 单元测试脚本跑没有问题,但是测试的同学也想覆盖到所有的 case,于是就帮忙写一个 html 页面去测试,然后就遇到了下面的问题:
XMLHttpRequest cannot load http://localhost:8085/predict. No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘null’ is therefore not allowed access.
这个是典型的跨域问题(跨域是指:协议、域名、端口有任何一个不同,都被当做是不同的域),想想之前也了解过跨域的知识,现在借着这个机会总结一下了。关于 GET 请求的跨域,使用 JSONP 是目前最好的解决方案,各大浏览器也基本都支持 JSONP,而 jQuery,AngularJS 等前端框架也都默认添加了对 JSONP 的封装,并且这次遇到的跨域问题是 POST 请求的,于是暂时先不写关于 JSONP 的相关知识。
服务器代码:
1 | from flask import Flask |
页面代码:
1 |
|
– 原谅我用 Angular 做页面 ☹
main.js
1 | angular.module('chatApp', []) |
要想解决跨域,必先理解跨域。那什么是跨域呢?
对于 web 开发来讲,由于浏览器的同源策略,我们需要经常使用一些 hack 的方法去跨域获取资源,直到 W3C 出了一个标准-CORS-“跨域资源共享”(Cross-origin resource sharing),
它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。
CORS 与 JSONP 的使用目的相同,但是比 JSONP 更强大。
JSONP 只支持 GET 请求,CORS 支持所有类型的 HTTP 请求。JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。
CORS 解决方案:
(1) 服务器代码
1 | from flask import Flask, Response, request |
(2) main.js
1 | angular.module('chatApp', []) |
此时再次发送 Ajax call就可以拿到结果了:
注意到服务器端代码发生了一点改动,那就是在Response header中增加了一个参数 “Access-Control-Allow-Origin”,表示接受某域名的请求,“*” 表示允许所有的请求。
也可以使用确定的值,如: “http://api.abc.com”。
于是代码中增加 headers = {“Access-Control-Allow-Origin”: ""}* 后服务器就可以响应所有的请求了。
再看 Web 端的代码,我们在请求头里面添加了 “Content-Type”,为了能向服务端传递数据。这里使用的 “Content-Type” 为 “application/x-www-form-urlencoded” 表示以表单提交的形式传递参数。
为什么要用表单的形式提交POST请求呢?
浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
(1) 请求方法是以下三种方法中的一个:
上文中的请求属于简单请求。
对于简单的跨域请求,浏览器会自动在请求的头信息加上 Origin 字段,表示本次请求来自哪个源(协议 + 域名 + 端口),服务端会获取到这个值,然后判断是否同意这次请求并返回。
// 请求
GET /cors HTTP/1.1
Origin: http://api.abc.com
Host: api.bcd.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0…
如果服务端许可本次请求,就会在返回的头信息多出关于 Access-Control 的信息,比如上述服务器返回的信息:
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是 PUT 或 DELETE,或者 Content-Type 字段的类型是 application/json。
非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为“预检”请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。
“预检”请求用的请求方法是 OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。
项目中使用的 Content-Type 为 application/json,属于非简单请求,将上述程序修改为
(1) main.js:
1 | angular.module('chatApp', []) |
服务器代码:
1 | from flask import Flask, Response, request |
启动后发送请求,发现可以跑通,但是获取不到参数,原因是使用 application/json 的形式发送 request, 参数并没有放在 form 里面,而是放在 request.data 里面了。
request.data 里面为 bytes 类型的数据,通过 request.json 可以获取其 dict 类型。
通过以上方式,完美地解决了复杂请求的跨域问题。
才怪嘞!!!♋
以上解决跨域的方式为 CORS,准确地说,这是一种服务器端的技术。而现实生产环境中,如果一个前端想要用这种方式实现跨域,不知道要跟后端做多少沟通,那有没有纯前端的解决方案呢?
且听下回分解。☛
Pandas 是 Python 做数据分析最重要的模块之一,本文源自Pandas 作者 Wes McKinney 写的 10-minute tour of pandas。
首先安装 Pandas 和相关的两个包 numpy、matplotlib
1 | pip install pandas |
导入 pandas、numpy、matplotlib
1 | import pandas as pd |
Series 是一个序列,使用 Pandas 创建一个整数索引的序列:
1 | 1,3,5,np.nan,6,8]) s = pd.Series([ |
DataFrame 是有多个列的数据表,每个列拥有一个 label,当然,DataFrame 也有索引:
1 | '20170101', periods=6) dates = pd.date_range( |
通过一个对象字典创建 DataFrame, dict 的每个 value 会被转化成一个 Series:
1 | 'A' : 1., df2 = pd.DataFrame({ |
查看每列的格式:
1 | df2.dtypes |
查看某一列的具体值
1 | df2.C |
使用 head() 查看 DataFrame 前几行; tail() 查看后几行:
1 | 3) df.head( |
实际上,DataFrame 内部用 numpy 格式存储数据。你也可以单独查看 index、columns 和 values:
1 | df.index |
使用 describe() 可以帮你做一些数据的概要
1 | df.describe() |
DataFrame 的矩阵转置
1 | df.T |
DataFrame 排序
(1) 使用 sort_index 按照索引排序
ascending 参数默认值为 True
axis = 0 指的是安装行排序,axis = 1 是指安装列排序:
1 | 1, ascending=False) df.sort_index(axis= |
(2) 使用 sort_values 按照值排序
1 | 'B', ascending=False) df.sort_values(by= |
选择单独的列:
1 | 'A'] df[ |
切片,使用[]选择特定的行
1 | 0:3] df[ |
通过 label 选择(dates[0]=Timestamp(‘2017-01-01 00:00:00’, offset=‘D’))
1 | 0]] df.loc[dates[ |
多选,「A:B」 表示从 A 到 B
1 | 'A','B']] df.loc[:,[ |
选择第四行所有元素
1 | 3] df.iloc[ |
选出34行,01列
1 | 3:5,0:2] df.iloc[ |
选择单个元素
1 | 1,1] df.iloc[ |
1 | 0] df[df.A > |
选出大于0 的全部元素,没有填充的值等于 NaN
1 | 0] df[df > |
isin() 函数:是否在集合中
1 | df2 = df.copy() |
按照 index 给 DataFrame 添加新的列:
1 | 1,2,3,4,5,6], index=pd.date_range('20170102', periods=6)) s1 = pd.Series([ |
通过 label 设置
1 | 0],'A'] = 0 df.at[dates[ |
通过下标设置
1 | 0,1] = 0 df.iat[ |
用 numpy 数组设置
1 | 'D'] = np.array([5] * len(df)) df.loc[:, |
使用比较设置
1 | df2 = df.copy() |
继承是面向对象语言中最重要的概念之一,许多 OO 语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于 ECMAScript 中没有方法签名,所以不能实现接口继承,而是通过原型链的方式完成实现继承。
每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针,而所有实例中都包含一个指向原型对象的内部指针。下面是一个实现原型链的基本方法:
1 | function SuperType() { |
上述代码定义了 SuperType 和 SubType 两种类型,每个类型分别有一个属性和一个方法,SubType 通过改写原型对象的方式实现对 SuperType 的继承。原来存在于 SuperType 中的属性和方法,现在也存在于 SubType.prototype 中。在确立了继承关系后,我们给 SubType.prototype 又添加了一个新方法,这个例子中的关系图如下:
在上述代码中,我们修改 SubType 默认的原型为 SuperType 的实例,新原型不仅具有作为一个 SuperType 的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了 SuperType 的原型。最终的结果是这样的:instance 指向了 SubType 的原型,SubType 的原型又指向了 SuperType 的原型。
1 | instance.__proto__ === SubType.prototype // true |
在 JavaScript 中,只要创建了新函数,都会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。默认情况下,所有原型对象都会自动获取一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。比如:
1 | function Person () {} |
通过 constructor,我们可以继续为原型对象添加其他属性和方法。
创建自定义的构造函数之后,其原型对象默认只会取得 constructor 属性,其它属性和方法都是从 Object 继承而来的。
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针([[Prototype]]),指向构造函数的原型对象,该指针在常用的浏览器中被定义为 __proto__
。需要说明的一点是,该连接存在于实例和构造函数的原型对象之间,而不是存在于原型和构造函数之间。
1 | let leo = new Person() |
构造函数,实例,prototype,__proto__
之间的关系可以理解为下图:
注意:__proto__
并非 JS 标准属性,而是浏览器的实现。
从图中可以看出构造函数 Person 和实例 leo 之间并没有直接关系,而是通过 Person.prototype 原型对象进行关联。虽然实例中并不包含属性和方法,但是可以通过调用 leo.sayName
进行调用。在非浏览器环境或者浏览器不支持 __proto__
的环境中,我们可以通过 isPrototypeOf() 方法来确定对象之间是否存在这种关系。
1 | Person.prototype.isPrototypeOf(leo) // true |
ECMAScript5 中增加了 Object.getPrototypeOf() 方法,该方法返回 [[Prototype]] 的值。
1 | Object.getPrototypeOf(leo) === Person.prototype |
每当代码读取某个对象的属性时,都会执行一次搜索:首先判断实例是否具有给定名字的属性,如果没有的话,继续搜索实例的原型对象。
原型对象中的属性对于实例来说是只读的,比如:
1 | function Person () {} |
hasOwnProperty 可以检测一个属性是存在于实例中,还是存在于原型对象中,这个方法继承自 Object 对象;无论属性存在于实例中还是原型中,使用 in 操作符都能得到 true。
1 | function Person () {} |
注:ES5 中 Object.getOwnpropertyDescriptor() 方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用 Object.getOwnpropertyDescriptor()。
1 | Object.getOwnPropertyDescriptor(p1, 'name') |
要取得对象上所有的可枚举的实例属性,可以使用 Object.keys() 方法。
1 | function Person () {} |
可以看出,Object.keys() 方法只枚举实例属性,并不枚举原型对象中的属性,而且 constructor 属性也是不可枚举的。
1 | function Person () {} |
这种写法存在一个问题,就是重设的 constructor 属性的 [[Enumerable]] 特性被设置为 true,默认情况下,原生的 constructor 属性是不可枚举的。所以可以写成如下情况:
1 | function Person () {} |
在修改原型的过程中,我们可以随时为原型添加属性和方法,但是如果重写整个原型对象,那有可能切断构造函数与原型之间的联系。
1 | function Person () {} |
为什么在调用 p1.sayName() 的时候会发生错误呢,因为 p1 指向的原型对象中并不包含 sayName 方法。
其关系可看下图:
重写原型对象后,切断了现有原型与任何之前已经存在的对象实例之间的联系,它们引用的任然是最初的原型。
原型对象省略了为构造函数传递参数这一环节,使得所有实例在默认情况下都取得相同的属性值,而且原型中所有的属性是被全部实例共享的,这种共享对于函数来说非常合适,但是对于属性值,尤其是引用类型的属性值来说,问题就比较严重了。
1 | function Person () {} |
修改实例 p1 的值的过程中,p2 的值也被修改了。这就导致了仅仅使用原型模式创建对象存在很大的问题。具体解决请查看深入学习JavaScript——面向对象。
]]>几乎所有面向对象的语言都有一个标志,那就是类,通过类创建具有相同属性和方法的对象。而 ECMAScript 中没有类的概念,它把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数”。即对象是一组没有特定顺序的值,对象的每个属性或方法都有一个名字,而这个名字都映射到一个值。因此对象的本质是一个散列表。
虽然 Object 构造函数或对象字面量都可以创建单个对象,但是这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量重复的代码。为了解决这个问题,就可以使用工厂模式来创建对象。
工厂模式用函数来封装特定接口创建对象。
1 | function createPerson(name, age, job) { |
工厂模式虽然解决了创建多个相似对象的问题,但没有解决对象识别的问题(即怎样知道一个对象的类型)。
ECMAScript 中的构造函数可以用来创建特定类型的对象,像 Object 和 Array 的原生的构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。代码如下所示:
1 | function Person(name, age, job) { |
构造函数模式与工厂模式有以下不同:
构造函数应该以大写字母开头,使用 new 操作符。new 操作符创建对象经历以下 4 个步骤:
生成的对象 leo 中有一个 constructor 属性,该属性指向 Person,并且可以用 instanceof 做类型检测。
1 | leo.constructor === Person // true |
构造函数的缺点在于每个方法都要在每个实例上重新创建一遍。在前面例子中,leo 和 jack 都有一个名为 sayName 的方法,但是这两个方法不属于同一个对象。
那么我们能不能共享一个 sayName() 方法。如果想要完成这种需求,大可像下面代码一样,通过把函数定义转移到构造函数的外部。
1 | function Person(name, age, job) { |
上面例子中的做法,确实解决了两个函数做同一件事的问题,但是无意中定义了很多全局函数,而这些全局函数中由于包含 “this” 关键字,又只能被某个函数调用。不仅污染了全局作用域,还使得这个自定义的引用类型完全丧失封装性。好在这些问题都可以通过原型模式解决。
JavaScript 中创建的每个函数都有一个 prototype 属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的 所有实例共享的属性和方法。prototype是通过调用构造函数而创建的那个对象实例的对象原型,使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
1 | function Person() {} |
在此,我们将 sayName() 方法和所有的属性直接添加到了 Person 的 prototype 属性中,构造函数变成了空函数,而通过 new 创建出来的对象具有相同的属性和方法。但是与构造函数模式不同对的是,新对象的这些属性和方法是由所有的实例共享的,也就是说
1 | leo1.sayName === leo2.sayName // true |
创建自定义对象最常见的形式就是组合使用构造函数模式和原型模式,构造函数用于定义类的实例属性,而原型模式用于定义对象的共享属性。
1 | function Person(name, age) { |
实例属性都是在构造函数中定义的,而实例共享属性 constructor 和方法 sayName() 则是在原型中定义的。这种构造函数与原型混成的模式,是目前 ECMAScript 中使用最广泛、认同度最高的一种创建自定义对象的方法。
动态原型模式将所有信息封装在了构造函数中,而通过构造函数中初始化原型(仅第一个对象实例化时初始化原型),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
1 | function Person(name, age) { |
Person 是一个构造函数,通过 new Person() 来生成实例对象。每当一个 Person 的对象生成时,Person 内部的代码都会被调用一次。
如果去掉 if 的话,你每 new 一次(即每当一个实例对象生产时),都会重新定义一个新的函数,然后挂到 Person.prototype.sayName 属性上。而实际上,你只需要定义一次就够了,因为所有实例都会共享此属性的。而加上 if 后,只在 new 第一个实例时才会定义 sayName 方法,之后就不会了。
假设除了sayName 方法外,你还定义了很多其他方法,比如 sayBye、cry、smile 等等。此时你只需要把它们都放到对 sayName 判断的 if 块里面就可以了。
1 | if (typeof this.sayName != "function") { |
这样一来,要么它们全都还没有定义(new 第一个实例时),要么已经全都定义了(new 其他实例后),即它们的存在性是一致的,用同一个判断就可以了,而不需要分别对它们进行判断。
使用动图原型模式时,不能使用对象字面量重写原型,如果在已经创建实例的情况下重写原型,会切断现有实例和原型之间的联系。
寄生构造函数的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后返回新创建的对象。
1 | function Person(name, age) { |
在这个例子中,Person 函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后返回这个对象。除了使用 new 操作符并把使用的包装函数叫做构造函数外,这个模式跟工厂模式一模一样。构造函数在不返回值的情况下,默认会返回新的对象实例。
这个模式在特殊的情况下可以用来为对象创建构造函数。假如我们想创建一个具有额外方法的特殊数组,由于不能直接修改 Array 的构造函数,因此可以使用这种模式。
1 | function SpecialArray() { |
关于寄生构造函数模式,有一点需要说明:返回的对象与构造函数或者构造函数的原型属性直接没有关系,所以不能依赖 instanceof 操作符来确定对象类型。
稳妥对象,是指没有公共属性,而且方法也不引用 this 的对象,适合在一些安全环境中(禁用 this 和 new),或者在防止数据被其它应用程序改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但是有两点不同:一是新创建对象的实例方法不引用 this,二是不使用 new 操作符调用构造函数。
1 | function Person(name, age) { |
注意在这种模式创建的对象中,除了使用 sayName 方法之外,没有其他办法访问 name 属性,即使有其他代码给这个对象添加属性或者方法,也不可能有别的办法访问传入到构造函数中的原始数据。
与寄生构造函数类似,稳妥构造函数模式创建的对象与构造函数直接也没有什么关系,所以不能依赖 instanceof 操作符来确定对象类型。
组合使用构造函数模式和原型模式是目前使用最广的方法,如果不希望构造函数和原型相互分离的话,可以使用动态原型模式。
]]>哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
JavaScript 中的对象也是以 Key-Value 的形式访问,那么 JavaScript 的对象是否以 Hash 的结构存储呢?
我们首先来看一下 Hash 表结构。
数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易,Hash 表综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构。
下图是最常见的 拉链法 做出的 Hash 表
左边是一个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。
元素特征转变为数组下标的方法就是散列法。上图运用的方法为 整除法,公式为:
index = value % 16
hash表的工作原理:
在我们创建或者访问对象属性的时候,如果使用 对象.属性名 的方式,属性名只能为字符串类型,而且不能以数字开头:
1 | let obj = {}; |
而使用字面量的形式创建对象,或者用 对象[属性名] 的方法,却没有这样的限制:
1 | let o = {}; |
此时 obj 里面的属性 2 是一个整数吗?
1 | for (let i in obj) { |
由此可见 JavaScript 中对象的 Key 均是 string 类型。
1 | console.log(obj[2] === obj['2']); // true |
可见解释器在访问 object[2] 的时候,先将方括号里面的 2 转换成字符串,然后再访问。
而使用 obj[{name: ‘Leo’}] = ‘object’ 的时候,也是同样的,解释器先调用 Objcet.toString 方法把对象 {name: ‘Leo’} 转换成字符串,然后再访问。
1 | let object = { |
上述的 object[{name: ‘Leo’}] 相当于 object[{name: ‘Leo’}.toString()] 亦相当于 object[‘2’],于是就得到结果 2。
这里也间接证明了 JavaScript 对象中,所有的 key 都是字符串,即使你访问的时候不是字符串的形式,解释器也会先将其转化为字符串。
可是我们知道整数值直接调用 toString 方法是会报错的,因为 JavaScript 解析器会试图将点操作符解析为浮点数字面值的一部分。不过有很多变通方法可以让数字的字面值看起来像对象。
1 | 2.toString() // Uncaught SyntaxError: Invalid or unexpected token |
所以 JavaScript 解释器应该有帮我们做这一部分工作。
在JavaScript高级程序设计(第三版)中,是这么描述属性的:属性在创建时都带有一些特征值,JavaScript引擎通过这些特征值来定义他们的行为。
1 | var person = {}; |
可见 value 的数据类型是结构体。
在 JavaScript 中,我们可以任意给对象添加或者删除属性,由此可以推断,对象不是由数组结构存储;链表虽然能够任意伸缩但是其查询效率低下,因此也排除链表。如果用树作为存储结构,效率较高的可能就是平衡树了。平衡树的查询效率还可以接受,但是当删除属性的时候,平衡树在调整的时候代价相比于 hash 表要大很多。于是 Hash 成为最好的选择。
假如有这么一段代码:
1 | function Person(id, name, age) { |
JavaScript 内存分析图如下:
变量 num、bol、str 为基本数据类型,它们的值直接存放在栈中。obj、person、arr 为复合数据类型,他们的引用变量存储在栈中,指向于存储在堆中的实际对象。
在 JavaScript 中变量分为基本类型和引用类型(对象类型),分别对应着两种不同的存储方式–栈存储和堆存储。
基本类型一旦初始化则内存大小固定,访问变量就是访问变量的内存上实际的数据,称之为按值访问。而对象类型内存大小不固定,无法在栈中维护,所以 JavaScript 就把对象类型的变量放到堆中,让解释器为其按需分配内存,而通过对象的引用指针对其进行访问,因为对象在堆中的内存地址大小是固定的,因此可以将内存地址保存在栈内存的引用中。这种方式称之为按引用访问。
在 JavaScript 中对象是以 Hash 结构存储的,用 <Key, Value> 键值对表示对象的属性,Key 的数据类型为字符串,Value 的数据类型是结构体,即对象是以 <String, Object> 类型的 HashMap 结构存储的。
]]>Spark RDD 支持2种类型的操作: transformations 和 actions。transformations: 从已经存在的数据集中创建一个新的数据集,如 map。actions: 数据集上进行计算之后返回一个值,如 reduce。
在 Spark 中,所有的 transformations 都是 lazy 的,它们不会马上计算它们的结果,而是仅仅记录转换操作是应用到哪些基础数据集上的,只有当 actions 要返回结果的时候计算才会发生。
默认情况下,每一个转换过的 RDD 会在每次执行 actions 的时候重新计算一次。但是可以使用 persist (或 cache)方法持久化一个 RDD 到内存中,这样Spark 会在集群上保存相关的元素,下次查询的时候会变得更快,也可以持久化 RDD 到磁盘,或在多个节点间复制。
在 Spark-shell 中运行如下脚本
1 | scala> val lines = sc.textFile("test.txt") |
第一步: 定义外部文件 RDD,lines 指向 test.txt 文件, 这个文件即没有加载到内存也没有做其他的操作,所以即使文件不存在也不会报错。
第二步: 定义 lineLengths,它是 map 转换(transformation)的结果。同样,lineLengths 由于 lazy 模式也没有立即计算。
第三步: reduce 是一个 action, 所以真正执行读文件和 map 计算是在这一步发生的。Spark 将计算分成多个 task,并且让它们运行在多台机器上。每台机器都运行自己的 map 部分和本地 reduce 部分,最后将结果返回给驱动程序。
如果我们想要再次使用 lineLengths,我们可以使用 persist 或者 cache 将 lineLengths 保存到内存中。
1 | scala> lineLengths.persist() |
Transformations 是 RDD 的基本转换操作,主要方法有: map, filter, flatMap, mapPartitions, mapPartitionsWithIndex, sample, union, intersection, distinct, groupByKey, reduceByKey, aggregateByKey, sortByKey, join, cogroup, cartesian, pipe, coalesce, repartition。
filter 返回一个新的数据集,从源数据中选出 func 返回 true 的元素。
1 | scala> val a = sc.parallelize(1 to 9) |
与 map 类似,区别是原 RDD 中的元素经 map 处理后只能生成一个元素,而经 flatmap 处理后可生成多个元素来构建新 RDD, 所以 func 必须返回一个 Seq,而不是单个 item。
举例:对原RDD中的每个元素x产生y个元素(从1到y,y为元素x的值)
1 | scala> val a = sc.parallelize(1 to 4, 2) |
mapPartitions 是 map 的一个变种。map 的输入函数是应用于 RDD 中每个元素,而 mapPartitions 的输入函数是应用于每个分区,也就是把每个分区中的内容作为整体来处理的。
它的函数定义为:
1 | def mapPartitions[U](f: (Iterator[T]) => Iterator[U], preservesPartitioning: Boolean = false)(implicit arg0: ClassTag[U]): RDD[U] |
f 即为输入函数,它处理每个分区里面的内容。每个分区中的内容将以 Iterator[T] 传递给输入函数 f,f 的输出结果是 Iterator[U]。最终的 RDD 由所有分区经过输入函数处理后的结果合并起来的。
1 | scala> val rdd = sc.makeRDD(1 to 5, 2) |
上述例子中 rdd2 将 rdd 每个分区中的数值累加。
函数定义
1 | def mapPartitionsWithIndex[U](f: (Int, Iterator[T]) => Iterator[U], preservesPartitioning: Boolean = false)(implicit arg0: ClassTag[U]): RDD[U] |
mapPartitionsWithIndex 的作用与 mapPartitions 相同,不过提供了两个参数,第一个参数为分区的索引。
1 | scala> val rdd = sc.makeRDD(1 to 5, 2) |
函数定义:
1 | def union(other: RDD[T]): RDD[T] |
该函数比较简单,就是将两个 RDD 进行合并,不去重。
1 | scala> var rdd1 = sc.makeRDD(1 to 2,1) |
函数定义:
1 | def intersection(other: RDD[T]): RDD[T] |
该函数返回两个 RDD 的交集,并且去重。
参数numPartitions指定返回的RDD的分区数。
参数partitioner用于指定分区函数
1 | scala> var rdd1 = sc.makeRDD(1 to 2,1) |
返回一个新的 RDD,里面包含源 RDD 中所有的(distinct)元素。
RDD 全称 Resilient Distributed Datasets,是 Spark 中的抽象数据结构类型,任何数据在Spark中都被表示为RDD。 Spark 建立在统一抽象的RDD之上,使得它可以以基本一致的方式应对不同的大数据处理场景,包括MapReduce,Streaming,SQL,Machine Learning 等。
简单的理解就是 RDD 就是一个数据结构,不过这个数据结构中的数据是分布式存储的,Spark 中封装了对 RDD 的各种操作,可以让用户显式地将数据存储到磁盘和内存中,并能控制数据的分区。
RDD 是 Spark 的核心,也是整个 Spark 的架构基础。它的特性可以总结如下:
本文中的例子全部基于 Spark-shell,需要的请自行安装。
创建 RDD 主要有两种方式,一种是使用 SparkContext 的 parallelize 方法创建并行集合,还有一种是通过外部外部数据集的方法创建,比如本地文件系统,HDFS,HBase,Cassandra等。
使用 parallelize 方法从普通数组中创建 RDD:
1 | val a = sc.parallelize(1 to 9, 3) |
parallelize 方法接受两个参数,第一个是数据集合,第二个是切片的个数,表示将数据存放在几个分区中。
一旦创建完成,这个分布式数据集(a)就可以被并行操作。例如,我们可以调用 a.reduce((m, n) => m + n) 将这个数组中的元素相加。 更多的操作请见 Spark RDD 操作。
文本文件 RDDs 可以使用 SparkContext 的 textFile 方法创建。 在这个方法里传入文件的 URI (机器上的本地路径或 hdfs://,s3n:// 等),然后它会将文件读取成一个行集合。
读取文件 test.txt 来创建RDD,文件中的每一行就是RDD中的一个元素。
1 | val b = sc.textFile("test.txt") |
一旦创建完成,(b) 就能做数据集操作。例如,我们可以用下面的方式使用 map 和 reduce 操作将所有行的长度相加: b.map(s => s.length).reduce((m, n) => m + n)
1 | b.collect |
如果使用本地文件系统路径,文件必须能在 worker 节点上用相同的路径访问到。要么复制文件到所有的 worker 节点,要么使用网络的方式共享文件系统。
所有 Spark 的基于文件的方法,包括 textFile,能很好地支持文件目录,压缩过的文件和通配符。例如,你可以使用 textFile("/文件目录"),textFile("/文件*.txt") 和 textFile("/文件目录/*.gz")。
textFile 方法也可以选择第二个可选参数来控制切片(slices)的数目。默认情况下,Spark 为每一个文件块(HDFS 默认文件块大小是 64M)创建一个切片(slice)。但是你也可以通过一个更大的值来设置一个更高的切片数目。注意,你不能设置一个小于文件块数目的切片值。
SparkContext.wholeTextFiles 让你读取一个包含多个小文本文件的文件目录并且返回每一个(filename, content)对。与 textFile 的差异是:它记录的是每个文件中的每一行。
对于 SequenceFiles,可以使用 SparkContext 的 sequenceFile[K, V] 方法创建,K 和 V 分别对应的是 key 和 values 的类型。像 IntWritable 与 Text 一样,它们必须是 Hadoop 的 Writable 接口的子类。另外,对于几种通用的 Writables,Spark 允许你指定原生类型来替代。例如: sequenceFile[Int, String] 将会自动读取 IntWritables 和 Text。
对于其他的 Hadoop InputFormats,你可以使用 SparkContext.hadoopRDD 方法,它可以指定任意的 JobConf,输入格式(InputFormat),key 类型,values 类型。你可以跟设置 Hadoop job 一样的方法设置输入源。你还可以在新的 MapReduce 接口(org.apache.hadoop.mapreduce)基础上使用 SparkContext.newAPIHadoopRDD(译者注:老的接口是 SparkContext.newHadoopRDD)。
RDD.saveAsObjectFile 和 SparkContext.objectFile 支持保存一个RDD,保存格式是一个简单的 Java
对象序列化格式。这是一种效率不高的专有格式,如 Avro,它提供了简单的方法来保存任何一个 RDD。
JavaScript的世界中「一切皆是对象」,而所有对象的起源就是 Object 对象。
神說:「要有光」。就有了光。
JavaScript中的对象其实是一组数据和功能的集合。我们通过执行 new 操作符 + 对象类型的名称来创建对象。
创建 Object 类型的实例并为其添加属性和方法就可以创建自定义对象,Object既是一个对象,也是自身的构造函数。
1 | let o = new Object; //如果不给构造函数传递参数可以省略圆括号,但不推荐这么写 |
仅仅创建 Object 实例并没有什么用处,但关键是理解一个重要的思想,即在JavaScript中,Object 类型是它所有实例的基础,换句话说,Object类型所具有的任何属性和方法同样存在于更具体的对象中。
Object 对象一共有三个属性: _proto_, constructor, prototype。
1 | function Rectangle() { |
1 | let proto = { y: 2 }; |
1 | let obj = {}; |
所有对象都会从它的原型上继承一个 constructor 属性, constructor 属性是保存当前对象的构造函数。
1 | let o = new Object; // 或者 o = {} |
Object.prototype 属性表示对象 Object 的原型对象,由于所有的对象都是基于 Object,所以 所有的对象都继承了Object.prototype的属性和方法,除非这些属性和方法被其他原型链更里层的改动所覆盖。
返回一个布尔值 ,表示某个对象是否含有指定的属性,而且此属性非原型链继承的。
1 | let o = new Object(); |
返回一个布尔值,表示指定的对象是否在本对象的原型链中。
1 | function Rectangle() { |
判断指定属性是否可枚举。
1 | object.propertyIsEnumerable(proName) |
如果 proName 存在于 object 中,且可以使用 for 循环对其进行枚举,则 propertyIsEnumerable 方法返回 true。如果 object 不具有所指定名称的属性或者所指定的属性是不可枚举的,则 propertyIsEnumerable 方法将返回 false。
1 | let a = new Array("apple", "banana", "cactus"); |
返回对象的字符串表示。
1 | let o = {}; |
上面代码调用空对象的 toString 方法,结果返回一个字符串 “[object Object]”,其中第二个Object表示该值的构造函数,
实例对象可能会自定义 toString 方法,覆盖掉 Object.prototype.toString 方法。通过函数的 call 方法,可以在任意值上调用 Object.prototype.toString 方法,帮助我们判断这个值的类型。
1 | Object.prototype.toString.call(0) // "[object Number]" |
返回指定对象的原始值。valueOf() 方法的作用是返回一个对象的“值”,默认情况下返回对象本身。
valueOf方法的主要用途是,JavaScript自动类型转换时会默认调用这个方法。
1 | let o = new Object(); |
函数 | 描述 |
---|---|
Object.assign(target, …sources) | 将来自一个或多个源对象中的值复制到一个目标对象。 |
Object.create(prototype, descriptors) | 创建具有指定原型并可选择包含指定属性的对象。 |
Object.defineProperties(obj, props) | 将一个或多个属性添加到对象,和/或修改现有属性的特性。 |
Object.defineProperty(obj, prop, descriptor) | 将属性添加到对象,或修改现有属性的特性。 |
Object.freeze(obj) | 防止修改现有属性的特性和值,并防止添加新属性。 |
Object.getOwnPropertyDescriptor(obj, prop) | 返回数据属性或访问器属性的定义。 |
Object.getOwnPropertyNames(obj) | 返回对象属性及方法的名称。 |
Object.getOwnPropertySymbols(obj) | 返回对象的符号属性。 |
Object.getPrototypeOf(obj) | 返回对象的原型。 |
Object.is(value1, value2) | 返回一个值,该值指示两个值是否相同。 |
Object.isExtensible(obj) | 返回指示是否可将新属性添加到对象的值。 |
Object.isFrozen(obj) | 如果无法在对象中修改现有属性的特性和值,并且无法将新属性添加到对象,则返回 true。 |
Object.seal(obj) | 防止修改现有属性的特性,并防止添加新属性。 |
Object.isSealed(obj) | 如果无法在对象中修改现有属性特性,并且无法将新属性添加到对象,则返回 true。 |
Object.keys(obj) | 返回对象的 可枚举属性和方法的名称。 |
Object.preventExtensions(obj) | 防止向对象添加新属性。 |
Object.setPrototypeOf(obj, prototype) | 设置对象的原型。 |
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
如果存在分配错误,此函数将引发 TypeError,这将终止复制操作。如果目标属性不可写,则将引发 TypeError。
1 | let first = { name: "Leo" }; |
创建一个具有指定原型且包含指定属性的对象。
1 | let newObj = Object.create(null, { |
使用Object.create实现类式继承
1 | function Shape() { |
返回对象可枚举的属性名组成的数组。
1 | let a = ["Hello", "World"]; |
返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性)组成的数组。
1 | let arr = ["a", "b", "c"]; |
该特性属于 ECMAScript 2015(ES6)规范。
Object.getOwnPropertySymbols() 方法会返回一个数组,该数组包含了指定对象自身的(非继承的)所有 symbol 属性键。
1 | let obj = {}; |
ES5中提供了一系列限制对象被修改的方法,用来防止被某些对象被无意间修改导致的错误。每种限制类型包含一个判断方法和一个设置方法。
Object.preventExtensions() 用来限制对象的扩展,设置之后,对象将无法添加新属性。
Object.seal() 可以密封一个对象并返回被密封的对象。
密封对象无法添加或删除已有属性,也无法修改属性的enumerable,writable,configurable,但是可以修改属性值。
通过 Object.isSealed() 判断一个对象是否密封。
Object.freeze() 方法用来冻结一个对象,被冻结的对象将无法添加,修改,删除属性值,也无法修改属性的特性值,即这个对象无法被修改。被冻结的对象无法删除自身的属性,但是通过其原型对象还是可以新增属性的。
通过 Object.isFrozen() 可以用来判断一个对象是否被冻结了。
Object.defineProperties、Object.defineProperty、Object.freeze、Object.getOwnPropertyDescriptor 的用法请参考使用Object.defineProperty为对象定义属性。
Object 对象虽然平时我们很少直接用到,但是很多对象的属性和方法都是由 Object 继承而来的,因此非常具有学习意义。
这篇 Blog 虽然都是 API 级别的学习,可是很多东西都是欠下的技术债,就当补课了。
“函数挂载父环境的时机,如果是定义时就是闭包,如果是执行时就不是闭包。”——听一位大神同事讲的。
“闭包是指那些能够访问独立(自由)变量的函数 (变量在本地使用,但定义在一个封闭的作用域中)。换句话说,这些函数可以“记忆”它被创建时候的环境。”——MDN
刚学JavaScript的时候看了这些定义后我就哭了,要想理解闭包还是要看例子。
1 | function foo() { |
函数 foo 返回一个内部函数 inner,所以“let fun = foo()”的结果应该是“fun = inner” 也就是 “fun = function (){console.log(a++)};”
那么当执行 fun() 的时候 a=?,显然在 fun 的外部环境中是没有 a 的定义的,于是就向 inner 函数定义时候的父环境中找 a,果然在 foo 函数中找到了。这样就可以理解上面给出的第一个闭包的定义了:一个函数在执行的时候,如果能拿到定义时候父环境的值,这样就是闭包,反之则不是闭包。
那闭包究竟是一个什么东西呢?我们可以把闭包理解成 “函数 + 函数创建时的环境”的组合,比如上面的 inner 函数 + 变量a 就是一个闭包。
通过使用闭包,我们可以做很多事情。
匿名自执行函数有两个作用:
比如我上面栗子中创建的函数 foo 会自动绑定到全局变量中
1 | window.foo()(); //1 |
这样我们每次创建一个函数都必须要使用 const/let/var 去声明一个变量等于函数,不然全局对象的属性会越来越多,从而影响访问速度(因为变量的取值是需要从原型链上遍历的),而且可能会导致变量冲突。
结果缓存是闭包能显著提高程序效率的一个用途。假如有一个处理过程很耗时的函数对象,我们可以将每次处理的结果缓存起来,当再次调用这个函数的时候,就先从缓存中查找。
1 | const cacheSearch = (function() { |
1 | const foo = (function() { |
1 | function Person() { |
这里的 Person 是一个函数,由于 JavaScript “没有” class 的概念(有 class 关键字)
,所以在 JavaScript 中,new 后面跟的是构造函数。
上面的代码里面定义了 Student 继承自 Person,所以拥有 getName 方法,然后通过prototype添加自己的方法。
实现每隔一秒输出一个递增的数字(0 到 5)
1 | for (var i = 0; i < 5; i++) { |
上面这种写法想必大家都知道结果是什么,那就是每隔一秒输出一个5
使用闭包实现输出数字为 0 到 5
1 | for (var i = 0; i < 5; i++) { |
还有一种使用闭包的方式是使用 Array 的 forEach 循环,forEach 里的执行函数也行成了一个闭包
1 | [0, 1, 2, 3, 4].forEach((i) => { |
当然使用 ES6 的 let 才是最好的选择
1 | for (var i = 0; i < 5; i++) { |
闭包三个特性:
闭包的优点:
闭包的缺点:
首先打开网易云音乐首页找到你想要的音药,点击 「生成外链播放器」
选择合适的尺寸后将生成的 iframe 插件或者 flash 插件代码复制到 markdown 中即可。
优酷暂时没有 https,这个比较讨厌。
B 站的视频,找到想要分享的视频,点击下方的分享即可。
]]>目前前端开发中比较流行的两个框架: Angular 和 Vue 都采用了数据双向绑定的技术。
Angular1 中数据双向绑定是通过「脏检测」的方式实现,每当数据发生变更,对所有的数据和视图的绑定关系进行一次检测,识别是否有数据发生了变化以及这个变化是否会影响其它数据的变化,然后将变更的数据发送到视图,更新页面展示。
Vue 数据双向绑定的原理与Angular有所不同,网上人称「数据劫持」。Vue使用的是 ES5 提供的 Object.defineProperty() 结合发布者-订阅者模式,通过Object.defineProperty() 来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。
我们来看下一般使用方法:
1 | let Leo = Object.defineProperty({}, 'name', { |
其基本语法规则如下:
1 | Object.defineProperty(obj, prop, descriptor) |
所以 Object.defineProperty(obj, ‘name’, { value: ‘Leo’}) 相当于 obj.name = ‘Leo’ 或者 **obj[‘name’] = ‘Leo’**喽。
那我们直接使用「对象.属性」就好了,为什么要用 Object.defineProperty 这么复杂的方法呢?
如果你想定义一个对象的属性为只读怎么办?
「对象.属性」能做到吗?显然不能!Object.defineProperty 却可以做到。因此 Object.defineProperty 方法是对属性更加精确的定义。
我们可以在descriptor参数中设置如下值,来实现对属性的控制:
1 | let Leo = Object.defineProperty({}, 'name', { |
1 | let Leo = Object.defineProperty({}, 'name', { |
configurable 参数不仅负责属性的删除,也与属性修改有关。
1 | let Leo = Object.defineProperty({}, 'name', { |
假如一个属性被定义成 configurable 为 false,则这个属性既不能修改值(value),又不能修改属性的属性(configurable,writable,enumerable);如果 configurable 为 true 就可以放心修改了。
1 | let Leo = Object.defineProperty({}, 'name', { |
属性特性 enumerable 定义了对象的属性是否可以在 for…in 循环和 Object.keys() 中被枚举。
1 | let o = Object.defineProperty({}, "a", {value: 1, enumerable: true}); |
1 | let name = 'Leo'; |
在对Leo.name进行赋值的时候,其实是调用了name的set方法;而使用Leo.name的时候则调用了get方法。这就是Vue数据双向绑定的原理:每当数据发生改变,其实是调用了set方法,set方法里面发布数据变动的消息给订阅者,触发相应的监听回调。
注意: 如果 get 方法与 value 同时出现,会报错。
1 | let name = 'Leo'; |
Object.getOwnPropertyDescriptor() 返回指定对象上一个自有属性对应的属性描述符。
1 | let Leo = Object.defineProperty({}, 'name', { |
Object.defineProperties 与 Object.defineProperty 作用相同,不过可以同时将多个属性添加/修改到对象。
Object.freeze() 方法可以冻结一个对象,冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。也就是说,这个对象永远是不可变的。该方法返回被冻结的对象。
了解了 Object.defineProperty 的用法,接下来就是写一个自己的 Vue.js 了。敬请期待。
]]>Travis CI 是一个持续集成的平台,我们可以使用其自动构建部署的功能帮我们简化 Hexo 博客的部署流程。
因为懒。
Hexo 部署 Blog 到 GitPage 通常需要三部曲:
1 | hexo clean |
很简单吧,但是如果是一个新的环境,你需要安装一大堆工具和依赖,比如要装 Node,要装 Hexo,还有 package.json 里面的各种依赖,虽然 Npm 提供了强大的包管理功能,但是有时候就是不方便。
使用 Travis,你只需要本地有一个 git 就可以了。
每当你 Push 一个 commit 到 Github 时,Travis CI 会检测到你的提交,并根据配置文件自动运行一些命令,通常这些命令用于测试,构建等等。
那么在我们的需求下,就可以用它运行一些 hexo deploy -g 之类的命令用来自动生成、部署我们的网站。
使用 Travis 构建 Hexo 只需要三步:
首先使用 GitHub 账号登录Travis CI,登录后会进入如下页面
点击「My Repositories」后面的 +,添加要自动构建的仓库
这里会显示你 GitHub 下所有的项目,选中博客仓库,我的博客在GitHub上的仓库名字就叫做 Blog。然后点击仓库名进入仓库配置页面。
选择 Settings,配置选择如下:
这个时候,我们已经开启要构建的仓库,但是如何将构建完成后的文件推送到 Github 上呢?
Github 支持一种特殊的 URL 来执行 push/pull 等等操作,而不需要输入用户名密码。但这需要事先在 Github 上创建一个 token。
首先去 GitHub Settings 页面选择 Personal access tokens,如果你已经登录了,点击链接进去即可。
选择 Generate new token,配置如下:
点击绿色确认按钮,copy 刚刚生成的 token。回到 Travis Settings 页面,将复制的 token 加入到环境变量,并命名为 GitHub_token。
上述步骤完成后,只需要在你 Blog 源代码的根目录下增加一个 .travis.yml 文件,
我的文件内容如下:
1 | language: node_js |
将上面的 name 和 email 还有 GH_REF 修改成你自己的。
这里用 Linux 环境变量的引用方式将 GH_REF 和 GitHub_token 其引入 git push 的 url,因此 push 方法就能通过 GitHub OAuth 授权,完成自动 push 的功能。
此时就万事俱备了。
使用 Hexo 创建新的 Blog 文件,然后 push 到 GitHub 上。
1 | hexo new test.md |
然后回到 Travis 主页面,发现部署已经开始了
在下面的 log 中可以看到部署的详细情况。
包括 nvm install,npm install,hexo g 等命令都在这里执行。
有了自动部署的功能,从此以后就可以将关注点集中在博客内容上,换了平台和环境也没有任何影响。
]]>Node.js 的单线程模型给了它无数的赞美,也带给它无数的诟病。单线程模型,让开发者远离了线程调度的复杂性,使用事件驱动也能开发出一个高并发的服务器;同样也是因为单线程,让CPU密集型计算应用完全不适用。
Node.js 中内建了一个 child_process模块,可以在程序中创建子进程,从而实现多核并行计算。
child_process 是 Node.js 中一个非常重要的模块,主要功能有:
使用 child_process 模块创建进程一共有六种方法(Node.js v7.1.0)
以异步函数中 spawn 是最基本的创建子进程的函数,其他三个异步函数都是对 spawn 不同程度的封装。spawn 只能运行指定的程序,参数需要在列表中给出,而 exec 可以直接运行复杂的命令。
spawn从定义来看,有3个参数。
1 | child_process.spawn(command[, args][, options]) |
- cwd [String] Current working directory of the child process
spawn 方法创建一个子进程来执行特定命令,它没有回调函数,只能通过监听事件,来获取运行结果。属于异步执行,适用于子进程长时间运行的情况。
1 | let child_process = require('child_process'); |
spawn 方法通过 stream 的方式发数据传给主进程,从而实现了多进程之间的数据交换。
exec 方法的定义如下:
1 | child_process.exec(command[, options][, callback]) |
exec 方法是对 spawn 方法的封装,增加了 shell/bash 命令解析和回调函数,更加符合 JavaScript 的函数调用习惯。
command参数是一个命令字符串
1 | let exec = require('child_process').exec; |
exec 方法第二个参数是回调函数,该函数接受三个参数,分别是发生的错误、标准输出的显示结果、标准错误的显示结果。
由于标准输出和标准错误都是流对象(stream),可以监听 data 事件,因此上面的代码也可以写成下面这样。
1 | let exec = require('child_process').exec; |
exec 方法会直接调用 bash(/bin/sh程序) 来解释命令,如果用户输入恶意代码,将会带来安全风险。因此,在有用户输入的情况下,最好不使用 exec 方法,而是使用 execFile 方法。
execFile的定义如下:
1 | child_process.execFile(file[, args][, options][, callback]) |
execFile 命令有四个参数,file 和callbakc 为必传参数,options、args 为可选参数:
execFile 从可执行程序启动子进程。与 exec 相比,execFile 不启动独立的 bash/shell,因此更加轻量级,也更加安全。 execFile 也可以用于执行命令。
1 | let childProcess = require('child_process'); |
那么,什么时候使用 exec,什么时候使用 execFile 呢?
如果命令参数是由用户来输入的,对于 exec 函数来说是有安全性风险的,因为 Shell 会运行多行命令,比如 ’ls -l .;pwd,如逗号分隔,之后的命令也会被系统运行。但使用 exeFile 命令时,命令和参数分来,防止了参数注入的安全风险。
fork 函数,用于在子进程中运行的模块,如 fork(’./son.js’) 相当于 spawn(‘node’, [’./son.js’]) 。与 spawn 方法不同的是,fork 会在父进程与子进程之间,建立一个通信管道,用于进程之间的通信。
假设有一个主进程文件 mian.js:
1 | let childProcess = require('child_process'); |
有一个子进程文件 son.js:
1 | process.on('message', (m) => { |
运行程序:
1 | $ node test.js |
通过 main.js 启动子进程 son.js,通过 process 在两个进程之间传递数据。
使用 child_process.fork() 生成新进程之后,就可以用 son.send(message, [sendHandle]) 向新进程发送消息,新进程中通过监听message事件,来获取消息,这就是主线程与子线程之间的通信方式。
在Windows上执行一个 .bat 或者 .cmd 文件的方式略有不同。
假如有一个bat文件 my.bat
1 | const spawn = require('child_process').spawn; |
1 | const exec = require('child_process').exec; |
如果文件名中有空格:
1 | const bat = spawn('"my script.cmd"', ['a', 'b'], { shell:true }); |
假设给你n个红色的水壶和n个蓝色的水壶。它们的形状和尺寸都各不相同。所有的红色水壶盛水量都各不相同,蓝色水壶也是如此。但对于每一个红色水壶来说,都有一个蓝色水壶盛水量和其相同;反之亦然。
你的任务是配对出全部盛水量相同的红色水壶和蓝色水壶。为此,可以执行的操作为,挑出一对水壶,一只红色一只蓝色,将红色水壶灌满水,将红色水壶的水倒入蓝色水壶中,看其是否恰好灌满来判断,这个红色水壶的盛水量大于、小于或等于蓝色水壶。假设这样的比较需要花费一个单位时间。
请找出一种算法,它能够用最少的比较次数来确定所有水壶的配对。
注意:不可直接比较两个红色或者两个蓝色水壶,一次比较必须取一只红色一只蓝色。
1.首先在集合中选取一个元素作为 「基准」 pivot
2.将集合中所有元素与「基准」元素进行对比,所有小于「基准」的元素,都移到「基准」的左边;所有大于「基准」的元素,都移到「基准」的右边。
3.对「基准」元素左右两边的集合,分别进行上述两步,直到所有的子集只剩下一个元素。
代码描述:
1 | const quickSort = arr=> { |
1.依次从红色水壶中选取一个水壶与蓝色水壶集合对比,对比过程如下:
2.红色水壶与每一个蓝色水壶对比,盛水量大于红色水壶的蓝水壶放在右边,小于的放在左边,水量相等的为当前集合的 「基准」 元素。
3.如果当前集合中已有 「基准」 元素,则拿红色水壶与「基准」元素对比: 红色水壶大于基准元素,则选取基准元素右边的集合重复第二步; 如果红色水壶小于基准元素,则选取基准元素左边边的集合重复第二步。
现在有红色水壶容量为: [3, 5, 1, 4, 8, 2, 6]
蓝色水壶: [6, 2, 3, 1, 8, 5, 4]
第一步,选取红色水壶中第一个水壶 3 跟蓝色水壶依次对比,大于 3 的放右边,小于 3 的放左边,等于 3 的水壶为当前集合的 「基准」 元素。
1 | [2, 1, ③, 6, 8, 5, 4] |
然后选取红色水壶中的第二个水壶 5 与 「基准」 元素对比,5 > 3, 因此使用第一步的方法,拿 5 与 「基准」 元素右边的元素依次对比。
1 | [2, 1, ③, 4, ⑤, 6, 8] |
红色第三个水壶为 1, 拿 1 与第一个 「基准」 元素比较, 1 < 3, 因此使用第一步的方法, 拿 1 与 「基准」 元素左边的元素依次对比。
1 | [①, 2, ③, 4, ⑤, 6, 8] |
红色第四个水壶为 4, 拿 4 与第一个 「基准」 元素比较, 4 > 3, 因此使用第一步的方法, 拿 4 与 「基准」 元素右边的元素依次对比。
右边元素集合中又有 「基准」 元素 5 ,因此先与 「基准」 元素对比, 4 < 5, 所以拿 4 与 「基准」 元素左边的元素依次对比。
1 | [①, 2, ③, ④, ⑤, 6, 8] |
后面的顺序为
1 | [①, 2, ③, ④, ⑤, 6, ⑧] |
代码描述:
1 | ; |
测试:
1 | let arrRed = [3, 5, 1, 4, 8, 2, 6]; |
这个算法有点类似于二叉树的思想,将红色水壶与蓝色水壶依次对比的时候,构建蓝色水壶二叉树,每个二叉树的根结点为红色水壶。平均时间复杂度为O(nlgn)。
]]>Cookie,指某些网站为了辨别用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密)。
html5 中的 Web Storage 包括了两种存储方式:sessionStorage和localStorage。
sessionStorage 用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。因此 sessionStorage 不是一种持久化的本地存储,仅仅是会话级别的存储。
而 localStorage 用于持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。浏览器中同一个域下的窗口可以共享 localStorage 数据。
特性 | Chrome | Firefox (Gecko) | Internet Explorer | Opera | Safari (WebKit) |
---|---|---|---|---|---|
localStorage | 4 | 3.5 | 8 | 10.50 | 4 |
sessionStorage | 5 | 2 | 8 | 10.50 | 4 |
Cookie 一般由服务器生成,可设置失效时间。如果在浏览器端生成 Cookie,默认是关闭浏览器后失效。Http 通信的时候 Cookie 的信息会保存的 Http 头中。
localStorage 和 sessionStorage 仅在客户端(即浏览器)中保存,不参与和服务器的通信。
因为每个 HTTP 请求都会带着 Cookie 的信息,所以 Cookie 应当尽可能精简,比较常用的一个应用场景就是判断用户是否登录。针对登录过的用户,服务器端会在他登录时往 Cookie 中插入一段加密过的唯一辨识单一用户的辨识码,下次只要读取这个值就可以判断当前用户是否登录啦。
localStorage 主要存储一些比较多的本地数据,如 HTML5 小游戏里面生成的数据。
如果遇到一些内容特别多的表单,为了优化用户体验,我们可能要把表单页面拆分成多个子页面,然后按步骤引导用户填写。这时候 sessionStorage 的作用就发挥出来了。
需要注意的是,不是什么数据都适合放在 Cookie、localStorage 和 sessionStorage 中的。使用它们的时候,需要时刻注意是否有代码存在 XSS 注入的风险。因为只要打开控制台,你就随意修改它们的值,所以千万不要用它们存储你系统中的敏感数据。
]]>本文介绍了一些平时用到的Python书写技巧。之后会不断更新。
1 | x = 6 |
1 | print("Hello") if True else "World" #Hello |
1 | a = [1, 2] |
1 | print(5.0//2) #2 地板除 |
1 | x = 2 |
1 | names = ('Jack','Leo','Sony') |
已知一个列表,我们可以筛选出偶数列表方法:
1 | numbers = [1,2,3,4,5,6] |
和列表推导类似,字典可以做同样的工作:
1 | names = ['Jack','Leo','Sony'] |
1 | items = [0]*3 |
1 | names = ["Leo", "Jack", "Lucy"] |
1 | data = {'user': 1, 'name': 'Max', 'age': 4} |
1 | x = [1,2,3,4,5,6] |
有一个简单的编程练习叫FizzBuzz,问题引用如下:
写一个程序,打印数字1到100,3的倍数打印“Fizz”来替换这个数,5的倍数打印“Buzz”,对于既是3的倍数又是5的倍数的数字打印“FizzBuzz”。
这里就是一个简短的,有意思的方法解决这个问题:
1 | for x in range(101):print("fizz"[x%3*4::]+"buzz"[x%5*4::]or x) |
除了python内置的数据类型外,在collection模块同样还包括一些特别的用例,在有些场合Counter非常实用。
1 | from collections import Counter |
和collections库一样,还有一个库叫itertools,对某些问题真能高效地解决。其中一个用例是查找所有组合,他能告诉你在一个组中元素的所有不同的组合方式
1 | from itertools import combinations |
在Python中,True和False是全局变量,因此:
1 | False = True |
前几天deploy博客的时候,发现打开blog页面是空的,只有head部分显示出来了。打开控制台排查问题,发现hexo主题里面有几个外部ajax call失败,导致整个页面都没有渲染出来,这是一件恼火的事情。
于是果断换主题,其实对之前的主题还是很满意的: 简洁,渲染速度也很快,功能虽然不多,但是基本满足我的需求。
这次选择的主题是腾讯的工程师Litten制作的 「yilia」
「yilia」 同样是我喜欢的简洁样式,作者甚至移除了搜索框。而且对于移动端的优化也做得很不错。
之前Blog里面的图片一直都选择 「yotuku」 生成在线图片,然后在markdown里面引用,如果图片大小或者位置不合适的话,会在md里面手写一段html,这样做很省事。
今天早上看自己的Blog发现有几张图片没有加载出来,以为是新主题渲染的问题,重新deploy以后发现还是没有。看来不是主题的锅。
使用控制台发现
原来这几张图片都没有拿到,已经在官网留言,希望能够解决。
不过使用免费云服务存储自己Blog的图片确实不太安全,像这样丢失图片的行为可能会导致几张图片加载不出来,但是如果以后云服务提供商挂掉了(这里不是诅咒yutuku不好,希望这样良心企业越来越好),那这些图片岂不就再也找不到了。
还是老老实实把图片放到Blog路径下,用相对地址引用吧。
]]>ECMAScript 5 引入了严格模式(strict mode)的概念。严格模式为JavaScript定义了一种不同的解析与执行模型。在严格模式下,ECMAScript 3中的一些不确定的行为将得到处理,而且对于某些不安全的操作也会抛出错误。(JavaScript高级程序设计)
设立严格模式的目的:
使用 ‘use strict’; 进入严格模式。 严格模式可以应用到整个script标签或个别函数中。
1 | // 整个语句都开启严格模式的语法 |
注意: 如果要为整个script开启严格模式,‘use strict’; 一定要放在第一行。 如果担心文件合并带来严格模式与正常模式的混合,可以将script写成自执行函数的形式。
1 | function strict() { |
在正常模式下,如果一个变量未声明就直接赋值,相当于创建一个全局变量。这给新人开发者带来便利的同时,给整个项目留下巨大隐患。严格模式将这种失误当成错误。
1 | ; |
严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常。
例如: NaN 是一个不可写的全局变量. 在正常模式下, 给 NaN 赋值不会产生任何作用; 开发者也不会受到任何错误反馈. 但在严格模式下, 给 NaN 赋值会抛出一个异常。
1 | ; |
给不可写属性赋值, 给只读属性(getter-only)赋值赋值, 给不可扩展对象(non-extensible object)的新属性赋值) 都会抛出异常:
1 | ; |
在严格模式下, 试图删除不可删除的属性时会抛出异常(之前这种操作不会产生任何效果)
1 | ; |
严格模式要求函数的参数名唯一。在正常模式下, 最后一个重名参数名会掩盖之前的重名参数。 之前的参数仍然可以通过 arguments[i] 来访问。
1 | function sum(a, a, c) { //SyntaxError: Strict mode function may not have duplicate parameter names |
1 | ; |
先看一个with的例子:
1 | var x = 17; |
结果是2, with块内x为全局变量x。
1 | var x = 17; |
结果是17, with块内x为变量obj.x。
所以with中块内的x究竟是指全局变量x还是obj.x在运行之前是无法得知的,这对编译器优化十分不利,因此严格模式禁用 with。
严格模式下的 eval 不在为上层范围(surrounding scope,注:包围eval代码块的范围)引入新变量。
在正常模式下, 代码 eval(“var x;”) 会给上层函数(surrounding function)或者全局引入一个新的变量 x 。
严格模式下,eval语句本身就是一个作用域,它所生成的变量只能用于eval内部。
1 | var x = 17; |
严格模式禁止删除声明变量。delete name 在严格模式下会引起语法错误
1 | ; |
eval 和 arguments 不能通过程序语法被绑定或赋值。 以下的所有尝试将引起语法错误:
1 | ; |
arguments对象不再追踪参数的变化
1 | function f(a) { |
正常模式下,arguments.callee 指向当前正在执行的函数。这个作用很小:直接给执行函数命名就可以了。
1 | ; |
严格模式下更容易写出“安全”的JavaScript。
在严格模式下通过this传递给一个函数的值不会被强制转换为一个对象。
1 | function f() { |
对一个普通的函数来说,this总会是一个对象:不管调用时this它本来就是一个对象;还是用布尔值,字符串或者数字调用函数时函数里面被封装成对象的this;还是使用undefined或者null调用函数时this代表的全局对象(使用call, apply或者bind方法来指定一个确定的this)。
这种自动转化为对象的过程不仅是一种性能上的损耗,同时在浏览器中暴露出全局对象也会成为安全隐患。
所以对于一个开启严格模式的函数,指定的this不再被封装为对象,而且如果没有指定this的话它值是undefined。
1 | ; |
在严格模式中一部分字符变成了保留的关键字。这些字符包括implements, interface, let, package, private, protected, public,
static和yield。在严格模式下,你不能再用这些名字作为变量名或者形参名。
1 | function package(protected) // !!! |
严格模式只允许在全局作用域或函数作用域的顶层声明函数。也就是说,不允许在非函数的代码块内声明函数。
1 | ; |
严格模式虽然限制了一部分JavaScript书写和运行的自由,但是随着JavaScript在更大的工程中扮演更重要的角色,规范化是必经之路。
在之前的项目 Regional Guideline 中,有一个操作点击 Ext 树的一个结点,展开这个结点的全部子树(树的深度未知),刚开始看到 TreeNode 中有一个名为 expand 的 Public Method, 其API如下:
[公司使用的版本为ExtJS 3.3]
简单明了,expand 第一个参数 deep 是一个 Boolean 型参数,如果为true的话,就展开当前结点以及子结点的所有子结点。
于是没有多加思考就用了。在开发测试环节一直没有出现什么问题,可是到了 Production 测试,帮忙测试的同学发现: 在操作树的时候,有时候浏览器会崩溃。刚开始以为是特殊情况,浏览器问题之类的,没有在意。可是不断地测试发现浏览器崩溃的情况是可复现的,就是在某几个固定的树展开的时候会出现这个问题。可见这不是浏览器的问题,是我代码的问题。
排查代码,发现这个 expand 方法似乎是罪魁祸首。查看ExtJS源码,果然是这个家伙的问题,原来这个函数使用递归的方式去展开所有的子结点,而当子结点比较多的时候,内存和CPU的消耗变得非常大,于是浏览器就崩溃了。
首先查看 ExtJS 源码, TreeNode 中的 expand 方法的源码如下
1 | /** |
expandChildNodes 的源码如下
1 | /** |
查看调用关系,发现 expand 方法如果传参 deep = true 的话会调用 expandChildNodes 方法去展开当前结点的子结点,而 expandChildNodes 方法又调用 expand 方法逐个展开子结点的所有子结点。 这样就变成了递归。
假设有一个求和函数sum: sum(n) = ∑ k
1 | function sum(n) { |
循环自然是速度和性能最好的,但是在编写复杂的代码时,循环代码的数学描述性不够强。
1 | function sum(n) { |
使用上述递归的方式可以说是将代码与数学描述完美结合,以上代码给一个完全不懂编程的人也看得懂。
但是我们分析其计算过程,比如计算sum(5)的时候,其计算过程是这样的:
1 | sum(5) |
这样的计算有什么问题呢?
我们知道线程在执行代码的时候,计算机会分配一定大小的栈空间,每次方法调用时都会在栈里储存一定信息(如参数、局部变量、返回地址、调用位置等等),这些信息会占用一定空间,成千上万个此类空间累积起来,可能会导致栈溢出。
1 | function sum(x, total = 0) { |
计算 sum(5)的时候,其过程是这样的:
1 | sum(5, 0) |
sum() 函数多了一个 total 参数,这个参数记录在递归调用时上一次计算的结果,并将其传入下一次递归调用中。每一次函数调用都发生在函数最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。
函数在尾部调用自身,就称为尾递归。
尾递归的本质,其实是将递归方法中的需要的“所有状态”通过方法的参数传入下一次调用中。
与普通递归相比,由于尾递归的调用处于方法的最后,因此方法之前所积累下的各种状态对于递归调用结果已经没有任何意义,因此完全可以把本次方法中留在堆栈中的数据完全清除,把空间让给最后的递归调用。这样的优化使得递归不会在调用堆栈上产生堆积,意味着即使是“无限”递归也不会让堆栈溢出。这便是尾递归的优势。
ES6 中将会资磁zīcí尾递归优化,通过尾递归优化,JavaScript 代码在解释成机器码的时候,会将尾递归函数解释成 while 函数,达到写的时候表达性强,运行的时候速度高的效果。
下面来看 Babel 编译的效果,将上述为递归的 sum 函数编译后如下:
1 | ; |
ES6的尾递归优化只在严格模式下开启,正常模式是无效的。
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
* arguments:返回调用时函数的参数。* func.caller:返回调用当前函数的那个函数。
尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
回到最早的问题,如何高效地展开一棵不知深浅的树?
当时并没有尾递归方面的知识,而且改 Ext 源码也不是那么方便,于是通过 Google 知道了一个比较好的解决方案:使用栈代替递归。
怎么做呢?
要展开一棵树,首先将树的根结点入栈,然后一个节点一个节点出栈,每次出栈后,将出栈节点的所有子节点入栈,以此达到遍历一颗树的效果。出栈的过程中逐一展开当前节点的字结点。
1 | expandAllChildNodes: function(node) { |
这个方法将递归转化为栈,可读性也不是很差,算是一个不错的解决方案。测试发现之前几个导致浏览器崩溃的树都可以完美展开,O(∩_∩)O~~。
递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。
循环代表着高效,递归代表着易读,如果能将递归方便地转化为循环是想必那是极好的,可是如果转化不是那么方便的话,尽量使用尾递归。
]]>异步操作一直都是 JavaScript 中一个比较麻烦的事情,从最早的 callback hell,到TJ大神的 co,再到 Promise 对象,然后ES6中的 Generator 函数,每次都有所改进,但都不是那么彻底,而且理解起来总是很复杂。
直到 async/await 出现,让写异步的人根本不用关心它是不是异步,可以说是目前最好的 JavaScript 异步解决方案。
ECMAScript 2016(ES7) 中已经确定支持 async/await,那我们怎么能够落后呢?
本文是 async/await 的学习笔记,涵盖基本用法以及一些小 demo。
阮一峰的 Blog async 函数的含义和用法, 对async的定义一语中的:async 函数就是 Generator 函数的语法糖。
假如有一个Generator函数:
1 | ; |
调用方法:
1 | let generator = gen(); |
将 gen 函数写成 async 函数,就是下面这样:
1 | const asyncF = async(()=> { |
一比较就会发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。
由于目前的大部分浏览器和 NodeJS 环境还不支持 async/await,所以本文程序借助 “asyncawait” 实现,需要额外安装
1 | npm install asyncawait |
当然如果你对 babel 比较熟悉的话,也可以通过 babel 将 async/await 编译为 ES5,就可直接运行了。
可以看到使用 Generator 的时候获取返回值必须使用 .then() 方法,而使用 async/await 就简单很多:
1 | ; |
await 等待的虽然是 promise 对象,但不必写使用 .then(),也可以得到返回值。
既然 .then() 不用写了,那 .catch()也不用写,可以直接用标准的try
catch语法捕捉错误
1 | const f = (time) => { |
await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中
await 最好用的地方是可以写在 for 循环里面,这是Promise无法做到的,使得 async/await 看起来更像是同步代码
1 | const f = (time) => { |