React V16入門手冊(2)

Little heaven發表於2019-01-23

使用React需要什麼樣的JS能力

在深入學習React之前找到你必須要掌握的內容

如果你想學習React時,首先你需要做一些事情。您必須熟悉一些必要的技術,特別是你將在React中反覆使用的一些最新JavaScript特性相關的技術。

有些人以為一些功能是React提供的一些特定的功能,實際上它們只是Javascript最新的語法。

立即掌握這些新語法,成為這方面的專家是不可能的,也沒必要。但你如果想要深入研究React,那你就需要熟練掌握他們了。

我會把它們列舉下來讓你快速瞭解一下。

變數

變數是分配給識別符號的文字,因此您可以引用它並在後面的程式裡使用它。學習如何用JavaScript宣告一個。

JavaScript中的變數沒有附加任何型別。將特定的文字型別分配給變數後,可以稍後再給這個變數分配型別,而不會出現型別錯誤或任何問題。

所以這就是為什麼有時候Javascript會報'untyped'這樣的問題。

一個變數必須在你使用之前就宣告。有三種方法可以做到這一點,使用var、let或const,這三種方法在以後與變數的互動方式上有所不同。

用Var

知道ES2015,var一直都是定義變數的唯一方法。

var a = 0
複製程式碼

如果你忘了新增var,你給未宣告的變數賦值,結果可能會有所不同,在嚴格模式下,會得到一個錯誤,在舊的環境,或者禁用嚴格模式下,就會使得該變數成為一個全域性變數,賦值也自然會賦值給一個全域性變數。

如果你沒有將變數初始化,那他的值就是undefined

var a //typeof a === 'undefined'

複製程式碼

你可以多次宣告變數

var a = 1
var a = 2
複製程式碼

最重要的,你可以一次就宣告多個變數

var a = 1, b = 2
複製程式碼

作用域是程式碼中變數可見的部分(有效)。在函式外部宣告一個變數,變數就是全域性的,所有的函式都可以獲得該變數的值,但在函式內部宣告一個變數,變數就是區域性的,只有在該函式內才能獲得該變數的值,就像函式的一個引數。

在與全域性變數同名的函式中定義的任何變數都優先於全域性變數,並對其進行跟蹤。

重要的是要理解一個塊(由一對花括號標識)沒有定義一個新的作用域。新作用域只在建立函式時建立,因為var沒有塊作用域,而是函式作用域。

函式內部,在函式的所有程式碼中,變數在任何位置都是可見的,即使函式的變數宣告在函式最後仍然可以引用。,因為JavaScript執行程式碼之前將所有變數提升。但為了避免混淆,總是在函式的開頭宣告變數。

用Let

let是ES2015中引入的一個新特性,它本質上是var的塊範圍版本。它的作用域僅限於定義它的塊、語句或表示式以及所有包含的內部塊。

現代JavaScript開發人員可能選擇只使用let,而完全放棄使用var。

如果let看起來是一個模糊的術語,只要看let color = 'red'就是讓顏色是紅色,這就更容易理解了

與var相反,在任何函式外部定義let都不會建立全域性變數。

用Const

用var或let宣告的變數可以稍後在程式中更改並重新分配。一旦const被初始化,它的值就再也不會被更改,也不能被重新分配到不同的值。

const a = 'test'
複製程式碼

我們不能給a常量定義不同的文字了。但是,如果物件提供了改變其內容的方法,我們可以對它進行變異。 const 定義了就不能修改或重新賦值

const和let一樣都有塊級作用域

現代JavaScript開發人員可能會選擇始終對不需要在程式中稍後重新分配的變數使用const。

為什麼?因為我們應該總是使用最簡單的結構來避免將來犯錯誤。

箭頭函式

箭頭函式是在ES6 / ECMAScript 2015中引入的,自從引入以來,它們徹底改變了JavaScript程式碼的外觀(和工作方式)。

在我看來,這種變化非常受歡迎,以至於你現在很少看到在現代程式碼庫中使用function關鍵字。

從視覺上看,這是一個簡單而受歡迎的改變,它允許你用更短的語法編寫函式,從:

const myFunction = function() {
//...
}
複製程式碼

const myFunction = () => {
//...
}
複製程式碼

如果函式體只包含一條語句,則可以省略括號,並將所有內容寫在一行中:

const myFunction = () => doSomething()
複製程式碼

引數在括號中傳遞:

const myFunction = (param1, param2) => doSomething(param1, param2)
複製程式碼

如果有一個(且只有一個)引數,可以完全省略括號:

const myFunction = param => doSomething(param)
複製程式碼

由於這種簡短的語法,箭頭函式讓我們能使用小體積的函式。

隱式返回

箭頭函式允許你使用隱式返回:返回的值不需要使用return關鍵字。

當函式體中有一行語句時,它就可以工作:

const myFunction = () => 'test'
myFunction() //'test'
複製程式碼

另一個例子,當返回一個物件時,記得將大括號括起來,以避免它被認為是括起來的函式的括號:

const myFunction = () => ({ value: 'test' })
myFunction() //{value: 'test'}
複製程式碼

this在箭頭函式裡如何工作

this是一個很難理解的概念。因為它會根據上下文和JavaScript的模式(嚴格模式或非嚴格模式)產生不同的含義。

澄清這個概念很重要,因為在箭頭函式中的this與常規函式中的this非常不同。

當定義一個物件的方法時,在常規函式中的this指向這個物件,案例如下:

const car = {
    model: 'Fiesta',
    manufacturer: 'Ford',
    fullName: function() {
        return `${this.manufacturer} ${this.model}`
    }
}
car.fullName() //"Ford Fiesta"
複製程式碼

執行car.fullName()時會返回"Ford Fiesta"

帶有箭頭函式的this作用域是從執行上下文中繼承的。箭頭函式根本不繫結this,因此它的值將在呼叫堆疊中查詢。因此在這個程式碼car.fullName()中不起作用,並將返回字串“undefined undefined”:

const car = {
    model: 'Fiesta',
    manufacturer: 'Ford',
    fullName: () => {
        return `${this.manufacturer} ${this.model}`
    }
}
car.fullName() //"undefined undefined"
複製程式碼

參考上面兩個例子,可以看出,箭頭函式並不適用於物件的方法。

箭頭函式也不能用作建構函式,因為例項化物件時將報錯TypeError。當不需要動態上下文時,應該在這裡使用箭頭函式來代替常規函式。

在處理事件時還有一個問題,DOM事件偵聽器將this設定為目標元素,如果在事件處理程式中依賴於此元素,則需要一個常規函式:

const link = document.querySelector('#link')
link.addEventListener('click', () => {
    // this === window
})
複製程式碼
const link = document.querySelector('#link')
link.addEventListener('click', function() {
    // this === link
})
複製程式碼

使用Rest和Spread處理物件和陣列

學習使用JavaScript處理陣列和物件的兩種現代技術

您可以使用spread操作符展開陣列、物件或字串 ...

看下面的例子:

const a = [1, 2, 3]
複製程式碼

你可以像下面一樣建立一個新陣列

const b = [...a, 4, 5, 6]
複製程式碼

還可以像下面一樣建立一個陣列的副本

const b = [...a]
複製程式碼

也能用這種方式拷貝一個物件

const newObj = { ...oldObj }
複製程式碼

使用字串時,spread操作符建立一個陣列,陣列內是每個字元:

const hey = 'hey'
const arrayized = [...hey] // ['h', 'e', 'y']
複製程式碼

這個操作符有一些非常有用的應用。最重要的是能夠以一種非常簡單的方式將陣列作為函式引數:

const f = (foo, bar) => {}
const a = [1, 2]
f(...a)
複製程式碼

(在過去,你可以用f.apply(null, a) 來做這個但是這樣做不太好,可讀性也不好)

rest元素和spread元素在使用陣列解構賦值時非常有用:

const numbers = [1, 2, 3, 4, 5]
[first, second, ...others] = numbers

const numbers = [1, 2, 3, 4, 5]
const sum = (a, b, c, d, e) => a + b + c + d + e
const sum = sum(...numbers)
複製程式碼

ES2018引入了rest屬性,它們是相同的,但是是用於物件。

Rest屬性

const { first, second, ...others } = {
    first: 1,
    second: 2,
    third: 3,
    fourth: 4,
    fifth: 5
}
first // 1
second // 2
others // { third: 3, fourth: 4, fifth: 5 }
複製程式碼

擴充套件屬性允許通過合併在擴充套件操作符之後傳遞的物件屬性來建立一個新物件:

const items = { first, second, ...others }
items //{ first: 1, second: 2, third: 3, fourth: 4, fifth: 5 }
複製程式碼

陣列和物件的解構賦值

學習如何使用解構賦值語法來處理JavaScript中的陣列和物件

給定一個物件,使用解構賦值語法,您可以提取一些值,並將它們放入命名變數:

const person = {
firstName: 'Tom',
lastName: 'Cruise',
actor: true,
age: 54 //made up
}
const { firstName: name, age } = person //name: Tom, age: 54
複製程式碼

name和age包含了所需要的值。

這個語法也可以在陣列上使用

const a = [1, 2, 3, 4, 5]
const [first, second] = a
複製程式碼

該語句通過從陣列a中獲取索引0、1、4的項來建立3個新變數(first,second,fifth)

const [first, second, , , fifth] = a
複製程式碼

模板字串

在ES2015(又名ES6)中引入的模板字串提供了一種宣告字串的新方法,也提供了一些已經非常流行的有趣的新構造方法。

模板字串介紹

模板文字是ES2015 / ES6的新特性,與ES5及以下版本相比,它允許你以一種新穎的方式處理字串。 語法乍一看非常簡單,只需使用反引號而不是單引號或雙引號:

const a_string = `something`
複製程式碼

它們是很獨特的的,因為它們提供了許多用引號構建的普通字串所沒有的特性,特別是:

  • 它們提供了一個很好的語法來定義多行字串
  • 它們提供了一種簡單的方法在字串中用變數和表示式插值
  • 它們允許您使用模板標記建立DSL (DSL意味著特定於領域的語言,例如在React by style元件中使用DSL為元件定義CSS)

讓我們詳細研究上面三個東西

多行字串

在es6之前,要建立一個跨越兩行的字串,您必須在一行末尾使用\字元

const string =
  'first part \
second part'
複製程式碼

這允許在兩行建立一個字串,但它只呈現在一行:

first part second part
複製程式碼

要在多行渲染字串,你需要顯式地在每行末尾新增\n,如下所示:

const string =
  'first line\n \
second line'

//或者
const string = 'first line\n' + 'second line'
複製程式碼

用模板字串就簡單多了

一旦模板文字使用回車,你只需按Enter鍵來建立一個沒有特殊字元的新行,它就會按原樣呈現:

const string = `Hey
this

string
is awesome!`

//結果如下
Hey
this

string
is awesome!
複製程式碼

記住,模板字串的空格是有意義的,所以這樣做:

const string = `First
                Second`
                
                
//結果如下
First
                Second
複製程式碼

解決這個問題的一種簡單方法是,在第一行中設定一個空行,並在結束後加上trim()方法,這將消除第一個字元之前的任何空格:

const string = `
First
Second`.trim()
複製程式碼

模板字串插值

模板字串提供了一種將變數和表示式插入字串的簡單方法。 你可以用這樣的語法${...}

const var = 'test'
const string = `something ${var}` //something test
複製程式碼

${...}裡你可以插入任何東西,甚至是表示式

const string = `something ${1 + 2 + 3}`
const string2 = `something ${foo() ? 'x' : 'y'}`
複製程式碼

Class(類)

2015年,ECMAScript 6 (ES6)標準引入了類。

JavaScript有一種非常少見的實現繼承的方法:原型繼承。雖然原型繼承在我看來很好,但它不同於大多數其他流行程式語言的繼承實現,後者是基於類的。

來自Java、Python或其他語言的人很難理解原型繼承的複雜性,所以ECMAScript委員會決定在原型繼承的基礎上新增語法糖,這樣就像其他流行實現中的基於類的繼承一樣。

這一點很重要:底層的JavaScript仍然是相同的,您還是可以用常規的方式訪問物件原型。

一個class類的定義

一個類長下面這樣

class Person {
  constructor(name) {
    this.name = name
  }
  hello() {
    return 'Hello, I am ' + this.name + '.'
  }
}
複製程式碼

類有一個識別符號,我們可以使用它來使用new ClassIdentifier()建立新物件

初始化物件時,呼叫constructor方法,並傳遞任意引數。

一個類也有它所需要的所有方法。在這種情況下,hello是一個方法,這個類派生的所有物件都可以呼叫這個方法:

const flavio = new Person('Flavio')
flavio.hello()
複製程式碼

類的例項

類可以擴充套件另一個類,使用該類初始化的物件繼承父類的所有方法。

如果繼承的類的方法與層次結構中較高層的類的名稱相同,則最近的方法優先:

class Programmer extends Person {
  hello() {
    return super.hello() + ' I am a programmer.'
  }
}
const flavio = new Programmer('Flavio')
flavio.hello()
//輸出 Hello, I am Flavio. I am a programmer.
複製程式碼

類沒有顯式的類變數宣告,但是必須初始化建構函式中的任何變數

在類中,可以用super()來引用父類。

靜態方法

通用方法是在例項上定義的,而不是在類上定義的。 靜態方法在類上執行:

class Person {
  static genericHello() {
    return 'Hello'
  }
}
Person.genericHello() //Hello
複製程式碼

私有方法

JavaScript沒有內建的方法來定義私有或受保護的方法。(可以參考閉包等概念)

有一些變通方法,但我不會在這裡描述它們。

Getters 和 setters

你可以新增以get或set為字首的方法來建立getter和setter,這是根據您正在做的事情執行兩段不同的程式碼:訪問變數或修改其值。

class Person {
  constructor(name) {
    this._name = name
  }
  set name(value) {
    this._name = value
  }
  get name() {
    return this._name
  }
}
複製程式碼

如果您只有一個getter,則無法設定該屬性,並且任何這樣做的嘗試都將被忽略:

class Person {
  constructor(name) {
    this.name = name
  }
  get name() {
    return this.name
  }
}
複製程式碼

如果你只有一個setter,你可以改變值,但不能從外部訪問它:

class Person {
  constructor(name) {
    this.name = name
  }
  set name(value) {
    this.name = value
  }
}
複製程式碼

回撥

計算機在設計上是非同步的。

非同步意味著事情可以獨立於主程式流發生。

在當前的客戶端計算機中,每個程式都執行一個特定的時間段,然後停止執行,讓另一個程式繼續執行。這個東西以一種無法察覺的速度迴圈執行,我們認為計算機同時執行許多程式,但這是一種錯覺(多處理器機器除外)。

程式在內部使用中斷——這是一種向處理器發出的訊號,以引起系統的注意。

我不會深入討論它的內部原理,但是要記住,程式是非同步的是很正常的,在它們需要注意的時候停止它們的執行,而計算機可以同時執行其他事情。當程式正在等待來自網路的響應時,它不能在請求完成之前停止處理器。

通常,程式語言是同步的,有些語言提供了一種方法來管理語言或庫中的非同步性。C, Java, c#, PHP, Go, Ruby, Swift, Python,它們預設都是同步的。其中一些通過使用執行緒處理非同步,生成一個新程式。

JavaScript預設是同步的,並且是單執行緒的。這意味著程式碼不能建立新執行緒並並行執行。

一行接一行的執行程式碼,例如:

const a = 1
const b = 2
const c = a * b
console.log(c)
doSomething()
複製程式碼

但是JavaScript是在瀏覽器中誕生的,它最初的主要工作是響應使用者操作,比如onClick、onMouseOver、onChange、onSubmit等等。它如何使用同步程式設計模型實現這一點呢?

答案就在它所處的環境中。瀏覽器提供了一種方法,它提供了一組api來處理這種功能。

最近,NodeJS引入了一個非阻塞I/O環境,將這個概念擴充套件到檔案訪問、網路呼叫等。

你不知道使用者什麼時候會點選按鈕,所以你要做的是,為點選事件定義一個事件處理器。此事件處理程式接受一個函式,該函式將在事件觸發時被呼叫:

document.getElementById('button').addEventListener('click', () => {
  //item clicked
})
複製程式碼

這就是回撥(callback)

回撥是一個簡單的函式,它作為一個值傳遞給另一個函式,只在事件發生時執行。我們可以這樣做,因為JavaScript具有一流的函式,可以將其分配給變數並傳遞給其他函式(稱為高階函式)

將所有程式碼包裝在windows物件上的load事件監聽器中是很常見的,它只在頁面準備好時才執行回撥函式:

window.addEventListener('load', () => {
  //window loaded
  //do what you want
})
複製程式碼

回撥可以用在任何地方,不只是dom事件上

一個常用的定時器例子:

setTimeout(() => {
  // runs after 2 seconds
}, 2000)
複製程式碼

XHR請求也接受回撥,在本例中,它將一個函式分配給一個屬性,該屬性將在特定事件發生時被呼叫(在本例中,請求狀態發生變化):

const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
  if (xhr.readyState === 4) {
    xhr.status === 200 ? console.log(xhr.responseText) : console.error('error')
  }
}
xhr.open('GET', 'https://yoursite.com')
xhr.send()
複製程式碼

處理回撥中的錯誤

如何在回撥處理錯誤?一個非常常見的策略是使用Node所採用的方法:任何回撥函式中的第一個引數都是error物件:error-first回撥

如果沒有錯誤,則物件為null。如果有錯誤,它包含錯誤的一些描述和其他資訊。

fs.readFile('/file.json', (err, data) => {
  if (err !== null) {
    //handle error
    console.log(err)
    return
  }
  //no errors, process data
  console.log(data)
})
複製程式碼

回撥存在的問題

回撥對於簡單的情況非常有用

然而,每個回撥都會增加一個巢狀級別,當你有很多回撥時,程式碼開始變得非常複雜:

window.addEventListener('load', () => {
  document.getElementById('button').addEventListener('click', () => {
    setTimeout(() => {
      items.forEach(item => {
        //your code here
      })
    }, 2000)
  })
})
複製程式碼

這只是一個簡單的4層程式碼,但我見過更多級別的巢狀,這並不有趣。 怎麼解呢?

回撥的替代品

從ES6開始,JavaScript引入了幾個特性,幫助我們處理不涉及回撥的非同步程式碼:

  • Promises (ES6)
  • Async/Await (ES8)

Promise

Promise是處理非同步程式碼的一種方法,無需在程式碼中編寫太多回撥。

儘管它們已經存在多年,但是在ES2015中已經被標準化並引入,現在在ES2017中已經被非同步函式所取代。

Async函式使用promise API作為它們的構建塊,因此理解它們是非常重要的,即使在較新的程式碼中您可能會使用Async函式而不是promise。

簡而言之,Promise是如何工作的

一旦Promise被呼叫,它將以pending狀態啟動。這意味著呼叫方函式將繼續執行,同時等待Promise自己進行處理,並給呼叫方函式一些反饋。

此時,呼叫方函式等待它以resolved狀態或rejected狀態返回承諾,但如您所知,JavaScript是非同步的,因此函式在Promise工作時繼續執行。

哪個JS API使用Promise?

除了您自己的程式碼和庫程式碼之外,Promises還被標準的現代Web api(如Fetch或Service Workers)使用。

在現代JavaScript中,您不太可能不使用承諾,所以讓我們開始深入研究它們。

建立一個promise

Promise API公開了一個Promise建構函式,您可以使用它進行初始化new Promise()

let done = true
const isItDoneYet = new Promise((resolve, reject) => {
  if (done) {
    const workDone = 'Here is the thing I built'
    resolve(workDone)
  } else {
    const why = 'Still working on something else'
    reject(why)
  }
})
複製程式碼

正如您所看到的,promise檢查done全域性常量,如果它為真,則返回一個resolve的promise,否則返回一個 reject的promise。

使用resolve和reject,我們可以返回一個值,在上面的例子中,我們只返回一個字串,但它也可以是一個物件。

使用promise

在上一節中,我們介紹瞭如何建立承諾。 現在讓我們看看如何使用承諾。

const isItDoneYet = new Promise()
//...
const checkIfItsDone = () => {
  isItDoneYet
    .then(ok => {
      console.log(ok)
    })
    .catch(err => {
      console.error(err)
    })
}
複製程式碼

執行checkIfItsDone()將執行isItDoneYet() promise,並使用then回撥等待它解決,如果出現錯誤,它將在catch回撥中處理它。

promise鏈

一個promise可以返回給另一個promise,建立一個promise鏈。

Fetch API是連結承諾的一個很好的例子,它是XMLHttpRequest API之上的一層,我們可以使用它來獲取資源,並在獲取資源時對要執行的promise鏈進行排隊。

Fetch API是一種基於promise的機制,呼叫Fetch()相當於使用new promise()定義我們自己的promise。

例子:

const status = response => {
  if (response.status >= 200 && response.status < 300) {
    return Promise.resolve(response)
  }
  return Promise.reject(new Error(response.statusText))
}
const json = response => response.json()
fetch('/todos.json')
  .then(status)
  .then(json)
  .then(data => {
    console.log('Request succeeded with JSON response', data)
  })
  .catch(error => {
    console.log('Request failed', error)
  })
複製程式碼

在本例中,我們呼叫fetch()從TODO中獲取TODO項的列表。在域根目錄中找到json檔案,然後建立promise鏈。

執行 fetch()後返回一個響應,它有很多屬性,在這些屬性中我們引用:

  • status表示HTTP狀態程式碼的數值
  • statusText狀態訊息,如果是OK就是請求成功

response也有一個json()方法,它返回一個promise,該promise將解析處理並轉換為JSON的主體內容。

在這些前提下,會發生這樣的情況:鏈中的第一個promise是我們定義的一個函式status(),它檢查響應狀態,如果它不是一個成功響應(在200到299之間),則拒絕該promise。

此操作將導致promise鏈跳過列出的所有連結的promise,並直接跳到底部的catch()語句,記錄請求失敗的文字和錯誤訊息。

如果成功,則呼叫我們定義的json()函式。由於上一個promise成功時返回響應物件,所以我們將它作為第二個promise的輸入。

在這種情況下,我們返回JSON處理過的資料,所以第三個promise直接接收JSON:

.then((data) => {
  console.log('Request succeeded with JSON response', data)
})
複製程式碼

我們只需將其列印到控制檯

處理錯誤

在上面的例子中,在上一節中,我們有一個catch,它被附加到promise鏈中。

當promise鏈中的任何內容失敗並引發錯誤或拒絕promise時,該控制元件將轉到鏈中最近的catch()語句。

new Promise((resolve, reject) => {
  throw new Error('Error')
}).catch(err => {
  console.error(err)
})
// or
new Promise((resolve, reject) => {
  reject('Error')
}).catch(err => {
  console.error(err)
})
複製程式碼

串聯錯誤

如果在catch()中引發錯誤,可以附加第二個catch()來處理它,依此類推。

new Promise((resolve, reject) => {
  throw new Error('Error')
})
  .catch(err => {
    throw new Error('Error')
  })
  .catch(err => {
    console.error(err)
  })
複製程式碼

用 Promise.all()來編排promise

如果您需要同步執行不同的promise,Promise.all()可以幫助您定義一個promise列表,並在所有promise都得到解析時執行某些操作。

例子:

const f1 = fetch('/something.json')
const f2 = fetch('/something2.json')
Promise.all([f1, f2])
  .then(res => {
    console.log('Array of results', res)
  })
  .catch(err => {
    console.error(err)
  })
複製程式碼

ES2015析構賦值語法也允許您這樣做

Promise.all([f1, f2]).then(([res1, res2]) => {
  console.log('Results', res1, res2)
})
複製程式碼

當然你不侷限於使用fetch,任何promise都是好的。

用Promise.race()編排promise

Promise.race()在您傳遞給它的某個promise解析時立即執行,並且在解析第一個promise的結果時,它只執行附加的回撥一次(最先執行成功的promise後就返回該promise,其他的promise就不管了)。

例子:

const promiseOne = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one')
})
const promiseTwo = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'two')
})
Promise.race([promiseOne, promiseTwo]).then(result => {
  console.log(result) // 'two'
})
複製程式碼

Async/Await

JavaScript在很短的時間內從回撥發展到了promise (ES2015),而且由於ES2017非同步JavaScript使用async/ wait語法更加簡單。

非同步函式是 promise和generate的組合,基本上,它們是比promise更高層次的抽象。讓我重複一遍:async/ wait基於promise。

為什麼要Async/Await

這種方式減少了promise的使用,和‘不打破promise鏈’的限制

當promise在ES2015中引入時,它們是為了解決非同步程式碼的問題,它們確實解決了這個問題,但是在ES2015和ES2017分開的兩年時間裡,很明顯promise並不是最終的解決方案。

引入promise是為了解決著名的回撥地獄問題,但它們本身也帶來了複雜性,以及語法複雜性。

它們語義化更好,可以向開發人員提供更好的語法,因此當時機成熟時,我們就可以使用async函式。

它們使程式碼看起來是同步的,但在幕後它是非同步的,非阻塞的。

async如何工作的

一個async函式返回一個promise,就像這個例子:

const doSomethingAsync = () => {
  return new Promise(resolve => {
    setTimeout(() => resolve('I did something'), 3000)
  })
}
複製程式碼

當您想要呼叫這個函式時,您需要預先等待,呼叫程式碼將停止,直到promise被resolve或reject。一個警告:函式必須定義為async的。這裡有一個例子:

const doSomething = async () => {
  console.log(await doSomethingAsync())
}
複製程式碼

一個快速的案例

這是一個簡單的async/await 的例子,用於非同步執行一個函式:

const doSomethingAsync = () => {
  return new Promise(resolve => {
    setTimeout(() => resolve('I did something'), 3000)
  })
}
const doSomething = async () => {
  console.log(await doSomethingAsync())
}
console.log('Before')
doSomething()
console.log('After')
複製程式碼

以上程式碼將列印以下內容到瀏覽器控制檯:

Before
After
I did something //after 3s
複製程式碼

所有的事都是promise

在任何函式前面加上async關鍵字意味著函式將返回一個promise。

即使它沒有顯式地返回promise,它也會在內部讓它返回一個promise。

這就是為什麼這個程式碼是有效的:

const aFunction = async () => {
  return 'test'
}
aFunction().then(alert) // This will alert 'test'
複製程式碼

上面也和下面一樣

const aFunction = async () => {
  return Promise.resolve('test')
}
aFunction().then(alert) // This will alert 'test'
複製程式碼

程式碼更易讀

正如您在上面的示例中看到的,我們的程式碼看起來非常簡單。將其與使用純promise(帶有連結和回撥函式)的程式碼進行比較。

這是一個非常簡單的例子,當程式碼更加複雜時,主要的好處就會顯現出來。

例如,下面是如何獲得JSON資源,並使用promise對其進行解析:

const getFirstUserData = () => {
  return fetch('/users.json') // get users list
    .then(response => response.json()) // parse JSON
    .then(users => users[0]) // pick first user
    .then(user => fetch(`/users/${user.name}`)) // get user data
    .then(userResponse => response.json()) // parse JSON
}
getFirstUserData()
複製程式碼

用await/async來實現上面的功能時

const getFirstUserData = async () => {
  const response = await fetch('/users.json') // get users list
  const users = await response.json() // parse JSON
  const user = users[0] // pick first user
  const userResponse = await fetch(`/users/${user.name}`) // get user data
  const userData = await user.json() // parse JSON
  return userData
}
getFirstUserData()
複製程式碼

串聯多個非同步函式

非同步函式可以很容易地連結起來,而且語法比普通的承諾更易讀

const promiseToDoSomething = () => {
  return new Promise(resolve => {
    setTimeout(() => resolve('I did something'), 10000)
  })
}
const watchOverSomeoneDoingSomething = async () => {
  const something = await promiseToDoSomething()
  return something + ' and I watched'
}
const watchOverSomeoneWatchingSomeoneDoingSomething = async () => {
  const something = await watchOverSomeoneDoingSomething()
  return something + ' and I watched as well'
}
watchOverSomeoneWatchingSomeoneDoingSomething().then(res => {
  console.log(res)
})

//輸出
I did something and I watched and I watched as well
複製程式碼

更好debug

除錯promise很困難,因為偵錯程式不會跳過非同步程式碼。

Async/ wait使這一切變得非常簡單,因為對於編譯器來說,它就像同步程式碼一樣。

ES 模組

ES模組是用於處理模組的ECMAScript標準。

nodeJS多年來一直使用CommonJS標準,瀏覽器從來沒有模組系統,因為每一個重大決策,比如模組系統,都必須首先由ECMAScript標準化,然後由瀏覽器實現。

這個標準化過程用ES6完成,瀏覽器開始實現這個標準,試圖保持一切正常執行,以相同的方式工作,現在在Chrome、Safari、Edge和Firefox(從版本60開始)中都支援ES模組。

模組非常酷,因為它們允許您封裝各種功能,並將這些功能作為庫公開給其他JavaScript檔案。

ES模組語法

匯入模組的語法是:

import package from 'module-name'
複製程式碼

用CommonJS 時:

const package = require('module-name')
複製程式碼

模組是一個JavaScript檔案,它使用export關鍵字匯出一個或多個值(物件、函式或變數)。例如,這個模組匯出一個函式,返回一個大寫字串:

uppercase.js

export default str => str.toUpperCase()
複製程式碼

在本例中,模組定義了一個default export,因此它可以是一個匿名函式。否則,它需要一個名稱來將其與其他匯出區分開。

現在,通過匯入這個檔案,任何其他JavaScript模組都可以用匯入的uppercase.js提供的功能。 HTML頁面可以使用 <script> 標記新增模組,該標記具有特殊的type="module"屬性:

<script type="module" src="index.js"></script>
複製程式碼

注意:此模組匯入的行為類似於defer指令碼載入。

需要注意的是,使用type="module"載入的任何指令碼都是在嚴格模式下載入的。

在這個例子中,uppercase.js 模組定義了一個 default export,所以當我們匯入它的時候,我們可以給它分配一個我們喜歡的名字:

import toUpperCase from './uppercase.js'
複製程式碼

我們可以這樣使用

toUpperCase('test') //'TEST'
複製程式碼

您還可以使用模組匯入的絕對路徑,來引用在另一個域中定義的模組:

import toUpperCase from 'https://flavio-es-modules-example.glitch.me/uppercase.js'
複製程式碼

這也是有效的匯入語法:

import { foo } from '/uppercase.js'
import { foo } from '../uppercase.js'
複製程式碼

下面是不對的:

import { foo } from 'uppercase.js'
import { foo } from 'utils/uppercase.js'
複製程式碼

它要麼是絕對的,要麼在名字前有一個./或者/。

其他 import/export方法

我們看到上面的例子:

export default str => str.toUpperCase()
複製程式碼

這將建立一個預設匯出。在一個檔案中,你可以匯出多個東西,通過使用以下語法:

const a = 1
const b = 2
const c = 3
export { a, b, c }
複製程式碼

另一個模組可以使用import *來匯入所有這些export的內容

import * from 'module'
複製程式碼

你可以只匯入其中的幾個匯出,使用析構賦值:

import { a } from 'module'
import { a, b } from 'module'
複製程式碼

為了方便,可以使用as重新命名任何匯入

import { a, b as two } from 'module'
複製程式碼

您可以按名稱匯入預設匯出和任何非預設匯出,如以下常見的React匯入:

import React, { Component } from 'react'
複製程式碼

CORS(跨域)

使用CORS獲取模組。這意味著如果您引用來自其他域的指令碼,它們必須具有允許跨站點載入的有效CORS頭(比如Access-Control-Allow-Origin: *)

那麼不支援模組的瀏覽器呢?

結合使用type="module"nomodule

<script type="module" src="module.js"></script>
<script nomodule src="fallback.js"></script>
複製程式碼

ES模組是現代瀏覽器中引入的最大特性之一。它們是ES6的一部分,但實現它們的道路是漫長的。

我們現在可以使用它們了!但是我們還必須記住,如果有多個模組,那麼頁面的效能將受到影響,因為這是瀏覽器在執行時必須執行更多一個步驟。

即使ES模組在瀏覽器裡能用了,Webpack可能仍然是一個巨大的玩家,但是直接在語言中構建這樣的特性對於統一模組在客戶端和nodeJS的工作方式是非常重要的。

下節預告:React的概念

相關文章