下面简单讨论几种在命令行传入参数,并在项目中获取参数值的方法。
process.env
可以获取到用户环境的对象。可以在执行命令时设置参数到环境变量,然后在项目中通过 process.env
去获取参数值。
不同系统的命令行中,设置环境变量的命令不同。如Windows系统:set key=value
,而Mac系统:key=value
。
为了项目中可以统一使用同一命令设置环境变量,可以使用cross-env
。
npm i -D cross-env
在package.json
中,配置
1 | "scripts": { |
在index.js
中,输出
1 | console.log(process.env.by) // baiyuWHITE |
process.argv
返回一个数组,包含启动node进程时传入的命令行参数。
默认包含两个值,第一个为node命令的绝对路径,第二个为执行的文件的路径。后面的值则为传入的命令行参数。
执行 node index.js by=baiyu age=29
命令,在 index.js 中输入 process.argv
:
1 | console.log(process.argv) |
得到以下数组:
1 | [ |
传递命令行参数时,执行JavaScript文件使用 node 命令与使用 npm 命令,有一些区别。
使用 node 命令时,命令后面的参数,会以空格分隔原样添加到 argv 数组中。
如执行 node index.js by=baiyu --age=29 actor
,会得到
1 | [ |
使用 npm 命令时,开始设置参数前,应该先添加 --
,否则参数名中以 --
开头的都会被忽略。
执行 npm start by=baiyu --age=29 actor
命令,得到
1 | [ |
如上,--
开头的参数 age 被忽略
因此,使用 npm 命令时,应该执行如下格式的命令:
npm start -- --by=baiyu age=29 actor
得到结果
1 | [ |
使用 node 命令时,不要在开始传参前添加
--
,否则--
也会被作为参数添加进argv
数组。
如上使用 process.argv
获取命令行参数时,得到的数组中,参数都是 key=value
格式的字符串,使用参数的话需要解析,非常麻烦。
这时,就可以使用 yargs 插件。
npm i -D yargs
index.js1
2let argv = require('yargs').argv
console.log(argv)
node命令
执行 node index.js --by=baiyu --age=29
,得到一个包含命令行参数的对象
1 | { _: [], by: 'baiyu', age: 29, '$0': 'index.js' } |
npm命令
与使用 process.argv
时同理,执行 npm 命令时,开始参数前,需要添加 --
,否则以 --
开头的参数会被忽略。
执行 npm start -- --by=baiyu --age=29 actor
,得到对象
1 | { _: [ 'actor' ], by: 'baiyu', age: 29, '$0': 'index.js' } |
如上,通过得到的对象,就可以很方便的获取参数的值。
使用 yargs 需要注意:
key=value
格式的参数,key 前面必须加上--
,否则将直接作为字符串添加到属性_
的数组中。
如执行以下命令:
npm start -- by=baiyu --age=29 actor
得到的结果为
1 | { _: [ 'by=baiyu', 'actor' ], age: 29, '$0': 'index.js' } |
MDN: https://developer.mozilla.org/zh-CN/docs/Web/Web_Components
自定义 HTML 标签,必须使用 -
连字符,以区分于原生 HTML标签。
定义一个自定义元素,首先需要定义一个类,并继承 HTMLElement
类,来继承 HTML 元素的特性。
通过标签使用自定义元素,即该类的实例。
1 | class UserCard extends HTMLElement { |
然后,需要调用 window.customElements.define
方法,将 user-card
标签与这个类关联。
1 | window.customElements.define('user-card', UserCard) |
自定义元素中的内容,在创建的 class 的构造函数中定义。this
即为自定义元素实例。
1 | class UserCard extends HTMLElement { |
npm config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/
npm config set electron_mirror https://npm.taobao.org/mirrors/electron/
npm config set phantomjs_cdnurl https://npm.taobao.org/mirrors/phantomjs/
https://
报错提示: must be a full url with 'http://'
解决:npm config set strict-ssl false
<br>
和 v-html
white-space: pre-wrap;
文本显示方式 | \n | <br> |
---|---|---|
vue 中使用 { { } } | 默认不换行,\n 的位置会有空格,设置样式 white-space: pre-wrap; ,可以换行 | 不换行 |
textContent | 默认不换行,\n 的位置会有空格,可通过设置 white-space: pre-wrap; 换行 | 不换行 |
innerHTML | 默认不换行,\n 的位置会有空格,可通过设置 white-space: pre-wrap; 换行 | 换行 |
innerText | 默认换行 | 不换行 |
textarea 标签的 value | 默认换行 | 不换行 |
demos代码 GitHub 地址:https://github.com/huajianduzhuo/es6/tree/master/proxy
Proxy 用于在目标对象之前架设一层拦截,外界对对象的访问,都必须经过这层拦截。因此可以对外界的访问进行过滤和改写。
通过 Proxy 构造函数,来生成 proxy 实例。
1 | let target = {} |
target
表示需要被代理的目标对象,handler
用来定义拦截行为,也是一个对象。
1 | let obj = {} |
如上,通过定义 get
方法,用来拦截所有对 obj
对象的访问。因此虽然 obj
对象是一个空对象,但是访问 aaa
属性,仍能得到结果 29.
需要注意的是,要使代理起作用,访问 obj 目标对象时,要通过
proxy
实例,直接使用目标对象 obj,代理无作用。
如果 handler 是一个空对象,没有设置任何拦截,则访问 proxy 实例等同于访问 target 目标对象。
1 | let obj2 = {} |
在 handler 中可以设置的拦截操作如下
get (target, key, receiver)
target 为目标对象,key 为属性名,receiver 为原始的读操作所在的对象,一般是 proxy 实例。
拦截访问对象的属性的操作,如 proxy.foo 或 proxy[‘foo’]
1 | let obj = { |
receiver 有时候不是 proxy 实例,比如当 proxy 实例作为原型的时候,这时 receiver 指向原始的读操作所在的对象
1 | let pr = new Proxy({}, { |
set (target, key, value, receiver)
拦截对对象属性的赋值操作,如 proxy.foo = 'aaa'
作用:
1 | function invariant(key, action) { |
set
方法不 return true,在严格模式下会报错。
has (target, key)
拦截 key in proxy
操作,返回一个布尔值
has
拦截对for...in
循环不生效
1 | let src = {name: '白宇'} |
deleteProperty (target, key)
拦截 delete proxy[key]
操作,返回一个布尔值
如果该方法返回 false 或报错,则属性无法被删除
1 | let obj = {_name: 'white', name: '白宇'} |
ownKeys (target)
拦截以下操作,返回一个数组
Object.getOwnPropertyNames(proxy)
Object.getOwnPropertySymbols(proxy)
Object.keys(proxy)
for...in
getOwnPropertyDescriptor (target, key)
拦截 Object.getOwnPropertyDescriptor(proxy, key)
操作
defineProperty (target, key, propDesc)
拦截对 target 添加属性、为属性赋值对操作,不拦截删除属性对操作
return false
则对属性的操作不生效
1 | let p = new Proxy({age: 29}, { |
preventExtensions (target)
拦截 Object.preventExtensions(proxy)
操作
getPrototypeOf (target)
拦截下列操作
Object.prototype.__proto__
Object.prototype.isPrototypeOf()
Object.getPrototypeOf()
Reflect.getPrototypeOf()
instanceof
1 | let myProto = {name: '白宇'} |
返回值必须是对象或 null。如果目标对象不可扩展,则必须返回目标对象的原型对象。
isExtensible (target)
拦截 Object.isExtensible(proxy)
操作
setPrototypeOf (target, proto)
拦截 Object.setPrototypeOf(proxy, proto)
操作
apply (target, thisObj, args)
当 target 为函数时使用,拦截 proxy 作为函数调用的操作。如 proxy(...args)
、proxy.call(thisObj, ...args)
、proxy.apply(thisObj, args)
thisObj
为目标函数对上下文对象 this,args
为参数数组。
1 | function fun(name, age) { |
construct (target, args)
当 target 为构造函数时使用,拦截 proxy 作为构造函数调用当操作。如 new proxy(...args)
1 | function Fun(firstName, lastName) { |
Proxy.revocable
方法返回一个可取消的 Proxy 实例。
1 | let {proxy, revoke} = Proxy.revocable({}, {}) |
Proxy.revocable
方法返回一个对象,该对象的 proxy
属性是 Proxy
实例,revoke
属性是一个函数,可以取消 Proxy
实例。上面代码中,当执行 revoke
函数之后,再访问 Proxy
实例,就会抛出一个错误。
Proxy.revocable
的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
Proxy 代理后,目标对象内部的方法中的 this
会指向代理对象
1 | let target = { |
此外,有些原生对象的内部属性,只有通过正确的
this
才能拿到,所以Proxy
也无法代理这些原生对象的属性。
1 | let t = new Date() |
demo (在手机上查看)
判断为长按事件的条件:
time
disX
和disY
import LongTap from './plugins/LongTap'
Vue.use(LongTap)
<div v-longtap:[arg]="callback"></div>
或
<div v-longtap:[arg]="{handler: cb}"></div>
使用对象字面量方式绑定长按事件,可以配置一些参数
参数 | 类型 | 是否必需 | 默认值 | 说明 |
---|---|---|---|---|
handler | 函数 | 是 | 无 | 长按事件回调函数 |
time | integer | 否 | 1200 | 单位:ms,长按间隔时间,必须超过 500ms |
disX | number | 否 | 10 | 单位:px,判断手指是否移动了的间隔,若为负数,则允许 X 方向上的移动 |
disY | number | 否 | 10 | 单位:px,判断手指是否移动了的间隔,若为负数,则允许 Y 方向上的移动 |
参数 | 类型 | 说明 |
---|---|---|
event | Event | |
data | any | 注册事件时传入的动态指令参数 |
vNode | vNode | 触发长按事件的虚拟节点 |
长按元素时,可以为该元素添加激活时的样式,只需要添加一个全局的样式 longtap-active
即可,若有与元素本身的样式重复的,应添加 !important
。
该样式在点击时也会应用。
1 | <template> |
1 | let r = null, // setTimeout 标志 |
Service Worker 的生命周期与我们的网页是完全分开的。
想要在网页中使用 Service Worker,需要在我们网页的 javascript 中注册它。注册一个 Service Worker,浏览器会在后台开始一个 Service Worker 的安装步骤。
代表性的,在安装 Service Worker 期间,我们会想要缓存一些静态资源。如果所有文件全部缓存成功,Service Worker 则为 installed 状态。如果任何一个文件加载并缓存失败,则安装步骤将会失败,Service Worker 则不会被激活。不过,如果文件加载失败,也无需担心,因为 Service Worker 会重新去尝试加载。
安装步骤成功之后,接下来便是激活步骤。激活步骤可以用来处理一些旧的版本的 Service Worker 中缓存的资源。
激活成功之后,serviceWorker 就可以控制页面了,但是只针对在成功注册了 Service Worker 后打开的页面。也就是说,页面打开时有没有 Service Worker,决定了接下来页面的生命周期内受不受 Service Worker 控制。所以,只有当页面刷新后,之前不受 Service Worker 控制的页面才有可能被控制起来。
一旦 Service Worker 控制了页面,它将会有两种状态:terminated(中止状态),可以节省内存,或者在网页发起请求时,处理 fetch 和 message 事件。
下面是一个简单版本的 Service Worker 第一次安装时的生命周期图。
Service Worker 目前已经被 chrome、firefox、opera 支持。Edge 浏览器已经表示了支持,safari 未来也会支持 Service Worker。
开发阶段,可以在 localhost
和 127.0.0.1
中使用 Service Worker,但是部署之后,则必需使用 HTTPS。
使用 Service Worker 我们可以劫持连接,伪造并过滤响应。这个强大的功能容易被黑客恶意使用,为了防止这种情况,我们必须使用 HTTPS 来保证连接不被干扰。
可以使用 Github Pages 来调试我们的 demos。
首先需要创建一个 Service Worker 线程的 js 文件(/public/sw.js
)
页面添加下面的代码
1 | if ('serviceWorker' in navigator) { |
在浏览器中打开页面,查看 application –> service wrokers,就可以看到当前 serviceWorker
查看上面 then 方法的参数 registration
,是一个 ServiceWorkerRegistration 类型的对象
在 sw.js
中查看 this
,是一个 ServiceWorkerGlobalScope 类型的对象
我们可以没有负担的在每一次页面加载时调用 register()
,浏览器会去查看当前网页是否已经注册过 Service Worker 线程并做出相应的处理。
需要注意 register 函数的第一个参数 sw.js
,即 Service Worker 的 javascript 文件的位置。如果该文件位于项目的根目录,则 Service Worker 的作用域即为整个域名,换句话说,Service Worker 将会捕捉该域名下所有的 fetch 事件。如果 Service Worker 文件放在 /example/sw.js
,则 Service Worker 仅捕捉 /example/
URL 下的 fetch 事件。
register 函数的 scope
参数是可选的,用于指定想要 Service Worker 控制的内容的目录。如本例中,因为 sw.js
文件不是位于根目录,指定 scope 为 ‘/‘,则可以让 Service Worker 捕捉整个页面的 fetch 事件。
当页面已经成功注册了一个 Service Worker,我们就可以将注意力转移到 Service Worker 的 js 文件,在该文件中我们可以处理 install
事件。
最基本的例子,我们需要为 install 事件添加一个 callback,并确定需要缓存的文件。
1 | self.addEventListener('install', function(event) { |
在 install callback 中,我们需要遵从以下步骤:
1 | var CACHE_NAME = 'my-first-cache-v1' |
event.waitUntil()
方法接收一个 promise,并通过它知道安装消耗的时间,以及是否安装成功。
如果所有文件缓存成功,Service Worker 则为 installed。如果任何一个文件缓存失败,则 Service Worker 安装失败。
我们还可以在 install 事件中执行其他任务,或者避免把所有任务放在一个 install 事件中。
Service Worker 安装成功,并且用户跳转到一个不同的页面或者刷新之后,Service Worker 将开始捕捉 fetch 事件。
1 | self.addEventListener('fetch', event => { |
在 chrome 中测试发现,仅刷新页面缓存的文件没有从 cache 中获取,新打开一个页面可以看到 Service Worker 的 fetch 事件的效果。
如果你想要渐进式的缓存资源,可以通过处理 fetch request 的 response,将其添加进 cache。
1 | self.addEventListener('fetch', event => { |
response 的 type 为 basic,说明请求的资源来自当前域名。上面的代码校验不等于 basic 直接返回,意为来自其他域名的资源不进行缓存。
更新 Service Worker 需要遵从以下步骤:
install
事件。waiting
状态。activate
事件会被触发。在 activate
事件回调里,一个共同的任务是 cache 管理。必须在 activate
事件清理旧版本的 Service Worker,而不是在 install
事件中清理的原因是,如果在 install
事件中清理,则包括正在控制当前页面的 Service Worker 在内的所有旧版本 Service Worker,都会被停止,使得当前页面没有可以使用的 Service Worker。
英文:https://developers.google.cn/web/fundamentals/primers/service-workers/
中文:https://lavas.baidu.com/pwa/offline-and-cache-loading/service-worker/how-to-use-service-worker
目前已经有成熟的 webpack 插件支持我们在项目中使用 Service Worker。
sw-precache-webpack-plugin 使用 sw-precache
来生成 service worker 文件,并添加到构建目录。
由于 sw-precache
和 sw-toolbox
已经被弃用,所以我之前使用此插件到代码就不贴出来了。
npm 网站上此插件到文档链接无法打开,正确链接为
https://developers.google.cn/web/tools/workbox/modules/workbox-webpack-plugin
]]>全局安装 typescript
npm install -g typescript
编译
创建后缀名为 .ts
的文件,在该文件夹下,执行 tsc 文件名
,得到编译后的 js 文件,运行该 js 文件
在项目根目录创建 tsconfig.json
文件,用来指定编译选项
在拥有 tsconfig.json 文件的项目根目录下直接执行 tsc
,则会将项目目录下所有符合条件的 typescript 文件进行编译。
执行 tsc -w
,可以监视文件改动,文件保存后实时编译
配置介绍:https://www.tslang.cn/docs/handbook/tsconfig-json.html
编译选项:https://www.tslang.cn/docs/handbook/compiler-options.html
demos:https://github.com/huajianduzhuo/typescript-learn/blob/master/demos/01-basicTypes.ts
1 | let isDone: boolean = false |
1 | let decLiteral: number = 6 |
1 | let name: string = 'bob' |
两种定义方式
1 | let list1: number[] = [1, 2, 4] |
元组:已知数量和类型的数组
1 | let t1: [string, number, boolean] |
1 | enum Color { |
不清楚变量类型,可以使用 any
any 类型,可以赋予任何类型的值,不进行类型检查
any 与 Object 类型的异同点:
1 | let any1: any = '卫庄' |
函数没有返回值时,其返回类型是 void
声明 void 类型的变量没有用,因为只能赋值为 undefined 和 null
1 | function fun(): void { |
默认情况下,null 和 undefined 是所有类型的子类型。null 和 undefined 可以赋值给任何类型
strictNullChecks 为 true,null 和 undefined 只能赋值给 void 和他们的各自类型
1 | // let u: undefined = null // 因为可以赋值给任何类型,strictNullChecks 为 true,会报错 |
1 | function error(message: string): never { |
表示非原始类型,即除了 number, string, boolean, symbol, null, or undefined 之外的类型
strictNullChecks 不为 true 的情况下,null 和 undefined 为任何类型的子类型,所以可以通过类型检查
1 | function create(o: object): void {} |
好比其它语言里的类型转换,但是不进行特殊的数据检查和解构
两种类型断言方式:
在 TypeScript 里使用 JSX 时,只有 as 语法断言是被允许的
1 | let val1: any = '卫庄' |
demos:https://github.com/huajianduzhuo/typescript-learn/blob/master/demos/02-interface.ts
TypeScript 的核心原则之一是对值所具有的结构进行类型检查,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约
我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。
接口中每个属性后面不是逗号,而是分号,分号可省略
1 | interface LabelledValue { |
指非必需的属性,可存在可不存在
定义:在属性名后加一个 ?
1 | interface LabelledValue { |
只读属性只有在对象创建时才能修改值
定义:属性名前加 readonly
1 | interface Point { |
TypeScript 具有 ReadonlyArray
1 | let a: number[] = [1, 2, 3, 4] |
上面代码的最后一行,可以看到就算把整个 ReadonlyArray 赋值到一个普通数组也是不可以的。但是你可以用类型断言重写:
1 | a = ro as number[] |
在 typescript 中,对象字面量会被特殊对待而且会经过额外属性检查,当将它们赋值给变量或作为参数传递的时候。
如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。
1 | interface SquareConfig { |
绕过这些检查的方式:
1 | const square = createSquare({yanse: 'lightblue', width: 12} as SquareConfig) |
1 | interface SquareConfig { |
1 | const squareOption = { yanse: 'lightblue', width: 12 } |
接口不止可以描述具有属性的普通对象的外形,也可以描述函数的类型
接口描述函数类型,就像是一个只拥有参数列表和返回值类型的函数定义,参数列表里的每一个参数都要有名字和类型
1 | interface SearchFunc { |
1 | let mySearch2: SearchFunc |
接口也可以描述可以通过索引得到的类型
具有索引签名,描述索引的类型,以及对应的返回值的类型
支持两种索引签名:数字、字符串
数字索引的返回值必须是字符串索引返回值类型的子类型
1 | interface StringArray { |
1 | interface StringArray { |
1 | interface StringArray { |
接口也可以用来明确的强制一个类符合某种契约
类实现接口:implements
接口定义类的方法:方法名(参数列表)
1 | interface ClockInterface { |
接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。
1 | interface int1 { |
接口可以描述多种类型
比如:一个对象同时作为对象和函数使用
1 | interface Counter { |
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。
接口同样会继承到类的 private 和 protected 成员。这意味着这个接口类型只能被这个类或其子类所实现。
1 | class Control { |
demos:https://github.com/huajianduzhuo/typescript-learn/blob/master/demos/03-classes.ts
注意:typescript 中的类,与 ES6 中类的定义方式不一样,不能想当然的认为两种是一样的写法!!!
1 | class Greeter { |
派生类:子类;
基类:超类
派生类的构造函数里必须调用 super()
,即基类的构造函数。并且,必须在构造函数里访问 this 之前调用 super()。
派生类的构造函数仍然可以省略,会默认调用基类的构造函数。
派生类可以重写基类的方法。
1 | class Animal { |
如上面定义变量 dage 的方法,dage 为 Cat 类的实例,但是类型可以是基类 Animal。
public 是默认的修饰符,成员都默认为 public
1 | class Animal { |
当成员被标记成 private 时,它就不能在声明它的类的外部访问。
1 | class Animal { |
虽然访问 private 成员 typescript 检测会到错误,编译时报错,但是编译出的 javascript 文件仍能正常执行
TypeScript 使用的是结构性类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。
然而,当我们比较带有 private 或 protected 成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个 private 成员,那么只有当另外一个类型中也存在这样一个 private 成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected 成员也使用这个规则。
1 | class Animal { |
如上,因为含有私有属性 name,所以尽管两个类结构一模一样,但是仍然不能将 Animal2 的实例赋值给 Animal 类型的变量。如果将 name 的属性改为 public,则不会报错。
protected 与 private 相似,不同的是,protected 成员在派生类中仍然可以访问。
1 | class Person { |
构造函数也可以被标记成 protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。
1 | class Person { |
readonly 将属性设置为只读,初始化后不可修改。
初始化方式:
1 | class Dog { |
通过给构造函数添加一个限定符来声明参数属性,可以方便的在一个地方定义并初始化一个成员。
简化了先声明一个属性,构造函数接收参数,然后将参数的值赋值给属性的步骤。
public 类型属性,public 不可省略,否则便是构造函数接收一个参数,但是不会赋值给成员。
1 | class Rabbit { |
如上编译结果为:
1 | class Rabbit { |
通过 getters/setters 截取对对象成员的访问
使用存取器需要注意:
1 | let password = '1234' |
静态属性:存在于类本身上而不是类实例上的属性
1 | class Qin { |
编译后:
1 | class Qin { |
抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。
不同于接口,抽象类可以包含成员的实现细节。
abstract
关键字是用于定义抽象类和在抽象类内部定义抽象方法。
不可以创建抽象类的实例。
创建派生类的实例,变量的类型可以是抽象类。但是这样如果派生类中包含抽象类中不存在的方法,则无法调用。
抽象类中的抽象方法:
1 | abstract class Vehicle { |
类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。
接口可以继承类
1 | class Point { |
demos:https://github.com/huajianduzhuo/typescript-learn/blob/master/demos/04-functions.ts
函数类型包含两部分:参数类型和返回值类型。函数的类型只是由参数类型和返回值组成的。
只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。
对于返回值,我们在函数和返回值类型之前使用( => )符号,使之清晰明了。返回值类型是函数类型的必要部分,如果函数没有返回任何值,你也必须指定返回值类型为 void 而不能留空。
1 | let add: (baseValue: number, increment: number) => number = function( |
对于 add2 函数,如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript 编译器会自动识别出类型。
这叫做“按上下文归类”,是类型推论的一种。
TypeScript 里的每个函数参数都是必须的。编译器检查用户是否为每个参数都传入了值。编译器还会假设只有这些参数会被传递进函数。
简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。
1 | function sayName(firstName: string, lastName: string) { |
1 | function sayName2(firstName: string, lastName?: string) { |
可选参数必须跟在必须参数后面。
1 | function sayName3(firstName: string, lastName = '处长') { |
在所有必须参数后面的带默认初始化的参数都是可选的
与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined 值来获得默认值。
当不知道有多少参数会被传递进来时,可以把所有参数收集到一个变量里。
剩余参数会被当做个数不限的可选参数。可以一个都没有,同样也可以有任意个。编译器创建参数数组,名字是你在省略号(…)后面给定的名字,你可以在函数体内使用这个数组。
1 | function sayName4(firstName: string, ...restOfNames: string[]): string { |
可以为函数提供一个显式的 this 参数。this 参数是个假的参数,它出现在参数列表的最前面。
当在 tsconfig.json 中配置 noImplicitThis
为 true 时,this 表达式的值为 any 类型时,会生成一个错误。提供一个具有类型的 this 参数,告诉 typescript 函数期待在那个对象上调用,就不会报错了。
1 | interface Card { |
因为 this 是假的参数,编译时不会被编译到参数中,调用时也不用传递
1 | function testThis(this: Card, extraParam: string) { |
上面的 typescript 代码编译结果为
1 | function testThis(extraParam) { |
有时候,函数会根据传进来的参数的类型,决定返回类型。
可以通过为一个函数提供多个函数类型定义来进行函数重载。
1 | function overloadTest(x: { firstName: string; lastName: string }): string |
为了让编译器能够选择正确的检查类型,它与 JavaScript 里的处理流程相似。它查找重载列表,尝试使用第一个重载定义。如果匹配的话就使用这个。因此,在定义重载的时候,一定要把最精确的定义放在最前面。
function overloadTest(x): any 并不是重载列表的一部分。
以其他方式调用 overloadTest 会报错
demos:https://github.com/huajianduzhuo/typescript-learn/blob/master/demos/05-generics.ts
定义一个 identify 函数,会返回任何传入它的值:
1 | function identify(x: any): any { |
如上,第一想法是使用 any 类型。但是使用 any 类型,无法保证传入的参数类型与返回值类型一致。
可以使用类型变量,它是一种特殊的变量,只用于表示类型而不是值。
1 | function identify<T>(x: T): T { |
使用泛型函数:
传入所有的参数,包含类型参数
1 | let output1 = identify<string>('a') |
利用类型推论 – 即编译器会根据传入的参数自动地帮助我们确定 T 的类型
1 | let output2 = identify('a') |
使用泛型创建像 identity 这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。换句话说,你必须把这些参数当做是任意或所有类型。
如下使用泛型,会报错:
1 | function identify<T>(x: T): T { |
因为 x 可能是不包含 length 属性的类型,如 number 类型。
我们可以把泛型变量 T 当做类型的一部分使用,而不是整个类型,增加了灵活性。
1 | function identify2<T>(x: T[]): T[] { |
如上,泛型变量 T 代表的是数组类型的参数 x 中的元素的类型,而不是参数整体的类型。
为泛型函数 identify 定义泛型类型的接口
1 | interface indentifyFn { |
可以把泛型参数当作整个接口的一个参数
1 | interface indentifyFn2<T> { |
1 | class genericClass<T> { |
类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。
如下,访问 x 的 length 属性,会报错,因为不能证明所有类型都有 length 属性:
1 | function identify4<T>(x: T): T { |
可以定义一个接口为类型参数 T 描述约束条件,并使用 extends
关键字来实现约束
1 | interface lengthWise { |
可以声明一个被其他类型参数约束的类型参数
1 | function getProperty<T, K extends keyof T>(obj: T, key: K) { |
如上,可以保证参数 key 是 obj 的一个属性
1 | class BeeKeeper { |
demos:https://github.com/huajianduzhuo/typescript-learn/blob/master/demos/06-enums.ts
使用枚举我们可以定义一些带名字的常量。使用枚举可以清晰地表达意图或创建一组有区别的用例。TypeScript 支持数字的和基于字符串的枚举。
1 | enum Direction { |
如上,定义一个数字枚举。UP 的值默认为 0,其余的成员会从 0 开始自动增长。即 UP 为 0,DOWN 为 1,LEFT 为 2,RIGHT 为 3.
我们也可以指定数字的初始值:
1 | enum Direction { |
这样,UP 的值为 1,其余成员从 1 开始自动增长。
1 | enum ABO { |
从技术的角度来说,枚举可以混合字符串和数字成员
除非你真的想要利用 JavaScript 运行时的行为,否则我们不建议这样做。
1 | enum HeterogeneousEnums { |
每个枚举成员都带有一个值,它可以是 常量 或 计算出来的。
当满足如下条件时,枚举成员被当作是常量:
它是枚举的第一个成员且没有初始化器,这种情况下它被赋予值 0
1 | enum E { |
它不带有初始化器且它之前的枚举成员是一个 数字常量。这种情况下,当前枚举成员的值为它上一个枚举成员的值加 1
1 | enum E { |
枚举成员使用 常量枚举表达式 初始化。常数枚举表达式是 TypeScript 表达式的子集,它可以在编译阶段求值。当一个表达式满足下面条件之一时,它就是一个常量枚举表达式:
所有其它情况的枚举成员被当作是需要计算得出的值
1 | enum FileAccess { |
数字枚举成员具有 反向映射
下面的代码:
1 | enum Direction { |
会被编译为:
1 | var Direction |
生成的代码中,枚举类型被编译成一个对象,它包含了正向映射( name -> value)和反向映射( value -> name)。
不会为字符串枚举成员生成反向映射
常量枚举通过在枚举上使用 const
修饰符来定义。
1 | const enum Enum { |
常量枚举只能使用常量枚举表达式,常量枚举不允许包含计算成员。
不同于常规的枚举,常量枚举在编译时会被删除。常量枚举成员在使用的地方会被内联进来。
常规枚举:
1 | enum Enum { |
编译结果为
1 | var Enum |
常量枚举:
1 | const enum Enum { |
编译结果为
1 | var enums = [1 /* A */, 2 /* B */] |
外部枚举用来描述已经存在的枚举类型的形状。
1 | declare enum Enum2 { |
外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。
? 无编译结果,没明白
demos:https://github.com/huajianduzhuo/typescript-learn/blob/master/demos/07-advancedTypes.ts
交叉类型是将多个类型合并为一个类型。这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
例如,Person & Serializable & Loggable 同时是 Person 和 Serializable 和 Loggable。就是说这个类型的对象同时拥有了这三种类型的成员。
1 | function extend<T, U>(first: T, second: U): T & U { |
联合类型表示一个值可以是几种类型之一。
我们用竖线( | )分隔每个类型,所以 number | string | boolean 表示一个值可以是 number,string,或 boolean。
1 | function padLeft(value: string, padding: string | number) { |
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
1 | interface Bird { |
上一节的代码中,访问 pet.fly
报错。为了能够访问联合类型中不属于共有成员的其他成员,可以使用类型断言:
1 | if ((<Bird>pet).fly) { |
上面的代码中多次使用了类型断言。
typescript 的 类型保护机制 可以让我们一旦检查过 pet 的类型,就能在之后的每个分支里知道 pet 的类型。
类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。
要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型谓词。
谓词为 parameterName is Type
这种形式, parameterName 必须是来自于当前函数签名里的一个参数名。
1 | function isFish(pet: Bird | Fish): pet is Fish { |
注意:TypeScript 不仅知道在 if 分支里 pet 是 Fish 类型;它还清楚在 else 分支里,一定不是 Fish 类型,一定是 Bird 类型。
对于原始类型,不需要定义一个函数来进行类型保护。可以直接使用 typeof x === 'number'
,因为 typescript 可以将他识别为一个类型保护。
这些 typeof 类型保护 只有两种形式能被识别:typeof v === "typename"
和 typeof v !== "typename"
,”typename” 必须是 number,string,boolean 或 symbol。但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。
1 | function padLeft2(value: string, padding: string | number) { |
instanceof
类型保护是通过构造函数来细化类型的一种方式。
1 | interface Padder { |
instanceof 的右侧要求是一个构造函数,TypeScript 将细化为:
TypeScript 具有两种特殊的类型, null 和 undefined,它们分别具有值 null 和 undefined.
默认情况下,类型检查器认为 null 与 undefined 可以赋值给任何类型。
–strictNullChecks 标记可以解决此错误:当你声明一个变量时,它不会自动地包含 null 或 undefined。
注意,按照 JavaScript 的语义,TypeScript 会把 null 和 undefined 区别对待。 string | null, string | undefined 和 string | undefined | null 是不同的类型。
1 | let s = 'abc' |
使用了 --strictNullChecks
,可选参数会被自动地加上 | undefined
:
1 | function f(a: number, b?: number): number { |
可选属性也会有同样的处理:
1 | class C { |
当一个变量的类型为联合类型,并且包含 null 时,使用这个变量时需要使用类型保护来去除 null。
可以使用 if 语句
1 | function fn(str: string | null) { |
也可以用 || 短路运算符
1 | function fn(str: string | null) { |
如果编译器不能够去除 null 或 undefined,你可以使用类型断言手动去除。 语法是添加 !
后缀: identifier!
从 identifier 的类型里去除了 null 和 undefined:
1 | function fn(str: string | null) { |
类型别名会给一个类型起个新名字。类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
起别名不会新建一个类型 - 它创建了一个新名字来引用那个类型。
1 | type s = string | number |
给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。
同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并在别名声明的右侧传入:
1 | type GenericType<T> = (c: T) => T |
类型别名像接口一样,但是仍有细微差别
如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。
字符串字面量类型允许你 指定字符串必须的固定值。
在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。通过结合使用这些特性,你可以实现类似枚举类型的字符串。
1 | type Easing = 'ease-in' | 'ease-out' | 'ease-in-out' |
字符串字面量类型还可以用于区分函数重载:
1 | function createElement(tagname: 'INPUT'): HTMLInputElement |
1 | type Num = 1 | 2 |
如我们在 枚举一节里提到的,当每个枚举成员都是用字面量初始化的时候枚举成员是具有类型的。
在我们谈及“ 单例类型 ”的时候,多数是指枚举成员类型和数字/字符串字面量类型,尽管大多数用户会互换使用“单例类型”和“字面量类型”。
你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合 的高级模式,它也称做 标签联合 或 代数数据类型。
可辨识联合在函数式编程很有用处。一些语言会自动地为你辨识联合;而 TypeScript 则基于已有的 JavaScript 模式。
它具有 3 个要素:
1 | interface Square { |
首先我们声明了将要联合的接口。每个接口都有 kind 属性但有不同的字符串字面量类型。kind 属性称做 可辨识的特征 或 标签。其它的属性则特定于各个接口。注意,目前各个接口间是没有联系的。下面我们把它们联合到一起:
1 | type Shape = Square | RectAngle | Circle |
现在我们使用可辨识联合:
1 | function area(s: Shape): number { |
当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。比如,如果我们添加了 Triangle 到 Shape,我们希望编译器可以提醒我们 area 函数应该涵盖这一情况。
一种方法是开启 strictNullChecks
,并指定返回值类型:
1 | type Shape = Square | RectAngle | Circle | Triangle |
因为 area 函数没有包含所有情况,所以编译器认为可能返回 undefined,指定返回 number 类型便会报错。
第二种方法使用 never
类型,编译器用它来进行完整性检查:
1 | function assertNever(n): never { |
多态的 this 类型表示的是某个包含类或接口的 子类型。 这被称做 F-bounded 多态性。它能很容易的表现连贯接口间的继承。
1 | class BasicCalculator { |
使用索引类型,编译器就能够检查使用了动态属性名的代码。
一个常见的 JavaScript 模式是从对象中选取属性的子集:
1 | function pluck(o, names) { |
在 TypeScript 里使用此函数,通过 索引类型查询 和 索引访问操作符:
keyof T
。对于任何类型 T
,keyof T
的结果为 T 上已知的公共属性名的联合。T[K]
。1 | function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] { |
没明白作用
TODO
demos:https://github.com/huajianduzhuo/typescript-learn/blob/master/demos/08-modules.ts
模块在其自身的作用域里执行,而不是在全局作用域里。
模块是自声明的;两个模块之间的关系是通过在文件级别上使用 imports
和 exports
建立的。
模块使用模块加载器去导入其它的模块。在运行时,模块加载器的作用是在执行此模块代码前去查找并执行这个模块的所有依赖。大家最熟知的 JavaScript 模块加载器是服务于 Node.js 的 CommonJS
和服务于 Web 应用的 Require.js
。
TypeScript 与 ECMAScript 2015 一样,任何包含顶级 import 或者 export 的文件都被当成一个模块。相反地,如果一个文件不带有顶级的 import 或者 export 声明,那么它的内容被视为全局可见的(因此对模块也是可见的)。
任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加 export
关键字来导出。
1 | export interface StringValidator { |
1 | class MobileValidator implements StringValidator { |
有时候我们会把导入的模块的部分重新导出。重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量。
1 | export * from './MobileValidator' |
1 | import { StringValidator } from './Validation' |
可以对导入内容重命名:
1 | import { StringValidator as SV } from './Validation' |
1 | import * as Validation from './Validation' |
尽管不推荐这么做,一些模块会设置一些全局状态供其它模块使用。这些模块可能没有任何的导出或用户根本就不关注它的导出。使用下面的方法来导入这类模块:
1 | import './my-module.js' |
每个模块都可以有且只能有一个 default
导出。
CommonJS 和 AMD 都有一个 exports 对象的概念,它包含了一个模块的所有导出内容。
TypeScript 模块支持 export =
语法以支持传统的 CommonJS 和 AMD 的工作流模型。
export =
语法定义一个模块的导出对象。它可以是类,接口,命名空间,函数或枚举。
若要导入一个使用了export =
的模块时,必须使用 TypeScript 提供的特定语法import module = require("module")
。
ZipCodeValidator.ts
1 | import { StringValidator } from './Validation' |
test.ts
1 | import ZipCodeValidator = require('./ZipCodeValidator') |
要想描述非 TypeScript 编写的类库的类型,我们需要声明类库所暴露出的 API。
我们叫它声明因为它不是“外部程序”的具体实现。它们通常是在 .d.ts
文件里定义的。
我们可以使用顶级的 export 声明来为每个模块都定义一个 .d.ts
文件,但最好还是写在一个大的 .d.ts
文件里。我们使用与构造一个外部命名空间相似的方法,但是这里使用 module
关键字并且把名字用引号括起来,方便之后 import。
node.d.ts
1 | declare module 'path' { |
现在我们可以 /// <reference> node.d.ts
并且使用 import module = require("module");
或 import * as module from "module"
加载模块。
1 | /// <reference path="node.d.ts" /> |
如上,声明了 .d.ts
文件后,import 的 path 依赖,会指向 node.d.ts
文件,且不能调用未在 node.d.ts
文件中导出的方法。
假如你不想在使用一个新模块之前花时间去编写声明,你可以采用声明的简写形式以便能够快速使用它。
简写模块里所有导出的类型将是 any。
node.d.ts
1 | declare module 'axios' |
使用
1 | /// <reference path="node.d.ts" /> |
某些模块加载器如 SystemJS 和 AMD 支持导入非 JavaScript 内容。 它们通常会使用一个前缀或后缀来表示特殊的加载语法。模块声明通配符可以用来表示这些情况。
1 | declare module '*!text' { |
现在你可以就导入匹配”!text”或”json!“的内容了。
1 | import fileContent from './xyz.txt!text' |
导出
1 | export class SomeType { |
导入
使用重新导出进行扩展
模块里不要使用命名空间
危险信号
以下均为模块结构上的危险信号。重新检查以确保你没有在对模块使用命名空间:
demos:https://github.com/huajianduzhuo/typescript-learn/blob/master/demos/09-namespaces.ts
使用 namespace
定义命名空间,命名空间内可提供给外部访问的内容用 export
导出。
1 | namespace Validation { |
当应用变得越来越大时,我们需要将代码分离到不同的文件中以便于维护。
多个文件可以是同一个命名空间,在使用时就如同他们在一个文件中定义的一样。
因为不同文件之间存在依赖关系,所以我们加入了引用标签来告诉编译器文件之间的关联。
demos 查看 /demos/namespaces/..
将所有的输入文件编译为一个输出文件,需要使用 --outFile
标记。编译器会根据源码里的引用标签自动地对输出进行排序。
tsc --outFile dist/namespaces/sample.js demos/namespaces/Test.ts
另一种简化命名空间操作的方法是使用import q = x.y.z
给常用的对象起一个短的名字。不要与用来加载模块的import x = require('name')
语法弄混了,这里的语法是为指定的符号创建一个别名。你可以用这种方法为任意标识符创建别名,也包括导入的模块中的对象。
1 | namespace Shapes { |
注意,我们并没有使用require
关键字,而是直接使用导入符号的限定名赋值。这与使用 var 相似,但它还适用于类型和导入的具有命名空间含义的符号。重要的是,对于值来讲,import
会生成与原始符号不同的引用,所以改变别名的 var 值并不会影响原始变量的值。
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上,可以修改类的行为。
使用方式:@expression
,expression 求值后必须是一个函数。
装饰器是一项实验性特性,若要使用,需要在
tsconfig.json
中的compilerOptions
里启用"experimentalDecorators": true
装饰器本身就是一个函数,被声明的信息会作为参数传入装饰器中。如下:
1 | function path(target: any) { |
上面的代码中,path
为装饰器函数,用在类 Hello
声明上,Hello
会作为参数传入 path
中。编译后的 js:
1 | var __decorate = |
如上,__decorate
为处理装饰器的函数。
有时候可能需要提前为装饰器函数绑定一些参数,可以使用类似 bind
的做法。
可以定义一个装饰器工厂函数,用来接收需要绑定的参数,并且该装饰器工厂函数必须返回一个函数,返回的函数才是真正的装饰器函数。
1 | function path(prePath: string) { |
如上代码中,path
不再是装饰器函数,而是装饰器工厂函数,它用来接收预先绑定的参数。path 返回的函数
则是真正的装饰器函数,Hello
会作为参数传入该函数中。编译后的 js:
1 | function path(prePath) { |
可以看到,与 path 未传参数时的结果 __decorate([path], Hello)
对比,传了参数的结果为 __decorate([path('src')], Hello)
,说明此时装饰器函数已经不是 path
,而是 path
执行后返回的函数,并且绑定了一些预传参数。
应用于类构造函数,会将类的构造函数作为参数传入装饰器函数中。
举例参考上面的 Decorator 基本使用
和 绑定参数
会应用在方法的属性描述符上,可以用来监视,修改或者替换方法定义。
装饰器函数会接收 3 个参数:
如果代码输出目标版本小于
ES5
,属性描述符将会是undefined
装饰器应用于实例方法:
1 | function Get(target: any, key: string, desc: PropertyDescriptor) { |
如上代码,装饰器函数 Get
的参数中,target
为类 Per
的原型对象,key
为方法名 say
,desc
为属性描述符。编译后的 js:
1 | function Get(target, key, desc) { |
需要注意,上面 __decorate
的参数中,desc
传的虽然是 null
,但是在 __decorate
函数内部,会将 desc
赋值为属性描述符。
装饰器应用于静态方法:
1 | function Get(target: any, key: string, desc: PropertyDescriptor) { |
编译后的 js:
1 | function Get(target, key, desc) { |
如上,装饰器函数 Get
的参数中,target
为类 Per
的构造函数,key
为方法名 move
,desc
为属性描述符。
如果方法装饰器函数返回一个值,它会被当作该方法的属性描述符。
访问器装饰器应用于访问器的属性描述符并且可以用来监视,修改或替换一个访问器的定义。
TypeScript 不允许同时装饰一个成员的 get 和 set 访问器。取而代之的是,一个成员的所有装饰器必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了 get 和 set 访问器,而不是分开声明的。
装饰器函数会接收 3 个参数:
1 | function configurable(value: boolean) { |
如果访问器装饰器函数返回一个值,它会被用作该访问器属性的属性描述符。
1 | function configurable(value: boolean) { |
如上代码,编译为 js 之后执行,x 的值为 4,因为装饰器函数返回的属性描述符替换了 x 本身的属性描述符。
装饰器函数会接收 2 个参数:
属性描述符不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。 因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。因此,属性描述符只能用来监视类中是否声明了某个名字的属性。
1 | function defaultValue(value: string) { |
应用于类构造函数或方法声明。
装饰器函数会接收 3 个参数:
参数装饰器只能用来监视一个方法的参数是否被传入
参数装饰器的返回值会被忽略
1 | function dd(target: any, methodName: string, index: number) { |
多个装饰器可以应用到一个声明上。
1 | function A() { |
如上代码,编译后运行的结果为:
1 | f |
复合装饰器时,装饰器工厂函数会由上至下依次执行,而装饰器函数则由下至上执行。
]]>demos 代码 github 地址:https://github.com/huajianduzhuo/es6/tree/master/generator
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
1 | function* helloWorldGenerator() { |
上面代码定义了一个 Generator 函数 helloWorldGenerator,它内部有两个 yield 表达式( hello 和 world ),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
执行 Generator 函数会返回一个遍历器对象(Iterator),也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object)。
下一步,必须调用遍历器对象的 next 方法,使得指针移向下一个状态。也就是说,每次调用 next 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return 语句)为止。换言之,Generator 函数是分段执行的,yield 表达式是暂停执行的标记,而 next 方法可以恢复执行。
1 | hw.next() |
总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的 next 方法,就会返回一个有着 value 和 done 两个属性的对象。value 属性表示当前的内部状态的值,是 yield 表达式后面那个表达式的值;done 属性是一个布尔值,表示是否遍历结束。
1 | // demo 02-generator.js |
遍历器对象的 next 方法的运行逻辑如下
遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性值。
下一次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式。
如果没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值。
如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined。
yield 表达式只能用在 Generator 函数里面,用在其他地方都会报错。
yield 表达式如果用在另一个表达式之中,必须放在圆括号里面。
1 | console.log('Hello' + (yield 123)); // OK |
yield 表达式本身没有返回值,或者说总是返回 undefined。next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。
1 | function* f() { |
这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过 next 方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
由于 next 方法的参数表示上一个 yield 表达式的返回值,所以在第一次使用 next 方法时,传递参数是无效的。V8 引擎直接忽略第一次使用 next 方法时的参数,只有从第二次使用 next 方法开始,参数才是有效的。从语义上讲,第一个 next 方法用来启动遍历器对象,所以不用带有参数。
Generator 函数返回的遍历器对象,都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
1 | var g = function*() { |
第一个错误被 Generator 函数体内的 catch 语句捕获。i 第二次抛出错误,由于 Generator 函数内部的 catch 语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的 catch 语句捕获。
1 | var g = function*() { |
1 | var gen = function* gen() { |
1 | function* gen() { |
1 | var gen = function* gen() { |
上面代码中,g.throw 方法被捕获以后,自动执行了一次 next 方法,所以会打印 b。另外,也可以看到,只要 Generator 函数内部部署了 try…catch 代码块,那么遍历器的 throw 方法抛出的错误,不影响下一次遍历。
1 | function* foo() { |
上面代码中,第二个 next 方法向函数体内传入一个参数 42,数值是没有 toUpperCase 方法的,所以会抛出一个 TypeError 错误,被函数体外的 catch 捕获。
1 | function* g() { |
Generator 函数返回的遍历器对象,还有一个 return 方法,可以返回给定的值,并且终结遍历 Generator 函数。
1 | function* gen() { |
如果 return 方法调用时,不提供参数,则返回值的 value 属性为 undefined。
如果 Generator 函数内部有 try…finally 代码块,那么 return 方法会推迟到 finally 代码块执行完再执行。
1 | function* numbers() { |
yield* 表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。
1 | function* foo() { |
从语法角度看,如果 yield 表达式后面跟的是一个遍历器对象,需要在 yield 表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为 yield*表达式。
yield*后面的 Generator 函数(没有 return 语句时),等同于在 Generator 函数内部,部署一个 for…of 循环。
1 | function* concat(iter1, iter2) { |
上面代码说明,yield 后面的 Generator 函数(没有 return 语句时),不过是 for…of 的一种简写形式,完全可以用后者替代前者。反之,在有 return 语句时,则需要用 `var value = yield iterator` 的形式获取 return 语句的值。
如果 yield*后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。
1 | function* gen() { |
实际上,任何数据结构只要有 Iterator 接口,就可以被 yield*遍历。
1 | let read = (function*() { |
demo 05-yield-xing.js
1 | function* iterTree(tree) { |
1 | let obj = { |
1 | function* g() {} |
上面代码表明,Generator 函数 g 返回的遍历器 obj,是 g 的实例,而且继承了 g.prototype。
1 | function* g() { |
使用 generator 封装异步任务,由下面的例子可以看出,异步任务定义很简单,但是流程管理很复杂。
demo: 07-async.js,在 index.html 中引入,使用浏览器查看结果
1 | function* gen() { |
简单来说,一个C程序就是由若干头文件和函数组成。
一个简单的 Hello World C 程序
1 |
|
以上C程序代码中,#include<stdio.h>
为包含头文件,int main(){}
为主函数。
#include<stdio.h>
就是一条预处理命令,它的作用是通知C语言编译系统在对C程序进行正式编译之前需要做一些预处理工作
函数就是实现代码逻辑的一个小的单元
在最新的C标准中,main 函数前的类型为 int 而不是 void
一个C程序有且只有一个主函数,即 main 函数。C程序就是执行主函数里的代码,也可以说主函数就是C语言中的唯一入口。
main 前面的 int 就是主函数的类型。
printf() 是格式输出函数,功能就是在屏幕上输出指定的信息。
return 是函数的返回值,根据函数类型的不同,返回的值也是不同的。
\n 是转义字符。
多行注释: /* 注释内容 */
单行注释: // 注释一行
C语言中,数据类型可分为:基本数据类型、构造数据类型、指针类型、空类型四大类。
给变量或者函数起的名字就是标识符,标识符可以是字母(A~Z, a~z)、数字(0~9)、下划线_组成的字符串,并且第一个字符必须是字母或者下划线。
在使用标识符时还要注意以下几点:
变量定义的一半形式:数据类型 变量名;
多个类型相同的变量:数据类型 变量名, 变量名, 变量名…;
变量的赋值:
1 | int num = 10; |
数据类型 | 说明 | 字节 | 应用 | 示例 |
---|---|---|---|---|
char | 字符型 | 1 | 用于存储单个字符 | char sex = ‘m’; |
int | 整型 | 2 | 用于存储整数 | int height = 18; |
float | 单精度浮点型 | 4 | 用于存储小数 | float price = 11.1; |
double | 双精度浮点型 | 8 | 用于存储位数更多的小数 | double pi = 3.1415926; |
C语言中不存在字符串变量,字符串只能存在于字符数组中。
格式化输出语句,也可以说是占位输出,是将各种类型的数据按照格式化后的类型及指定的位置从计算机上显示。
格式:printf(“输出格式符”, 输出项);
常用格式化符:
格式符 | 说明 |
---|---|
%d | 带符号十进制整数 |
%c | 单个字符 |
%s | 字符串 |
%f | 6位小数 |
输出多个变量,变量之间用逗号隔开
1 | int a = 10; |
C语言中常量可以分为直接常量和符号常量
直接常量也成为字面量,可以直接拿来使用,无需定义。
可以使用一个标识符来表示一个常量,称之为符号常量。符号常量在使用之前必须先定义,一般形式为:
#define 标识符 常量值
符号常量的标识符一般习惯使用大写字母,变量的标识符习惯使用小写字母,加以区分。
1 |
|
自动转换发生在不同数据类型运算时,在编译的时候自动完成。自动转换遵循的规则就好比小盒子可以放进大盒子里,下图表示自动转换规则:
char类型数据转换为int类型数据遵循ASCII码中的对应值
字节小的可以向字节大的自动转换,但字节大的不能向字节小的自动转换
1 |
|
强制类型转换是通过定义类型转换运算来实现的。一般形式为:
(数据类型)(表达式)
1 | double a = 6.777; |
使用强制类型转换应注意以下问题:
名称 | 运算符号 |
---|---|
加法 | + |
减法 | - |
乘法 | * |
除法 | / |
求余(模运算符) | % |
自增 | ++ |
自减 | -- |
除法运算中,如果相除的两个数都是整数的话,则结果也为整数,小数部分省略。两数中有一个为小数,结果则为小数。
求余运算,只适合用两个整数进行求余运算。运算后的符号取决于被模数的符号,如 (-10)%3 = -1,而 10%(-3) = 1.
C语言中的赋值运算符分为简单赋值运算符和复合赋值运算符。
简单赋值运算符:”=”
复合赋值运算符就是在简单赋值符 “=” 之前加上其他运算符构成,例如:+=、-=、*=、/=、%=
符号 | 意义 |
---|---|
> | 大于 |
>= | 大于等于 |
< | 小于 |
<= | 小于等于 |
== | 等于 |
!= | 不等于 |
关系表达式的值是“真”和“假”,在C程序用整数 1 和 0 表示。
符号 | 意义 |
---|---|
&& | 逻辑与 |
|| | 逻辑或 |
! | 逻辑非 |
格式:
表达式1 ? 表达式2 : 表达式3;
优先级别为 1 的最高,级别为 10 的最低。
1 | if(表达式1) |
1 | while(表达式) |
1 | do |
1 | for(表达式1; 表达式2; 表达式3) |
注意:
break 语句可以中断循环
continue 语句结束本次循环开始执行下一次循环
1 | switch(表达式) |
注意:
使用格式:
goto 语句标号;
其中语句标号是一个标识符,该标识符一般用英文大写并遵守标识符命名规则,这个标识符加上一个“:”一起出现在函数内某处,执行 goto 语句后,程序将跳转到该标号处并执行其后的语句。
1 |
|
C语言提供了大量的库函数,比如stdio.h提供输出函数。
调用形式:
函数名([参数]);
[数据类型说明] 函数名称(参数类型 参数名, …)
{
执行代码块;
return 表达式;
}
1 | int learn(int n) |
形参是定义函数和函数体时使用的参数,实参是在调用时传递给该函数的参数。
return 表达式 或者 return (表达式)
如果函数返回值的类型与函数定义中函数的类型不一致,则以函数返回类型为准,自动进行类型转换
在函数内部、复合语句内部定义的变量
C语言根据变量的生存周期来划分,可以分为静态存储方式和动态存储方式。
静态存储方式:是指在程序运行期间分配固定的存储空间的方式。静态存储区中存放了在整个程序执行过程中都存在的变量,如全局变量。
动态存储方式:是指在程序运行期间根据需要进行动态的分配存储空间的方式。动态存储区中存放的变量是根据程序运行的需要而建立和释放的,通常包括:函数形参、自动变量、函数调用时的现场保护和返回地址等。
C语言中存储类别又分为四类:自动(auto)、静态(static)、寄存器的(register)和外部的(extern)。
用关键字 auto 定义的变量为自动变量,auto 可以省略,auto不写则隐含定义为“自动存储类别”,属于动态存储方式。
1 | int fn(int a) |
用 static 修饰的为静态变量,如果定义在函数内部,称之为静态局部变量;如果定义在函数外部,称之为静态外部变量。
1 | int fn(int a) |
静态局部变量属于静态存储类别,在静态存储区内分配存储单元,在整个程序运行期间都不释放;静态局部变量在编译时赋初值,即只赋初值一次;如果在定义局部变量时不赋初值的话,则对静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符(对字符变量)。
1 | int fn(int a) |
只有局部自动变量和形参可以作为寄存器变量;一个计算机系统中的寄存器数目有限,不能定义任意多个寄存器变量;局部静态变量不能定义为寄存器变量。
1 |
|
在C语言中不能被其他源文件调用的函数称为内部函数,由 static 关键字来定义,又被称为静态函数。形式为:
static [数据类型] 函数名()
在C语言中能被其他源文件调用的函数称为外部函数,由 extern 关键字来定义。形式为:
extern [数据类型] 函数名()
在没有指定函数的作用范围时,默认为外部函数,因此 extern 可以省略。
]]>引入源文件
#include "test.c"
input
设置 lineHeight: 1;
, 可以使 input 输入框内用户输入的文本垂直居中,但是在 IOS 的 Safari 浏览器中查看, placeholder
提示文字垂直方向靠上,解决此问题,可以为该 input 设置 lineHeight: normal;
。]]>WebViewJavascriptBridge
。项目中最简单的一个交互需求,是在客户端打开 H5 页面后,页面上有一个后退按钮,可以退回到客户端页面。这个需求纯前端是无法做到的,前端必须调用 IOS 的退回方法。
H5 页面需要调用 IOS 端的方法,且不需要获取返回值时,可以很简单的使用 schema
的方式,而不需要通过第三方库来实现。具体方式是:
location.href="goback://"
或 iframe.src="goback://"
的方式发起请求当 H5 页面与 IOS 端交互比较复杂时,比如页面需要获取 IOS 端传回的返回值,或者 IOS 端需要调用 js 方法。
可以通过第三方库来实现,我们项目用的是 WebViewJavascriptBridge
。下面介绍我怎么在 vue 项目中使用 WebViewJavascriptBridge
WebViewJavascriptBridge GitHub 地址
将以下代码拷贝到 bridge.js 文件中
1 | function setupWebViewJavascriptBridge (callback) { |
在 main.js 中引入该文件
1 | import Bridge from './config/bridge.js' |
在需要调用客户端方法的组件中(事先需要与客户端同事约定好方法名)
1 | this.$bridge.callhandler('ObjC Echo', params, (data) => { |
当客户端需要调用 js 函数时,事先注册约定好的函数即可
1 | this.$bridge.registerhandler('JS Echo', (data, responseCallback) => { |
最近项目需要做一个通讯录页面,并且要仿照 iPhone 原生的通讯录,其中有一个功能,即滑动右侧的英文字母列表,通讯录列表要相应滑动到对应的位置。如下图所示:
右侧字母列表的代码如下(vue 实现):
1 | <ul class="letter-list" @touchmove.prevent="scrollToLetter()"> |
如代码所示,我想要使用事件委托,为 ul 绑定 touchmove 事件,通过 e.changedTouches[0].target.id
,可以获取当前触摸到的字符,并将联系人列表移动到相应字符的位置。
然而,当我这样写完,测试时发现效果不对,联系人列表只能滑动到 touchstart 时对应的字符位置,通过 log 打印发现,e.changedTouches[0].target
获取到的永远是 touchstart 时的 target,所有这种方式获取 target 是错误的。
通过上网查阅资料,最终通过以下方式获取到了正确的 target:
1 | const touch = event.changedTouches[0] |
document.elementFromPoint
方法的浏览器兼容性如下:
Feature | Chrome | Firefox (Gecko) | Internet Explorer | Opera | Safari (WebKit) |
---|---|---|---|---|---|
Basic support | 53.0 | ? | ? | ? |
Feature | Android | Android Webview | Firefox Mobile (Gecko) | Firefox OS | IE Mobile | Opera Mobile | Safari Mobile | Chrome for Android |
---|---|---|---|---|---|---|---|---|
Basic support | 未实现 | 53.0 | ? | ? | ? | ? | 53.0 |
Symbol
是ES6引入的一种新的原始数据类型,表示独一无二的值。
Symbol 值通过 Symbol 函数生成。Symbol 函数前不能使用 new
命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。
1 | let a = Symbol() |
Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
如果 Symbol 的参数是一个对象,就会调用该对象的 toString 方法,将其转为字符串,然后才生成一个 Symbol 值。
1 | let a = Symbol('haha') |
注意,Symbol 函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的 Symbol 函数的返回值是不相等的。
1 | let a = Symbol('zhuang') |
Symbol 值不能与其他类型的值进行运算,会报错。
1 | let a = Symbol('zhuang') |
Symbol 值可以显式转为字符串。
1 | console.log(String(a)); // Symbol(zhuang) |
Symbol 值也可以转为布尔值,但是不能转为数值。
1 | console.log(Boolean(a)); // true |
Symbol 值可以作为对象的属性名。由于每一个 Symbol 值都是不相等的,这样就可以保证不会出现同名的属性名。
1 | let a = Symbol('a') |
注意,Symbol 值作为对象属性名时,不能用点运算符。
Symbol 作为属性名,该属性不会出现在 for…in、for…of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols
方法,可以获取指定对象的所有 Symbol 属性名。
Object.getOwnPropertySymbols
方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
1 | let a = Symbol('a') |
另一个新的 API,Reflect.ownKeys
方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
1 | console.log(Reflect.ownKeys(obj)); // ["b", Symbol(a)] |
有时,我们希望重新使用同一个 Symbol 值,Symbol.for
方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
Symbol.for() 与 Symbol() 这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for() 不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的 key 是否已经存在,如果不存在才会新建一个值。
1 | let s1 = Symbol.for('zhuang') |
Symbol.keyFor
方法返回一个已登记的 Symbol 类型值的key。
1 | let s1 = Symbol.for('zhuang') |
]]>上面代码中,变量 s3 属于未登记的 Symbol 值,所以返回 undefined。
1 | function move({x, y}) { |
但是,以上写法,当函数调用没有传递参数时,就会报错:
1 | move(); // Cannot destructure property `x` of 'undefined' or 'null'. |
所以,当参数使用解构赋值时,需要为参数设置一个默认值。上面的函数改写为:
1 | function move({x, y} = {}) { |
如下函数 move,接受一个对象为参数,并被解构为变量 x 和 y。变量 x 和 y 使用默认值,可以写成如下形式:
1 | function move({x=0, y=0} = {}) { |
调用该函数:
1 | move({x:1, y:2}); // [1, 2] |
需要注意的是,ES6 内部使用严格相等运算符(===),判断一个位置是否有值,只有一个解构的成员严格等于 undefined,才会触发默认值。
1 | move({x: undefined}); // [0, 0] |
主要分析 Vue 数据代理、模板解析、数据绑定等方面,配合一些代码,简单实现 Vue 基本功能。
注意:本文并没有直接参考 Vue 源码,参考源码为:https://github.com/DMQ/mvvm
Vue 中,配置对象中的 data 对象中的数据,保存在 vm 对象的 $data
属性中,由 vm 对象进行代理。
创建如下 vm 实例:1
2
3
4
5
6let vm = new Vue({
el: '#app',
data: {
msg: 'cencen'
}
})
通过 vm 代理读取 $data
中的数据:1
console.log(vm.msg); // cencen
通过 vm 代理更改 $data
中的数据:1
2vm.msg = '岑大王';
console.log(vm.$data.msg); // 岑大王
模拟实现原理
将传入的选项对象中的 data 属性值,保存在 vm 实例的 $data
属性中
遍历 data 对象的所有属性,添加到 vm 实例上。
在 vm 上定义新的属性时,使用访问描述符
$data
属性对象上对应的属性值$data
对象上对应的属性。1 | function Vue(option){ |
Vue 使用模板,实现在页面上使用 model 中的数据。
模板解析,就是操作页面上的节点,按照相应的规则解析所使用的模板语法,并显示出理想的页面。
解析模板时,为了防止多次操作页面上的节点,造成过多的重绘重排,可以采用批量更新的方法:
这样对于页面来说,只进行了移出所有子元素和插入编译好的代码片段两次重排操作。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32function Compile(el, vm) {
this.$vm = vm;
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el); // 移出
this.init(); // 解析 fragment
this.$el.appendChild(this.$fragment); // 插入
}
}
Compile.prototype = {
node2Fragment: function(el) {
// 将 el 所有子节点取出,放入暂存元素
let childStr = el.innerHTML;
el.innerHTML = '';
let tempEl = document.createElement('div');
tempEl.innerHTML = childStr;
// 创建fragment
let fragment = document.createDocumentFragment();
let child;
// 遍历暂存元素,将所有子节点放入fragment
while(child = tempEl.firstChild){
fragment.appendChild(child);
}
return fragment;
},
isElementNode: function(node) {
return node.nodeType == 1;
}
}
在 fragment 中解析模板时,需要遍历 fragment 的所有子节点,根据节点类型,具有不同的解析方式。
fragment.chileNodes
node.nodeType
/\{\{(.*)\}\}/
exp = RegExp.$1
获取表达式名vm[exp]
后,设置到该文本节点的 textContentif(node.childNodes && node.childNodes.length)
,递归解析该子节点的所有子节点1 | Compile.prototype = { |
attrs = node.attributes
[].slice.call(attrs).forEach(function(attr){})
attrName = attr.name
exp = attr.value
attrName.indexOf('v-') == 0
direcName = attrName.substring(2)
direcName.indexOf('on') === 0
eventType = direcName.split(':')[1]
callback = vm.$options.methods[exp]
node.addEventListener(eventType, callback.bind(vm), false)
v-text
:操作节点 textContentv-model
:操作节点 valuev-html
:操作节点 innerHTMLv-class
:操作节点 classNamenode.removeAttribute(attrName)
1 | Compile.prototype = { |
数据绑定(model => view),一旦更新了 data 中的数据,页面中直接或间接使用了该属性的节点都会更新。
vue.js 采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()
来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
Vue 通过数据劫持实现数据绑定,最核心的方法就是Object.defineProperty()
,在属性的 getter 方法中,将数据与页面中使用了该数据的节点进行绑定,在 setter 方法中,监视数据变化,当数据发生了变化,通知绑定了该数据的页面节点进行更新。
实现过程中,比较重要的几点:
定义 observe
方法,传入一个参数,判断参数如果是对象,则调用 Observer 构造函数,监视该对象所有属性。这里传入 data,监视 data 中所有属性。
Observer
构造函数中,遍历 data 所有属性,进行如下操作:
observe
方法,传入当前属性,若该属性值是对象,则可以实现监视 data 任意层次数据defineProperty()
为 data 重新定义所有属性,定义 getter/setter 方法,实现数据劫持。getter
方法,用于获取值。当一个 watcher 获取值时,getter 方法会判断当前 dep 和 watcher 是否建立了数据订阅关系,如果没有,则在当前属性的 dep 对象的 subs 属性中,存储该 watcher,并在 watcher 对象的 depIds 属性中存储当前 dep。setter
方法,用于监视当前属性数据变化,当数据发生改变,则通知该属性的 dep,dep 通知 subs 属性中所有 watcher,watcher 则触发绑定的回调函数,更新视图。data 对象中每个层次的属性,都对应一个 dep 对象。
Compile 解析模板的过程,在第二章已经分析过,这里需要分析一下 Compile 解析模板过程中,是如何实现订阅数据变化的。
模板解析过程中,当解析表达式({ {…} })和元素节点的非事件指令(v-model、v-text、v-html、v-class等)时,将该模板替换成数据显示到页面后,会调用 Wacther 构造函数,为该节点创建一个 watcher 对象,并为该 watcher 对象绑定一个更新该节点视图的回调函数。
watcher 对象在编译模板的过程中被创建,作为 data 中的数据和视图页面的桥梁。
页面中每一个表达式、元素非事件指令,都对应一个 watcher 对象。
watcher 对象中包含如下属性:
由于 watcher 对象中存储了模板对应表达式的值,所以创建 watcher 对象时,会调用该表达式的各级属性的 getter 方法来获取当前值。在 Observer 中,已经介绍过,getter 方法会判断当前 watcher 对象的 depIds 属性中,是否包含该数据的 dep 对象,若没有,则会分别在 dep 对象的 subs 属性存储当前 watcher 对象,在 watcher 对象的 depIds 属性中,存储该数据的 dep 对象。
watcher 对象中,存储了更新该对象对应的页面节点的回调函数,并且在相应表达式的各级属性中订阅了数据变化的通知。
当数据发生变化时,由于数据劫持,在 setter 方法中,会通过该数据对应的 dep 对象,通知所有订阅了该数据变化的 watcher,watcher 对象则调用存储的回调函数,更新视图。
前面介绍了 model => view 的数据绑定,Vue 通过 v-model
指令实现了 view => model 的数据绑定。
当解析 v-model
指令时,会给当前元素添加 input
监听事件,当元素的值发生改变时,会将最新的值赋给当前表达式对应的 data 属性。
1 | model: function(node, vm, exp) { |
在介绍 Socket.io 之前,首先需要说一说什么是 WebSocket。
详细了解参考:
我们知道,在 HTML5 之前,客户端和服务器通过 HTTP 协议交换数据,但是,HTTP 协议具有两个特点:
HTTP 协议是一种单向的网络协议。在建立连接后,它只允许客户端 Browser/UA (User Agent) 向服务器 WebServer 发送请求后,WebServer 才能返回相应的数据。而 WebServer 不能主动推送数据给 Browser/UA。
HTTP 协议是无状态的。客户端向服务器发送连接请求中会包含 identity info(鉴别信息),每次当一个连接结束时,服务器就会将这些鉴别信息丢掉,客户端再次发送 HTTP 请求时,就需要重新发送这些信息。
现在,假设我们需要开发一个基于 Web 的应用程序,需要获取服务器的实时数据,比如股票的实时行情、聊天室的聊天内容等,这就需要客户端和服务器之间反复进行 HTTP 通信,客户端不断发送请求,去获取当前的实时数据。下面介绍两种常见的方式:
ajax 轮询
ajax 轮询的原理非常简单,就是让浏览器定时(隔几秒)向服务器发送一次请求,询问是否有新的数据,如果有就返回最新数据,浏览器接收到后将最新数据显示出来,然后重复这一过程。
Long Polling
Long Polling 的原理与 ajax 轮询的原理差不多,都是采用轮询的方式,它是 Polling 的一种改进。客户端发送请求到服务器后,服务器并不立即响应客户端,而是保持住这次连接,当有新的数据时,才返回给客户端,客户端接收到数据,进行展示,再立即发送一个新的请求给服务器,并重复这个过程。如果服务器的数据长期没有更新,一段时间后,这个请求就会超时,客户端收到超时消息后,再立即发送一个新的请求给服务器。
从上面可以看出,这两种方式都需要不断的建立 HTTP 连接,然后等待服务器处理。
ajax 轮询假如某段时间内服务器没有更新的数据,但是客户端仍然需要定时发送请求,服务器再把以前的老数据返回过来,客户端拿到老数据,再把没有变化的数据再显示出来,即这段时间内,客户端和服务器会定时交换不变的数据信息,这样既浪费了带宽,又浪费了 CPU 的利用率。
Long Polling 虽然解决了带宽和 CPU 利用率的问题,但是如果服务器的数据更新的过快,服务器在返回给客户端一次数据包之后,只能等待客户端再次发送一次请求来之后,才能发送下一个数据包给客户端。在服务器两次返回数据之间,需要等待客户端接收到数据之后处理数据的时间,以及客户端再次发送连接请求后,服务器验证客户端的鉴别信息,并成功建立连接的时间,在网络拥塞的情况下,这个应该是用户不能接受的。
另外,由于 HTTP 协议是无状态的,每次建立连接都需要重新传输 identity info(鉴别信息),这些信息不仅浪费处理时间,而且在网络传输中会耗费大量的流量,往往比实际需要传输的数据量还要大。这样的数据包在网络上周期性的传输,对网络带宽也是一直浪费。
在这样的情况下,假如客户端能有一种新的网络协议,可以支持客户端和服务器的双向通信的就好了。于是,WebSocket 应运而生。
WebSocket 是 HTML5 新增的一种通信协议。WebSocket 协议是一种持久化的双向通信协议,它建立在TCP之上,同 HTTP 一样通过 TCP 来传输数据,但是它和 HTTP 最大的不同有两点:
WebSocket 是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/UA 都能主动的向对方发送或接收数据,就像 Socket 一样,不同的是 WebSocket 是一种建立在 Web 基础上的一种简单模拟 Socket 的协议。
WebSocket 需要通过握手连接,类似于 TCP 它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。因为 WebSocket 连接本质上就是一个 TCP 连接,所以在数据传输的稳定性和数据传输量的大小方面,和传统轮询以技术比较,具有很大的性能优势。
为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息 “Upgrade: WebSocket” 表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。
下面是一个典型的 WebSocket 发送请求和响应请求的例子:
浏览器向服务器发起 WebSocket 请求:1
2
3
4
5
6
7
8GET / HTTP/1.1
Connection:Upgrade
Host:127.0.0.1:8088
Origin:null
Sec-WebSocket-Extensions:x-webkit-deflate-frame
Sec-WebSocket-Key:puVOuWb7rel6z2AVZBKnfw==
Sec-WebSocket-Version:13
Upgrade:websocket
这个请求与普通的 HTTP 请求有一些区别
Upgrade: websocket
Connection: Upgrade
表示请求的目的就是要将客户端和服务器端的通讯协议从 HTTP 协议升级到 WebSocket 协议
Sec-WebSocket-Key:
Sec-WebSocket-Extensions:
Sec-WebSocket-Version:
客户端浏览器需要向服务器端提供的握手信息,服务器端解析这些头信息。Sec-WebSocket-Key 是一个 Base64 encode的值,这个是浏览器随机生成的,Sec-WebSocket-Version 是告诉服务器所使用的 Websocket Draft(协议版本)。
服务器返回:
服务器端返回以下信息,以表明服务器端获取了客户端的请求,同意创建 WebSocket 连接。1
2
3
4
5
6
7
8HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Server:beetle websocket server
Upgrade:WebSocket
Date:Mon, 26 Nov 2013 23:42:44 GMT
Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:content-type
Sec-WebSocket-Accept:FCKgUr8c7OsDsLFeJTWrJw6WO8Q=
Upgrade: websocket
Connection: Upgrade
告诉客户端即将升级的是Websocket协议
Sec-WebSocket-Accept
这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key。
从握手的协议可以看出,如果我们要使用 WebSocket,我们需要一个实现 WebSocket 协议规范的服务器,这不在我们讨论的范围。
值得一提的是:WebSocket 是可以和 HTTP 共用监听端口的,也就是它可以公用端口完成 socket 任务。
WebSocket 与 HTTP 协议一样都是基于 TCP 的,所以他们都是可靠的协议,Web 开发者调用的 WebSocket 的 send 函数在 browser 的实现中最终都是通过 TCP 的系统接口进行传输的。
WebSocket 和 Http 协议一样都属于应用层的协议,那么他们之间有没有什么关系呢?答案是肯定的,WebSocket 在建立握手连接时,数据是通过 HTTP 协议传输的,正如我们上面所看到的 “GET/chat HTTP/1.1”,这里面用到的只是 HTTP 协议一些简单的字段。但是在建立连接之后,真正的数据传输阶段是不需要 HTTP 协议参与的。
1 | var ws = new WebSocket(“ws://echo.websocket.org”); |
上面的 JavaScript 代码中,调用了 WebSocket 的 API。
创建一个 WebSocket 对象,需要调用 WebSocket 的构造函数,并传入需要连接的服务器地址。WebSocket 的 URL 以 ws:// 开头。
WebSocket 对象具有4个事件:
Desktop
Feature | Chrome | Edge | Firefox (Gecko) | Internet Explorer | Opera | Safari |
---|---|---|---|---|---|---|
Version -76 support | 6 | No support | 4.0 (2.0) | No support | 11.00 (disabled) | 5.0.1 |
Protocol version 7 support | No support | No support | 6.0 (6.0)Moz | No support | No support | No support |
Protocol version 10 support | 14 | No support | 7.0 (7.0)Moz | HTML5 Labs | ? | ? |
Standard - RFC 6455 Support | 16 | (Yes) | 11.0 (11.0) | 10 | 12.10 | 6.0 |
Usable in Workers | (Yes) | (Yes) | 37.0 (37.0) | ? | ? | ? |
Mobile
Feature | Android | Edge | Firefox Mobile (Gecko) | IE Mobile | Opera Mobile | Safari Mobile |
---|---|---|---|---|---|---|
Version -76 support | ? | No support | ? | ? | ? | ? |
Protocol version 7 support | ? | No support | ? | ? | ? | ? |
Protocol version 8 support (IETF draft 10) | ? | No support | 7.0 (7.0) | ? | ? | ? |
Standard - RFC 6455 Support | 4.4 | (Yes) | 11.0 (11.0) | ? | 12.10 | 6.0 |
Usable in Workers | (Yes) | (Yes) | 37.0 (37.0) | ? | ? | ? |
Socket.io 用于浏览器与 Node.js 之间实现实时通信。
在写这篇文章之前,我只是使用了 Socket.io,但对于它却并不是很了解,之前我一直认为 Socket.io 就是对 WebSocket 协议的实现。事实上,这种看法并不完全正确。
Socket.io 是一个完全由 JavaScript 实现、基于 Node.js、支持 WebSocket 协议的用于实时通信、跨平台的开源框架,它包括了客户端的 JavaScript 和服务器端的 Node.js。
Socket.io 设计的目标是支持任何的浏览器,任何 Mobile 设备。支持主流的 PC 浏览器 (IE,Safari,Chrome,Firefox,Opera等),Mobile 浏览器(iphone Safari/ipad Safari/Android WebKit/WebOS WebKit等)。
但是,WebSocket 协议是 HTML5 新推出的协议,浏览器对它的支持并不完善,由此可以看出,Socket.io 不可能仅仅是对 WebSocket 的实现,它还支持其他的通信方式,如上面介绍过的 ajax 轮询和 Long Polling。根据浏览器的支持程度,自主选择使用哪种方式进行通讯。
Socket.io 支持的通信方式:
node 端使用 express 框架
服务器端:npm install --save socket.io
浏览器端(引入本地文件):<script src="/socket.io/socket.io.js"></script>
浏览器端(CDN 加速):<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"> </script>
1 | var app = require('express')(); |
Socket.IO 提供了默认事件(如:connect, message, disconnect)。另外,Socket.IO允许发送并接收自定义事件。
监听客户端连接,回调函数会传递本次连接的socket
io.on(‘connection’,function(socket){ });
给所有客户端广播消息
io.sockets.emit(‘String’,data);
给指定的客户端发送自定义事件
socket.emit(‘String’, data);
io.sockets.socket(socketid).emit(‘String’, data);
接收客户端发送的自定义事件
socket.on(‘String’,function(data));
给除了自己以外的客户端广播消息
socket.broadcast.emit(“msg”, data);
房间是 Socket.IO 提供的一个非常好用的功能。房间相当于为指定的一些客户端提供了一个命名空间,所有在房间里的广播和通信都不会影响到房间以外的客户端。
使用 join() 方法将 socket 加入房间:1
2
3
4
5
6
7
8io.on('connection', function(socket){
socket.on('group1', function (data) {
socket.join('group1');
});
socket.on('group2',function(data){
socket.join('group2');
});
});
使用 leave() 方法离开房间:
socket.leave(‘some room’);
向房间中除了当前 socket 的其他 socket 发送消息
socket.broadcast.to(‘group1’).emit(‘event_name’, data);
broadcast方法允许当前socket client不在该分组内
向房间中所有的 socket 发送消息
io.sockets.in(‘group1’).emit(‘event_name’, data);
获取连接的客户端 socket1
2
3io.sockets.clients().forEach(function (socket) {
//.....
})
获取所有房间(分组)信息
io.sockets.manager.rooms
来获取此socketid进入的房间信息
io.sockets.manager.roomClients[socket.id]
获取particular room中的客户端,返回所有在此房间的socket实例
io.sockets.clients(‘particular room’)
通过命名空间可以为 Socket.IO 设置子程序。默认命名空间为 “/”,Socket.IO 默认连接该路径。
使用 of() 函数可以自定义命名空间。
1 | var chat = io.of('/chat'); |
建立一个 socket 连接
var socket = io.connect( window.location.protocol + ‘//‘ + window.location.host);
或
var socket = io( window.location.protocol + ‘//‘ + window.location.host);
建立有命名空间的 socket 连接
var chat = io.connect( window.location.protocol + ‘//‘ + window.location.host + ‘/chat’);
监听服务器消息1
2
3socket.on('msg',function(data){
console.log(data);
});
socket.on(“String”,function(data){}) 监听服务端发送的消息, String 参数与服务器端 socket.emit(‘String’, data) 第一个参数 String 相同。
向服务器发送消息
socket.emit(‘msg’, data);
监听 socket 断开与重连
1 | socket.on('disconnect', function() { |
客户端 socket.on() 监听的事件
流程:
node 端代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18var express = require('express');
var app = express();
var server = require('http').Server(app);
var io = require('socket.io')(server);
io.sockets.on('connection', function(socket) {
// 监听客户端发送的 chat 事件
socket.on('chat', function (chatinfo) {
// 向当前 socket 发送聊天信息
socket.emit('chat', chatinfo);
// 向除了当前 socket 外的所有 socket 发送聊天信息
socket.broadcast.emit('chat', chatinfo);
});
});
server.listen(3000, function() {
console.log('App listening on port 3000!');
});
HTML 代码:1
2
3
4
5
6
7
8<div id="chat">
<ul id="chatList">
</ul>
<form>
<input type="text" name="chatContent" id="chatContent" />
<input type="button" id="sendChatContent" value="发送" />
</form>
</div>
浏览器 socket 代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 建立 socket 连接
var url = window.location.protocol+'//'+window.location.host;
socket = io.connect(url);
// 点击“发送”,向服务器发送聊天信息
$('#sendChatContent').click(function (ev) {
var username = $('#username').text();
var chatContent = $('#chatContent').val().trim();
if(!chatContent){
return;
}
if(socket){
// 向服务器 chat 事件,发送信息
socket.emit('chat', {username: username, chatContent: chatContent});
}
$('#chatContent').val('');
});
// 监听服务器发送来的 chat 事件
socket.on('chat', function (chatinfo) {
$('#chatList').append('<li><span class="chatusername">' + chatinfo.username + '</span>:<span class="chatcontent">' + chatinfo.chatContent + '</span></li>');
$('#chatList').scrollTop(10000);
});
beforeCreate
数据代理
数据绑定
created => 异步任务(定时器、ajax、事件监听)
编译模板
beforeMount
批量更新到挂载元素
mounted => 异步任务(定时器、ajax、事件监听)
更新数据
beforeUpdate
重新渲染虚拟 DOM
updated
vm.$destroy()
beforeDestroy => 清除定时器
destroyed
指令:自定义元素属性
Vue 预定义了一些指令,也可以自定义
1 | Vue.directive('my-directive', function(el, binding){ |
1 | directives: { |
注册指令时,指令名不用写 v-
,但是使用指令时,必须添加上。1
<div v-my-directive="msg"></div>
进程
线程
关系
多进程:一应用程序可以同时启动多个实例运行。
多线程:在一个进程内, 同时有多个线程运行。
比较 | 单线程 | 多线程 |
---|---|---|
优点 | 顺序编程简单易懂 | 能有效提升CPU的利用率 |
缺点 | 效率低 | 创建多线程开销 线程间切换开销 死锁与状态同步问题 |
如何查看浏览器是否是多进程运行的呢? == 任务管理器==>进程
浏览器都是多线程运行的
浏览器内核(browser core)是支持浏览器运行的最核心的程序。
浏览器 | 内核 |
---|---|
Chrome、Safari | webkit |
firefox | Gecko |
IE | Trident |
360、搜狗等国内浏览器 | Trident + webkit (双内核) |
JavaScript 的执行是单线程的。所有的 JavaScript 代码,包括回调代码,最终都会在执行栈(execution stack)中执行。
JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。
浏览器将代码分为两类:
模型的2个重要组成部分
事件循环模型的运转流程
通过上面介绍过的事件循环运转流程,可以得知,定时器(setTimeout)回调函数只有在执行栈中的初始化代码全部执行完后才执行。因此,定时器并不能保证真正定时执行,如果在主线程在启动定时器之后执行了一个长时间的操作(时间超过定时器设置的时间),就会导致定时器回调函数延时处理。
传统 JavaScript 是单线程运行的,HTML5 新推出了一个 Web Worker 接口,可以实现在分线程中执行一个单独的 js 文件。
Web Workers是一种机制,通过它可以使一个脚本操作在与Web应用程序的主执行线程分离的后台线程中运行。这样做的优点是可以在单独的线程中执行繁琐的处理,让主(通常是UI)线程运行而不被阻塞/减慢。
一个 worker 是使用构造函数创建的一个对象(例如,Worker()), 运行一个命名的 JavaScript文件 — 这个文件包含了将在 worker 线程中运行的代码,并且 worker 在与当前 window 不同的另一个全局上下文中运行。这个上下文由专用worker的情况下的一个 DedicatedWorkerGlobalScope 对象表示(标准 workers 由单个脚本使用; 共享 workers 使用 SharedWorkerGlobalScope )。
在 worker 线程中可以运行任意的代码,以下情况除外:不能直接在 worker 线程中操纵 DOM 元素, 或者使用某些 window 对象中默认的方法和属性。 但是 window 对象中很多的方法和属性是可以使用的,包括 WebSockets,以及诸如 IndexedDB 和 FireFox OS 中独有的 Data Store API 这一类数据存储机制。
主线程和 worker 线程之间通过这样的方式互相传输信息:两端都使用 postMessage() 方法来发送信息, 并且通过 onmessage 这个 event handler 来接收信息。 (传递的信息包含在 Message 这个事件的数据属性内) 。数据的交互是通过传递副本,而不是直接共享数据。
一个 worker 可以生成另外的新的 worker,这些 worker 的宿主和它们父页面的宿主相同。 此外,worker 可以通过 XMLHttpRequest 来访问网络,只是 XMLHttpRequest 的 responseXML 和 channel 这两个属性将总是 null 。
实现效果
在输入框中输入一个数字 n,点击按钮,得到斐波那契数列中第 n 个数字的值。
分析
当数字 n 的值较大时,计算结果耗用时间比较长,如果计算的过程在主线程执行,则这段时间内页面将无法操作。
这种情况下,可以将计算的过程放在一个分线程中执行,主线程则可以继续执行其他代码,不会导致页面无法操作。当分线程得到结果之后,再将数据返回给主线程,主线程接收到数据,再进行处理。
代码
页面(主线程)代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<body>
<input type="text" id="number" value="30">
<button id="btn2">分线程计算fibonacci值</button>
<script type="text/javascript">
let number = document.getElementById('number');
let btn2 = document.getElementById('btn2');
btn2.onclick = ev => {
let n = number.value * 1;
let worker = new Worker('worker.js');
console.log('主线程向子线程发送消息');
worker.postMessage(n);
worker.onmessage = event => {
console.log('主线程接受到子线程发来的消息');
alert(event.data);
}
}
</script>
</body>
worker.js文件(分线程)代码1
2
3
4
5
6
7
8
9
10
11function fibonacci(n) {
return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2);
}
var onmessage = event => {
let n = event.data;
console.log('子线程接收到主线程发送的消息');
let result = fibonacci(n);
postMessage(result);
console.log('子线程向主线程发送消息');
}
概念
1、在Node.js语言中,包和模块并没有本质的不同,包是在模块的基础上更深一步的抽象。
2、包将某个独立的功能封装起来,用于发布、更新、依赖管理和进行版本控制。
3、Node.js根据 CommonJS 规范实现了包机制,开发了 npm 来解决包的发布和获取需求。
包的说明文件(package.json)
package.json属性详解
本质:json对象
1 | { |
扩展:
1 | "jquery": "^3.2.1" -----向上的尖括号可以管理二级,三级版本 |
1、Node 包管理器 (npm) 是一个由 Node.js 官方提供的第三方包管理工具,
2、npm 是一个完全由 JavaScript 实现的命令行工具,通过 Node.js 执行,因此严格来讲它不属于 Node.js 的一部分。
3、在最初的版本中,我们需要在安装完 Node.js 以后手动安装npm。
但从Node.js 0.6开始,npm已包含在发行包中了,安装Node.js时会自动安装npm。
现在的版本大都使用6.0以上。。。
使用npm命令来下载依赖模块及对项目包(模块)进行管理
常用命令:
使用npm导致的问题(更多的是针对5.0以下版本)
2017年5月30日发布node 8.0,其中自带的npm也由3.xxx版本升级到5.0
npm5变化: