好的,看到?家這么熱情,那班班接下來就化身Johnson
本文將研究 ES6 的 for ... of
循環。
在過去,有兩種方法可以遍歷 javascript。
首先是經典的 for i
循環,它使你可以遍歷數組或可索引的且有 length
屬性的任何對象。
for(i=0;i<things.length;i++) { var thing = things[i] /* ... */ }
其次是 for ... in
循環,用于循環一個對象的鍵/值對。
for(key in things) { if(!thing.hasOwnProperty(key)) { continue; } var thing = things[key] /* ... */ }
for ... in
循環通常被視作旁白,因為它循環了對象的每一個可枚舉屬性。這包括原型鏈中父對象的屬性,以及被分配為方法的所以屬性。換句話說,它遍歷了一些人們可能想不到的東西。使用 for ... in
通常意味著循環塊中有很多保護子句,以避免出現不需要的屬性。
早期的 javascript 通過庫解決了這個問題。許多 JavaScript庫(例如:Prototype.js,jQuery,lodash 等)都有類似 each
或 foreach
這樣的工具方法或函數,可讓你無需 for i
或 for ... in
循環去遍歷對象和數組。
for ... of
循環是 ES6 試圖不用第三方庫去解決其中一些問題的方式。
for ... of
循環
for(const thing of things) { /* ... */ }
它將遍歷一個可迭代(iterable)對象。
可迭代對象是定義了 @@ iterator 方法的對象,而且 @@iterator 方法返回一個實現了迭代器協議的對象,或者該方法是生成器函數。
在這句話中你需要理解很多東西:
@@
是什么意思?)
下面逐個解決這些疑問。
首先,javascript 對象中的一些內置對象天然的可以迭代,比如最容易想到的就是數組對象??梢韵裣旅娴拇a中一樣在 for ... of
循環中使用數組:
const foo = [ 'apples','oranges','pears' ] for(const thing of foo) { console.log(thing)
}
輸出結果是數組中的所有元素。
apples oranges
pears
還有數組的 entries
方法,它返回一個可迭代對象。這個可迭代對象在每次循環中返回鍵和值。例如下面的代碼:
const foo = [ 'apples','oranges','pears' ] for(const thing of foo.entries()) { console.log(thing)
}
將輸出以下內容
[ 0, 'apples' ]
[ 1, 'oranges' ]
[ 2, 'pears' ]
當用下面的語法時,entries
方法會更有用
const foo = [ 'apples','oranges','pears' ] for(const [key, value] of foo.entries()) { console.log(key,':',value)
}
在 for 循環中聲明了兩個變量:一個用于返回數組的第一項(值的鍵或索引),另一個用于第二項(該索引實際對應的值)。
一個普通的 javascript 對象是不可迭代的。如果你執行下面這段代碼:
// 無法正常執行 const foo = { 'apples':'oranges', 'pears':'prunes' } for(const [key, value] of foo)
{ console.log(key,':',value)
}
會得到一個錯誤
$ node test.js
/path/to/test.js:6 for(const [key, value] of foo) {
TypeError: foo is not iterable
然而全局 Object
對象的靜態 entries
方法接受一個普通對象作為參數,并返回一個可迭代對象。就像這樣的程序:
const foo = { 'apples':'oranges', 'pears':'prunes' } for(const [key, value] of Object.entries(foo))
{ console.log(key,':',value)
}
能夠得到你期望的輸出:
$ node test.js
apples : oranges
pears : prunes
如果你想創建自己的可迭代對象,則需要花費更多的時間。你會記得前面說過:
可迭代對象是定義了 @@ iterator 方法的對象,而且 @@iterator 方法返回一個實現了迭代器協議的對象,或者該方法是生成器函數。
搞懂這些內容的最簡單方法就是一步一步的去創建可迭代對象。首先,我們需要一個實現 @@iterator 方法的對象。 @@
表示法有點誤導性,我們真正要做的是用預定義的 Symbol.iterator
符號定義方法。
如果用迭代器方法定義對象并嘗試遍歷:
const foo = {
[Symbol.iterator]: function() {
}
} for(const [key, value] of foo) { console.log(key, value)
}
得到一個新錯誤:
for(const [key, value] of foo) {
^
TypeError: Result of the Symbol.iterator method is not an object
這是 javascript 告訴我們它在試圖調用 Symbol.iterator
方法,但是調用的結果不是對象。
為了消除這個錯誤,需要用迭代器方法來返回實現了迭代器協議的對象。這意味著迭代器方法需要返回一個有 next
鍵的對象,而 next
鍵是一個函數。
const foo = {
[Symbol.iterator]: function() { return { next: function() {
}
}
}
} for(const [key, value] of foo) { console.log(key, value)
}
如果運行上面的代碼,則會出現新錯誤。
for(const [key, value] of foo) {
^
TypeError: Iterator result undefined is not an object
這次 javascript 告訴我們它試圖調用 Symbol.iterator
方法,而該對象的確是一個對象,并且實現了 next
方法,但是 next
的返回值不是 javascript 預期的對象。
next
函數需要返回有特定格式的對象——有 value
和 done
這兩個鍵。
next: function() { //... return { done: false, value: 'next value' }
}
done
鍵是可選的。如果值為 true
(表示迭代器已完成迭代),則說明迭代已結束。
如果 done
為 false
或不存在,則需要 value
鍵。 value
鍵是通過循環此應該返回的值。
所以在代碼中放入另一個程序,它帶有一個簡單的迭代器,該迭代器返回前十個偶數。
class First20Evens { constructor() { this.currentValue = 0 }
[Symbol.iterator]() { return { next: (function() { this.currentValue+=2 if(this.currentValue > 20) { return {done:true}
} return { value:this.currentValue
}
}).bind(this)
}
}
} const foo = new First20Evens; for(const value of foo) { console.log(value)
}
手動去構建實現迭代器協議的對象不是唯一的選擇。生成器對象(由生成器函數返回)也實現了迭代器協議。上面的例子用生成器構建的話看起來像這樣:
class First20Evens { constructor() { this.currentValue = 0 }
[Symbol.iterator]() { return function*() { for(let i=1;i<=10;i++) { if(i % 2 === 0) { yield i
}
}
}()
}
} const foo = new First20Evens; for(const item of foo) { console.log(item)
}
本文不會過多地介紹生成器,如果你需要入門的話可以看這篇文章。今天的重要收獲是,我們可以使自己的 Symbol.iterator
方法返回一個生成器對象,并且該生成器對象能夠在 for ... of
循環中“正常工作”。 “正常工作”是指循環能夠持續的在生成器上調用 next
,直到生成器停止 yield
值為止。
$ node sample-program.js 2 4 6 8
10
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
Flutter 構建模式
目前,Flutter一共提供了三種運行模式,分別是Debug、Release和Profile模式。其中,Debug模式主要用在軟件編寫過程中,Release模式主要用于應用發布過程中,而Profile模式則主要用于應用性能分析時,每個模式都有自己特殊的使用場景。下面簡介介紹下這幾種模式:
Debug模式
Debug模式又名調試模式,Debug模式可以同時在物理設備、仿真器或者模擬器上運行應用。默認情況下,使用flutter run命令運行應用程序時就是使用的Debug模式。在Debug模式下,所有的斷言、服務擴展是開啟的,并且在模式對快速開發和運行周期進行了編譯優化,當使用調試工具進行代碼調試時可以直接連接到應用的進程里。
Release模式
Release模式又名發布模式,此模式只能在物理設備上運行,不能在模擬器上運行。使用flutter run --release命令運行應用程序時就是使用的Release模式。在Release模式下,斷點、調試信息和服務擴展是不可用的,并且Release模式針對快速啟動、快速執行和安裝包大小進行了優化。
Profile模式
Profile模式只能在物理設備上運行,不能在模擬器上運行。此模式主要用于應用性能分析,一些應用調試能力是被保留的,目的是分析應用存在的性能問題。Profile模式和Release模式大體相同,不同點體現在,Profile模式的某些服務擴展是啟用的,某些進程調試手段也是開啟的。
調試模式
在 Debug 模式下,app 可以被安裝在物理設備、仿真器或者模擬器上進行調試。在Debug模式下,可以進行如下操作:
斷點 是開啟的。
服務擴展是開啟的。
針對快速開發和運行周期進行了編譯優化(但不是針對執行速度、二進制文件大小或者部署)。
調試開啟,類似 開發者工具 等調試工具可以連接到進程里。
如果是在 Web 平臺下的調試模式,可以進行如下操作:
本次構建 沒有 最小化資源并且整個構建 沒有 優化性能。
為了簡化調試,這個 Web 應用使用了 dartdevc 編譯器。
默認情況下,運行 flutter run 會使用 Debug 模式,同時 IDE 也支持這些模式。例如,Android Studio 提供了 Run > Debug… 菜單選項,而且在項目面板中還有一個三角形的綠色運行按鈕圖標 。
Release 模式
當你想要最大的優化以及最小的占用空間時,就使用 Release 模式來部署 app。 release 模式是不支持模擬器或者仿真器的,使用 Release 模式意味著。
斷點是不可用的。
調試信息是不可見的。
調試是禁用的。
編譯針對快速啟動、快速執行和小的 package 的大小進行了優化。
服務擴展是禁用的。
對于Web開發來說,使用 Release 模式意味著。
這次構建資源已經被壓縮,并且性能得以優化。
這個 Web 應用通過 dart2js 編譯器構建,以確保更優秀的性能。
Profile 模式
在 Profile 模式下,一些調試能力是被保留的,足夠分析你的 app 性能。Profile 模式在仿真器和模擬器上是不可用的,因為他們的行為不能代表真實的性能。和 release 相比, profile 模式有以下不同:
一些服務擴展是啟用的。例如,支持 performance overlay。
Tracing 是啟用的,一些調試工具,比如 開發者工具 可以連接到進程里。
在 Web 平臺使用Profile 模式意味著:
資源文件沒有被壓縮,但是整體性能已經優化。
這個 Web 應用通過 dart2js 編譯器構建。
調試工具
在Flutter應用開發中,有很多工具可以幫助調試 Flutter 應用程序,常見的如下所示。
開發者工具,是一套運行在瀏覽器的性能及分析工具。
Android Studio/IntelliJ 和 VS Code(借助 Flutter 和 Dart 插件)支持內置的源代碼調試器,可以設置斷點,單步調試,檢查數值。
Flutter inspector,是開發者工具提供的 widget 檢查器,也可直接在 Android Studio 和 IntelliJ 中使用(借助 Flutter 插件)。檢查器可以可視化展現 widget 樹,查看單個 widget 及其屬性值,開啟性能圖層,等等。
開發者工具
要調試及分析應用,開發者工具可能是你的首選。開發者工具運行在瀏覽器,支持以下特性:
源代碼調試器
Widget 檢查器,展示可視化的 widget 樹; “widget select” 模式,在應用中選擇一個 widget,會在 widget 樹直接定位到它的位置。
內存分析
時間線視圖,支持跟蹤,導入及導出跟蹤信息
日志視圖
如果你在Debug 模式 或Profile 模式 運行,那么可以在瀏覽器打開開發者工具連接到你的應用。開發者工具不能用在 Release 模式 編譯的應用,因為調試和分析信息都被刪除了。如果你要用開發者工具分析應用,需確保使用 Profile 模式運行應用。
在這里插入圖片描述
斷點調試
和其他語言一樣,Flutter的斷點調試支持在 IDE 或編輯器(比如 Android Studio/IntelliJ 和 VS Code)、或者通過編碼兩種方式。
其中,開發者工具調試器如下圖所示。
在這里插入圖片描述
如果需要,在源代碼中設置斷點,然后點擊工具欄中的 【Debug】 按鈕,或選擇 【Run】 > 【Debug】即可開啟調試功能。
在這里插入圖片描述
開啟調試后,可以在控制臺看到如下一些信息。
底部的 Debugger 窗口會顯示出堆棧和變量信息。
底部的 Console 窗口會顯示詳細的日志輸出。
調試基于默認的啟動配置,如果需要自定義,點擊選擇目標下拉按鈕,選擇 Edit configuration 進行配置。
在進行斷點調試時,使用得最多的就是單步調試,三個單步調試按鈕在暫停后會變為可用狀態。
使用 Step in 來進入被調用的方法,在遇到方法內的第一行可執行代碼時結束。
使用 Step over 直接執行某個方法調用而不進入內部;該按鈕在當前方法內按行執行。
使用 Step out 來跳出當前方法,這種方式會直接執行完所有當前方法內的語句。
除此之外,我們還可以使用代碼的方式進行斷點調試,我們可以在源代碼中使用 debugger()函數來開啟斷點,當代碼運行到此處時就會刮起,如下所示。
import 'dart:developer';
void someFunction(double offset) {
debugger(when: offset > 30.0);
// ...
}
Dart 分析器
如果你使用的是 Android Studio或者VSCode,那么工具會自帶的 Dart 分析器默認會檢查代碼,并發現可能的錯誤。如果你使用命令行,則可以使用 flutter analyze命令來檢查代碼。Dart 分析器非常依賴你在代碼中添加的類型注解,以幫助跟蹤問題。
另外,我們可以使用flutter analyze --flutter-repo命令將分析結果打印到控制臺上,每次運行這個命名之前,請先運行flutter update-packages 升級的包,這樣就可以獲取的依賴包。如果你不這樣做,你可能會從dart:ui得到一些錯誤消息,比如偏移量等。因為執行flutter analysis 命令時并不會主動去拉取依賴。
對于一次性的Dart分析,直接使用flutter analyze --flutter-repo即可,對于連續分析,則可以使用flutter analyze --flutter-repo --watch命令。如果你想知道多少個成員變量丟失了dartdocs,可以添加一個dartdocs參數。
Flutter inspector 工具
Flutter inspector 是分析Flutter組件狀態樹的利器,Flutter使用小部件來控制頁面組件到布局的精準控制,Flutter inspector 可以幫助我們進行如下一些分析。
進行布局分析,理解布局層次
診斷布局問題
在這里插入圖片描述
在調試模式下,我們點擊Android Studio右邊Flutter inspector按鈕即可開啟Flutter inspector分析,Flutter inspector提供了如下的可視化調試工具。
在這里插入圖片描述
Select widget mode:啟用此按鈕后,選擇組件樹的代碼會自動跳轉到對應的源代碼里面。
Refresh tree : 重新加載的組件信息。
Slow Animations:放慢動畫速度,以便進行視覺上的查驗。
Debug Paint: 邊框、方向的可視化。
Paint Baselines: 每個渲染框在它的每個文本基線上畫一條線。
Repaint Rainbow:查看重繪的嚴重程度,嚴重的會被爆紅。
除了上面的功能外,我們還可以點擊【Open DevTools】打開Flutter的調試頁面,可以借助它進行很多性能分析,后面會具體介紹。
在這里插入圖片描述
測量應用啟動時間
要收集有關 Flutter 應用程序啟動所需時間的詳細信息,可以在運行 flutter run 命令時使用 trace-startup 和 profile 選項,如下所示。
flutter run --trace-startup --profile
跟蹤輸出被保存到 Flutter 工程目錄在 build 目錄下,一個名為 start_up_info.json 的 JSON 文件中,輸出列出了從應用程序啟動到這些跟蹤事件(以微秒捕獲)所用的時間,如下所示。
{
"engineEnterTimestampMicros": 2346054348633,
"timeToFrameworkInitMicros": 812748,
"timeToFirstFrameRasterizedMicros": 1573154,
"timeToFirstFrameMicros": 1221472,
"timeAfterFrameworkInitMicros": 408724
}
對應的具體含義如下:
進入 Flutter 引擎時
展示應用第一幀時
初始化Flutter框架時
完成Flutter框架初始化時
使用Android Studio進行調試
Flutter官方推薦使用Android Studio或VSCode進行應用開發, 和其他語言的調試一樣,Dart代碼的調試流程也差不多。如果還沒有Flutter項目,可以新建一個示例項目。通過單擊首先,點擊調試圖標(Debug-run icon)同時打開調試面板并在控制臺中運行應用,首次運行應用是最慢的,應用啟動后,界面應該是下面這樣的。
在這里插入圖片描述
然后,我們在在 counter++ 這一行上添加斷點。在應用里,點擊 + 按鈕(FloatingActionButton,或者簡稱 FAB)來增加數字,應用會暫停。
在這里插入圖片描述
你可以 step in/out/over Dart 語句、熱重載和恢復執行應用、以及像使用其他調試器一樣來使用 Dart 調試器。
Flutter inspector
Flutter inspector 是一個用來可視化以及查看 Flutter widget 樹的工具,提供如下功能:
了解現有布局
診斷布局問題
可以使用 Android Studio 窗口右側的垂直按鈕來打開Flutter inspector,如下圖所示。
在這里插入圖片描述
Flutter outline
Flutter Outline 是一個可視的顯示頁面構建方法的功能,注意在構建方法上可能與 widget 樹不同,可以使用 Android Studio 窗口右側的垂直按鈕切換 outline 的顯示。
在這里插入圖片描述
Tip: 我們可以安裝一個 Presentation Assistant 插件來輔助我們進行開發,Presentation Assistant 提供了很多的快捷功能。例如,當焦點在編輯面板中時,輸入 command-Shift-A(Mac)或者 shift-control-A(Windows 和 Linux),該插件會同時顯示「查找」面板并顯示在所有三個平臺上執行此操作的提示。
在這里插入圖片描述
然后在輸入框中輸入attach關鍵字,顯示如下圖。
在這里插入圖片描述
使用 Android Gradle 調試
為了調試原生代碼,你需要一個包含 Android 原生代碼的應用。在本節中,你將學會如何連接兩個調試器到你的應用:
1)Dart 調試器。
2)Android Gradle 調試器。
創建一個基本的 Flutter 應用,然后替換 lib/main.dart 的代碼為以下示例代碼。
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'URL Launcher',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'URL Launcher'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Future<void> _launched;
Future<void> _launchInBrowser(String url) async {
if (await canLaunch(url)) {
await launch(url, forceSafariVC: false, forceWebView: false);
} else {
throw 'Could not launch $url';
}
}
Future<void> _launchInWebViewOrVC(String url) async {
if (await canLaunch(url)) {
await launch(url, forceSafariVC: true, forceWebView: true);
} else {
throw 'Could not launch $url';
}
}
Widget _launchStatus(BuildContext context, AsyncSnapshot<void> snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return Text('');
}
}
@override
Widget build(BuildContext context) {
String toLaunch = 'https://flutter.dev';
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: EdgeInsets.all(16.0),
child: Text(toLaunch),
),
RaisedButton(
onPressed: () => setState(() {
_launched = _launchInBrowser(toLaunch);
}),
child: Text('Launch in browser'),
),
Padding(padding: EdgeInsets.all(16.0)),
RaisedButton(
onPressed: () => setState(() {
_launched = _launchInWebViewOrVC(toLaunch);
}),
child: Text('Launch in app'),
),
Padding(padding: EdgeInsets.all(16.0)),
FutureBuilder<void>(future: _launched, builder: _launchStatus),
],
),
),
);
}
}
然后,添加 url_launcher 依賴到 pubspec 文件,并執行 flutter pub get命令拉取依賴包。
name: flutter_app
description: A new Flutter application.
version: 1.0.0+1
dependencies:
flutter:
sdk: flutter
url_launcher: ^3.0.3
cupertino_icons: ^0.1.2
dev_dependencies:
flutter_test:
sdk: flutter
點擊調試按鈕(Debug-run icon)來同時打開調試面板并啟動應用,如下圖所示。
在這里插入圖片描述
點擊 【Attach debugger to Android process】 按鈕,從進程對話框中,你應該可以看到每一個設備的入口。選擇 show all processes 來顯示每個設備可用的進程。
在這里插入圖片描述
在調試面板中,你現在應該可以看到一個 Android Debugger 標簽頁,然后依次選擇【app_name】 > 【android】 > 【app】 > 【src】 >【 main】 > 【java】 > 【io.flutter plugins】在項目面板,然后雙擊 GeneratedProjectRegistrant 在編輯面板中打開 Java 代碼,此時Dart 和原生調試器都在與同一個進程交互。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
構建庫的常見方法有兩種:一種是自己手動構建webpack庫打包,設置output為 library; 另一種是基于vue-cli3輸出庫資源包。我們采用第二種vue腳手架的方式構建庫。
新增編譯庫命令
// package.json
"scripts": {
// ...
"lib": "vue-cli-service build --target lib --name Step --dest dist packages/index.js"
}
// packages/index.js 默認打包Step
import Step from '../steps/src/step';
Step.install = function(Vue) {
Vue.component(Step.name, Step);
};
export default Step;
--name: 庫名稱。
--target: 構建目標,默認為應用模式。這里修改為 lib 啟用庫模式。
--dest: 輸出目錄,默認 dist。
[entry]: 最后一個參數為入口文件,默認為 src/App.vue。
更多詳細配置查看 ? vue腳手架官網
如果該庫依賴于其他庫,請在vue.config.js 配置 externals
// vue.config.js
module.exports = {
configureWebpack:{
externals: {
vue: 'Vue',
'vue-router':'VueRouter',
axios: 'axios'
}
}
}
執行 npm run lib 就可以發現我們的庫被打包到了 根目錄的dist文件夾下。
添加 .npmignore 文件(可選)
和 .gitignore 的語法一樣,具體需要提交什么文件,看各自的實際情況
# 忽略目錄
examples/
packages/
public/
# 忽略指定文件
vue.config.js
babel.config.js
*.map
配置npm庫信息
配置package.json文件,以發布庫文件。
{
"name": "gis",
"version": "1.2.5",
"description": "基于 Vue 的庫文件",
"main": "dist/gis.umd.min.js",
"keyword": "vue gis",
"private": false,
"files": ["dist"],
"license": "MIT"
}
name: 包名,該名字是唯一的。可在 npm 官網搜索名字,如果存在則需換個名字。
version: 版本號,每次發布至 npm 需要修改版本號,不能和歷史版本號相同。
description: 描述。
main: 入口文件,該字段需指向我們最終編譯后的包文件。
keyword:關鍵字,以空格分離希望用戶最終搜索的詞。
author:作者
files: 要上傳的文件
private:是否私有,需要修改為 false 才能發布到 npm
license: 開源協議
dependencies: 依賴庫
注意每次發布新的庫,需要更改版本號,規則如下:
"version": "1.2.5" 主版本號為 1,次版本號 2,修訂號 5
主版本號(Major):當你做了不兼容的API修改
次版本號(Minor):當你做了向下兼容的功能性新增
修訂號(Patch):當你做了向下兼容的問題修正
登錄npm
首先設置登錄的npm鏡像地址
npm config set registry http://168.20.20.57.4873
然后在終端執行登錄命令,輸入用戶名、密碼、郵箱即可登錄
npm login
接著發布庫資源到npm
npm publish
最后發布成功可到官網查看對應的包并下載
npm install package_name
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務無縫輪播一直是面試的熱門題目,而大部分答案都是復制第一張到最后。誠然,這種方法是非常標準,那么有沒有另類一點的方法呢?
第一種方法是需要把所有圖片一張張擺好,然后慢慢移動的,
但是我能不能直接不擺就硬移動呢?
如果你使用過vue的transition
,我們是可以通過給每一張圖片來添加入場動畫和離場動畫來模擬這個移動
這樣看起來的效果就是圖片從右邊一直往左移動,但是這個不一樣的地方是,我們每一個元素都有這個進場動畫和離場動畫,我們根本不用關心它是第幾個元素,你只管輪播就是。
很簡單,我們自己實現一個transtition
的效果就好啦,主要做的是以下兩點
xx-enter-active
動畫
xx-leave-active
, 注意要讓動畫播完才消失
function hide(el){
el.className = el.className.replace(' slide-enter-active','')
el.className += ' slide-leave-active' el.addEventListener('animationend',animationEvent)
} function animationEvent(e){
e.target.className = e.target.className.replace(' slide-leave-active','')
e.target.style.display = 'none' e.target.removeEventListener('animationend',animationEvent)
} function show(el){
el.style.display = 'flex' el.className += ' slide-enter-active' }
這里我們使用了animationend
來監聽動畫結束,注意這里每次從新添加類的時候需要重新添加監聽器,不然會無法監聽。如果不使用這個方法你可以使用定時器的方式來移除leave-active類。
function hide(el){
el.className = el.className.replace(' slide-enter-active','')
el.className += ' slide-leave-active' setTimeout(()=>
{ //動畫結束后清除class el.className = el.className.replace(' slide-leave-active','')
el.style.display = 'none' }, ANIMATION_TIME) //這個ANIMATION_TIME為你在css中動畫執行的時間 }
.slide-enter-active{ position: absolute; animation: slideIn ease .5s forwards;
} .slide-leave-active{ position: absolute; animation: slideOut ease .5s forwards;
} @keyframes slideIn {
0%{ transform: translateX(100%);
}
100%{ transform: translateX(0);
}
} @keyframes slideOut {
0%{ transform: translateX(0);
}
100%{ transform: translateX(-100%);
}
}
需要注意的是這里的 forwards
屬性,這個屬性表示你的元素狀態將保持動畫后的狀態,如果不設置的話,動畫跑完一遍,你的元素本來執行了離開動畫,執行完以后會回來中央位置杵著。這個時候你會問了,上面的代碼不是寫了,動畫執行完就隱藏元素嗎?
如果你使用上面的setTimeout來命令元素執行完動畫后消失,那么可能會有一瞬間的閃爍,因為實際業務中,你的代碼可能比較復雜,setTimeout沒法在那么精準的時間內執行。保險起見,就讓元素保持動畫離開的最后狀態,即translateX(-100%)
。此時元素已經在屏幕外了,不用關心它的表現了
很簡單,我們進一個新元素的時候同時移除舊元素即可,兩者同時執行進場和離場動畫即可。
function autoPlay(){
setTimeout(()=>{
toggleShow(新元素, 舊元素) this.autoPlay()
},DURATION) //DURATION為動畫間隔時間 } function toggleShow(newE,oldE){ //舊ele和新ele同時動畫 hide(oldE)
show(newE)
}
垂直居中基本上是入門 CSS 必須要掌握的問題了,我們肯定在各種教程中都看到過“CSS 垂直居中的 N 種方法”,通常來說,這些方法已經可以滿足各種使用場景了,然而當我們碰到了需要使用某些特殊字體進行混排、或者使文字對齊圖標的情況時,也許會發現,無論使用哪種垂直居中的方法,總是感覺文字向上或向下偏移了幾像素,不得不專門對它們進行位移,為什么會出現這種情況呢?
下圖是一個使用各種常見的垂直居中的方法來居中文字的示例,其中涉及到不同字體的混排,可以看出,雖然這里面用了幾種常用的垂直居中的方法,但是在實際的觀感上這些文字都沒有恰好垂直居中,有些文字看起來比較居中,而有些文字則偏移得很厲害。
在線查看:CodePen(字體文件直接引用了谷歌字體,如果沒有效果需要注意網絡情況)
通過設置vertical-align:middle
對文字進行垂直居中時,父元素需要設置font-size: 0
,因為vertical-align:middle
是將子元素的中點與父元素的baseline + x-height / 2
的位置進行對齊的,設置字號為 0 可以保證讓這些線的位置都重合在中點。
我們用鼠標選中這些文字,就能發現選中的區域確實是在父層容器里垂直居中的,那么為什么文字卻各有高低呢?這里就涉及到了字體本身的構造和相關的度量值。
這里先提出一個問題,我們在 CSS 中給文字設置了 font-size
,這個值實際設置的是字體的什么屬性呢?
下面的圖給出了一個示例,文字所在的標簽均為 span
,對每種字體的文字都設置了紅色的 outline
以便觀察,且設有 line-height: normal
。從圖中可以看出,雖然這些文字的字號都是 40px,但是他們的寬高都各不相同,所以字號并非設置了文字實際顯示的大小。
為了解答這個問題,我們需要對字體進行深入了解,以下這些內容是西文字體的相關概念。首先一個字體會有一個 EM Square(也被稱為 UPM、em、em size)[4],這個值最初在排版中表示一個字體中大寫 M 的寬度,以這個值構成一個正方形,那么所有字母都可以被容納進去,此時這個值實際反映的就成了字體容器的高度。在金屬活字中,這個容器就是每個字符的金屬塊,在一種字體里,它們的高度都是統一的,這樣每個字模都可以放入印刷工具中并進行排印。在數碼排印中,em 是一個被設置了大小的方格,計量單位是一種相對單位,會根據實際字體大小縮放,例如 1000 單位的字體設置了 16pt 的字號,那么這里 1000 單位的大小就是 16pt。Em 在 OpenType 字體中通常為 1000 ,在 TrueType 字體中通常為 1024 或 2048(2 的 n 次冪)。
金屬活字,圖片來自 http://designwithfontforge.com/en-US/The_EM_Square.html
字體本身還有很多概念和度量值(metrics),這里介紹幾個常見的概念,以維基百科的這張圖為例(下面的度量值的計量單位均為基于 em 的相對單位):
接下來我們在 FontForge 軟件里看看這些值的取值,這里以 Arial
字體給出一個例子:
從圖中可以看出,在 General 菜單中,Arial 的 em size 是 2048,字體的 ascent 是1638,descent 是410,在 OS/2 菜單的 Metrics 信息中,可以得到 capital height 是 1467,x height 為 1062,line gap 為 67。
然而這里需要注意,盡管我們在 General 菜單中得到了 ascent 和 descent 的取值,但是這個值應該僅用于字體的設計,它們的和永遠為 em size;而計算機在實際進行渲染的時候是按照 OS/2 菜單中對應的值來計算,一般操作系統會使用 hhea(Horizontal Header Table)表的 HHead Ascent 和 HHead Descent,而 Windows 是個特例,會使用 Win Ascent 和 Win Descent。通常來說,實際用于渲染的 ascent 和 descent 取值要比用于字體設計的大,這是因為多出來的區域通常會留給注音符號或用來控制行間距,如下圖所示,字母頂部的水平線即為第一張圖中 ascent 高度 1638,而注音符號均超過了這個區域。根據資料的說法[5],在一些軟件中,如果文字內容超過用于渲染的 ascent 和 descent,就會被截斷,不過我在瀏覽器里實驗后發現瀏覽器并沒有做這個截斷(Edge 86.0.608.0 Canary (64 bit), MacOS 10.15.6)。
在本文中,我們將后面提到的 ascent 和 descent 均認為是 OS/2 選項中讀取到的用于渲染的 ascent 和 descent 值,同時我們將 ascent + descent 的值叫做 content-area。
理論上一個字體在 Windows 和 MacOS 上的渲染應該保持一致,即各自系統上的 ascent 和 descent 應該相同,然而有些字體在設計時不知道出于什么原因,導致其確實在兩個系統中有不同的表現。以下是 Roboto 的例子:
Differences between Win and HHead metrics cause the font to be rendered differently on Windows vs. iOS (or Mac I assume) · Issue #267 · googlefonts/roboto
那么回到本節一開始的問題,CSS 中的font-size
設置的值表示什么,想必我們已經有了答案,那就是一個字體 em size 對應的大??;而文字在設置了line-height: normal
時,行高的取值則為 content-area + line-gap,即文本實際撐起來的高度。
知道了這些,我們就不難算出一個字體的顯示效果,上面 Arial 字體在line-height: normal
和font-size: 100px
時撐起的高度為(1854 + 434 + 67) / 2048 * 100px = 115px
。
在實驗中發現,對于一個行內元素,鼠標拉取的 selection 高度為當前行line-height
最高的元素值。如果是塊狀元素,當line-height
的值為大于 content-area 時,selection 高度為line-height
,當其小于等于 content-area 時,其高度為 content-area 的高度。
在中間插一個問題,我們應該都使用過 line-height
來給文字進行垂直居中,那么 line-height
實際是以字體的哪個部分的中點進行計算呢?為了驗證這個問題,我新建了一個很有“設計感”的字體,em size 設為 1000,ascent 為 800,descent 為 200,并對其分別設置了正常的和比較夸張的 metrics:
上面圖中左邊是 FontForge 里設置的 metrics,右邊是實際顯示效果,文字字號設為 100px,四個字母均在父層的 flex 布局下垂直居中,四個字母的 line-height
分別為 0、1em、normal、3em,紅色邊框是元素的 outline
,黃色背景是鼠標選取的背景。由上面兩張圖可以看出,字體的 metrics 對文字渲染位置的影響還是很大的。同時可以看出,在設置 line-height
時,雖然 line gap 參與了撐起取值為 normal
的空間,但是不參與文字垂直居中的計算,即垂直居中的中點始終是 content-area 的中點。
我們又對字體進行了微調,使其 ascent 有一定偏移,這時可以看出 1em 行高的文字 outline 恰好在正中間,因此可以得出結論:在瀏覽器進行渲染時,em square 總是相對于 content-area 垂直居中。
說完了字體構造,又回到上一節的問題,為什么不同字體文字混排的時候進行垂直居中,文字各有高低呢?
在這個問題上,本文給出這樣一個結論,那就是因為不同字體的各項度量值均不相同,在進行垂直居中布局時,content-area 的中點與視覺的中點不統一,因此導致實際看起來存在位置偏移,下面這張圖是 Arial 字體的幾個中線位置:
從圖上可以看出來,大寫字母和小寫字母的視覺中線與整個字符的中線還是存在一定的偏移的。這里我沒有找到排版相關學科的定論,究竟以哪條線進行居中更符合人眼觀感的居中,以我個人的觀感來看,大寫字母的中線可能看起來更加舒服一點(尤其是與沒有小寫字母的內容進行混排的時候)。
需要注意一點,這里選擇的 Arial 這個字體本身的偏移比較少,所以使用時整體感覺還是比較居中的,這并不代表其他字體也都是這樣。
對于中文字體,本身的設計上沒有基線、升部、降部等說法,每個字都在一個方形盒子中。但是在計算機上顯示時,也在一定程度上沿用了西文字體的概念,通常來說,中文字體的方形盒子中文字體底端在 baseline 和 descender 之間,頂端超出一點 ascender,而標點符號正好在 baseline 上。
我們已經了解了字體的相關概念,那么如何解決在使用字體時出現的偏移問題呢?
通過上面的內容可以知道,文字顯示的偏移主要是視覺上的中點和渲染時的中點不一致導致的,那么我們只要把這個不一致修正過來,就可以實現視覺上的居中了。
為了實現這個目標,我們可以借助 vertical-align
這個屬性來完成。當 vertical-align
取值為數值的時候,該值就表示將子元素的基線與父元素基線的距離,其中正數朝上,負數朝下。
這里介紹的方案,是把某個字體下的文字通過計算設置 vertical-align
的數值偏移,使其大寫字母的視覺中點與用于計算垂直居中的點重合,這樣字體本身的屬性就不再影響居中的計算。
具體我們將通過以下的計算方法來獲?。菏紫任覀冃枰阎斍白煮w的 em-size,ascent,descent,capital height 這幾個值(如果不知道 em-size,也可以提供其他值與 em-size 的比值),以下依然以 Arial 為例:
const emSize = 2048; const ascent = 1854; const descent = 434; const capitalHeight = 1467;
// 計算前需要已知給定的字體大小 const fontSize = FONT_SIZE; // 根據文字大小,求得文字的偏移 const verticalAlign = ((ascent - descent - capitalHeight) / emSize) * fontSize; return ( <span style={{ fontFamily: FONT_FAMILY, fontSize }}> <span style={{ verticalAlign }}>TEXT</span> </span> )
由此設置以后,外層 span 將表現得像一個普通的可替換元素參與行內的布局,在一定程度上無視字體 metrics 的差異,可以使用各種方法對其進行垂直居中。
由于這種方案具有固定的計算步驟,因此可以根據具體的開發需求,將其封裝為組件、使用 CSS 自定義屬性或使用 CSS 預處理器對文本進行處理,通過傳入字體信息,就能修正文字垂直偏移。
雖然上述的方案可以在一定程度上解決文字垂直居中的問題,但是在實際使用中還存在著不方便的地方,我們需要在使用字體之前就知道字體的各項 metrics,在自定義字體較少的情況下,開發者可以手動使用 FontForge 等工具查看,然而當字體較多時,挨個查看還是比較麻煩的。
目前的一種思路是我們可以使用 Canvas 獲取字體的相關信息,如現在已經有開源的獲取字體 metrics 的庫 FontMetrics.js。它的核心思想是使用 Canvas 渲染對應字體的文字,然后使用 getImageData
對渲染出來的內容進行分析。如果在實際項目中,這種方案可能導致潛在的性能問題;而且這種方式獲取到的是渲染后的結果,部分字體作者在構建字體時并沒有嚴格將設計的 metrics 和字符對應,這也會導致獲取到的 metrics 不夠準確。
另一種思路是直接解析字體文件,拿到字體的 metrics 信息,如 opentype.js 這個項目。不過這種做法也不夠輕量,不適合在實際運行中使用,不過可以考慮在打包過程中自動執行這個過程。
此外,目前的解決方案更多是偏向理論的方法,當文字本身字號較小的情況下,瀏覽器可能并不能按照預期的效果渲染,文字會根據所處的 DOM 環境不同而具有 1px 的偏移[9]。
CSS Houdini 提出了一個 Font Metrics 草案[6],可以針對文字渲染調整字體相關的 metrics。從目前的設計來看,可以調整 baseline 位置、字體的 em size,以及字體的邊界大?。?content-area)等配置,通過這些可以解決因字體的屬性導致的排版問題。
[Exposed=Window] interface FontMetrics {
readonly attribute double width;
readonly attribute FrozenArray<double> advances;
readonly attribute double boundingBoxLeft;
readonly attribute double boundingBoxRight;
readonly attribute double height;
readonly attribute double emHeightAscent;
readonly attribute double emHeightDescent;
readonly attribute double boundingBoxAscent;
readonly attribute double boundingBoxDescent;
readonly attribute double fontBoundingBoxAscent;
readonly attribute double fontBoundingBoxDescent;
readonly attribute Baseline dominantBaseline;
readonly attribute FrozenArray<Baseline> baselines;
readonly attribute FrozenArray<Font> fonts;
};
從 https://ishoudinireadyyet.com/ 這個網站上可以看到,目前 Font Metrics 依然在提議階段,還不能確定其 API 具體內容,或者以后是否會存在這一個特性,因此只能說是一個在未來也許可行的文字排版處理方案。
文本垂直居中的問題一直是 CSS 中最常見的問題,但是卻很難引起注意,我個人覺得是因為我們常用的微軟雅黑、蘋方等字體本身在設計上比較規范,在通常情況下都顯得比較居中。但是當一個字體不是那么“規范”時,傳統的各種方法似乎就有點無能為力了。
本文分析了導致了文字偏移的因素,并給出尋找文字垂直居中位置的方案。
由于涉及到 IFC 的問題本身就很復雜[7],關于內聯元素使用 line-height
與 vertical-align
進行居中的各種小技巧因為與本文不是強相關,所以在文章內也沒有提及,如果對這些內容比較感興趣,也可以通過下面的參考資料尋找一些相關介紹。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
ECMAScript模塊(簡稱ES模塊)是一種JavaScript代碼重用的機制,于2015年推出,一經推出就受到前端開發者的喜愛。在2015之年,JavaScript 還沒有一個代碼重用的標準機制。多年來,人們對這方面的規范進行了很多嘗試,導致現在有多種模塊化的方式。
你可能聽說過AMD模塊,UMD,或CommonJS,這些沒有孰優孰劣。最后,在ECMAScript 2015中,ES 模塊出現了。
我們現在有了一個“正式的”模塊系統。
理論上,ES 模塊應該在所有JavaScript環境中。實際上,ES 模塊的主要應用還是在瀏覽器上。
2020年5月,Node.js v12.17.0 增加了在不使用標記前提下對ECMAScript模塊的支持。 這意味著我們現在可以在Node.js
中使用import
和export
,而無需任何其他命令行標志。
ECMAScript模塊要想在任何JavaScript環境通用,可能還需要很長的路要走,但方向是正確的。
ES 模塊是一個簡單的文件,我們可以在其中聲明一個或多個導出。以下面utils.js
為例:
// utils.js export function funcA() { return "Hello named export!";
} export default function funcB() { return "Hello default export!";
}
這里有兩個導出。
第一個是命名導出,后面是export default
,表示為默認導出。
假設我們的項目文件夾中有一個名為utils.js
的文件,我們可以將這個模塊提供的對象導入到另一個文件中。
假設我們在項目文中還有一個Consumer.js
的文件。 要導入utils.js
公開的函數,我們可以這樣做:
// consumer.js import { funcA } from "./util.js";
這種對應我們的命名導入方式.
如果我們要導入 utils.js
中的默認導出也就是 funcB
方法,我們可以這樣做:
// consumer.js import { funcA } from "./util.js";
當然,我們可以導入同時導入命名和默認的:
// consumer.js import funcB, { funcA } from "./util.js";
funcB();
funcA();
我們也可以用星號導入整個模塊:
import * as myModule from './util.js';
myModule.funcA();
myModule.default();
注意,這里要使用默認到處的方法是使用 default()
而不是 funcB()
。
從遠程模塊導入:
import { createStore } from "https://unpkg.com/redux@4.0.5/es/redux.mjs"; const store = createStore(/* do stuff */)
現代瀏覽器支持ES模塊,但有一些警告。 要使用模塊,需要在 script
標簽上添加屬性 type
, 對應值 為 module
。
<html lang="en"> <head> <meta charset="UTF-8"> <title>ECMAScript modules in the browser</title>
</head> <body> <p id="el">The result is:
</p> </body> <script type="module"> import { appendResult } from "./myModule.js"; const el = document.getElementById("el");
appendResult(el);
appendResult(el);
appendResult(el);
appendResult(el);
appendResult(el); </script> </html>
myModule.js
內容如下:
export function appendResult(element) { const result = Math.random();
element.innerText += result;
}
ES 模塊是靜態的,這意味著我們不能在運行時更改導入。隨著2020年推出的動態導入(dynamic imports),我們可以動態加載代碼來響應用戶交互(webpack早在ECMAScript 2020推出這個特性之前就提供了動態導入)。
考慮下面的代碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8">
<title>Dynamic imports</title> </head> <body> <button id="btn">Load!</button> </body> <script src="loader.js"></script> </html>
再考慮一個帶有兩個導出的JavaScript模塊
// util.js export function funcA() { console.log("Hello named export!");
} export default function funcB() { console.log("Hello default export!");
}
為了動態導入 util.js 模塊,我們可以點擊按鈕在去導入:
/ loader.js
const btn = document.getElementById("btn");
btn.addEventListener("click", () => { // loads named export import("./util.js").then(({ funcA }) => {
funcA();
});
});
這里使用解構的方式,取出命名導出 funcA
方法:
({ funcA }) => {}
ES模塊實際上是JavaScript對象:我們可以解構它們的屬性以及調用它們的任何公開方法。
要使用動態導入的默認方法,可以這樣做
// loader.js const btn = document.getElementById("btn");
btn.addEventListener("click", () => { import("./util.js").then((module) => { module.default();
});
});
當作為一個整體導入一個模塊時,我們可以使用它的所有導出
// loader.js const btn = document.getElementById("btn");
btn.addEventListener("click", () =>
{ // loads entire module // uses everything import("./util.js").then((module) => { module.funcA(); module.default();
});
});
還有另一種用于動態導入的常見樣式,如下所示:
const loadUtil = () => import("./util.js"); const btn = document.getElementById("btn");
btn.addEventListener("click", () => { // });
loadUtil
返回的是一個 promise,所以我們可以這樣操作
const loadUtil = () => import("./util.js"); const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
loadUtil().then(module => { module.funcA(); module.default();
})
})
動態導入看起來不錯,但是它們有什么用呢?
使用動態導入,我們可以拆分代碼,并只在適當的時候加載重要的代碼。在 JavaScript 引入動態導入之前,這種模式是webpack(模塊綁定器)獨有的。
像React
和Vue
通過動態導入代碼拆分來加載響應事件的代碼塊,比如用戶交互或路由更改。
假設我們項目有一個 person.json
文件,內容如下:
{ "name": "Jules", "age": 43 }
現在,我們需要動態導入該文件以響應某些用戶交互。
因為 JSON 文件不是一個方法,所以我們可以使用默認導出方式:
const loadPerson = () => import('./person.json'); const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
loadPerson().then(module => { const { name, age } = module.default; console.log(name, age);
});
});
這里我們使用解構的方式取出 name
和 age
:
const { name, age } = module.default;
因為 import()
語句返回是一個 Promise,所以我們可以使用 async/await
:
const loadUtil = () => import("./util.js"); const btn = document.getElementById("btn");
btn.addEventListener("click", async () => { const utilsModule = await loadUtil();
utilsModule.funcA();
utilsModule.default();
})
使用import()
導入模塊時,可以按照自己的意愿命名它,但要調用的方法名保持一致:
import("./util.js").then((module) => { module.funcA(); module.default();
});
或者:
import("./util.js").then((utilModule) => {
utilModule.funcA();
utilModule.default();
});
原文:https://www.valentinog.com/bl...
代碼部署后可能存在的BUG沒法實時知道,事后為了解決這些BUG,花了大量的時間進行log 調試,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。
TypeScript 是一種由微軟開發的自由和開源的編程語言。它是 JavaScript 的一個超集,而且本質上向這個語言添加了可選的靜態類型和基于類的面向對象編程。
本文阿寶哥將分享這些年在學習 TypeScript 過程中,遇到的 10 大 “奇怪” 的符號。其中有一些符號,阿寶哥第一次見的時候也覺得 “一臉懵逼”,希望本文對學習 TypeScript 的小伙伴能有一些幫助。
好的,下面我們來開始介紹第一個符號 —— ! 非空斷言操作符。
一、! 非空斷言操作符
在上下文中當類型檢查器無法斷定類型時,一個新的后綴表達式操作符 ! 可以用于斷言操作對象是非 null 和非 undefined 類型。具體而言,x! 將從 x 值域中排除 null 和 undefined 。
那么非空斷言操作符到底有什么用呢?下面我們先來看一下非空斷言操作符的一些使用場景。
1.1 忽略 undefined 和 null 類型
function myFunc(maybeString: string | undefined | null) { // Type 'string | null | undefined' is not assignable to type 'string'. // Type 'undefined' is not assignable to type 'string'. const onlyString: string = maybeString; // Error const ignoreUndefinedAndNull: string = maybeString!; // Ok }
1.2 調用函數時忽略 undefined 類型
type NumGenerator = () => number; function myFunc(numGenerator: NumGenerator | undefined) { // Object is possibly 'undefined'.(2532) // Cannot invoke an object which is possibly 'undefined'.(2722) const num1 = numGenerator(); // Error const num2 = numGenerator!(); //OK }
因為 ! 非空斷言操作符會從編譯生成的 JavaScript 代碼中移除,所以在實際使用的過程中,要特別注意。比如下面這個例子:
const a: number | undefined = undefined; const b: number = a!; console.log(b);
以上 TS 代碼會編譯生成以下 ES5 代碼:
"use strict"; const a = undefined; const b = a; console.log(b);
雖然在 TS 代碼中,我們使用了非空斷言,使得 const b: number = a!; 語句可以通過 TypeScript 類型檢查器的檢查。但在生成的 ES5 代碼中,! 非空斷言操作符被移除了,所以在瀏覽器中執行以上代碼,在控制臺會輸出 undefined。
二、?. 運算符
TypeScript 3.7 實現了呼聲最高的 ECMAScript 功能之一:可選鏈(Optional Chaining)。有了可選鏈后,我們編寫代碼時如果遇到 null 或 undefined 就可以立即停止某些表達式的運行??蛇x鏈的核心是新的 ?. 運算符,它支持以下語法:
obj?.prop
obj?.[expr]
arr?.[index] func?.(args)
這里我們來舉一個可選的屬性訪問的例子:
const val = a?.b;
為了更好的理解可選鏈,我們來看一下該 const val = a?.b 語句編譯生成的 ES5 代碼:
var val = a === null || a === void 0 ? void 0 : a.b;
上述的代碼會自動檢查對象 a 是否為 null 或 undefined,如果是的話就立即返回 undefined,這樣就可以立即停止某些表達式的運行。你可能已經想到可以使用 ?. 來替代很多使用 && 執行空檢查的代碼:
if(a && a.b) { } if(a?.b){ } /**
* if(a?.b){ } 編譯后的ES5代碼
*
* if(
* a === null || a === void 0
* ? void 0 : a.b) {
* }
*/
但需要注意的是,?. 與 && 運算符行為略有不同,&& 專門用于檢測 falsy 值,比如空字符串、0、NaN、null 和 false 等。而 ?. 只會驗證對象是否為 null 或 undefined,對于 0 或空字符串來說,并不會出現 “短路”。
2.1 可選元素訪問
可選鏈除了支持可選屬性的訪問之外,它還支持可選元素的訪問,它的行為類似于可選屬性的訪問,只是可選元素的訪問允許我們訪問非標識符的屬性,比如任意字符串、數字索引和 Symbol:
function tryGetArrayElement<T>(arr?: T[], index: number = 0) { return arr?.[index];
}
以上代碼經過編譯后會生成以下 ES5 代碼:
"use strict"; function tryGetArrayElement(arr, index) { if (index === void 0) { index = 0; } return arr === null || arr === void 0 ? void 0 : arr[index];
}
通過觀察生成的 ES5 代碼,很明顯在 tryGetArrayElement 方法中會自動檢測輸入參數 arr 的值是否為 null 或 undefined,從而保證了我們代碼的健壯性。
2.2 可選鏈與函數調用
當嘗試調用一個可能不存在的方法時也可以使用可選鏈。在實際開發過程中,這是很有用的。系統中某個方法不可用,有可能是由于版本不一致或者用戶設備兼容性問題導致的。函數調用時如果被調用的方法不存在,使用可選鏈可以使表達式自動返回 undefined 而不是拋出一個異常。
可選調用使用起來也很簡單,比如:
let result = obj.customMethod?.();
該 TypeScript 代碼編譯生成的 ES5 代碼如下:
var result = (_a = obj.customMethod) === null || _a === void 0 ? void 0 : _a.call(obj);
另外在使用可選調用的時候,我們要注意以下兩個注意事項:
如果存在一個屬性名且該屬性名對應的值不是函數類型,使用 ?. 仍然會產生一個 TypeError 異常。
可選鏈的運算行為被局限在屬性的訪問、調用以及元素的訪問 —— 它不會沿伸到后續的表達式中,也就是說可選調用不會阻止 a?.b / someMethod() 表達式中的除法運算或 someMethod 的方法調用。
三、?? 空值合并運算符
在 TypeScript 3.7 版本中除了引入了前面介紹的可選鏈 ?. 之外,也引入了一個新的邏輯運算符 —— 空值合并運算符 ??。當左側操作數為 null 或 undefined 時,其返回右側的操作數,否則返回左側的操作數。
與邏輯或 || 運算符不同,邏輯或會在左操作數為 falsy 值時返回右側操作數。也就是說,如果你使用 || 來為某些變量設置默認的值時,你可能會遇到意料之外的行為。比如為 falsy 值(''、NaN 或 0)時。
這里來看一個具體的例子:
const foo = null ?? 'default string'; console.log(foo); // 輸出:"default string" const baz = 0 ?? 42; console.log(baz); // 輸出:0
以上 TS 代碼經過編譯后,會生成以下 ES5 代碼:
"use strict"; var _a, _b; var foo = (_a = null) !== null && _a !== void 0 ? _a : 'default string';
console.log(foo); // 輸出:"default string" var baz = (_b = 0) !== null && _b !== void 0 ? _b : 42;
console.log(baz); // 輸出:0
通過觀察以上代碼,我們更加直觀的了解到,空值合并運算符是如何解決前面 || 運算符存在的潛在問題。下面我們來介紹空值合并運算符的特性和使用時的一些注意事項。
3.1 短路
當空值合并運算符的左表達式不為 null 或 undefined 時,不會對右表達式進行求值。
function A() { console.log('A was called'); return undefined;} function B() { console.log('B was called'); return false;} function C() { console.log('C was called'); return "foo";} console.log(A() ?? C()); console.log(B() ?? C());
上述代碼運行后,控制臺會輸出以下結果:
A was called
C was called
foo
B was called
false
3.2 不能與 && 或 || 操作符共用
若空值合并運算符 ?? 直接與 AND(&&)和 OR(||)操作符組合使用 ?? 是不行的。這種情況下會拋出 SyntaxError。
// '||' and '??' operations cannot be mixed without parentheses.(5076) null || undefined ?? "foo"; // raises a SyntaxError // '&&' and '??' operations cannot be mixed without parentheses.(5076) true && undefined ?? "foo"; // raises a SyntaxError
但當使用括號來顯式表明優先級時是可行的,比如:
(null || undefined ) ?? "foo"; // 返回 "foo"
3.3 與可選鏈操作符 ?. 的關系
空值合并運算符針對 undefined 與 null 這兩個值,可選鏈式操作符 ?. 也是如此。可選鏈式操作符,對于訪問屬性可能為 undefined 與 null 的對象時非常有用。
interface Customer {
name: string;
city?: string;
} let customer: Customer = {
name: "Semlinker" }; let customerCity = customer?.city ?? "Unknown city"; console.log(customerCity); // 輸出:Unknown city
前面我們已經介紹了空值合并運算符的應用場景和使用時的一些注意事項,該運算符不僅可以在 TypeScript 3.7 以上版本中使用。當然你也可以在 JavaScript 的環境中使用它,但你需要借助 Babel,在 Babel 7.8.0 版本也開始支持空值合并運算符。
四、?: 可選屬性
在面向對象語言中,接口是一個很重要的概念,它是對行為的抽象,而具體如何行動需要由類去實現。 TypeScript 中的接口是一個非常靈活的概念,除了可用于對類的一部分行為進行抽象以外,也常用于對「對象的形狀(Shape)」進行描述。
在 TypeScript 中使用 interface 關鍵字就可以聲明一個接口:
interface Person {
name: string;
age: number;
} let semlinker: Person = {
name: "semlinker",
age: 33,
};
在以上代碼中,我們聲明了 Person 接口,它包含了兩個必填的屬性 name 和 age。在初始化 Person 類型變量時,如果缺少某個屬性,TypeScript 編譯器就會提示相應的錯誤信息,比如:
// Property 'age' is missing in type '{ name: string; }' but required in type 'Person'.(2741) let lolo: Person = { // Error name: "lolo" }
為了解決上述的問題,我們可以把某個屬性聲明為可選的:
interface Person {
name: string;
age?: number;
} let lolo: Person = {
name: "lolo" }
4.1 工具類型
4.1.1 Partial<T>
在實際項目開發過程中,為了提高代碼復用率,我們可以利用 TypeScript 內置的工具類型 Partial<T> 來快速把某個接口類型中定義的屬性變成可選的:
interface PullDownRefreshConfig {
threshold: number;
stop: number;
} /**
* type PullDownRefreshOptions = {
* threshold?: number | undefined;
* stop?: number | undefined;
* }
*/ type PullDownRefreshOptions = Partial<PullDownRefreshConfig>
是不是覺得 Partial<T> 很方便,下面讓我們來看一下它是如何實現的:
/**
* Make all properties in T optional
*/ type Partial<T> = {
[P in keyof T]?: T[P];
};
4.1.2 Required<T>
既然可以快速地把某個接口中定義的屬性全部聲明為可選,那能不能把所有的可選的屬性變成必選的呢?答案是可以的,針對這個需求,我們可以使用 Required<T> 工具類型,具體的使用方式如下:
interface PullDownRefreshConfig {
threshold: number;
stop: number;
} type PullDownRefreshOptions = Partial<PullDownRefreshConfig> /**
* type PullDownRefresh = {
* threshold: number;
* stop: number;
* }
*/ type PullDownRefresh = Required<Partial<PullDownRefreshConfig>>
同樣,我們來看一下 Required<T> 工具類型是如何實現的:
/**
* Make all properties in T required
*/ type Required<T> = {
[P in keyof T]-?: T[P];
};
原來在 Required<T> 工具類型內部,通過 -? 移除了可選屬性中的 ?,使得屬性從可選變為必選的。
五、& 運算符
在 TypeScript 中交叉類型是將多個類型合并為一個類型。通過 & 運算符可以將現有的多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性。
type PartialPointX = { x: number; }; type Point = PartialPointX & { y: number; }; let point: Point = {
x: 1,
y: 1 }
在上面代碼中我們先定義了 PartialPointX 類型,接著使用 & 運算符創建一個新的 Point 類型,表示一個含有 x 和 y 坐標的點,然后定義了一個 Point 類型的變量并初始化。
5.1 同名基礎類型屬性的合并
那么現在問題來了,假設在合并多個類型的過程中,剛好出現某些類型存在相同的成員,但對應的類型又不一致,比如:
interface X {
c: string;
d: string;
} interface Y {
c: number;
e: string } type XY = X & Y; type YX = Y & X; let p: XY; let q: YX;
在上面的代碼中,接口 X 和接口 Y 都含有一個相同的成員 c,但它們的類型不一致。對于這種情況,此時 XY 類型或 YX 類型中成員 c 的類型是不是可以是 string 或 number 類型呢?比如下面的例子:
p = { c: 6, d: "d", e: "e" };
q = { c: "c", d: "d", e: "e" };
為什么接口 X 和接口 Y 混入后,成員 c 的類型會變成 never 呢?這是因為混入后成員 c 的類型為 string & number,即成員 c 的類型既可以是 string 類型又可以是 number 類型。很明顯這種類型是不存在的,所以混入后成員 c 的類型為 never。
5.2 同名非基礎類型屬性的合并
在上面示例中,剛好接口 X 和接口 Y 中內部成員 c 的類型都是基本數據類型,那么如果是非基本數據類型的話,又會是什么情形。我們來看個具體的例子:
interface D { d: boolean; } interface E { e: string; } interface F { f: number; } interface A { x: D; } interface B { x: E; } interface C { x: F; } type ABC = A & B & C; let abc: ABC = {
x: {
d: true,
e: 'semlinker',
f: 666 }
}; console.log('abc:', abc);
以上代碼成功運行后,控制臺會輸出以下結果:
由上圖可知,在混入多個類型時,若存在相同的成員,且成員類型為非基本數據類型,那么是可以成功合并。
六、| 分隔符
在 TypeScript 中聯合類型(Union Types)表示取值可以為多種類型中的一種,聯合類型使用 | 分隔每個類型。聯合類型通常與 null 或 undefined 一起使用:
const sayHello = (name: string | undefined) => { /* ... */ };
以上示例中 name 的類型是 string | undefined 意味著可以將 string 或 undefined 的值傳遞給 sayHello 函數。
sayHello("semlinker");
sayHello(undefined);
此外,對于聯合類型來說,你可能會遇到以下的用法:
let num: 1 | 2 = 1; type EventNames = 'click' | 'scroll' | 'mousemove';
示例中的 1、2 或 'click' 被稱為字面量類型,用來約束取值只能是某幾個值中的一個。
6.1 類型保護
當使用聯合類型時,我們必須盡量把當前值的類型收窄為當前值的實際類型,而類型保護就是實現類型收窄的一種手段。
類型保護是可執行運行時檢查的一種表達式,用于確保該類型在一定的范圍內。換句話說,類型保護可以保證一個字符串是一個字符串,盡管它的值也可以是一個數字。類型保護與特性檢測并不是完全不同,其主要思想是嘗試檢測屬性、方法或原型,以確定如何處理值。
目前主要有四種的方式來實現類型保護:
6.1.1 in 關鍵字
interface Admin {
name: string;
privileges: string[];
} interface Employee {
name: string;
startDate: Date;
} type UnknownEmployee = Employee | Admin; function printEmployeeInformation(emp: UnknownEmployee) { console.log("Name: " + emp.name); if ("privileges" in emp) { console.log("Privileges: " + emp.privileges);
} if ("startDate" in emp) { console.log("Start Date: " + emp.startDate);
}
}
6.1.2 typeof 關鍵字
function padLeft(value: string, padding: string | number) { if (typeof padding === "number") { return Array(padding + 1).join(" ") + value;
} if (typeof padding === "string") { return padding + value;
} throw new Error(`Expected string or number, got '${padding}'.`);
}
typeof 類型保護只支持兩種形式:typeof v === "typename" 和 typeof v !== typename,"typename" 必須是 "number", "string", "boolean" 或 "symbol"。 但是 TypeScript 并不會阻止你與其它字符串比較,語言不會把那些表達式識別為類型保護。
6.1.3 instanceof 關鍵字
interface Padder {
getPaddingString(): string;
} class SpaceRepeatingPadder implements Padder { constructor(private numSpaces: number) {}
getPaddingString() { return Array(this.numSpaces + 1).join(" ");
}
} class StringPadder implements Padder { constructor(private value: string) {}
getPaddingString() { return this.value;
}
} let padder: Padder = new SpaceRepeatingPadder(6); if (padder instanceof SpaceRepeatingPadder) { // padder的類型收窄為 'SpaceRepeatingPadder' }
6.1.4 自定義類型保護的類型謂詞(type predicate)
function isNumber(x: any): x is number { return typeof x === "number";
} function isString(x: any): x is string { return typeof x === "string";
}
七、_ 數字分隔符
TypeScript 2.7 帶來了對數字分隔符的支持,正如數值分隔符 ECMAScript 提案中所概述的那樣。對于一個數字字面量,你現在可以通過把一個下劃線作為它們之間的分隔符來分組數字:
const inhabitantsOfMunich = 1_464_301; const distanceEarthSunInKm = 149_600_000; const fileSystemPermission = 0b111_111_000; const bytes = 0b1111_10101011_11110000_00001101;
分隔符不會改變數值字面量的值,但邏輯分組使人們更容易一眼就能讀懂數字。以上 TS 代碼經過編譯后,會生成以下 ES5 代碼:
"use strict"; var inhabitantsOfMunich = 1464301; var distanceEarthSunInKm = 149600000; var fileSystemPermission = 504; var bytes = 262926349;
7.1 使用限制
雖然數字分隔符看起來很簡單,但在使用時還是有一些限制。比如你只能在兩個數字之間添加 _ 分隔符。以下的使用方式是非法的:
// Numeric separators are not allowed here.(6188) 3_.141592 // Error 3._141592 // Error // Numeric separators are not allowed here.(6188) 1_e10 // Error 1e_10 // Error // Cannot find name '_126301'.(2304) _126301 // Error // Numeric separators are not allowed here.(6188) 126301_ // Error // Cannot find name 'b111111000'.(2304) // An identifier or keyword cannot immediately follow a numeric literal.(1351) 0_b111111000 // Error // Numeric separators are not allowed here.(6188) 0b_111111000 // Error
當然你也不能連續使用多個 _ 分隔符,比如:
// Multiple consecutive numeric separators are not permitted.(6189) 123__456 // Error
7.2 解析分隔符
此外,需要注意的是以下用于解析數字的函數是不支持分隔符:
Number()
parseInt()
parseFloat()
這里我們來看一下實際的例子:
Number('123_456') NaN parseInt('123_456') 123 parseFloat('123_456') 123
很明顯對于以上的結果不是我們所期望的,所以在處理分隔符時要特別注意。當然要解決上述問題,也很簡單只需要非數字的字符刪掉即可。這里我們來定義一個 removeNonDigits 的函數:
const RE_NON_DIGIT = /[^0-9]/gu; function removeNonDigits(str) {
str = str.replace(RE_NON_DIGIT, ''); return Number(str);
}
該函數通過調用字符串的 replace 方法來移除非數字的字符,具體的使用方式如下:
removeNonDigits('123_456') 123456 removeNonDigits('149,600,000') 149600000 removeNonDigits('1,407,836') 1407836
八、<Type> 語法
8.1 TypeScript 斷言
有時候你會遇到這樣的情況,你會比 TypeScript 更了解某個值的詳細信息。通常這會發生在你清楚地知道一個實體具有比它現有類型更確切的類型。
通過類型斷言這種方式可以告訴編譯器,“相信我,我知道自己在干什么”。類型斷言好比其他語言里的類型轉換,但是不進行特殊的數據檢查和解構。它沒有運行時的影響,只是在編譯階段起作用。
類型斷言有兩種形式:
8.1.1 “尖括號” 語法
let someValue: any = "this is a string"; let strLength: number = (<string>someValue).length;
8.1.2 as 語法
let someValue: any = "this is a string"; let strLength: number = (someValue as string).length;
8.2 TypeScript 泛型
對于剛接觸 TypeScript 泛型的讀者來說,首次看到 <T> 語法會感到陌生。其實它沒有什么特別,就像傳遞參數一樣,我們傳遞了我們想要用于特定函數調用的類型。
參考上面的圖片,當我們調用 identity<Number>(1) ,Number 類型就像參數 1 一樣,它將在出現 T 的任何位置填充該類型。圖中 <T> 內部的 T 被稱為類型變量,它是我們希望傳遞給 identity 函數的類型占位符,同時它被分配給 value 參數用來代替它的類型:此時 T 充當的是類型,而不是特定的 Number 類型。
其中 T 代表 Type,在定義泛型時通常用作第一個類型變量名稱。但實際上 T 可以用任何有效名稱代替。除了 T 之外,以下是常見泛型變量代表的意思:
K(Key):表示對象中的鍵類型;
V(Value):表示對象中的值類型;
E(Element):表示元素類型。
其實并不是只能定義一個類型變量,我們可以引入希望定義的任何數量的類型變量。比如我們引入一個新的類型變量 U,用于擴展我們定義的 identity 函數:
function identity <T, U>(value: T, message: U) : T { console.log(message); return value;
} console.log(identity<Number, string>(68, "Semlinker"));
除了為類型變量顯式設定值之外,一種更常見的做法是使編譯器自動選擇這些類型,從而使代碼更簡潔。我們可以完全省略尖括號,比如:
function identity <T, U>(value: T, message: U) : T { console.log(message); return value;
} console.log(identity(68, "Semlinker"));
對于上述代碼,編譯器足夠聰明,能夠知道我們的參數類型,并將它們賦值給 T 和 U,而不需要開發人員顯式指定它們。
九、@XXX 裝飾器
9.1 裝飾器語法
對于一些剛接觸 TypeScript 的小伙伴來說,在第一次看到 @Plugin({...}) 這種語法可能會覺得很驚訝。其實這是裝飾器的語法,裝飾器的本質是一個函數,通過裝飾器我們可以方便地定義與對象相關的元數據。
@Plugin({
pluginName: 'Device',
plugin: 'cordova-plugin-device',
pluginRef: 'device',
repo: 'https://github.com/apache/cordova-plugin-device',
platforms: ['Android', 'Browser', 'iOS', 'macOS', 'Windows'],
}) @Injectable() export class Device extends IonicNativePlugin {}
在以上代碼中,我們通過裝飾器來保存 ionic-native 插件的相關元信息,而 @Plugin({...}) 中的 @ 符號只是語法糖,為什么說是語法糖呢?這里我們來看一下編譯生成的 ES5 代碼:
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r;
}; var Device = /** @class */ (function (_super) {
__extends(Device, _super); function Device() { return _super !== null && _super.apply(this, arguments) || this;
}
Device = __decorate([
Plugin({ pluginName: 'Device', plugin: 'cordova-plugin-device', pluginRef: 'device', repo: 'https://github.com/apache/cordova-plugin-device', platforms: ['Android', 'Browser', 'iOS', 'macOS', 'Windows'],
}),
Injectable()
], Device); return Device;
}(IonicNativePlugin));
通過生成的代碼可知,@Plugin({...}) 和 @Injectable() 最終會被轉換成普通的方法調用,它們的調用結果最終會以數組的形式作為參數傳遞給 __decorate 函數,而在 __decorate 函數內部會以 Device 類作為參數調用各自的類型裝飾器,從而擴展對應的功能。
9.2 裝飾器的分類
在 TypeScript 中裝飾器分為類裝飾器、屬性裝飾器、方法裝飾器和參數裝飾器四大類。
9.2.1 類裝飾器
類裝飾器聲明:
declare type ClassDecorator = <TFunction extends Function>(
target: TFunction
) => TFunction | void;
類裝飾器顧名思義,就是用來裝飾類的。它接收一個參數:
target: TFunction - 被裝飾的類
看完第一眼后,是不是感覺都不好了。沒事,我們馬上來個例子:
function Greeter(target: Function): void {
target.prototype.greet = function (): void { console.log("Hello Semlinker!");
};
} @Greeter class Greeting { constructor() { // 內部實現 }
} let myGreeting = new Greeting();
myGreeting.greet(); // console output: 'Hello Semlinker!';
上面的例子中,我們定義了 Greeter 類裝飾器,同時我們使用了 @Greeter 語法糖,來使用裝飾器。
友情提示:讀者可以直接復制上面的代碼,在 TypeScript Playground 中運行查看結果。
9.2.2 屬性裝飾器
屬性裝飾器聲明:
declare type PropertyDecorator = (target:Object,
propertyKey: string | symbol ) => void;
屬性裝飾器顧名思義,用來裝飾類的屬性。它接收兩個參數:
target: Object - 被裝飾的類
propertyKey: string | symbol - 被裝飾類的屬性名
趁熱打鐵,馬上來個例子熱熱身:
function logProperty(target: any, key: string) { delete target[key]; const backingField = "_" + key; Object.defineProperty(target, backingField, {
writable: true,
enumerable: true,
configurable: true }); // property getter const getter = function (this: any) { const currVal = this[backingField]; console.log(`Get: ${key} => ${currVal}`); return currVal;
}; // property setter const setter = function (this: any, newVal: any) { console.log(`Set: ${key} => ${newVal}`); this[backingField] = newVal;
}; // Create new property with getter and setter Object.defineProperty(target, key, { get: getter, set: setter,
enumerable: true,
configurable: true });
} class Person { @logProperty public name: string; constructor(name : string) { this.name = name;
}
} const p1 = new Person("semlinker");
p1.name = "kakuqo";
以上代碼我們定義了一個 logProperty 函數,來跟蹤用戶對屬性的操作,當代碼成功運行后,在控制臺會輸出以下結果:
Set: name => semlinker Set: name => kakuqo
9.2.3 方法裝飾器
方法裝飾器聲明:
declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,
descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;
方法裝飾器顧名思義,用來裝飾類的方法。它接收三個參數:
target: Object - 被裝飾的類
propertyKey: string | symbol - 方法名
descriptor: TypePropertyDescript - 屬性描述符
廢話不多說,直接上例子:
function LogOutput(tarage: Function, key: string, descriptor: any) { let originalMethod = descriptor.value; let newMethod = function(...args: any[]): any { let result: any = originalMethod.apply(this, args); if(!this.loggedOutput) { this.loggedOutput = new Array<any>();
} this.loggedOutput.push({
method: key,
parameters: args,
output: result,
timestamp: new Date()
}); return result;
};
descriptor.value = newMethod;
} class Calculator { @LogOutput double (num: number): number { return num * 2;
}
} let calc = new Calculator();
calc.double(11); // console ouput: [{method: "double", output: 22, ...}] console.log(calc.loggedOutput);
9.2.4 參數裝飾器
參數裝飾器聲明:
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol,
parameterIndex: number ) => void
參數裝飾器顧名思義,是用來裝飾函數參數,它接收三個參數:
target: Object - 被裝飾的類
propertyKey: string | symbol - 方法名
parameterIndex: number - 方法中參數的索引值
function Log(target: Function, key: string, parameterIndex: number) { let functionLogged = key || target.prototype.constructor.name; console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has
been decorated`);
} class Greeter {
greeting: string; constructor(@Log phrase: string) { this.greeting = phrase;
}
} // console output: The parameter in position 0 // at Greeter has been decorated
十、#XXX 私有字段
在 TypeScript 3.8 版本就開始支持 ECMAScript 私有字段,使用方式如下:
class Person {
#name: string; constructor(name: string) { this.#name = name;
}
greet() { console.log(`Hello, my name is ${this.#name}!`);
}
} let semlinker = new Person("Semlinker");
semlinker.#name; // ~~~~~ // Property '#name' is not accessible outside class 'Person' // because it has a private identifier.
與常規屬性(甚至使用 private 修飾符聲明的屬性)不同,私有字段要牢記以下規則:
私有字段以 # 字符開頭,有時我們稱之為私有名稱;
每個私有字段名稱都唯一地限定于其包含的類;
不能在私有字段上使用 TypeScript 可訪問性修飾符(如 public 或 private);
私有字段不能在包含的類之外訪問,甚至不能被檢測到。
10.1 私有字段與 private 的區別
說到這里使用 # 定義的私有字段與 private 修飾符定義字段有什么區別呢?現在我們先來看一個 private 的示例:
class Person { constructor(private name: string){}
} let person = new Person("Semlinker"); console.log(person.name);
在上面代碼中,我們創建了一個 Person 類,該類中使用 private 修飾符定義了一個私有屬性 name,接著使用該類創建一個 person 對象,然后通過 person.name 來訪問 person 對象的私有屬性,這時 TypeScript 編譯器會提示以下異常:
Property 'name' is private and only accessible within class 'Person'.(2341)
那如何解決這個異常呢?當然你可以使用類型斷言把 person 轉為 any 類型:
console.log((person as any).name);
通過這種方式雖然解決了 TypeScript 編譯器的異常提示,但是在運行時我們還是可以訪問到 Person 類內部的私有屬性,為什么會這樣呢?我們來看一下編譯生成的 ES5 代碼,也許你就知道答案了:
var Person = /** @class */ (function () { function Person(name) { this.name = name;
} return Person;
}()); var person = new Person("Semlinker"); console.log(person.name);
這時相信有些小伙伴會好奇,在 TypeScript 3.8 以上版本通過 # 號定義的私有字段編譯后會生成什么代碼:
class Person {
#name: string; constructor(name: string) { this.#name = name;
}
greet() { console.log(`Hello, my name is ${this.#name}!`);
}
}
以上代碼目標設置為 ES2015,會編譯生成以下代碼:
"use strict"; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet)
|| function (receiver, privateMap, value) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to set private field on non-instance");
}
privateMap.set(receiver, value); return value;
}; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet)
|| function (receiver, privateMap) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to get private field on non-instance");
} return privateMap.get(receiver);
}; var _name; class Person { constructor(name) {
_name.set(this, void 0);
__classPrivateFieldSet(this, _name, name);
}
greet() { console.log(`Hello, my name is ${__classPrivateFieldGet(this, _name)}!`);
}
}
_name = new WeakMap();
通過觀察上述代碼,使用 # 號定義的 ECMAScript 私有字段,會通過 WeakMap 對象來存儲,同時編譯器會生成 __classPrivateFieldSet 和 __classPrivateFieldGet 這兩個方法用于設置值和獲取值。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務https://github.com/krasimir/l...
如果你必須在同一個瀏覽器中從一個標簽頁發送消息到另一個標簽頁,你不必用艱難的方式。Local storage bridge在這里讓任務變得更簡單。
基本使用:
// 發送 lsbridge.send(‘app.message.error’, { error: ‘Out of memory’ });
// 監聽 lsbridge.subscribe(‘app.message.error’, function(data) { console.log(data); // { error: ‘Out of memory’ } });
Basil.js統一了session、localStorage和cookie,為你提供了一種處理數據的直接方法。
基本使用:
let basil = new Basil(options);
basil.set(‘name’, ‘Amy’);
basil.get(‘name’);
basil.remove(‘name’);
basil.reset();
https://github.com/marcuswest...
Store.js像其他東西一樣處理數據存儲。但還有更多的功能,它的一個高級特性是讓你更深入地訪問瀏覽器支持。
基本使用:
store.set(‘book’, { title: ‘JavaScript’ }); // Store a book store.get(‘book’);
// Get stored book store.remove(‘book’); // Remove stored book store.clearAll(); // Clear all keys
https://github.com/pamelafox/...
它與localStorage API類似。事實上,它是localStorage的一個封裝器,并使用HTML5模擬memcaches函數。在上面的文檔中發現更多的功能。
基本使用:
lscache.set(‘name’, ‘Amy’, 5); // 數據將在5分鐘后過期 lscache.get(‘name’);
Lockr建立在localStorage API之上。它提供了一些有用的方法來更輕松地處理本地數據。
是什么讓你要使用此庫而不是localStorage API?
好吧,localStorage API僅允許你存儲字符串。如果要存儲數字,則需要先將該數字轉換為字符串。在Lockr中不會發生這種情況,因為Lockr允許你存儲更多的數據類型甚至對象。
基本使用:
Lockr.set(‘name’, ‘Amy’);
Lockr.set(‘age’, 28);
Lockr.set(‘books’, [{title: ‘JavaScript’, price: 11.0}, {title: ‘Python’, price: 9.0}]);
https://github.com/arokor/barn
Barn在localStorage之上提供了一個類似Redis的API。如果持久性很重要,那么你將需要這個庫來保持數據狀態,以防發生錯誤。
基本使用:
let barn = new Barn(localStorage); // 原始類型 barn.set(‘name’, ‘Amy’); let name = barn.get(‘name’);
// Amy // List barn.lpush(‘names’, ‘Amy’);
barn.lpush(‘names’, ‘James’); let name1 = barn.rpop(‘names’); // Amy let name2 = barn.rpop(‘names’);
// James
https://github.com/localForag...
這個簡單而快速的庫將通過IndexedDB或WebSQL使用異步存儲來改善Web的脫機體驗。它類似于localStorage,但具有回調功能。
基本使用:
localforage.setItem(‘name’, ‘Amy’, function(error, value) { // Do something });
localforage.getItem(‘name’, function(error, value) { if (error) { console.log(‘an error occurs’);
} else { // Do something with the value }
});
很神奇的是它提供中文文檔
https://github.com/jas-/crypt.io
crypt.io使用標準JavaScript加密庫實現安全的瀏覽器存儲。使用crypto.io時,有三個存儲選項:sessionStorage,localStorage或cookie。
基本使用:
let storage = crypto; let book = { title: ‘JavaScript’, price: 13 };
storage.set(‘book’, book, function(error, results) { if (error) { throw error;
} // Do something });
storage.get(‘book’, function(error, results) { if (error) { throw error;
} // Do something });
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
在開始正文前,我們先把本文涉及到的一些內容提前定個基調。
Promise 中只有涉及到狀態變更后才需要被執行的回調才算是微任務,比如說 then
、 catch
、finally
,其他所有的代碼執行都是宏任務(同步執行)。
上圖中藍色為同步執行,黃色為異步執行(丟到微任務隊列中)。
這個問題我們根據 ecma 規范來看:
[[PromiseFulfillReactions]]
和 [[PromiseRejectReactions]]
中。如果你看過手寫 Promise 的代碼的話,應該能發現有兩個數組存儲這些回調函數。
了解完以上知識后,正片開始。
Promise.resolve()
.then(() => { console.log("then1"); Promise.resolve().then(() => { console.log("then1-1");
});
})
.then(() => { console.log("then2");
});
以上代碼大家應該都能得出正確的答案:then1 → then1-1 → then2
。
雖然 then
是同步執行,并且狀態也已經變更。但這并不代表每次遇到 then
時我們都需要把它的回調丟入微任務隊列中,而是等待 then
的回調執行完畢后再根據情況執行對應操作。
基于此,我們可以得出第一個結論:鏈式調用中,只有前一個 then
的回調執行完畢后,跟著的 then
中的回調才會被加入至微任務隊列。
大家都知道了 Promise resolve
后,跟著的 then
中的回調會馬上進入微任務隊列。
那么以下代碼你認為的輸出會是什么?
let p = Promise.resolve();
p.then(() => { console.log("then1"); Promise.resolve().then(() => { console.log("then1-1");
});
}).then(() => { console.log("then1-2");
});
p.then(() => { console.log("then2");
});
按照一開始的認知我們不難得出 then2
會在 then1-1
后輸出,但是實際情況卻是相反的。
基于此我們得出第二個結論:每個鏈式調用的開端會首先依次進入微任務隊列。
接下來我們換個寫法:
let p = Promise.resolve().then(() => { console.log("then1"); Promise.resolve().then(() => { console.log("then1-1");
});
}).then(() => { console.log("then2");
});
p.then(() => { console.log("then3");
});
上述代碼其實有個陷阱,then
每次都會返回一個新的 Promise,此時的 p
已經不是 Promise.resolve()
生成的,而是最后一個 then
生成的,因此 then3
應該是在 then2
后打印出來的。
順便我們也可以把之前得出的結論優化為:同一個 Promise 的每個鏈式調用的開端會首先依次進入微任務隊列。
以下大家可以猜猜 then1-2
會在何時打印出來?
Promise.resolve()
.then(() => { console.log("then1"); Promise.resolve()
.then(() => { console.log("then1-1"); return 1;
})
.then(() => { console.log("then1-2");
});
})
.then(() => { console.log("then2");
})
.then(() => { console.log("then3");
})
.then(() => { console.log("then4");
});
這題肯定是簡單的,記住第一個結論就能得出答案,以下是解析:
resolve
后第一個 then
的回調進入微任務隊列并執行,打印 then1
resolve
后內部第一個 then
的回調進入微任務隊列,此時外部第一個 then
的回調全部執行完畢,需要將外部的第二個 then
回調也插入微任務隊列。
then1-1
和 then2
,然后分別再將之后 then
中的回調插入微任務隊列
then1-2
和 then3
,之后的內容就不一一說明了
接下來我們把 return 1
修改一下,結果可就大不相同啦:
Promise.resolve()
.then(() => { console.log("then1"); Promise.resolve()
.then(() => { console.log("then1-1"); return Promise.resolve();
})
.then(() => { console.log("then1-2");
});
})
.then(() => { console.log("then2");
})
.then(() => { console.log("then3");
})
.then(() => { console.log("then4");
});
當我們 return Promise.resolve()
時,你猜猜 then1-2
會何時打印了?
答案是最后一個才被打印出來。
為什么在 then
中分別 return
不同的東西,微任務的執行順序竟有如此大的變化?以下是筆者的解析。
PS:then
返回一個新的 Promise,并且會用這個 Promise 去 resolve
返回值,這個概念需要大家先了解一下。
根據規范 2.3.2,如果 resolve
了一個 Promise,需要為其加上一個 then
并 resolve
。
if (x instanceof MyPromise) { if (x.currentState === PENDING) {
} else {
x.then(resolve, reject);
} return;
}
上述代碼節選自手寫 Promise 實現。
那么根據 A+ 規范來說,如果我們在 then
中返回了 Promise.resolve
的話會多入隊一次微任務,但是這個結論還是與實際不符的,因此我們還需要尋找其他權威的文檔。
根據規范 25.6.1.3.2,當 Promise resolve
了一個 Promise 時,會產生一個NewPromiseResolveThenableJob,這是屬于 Promise Jobs 中的一種,也就是微任務。
This Job uses the supplied thenable and its then method to resolve the given promise. This process must take place as a Job to ensure that the evaluation of the then method occurs after evaluation of any surrounding code has completed.
并且該 Jobs 還會調用一次 then
函數來 resolve Promise
,這也就又生成了一次微任務。
這就是為什么會觸發兩次微任務的來源。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
藍藍設計的小編 http://www.syprn.cn