Vue3+TypeScript?看這一篇就夠了

1、TypeScript快速上手

1.1 初識 TypeScript

TS與JS.png

TypeScript 的介紹

TypeScript是一種由微軟開發的開源、跨平臺的編程語言。它是JavaScript的超集,最終會被編譯爲JavaScript代碼。

2012年10月,微軟發佈了首個公開版本的TypeScript,2013年6月19日,在經歷了一個預覽版之後微軟正式發佈了正式版TypeScript

TypeScript的作者是安德斯·海爾斯伯格,C#的首席架構師。它是開源和跨平臺的編程語言。

TypeScript擴展了JavaScript的語法,所以任何現有的JavaScript程序可以運行在TypeScript環境中。

TypeScript是爲大型應用的開發而設計,並且可以編譯爲JavaScript。

TypeScript 是 JavaScript 的一個超集,主要提供了類型系統和對 ES6+ 的支持**,它由 Microsoft 開發,代碼開源於 GitHub 上

TypeScript 是 JavaScript 的一個超集,主要提供了類型系統對 ES6+ 的支持,它由 Microsoft 開發,代碼開源於 GitHub

TypeScript 的特點

TypeScript 主要有 3 大特點:

  • 始於JavaScript,歸於JavaScript

TypeScript 可以編譯出純淨、 簡潔的 JavaScript 代碼,並且可以運行在任何瀏覽器上、Node.js 環境中和任何支持 ECMAScript 3(或更高版本)的JavaScript 引擎中。

  • 強大的類型系統

類型系統允許 JavaScript 開發者在開發 JavaScript 應用程序時使用高效的開發工具和常用操作比如靜態檢查和代碼重構。

  • 先進的 JavaScript

TypeScript 提供最新的和不斷髮展的 JavaScript 特性,包括那些來自 2015 年的 ECMAScript 和未來的提案中的特性,比如異步功能和 Decorators,以幫助建立健壯的組件。

總結

TypeScript 在社區的流行度越來越高,它非常適用於一些大型項目,也非常適用於一些基礎庫,極大地幫助我們提升了開發效率和體驗。

1.2 安裝 TypeScript

命令行運行如下命令,全局安裝 TypeScript:

npm install -g typescript

安裝完成後,在控制檯運行如下命令,檢查安裝是否成功(3.x):

tsc -V 

1.3. 第一個 TypeScript 程序

編寫 TS 程序

src/helloworld.ts

function greeter (person) {
  return 'Hello, ' + person
}

let user = 'Yee'

console.log(greeter(user))

手動編譯代碼

我們使用了 .ts 擴展名,但是這段代碼僅僅是 JavaScript 而已。

在命令行上,運行 TypeScript 編譯器:

tsc helloworld.ts

輸出結果爲一個 helloworld.js 文件,它包含了和輸入文件中相同的 JavsScript 代碼。

在命令行上,通過 Node.js 運行這段代碼:

node helloworld.js

控制檯輸出:

Hello, Yee

VsCode自動編譯

1). 生成配置文件tsconfig.json
    tsc --init
2). 修改tsconfig.json配置
    "outDir": "./js",
    "strict": false,    
3). 啓動監視任務: 
    終端 -> 運行任務 -> 監視tsconfig.json

類型註解

接下來讓我們看看 TypeScript 工具帶來的高級功能。 給 person 函數的參數添加 : string 類型註解,如下:

function greeter (person: string) {
  return 'Hello, ' + person
}

let user = 'Yee'

console.log(greeter(user))

TypeScript 裏的類型註解是一種輕量級的爲函數或變量添加約束的方式。 在這個例子裏,我們希望 greeter 函數接收一個字符串參數。 然後嘗試把 greeter 的調用改成傳入一個數組:

function greeter (person: string) {
  return 'Hello, ' + person
}

let user = [0, 1, 2]

console.log(greeter(user))

重新編譯,你會看到產生了一個錯誤:

error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.

類似地,嘗試刪除 greeter 調用的所有參數。 TypeScript 會告訴你使用了非期望個數的參數調用了這個函數。 在這兩種情況中,TypeScript提供了靜態的代碼分析,它可以分析代碼結構和提供的類型註解。

要注意的是儘管有錯誤,greeter.js 文件還是被創建了。 就算你的代碼裏有錯誤,你仍然可以使用 TypeScript。但在這種情況下,TypeScript 會警告你代碼可能不會按預期執行。

接口

讓我們繼續擴展這個示例應用。這裏我們使用接口來描述一個擁有 firstNamelastName 字段的對象。 在 TypeScript 裏,只在兩個類型內部的結構兼容,那麼這兩個類型就是兼容的。 這就允許我們在實現接口時候只要保證包含了接口要求的結構就可以,而不必明確地使用 implements 語句。

interface Person {
  firstName: string
  lastName: string
}

function greeter (person: Person) {
  return 'Hello, ' + person.firstName + ' ' + person.lastName
}

let user = {
  firstName: 'Yee',
  lastName: 'Huang'
}

console.log(greeter(user))

最後,讓我們使用類來改寫這個例子。 TypeScript 支持 JavaScript 的新特性,比如支持基於類的面向對象編程。

讓我們創建一個 User 類,它帶有一個構造函數和一些公共字段。因爲類的字段包含了接口所需要的字段,所以他們能很好的兼容。

還要注意的是,我在類的聲明上會註明所有的成員變量,這樣比較一目瞭然。

class User {
  fullName: string
  firstName: string
  lastName: string

  constructor (firstName: string, lastName: string) {
    this.firstName = firstName
    this.lastName = lastName
    this.fullName = firstName + ' ' + lastName
  }
}

interface Person {
  firstName: string
  lastName: string
}

function greeter (person: Person) {
  return 'Hello, ' + person.firstName + ' ' + person.lastName
}

let user = new User('Yee', 'Huang')

console.log(greeter(user))

重新運行 tsc greeter.ts,你會看到 TypeScript 裏的類只是一個語法糖,本質上還是 JavaScript 函數的實現。

總結

到這裏,你已經對 TypeScript 有了一個大致的印象,那麼下一章讓我們來一起學習 TypeScript 的一些常用語法吧。

1.4 使用webpack打包TS

下載依賴

yarn add -D typescript
yarn add -D webpack webpack-cli
yarn add -D webpack-dev-server
yarn add -D html-webpack-plugin clean-webpack-plugin
yarn add -D ts-loader
yarn add -D cross-env

入口JS: src/main.ts

// import './01_helloworld'

document.write('Hello Webpack TS!')

index頁面: public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>webpack & TS</title>
</head>
<body>
  
</body>
</html>

build/webpack.config.js

const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')

const isProd = process.env.NODE_ENV === 'production' // 是否生產環境

function resolve (dir) {
  return path.resolve(__dirname, '..', dir)
}

module.exports = {
  mode: isProd ? 'production' : 'development',
  entry: {
    app: './src/main.ts'
  },

  output: {
    path: resolve('dist'),
    filename: '[name].[contenthash:8].js'
  },

  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        include: [resolve('src')]
      }
    ]
  },

  plugins: [
    new CleanWebpackPlugin({
    }),

    new HtmlWebpackPlugin({
      template: './public/index.html'
    })
  ],

  resolve: {
    extensions: ['.ts', '.tsx', '.js']
  },

  devtool: isProd ? 'cheap-module-source-map' : 'cheap-module-eval-source-map',

  devServer: {
    host: 'localhost', // 主機名
    stats: 'errors-only', // 打包日誌輸出輸出錯誤信息
    port: 8081,
    open: true
  },
}

配置打包命令

"dev": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.js",
"build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js"

運行與打包

yarn dev
yarn build

2、TypeScript常用語法

2.1 基礎類型

TypeScript 支持與 JavaScript 幾乎相同的數據類型,此外還提供了實用的枚舉類型方便我們使用。

布爾值

最基本的數據類型就是簡單的 true/false 值,在JavaScript 和 TypeScript 裏叫做 boolean(其它語言中也一樣)。

let isDone: boolean = false;
isDone = true;
// isDone = 2 // error

數字

和 JavaScript 一樣,TypeScript 裏的所有數字都是浮點數。 這些浮點數的類型是 number。 除了支持十進制和十六進制字面量,TypeScript 還支持 ECMAScript 2015中引入的二進制和八進制字面量。

let a1: number = 10 // 十進制
let a2: number = 0b1010  // 二進制
let a3: number = 0o12 // 八進制
let a4: number = 0xa // 十六進制

字符串

JavaScript 程序的另一項基本操作是處理網頁或服務器端的文本數據。 像其它語言裏一樣,我們使用 string 表示文本數據類型。 和 JavaScript 一樣,可以使用雙引號(")或單引號(')表示字符串。

let name:string = 'tom'
name = 'jack'
// name = 12 // error
let age:number = 12
const info = `My name is ${name}, I am ${age} years old!`

undefined 和 null

TypeScript 裏,undefinednull 兩者各自有自己的類型分別叫做 undefinednull。 它們的本身的類型用處不是很大:

let u: undefined = undefined
let n: null = null

默認情況下 nullundefined 是所有類型的子類型。 就是說你可以把 nullundefined 賦值給 number 類型的變量。

數組

TypeScript 像 JavaScript 一樣可以操作數組元素。 有兩種方式可以定義數組。 第一種,可以在元素類型後面接上[],表示由此類型元素組成的一個數組:

let list1: number[] = [1, 2, 3]

第二種方式是使用數組泛型,Array<元素類型>

let list2: Array<number> = [1, 2, 3]

元組 Tuple

元組類型允許表示一個已知元素數量和類型的數組,各元素的類型不必相同。 比如,你可以定義一對值分別爲 stringnumber 類型的元組。

let t1: [string, number]
t1 = ['hello', 10] // OK
t1 = [10, 'hello'] // Error

當訪問一個已知索引的元素,會得到正確的類型:

console.log(t1[0].substring(1)) // OK
console.log(t1[1].substring(1)) // Error, 'number' 不存在 'substring' 方法

枚舉

enum 類型是對 JavaScript 標準數據類型的一個補充。 使用枚舉類型可以爲一組數值賦予友好的名字

enum Color {
  Red,
  Green,
  Blue
}

// 枚舉數值默認從0開始依次遞增
// 根據特定的名稱得到對應的枚舉數值
let myColor: Color = Color.Green  // 0
console.log(myColor, Color.Red, Color.Blue)

默認情況下,從 0 開始爲元素編號。 你也可以手動的指定成員的數值。 例如,我們將上面的例子改成從 1 開始編號:

enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green

或者,全部都採用手動賦值:

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green

枚舉類型提供的一個便利是你可以由枚舉的值得到它的名字。 例如,我們知道數值爲 2,但是不確定它映射到 Color 裏的哪個名字,我們可以查找相應的名字:

enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2]

console.log(colorName)  // 'Green'

any

有時候,我們會想要爲那些在編程階段還不清楚類型的變量指定一個類型。 這些值可能來自於動態的內容,比如來自用戶輸入或第三方代碼庫。 這種情況下,我們不希望類型檢查器對這些值進行檢查而是直接讓它們通過編譯階段的檢查。 那麼我們可以使用 any 類型來標記這些變量:

let notSure: any = 4
notSure = 'maybe a string'
notSure = false // 也可以是個 boolean

在對現有代碼進行改寫的時候,any 類型是十分有用的,它允許你在編譯時可選擇地包含或移除類型檢查。並且當你只知道一部分數據的類型時,any 類型也是有用的。 比如,你有一個數組,它包含了不同的類型的數據:

let list: any[] = [1, true, 'free']

list[1] = 100

void

某種程度上來說,void 類型像是與 any 類型相反,它表示沒有任何類型。 當一個函數沒有返回值時,你通常會見到其返回值類型是 void

/* 表示沒有任何類型, 一般用來說明函數的返回值不能是undefined和null之外的值 */
function fn(): void {
  console.log('fn()')
  // return undefined
  // return null
  // return 1 // error
}

聲明一個 void 類型的變量沒有什麼大用,因爲你只能爲它賦予 undefinednull

let unusable: void = undefined

object

object 表示非原始類型,也就是除 numberstringboolean之外的類型。

使用 object 類型,就可以更好的表示像 Object.create 這樣的 API。例如:

function fn2(obj:object):object {
  console.log('fn2()', obj)
  return {}
  // return undefined
  // return null
}
console.log(fn2(new String('abc')))
// console.log(fn2('abc') // error
console.log(fn2(String))

聯合類型

聯合類型(Union Types)表示取值可以爲多種類型中的一種
需求1: 定義一個一個函數得到一個數字或字符串值的字符串形式值

function toString2(x: number | string) : string {
  return x.toString()
}

需求2: 定義一個一個函數得到一個數字或字符串值的長度

function getLength(x: number | string) {

  // return x.length // error

  if (x.length) { // error
    return x.length
  } else {
    return x.toString().length
  }
}

類型斷言

通過類型斷言這種方式可以告訴編譯器,“相信我,我知道自己在幹什麼”。 類型斷言好比其它語言裏的類型轉換,但是不進行特殊的數據檢查和解構。 它沒有運行時的影響,只是在編譯階段起作用。 TypeScript 會假設你,程序員,已經進行了必須的檢查。

類型斷言有兩種形式。 其一是“尖括號”語法, 另一個爲 as 語法

/* 
類型斷言(Type Assertion): 可以用來手動指定一個值的類型
語法:
    方式一: <類型>值
    方式二: 值 as 類型  tsx中只能用這種方式
*/

/* 需求: 定義一個函數得到一個字符串或者數值數據的長度 */
function getLength(x: number | string) {
  if ((<string>x).length) {
    return (x as string).length
  } else {
    return x.toString().length
  }
}
console.log(getLength('abcd'), getLength(1234))

類型推斷

類型推斷: TS會在沒有明確的指定類型的時候推測出一個類型
有下面2種情況: 1. 定義變量時賦值了, 推斷爲對應的類型. 2. 定義變量時沒有賦值, 推斷爲any類型

/* 定義變量時賦值了, 推斷爲對應的類型 */
let b9 = 123 // number
// b9 = 'abc' // error

/* 定義變量時沒有賦值, 推斷爲any類型 */
let b10  // any類型
b10 = 123
b10 = 'abc'

2.2 接口

TypeScript 的核心原則之一是對值所具有的結構進行類型檢查。我們使用接口(Interfaces)來定義對象的類型。接口是對象的狀態(屬性)和行爲(方法)的抽象(描述)

接口初探

需求: 創建人的對象, 需要對人的屬性進行一定的約束

id是number類型, 必須有, 只讀的
name是string類型, 必須有
age是number類型, 必須有
sex是string類型, 可以沒有

下面通過一個簡單示例來觀察接口是如何工作的:

/* 
在 TypeScript 中,我們使用接口(Interfaces)來定義對象的類型
接口: 是對象的狀態(屬性)和行爲(方法)的抽象(描述)
接口類型的對象
    多了或者少了屬性是不允許的
    可選屬性: ?
    只讀屬性: readonly
*/

/* 
需求: 創建人的對象, 需要對人的屬性進行一定的約束
  id是number類型, 必須有, 只讀的
  name是string類型, 必須有
  age是number類型, 必須有
  sex是string類型, 可以沒有
*/

// 定義人的接口
interface IPerson {
  id: number
  name: string
  age: number
  sex: string
}

const person1: IPerson = {
  id: 1,
  name: 'tom',
  age: 20,
  sex: '男'
}

類型檢查器會查看對象內部的屬性是否與IPerson接口描述一致, 如果不一致就會提示類型錯誤。

可選屬性

接口裏的屬性不全都是必需的。 有些是隻在某些條件下存在,或者根本不存在。

interface IPerson {
  id: number
  name: string
  age: number
  sex?: string
}

帶有可選屬性的接口與普通的接口定義差不多,只是在可選屬性名字定義的後面加一個 ? 符號。

可選屬性的好處之一是可以對可能存在的屬性進行預定義,好處之二是可以捕獲引用了不存在的屬性時的錯誤。

const person2: IPerson = {
  id: 1,
  name: 'tom',
  age: 20,
  // sex: '男' // 可以沒有
}

只讀屬性

一些對象屬性只能在對象剛剛創建的時候修改其值。 你可以在屬性名前用 readonly 來指定只讀屬性:

interface IPerson {
  readonly id: number
  name: string
  age: number
  sex?: string
}

一旦賦值後再也不能被改變了。

const person2: IPerson = {
  id: 2,
  name: 'tom',
  age: 20,
  // sex: '男' // 可以沒有
  // xxx: 12 // error 沒有在接口中定義, 不能有
}
person2.id = 2 // error

readonly vs const

最簡單判斷該用 readonly 還是 const 的方法是看要把它做爲變量使用還是做爲一個屬性。 做爲變量使用的話用 const,若做爲屬性則使用 readonly

函數類型

接口能夠描述 JavaScript 中對象擁有的各種各樣的外形。 除了描述帶有屬性的普通對象外,接口也可以描述函數類型。

爲了使用接口表示函數類型,我們需要給接口定義一個調用簽名。它就像是一個只有參數列表和返回值類型的函數定義。參數列表裏的每個參數都需要名字和類型。

/* 
接口可以描述函數類型(參數的類型與返回的類型)
*/

interface SearchFunc {
  (source: string, subString: string): boolean
}

這樣定義後,我們可以像使用其它接口一樣使用這個函數類型的接口。 下例展示瞭如何創建一個函數類型的變量,並將一個同類型的函數賦值給這個變量。

const mySearch: SearchFunc = function (source: string, sub: string): boolean {
  return source.search(sub) > -1
}

console.log(mySearch('abcd', 'bc'))

類類型

類實現接口

與 C# 或 Java 裏接口的基本作用一樣,TypeScript 也能夠用它來明確的強制一個類去符合某種契約。

/* 
類類型: 實現接口
1. 一個類可以實現多個接口
2. 一個接口可以繼承多個接口
*/

interface Alarm {
  alert(): any;
}

interface Light {
  lightOn(): void;
  lightOff(): void;
}

class Car implements Alarm {
  alert() {
      console.log('Car alert');
  }
}

一個類可以實現多個接口

class Car2 implements Alarm, Light {
  alert() {
    console.log('Car alert');
  }
  lightOn() {
    console.log('Car light on');
  }
  lightOff() {
    console.log('Car light off');
  }
}

接口繼承接口

和類一樣,接口也可以相互繼承。 這讓我們能夠從一個接口裏複製成員到另一個接口裏,可以更靈活地將接口分割到可重用的模塊裏。

interface LightableAlarm extends Alarm, Light {

}

2.3 類

對於傳統的 JavaScript 程序我們會使用函數基於原型的繼承來創建可重用的組件,但對於熟悉使用面向對象方式的程序員使用這些語法就有些棘手,因爲他們用的是基於類的繼承並且對象是由類構建出來的。 從 ECMAScript 2015,也就是 ES6 開始, JavaScript 程序員將能夠使用基於類的面向對象的方式。 使用 TypeScript,我們允許開發者現在就使用這些特性,並且編譯後的 JavaScript 可以在所有主流瀏覽器和平臺上運行,而不需要等到下個 JavaScript 版本。

基本示例

下面看一個使用類的例子:

/* 
類的基本定義與使用
*/

class Greeter {
  // 聲明屬性
  message: string

  // 構造方法
  constructor (message: string) {
    this.message = message
  }

  // 一般方法
  greet (): string {
    return 'Hello ' + this.message
  }
}

// 創建類的實例
const greeter = new Greeter('world')
// 調用實例的方法
console.log(greeter.greet())

如果你使用過 C# 或 Java,你會對這種語法非常熟悉。 我們聲明一個 Greeter 類。這個類有 3 個成員:一個叫做 message 的屬性,一個構造函數和一個 greet 方法。

你會注意到,我們在引用任何一個類成員的時候都用了 this。 它表示我們訪問的是類的成員。

後面一行,我們使用 new 構造了 Greeter 類的一個實例。它會調用之前定義的構造函數,創建一個 Greeter 類型的新對象,並執行構造函數初始化它。

最後一行通過 greeter 對象調用其 greet 方法

繼承

在 TypeScript 裏,我們可以使用常用的面向對象模式。 基於類的程序設計中一種最基本的模式是允許使用繼承來擴展現有的類。

看下面的例子:

/* 
類的繼承
*/

class Animal {
  run (distance: number) {
    console.log(`Animal run ${distance}m`)
  }
}

class Dog extends Animal {
  cry () {
    console.log('wang! wang!')
  }
}

const dog = new Dog()
dog.cry() 
dog.run(100) // 可以調用從父中繼承得到的方法

這個例子展示了最基本的繼承:類從基類中繼承了屬性和方法。 這裏,Dog 是一個 派生類,它派生自 Animal 基類,通過 extends 關鍵字。 派生類通常被稱作子類,基類通常被稱作超類

因爲 Dog 繼承了 Animal 的功能,因此我們可以創建一個 Dog 的實例,它能夠 cry()run()

下面我們來看個更加複雜的例子。

class Animal {
  name: string
  
  constructor (name: string) {
    this.name = name
  }

  run (distance: number=0) {
    console.log(`${this.name} run ${distance}m`)
  }

}

class Snake extends Animal {
  constructor (name: string) {
    // 調用父類型構造方法
    super(name)
  }

  // 重寫父類型的方法
  run (distance: number=5) {
    console.log('sliding...')
    super.run(distance)
  }
}

class Horse extends Animal {
  constructor (name: string) {
    // 調用父類型構造方法
    super(name)
  }

  // 重寫父類型的方法
  run (distance: number=50) {
    console.log('dashing...')
    // 調用父類型的一般方法
    super.run(distance)
  }

  xxx () {
    console.log('xxx()')
  }
}

const snake = new Snake('sn')
snake.run()

const horse = new Horse('ho')
horse.run()

// 父類型引用指向子類型的實例 ==> 多態
const tom: Animal = new Horse('ho22')
tom.run()

/* 如果子類型沒有擴展的方法, 可以讓子類型引用指向父類型的實例 */
const tom3: Snake = new Animal('tom3')
tom3.run()
/* 如果子類型有擴展的方法, 不能讓子類型引用指向父類型的實例 */
// const tom2: Horse = new Animal('tom2')
// tom2.run()

這個例子展示了一些上面沒有提到的特性。 這一次,我們使用 extends 關鍵字創建了 Animal的兩個子類:HorseSnake

與前一個例子的不同點是,派生類包含了一個構造函數,它 必須調用 super(),它會執行基類的構造函數。 而且,在構造函數裏訪問 this 的屬性之前,我們 一定要調用 super()。 這個是 TypeScript 強制執行的一條重要規則。

這個例子演示瞭如何在子類裏可以重寫父類的方法。Snake類和 Horse 類都創建了 run 方法,它們重寫了從 Animal 繼承來的 run 方法,使得 run 方法根據不同的類而具有不同的功能。注意,即使 tom 被聲明爲 Animal 類型,但因爲它的值是 Horse,調用 tom.run(34) 時,它會調用 Horse 裏重寫的方法。

sliding...
sn run 5m
dashing...
ho run 50m

公共,私有與受保護的修飾符

默認爲 public

在上面的例子裏,我們可以自由的訪問程序裏定義的成員。 如果你對其它語言中的類比較瞭解,就會注意到我們在之前的代碼裏並沒有使用 public 來做修飾;例如,C# 要求必須明確地使用 public 指定成員是可見的。 在 TypeScript 裏,成員都默認爲 public

你也可以明確的將一個成員標記成 public。 我們可以用下面的方式來重寫上面的 Animal 類:

理解 private

當成員被標記成 private 時,它就不能在聲明它的類的外部訪問。

理解 protected

protected 修飾符與 private 修飾符的行爲很相似,但有一點不同,protected成員在派生類中仍然可以訪問。例如:

/* 
訪問修飾符: 用來描述類內部的屬性/方法的可訪問性
  public: 默認值, 公開的外部也可以訪問
  private: 只能類內部可以訪問
  protected: 類內部和子類可以訪問
*/

class Animal {
  public name: string

  public constructor (name: string) {
    this.name = name
  }

  public run (distance: number=0) {
    console.log(`${this.name} run ${distance}m`)
  }
}

class Person extends Animal {
  private age: number = 18
  protected sex: string = '男'

  run (distance: number=5) {
    console.log('Person jumping...')
    super.run(distance)
  }
}

class Student extends Person {
  run (distance: number=6) {
    console.log('Student jumping...')

    console.log(this.sex) // 子類能看到父類中受保護的成員
    // console.log(this.age) //  子類看不到父類中私有的成員

    super.run(distance)
  }
}

console.log(new Person('abc').name) // 公開的可見
// console.log(new Person('abc').sex) // 受保護的不可見
// console.log(new Person('abc').age) //  私有的不可見

readonly 修飾符

你可以使用 readonly 關鍵字將屬性設置爲只讀的。 只讀屬性必須在聲明時或構造函數裏被初始化。

class Person {
  readonly name: string = 'abc'
  constructor(name: string) {
    this.name = name
  }
}

let john = new Person('John')
// john.name = 'peter' // error

參數屬性

在上面的例子中,我們必須在 Person 類裏定義一個只讀成員 name 和一個參數爲 name 的構造函數,並且立刻將 name 的值賦給 this.name,這種情況經常會遇到。 參數屬性可以方便地讓我們在一個地方定義並初始化一個成員。 下面的例子是對之前 Person 類的修改版,使用了參數屬性:

class Person2 {
  constructor(readonly name: string) {
  }
}

const p = new Person2('jack')
console.log(p.name)

注意看我們是如何捨棄參數 name,僅在構造函數裏使用 readonly name: string 參數來創建和初始化 name 成員。 我們把聲明和賦值合併至一處。

參數屬性通過給構造函數參數前面添加一個訪問限定符來聲明。使用 private 限定一個參數屬性會聲明並初始化一個私有成員;對於 publicprotected 來說也是一樣。

存取器

TypeScript 支持通過 getters/setters 來截取對對象成員的訪問。 它能幫助你有效的控制對對象成員的訪問。

下面來看如何把一個簡單的類改寫成使用 getset。 首先,我們從一個沒有使用存取器的例子開始。

class Person {
  firstName: string = 'A'
  lastName: string = 'B'
  get fullName () {
    return this.firstName + '-' + this.lastName
  }
  set fullName (value) {
    const names = value.split('-')
    this.firstName = names[0]
    this.lastName = names[1]
  }
}

const p = new Person()
console.log(p.fullName)

p.firstName = 'C'
p.lastName =  'D'
console.log(p.fullName)

p.fullName = 'E-F'
console.log(p.firstName, p.lastName)

靜態屬性

到目前爲止,我們只討論了類的實例成員,那些僅當類被實例化的時候纔會被初始化的屬性。 我們也可以創建類的靜態成員,這些屬性存在於類本身上面而不是類的實例上。 在這個例子裏,我們使用 static 定義 origin,因爲它是所有網格都會用到的屬性。 每個實例想要訪問這個屬性的時候,都要在 origin 前面加上類名。 如同在實例屬性上使用 this.xxx 來訪問屬性一樣,這裏我們使用 Grid.xxx 來訪問靜態屬性。

/* 
靜態屬性, 是類對象的屬性
非靜態屬性, 是類的實例對象的屬性
*/

class Person {
  name1: string = 'A'
  static name2: string = 'B'
}

console.log(Person.name2)
console.log(new Person().name1)

抽象類

抽象類做爲其它派生類的基類使用。 它們不能被實例化。不同於接口,抽象類可以包含成員的實現細節。 abstract 關鍵字是用於定義抽象類和在抽象類內部定義抽象方法。

/* 
抽象類
  不能創建實例對象, 只有實現類才能創建實例
  可以包含未實現的抽象方法
*/

abstract class Animal {

  abstract cry ()

  run () {
    console.log('run()')
  }
}

class Dog extends Animal {
  cry () {
    console.log(' Dog cry()')
  }
}

const dog = new Dog()
dog.cry()
dog.run()

2.4 函數

函數是 JavaScript 應用程序的基礎,它幫助你實現抽象層,模擬類,信息隱藏和模塊。在 TypeScript 裏,雖然已經支持類,命名空間和模塊,但函數仍然是主要的定義行爲的地方。TypeScript 爲 JavaScript 函數添加了額外的功能,讓我們可以更容易地使用。

基本示例

和 JavaScript 一樣,TypeScript 函數可以創建有名字的函數和匿名函數。你可以隨意選擇適合應用程序的方式,不論是定義一系列 API 函數還是隻使用一次的函數。

通過下面的例子可以迅速回想起這兩種 JavaScript 中的函數:

// 命名函數
function add(x, y) {
  return x + y
}

// 匿名函數
let myAdd = function(x, y) { 
  return x + y;
}

函數類型

爲函數定義類型

讓我們爲上面那個函數添加類型:

function add(x: number, y: number): number {
  return x + y
}

let myAdd = function(x: number, y: number): number { 
  return x + y
}

我們可以給每個參數添加類型之後再爲函數本身添加返回值類型。TypeScript 能夠根據返回語句自動推斷出返回值類型。

書寫完整函數類型

現在我們已經爲函數指定了類型,下面讓我們寫出函數的完整類型。

let myAdd2: (x: number, y: number) => number = 
function(x: number, y: number): number {
  return x + y
}

可選參數和默認參數

TypeScript 裏的每個函數參數都是必須的。 這不是指不能傳遞 nullundefined 作爲參數,而是說編譯器檢查用戶是否爲每個參數都傳入了值。編譯器還會假設只有這些參數會被傳遞進函數。 簡短地說,傳遞給一個函數的參數個數必須與函數期望的參數個數一致。

JavaScript 裏,每個參數都是可選的,可傳可不傳。 沒傳參的時候,它的值就是 undefined。 在TypeScript 裏我們可以在參數名旁使用 ? 實現可選參數的功能。 比如,我們想讓 lastName 是可選的:

在 TypeScript 裏,我們也可以爲參數提供一個默認值當用戶沒有傳遞這個參數或傳遞的值是 undefined 時。 它們叫做有默認初始化值的參數。 讓我們修改上例,把firstName 的默認值設置爲 "A"

function buildName(firstName: string='A', lastName?: string): string {
  if (lastName) {
    return firstName + '-' + lastName
  } else {
    return firstName
  }
}

console.log(buildName('C', 'D'))
console.log(buildName('C'))
console.log(buildName())

剩餘參數

必要參數,默認參數和可選參數有個共同點:它們表示某一個參數。 有時,你想同時操作多個參數,或者你並不知道會有多少參數傳遞進來。 在 JavaScript 裏,你可以使用 arguments 來訪問所有傳入的參數。

在 TypeScript 裏,你可以把所有參數收集到一個變量裏:
剩餘參數會被當做個數不限的可選參數。 可以一個都沒有,同樣也可以有任意個。 編譯器創建參數數組,名字是你在省略號( ...)後面給定的名字,你可以在函數體內使用這個數組。

function info(x: string, ...args: string[]) {
  console.log(x, args)
}
info('abc', 'c', 'b', 'a')

函數重載

函數重載: 函數名相同, 而形參不同的多個函數
在JS中, 由於弱類型的特點和形參與實參可以不匹配, 是沒有函數重載這一說的
但在TS中, 與其它面向對象的語言(如Java)就存在此語法

/* 
函數重載: 函數名相同, 而形參不同的多個函數
需求: 我們有一個add函數,它可以接收2個string類型的參數進行拼接,也可以接收2個number類型的參數進行相加 
*/

// 重載函數聲明
function add (x: string, y: string): string
function add (x: number, y: number): number

// 定義函數實現
function add(x: string | number, y: string | number): string | number {
  // 在實現上我們要注意嚴格判斷兩個參數的類型是否相等,而不能簡單的寫一個 x + y
  if (typeof x === 'string' && typeof y === 'string') {
    return x + y
  } else if (typeof x === 'number' && typeof y === 'number') {
    return x + y
  }
}

console.log(add(1, 2))
console.log(add('a', 'b'))
// console.log(add(1, 'a')) // error

2.5 泛型

指在定義函數、接口或類的時候,不預先指定具體的類型,而在使用的時候再指定具體類型的一種特性。

引入

下面創建一個函數, 實現功能: 根據指定的數量 count 和數據 value , 創建一個包含 countvalue 的數組
不用泛型的話,這個函數可能是下面這樣:

function createArray(value: any, count: number): any[] {
  const arr: any[] = []
  for (let index = 0; index < count; index++) {
    arr.push(value)
  }
  return arr
}

const arr1 = createArray(11, 3)
const arr2 = createArray('aa', 3)
console.log(arr1[0].toFixed(), arr2[0].split(''))

使用函數泛型

function createArray2 <T> (value: T, count: number) {
  const arr: Array<T> = []
  for (let index = 0; index < count; index++) {
    arr.push(value)
  }
  return arr
}
const arr3 = createArray2<number>(11, 3)
console.log(arr3[0].toFixed())
// console.log(arr3[0].split('')) // error
const arr4 = createArray2<string>('aa', 3)
console.log(arr4[0].split(''))
// console.log(arr4[0].toFixed()) // error

多個泛型參數的函數

一個函數可以定義多個泛型參數

function swap <K, V> (a: K, b: V): [K, V] {
  return [a, b]
}
const result = swap<string, number>('abc', 123)
console.log(result[0].length, result[1].toFixed())

泛型接口

在定義接口時, 爲接口中的屬性或方法定義泛型類型
在使用接口時, 再指定具體的泛型類型

interface IbaseCRUD <T> {
  data: T[]
  add: (t: T) => void
  getById: (id: number) => T
}

class User {
  id?: number; //id主鍵自增
  name: string; //姓名
  age: number; //年齡

  constructor (name, age) {
    this.name = name
    this.age = age
  }
}

class UserCRUD implements IbaseCRUD <User> {
  data: User[] = []
  
  add(user: User): void {
    user = {...user, id: Date.now()}
    this.data.push(user)
    console.log('保存user', user.id)
  }

  getById(id: number): User {
    return this.data.find(item => item.id===id)
  }
}


const userCRUD = new UserCRUD()
userCRUD.add(new User('tom', 12))
userCRUD.add(new User('tom2', 13))
console.log(userCRUD.data)

泛型類

在定義類時, 爲類中的屬性或方法定義泛型類型
在創建類的實例時, 再指定特定的泛型類型

class GenericNumber<T> {
  zeroValue: T
  add: (x: T, y: T) => T
}

let myGenericNumber = new GenericNumber<number>()
myGenericNumber.zeroValue = 0
myGenericNumber.add = function(x, y) {
  return x + y 
}

let myGenericString = new GenericNumber<string>()
myGenericString.zeroValue = 'abc'
myGenericString.add = function(x, y) { 
  return x + y
}

console.log(myGenericString.add(myGenericString.zeroValue, 'test'))
console.log(myGenericNumber.add(myGenericNumber.zeroValue, 12))

泛型約束

如果我們直接對一個泛型參數取 length 屬性, 會報錯, 因爲這個泛型根本就不知道它有這個屬性

// 沒有泛型約束
function fn <T>(x: T): void {
  // console.log(x.length)  // error
}

我們可以使用泛型約束來實現

interface Lengthwise {
  length: number;
}

// 指定泛型約束
function fn2 <T extends Lengthwise>(x: T): void {
  console.log(x.length)
}

我們需要傳入符合約束類型的值,必須包含必須 length 屬性:

fn2('abc')
// fn2(123) // error  number沒有length屬性

2.6 其它

聲明文件

當使用第三方庫時,我們需要引用它的聲明文件,才能獲得對應的代碼補全、接口提示等功能

什麼是聲明語句

假如我們想使用第三方庫 jQuery,一種常見的方式是在 html 中通過 <script> 標籤引入 jQuery,然後就可以使用全局變量 $jQuery 了。

但是在 ts 中,編譯器並不知道 $ 或 jQuery 是什麼東西

/* 
當使用第三方庫時,我們需要引用它的聲明文件,才能獲得對應的代碼補全、接口提示等功能。
聲明語句: 如果需要ts對新的語法進行檢查, 需要要加載了對應的類型說明代碼
  declare var jQuery: (selector: string) => any;
聲明文件: 把聲明語句放到一個單獨的文件(jQuery.d.ts)中, ts會自動解析到項目中所有聲明文件
下載聲明文件: npm install @types/jquery --save-dev
*/

jQuery('#foo');
// ERROR: Cannot find name 'jQuery'.

這時,我們需要使用 declare var 來定義它的類型

declare var jQuery: (selector: string) => any;

jQuery('#foo');

declare var 並沒有真的定義一個變量,只是定義了全局變量 jQuery 的類型,僅僅會用於編譯時的檢查,在編譯結果中會被刪除。它編譯結果是:

jQuery('#foo');

一般聲明文件都會單獨寫成一個 xxx.d.ts 文件

創建 01_jQuery.d.ts, 將聲明語句定義其中, TS編譯器會掃描並加載項目中所有的TS聲明文件

declare var jQuery: (selector: string) => any;

很多的第三方庫都定義了對應的聲明文件庫, 庫文件名一般爲 @types/xxx, 可以在 https://www.npmjs.com/package/package 進行搜索

有的第三庫在下載時就會自動下載對應的聲明文件庫(比如: webpack),有的可能需要單獨下載(比如jQuery/react)

內置對象

JavaScript 中有很多內置對象,它們可以直接在 TypeScript 中當做定義好了的類型。

內置對象是指根據標準在全局作用域(Global)上存在的對象。這裏的標準是指 ECMAScript 和其他環境(比如 DOM)的標準。

  1. ECMAScript 的內置對象

Boolean
Number
String
Date
RegExp
Error

/* 1. ECMAScript 的內置對象 */
let b: Boolean = new Boolean(1)
let n: Number = new Number(true)
let s: String = new String('abc')
let d: Date = new Date()
let r: RegExp = /^1/
let e: Error = new Error('error message')
b = true
// let bb: boolean = new Boolean(2)  // error
  1. BOM 和 DOM 的內置對象

Window
Document
HTMLElement
DocumentFragment
Event
NodeList

const div: HTMLElement = document.getElementById('test')
const divs: NodeList = document.querySelectorAll('div')
document.addEventListener('click', (event: MouseEvent) => {
  console.dir(event.target)
})
const fragment: DocumentFragment = document.createDocumentFragment()

3、認識Vue3

3.1 瞭解相關信息

  • Vue.js 3.0 "One Piece" 正式版在今年9月份發佈
  • 2年多開發, 100+位貢獻者, 2600+次提交, 600+次PR
  • Vue3支持vue2的大多數特性
  • 更好的支持Typescript

3.2 性能提升:

  • 打包大小減少41%
  • 初次渲染快55%, 更新渲染快133%
  • 內存減少54%
  • 使用Proxy代替defineProperty實現數據響應式
  • 重寫虛擬DOM的實現和Tree-Shaking

3.3 新增特性

  • Composition (組合) API

  • setup

    • ref 和 reactive
    • computed 和 watch
    • 新的生命週期函數
    • provide與inject
    • ...
  • 新組件

    • Fragment - 文檔碎片
    • Teleport - 瞬移組件的位置
    • Suspense - 異步加載組件的loading界面
  • 其它API更新

    • 全局API的修改
    • 將原來的全局API轉移到應用對象
    • 模板語法變化

4、創建vue3項目

4.1 使用 vue-cli 創建

文檔: https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-create

## 安裝或者升級
npm install -g @vue/cli
## 保證 vue cli 版本在 4.5.0 以上
vue --version
## 創建項目
vue create my-project

然後的步驟

  • Please pick a preset - 選擇 Manually select features
  • Check the features needed for your project - 選擇上 TypeScript ,特別注意點空格是選擇,點回車是下一步
  • Choose a version of Vue.js that you want to start the project with - 選擇 3.x (Preview)
  • Use class-style component syntax - 直接回車
  • Use Babel alongside TypeScript - 直接回車
  • Pick a linter / formatter config - 直接回車
  • Use history mode for router? - 直接回車
  • Pick a linter / formatter config - 直接回車
  • Pick additional lint features - 直接回車
  • Where do you prefer placing config for Babel, ESLint, etc.? - 直接回車
  • Save this as a preset for future projects? - 直接回車

4.2 使用 vite 創建

  • 文檔: https://v3.cn.vuejs.org/guide/installation.html

  • vite 是一個由原生 ESM 驅動的 Web 開發構建工具。在開發環境下基於瀏覽器原生 ES imports 開發,

  • 它做到了本地快速開發啓動, 在生產環境下基於 Rollup 打包。

    • 快速的冷啓動,不需要等待打包操作;
    • 即時的熱模塊更新,替換性能和模塊數量的解耦讓更新飛起;
    • 真正的按需編譯,不再等待整個應用編譯完成,這是一個巨大的改變。
npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev

5、Composition API(常用部分)

文檔:

https://composition-api.vuejs.org/zh/api.html

5.1 setup

  • 新的option, 所有的組合API函數都在此使用, 只在初始化時執行一次
  • 函數如果返回對象, 對象中的屬性或方法, 模板中可以直接使用

5.2 ref

  • 作用: 定義一個數據的響應式
  • 語法: const xxx = ref(initValue):
    • 創建一個包含響應式數據的引用(reference)對象
    • js中操作數據: xxx.value
    • 模板中操作數據: 不需要.value
  • 一般用來定義一個基本類型的響應式數據
<template>
  <h2>{{count}}</h2>
  <hr>
  <button @click="update">更新</button>
</template>

<script>
import {
  ref
} from 'vue'
export default {

  /* 在Vue3中依然可以使用data和methods配置, 但建議使用其新語法實現 */
  // data () {
  //   return {
  //     count: 0
  //   }
  // },
  // methods: {
  //   update () {
  //     this.count++
  //   }
  // }

  /* 使用vue3的composition API */
  setup () {

    // 定義響應式數據 ref對象
    const count = ref(1)
    console.log(count)

    // 更新響應式數據的函數
    function update () {
      // alert('update')
      count.value = count.value + 1
    }

    return {
      count,
      update
    }
  }
}
</script>

5.3 reactive

  • 作用: 定義多個數據的響應式
  • const proxy = reactive(obj): 接收一個普通對象然後返回該普通對象的響應式代理器對象
  • 響應式轉換是“深層的”:會影響對象內部所有嵌套的屬性
  • 內部基於 ES6 的 Proxy 實現,通過代理對象操作源對象內部數據都是響應式的
<template>
  <h2>name: {{state.name}}</h2>
  <h2>age: {{state.age}}</h2>
  <h2>wife: {{state.wife}}</h2>
  <hr>
  <button @click="update">更新</button>
</template>

<script>
/* 
reactive: 
    作用: 定義多個數據的響應式
    const proxy = reactive(obj): 接收一個普通對象然後返回該普通對象的響應式代理器對象
    響應式轉換是“深層的”:會影響對象內部所有嵌套的屬性
    內部基於 ES6 的 Proxy 實現,通過代理對象操作源對象內部數據都是響應式的
*/
import {
  reactive,
} from 'vue'
export default {
  setup () {
    /* 
    定義響應式數據對象
    */
    const state = reactive({
      name: 'tom',
      age: 25,
      wife: {
        name: 'marry',
        age: 22
      },
    })
    console.log(state, state.wife)

    const update = () => {
      state.name += '--'
      state.age += 1
      state.wife.name += '++'
      state.wife.age += 2
    }

    return {
      state,
      update,
    }
  }
}
</script>

5.4 比較Vue2與Vue3的響應式(重要)

5.4.1 vue2的響應式

  • 核心:
    • 對象: 通過defineProperty對對象的已有屬性值的讀取和修改進行劫持(監視/攔截)
    • 數組: 通過重寫數組更新數組一系列更新元素的方法來實現元素修改的劫持
Object.defineProperty(data, 'count', {
    get () {}, 
    set () {}
})
  • 問題
    • 對象直接新添加的屬性或刪除已有屬性, 界面不會自動更新
    • 直接通過下標替換元素或更新length, 界面不會自動更新 arr[1] = {}

5.4.2 Vue3的響應式

new Proxy(data, {
	// 攔截讀取屬性值
    get (target, prop) {
    	return Reflect.get(target, prop)
    },
    // 攔截設置屬性值或添加新屬性
    set (target, prop, value) {
    	return Reflect.set(target, prop, value)
    },
    // 攔截刪除屬性
    deleteProperty (target, prop) {
    	return Reflect.deleteProperty(target, prop)
    }
})

proxy.name = 'tom'   
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Proxy 與 Reflect</title>
</head>
<body>
  <script>
    
    const user = {
      name: "John",
      age: 12
    };

    /* 
    proxyUser是代理對象, user是被代理對象
    後面所有的操作都是通過代理對象來操作被代理對象內部屬性
    */
    const proxyUser = new Proxy(user, {

      get(target, prop) {
        console.log('劫持get()', prop)
        return Reflect.get(target, prop)
      },

      set(target, prop, val) {
        console.log('劫持set()', prop, val)
        return Reflect.set(target, prop, val); // (2)
      },

      deleteProperty (target, prop) {
        console.log('劫持delete屬性', prop)
        return Reflect.deleteProperty(target, prop)
      }
    });
    // 讀取屬性值
    console.log(proxyUser===user)
    console.log(proxyUser.name, proxyUser.age)
    // 設置屬性值
    proxyUser.name = 'bob'
    proxyUser.age = 13
    console.log(user)
    // 添加屬性
    proxyUser.sex = '男'
    console.log(user)
    // 刪除屬性
    delete proxyUser.sex
    console.log(user)
  </script>
</body>
</html>

5.5 setup細節

  • setup執行的時機

    • 在beforeCreate之前執行(一次), 此時組件對象還沒有創建
    • this是undefined, 不能通過this來訪問data/computed/methods / props
    • 其實所有的composition API相關回調函數中也都不可以
  • setup的返回值

    • 一般都返回一個對象: 爲模板提供數據, 也就是模板中可以直接使用此對象中的所有屬性/方法
    • 返回對象中的屬性會與data函數返回對象的屬性合併成爲組件對象的屬性
    • 返回對象中的方法會與methods中的方法合併成功組件對象的方法
    • 如果有重名, setup優先
    • 注意:
    • 一般不要混合使用: methods中可以訪問setup提供的屬性和方法, 但在setup方法中不能訪問data和methods
    • setup不能是一個async函數: 因爲返回值不再是return的對象, 而是promise, 模板看不到return對象中的屬性數據
  • setup的參數

    • setup(props, context) / setup(props, {attrs, slots, emit})
    • props: 包含props配置聲明且傳入了的所有屬性的對象
    • attrs: 包含沒有在props配置中聲明的屬性的對象, 相當於 this.$attrs
    • slots: 包含所有傳入的插槽內容的對象, 相當於 this.$slots
    • emit: 用來分發自定義事件的函數, 相當於 this.$emit
<template>
  <h2>App</h2>
  <p>msg: {{msg}}</p>
  <button @click="fn('--')">更新</button>

  <child :msg="msg" msg2="cba" @fn="fn"/>
</template>

<script lang="ts">
import {
  reactive,
  ref,
} from 'vue'
import child from './child.vue'

export default {

  components: {
    child
  },

  setup () {
    const msg = ref('abc')

    function fn (content: string) {
      msg.value += content
    }
    return {
      msg,
      fn
    }
  }
}
</script>
<template>
  <div>
    <h3>{{n}}</h3>
    <h3>{{m}}</h3>

    <h3>msg: {{msg}}</h3>
    <h3>msg2: {{$attrs.msg2}}</h3>

    <slot name="xxx"></slot>

    <button @click="update">更新</button>
  </div>
</template>

<script lang="ts">

import {
  ref,
  defineComponent
} from 'vue'

export default defineComponent({
  name: 'child',

  props: ['msg'],

  emits: ['fn'], // 可選的, 聲明瞭更利於程序員閱讀, 且可以對分發的事件數據進行校驗

  data () {
    console.log('data', this)
    return {
      // n: 1
    }
  },

  beforeCreate () {
    console.log('beforeCreate', this)
  },

  methods: {
    // update () {
    //   this.n++
    //   this.m++
    // }
  },

  // setup (props, context) {
  setup (props, {attrs, emit, slots}) {

    console.log('setup', this)
    console.log(props.msg, attrs.msg2, slots, emit)

    const m = ref(2)
    const n = ref(3)

    function update () {
      // console.log('--', this)
      // this.n += 2 
      // this.m += 2

      m.value += 2
      n.value += 2

      // 分發自定義事件
      emit('fn', '++')
    }

    return {
      m,
      n,
      update,
    }
  },
})
</script>

5.6 reactive與ref-細節

  • 是Vue3的 composition API中2個最重要的響應式API
  • ref用來處理基本類型數據, reactive用來處理對象(遞歸深度響應式)
  • 如果用ref對象/數組, 內部會自動將對象/數組轉換爲reactive的代理對象
  • ref內部: 通過給value屬性添加getter/setter來實現對數據的劫持
  • reactive內部: 通過使用Proxy來實現對對象內部所有數據的劫持, 並通過Reflect操作對象內部數據
  • ref的數據操作: 在js中要.value, 在模板中不需要(內部解析模板時會自動添加.value)
<template>
  <h2>App</h2>
  <p>m1: {{m1}}</p>
  <p>m2: {{m2}}</p>
  <p>m3: {{m3}}</p>
  <button @click="update">更新</button>
</template>

<script lang="ts">
import {
  reactive,
  ref
} from 'vue'

export default {

  setup () {
    const m1 = ref('abc')
    const m2 = reactive({x: 1, y: {z: 'abc'}})

    // 使用ref處理對象  ==> 對象會被自動reactive爲proxy對象
    const m3 = ref({a1: 2, a2: {a3: 'abc'}})
    console.log(m1, m2, m3)
    console.log(m3.value.a2) // 也是一個proxy對象

    function update() {
      m1.value += '--'
      m2.x += 1
      m2.y.z += '++'

      m3.value = {a1: 3, a2: {a3: 'abc---'}}
      m3.value.a2.a3 += '==' // reactive對對象進行了深度數據劫持
      console.log(m3.value.a2)
    }

    return {
      m1,
      m2,
      m3,
      update
    }
  }
}
</script>

5.7 計算屬性與監視

  • computed函數:

    • 與computed配置功能一致
    • 只有getter
    • 有getter和setter
  • watch函數

    • 與watch配置功能一致
    • 監視指定的一個或多個響應式數據, 一旦數據變化, 就自動執行監視回調
    • 默認初始時不執行回調, 但可以通過配置immediate爲true, 來指定初始時立即執行第一次
    • 通過配置deep爲true, 來指定深度監視
  • watchEffect函數

    • 不用直接指定要監視的數據, 回調函數中使用的哪些響應式數據就監視哪些響應式數據
    • 默認初始時就會執行第一次, 從而可以收集需要監視的數據
    • 監視數據發生變化時回調
<template>
  <h2>App</h2>
  fistName: <input v-model="user.firstName"/><br>
  lastName: <input v-model="user.lastName"/><br>
  fullName1: <input v-model="fullName1"/><br>
  fullName2: <input v-model="fullName2"><br>
  fullName3: <input v-model="fullName3"><br>

</template>

<script lang="ts">
/*
計算屬性與監視
1. computed函數: 
  與computed配置功能一致
  只有getter
  有getter和setter
2. watch函數
  與watch配置功能一致
  監視指定的一個或多個響應式數據, 一旦數據變化, 就自動執行監視回調
  默認初始時不執行回調, 但可以通過配置immediate爲true, 來指定初始時立即執行第一次
  通過配置deep爲true, 來指定深度監視
3. watchEffect函數
  不用直接指定要監視的數據, 回調函數中使用的哪些響應式數據就監視哪些響應式數據
  默認初始時就會執行第一次, 從而可以收集需要監視的數據
  監視數據發生變化時回調
*/

import {
  reactive,
  ref,
  computed,
  watch,
  watchEffect
} from 'vue'

export default {

  setup () {
    const user = reactive({
      firstName: 'A',
      lastName: 'B'
    })

    // 只有getter的計算屬性
    const fullName1 = computed(() => {
      console.log('fullName1')
      return user.firstName + '-' + user.lastName
    })

    // 有getter與setter的計算屬性
    const fullName2 = computed({
      get () {
        console.log('fullName2 get')
        return user.firstName + '-' + user.lastName
      },

      set (value: string) {
        console.log('fullName2 set')
        const names = value.split('-')
        user.firstName = names[0]
        user.lastName = names[1]
      }
    })

    const fullName3 = ref('')

    /* 
    watchEffect: 監視所有回調中使用的數據
    */
    /* 
    watchEffect(() => {
      console.log('watchEffect')
      fullName3.value = user.firstName + '-' + user.lastName
    }) 
    */

    /* 
    使用watch的2個特性:
      深度監視
      初始化立即執行
    */
    watch(user, () => {
      fullName3.value = user.firstName + '-' + user.lastName
    }, {
      immediate: true,  // 是否初始化立即執行一次, 默認是false
      deep: true, // 是否是深度監視, 默認是false
    })

    /* 
    watch一個數據
      默認在數據發生改變時執行回調
    */
    watch(fullName3, (value) => {
      console.log('watch')
      const names = value.split('-')
      user.firstName = names[0]
      user.lastName = names[1]
    })

    /* 
    watch多個數據: 
      使用數組來指定
      如果是ref對象, 直接指定
      如果是reactive對象中的屬性,  必須通過函數來指定
    */
    watch([() => user.firstName, () => user.lastName, fullName3], (values) => {
      console.log('監視多個數據', values)
    })

    return {
      user,
      fullName1,
      fullName2,
      fullName3
    }
  }
}
</script>

5.8 生命週期

vue2.x的生命週期

lifecycle_2

vue3的生命週期

lifecycle_3

與 2.x 版本生命週期相對應的組合式 API

  • beforeCreate -> 使用 setup()
  • created -> 使用 setup()
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeDestroy -> onBeforeUnmount
  • destroyed -> onUnmounted
  • errorCaptured -> onErrorCaptured

新增的鉤子函數

組合式 API 還提供了以下調試鉤子函數:

  • onRenderTracked
  • onRenderTriggered
<template>
<div class="about">
  <h2>msg: {{msg}}</h2>
  <hr>
  <button @click="update">更新</button>
</div>
</template>

<script lang="ts">
import {
  ref,
  onMounted,
  onUpdated,
  onUnmounted, 
  onBeforeMount, 
  onBeforeUpdate,
  onBeforeUnmount
} from "vue"

export default {
  beforeCreate () {
    console.log('beforeCreate()')
  },

  created () {
    console.log('created')
  },

  beforeMount () {
    console.log('beforeMount')
  },

  mounted () {
    console.log('mounted')
  },

  beforeUpdate () {
    console.log('beforeUpdate')
  },

  updated () {
    console.log('updated')
  },

  beforeUnmount () {
    console.log('beforeUnmount')
  },

  unmounted () {
     console.log('unmounted')
  },
  

  setup() {
    
    const msg = ref('abc')

    const update = () => {
      msg.value += '--'
    }

    onBeforeMount(() => {
      console.log('--onBeforeMount')
    })

    onMounted(() => {
      console.log('--onMounted')
    })

    onBeforeUpdate(() => {
      console.log('--onBeforeUpdate')
    })

    onUpdated(() => {
      console.log('--onUpdated')
    })

    onBeforeUnmount(() => {
      console.log('--onBeforeUnmount')
    })

    onUnmounted(() => {
      console.log('--onUnmounted')
    })
    
    return {
      msg,
      update
    }
  }
}
</script>

<template>
  <h2>App</h2>
  <button @click="isShow=!isShow">切換</button>
  <hr>
  <Child v-if="isShow"/>
</template>

<script lang="ts">
import Child from './Child.vue'
export default {

  data () {
    return {
      isShow: true
    }
  },

  components: {
    Child
  }
}
</script>

5.9 自定義hook函數

  • 使用Vue3的組合API封裝的可複用的功能函數

  • 自定義hook的作用類似於vue2中的mixin技術

  • 自定義Hook的優勢: 很清楚複用功能代碼的來源, 更清楚易懂

  • 需求1: 收集用戶鼠標點擊的頁面座標

    hooks/useMousePosition.ts

import { ref, onMounted, onUnmounted } from 'vue'
/* 
收集用戶鼠標點擊的頁面座標
*/
export default function useMousePosition () {
  // 初始化座標數據
  const x = ref(-1)
  const y = ref(-1)

  // 用於收集點擊事件座標的函數
  const updatePosition = (e: MouseEvent) => {
    x.value = e.pageX
    y.value = e.pageY
  }

  // 掛載後綁定點擊監聽
  onMounted(() => {
    document.addEventListener('click', updatePosition)
  })

  // 卸載前解綁點擊監聽
  onUnmounted(() => {
    document.removeEventListener('click', updatePosition)
  })

  return {x, y}
}
<template>
<div>
  <h2>x: {{x}}, y: {{y}}</h2>
</div>
</template>

<script>

import {
  ref
} from "vue"
/* 
在組件中引入並使用自定義hook
自定義hook的作用類似於vue2中的mixin技術
自定義Hook的優勢: 很清楚複用功能代碼的來源, 更清楚易懂
*/
import useMousePosition from './hooks/useMousePosition'

export default {
  setup() {

    const {x, y} = useMousePosition()

    return {
      x,
      y,
    }
  }
}
</script>
  • 利用TS泛型強化類型檢查

  • 需求2: 封裝發ajax請求的hook函數

    hooks/useRequest.ts

import { ref } from 'vue'
import axios from 'axios'

/* 
使用axios發送異步ajax請求
*/
export default function useUrlLoader<T>(url: string) {

  const result = ref<T | null>(null)
  const loading = ref(true)
  const errorMsg = ref(null)

  axios.get(url)
    .then(response => {
      loading.value = false
      result.value = response.data
    })
    .catch(e => {
      loading.value = false
      errorMsg.value = e.message || '未知錯誤'
    })

  return {
    loading,
    result,
    errorMsg,
  }
}
<template>
<div class="about">
  <h2 v-if="loading">LOADING...</h2>
  <h2 v-else-if="errorMsg">{{errorMsg}}</h2>
  <!-- <ul v-else>
    <li>id: {{result.id}}</li>
    <li>name: {{result.name}}</li>
    <li>distance: {{result.distance}}</li>
  </ul> -->

  <ul v-for="p in result" :key="p.id">
    <li>id: {{p.id}}</li>
    <li>title: {{p.title}}</li>
    <li>price: {{p.price}}</li>
  </ul>
  <!-- <img v-if="result" :src="result[0].url" alt=""> -->
</div>
</template>

<script lang="ts">
import {
  watch
} from "vue"
import useRequest from './hooks/useRequest'

// 地址數據接口
interface AddressResult {
  id: number;
  name: string;
  distance: string;
}

// 產品數據接口
interface ProductResult {
  id: string;
  title: string;
  price: number;
}

export default {
  setup() {

    // const {loading, result, errorMsg} = useRequest<AddressResult>('/data/address.json')
    const {loading, result, errorMsg} = useRequest<ProductResult[]>('/data/products.json')

    watch(result, () => {
      if (result.value) {
        console.log(result.value.length) // 有提示
      }
    })

    return {
      loading,
      result, 
      errorMsg
    }
  }
}
</script>

5.10 toRefs

把一個響應式對象轉換成普通對象,該普通對象的每個 property 都是一個 ref

應用: 當從合成函數返回響應式對象時,toRefs 非常有用,這樣消費組件就可以在不丟失響應式的情況下對返回的對象進行分解使用

問題: reactive 對象取出的所有屬性值都是非響應式的

解決: 利用 toRefs 可以將一個響應式 reactive 對象的所有原始屬性轉換爲響應式的 ref 屬性

<template>
  <h2>App</h2>
  <h3>foo: {{foo}}</h3>
  <h3>bar: {{bar}}</h3>
  <h3>foo2: {{foo2}}</h3>
  <h3>bar2: {{bar2}}</h3>


</template>

<script lang="ts">
import { reactive, toRefs } from 'vue'
/*
toRefs:
  將響應式對象中所有屬性包裝爲ref對象, 並返回包含這些ref對象的普通對象
  應用: 當從合成函數返回響應式對象時,toRefs 非常有用,
        這樣消費組件就可以在不丟失響應式的情況下對返回的對象進行分解使用
*/
export default {

  setup () {

    const state = reactive({
      foo: 'a',
      bar: 'b',
    })

    const stateAsRefs = toRefs(state)

    setTimeout(() => {
      state.foo += '++'
      state.bar += '++'
    }, 2000);

    const {foo2, bar2} = useReatureX()

    return {
      // ...state,
      ...stateAsRefs,
      foo2, 
      bar2
    }
  },
}

function useReatureX() {
  const state = reactive({
    foo2: 'a',
    bar2: 'b',
  })

  setTimeout(() => {
    state.foo2 += '++'
    state.bar2 += '++'
  }, 2000);

  return toRefs(state)
}

</script>

5.11 ref獲取元素

利用ref函數獲取組件中的標籤元素

功能需求: 讓輸入框自動獲取焦點

<template>
  <h2>App</h2>
  <input type="text">---
  <input type="text" ref="inputRef">
</template>

<script lang="ts">
import { onMounted, ref } from 'vue'
/* 
ref獲取元素: 利用ref函數獲取組件中的標籤元素
功能需求: 讓輸入框自動獲取焦點
*/
export default {
  setup() {
    const inputRef = ref<HTMLElement|null>(null)

    onMounted(() => {
      inputRef.value && inputRef.value.focus()
    })

    return {
      inputRef
    }
  },
}
</script>

6、Composition API(其它部分)

6.1 shallowReactive 與 shallowRef

  • shallowReactive : 只處理了對象內最外層屬性的響應式(也就是淺響應式)

  • shallowRef: 只處理了value的響應式, 不進行對象的reactive處理

  • 什麼時候用淺響應式呢?

    • 一般情況下使用ref和reactive即可
    • 如果有一個對象數據, 結構比較深, 但變化時只是外層屬性變化 ===> shallowReactive
    • 如果有一個對象數據, 後面會產生新的對象來替換 ===> shallowRef
<template>
  <h2>App</h2>

  <h3>m1: {{m1}}</h3>
  <h3>m2: {{m2}}</h3>
  <h3>m3: {{m3}}</h3>
  <h3>m4: {{m4}}</h3>

  <button @click="update">更新</button>
</template>

<script lang="ts">
import { reactive, ref, shallowReactive, shallowRef } from 'vue'
/* 
shallowReactive與shallowRef
  shallowReactive: 只處理了對象內最外層屬性的響應式(也就是淺響應式)
  shallowRef: 只處理了value的響應式, 不進行對象的reactive處理
總結:
  reactive與ref實現的是深度響應式, 而shallowReactive與shallowRef是淺響應式
  什麼時候用淺響應式呢?
    一般情況下使用ref和reactive即可,
    如果有一個對象數據, 結構比較深, 但變化時只是外層屬性變化 ===> shallowReactive
    如果有一個對象數據, 後面會產生新的對象來替換 ===> shallowRef
*/

export default {

  setup () {

    const m1 = reactive({a: 1, b: {c: 2}})
    const m2 = shallowReactive({a: 1, b: {c: 2}})

    const m3 = ref({a: 1, b: {c: 2}})
    const m4 = shallowRef({a: 1, b: {c: 2}})

    const update = () => {
      // m1.b.c += 1
      // m2.b.c += 1

      // m3.value.a += 1
      m4.value.a += 1
    }

    return {
      m1,
      m2,
      m3,
      m4,
      update,
    }
  }
}
</script>

6.2 readonly 與 shallowReadonly

  • readonly:
    • 深度只讀數據
    • 獲取一個對象 (響應式或純對象) 或 ref 並返回原始代理的只讀代理。
    • 只讀代理是深層的:訪問的任何嵌套 property 也是隻讀的。
  • shallowReadonly
    • 淺只讀數據
    • 創建一個代理,使其自身的 property 爲只讀,但不執行嵌套對象的深度只讀轉換
  • 應用場景:
    • 在某些特定情況下, 我們可能不希望對數據進行更新的操作, 那就可以包裝生成一個只讀代理對象來讀取數據, 而不能修改或刪除
<template>
  <h2>App</h2>
  <h3>{{state}}</h3>
  <button @click="update">更新</button>
</template>

<script lang="ts">
import { reactive, readonly, shallowReadonly } from 'vue'
/*
readonly: 深度只讀數據
  獲取一個對象 (響應式或純對象) 或 ref 並返回原始代理的只讀代理。
  只讀代理是深層的:訪問的任何嵌套 property 也是隻讀的。
shallowReadonly: 淺只讀數據
  創建一個代理,使其自身的 property 爲只讀,但不執行嵌套對象的深度只讀轉換 
應用場景: 
  在某些特定情況下, 我們可能不希望對數據進行更新的操作, 那就可以包裝生成一個只讀代理對象來讀取數據, 而不能修改或刪除
*/

export default {

  setup () {

    const state = reactive({
      a: 1,
      b: {
        c: 2
      }
    })

    // const rState1 = readonly(state)
    const rState2 = shallowReadonly(state)

    const update = () => {
      // rState1.a++ // error
      // rState1.b.c++ // error

      // rState2.a++ // error
      rState2.b.c++
    }
    
    return {
      state,
      update
    }
  }
}
</script>

6.3 toRaw 與 markRaw

  • toRaw
    • 返回由 reactivereadonly 方法轉換成響應式代理的普通對象。
    • 這是一個還原方法,可用於臨時讀取,訪問不會被代理/跟蹤,寫入時也不會觸發界面更新。
  • markRaw
    • 標記一個對象,使其永遠不會轉換爲代理。返回對象本身
    • 應用場景:
      • 有些值不應被設置爲響應式的,例如複雜的第三方類實例或 Vue 組件對象。
      • 當渲染具有不可變數據源的大列表時,跳過代理轉換可以提高性能。
<template>
  <h2>{{state}}</h2>
  <button @click="testToRaw">測試toRaw</button>
  <button @click="testMarkRaw">測試markRaw</button>
</template>

<script lang="ts">
/* 
toRaw: 得到reactive代理對象的目標數據對象
*/
import {
  markRaw,
  reactive, toRaw,
} from 'vue'
export default {
  setup () {
    const state = reactive<any>({
      name: 'tom',
      age: 25,
    })

    const testToRaw = () => {
      const user = toRaw(state)
      user.age++  // 界面不會更新

    }

    const testMarkRaw = () => {
      const likes = ['a', 'b']
      // state.likes = likes
      state.likes = markRaw(likes) // likes數組就不再是響應式的了
      setTimeout(() => {
        state.likes[0] += '--'
      }, 1000)
    }

    return {
      state,
      testToRaw,
      testMarkRaw,
    }
  }
}
</script>

6.4 toRef

  • 爲源響應式對象上的某個屬性創建一個 ref對象, 二者內部操作的是同一個數據值, 更新時二者是同步的
  • 區別ref: 拷貝了一份新的數據值單獨操作, 更新時相互不影響
  • 應用: 當要將 某個prop 的 ref 傳遞給複合函數時,toRef 很有用
<template>
  <h2>App</h2>
  <p>{{state}}</p>
  <p>{{foo}}</p>
  <p>{{foo2}}</p>

  <button @click="update">更新</button>

  <Child :foo="foo"/>
</template>

<script lang="ts">
/*
toRef:
  爲源響應式對象上的某個屬性創建一個 ref對象, 二者內部操作的是同一個數據值, 更新時二者是同步的
  區別ref: 拷貝了一份新的數據值單獨操作, 更新時相互不影響
  應用: 當要將某個 prop 的 ref 傳遞給複合函數時,toRef 很有用
*/

import {
  reactive,
  toRef,
  ref,
} from 'vue'
import Child from './Child.vue'

export default {

  setup () {

    const state = reactive({
      foo: 1,
      bar: 2
    })

    const foo = toRef(state, 'foo')
    const foo2 = ref(state.foo)

    const update = () => {
      state.foo++
      // foo.value++
      // foo2.value++  // foo和state中的數據不會更新
    }

    return {
      state,
      foo,
      foo2,
      update,
    }
  },

  components: {
    Child
  }
}
</script>

<template>
  <h2>Child</h2>
  <h3>{{foo}}</h3>
  <h3>{{length}}</h3>
</template>

<script lang="ts">
import { computed, defineComponent, Ref, toRef } from 'vue'

const component = defineComponent({
  props: {
    foo: {
      type: Number,
      require: true
    }
  },

  setup (props, context) {
    const length = useFeatureX(toRef(props, 'foo'))

    return {
      length
    }
  }
})

function useFeatureX(foo: Ref) {
  const lenth = computed(() => foo.value.length)

  return lenth
}

export default component
</script>

6.5 customRef

  • 創建一個自定義的 ref,並對其依賴項跟蹤和更新觸發進行顯式控制
  • 需求: 使用 customRef 實現 debounce 的示例
<template>
  <h2>App</h2>
  <input v-model="keyword" placeholder="搜索關鍵字"/>
  <p>{{keyword}}</p>
</template>

<script lang="ts">
/*
customRef:
  創建一個自定義的 ref,並對其依賴項跟蹤和更新觸發進行顯式控制

需求: 
  使用 customRef 實現 debounce 的示例
*/

import {
  ref,
  customRef
} from 'vue'

export default {

  setup () {
    const keyword = useDebouncedRef('', 500)
    console.log(keyword)
    return {
      keyword
    }
  },
}

/* 
實現函數防抖的自定義ref
*/
function useDebouncedRef<T>(value: T, delay = 200) {
  let timeout: number
  return customRef((track, trigger) => {
    return {
      get() {
        // 告訴Vue追蹤數據
        track()
        return value
      },
      set(newValue: T) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          // 告訴Vue去觸發界面更新
          trigger()
        }, delay)
      }
    }
  })
}

</script>

6.6 provide 與 inject

  • provideinject提供依賴注入,功能類似 2.x 的provide/inject

  • 實現跨層級組件(祖孫)間通信

<template>
  <h1>父組件</h1>
  <p>當前顏色: {{color}}</p>
  <button @click="color='red'">紅</button>
  <button @click="color='yellow'">黃</button>
  <button @click="color='blue'">藍</button>
  
  <hr>
  <Son />
</template>

<script lang="ts">
import { provide, ref } from 'vue'
/* 
- provide` 和 `inject` 提供依賴注入,功能類似 2.x 的 `provide/inject
- 實現跨層級組件(祖孫)間通信
*/

import Son from './Son.vue'
export default {
  name: 'ProvideInject',
  components: {
    Son
  },
  setup() {
    
    const color = ref('red')

    provide('color', color)

    return {
      color
    }
  }
}
</script>
<template>
  <div>
    <h2>子組件</h2>
    <hr>
    <GrandSon />
  </div>
</template>

<script lang="ts">
import GrandSon from './GrandSon.vue'
export default {
  components: {
    GrandSon
  },
}
</script>
<template>
  <h3 :style="{color}">孫子組件: {{color}}</h3>
  
</template>

<script lang="ts">
import { inject } from 'vue'
export default {
  setup() {
    const color = inject('color')

    return {
      color
    }
  }
}
</script>

6.7 響應式數據的判斷

  • isRef: 檢查一個值是否爲一個 ref 對象
  • isReactive: 檢查一個對象是否是由 reactive 創建的響應式代理
  • isReadonly: 檢查一個對象是否是由 readonly 創建的只讀代理
  • isProxy: 檢查一個對象是否是由 reactive 或者 readonly 方法創建的代理

7、手寫組合API

7.1 shallowReactive 與 reactive

const reactiveHandler = {
  get (target, key) {

    if (key==='_is_reactive') return true

    return Reflect.get(target, key)
  },

  set (target, key, value) {
    const result = Reflect.set(target, key, value)
    console.log('數據已更新, 去更新界面')
    return result
  },

  deleteProperty (target, key) {
    const result = Reflect.deleteProperty(target, key)
    console.log('數據已刪除, 去更新界面')
    return result
  },
}

/* 
自定義shallowReactive
*/
function shallowReactive(obj) {
  return new Proxy(obj, reactiveHandler)
}

/* 
自定義reactive
*/
function reactive (target) {
  if (target && typeof target==='object') {
    if (target instanceof Array) { // 數組
      target.forEach((item, index) => {
        target[index] = reactive(item)
      })
    } else { // 對象
      Object.keys(target).forEach(key => {
        target[key] = reactive(target[key])
      })
    }

    const proxy = new Proxy(target, reactiveHandler)
    return proxy
  }

  return target
}


/* 測試自定義shallowReactive */
const proxy = shallowReactive({
  a: {
    b: 3
  }
})

proxy.a = {b: 4} // 劫持到了
proxy.a.b = 5 // 沒有劫持到


/* 測試自定義reactive */
const obj = {
  a: 'abc',
  b: [{x: 1}],
  c: {x: [11]},
}

const proxy = reactive(obj)
console.log(proxy)
proxy.b[0].x += 1
proxy.c.x[0] += 1

7.2 shallowRef 與 ref

/*
自定義shallowRef
*/
function shallowRef(target) {
  const result = {
    _value: target, // 用來保存數據的內部屬性
    _is_ref: true, // 用來標識是ref對象
    get value () {
      return this._value
    },
    set value (val) {
      this._value = val
      console.log('set value 數據已更新, 去更新界面')
    }
  }

  return result
}

/* 
自定義ref
*/
function ref(target) {
  if (target && typeof target==='object') {
    target = reactive(target)
  }

  const result = {
    _value: target, // 用來保存數據的內部屬性
    _is_ref: true, // 用來標識是ref對象
    get value () {
      return this._value
    },
    set value (val) {
      this._value = val
      console.log('set value 數據已更新, 去更新界面')
    }
  }

  return result
}

/* 測試自定義shallowRef */
const ref3 = shallowRef({
  a: 'abc',
})
ref3.value = 'xxx'
ref3.value.a = 'yyy'


/* 測試自定義ref */
const ref1 = ref(0)
const ref2 = ref({
  a: 'abc',
  b: [{x: 1}],
  c: {x: [11]},
})
ref1.value++
ref2.value.b[0].x++
console.log(ref1, ref2)

7.3 shallowReadonly 與 readonly

const readonlyHandler = {
  get (target, key) {
    if (key==='_is_readonly') return true

    return Reflect.get(target, key)
  },

  set () {
    console.warn('只讀的, 不能修改')
    return true
  },

  deleteProperty () {
    console.warn('只讀的, 不能刪除')
    return true
  },
}

/* 
自定義shallowReadonly
*/
function shallowReadonly(obj) {
  return new Proxy(obj, readonlyHandler)
}

/* 
自定義readonly
*/
function readonly(target) {
  if (target && typeof target==='object') {
    if (target instanceof Array) { // 數組
      target.forEach((item, index) => {
        target[index] = readonly(item)
      })
    } else { // 對象
      Object.keys(target).forEach(key => {
        target[key] = readonly(target[key])
      })
    }
    const proxy = new Proxy(target, readonlyHandler)

    return proxy 
  }

  return target
}

/* 測試自定義readonly */
/* 測試自定義shallowReadonly */
const objReadOnly = readonly({
  a: {
    b: 1
  }
})
const objReadOnly2 = shallowReadonly({
  a: {
    b: 1
  }
})

objReadOnly.a = 1
objReadOnly.a.b = 2
objReadOnly2.a = 1
objReadOnly2.a.b = 2

7.4 isRef, isReactive 與 isReadonly

/* 
判斷是否是ref對象
*/
function isRef(obj) {
  return obj && obj._is_ref
}

/* 
判斷是否是reactive對象
*/
function isReactive(obj) {
  return obj && obj._is_reactive
}

/* 
判斷是否是readonly對象
*/
function isReadonly(obj) {
  return obj && obj._is_readonly
}

/* 
是否是reactive或readonly產生的代理對象
*/
function isProxy (obj) {
  return isReactive(obj) || isReadonly(obj)
}


/* 測試判斷函數 */
console.log(isReactive(reactive({})))
console.log(isRef(ref({})))
console.log(isReadonly(readonly({})))
console.log(isProxy(reactive({})))
console.log(isProxy(readonly({})))

8、Composition API VS Option API

8.1 Option API的問題

  • 在傳統的Vue OptionsAPI中,新增或者修改一個需求,就需要分別在data,methods,computed裏修改 ,滾動條反覆上下移動

8.2 使用Compisition API

我們可以更加優雅的組織我們的代碼,函數。讓相關功能的代碼更加有序的組織在一起

9、新組件

9.1 Fragment(片斷)

  • 在Vue2中: 組件必須有一個根標籤
  • 在Vue3中: 組件可以沒有根標籤, 內部會將多個標籤包含在一個Fragment虛擬元素中
  • 好處: 減少標籤層級, 減小內存佔用
<template>
    <h2>aaaa</h2>
    <h2>aaaa</h2>
</template>

9.2 Teleport(瞬移)

  • Teleport 提供了一種乾淨的方法, 讓組件的html在父組件界面外的特定標籤(很可能是body)下插入顯示

ModalButton.vue

<template>
  <button @click="modalOpen = true">
      Open full screen modal! (With teleport!)
  </button>

  <teleport to="body">
    <div v-if="modalOpen" class="modal">
      <div>
        I'm a teleported modal! 
        (My parent is "body")
        <button @click="modalOpen = false">
          Close
        </button>
      </div>
    </div>
  </teleport>
</template>

<script>
import { ref } from 'vue'
export default {
  name: 'modal-button',
  setup () {
    const modalOpen = ref(false)
    return {
      modalOpen
    }
  }
}
</script>


<style>
.modal {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  background-color: rgba(0,0,0,.5);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.modal div {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: white;
  width: 300px;
  height: 300px;
  padding: 5px;
}
</style>

App.vue

<template>
  <h2>App</h2>
  <modal-button></modal-button>
</template>

<script lang="ts">
import ModalButton from './ModalButton.vue'

export default {
  setup() {
    return {
    }
  },

  components: {
    ModalButton
  }
}
</script>

9.3 Suspense(不確定的)

  • 它們允許我們的應用程序在等待異步組件時渲染一些後備內容,可以讓我們創建一個平滑的用戶體驗
<template>
  <Suspense>
    <template v-slot:default>
      <AsyncComp/>
      <!-- <AsyncAddress/> -->
    </template>

    <template v-slot:fallback>
      <h1>LOADING...</h1>
    </template>
  </Suspense>
</template>

<script lang="ts">
/* 
異步組件 + Suspense組件
*/
// import AsyncComp from './AsyncComp.vue'
import AsyncAddress from './AsyncAddress.vue'
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'))
export default {
  setup() {
    return {
     
    }
  },

  components: {
    AsyncComp,
    AsyncAddress
  }
}
</script>
  • AsyncComp.vue
<template>
  <h2>AsyncComp22</h2>
  <p>{{msg}}</p>
</template>

<script lang="ts">

export default {
  name: 'AsyncComp',
  setup () {
    // return new Promise((resolve, reject) => {
    //   setTimeout(() => {
    //     resolve({
    //       msg: 'abc'
    //     })
    //   }, 2000)
    // })
    return {
      msg: 'abc'
    }
  }
}
</script>
  • AsyncAddress.vue
<template>
<h2>{{data}}</h2>
</template>

<script lang="ts">
import axios from 'axios'
export default {
  async setup() {
    const result = await axios.get('/data/address.json')
    return {
      data: result.data
    }
  }
}
</script>

10、其他新的API

10.1 全新的全局API

  • createApp()
  • defineProperty()
  • defineAsyncComponent()
  • nextTick()

10.2 將原來的全局API轉移到應用對象

  • app.component()
  • app.config()
  • app.directive()
  • app.mount()
  • app.unmount()
  • app.use()

10.3 模板語法變化

  • v-model的本質變化
    • prop:value -> modelValue;
    • event:input -> update:modelValue;
  • .sync修改符已移除, 由v-model代替
  • v-if優先v-for解析

11、使用VuePress搭建在線文檔網站

11.1 在線文檔

VuePress官方在線文檔

11.2 搭建基本環境

# 將 VuePress 作爲一個本地依賴安裝
npm install -D vuepress

# 新建一個 docs 文件夾
mkdir docs

# 新建一個文件: docs/README.md
echo '# Hello VuePress!' > docs/README.md

# 啓動文檔項目
npx vuepress dev docs

# 構建靜態文件
npx vuepress build docs
  |-- docs
    |-- .vuepress
      |-- config.js
    |-- README.md

11.3 配置ts教程文檔

  1. 整體結構
|-- dist
|-- dics
  |-- .vuepress
    |-- public
      |-- ts-logo.png
    |-- config.js
  |-- chapter1
    |-- 01_初識TS.md
    |-- 02_安裝TS.md
    |-- 03_HelloWorld.md
  |-- chapter2
    |-- 1_type.md
    |-- 2_interface.md
    |-- 3_class.md
    |-- 4_function.md
    |-- 5_generic.md
    |-- 6_other.md
  |-- chapter3
    |-- 01_react.md
    |-- 02_vue.md
  |-- chapter4
    |-- README.md
  |-- README.md
|-- package.json
  1. docs/.vuepress/config.js
// 注意: base的值爲github倉庫的名稱
module.exports = {
  base: '/ts-study/', /* 基礎虛擬路徑: */
  dest: 'dist', /* 打包文件基礎路徑, 在命令所在目錄下 */
  title: 'TypeScript 入門', // 標題
  description: '學習使用 TypeScript', // 標題下的描述
  themeConfig: { // 主題配置
    sidebar: [ // 左側導航
      {
        title: '初識 TypeScript', // 標題
        collapsable: false, // 下級列表不可摺疊
        children: [ // 下級列表
          'chapter1/01_初識TS',
          'chapter1/02_安裝TS',
          'chapter1/03_HelloWorld'
        ]
      },
      {
        title: 'TypeScript 常用語法',
        collapsable: false,
        children: [
          'chapter2/1_type',
          'chapter2/2_interface',
          'chapter2/3_class',
          'chapter2/4_function',
          'chapter2/5_generic',
        ]
      },
    ]
  }
}
  1. docs/README.md
---
#首頁
home: true  
# 圖標
heroImage: /ts-logo.png
# 按鈕文本
actionText: 開始學習 →
# 按鈕點擊跳轉路徑
actionLink: /chapter1/01_初識TS
---
  1. package.json
"scripts": {
  "doc:dev": "vuepress dev docs",
  "doc:build": "vuepress build docs",
  "doc:deploy": "gh-pages -d docs/dist"
}

11.4 發佈到gitpage

  1. 使用git管理當前項目

  2. 將打包的項目推送到gitpage

# 下載工具包
yarn add -D gh-pages
# 執行打包命令
yarn doc:build
# 執行部署命令
yarn doc:deploy
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章