TT electronics推出用於混合動力汽車的大功率電阻

 

TT electronics公司推出WPRT系列線繞功率徑向端子(Wirewound Power Radial Terminal)電阻,符合AEC-Q200標準,設計用於滿足包括混合動力汽車(Hybrid Electric Vehicle, HEV)的大功率應用的特殊要求。   隨著WPRT系列的推出,TT electronics現在已提供業界最廣泛的規格和配置。新電阻系列由高純度陶瓷棒和壓配 (force-fit) 端帽(end-cap)構成,其上纏繞著導線元件,而後使用防火絕緣接合劑將其放置在陶瓷容器內,因此,在規定的溫度和超載狀況下電阻能夠完全防火。   這款電阻產品採用針對浪湧性能而優化的獨特設計,使之成為電動汽車和電機驅動的理想選擇。WPRT系列根據最高容差標準製造,可以很好地適合汽車保護裝置,大大改進了裝配性能。   TT electronics高級應用工程師 Stephen Oxley 評論說:“WPRT系列瞄準工業、汽車和能源領域的特定需求,通過開發最廣泛的此種類別功率電阻,TT electronics正在實踐承諾,不但支援客戶的節能設計,同時消除開發定制功率電阻解決方案的風險、成本和複雜性。”   WPRT系列可用於HEV預充電和BDU放電應用,以及保護電機避免浪湧電流的損壞。此外,該系列採用了浪湧保護設計,也適用於UPS系統。   WPRT系列包含了從10W至50W的六種額定功率,對於WPRT50 (50W)器件,可耐受最大250W超載。TT electronics提供的這些電阻都符合E24標準電阻值。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

2013自行車展-自行車輛更來電

2013自行車展盛大展出,根據集邦科技旗下綠能事業部EnergyTrend的現場了解,除了汽車廠豐田汽車提出了鋰電池在電動載具的應用看法外,自行車方面,包括了變速器廠以及電池廠也紛紛對於電動自行車提出各自的最新的商品展示,無論是零組件廠以及整車廠,都讓參觀者對於車輛電動化的延伸發展,產生的極大期待,讓電動自行車從過去的代步、健身兼具延伸續航距離,發展到未來將整合隨身電子裝置,讓人們對電動車量的普及充滿樂觀。

豐田汽車鋰電池電動車開賣

雖然全球電動汽車銷售市況不如預期,以Nissan leaf來看,銷量逐步好轉,目前在日本每月銷售約800輛、北美則有1,600輛,截至目前累計約兩萬多輛。根據自行車研討會的演說分享,豐田汽車所寄予厚望的重量級車款Prius α 預計在2013年底上市(全球),並且為首度配置的鋰電池中階車款,電池位置也因為體積輕巧而配置於車室內。豐田目前使用NCA材料做為正極材料,電池芯單元為3.7V, 5Ah,總電池容量約4.4kWh,2014年將推出新款車款IQ,12kWh,預計仍是使用NCA材料(圖一)。
 
圖一 豐田汽車

 
   
變速器、系統整合、電池模組熱鬧參與

作為自行車變速系統龍頭大廠,Shimano也將電子化與變速系統做了創新結合。在Shimano E tube計畫裡頭,將以變速系統Di2做為變速系統電子化為主要訴求,初期將落實在維修體系以及變速資訊的便利性上,未來更有可能藉著電池系統的搭配,提供車主更多的資訊來源(圖二),進而成為讓消費者改變使用習慣的革命。
 
圖二 Shimano E tube應用展示

 
三星SDI做為目前全球第二大的電池芯廠(僅次於合併後的Sanyo + Panasonic),除了在消費型應用的鋰電池產業著墨甚多外,近年也積極切入電動工具以及電動自行車等利基市場,更從過去的電池芯供應,逐步跨足到電池模組(圖三)。

以電動自行車來看,此次展出全方位的自行車電池,可提供後架、坐管、下管等不同位置的應用模組。目前全球各地電動自行車因使用族群不同而衍生出不同擺放位置的設計,中國市場大多以坐管以及後架位置為主要設計,主要訴求在荷重與價格;歐洲則集中在下管及座管位置,用於強調於設計外觀一體性,根據會場的展示DM,目前三星主推2.1Ah以及2.8Ah兩顆自行車專用三元材料電池芯,最大放電率可達到3C(C-rate),已可滿足各種路況的電動自行車的使用需求。
 
圖三 三星自行車電池解決方案展示


   
中華車在配合工業局補助政策,全力推廣電動機車銷售,並且在2012年繳出了六千輛的銷售佳績,未來也將從整車品牌退居幕後,藉著曾經扮演整車零組件整合的豐富經驗,轉型成為關鍵零組件提供者,本次特別展示了關鍵零組件品牌GreenTrans,提供中華車的Power Kit(圖四),包含了扭力感測器、LCD顯示、以及馬達驅動系統,2012年起已與台灣多家自行車廠合作,包括永祺、世同、達生、紹凱、吉安等,都採用中華的Power Kit系統組裝成車。

圖四 中華電動車套件供應分類展示


   
台灣電池模組大廠新普科技也展示了已耕耘三年的電動自行車商品,本次特別針對歐洲市場,與美國品牌Specialize、法國品牌BTwin進行合作(圖五),分別展出下管以及後座電池組產品。

與Specialize合作的登山車,下管產品特色在於電池與車架進行整合,呈現高度客製化,與客戶的關係也將因此而更形緊密。另外BTwin所推出的tilt折疊車系列,目前定位在堅固耐用,為了維持車體的強度,電池模組僅在延伸支架上做結合。新普科技憑藉著對於電池芯的品質掌握度,再加上電源管理的豐富經驗,相信未來在電動自行車市場後市可期。

圖五 新普科技電池模組展示

 
   
另一家作風一向低調的台灣電池模組大廠順達科技也積極跨足各種電池類型應用,於2012年與台達電合作的備用電源產品首次發表即得到設計大獎的殊榮外,此次自行車展也不遑多讓,搭配合作的車廠於現場展出了電動自行車模組的電池模組應用,與中國第一大馬達與控制器廠安乃達做配合,搭配了恪萊博(climbull)各系列整車系統(圖六),這樣的策略合作與全系列搭配的組合,也產生令人期待的商品。

恪萊博的產品策略主打戶外運動客層,屬於高階客群,有別於一般通勤族,由於戶外運動屬於白領階級活動,產品單價較高,且一般通勤族需要載重負荷,容易導致產品壽命偏低,對品牌形象反而產生負面影響,因而改以戶外運動客層為主要訴求對象為主,從應用定位面來扭轉可能造成的問題。而整車產品賣點在於搭配新型中置高速馬達,結合其馬達減速系統,可與外變速系統做最有效益的齒輪比搭配,進而提升產品的壽命與動力表現。

圖六 順達科技電池模組展示

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※FB行銷專家,教你從零開始的技巧

一個非侵入的Go事務管理庫——工作原理

在上一篇文章“一個非侵入的Go事務管理庫——如何使用”中,我講述了如何使用事務庫。有些讀者可能讀過”清晰架構(Clean Architecture)的Go微服務: 事物管理” ,其中描述了事務管理系統的舊版本。那篇文章和本文之間會有一些重疊。因為大多數人可能還沒有讀過那篇文章或者即使讀了也忘記了它的內容。因此為了照顧多數讀者,本文還是從頭開始(假設你沒有讀過前文)。如果你讀過,那你可以直接跳過熟悉的部分。

好的事務庫對於使用它的應用程序是透明的。在Go的“sql”庫中,有兩種類型的數據庫鏈接,“sql.DB”和“sql.Tx”。當你不需要事務支持時,使用“sql.DB”;否則使用“sql.Tx”。為了讓這兩種不同場景共享相同的持久層代碼,我們需要對數據庫鏈接進行一個封裝來同時支持這兩種場景。我從”db transaction in golang” 里得到了這個想法。

數據庫層的接口

數據庫層是事務管理庫中處理數據庫訪問的最低層。應用程序不需要修改該層,只有事務管理庫需要這樣做。

數據庫訪問封裝

下面是可同時支持事務和非事務操作的共享數據庫訪問接口, 它在“gdbc.go”中定義。

// SqlGdbc (SQL Go database connection) is a wrapper for SQL database handler ( can be *sql.DB or *sql.Tx)
// It should be able to work with all SQL data that follows SQL standard.
type SqlGdbc interface {
	Exec(query string, args ...interface{}) (sql.Result, error)
	Prepare(query string) (*sql.Stmt, error)
	Query(query string, args ...interface{}) (*sql.Rows, error)
	QueryRow(query string, args ...interface{}) *sql.Row
	// If need transaction support, add this interface
	Transactioner
}

// Transactioner is the transaction interface for database handler
// It should only be applicable to SQL database
type Transactioner interface {
	// Rollback a transaction
	Rollback() error
	// Commit a transaction
	Commit() error
	// TxEnd commits a transaction if no errors, otherwise rollback
	// txFunc is the operations wrapped in a transaction
	TxEnd(txFunc func() error) error

}

它有兩部分。一個是數據庫接口,它包含了常規的數據庫操作,如查詢表、更新表記錄。另一個事務接口,它包含里支持事務所需要的函數,如“提交”和“回滾”。“SqlGdbc”接口是兩者的結合。該接口將用於連接數據庫。

數據庫訪問接口的實現

下面是數據庫訪問接口的代碼實現。它在“sqlConnWrapper.go”文件中。它定義了兩個結構體,“SqlDBTx”是對“sql.DB”的封裝,將被非事務函數使用。“SqlConnTx”是對“sql.Tx”的封裝,將被事務函數使用。

// SqlDBTx is the concrete implementation of sqlGdbc by using *sql.DB
type SqlDBTx struct {
	DB *sql.DB
}

// SqlConnTx is the concrete implementation of sqlGdbc by using *sql.Tx
type SqlConnTx struct {
	DB *sql.Tx
}

func (sdt *SqlDBTx) Exec(query string, args ...interface{}) (sql.Result, error) {
	return sdt.DB.Exec(query, args...)
}

func (sdt *SqlDBTx) Prepare(query string) (*sql.Stmt, error) {
	return sdt.DB.Prepare(query)
}

func (sdt *SqlDBTx) Query(query string, args ...interface{}) (*sql.Rows, error) {
	return sdt.DB.Query(query, args...)
}

func (sdt *SqlDBTx) QueryRow(query string, args ...interface{}) *sql.Row {
	return sdt.DB.QueryRow(query, args...)
}

func (sdb *SqlConnTx) Exec(query string, args ...interface{}) (sql.Result, error) {
	return sdb.DB.Exec(query, args...)
}

func (sdb *SqlConnTx) Prepare(query string) (*sql.Stmt, error) {
	return sdb.DB.Prepare(query)
}

func (sdb *SqlConnTx) Query(query string, args ...interface{}) (*sql.Rows, error) {
	return sdb.DB.Query(query, args...)
}

func (sdb *SqlConnTx) QueryRow(query string, args ...interface{}) *sql.Row {
	return sdb.DB.QueryRow(query, args...)
}

事務接口的實現

下面是“Transactioner”接口的代碼實現,它在文件 “txConn.go”中。我從”database/sql Tx — detecting Commit or Rollback”中得到這個想法。

因為“SqlDBTx”不支持事務,所以它的所有函數都返回“nil”。

// DB doesn't rollback, do nothing here
func (cdt *SqlDBTx) Rollback() error {
	return nil
}

//DB doesnt commit, do nothing here
func (cdt *SqlDBTx) Commit() error {
	return nil
}

// DB doesnt rollback, do nothing here
func (cdt *SqlDBTx) TxEnd(txFunc func() error) error {
	return nil
}

func (sct *SqlConnTx) TxEnd(txFunc func() error) error {
	var err error
	tx := sct.DB

	defer func() {
		if p := recover(); p != nil {
			log.Println("found p and rollback:", p)
			tx.Rollback()
			panic(p) // re-throw panic after Rollback
		} else if err != nil {
			log.Println("found error and rollback:", err)
			tx.Rollback() // err is non-nil; don't change it
		} else {
			log.Println("commit:")
			err = tx.Commit() // if Commit returns error update err with commit err
		}
	}()
	err = txFunc()
	return err
}

func (sct *SqlConnTx) Rollback() error {
	return sct.DB.Rollback()
}

func (sct *SqlConnTx) Commit() error {
	return sct.DB.Commit()
}

持久層的接口

在數據庫層之上是持久層,應用程序使用持久層來訪問數據庫表中的記錄。你需要定義一個函數在本層中實現對事務的支持。下面是持久層的事務接口,它位於“txDataService.go”文件中。

// TxDataInterface represents operations needed for transaction support.
type TxDataInterface interface {
	// EnableTx is called at the end of a transaction and based on whether there is an error, it commits or rollback the
	// transaction.
	// txFunc is the business function wrapped in a transaction
	EnableTx(txFunc func() error) error
}

以下是它的實現代碼。它只是調用下層數據庫中的函數“TxEnd()”,該函數已在數據庫層實現。下面的代碼不是事務庫的代碼(它是本文中惟一的不是事務庫中的代碼),你需要在應用程序中實現它。

func (uds *UserDataSql) EnableTx(txFunc func() error) error {
	return uds.DB.TxEnd(txFunc)
}

獲取數據庫鏈接的代碼

除了我們上面描述的調用接口之外,在應用程序中你還需要先獲得數據庫鏈接。事務庫中有兩個函數可以完成這個任務。

返回”SqlGdbc”接口的函數

函數”Build()”(在”factory.go”中)將返回”SqlGdbc”接口。根據傳入的參數,它講返回滿足”SqlGdbc”接口的結構,如果需要事務支持就是“SqlConnTx”,不需要就是“SqlDBTx”。如果你不需要在應用程序中直接使用數據庫鏈接,那麼調用它是最好的。

// Build returns the SqlGdbc interface. This is the interface that you can use directly in your persistence layer
// If you don't need to cache sql.DB connection, you can call this function because you won't be able to get the sql.DB
// in SqlGdbc interface (if you need to do it, call BuildSqlDB()
func Build(dsc *config.DatabaseConfig) (gdbc.SqlGdbc, error) {
	db, err := sql.Open(dsc.DriverName, dsc.DataSourceName)
	if err != nil {
		return nil, errors.Wrap(err, "")
	}
	// check the connection
	err = db.Ping()
	if err != nil {
		return nil, errors.Wrap(err, "")
	}
	dt, err := buildGdbc(db, dsc)
	if err != nil {
		return nil, err
	}
	return dt, nil
}

func buildGdbc(sdb *sql.DB,dsc *config.DatabaseConfig) (gdbc.SqlGdbc, error){
	var sdt gdbc.SqlGdbc
	if dsc.Tx {
		tx, err := sdb.Begin()
		if err != nil {
			return nil, err
		}
		sdt = &gdbc.SqlConnTx{DB: tx}
		log.Println("buildGdbc(), create TX:")
	} else {
		sdt = &gdbc.SqlDBTx{sdb}
		log.Println("buildGdbc(), create DB:")
	}
	return sdt, nil
}

返回數據庫鏈接的函數

函數”BuildSqlDB()”(在”factory.go”中)將返回”sql.DB”。它會忽略傳入的事務標識參數。應用程序在調用這個函數獲得數據庫鏈接后,還需要根據事務標識自己生成“SqlConnTx”或“SqlDBTx”。如果你需要在應用程序里緩存”sql.DB”,那麼你必須調用這個函數。

// BuildSqlDB returns the sql.DB. The calling function need to generate corresponding gdbc.SqlGdbc struct based on
// sql.DB in order to use it in your persistence layer
// If you need to cache sql.DB connection, you need to call this function
func BuildSqlDB(dsc *config.DatabaseConfig) (*sql.DB, error) {
	db, err := sql.Open(dsc.DriverName, dsc.DataSourceName)
	if err != nil {
		return nil, errors.Wrap(err, "")
	}
	// check the connection
	err = db.Ping()
	if err != nil {
		return nil, errors.Wrap(err, "")
	}
	return db, nil

}

局限性

首先,它只支持SQL數據庫的事務。如果你有一個NoSql數據庫,那麼它不支持(大多數NoSql數據庫不支持事務)。

其次,如果你的事務跨越數據庫(例如在不同的微服務之間),那麼它將無法工作。常用的做法是使用“Saga Pattern”。你可以為事務中的每個操作編寫一個補償操作,並在回滾階段逐個執行補償操作。在應用程序中添加“Saga”解決方案並不困難。你可能會問,為什麼不把“Saga”加到事務庫中呢? 這是一個有趣的問題。我覺得還是單獨為“Saga”建一個庫比較合適。

第三,它不支持嵌套事務(Nested Transaction),因此你需要手動確保在代碼中沒有嵌套事務。如果代碼庫不是太複雜,這很容易做到。如果你有一個非常複雜的代碼庫,其中有很多事務和非事務代碼混在一起,那麼你需要一個支持嵌套事務的解決方案。我沒有花時間研究如何添加嵌套事務,但它應該有一定的工作量。如果你對此感興趣,可以從”database/sql: nested transaction or save point support”開始。到目前為止,對於大多數場景,當前的解決方案可能是在代價不大的情況下的最佳方案。

如何擴展庫的功能

“SqlGdbc”接口沒有列出“sql”包中的所有函數,只列出我的應用程序中需要的函數。你可以輕鬆地擴展該接口以包含其他函數。

例如,如果需要將全鏈路跟蹤(詳情請見”Go微服務全鏈路跟蹤詳解”)擴展到數據庫中,則可能需要在上下文中傳遞到數據庫函數中。“sql”庫已經支持具有上下文的數據庫函數。你只需要找到它們並將它們添加到”SqlGdbc”接口中,然後在”sqlConnWrapper “中實現它們。然後在持久層中,需要使用上下文作為參數調用函數。

源碼:

完整源碼: “jfeng45/gtransaction”

索引:

1 “一個非侵入的Go事務管理庫——如何使用”

2 “清晰架構(Clean Architecture)的Go微服務: 事物管理”

3 “db transaction in golang”

4 “database/sql Tx — detecting Commit or Rollback”

5 “Applying the Saga Pattern – GOTO Conference”

6 “database/sql: nested transaction or save point support”

7 “Go微服務全鏈路跟蹤詳解”

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

C# 9.0 新特性之只讀屬性和記錄

閱讀本文大概需要 2 分鐘。

大家好,這是 C# 9.0 新特性系列的第 4 篇文章。

熟悉函數式編程的童鞋一定對“只讀”這個詞不陌生。為了保證代碼塊自身的“純潔”,函數式編程是不能隨便“弄髒”外來事物(參數、變量等)的,所以“只讀”對函數式編程非常重要。

為了豐富 C# 對函數式編程支持,較新的 C# 版本引入了一些很有用的新特性。比如 C# 8 中就對 struct 類型的方法增加了 readonly 修飾符支持,被 readonly 修飾的方法是不能修改該方法所在類的屬性的。舉個例子:

public struct FooValue
{
    private int A { get; set; }
    public readonly int IncreaseA()
    {
        A = A + 1; // 報錯
        return A;
    }
}

而 C# 9 又進一步增加了對“只讀”的支持,此次增加了 init-only 屬性和 record 相關特性,下面一一介紹。

Init-only 屬性

我們知道類的屬性有 set 和 get 兩種訪問器,現在 C# 9 增加一種屬性訪問器:init。init 是 set 訪問器的變體,它的作用是使屬性只能在對象初始化的時候對其賦值,之後該屬性就是只讀的,因此叫 init-only 屬性。使用方式如下:

public class Foo
{
    public string PropA { get; init; }
    public string PropB { get; init; }
}

賦值操作:

var foo = new Foo {  PropA = "A", PropB = "B" };
foo.PropA = "AA"; // 報錯,PropA 此時是只讀的!

由於 init 是在初始化階段賦值,所以它可以在類內部修改 readonly 修飾的字段。比如:

public class Foo
{
    private readonly string propA;
    private readonly string propB;

    public string PropA
    {
        get => propA;
        init => propA = (value ?? throw new ArgumentNullException(nameof(propA)));
    }
    public string PropA
    {
        get => propB;
        init => propB = (value ?? throw new ArgumentNullException(nameof(propB)));
    }
}

如果你知道在構造函數中可以對只讀字段/屬性賦值就自然也理解這一點。

記錄 (Record)

做過財務系統的人都知道交易記錄一旦入賬是不能修改的,如果錄入錯誤,就要新錄入一筆負的記錄把之前的紅衝掉,再錄入正確的記錄。應對類似這種只讀記錄的場景,C# 9 引入了 Record(記錄,下文均使用中文的“記錄”)的概念,它用來支持整個對象的只讀特性(即實例化後為只讀)。使用方式如下:

public data class Foo
{
    public string PropA { get; init; }
    public string PropB { get; init; }
}

這裏用了一個 data 關鍵字,表示該類的對象只是純粹的記錄值,它不是可修改的狀態(在函數式編程中,所有的數據修改都是狀態在發生變化)。

上面的太麻煩了,可以這樣簡寫:

public data class Foo
{
    string PropA;
    string PropB;
}

默認屬性都是 public 的,如果實在要改為 private,可以在屬性定義前面加上 private 修飾符。

定位記錄 (Positional Record)

有時候為了初始化更方便,可以定義構造函數來給屬性賦值,初始化時只需要把屬性值按順序傳給構造函數即可,這個操作稱為定位構造(Positional Construction)。同樣,也可以使用解構函數(Deconstructor)來實現屬性的解構,即按照解構函數的參數順序從對象中提取屬性的值,被稱為定位解構(Positional Deconstructor)。實現了定位構造或定位解構的記錄稱為定位記錄(Positional Record)。下面是一個定位記錄的實現:

public data class Foo
{
    string PropA;
    string PropB;
    public Foo(string propA, string propB)
      => (PropA, PropB) = (propA, propB);
    public void Deconstruct(out string propA, out string propB)
      =>  (propA, propB) = (PropA, PropB);
}

這個寫法太麻煩了,可以直接簡寫為:

public data class Foo(string PropA, string PropB);

這樣簡短一句代碼,其內部默認實現了 init-only 自動屬性,且同時為所有屬性定義了構造函數和解構函數。

使用示例:

var foo = new Foo("AA", "BB");  // 構造定位
var (a, b) = foo;               // 解構定位

可以想象,記錄的大部分使用場景,以上簡寫的寫法能滿足需求。若有特殊場景,就不能簡單,需要進行自定義修改其默認行為。

with 表達式

當處理不可變數據時,若要生成不同的狀態,一個常見的場景是在一條舊記錄基礎上拷貝一條新的記錄。比如我們要修改 Foo 對象的 PropA 屬性,我們就要拷貝該對象生成一個新的對象。這個操作在函數式編程中被稱為“非破壞性修改 (non-destructive mutation)”。為了支持記錄這個操作,C# 9 引入了 with 表達式,它可以很方便在一條原有記錄基礎上創建一條新記錄。示例:

var other = foo with { PropA = "AA" };

with 表達式內部其實是通過一個默認的 protected 構造函數來實現的,大致如下:

protected Foo(Foo original)
{
    // 拷貝 original 的所有字段
}

如果默認實現的字段拷貝不符合你的需求,你也可以手動實現這個構造函數。

今天就分享到這裏,敬請期待一下篇關於 C# 9 新特性的文章!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

十萬同時在線用戶,需要多少內存?——Newbe.Claptrap 框架水平擴展實驗

Newbe.Claptrap 項目是筆者正在構建以反應式、Actor模式和事件溯源為理論基礎的一套服務端開發框架。本篇我們將來了解一下框架在水平擴展方面的能力。

前情提要

時隔許久,今日我們再次見面。首先介紹一下過往的項目情況:

第一次接觸本框架的讀者,可以先點擊此處閱讀本框架相關的基礎理論和工作原理。

日前,我們也編寫了一些預熱文章和工具,讀者可以通過以下鏈接進行了解:

  • 談反應式編程在服務端中的應用,數據庫操作優化,從 20 秒到 0.5 秒
  • docker-mcr 助您全速下載 dotnet 鏡像
  • Newbe.Claptrap 項目周報 1 – 還沒輪影,先用輪跑

今日主題

今天,我們來做一套實驗預演,來驗證 Newbe.Claptrap 框架,如何通過水平擴展的形式來適應逐漸增長的同時在線用戶數。

由於此次實驗涉及的內容很多,因此筆者將內容進行了歸類,讀者可以按照自己的興趣閱讀相關的章節:

  • 業務需求說明
  • 調用時序關係
  • 物理結構設計
  • 實際測試數據
  • 源碼構建說明
  • 常見問題解答

業務需求說明

先看看今天要實現的業務場景:

  • 用戶通過 API 登錄後生成一個 JWT token
  • 用戶調用 API 時驗證 JWT token 的有效性
  • 沒有使用常規的 JWS 公私鑰方式進行 JWT token 頒發,而是為每個用戶單獨使用 secret 進行哈希驗證
  • 驗證看不同的在線用戶需要消耗的內存情況
  • 用戶登錄到生成 token 所消耗時間不得超過 200 ms
  • tokn 的驗證耗時不得超過 10 ms

吹牛先打草稿

筆者沒有搜索到於 “在線用戶數” 直接相關的理論定義,因此,為了避免各位的理解存在差異。筆者先按照自己的理解來點明:在線用戶數到底意味着什麼樣的技術要求?

未在線用戶若上線,不應該受到已在線用戶數的影響

如果一個用戶登錄上線需要消耗 100 ms。那麼不論當前在線的用戶數是十人還是百萬人。這個登錄上線所消耗的時間都不會明顯的超過 100 ms。

當然,有限的物理硬件肯定會使得,當在線用戶數超過一個閾值(例如兩百萬)時,新用戶登錄上線會變慢甚至出錯。

但是,增加物理機器就能提高這個閾值,我們就可以認為水平擴展設計是成功的。

對於任意一個已在線用戶,得到的系統性能反饋應當相同

例如已在線的用戶查詢自己的訂單詳情,需要消耗 100 ms。那麼當前任何一個用戶進行訂單查詢的平均消耗都應該穩定在 100 ms。

當然,這裏需要排除類似於 “搶購” 這種高集中性能問題。此處主要還是討論日常穩定的容量增加。(我們以後會另外討論 “搶購” 這種問題)

具體一點可以這樣理解。假設我們做的是一個雲筆記產品。

那麼,如果增加物理機器就能增加同時使用雲筆記產品的用戶數,而且不犧牲任何一個用戶的性能體驗,我們就認為水平擴展設計是成功的。

在此次的實驗中,若用戶已經登錄,則驗證 JWT 有效性的時長大約為 0.5 ms。

調用時序關係

簡要說明:

  1. 客戶端發起登錄請求將會逐層傳達到 UserGrain 中
  2. UserGrain 將會在內部激活一個 Claptrap 來進行維持 UserGrain 中的狀態數據。包括用戶名、密碼和用於 JWT 簽名的 Secret。
  3. 隨後的生成 JWT 生成和驗證都將直接使用 UserGrain 中的數據。由於 UserGrain 中的數據是在一段時間內是 “緩存” 在內存中的。所以之後的 JWT 生成和驗證將非常快速。實測約為 0.5 ms。

物理結構設計

如上圖所示,便是此次進行測試的物理組件:

名稱 說明
WebAPI 公開給外部調用 WebAPI 接口。提供登錄和驗證 token 的接口。
Orleans Cluster 託管 Grain 的核心進程.
Orleans Gateway 於 Orleans Cluster 基本相同,但是 WebAPI 只能與 Gateway 進行通信
Orleans Dashboard 於 Orleans Gateway 基本相同,但增加了 Dashboard 的展示,以查看整個 Orleans 集群的情況
Consul 用於 Orleans 集群的集群發現和維護
Claptrap DB 用於保存 Newbe.Claptrap 框架的事件和狀態數據
Influx DB & Grafana 用於監控 Newbe.Claptrap 相關的性能指標數據

此次實驗的 Orleans 集群節點的數量實際上是 Cluster + Gateway + Dashboard 的總數。以上的劃分實際上是由於功能設定的不同而進行的區分。

此次測試 “水平擴展” 特性的物理節點主要是 Orleans Cluster 和 Orleans Gateway 兩個部分。將會分別測試以下這些情況的內存使用情況。

Orleans Dashboard Orleans Gateway Orleans Cluster
1 0 0
1 1 1
1 3 5

此次實驗採用的是 Windows Docker Desktop 結合 WSL 2 進行的部署測試。

以上的物理結構實際上是按照最為此次實驗最為複雜的情況設計的。實際上,如果業務場景足夠簡單,該物理結構可以進行裁剪。詳細可以查看下文 “常見問題解答” 中的說明。

實際測試數據

以下,分別對不同的集群規模和用戶數量進行測試

0 Gateway 0 Cluster

默認情況下,剛剛啟動 Dashboard 節點時,通過 portainer 可以查看 container 佔用的內存約為 200 MB 左右,如下圖所示:

通過測試控制台,向 WebAPI 發出 30,000 次請求。每批 100 個請求,分批發送。

經過約兩分鐘的等待后,再次查看內存情況,約為 9.2 GB,如下圖所示:

因此,我們簡單的估算每個在線用戶需要消耗的內存情況約為 (9.2*1024-200)/30000 = 0.3 MB。

另外,可以查看一些輔助數據:

CPU 使用情況

網絡吞吐量

Orleans Dashboard 情況。左上角的 TOTAL ACTIVATIONS 中 30,000 即表示當前內存中存在的 UserGrain 數量,另外的 3 個為 Dashboard 使用的 Grain。

Grafana 中查看 Newbe.Claptrap 的事件平均處理時長約為 100-600 ms。此次測試的主要是內存情況,處理時長的採集時間為 30s 一次,因此樣本數並不多。關於處理時長我們將在後續的文章中進行詳細測試。

Grafana 中查看 Newbe.Claptrap 的事件的保存花費的平均時長約為 50-200 ms。事件的保存時長是事件處理的主要部分。

Grafana 中查看 Newbe.Claptrap 的事件已處理總數。一種登錄了三萬次,因此事件總數也是三萬。

1 Gateway 1 Cluster

接下來,我們測試額外增加兩個節點進行測試。

還是再提一下,Orleans 集群節點的數量實際上是 Cluster + Gateway + Dashboard 的總數。因此,對比上一個測試,該測試的節點數為 3。

測試得到的內存使用情況如下:

用戶數 節點平均內存 內存總佔用
10000 1.8 GB 1.8*3 = 5.4 GB
20000 3.3 GB 3.3*3 = 9.9 GB
30000 4.9 GB 4.9*3 = 14.7 GB

那麼,以三萬用戶為例,平均每個用戶佔用的內存約為 (14.7*1024-200*3)/30000 = 0.48 MB

為什麼節點數增加了,平均消耗內存上升了呢?筆者推測,沒有進行過驗證:節點增加,實際上節點之間的通訊還需要消耗額外的內存,因此平均來說有所增加。

3 Gateway 5 Cluster

我們再次增加節點。總結點數為 1 (dashboard) + 3 (cluster) + 5 (gateway) = 9 節點

測試得到的內存使用情況如下:

用戶數 節點平均內存 內存總佔用
20000 1.6 GB 3.3*9 = 14.4 GB
30000 2 GB 4.9*9 = 18 GB

那麼,以三萬用戶為例,平均每個用戶佔用的內存約為 (18*1024-200*9)/30000 = 0.55 MB

十萬用戶究竟要多少內存?

以上所有的測試都是以三萬為用戶數進行的測試,這是一個特殊的数字。因為繼續增加用戶數的話,內存將會超出測試機的內存余量。(求贊助兩條 16G)

如果繼續增加用戶數,將會開始使用操作系統的虛擬內存。雖然可以運行,但是運行效率會降低。原來登錄可能只需要 100 ms。使用到虛擬內存的用戶則需要 2 s。

因此,速度降低的情況下,在驗證需要多少內存意義可能不大。

但是,這不意味着不能夠繼續登錄,以下便是 1+1+1 的情況下,十萬用戶全部登錄后的情況。(有十萬用戶同時在線,加點內存吧,不差錢了。)

源碼構建說明

此次測試的代碼均可以在文末的樣例代碼庫中找到。為了方便讀者自行實驗,主要採用的是 docker-compose 進行構建和部署。

因此對於測試機的唯一環境需求就是要正確的安裝好 Docker Desktop 。

可以從以下任一地址獲取最新的樣例代碼:

  • https://github.com/newbe36524/Newbe.Claptrap.Examples
  • https://gitee.com/yks/Newbe.Claptrap.Examples

快速啟動

使用控制台進入 src/Newbe.Claptrap.Auth/LocalCluster 文件夾。運行以下命令便可以在本地啟動所有的組件:

1
docker-compose up -d

途中需要拉取一些託管於 Dockerhub 上的公共鏡像,請確保本地已經正確配置了相關的加速器,以便您可以快速構建。可以參看這篇文檔進行設置

成功啟動之後可以通過 docker ps 查看到所有的組件。

1
2
3
4
5
6
7
8
9
10
11
12
13
PS>docker ps
CONTAINER ID        IMAGE                                                                            COMMAND                  CREATED             STATUS              PORTS                                                                                                                              NAMES
66470e5393e2        registry.cn-hangzhou.aliyuncs.com/newbe36524/newbe-claptrap-auth-webapi          "dotnet Newbe.Claptr…"   4 hours ago         Up About an hour    0.0.0.0:10080->80/tcp                                                                                                              localcluster_webapi_1
3bbaf5538ab9        registry.cn-hangzhou.aliyuncs.com/newbe36524/newbe-claptrap-auth-backendserver   "dotnet Newbe.Claptr…"   4 hours ago         Up About an hour    80/tcp, 443/tcp, 0.0.0.0:19000->9000/tcp, 0.0.0.0:32785->11111/tcp, 0.0.0.0:32784->30000/tcp                                       localcluster_dashboard_1
3f60f51e4641        registry.cn-hangzhou.aliyuncs.com/newbe36524/newbe-claptrap-auth-backendserver   "dotnet Newbe.Claptr…"   4 hours ago         Up About an hour    80/tcp, 443/tcp, 9000/tcp, 0.0.0.0:32787->11111/tcp, 0.0.0.0:32786->30000/tcp                                                      localcluster_cluster_gateway_1
7d516ada2b26        registry.cn-hangzhou.aliyuncs.com/newbe36524/newbe-claptrap-auth-backendserver   "dotnet Newbe.Claptr…"   4 hours ago         Up About an hour    80/tcp, 443/tcp, 9000/tcp, 30000/tcp, 0.0.0.0:32788->11111/tcp                                                                     localcluster_cluster_core_1
fc89fcd973f9        grafana/grafana                                                                  "/run.sh"                4 hours ago         Up 6 seconds        0.0.0.0:23000->3000/tcp                                                                                                            localcluster_grafana_1
1f10ed0eb25f        postgres                                                                         "docker-entrypoint.s…"   4 hours ago         Up About an hour    0.0.0.0:32772->5432/tcp                                                                                                            localcluster_claptrap_db_1
d5d2bec74311        adminer                                                                          "entrypoint.sh docke…"   4 hours ago         Up About an hour    0.0.0.0:58080->8080/tcp                                                                                                            localcluster_adminer_1
4c4be69f2f41        bitnami/consul                                                                   "/opt/bitnami/script…"   4 hours ago         Up About an hour    8300-8301/tcp, 8500/tcp, 8301/udp, 8600/tcp, 8600/udp                                                                              localcluster_consulnode3_1
88811d3aa0d2        influxdb                                                                         "/entrypoint.sh infl…"   4 hours ago         Up 6 seconds        0.0.0.0:29086->8086/tcp                                                                                                            localcluster_influxdb_1
d31c73b62a47        bitnami/consul                                                                   "/opt/bitnami/script…"   4 hours ago         Up About an hour    8300-8301/tcp, 8500/tcp, 8301/udp, 8600/tcp, 8600/udp                                                                              localcluster_consulnode2_1
72d4273eba2c        bitnami/consul                                                                   "/opt/bitnami/script…"   4 hours ago         Up About an hour    0.0.0.0:8300-8301->8300-8301/tcp, 0.0.0.0:8500->8500/tcp, 0.0.0.0:8301->8301/udp, 0.0.0.0:8600->8600/tcp, 0.0.0.0:8600->8600/udp   localcluster_consulnode1_1

啟動完成之後,便可以通過以下鏈接來查看相關的界面

地址 說明
http://localhost:19000 Orleans Dashboard 查看 Orleans 集群中各節點的狀態
http://localhost:10080 Web API 基地址,此次使用所測試的 API 基地址
http://localhost:23000 Grafana 地址,查看 Newbe.Claptrap 相關的性能指標情況

源碼構建

使用控制台進入 src/Newbe.Claptrap.Auth 文件夾。運行以下命令便可以在本地完成代碼的構建:

1
2
./LocalCluster/pullimage.cmd
docker-compose build

pullimage.cmd 使用了筆者編寫的 docker-mcr 加速器功能。您可以通過該文檔來了解其工作原理

等待構建完畢之後,本地便生成好了相關的鏡像。接下來便可以初次嘗試在本地啟動應用:

使用控制台進入 src/Newbe.Claptrap.Auth/LocalCluster 文件夾。運行以下命令便可以啟動相關的容器:

1
docker-compose up -d

常見問題解答

文中為何沒有說明代碼和配置的細節?

本文主要為讀者展示該方案的實驗可行性,具體應該如何應用 Newbe.Claptrap 框架編寫代碼,並非本文的主旨,因此沒有提及。

當然,另外一點就是目前框架沒有最終定版,所有內容都有可能發生變化,講解代碼細節意義不大。

但可以提前說明的是:編寫非常簡單,由於本樣例的業務需求非常簡單,因此代碼內容也不多。全部都可以在示例倉庫中找到。

用 Redis 存儲 Token 也可以實現上面的需求,為什麼要選擇這個框架?

目前來說,筆者沒有十足的理由說服讀者必須使用哪種方案,此處也只是提供一種可行方案,至於實際應該選擇哪種方案,應該有讀者自己來考量,畢竟工具是否趁手還是需要試試才知道。

如果是最多 100 個在線用戶,那怎麼裁剪系統?

必要的組件只有 Orleans Dashboard 、 WebAPI 和 Claptrap Db。其他的組件全部都是非必要的。而且如果修改代碼, Orleans Dashboard 和 WebAPI 是可以合併的。

所以最小規模就是一個進程加一個數據庫。

Grafana 為什麼沒有報表?

Grafana 首次啟動之後需要手動的創建 DataSource 和導入 Dashboard.

本實驗相關的參數如下:

DataSource

  • URL: http://influxdb:8086
  • Database: metricsdatabase
  • User: claptrap
  • Password: claptrap

點擊此處獲取 Dashboard 定義文件

測試機的物理配置是什麼?

沒有專門騰內存,未開始測試前已佔用 16GB 內存。以下是測試機的身材數據(洋垃圾,3500 元左右):

處理器 英特爾 Xeon (至強) E5-2678 v3 @ 2.50GHz 12 核 24 線程
主板 HUANANZHI X99-AD3 GAMING (Wellsburg)
顯卡 Nvidia GeForce GTX 750 Ti (2 GB / Nvidia)
內存 32 GB (三星 DDR3L 1600MHz) 2013 年產 高齡內存
主硬盤 金士頓 SA400S37240G (240 GB / 固態硬盤)

如果您有更好的物理配置,相信可以得出更加優秀的數據。

即使是 0.3 MB 平均每用戶的佔用的我也覺得太高了

框架還在優化。未來會更好。

最後但是最重要!

最近作者正在構建以反應式、Actor模式和事件溯源為理論基礎的一套服務端開發框架。希望為開發者提供能夠便於開發出 “分佈式”、“可水平擴展”、“可測試性高” 的應用系統 ——Newbe.Claptrap

本篇文章是該框架的一篇技術選文,屬於技術構成的一部分。如果讀者對該內容感興趣,歡迎轉發、評論、收藏文章以及項目。您的支持是促進項目成功的關鍵。

GitHub 項目地址:https://github.com/newbe36524/Newbe.Claptrap

Gitee 項目地址:https://gitee.com/yks/Newbe.Claptrap

如果你對該項目感興趣,你可以通過 github issues 提交您的看法。

如果您無法正常訪問 github issue,您也可以發送郵件到 newbe-claptrap@googlegroups.com 來參与我們的討論。

點擊鏈接 QQ 交流【Newbe.Claptrap】:https://jq.qq.com/?_wv=1027&k=5uJGXf5。

​​​​​​​

    • 本文作者: newbe36524
    • 本文鏈接: https://www.newbe.pro/Newbe.Claptrap/How-Many-RAMs-In-Used-While-There-Are-One-Hundred-Thousand-Users-Online/
    • 版權聲明: 本博客所有文章除特別聲明外,均採用 BY-NC-SA 許可協議。轉載請註明出處!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※FB行銷專家,教你從零開始的技巧

JSON類庫Jackson優雅序列化Java枚舉類

1. 前言

在Java開發中我們為了避免過多的魔法值,使用枚舉類來封裝一些靜態的狀態代碼。但是在將這些枚舉的意思正確而全面的返回給前端卻並不是那麼順利,我們通常會使用Jackson類庫序列化對象為JSON,今天就來講一個關於使用Jackson序列化枚舉的通用性技巧。

2. 通用枚舉範式

為了便於統一處理和規範統一的風格,建議指定一個統一的抽象接口,例如:

/**
 * The interface Enumerator.
 */
public interface Enumerator {
    /**
     * Code integer.
     *
     * @return the integer
     */
    Integer code();

    /**
     * Description string.
     *
     * @return the string
     */
    String description();
}

我們來寫一個實現來標識性別:

public enum GenderEnum implements Enumerator {
   
    UNKNOWN(0, "未知"),

    MALE(1, "男"),

    FEMALE(2, "女");


    private final Integer code;
    private final String description;

    GenderEnum(Integer code, String description) {
        this.code = code;
        this.description = description;
    }


    @Override
    public Integer code() {
        return code;
    }

    @Override
    public String description() {
        return description;
    }
}

3. 序列化枚舉

如果我們直接使用Jackson對枚舉進行序列化,將只能簡單的輸出枚舉的String名稱:

    @Resource
    private ObjectMapper objectMapper;

    @Test
    void enumTest() {
        try {
            String s = objectMapper.writeValueAsString(GenderEnum.MALE);
            // 輸出字符串 MALE
            System.out.println(s);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }

我們期望將GenderEnum.MALE 序列化為 {"code":1,"description":"男"} 。我們可以向ObjectMapper定製化一個Module來實現這種個性化需求:

         // 聲明一個簡單Module 對象
         SimpleModule module = new SimpleModule();
           // 給Module 添加一個序列化器
            module.addSerializer(Enumerator.class, new JsonSerializer<Enumerator>() {
                @Override
                public void serialize(Enumerator value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
                   // 開始寫入對象
                    gen.writeStartObject();
                    // 分別指定 k v   code   description 
                    gen.writeNumberField("code",value.code());
                    gen.writeStringField("description",value.description());
                    // 顯式結束操作
                    gen.writeEndObject();
                }
            });

        // 註冊 Module
        objectMapper.registerModule(module);

然後再次執行就會獲取我們期望的結果。然而這並不算合理。

4. Spring Boot 中自動全局配置

在Spring Boot應用中我們希望能全局配置。Spring Boot的自動配置為我們提供了一個個性化定製ObjectMapper的可能性,你只需要聲明一個Jackson2ObjectMapperBuilderCustomizer並注入Spring IoC:

@Bean
public Jackson2ObjectMapperBuilderCustomizer enumCustomizer(){
    return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.serializerByType(Enumerator.class, new JsonSerializer<Enumerator>() {
        @Override
        public void serialize(Enumerator value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
                    gen.writeStartObject();
                    gen.writeNumberField("code",value.code());
                    gen.writeStringField("description",value.description());
                    gen.writeEndObject();


        }
    });
}

這樣就實現了全局配置。

5. 總結

這裏我們介紹了如何定製Jackson庫以達到對枚舉進行更加友好的序列化的目的。其實不單單枚舉,你也可以實現其它序列化,反序列化,時間輸出格式的定製。這些特性留給你自己挖掘。多多關注:碼農小胖哥 獲取更多開發技巧。

關注公眾號:Felordcn 獲取更多資訊

個人博客:https://felord.cn

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

Project Loom:Reactive模型和協程進行時(翻譯)

Java 15將發布Project Loom的第一個版本。我相信這將改變JVM。在這篇文章中,我想深入探討一下導致我相信這一點的原因。

首先,我們需要了解核心問題。然後,我將嘗試描述以前的技術如何解決它。之後,我們將看到Project Loom採取的方法。最後,我將推斷後者可能對生態系統產生什麼影響。

Project Loom

我們首先必須記住,很長一段時間以來,計算機只有一個內核。即使這樣,還是需要同時運行多個程序:這至少要運行兩個操作系統和適當的程序。

為了實現并行性的幻覺,它依賴於一個技巧。它運行一個程序,如果該程序沒有在特定的時間範圍內完成,它將存儲其狀態以供以後使用。然後,它運行下一個要運行的程序。有幾種算法可用來調度下一個程序:循環調度,加權循環調度等。好處是,所有這些都可以通過操作系統線程的概念很好地從開發人員中抽象出來。該操作系統通吃繁重的護理,包括存儲執行的狀態。但是,線程有兩個缺點:

  • 線程很重,因為它帶有很多狀態
  • 線程需要大量的機器資源來創建
    在所有情況下,現代OS都允許M個線程,其中M是數千個線程。現代機器是多核的,提供N個核,比M少兩個數量級。擁有多個內核(無論是物理內核還是虛擬內核)並不會從根本上改變底層機制:M遠高於N,並且OS負責從線程到內核的映射。

阻塞線程

面的模型在傳統方案中效果很好,但在網絡方案中效果不佳。假設有一個Web服務器需要響應HTTP請求。在過去不太好的時候,CGI是處理請求的一種方法。它將每個請求映射到一個流程,以便處理創建整個新流程所需的請求,並在發送響應后將其清除。

Java EE應用程序服務器極大地改善了這種情況,因為實現將線程保留在池中以供以後重用。但是,想象一下響應的生成需要時間,例如因為它需要訪問數據庫以讀取數據。在數據庫返回數據之前,線程需要等待。

這意味着線程實際上正在等待其生命周期的大部分時間。一方面,此類線程自己不使用任何CPU。另一方面,它使用其他種類的資源,尤其是內存。

同樣,太多線程是操作系統的負擔:操作系統必須在數量有限的CPU內核上平衡大量線程。這花費了寶貴的CPU周期,因此,操作系統正在與應用程序競爭CPU。

現在,併發請求的數量可以遠遠超過服務器可用的線程數量。因此,阻塞的線程浪費資源,並使服務器無響應。為了解決這個問題,可以添加更多的Web服務器來處理負載:這是水平縮放。

在大多數情況下,水平縮放就足夠了。等待阻塞線程所花費的時間是浪費的,但是沒有任何相關的費用…除非一個人的基礎架構位於’雲’中。在那種情況下,人們要為未使用的資源付費:這絕不是一個明智的主意。

單線程,反應式和Kotlin協程模型

對於開發人員來說,管理多個線程很複雜。例如,如果您一直在使用(或開發)Swing應用程序,則可能知道“灰色矩形效果”。當用戶與窗口的交互(例如單擊)啟動長時間運行的任務時,就會發生這種情況。如果將另一個窗口移到Swing窗口上,然後再移出,Swing不會重畫另一個窗口與Swing窗口相交的區域,而不會留下難看的灰色矩形。原因是長時間運行的任務是在“ 事件調度線程”上啟動的,而不是在專用線程上啟動的。而且這很容易避免,甚至不涉及在共享的可變狀態上進行同步!

為了避免這種情況,某些堆棧完全禁止開發人員使用多個線程。例如,Node.js的API僅提供一個非阻塞事件循環線程:提交的函數採用回調的形式。請注意,這不會阻止實現使用多個線程。

該反應的方法是另一種選擇,其實頗為相似。儘管它擺脫了單線程API的限制,並提供了反壓力機制,但它仍然需要非阻塞代碼。由於OS線程很昂貴,因此Reactive將它們池化,並在整個應用程序生命周期中重複使用它們。核心過程是從池中獲取空閑線程,讓其執行代碼,然後將線程釋放回池中。這就是為什麼它需要非阻塞代碼的原因:如果代碼阻塞了,那麼執行線程將不會被釋放,並且池將在某一點或另一點耗盡。

我感興趣地觀察了Reactive模型如何像篝火一樣在Spring生態系統中傳播,儘管我選擇站在一邊。恕我直言,反應式有幾個缺點:

  • 編寫(和閱讀!)反應式代碼的思維方式與編寫傳統代碼的思維方式非常不同。我願意承認改變心態只需要時間,持續時間取決於每個開發人員。
  • 儘管真正的開發人員不會調試,但我知道很多人會調試-包括我自己。由於上述線程切換的魔力,要跟蹤一段代碼及其相關的狀態並不容易。這需要足夠的工具,例如帶有相關插件的 IntelliJ IDEA 。
  • 最後,出於相同的原因,傳統的堆棧跟蹤也無濟於事。一些黑魔法可以繞開它。但是,這不是為了膽小者。有關選項的完整列表,請查看此文檔。

Kotlin語言提供了Reactive方法的替代方法:協程。簡而言之,當使用suspend關鍵字時,Kotlin編譯器會在字節碼中生成一個有限狀態機。好處是在協程塊中調用的函數看起來像是順序執行的,儘管它們是并行執行的-更確切地說,取決於確切的範圍,有可能會執行。

Project Loom和虛擬線程

Reactive模型和Kotlin協程都在客戶端代碼和JVM線程之間添加了一個額外的抽象層。框架/庫的職責是動態地將一個映射到另一個。問題的癥結在於JVM線程是OS線程的薄包裝:請記住,OS線程創建起來很昂貴,並且數量限製為數千個。

Project Loom的目標是實際上將JVM線程與OS線程解耦。
當我第一次意識到該倡議時,其想法是創建一個稱為Fiber(線程,Project Loom,您能抓住麻煩嗎?)的抽象。一個Fiber責任是讓一個操作系統線程,使其運行代碼,釋放回池,就像無棧一樣。

當前的建議有很大的不同:Fiber它沒有使用新的類,而是重新使用了Java開發人員非常熟悉的一個類- java.lang.Thread!

因此,在新的JVM版本中,某些Thread對象可能是重量級的並映射到OS線程,而另一些對象可能是虛擬線程。

Project Loom發布的後續影響

主要問題是,既然JVM API提供了對OS線程的抽象,那麼其他抽象(例如響應式和協程)又會變成什麼樣呢?我對預測不滿意,但以下是Reactive /協程背後的公司可能採取的一些態度:

  • 正面態度,他們意識到自己的框架不再帶來任何附加值,而只是重複。他們停止了開發工作,僅向現有客戶提供維護版本。他們幫助說客戶遷移到新的ThreadAPI,一些幫助可能是以付費諮詢的形式。
  • 反面態度,他們在各自的框架中投入了大量的精力之後,他們決定繼續進行,好像什麼也沒有發生。例如,Spring框架負責實際設計一個共享的Reactive API,稱為Reactive Streams,沒有Spring依賴項。當前有兩種實現,RxJava v2和Pivotal的Project Reactor。另一方面,JetBrains宣傳Kotlin的協程是并行運行代碼的最簡單方法。
  • 中間態度。這兩個框架都將繼續其生命,但是會將它們各自的基礎實現更改為使用虛擬線程。
    由於沉沒成本的謬誤,排在第一位的可能性極小:銷售和市場營銷將努力保持其“競爭優勢”-無論在他們眼中意味着什麼。儘管有些工程師出於相同的原因希望保留現有代碼,但其他一些工程師則將努力使用新的API。因此,我也不相信第二名也會發生。但是,我認為這兩個工程派之間都發揮着力量,然後它們與市場營銷/銷售之間將在#3之間找到平衡。

結論

Project Looms將現有的Thread實現方式從OS線程的映射更改為可以表示此類線程或虛擬線程的抽象。就其本身而言,這是一個有趣的舉動,它在一個平台上歷來比創新更重視向後兼容性。與其他最新的Java版本相比,此功能是真正的遊戲規則改變者。一般而言,開發人員應儘快開始熟悉它。打算學習Reactive和協程的開發人員可能應該退後一步,並評估他們是否應該學習新的ThreadAPI- 是否需要。

翻譯原文

https://blog.frankel.ch/project-loom-reactive-coroutines/

擴展閱讀

Project Loom地址: https://github.com/openjdk/loom
Project Loom現有狀態: http://cr.openjdk.java.net/~rpressler/loom/loom/sol1_part1.html

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

手摸手帶你理解Vue響應式原理

前言

響應式原理作為 Vue 的核心,使用數據劫持實現數據驅動視圖。在面試中是經常考查的知識點,也是面試加分項。

本文將會循序漸進的解析響應式原理的工作流程,主要以下面結構進行:

  1. 分析主要成員,了解它們有助於理解流程
  2. 將流程拆分,理解其中的作用
  3. 結合以上的點,理解整體流程

文章稍長,但部分是代碼,還請耐心觀看。為了方便理解原理,文中的代碼會進行簡化,如果可以請對照源碼學習。

主要成員

在響應式原理中,Observe、Dep、Watcher 這三個類是構成完整原理的主要成員。

  • Observe,響應式原理的入口,根據數據類型處理觀測邏輯
  • Dep,依賴收集器,屬性都會有一個Dep,方便發生變化時能夠找到對應的依賴觸發更新
  • Watcher,用於執行更新渲染,組件會擁有一個渲染Watcher,我們常說的收集依賴,就是收集 Watcher

下面來看看這些類的實現,包含哪些主要屬性和方法。

Observe:我會對數據進行觀測

溫馨提示:代碼里的序號對應代碼塊下面序號的講解

// 源碼位置:/src/core/observer/index.js
class Observe {
  constructor(data) {
    this.dep = new Dep()
    // 1
    def(data, '__ob__', this)
    if (Array.isArray(data)) {
      // 2
      protoAugment(data, arrayMethods)
      // 3
      this.observeArray(data)
    } else {
      // 4
      this.walk(data)
    }
  }
  walk(data) {
    Object.keys(data).forEach(key => {
      defineReactive(data, key, data[key])
    })
  }
  observeArray(data) {
    data.forEach(item => {
      observe(item)
    })
  }
}
  1. 為觀測的屬性添加 __ob__ 屬性,它的值等於 this,即當前 Observe 的實例
  2. 為數組添加重寫的數組方法,比如:push、unshift、splice 等方法,重寫目的是在調用這些方法時,進行更新渲染
  3. 觀測數組內的數據,observe 內部會調用 new Observe,形成遞歸觀測
  4. 觀測對象數據,defineReactive 為數據定義 get 和 set ,即數據劫持

Dep:我會為數據收集依賴

// 源碼位置:/src/core/observer/dep.js
let id = 0
class Dep{
  constructor() {
    this.id = ++id // dep 唯一標識
    this.subs = [] // 存儲 Watcher
  }
  // 1
  depend() {
    Dep.target.addDep(this)
  }
  // 2
  addSub(watcher) {
    this.subs.push(watcher)
  }
  // 3
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}

// 4
Dep.target = null

export function pushTarget(watcher) {
  Dep.target = watcher
} 

export function popTarget(){
  Dep.target = null
}

export default Dep
  1. 數據收集依賴的主要方法,Dep.target 是一個 watcher 實例
  2. 添加 watcher 到數組中,也就是添加依賴
  3. 屬性在變化時會調用 notify 方法,通知每一個依賴進行更新
  4. Dep.target 用來記錄 watcher 實例,是全局唯一的,主要作用是為了在收集依賴的過程中找到相應的 watcher

pushTarget 和 popTarget 這兩個方法顯而易見是用來設置 Dep.target的。Dep.target 也是一個關鍵點,這個概念可能初次查看源碼會有些難以理解,在後面的流程中,會詳細講解它的作用,需要注意這部分的內容。

Watcher:我會觸發視圖更新

// 源碼位置:/src/core/observer/watcher.js
let id = 0
export class Watcher {
  constructor(vm, exprOrFn, cb, options){
    this.id = ++id  // watcher 唯一標識
    this.vm = vm
    this.cb = cb
    this.options = options
    // 1
    this.getter = exprOrFn
    this.deps = []
    this.depIds = new Set()

    this.get()
  }
  run() {
    this.get()
  }
  get() {
    pushTarget(this)
    this.getter()
    popTarget(this)
  }
  // 2
  addDep(dep) {
    // 防止重複添加 dep
    if (!this.depIds.has(dep.id)) {
      this.depIds.add(dep.id)
      this.deps.push(dep)
      dep.addSub(this)
    }
  }
  // 3
  update() {
    queueWatcher(this)
  }
}
  1. this.getter 存儲的是更新視圖的函數
  2. watcher 存儲 dep,同時 dep 也存儲 watcher,進行雙向記錄
  3. 觸發更新,queueWatcher 是為了進行異步更新,異步更新會調用 run 方法進行更新頁面

響應式原理流程

對於以上這些成員具有的功能,我們都有大概的了解。下面結合它們,來看看這些功能是如何在響應式原理流程中工作的。

數據觀測

數據在初始化時會通過 observe 方法來調用 Observe

// 源碼位置:/src/core/observer/index.js
export function observe(data) {
  // 1
  if (!isObject(data)) {
    return
  }
  let ob;
  // 2
  if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observe) {
    ob = data.__ob__
  } else {
    // 3
    ob = new Observe(data)
  }
  return ob
}

在初始化時,observe 拿到的 data 就是我們在 data 函數內返回的對象。

  1. observe 函數只對 object 類型數據進行觀測
  2. 觀測過的數據都會被添加上 __ob__ 屬性,通過判斷該屬性是否存在,防止重複觀測
  3. 創建 Observe 實例,開始處理觀測邏輯

對象觀測

進入 Observe 內部,由於初始化的數據是一個對象,所以會調用 walk 方法:

walk(data) {
  Object.keys(data).forEach(key => {
    defineReactive(data, key, data[key])
  })
}

defineReactive 方法內部使用 Object.defineProperty 對數據進行劫持,是實現響應式原理最核心的地方。

function defineReactive(obj, key, value) {
  // 1
  let childOb = observe(value)
  // 2
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        // 3
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return value
    },
    set(newVal) {
      if (newVal === value) {
        return
      }
      value = newVal
      // 4
      childOb = observe(newVal)
      // 5
      dep.notify()
      return value
    }
  })
}
  1. 由於值可能是對象類型,這裏需要調用 observe 進行遞歸觀測
  2. 這裏的 dep 就是上面講到的每一個屬性都會有一個 dep,它是作為一個閉包的存在,負責收集依賴和通知更新
  3. 在初始化時,Dep.target 是組件的渲染 watcher,這裏 dep.depend 收集的依賴就是這個 watcher,childOb.dep.depend 主要是為數組收集依賴
  4. 設置的新值可能是對象類型,需要對新值進行觀測
  5. 值發生改變,dep.notify 通知 watcher 更新,這是我們改變數據后能夠實時更新頁面的觸發點

通過 Object.defineProperty 對屬性定義后,屬性的獲取觸發 get 回調,屬性的設置觸發 set 回調,實現響應式更新。

通過上面的邏輯,也能得出為什麼 Vue3.0 要使用 Proxy 代替 Object.defineProperty 了。Object.defineProperty 只能對單個屬性進行定義,如果屬性是對象類型,還需要遞歸去觀測,會很消耗性能。而 Proxy 是代理整個對象,只要屬性發生變化就會觸發回調。

數組觀測

對於數組類型觀測,會調用 observeArray 方法:

observeArray(data) {
  data.forEach(item => {
    observe(item)
  })
}

與對象不同,它執行 observe 對數組內的對象類型進行觀測,並沒有對數組的每一項進行 Object.defineProperty 的定義,也就是說數組內的項是沒有 dep 的。

所以,我們通過數組索引對項進行修改時,是不會觸發更新的。但可以通過 this.$set 來修改觸發更新。那麼問題來了,為什麼 Vue 要這樣設計?

結合實際場景,數組中通常會存放多項數據,比如列表數據。這樣觀測起來會消耗性能。還有一點原因,一般修改數組元素很少會直接通過索引將整個元素替換掉。例如:

export default {
    data() {
        return {
            list: [
                {id: 1, name: 'Jack'},
                {id: 2, name: 'Mike'}
            ]
        }
    },
    cretaed() {
        // 如果想要修改 name 的值,一般是這樣使用
        this.list[0].name = 'JOJO'
        // 而不是以下這樣
        // this.list[0] = {id:1, name: 'JOJO'}
        // 當然你可以這樣更新
        // this.$set(this.list, '0', {id:1, name: 'JOJO'})
    }
}

數組方法重寫

當數組元素新增或刪除,視圖會隨之更新。這並不是理所當然的,而是 Vue 內部重寫了數組的方法,調用這些方法時,數組會更新檢測,觸發視圖更新。這些方法包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

回到 Observe 的類中,當觀測的數據類型為數組時,會調用 protoAugment 方法。

if (Array.isArray(data)) {
  protoAugment(data, arrayMethods)
  // 觀察數組
  this.observeArray(data)
} else {
  // 觀察對象
  this.walk(data)
}

這個方法里把數組原型替換為 arrayMethods ,當調用改變數組的方法時,優先使用重寫后的方法。

function protoAugment(data, arrayMethods) {
  data.__proto__ = arrayMethods
}

接下來看看 arrayMethods 是如何實現的:

// 源碼位置:/src/core/observer/array.js
// 1
let arrayProto = Array.prototype
// 2
export let arrayMethods = Object.create(arrayProto)

let methods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'reverse',
  'sort',
  'splice'
]

methods.forEach(method => {
  arrayMethods[method] = function(...args) {
    // 3
    let res = arrayProto[method].apply(this, args)
    let ob = this.__ob__
    let inserted = ''
    switch(method){
      case 'push':
      case 'unshift':
        inserted = args
        break;
      case 'splice':
        inserted = args.slice(2)
        break;
    }
    // 4
    inserted && ob.observeArray(inserted)
    // 5
    ob.dep.notify()
    return res
  }
})
  1. 將數組的原型保存起來,因為重寫的數組方法里,還是需要調用原生數組方法的
  2. arrayMethods 是一個對象,用於保存重寫的方法,這裏使用 Object.create(arrayProto) 創建對象是為了使用者在調用非重寫方法時,能夠繼承使用原生的方法
  3. 調用原生方法,存儲返回值,用於設置重寫函數的返回值
  4. inserted 存儲新增的值,若 inserted 存在,對新值進行觀測
  5. ob.dep.notify 觸發視圖更新

依賴收集

依賴收集是視圖更新的前提,也是響應式原理中至關重要的環節。

偽代碼流程

為了方便理解,這裏寫一段偽代碼,大概了解依賴收集的流程:

// data 數據
let data = {
    name: 'joe'
}

// 渲染watcher
let watcher = {
    run() {
        dep.tagret = watcher
        document.write(data.name)
    }
}

// dep
let dep = [] // 存儲依賴 
dep.tagret = null // 記錄 watcher

// 數據劫持
let oldValue = data.name
Object.defineProperty(data, 'name', {
   get(){
       // 收集依賴
       dep.push(dep.tagret)
       return oldValue
   },
   set(newVal){
       oldValue = newVal
       dep.forEach(watcher => {
           watcher.run()
       })
       
   }
})

初始化:

  1. 首先會對 name 屬性定義 get 和 set
  2. 然後初始化會執行一次 watcher.run 渲染頁面
  3. 這時候獲取 data.name,觸發 get 函數收集依賴。

更新:

修改 data.name,觸發 set 函數,調用 run 更新視圖。

真正流程

下面來看看真正的依賴收集流程是如何進行的。

function defineReactive(obj, key, value) {
  let childOb = observe(value)
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        dep.depend() // 收集依賴
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return value
    },
    set(newVal) {
      if (newVal === value) {
        return
      }
      value = newVal
      childOb = observe(newVal)
      dep.notify()
      return value
    }
  })
}

首先初始化數據,調用 defineReactive 函數對數據進行劫持。

export class Watcher {
  constructor(vm, exprOrFn, cb, options){
    this.getter = exprOrFn
    this.get()
  }
  get() {
    pushTarget(this)
    this.getter()
    popTarget(this)
  }
}

初始化將 watcher 掛載到 Dep.target,this.getter 開始渲染頁面。渲染頁面需要對數據取值,觸發 get 回調,dep.depend 收集依賴。

class Dep{
  constructor() {
    this.id = id++
    this.subs = []
  }
  depend() {
    Dep.target.addDep(this)
  }
}

Dep.target 為 watcher,調用 addDep 方法,並傳入 dep 實例。

export class Watcher {
  constructor(vm, exprOrFn, cb, options){
    this.deps = []
    this.depIds = new Set()
  }
  addDep(dep) {
    if (!this.depIds.has(dep.id)) {
      this.depIds.add(dep.id)
      this.deps.push(dep)
      dep.addSub(this)
    }
  }
}

addDep 中添加完 dep 后,調用 dep.addSub 並傳入當前 watcher 實例。

class Dep{
  constructor() {
    this.id = id++
    this.subs = []
  }
  addSub(watcher) {
    this.subs.push(watcher)
  }
}

將傳入的 watcher 收集起來,至此依賴收集流程完畢。

補充一點,通常頁面上會綁定很多屬性變量,渲染會對屬性取值,此時每個屬性收集的依賴都是同一個 watcher,即組件的渲染 watcher。

數組的依賴收集

methods.forEach(method => {
  arrayMethods[method] = function(...args) {
    let res = arrayProto[method].apply(this, args)
    let ob = this.__ob__
    let inserted = ''
    switch(method){
      case 'push':
      case 'unshift':
        inserted = args
        break;
      case 'splice':
        inserted = args.slice(2)
        break;
    }
    // 對新增的值觀測
    inserted && ob.observeArray(inserted)
    // 更新視圖
    ob.dep.notify()
    return res
  }
})

還記得重寫的方法里,會調用 ob.dep.notify 更新視圖,__ob__ 是我們在 Observe 為觀測數據定義的標識,值為 Observe 實例。那麼 ob.dep 的依賴是在哪裡收集的?

function defineReactive(obj, key, value) {
  // 1
  let childOb = observe(value)
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        dep.depend()
        // 2
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return value
    },
    set(newVal) {
      if (newVal === value) {
        return
      }
      value = newVal
      childOb = observe(newVal)
      dep.notify()
      return value
    }
  })
}
  1. observe 函數返回值為 Observe 實例
  2. childOb.dep.depend 執行,為 Observe 實例的 dep 添加依賴

所以在數組更新時,ob.dep 內已經收集到依賴了。

整體流程

下面捋一遍初始化流程和更新流程,如果你是初次看源碼,不知道從哪裡看起,也可以參照以下的順序。由於源碼實現比較多,下面展示的源碼會稍微刪減一些代碼

初始化流程

入口文件:

// 源碼位置:/src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

_init:

// 源碼位置:/src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // mergeOptions 對 mixin 選項和傳入的 options 選項進行合併
      // 這裏的 $options 可以理解為 new Vue 時傳入的對象
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    // 初始化數據
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    if (vm.$options.el) {
      // 初始化渲染頁面 掛載組件
      vm.$mount(vm.$options.el)
    }
  }
}

上面主要關注兩個函數,initState 初始化數據,vm.$mount(vm.$options.el) 初始化渲染頁面。

先進入 initState:

// 源碼位置:/src/core/instance/state.js 
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    // data 初始化
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

function initData (vm: Component) {
  let data = vm.$options.data
  // data 為函數時,執行 data 函數,取出返回值
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  // 這裏就開始走觀測數據的邏輯了
  observe(data, true /* asRootData */)
}

observe 內部流程在上面已經講過,這裏再簡單過一遍:

  1. new Observe 觀測數據
  2. defineReactive 對數據進行劫持

initState 邏輯執行完畢,回到開頭,接下來執行 vm.$mount(vm.$options.el) 渲染頁面:

$mount:

// 源碼位置:/src/platforms/web/runtime/index.js 
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

mountComponent:

// 源碼位置:/src/core/instance/lifecycle.js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    // 數據改變時  會調用此方法
    updateComponent = () => {
      // vm._render() 返回 vnode,這裏面會就對 data 數據進行取值
      // vm._update 將 vnode 轉為真實dom,渲染到頁面上
      vm._update(vm._render(), hydrating)
    }
  }
  
  // 執行 Watcher,這個就是上面所說的渲染wacther 
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

Watcher:

// 源碼位置:/src/core/observer/watcher.js 
let uid = 0

export default class Watcher {
  constructor(vm, exprOrFn, cb, options){
    this.id = ++id
    this.vm = vm
    this.cb = cb
    this.options = options
    // exprOrFn 就是上面傳入的 updateComponent
    this.getter = exprOrFn

    this.deps = []
    this.depIds = new Set()

    this.get()
  }
  get() {
    // 1. pushTarget 將當前 watcher 記錄到 Dep.target,Dep.target 是全局唯一的
    pushTarget(this)
    let value
    const vm = this.vm
    try {
    // 2. 調用 this.getter 相當於會執行 vm._render 函數,對實例上的屬性取值,
    //由此觸發 Object.defineProperty 的 get 方法,在 get 方法內進行依賴收集(dep.depend),這裏依賴收集就需要用到 Dep.target
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      // 3. popTarget 將 Dep.target 置空
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
}

至此初始化流程完畢,初始化流程的主要工作是數據劫持、渲染頁面和收集依賴。

更新流程

數據發生變化,觸發 set ,執行 dep.notify

// 源碼位置:/src/core/observer/dep.js 
let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      // 執行 watcher 的 update 方法
      subs[i].update()
    }
  }
}

wathcer.update:

// 源碼位置:/src/core/observer/watcher.js 
/**
 * Subscriber interface.
 * Will be called when a dependency changes.
 */
update () {
  /* istanbul ignore else */
  if (this.lazy) {  // 計算屬性更新
    this.dirty = true
  } else if (this.sync) {  // 同步更新
    this.run()
  } else {
    // 一般的數據都會進行異步更新
    queueWatcher(this)
  }
}

queueWatcher:

// 源碼位置:/src/core/observer/scheduler.js

// 用於存儲 watcher
const queue: Array<Watcher> = []
// 用於 watcher 去重
let has: { [key: number]: ?true } = {}
/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  let watcher, id

  // 對 watcher 排序
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    // run方法更新視圖
    watcher.run()
  }
}
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    // watcher 加入數組
    queue.push(watcher)
    // 異步更新
    nextTick(flushSchedulerQueue)
  }
}

nextTick:

// 源碼位置:/src/core/util/next-tick.js

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  // 遍歷回調函數執行
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
  }
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 將回調函數加入數組
  callbacks.push(() => {
    if (cb) {
      cb.call(ctx)
    }
  })
  if (!pending) {
    pending = true
    // 遍歷回調函數執行
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

這一步是為了使用微任務將回調函數異步執行,也就是上面的p.then。最終,會調用 watcher.run 更新頁面。

至此更新流程完畢。

寫在最後

如果沒有接觸過源碼的同學,我相信看完可能還是會有點懵的,這很正常。建議對照源碼再自己多看幾遍就能知道流程了。對於有基礎的同學就當做是複習了。

想要變強,學會看源碼是必經之路。在這過程中,不僅能學習框架的設計思想,還能培養自己的邏輯思維。萬事開頭難,遲早都要邁出這一步,不如就從今天開始。

簡化后的代碼我已放在 github,有需要的可以看看。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※FB行銷專家,教你從零開始的技巧

龍捲風侵襲美國阿肯色州 當局為加速救災實施宵禁

摘錄自2020年3月29日民視報導

美國中部阿肯色州瓊斯伯勒市,昨(28)日遭巨大龍捲風襲擊,多處建築物受損、飛機被吹翻,已知6人受傷。當局事後派出人員搜救又實施宵禁,確保民眾晚上7點之後不會外出,加速救災。

事情發生在美國中部時間28日下午5點左右,突如其來的巨大龍捲風,席捲阿肯色州的瓊斯伯勒市,多棟建築物屋頂瞬間被吹毀、路牌吹歪,甚至傳出當地機場有一架飛機被吹翻,損失慘重。所幸因為武漢肺炎疫情,多數店家早就拉下鐵門,居民也幾乎待在室內,因此只傳出數人受到輕傷。

當地政府第一時間發出警報,要求民眾待在室內避難,事後也派出國民兵進行三次搜救。最後決定實施宵禁,禁止民眾晚上7點後出門,方便有關單位加速清理。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

最天然的野溪整治 英國實測河狸治水法

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!