1. 前言
大家好,我是若川,歡迎關注我的公眾號:若川視野。從 2021 年 8 月起,我持續組織了好幾年的每週大家一起學習 200 行左右的源碼共讀活動,感興趣的可以點此掃碼加我微信 ruochuan02 參與。另外,想學源碼,極力推薦關注我寫的專欄《學習源碼整體架構系列》,目前是掘金關注人數(6k+人)第一的專欄,寫有幾十篇源碼文章。
截至目前(2024-08-18),taro 4.0 正式版已經發布,目前最新是 4.0.4,官方4.0正式版本的介紹文章暫未發佈。官方之前發過Taro 4.0 Beta 發佈:支持開發鴻蒙應用、小程序編譯模式、Vite 編譯等。
計劃寫一個 Taro 源碼揭秘系列,博客地址:https://ruochuan12.github.io/taro 可以加入書籤,持續關注若川。
- [x] 1. 揭開整個架構的入口 CLI => taro init 初始化項目的秘密
- [x] 2. 揭開整個架構的插件系統的秘密
- [x] 3. 每次創建新的 taro 項目(taro init)的背後原理是什麼
- [x] 4. 每次 npm run dev:weapp 開發小程序,build 編譯打包是如何實現的?
- [x] 5. 高手都在用的發佈訂閲機制 Events 在 Taro 中是如何實現的?
- [x] 6. 為什麼通過 Taro.xxx 能調用各個小程序平台的 API,如何設計實現的?
- [x] 7. Taro.request 和請求響應攔截器是如何實現的
- [x] 8. Taro 是如何使用 webpack 打包構建小程序的?
- [x] 9. Taro 是如何生成 webpack 配置進行構建小程序的?
- [x] 10. Taro 到底是怎樣轉換成小程序文件的?
- [ ] 等等
前面 4 篇文章都是講述編譯相關的,CLI、插件機制、初始化項目、編譯構建流程。第 5 篇我們來講些相對簡單的,Taro 是如何實現發佈訂閲機制 Events 的。
學完本文,你將學到:
1. 瞭解發佈訂閲機制
2. 瞭解 taro 一些 npm 包的作用,尋找到 Events 源碼
3. Taro 源碼中發佈訂閲機制 Events 是如何實現的
等等
2. Taro 消息機制
Taro 消息機制文檔上,Taro 提供了消息機制 Events,用來實現組件間通信。我們來學習下如何實現的。
Taro 提供了 Taro.Events 來實現消息機制,使用時需要實例化它,如下
import Taro, { Events } from '@tarojs/taro'
const events = new Events()
// 監聽一個事件,接受參數
events.on('eventName', (arg) => {
// doSth
})
// 監聽同個事件,同時綁定多個 handler
events.on('eventName', handler1)
events.on('eventName', handler2)
events.on('eventName', handler3)
// 觸發一個事件,傳參
events.trigger('eventName', arg)
// 觸發事件,傳入多個參數
events.trigger('eventName', arg1, arg2, ...)
// 取消監聽一個事件
events.off('eventName')
// 取消監聽一個事件某個 handler
events.off('eventName', handler1)
// 取消監聽所有事件
events.off()
同時 Taro 還提供了一個全局消息中心 Taro.eventCenter 以供使用,它是 Taro.Events 的實例
import Taro from '@tarojs/taro'
Taro.eventCenter.on
Taro.eventCenter.trigger
Taro.eventCenter.off
Vue2 中也有類似的事件 events api $on、$off、$once、$emit,不過 Vue3 移除了。
vue2 events
也有一些 npm 包,如:mitt、tiny-emitter
源碼共讀也有一期第8期 | mitt、tiny-emitter 發佈訂閲
3. 根據文檔使用實現 Events
文檔中,主要有如下幾個需求點:
- 監聽同個事件,同時綁定多個 handler
- 觸發事件,傳入多個參數
- 取消監聽一個事件某個 handler
- 取消監聽所有事件
我們可以先自行實現一個符合要求的 Events 類,然後再去 Taro 源碼中尋找實現,最後可以對比各自的實現優缺點。
3.1 初步實現 Events
class Events {
constructor(){
this.callbacks = [];
}
on(eventName, callback){
this.callbacks.push({
eventName,
callback,
});
}
off(){}
trigger(){}
}
我們用 callbacks 數組來存儲事件,push 方法用來添加事件,支持多個同名的 eventName。
3.2 off 方法實現
off(eventName, callback){
this.callbacks = this.callbacks.filter((item) => {
if(typeof eventName === 'string'){
if(typeof callback === 'function'){
return !(item.eventName === eventName && item.callback === callback);
}
return item.eventName !== eventName;
}
return false;
});
}
off 方法用來取消監聽事件,如果傳入 eventName 參數,則取消監聽該事件,如果還傳入了特定的 handler,則只取消監聽這個 handler。否則取消所有事件。
3.3 trigger 方法實現
trigger(eventName, ...args){
this.callbacks.forEach((item) => {
if(item.eventName === eventName){
item.callback(...args);
}
});
}
trigger 傳入 eventName 和參數,遍歷所有事件,如果 eventName 匹配,則執行 handler。
Taro events 自行實現所有代碼 demo,可打開調試運行
4. 在茫茫源碼中尋找 class Events 實現
文檔示例:
import Taro, { Events } from '@tarojs/taro'
const events = new Events()
Taro.eventCenter.on
Taro.eventCenter.trigger
Taro.eventCenter.off
@tarojs/taro 對應的源碼路徑是 taro/packages/taro
4.1 @tarojs/taro 暴露者開發者的 Taro 核心 API
暴露給應用開發者的 Taro 核心 API。包含以下小程序端入口文件 index.js 等。
const { hooks } = require('@tarojs/runtime')
const taro = require('@tarojs/api').default
if (hooks.isExist('initNativeApi')) {
hooks.call('initNativeApi', taro)
}
module.exports = taro
module.exports.default = module.exports
@tarojs/api 對應的源碼路徑是 taro/packages/taro-api
4.2 @tarojs/api 所有端的公有 API
暴露給 @tarojs/taro 的所有端的公有 API。@tarojs/api 會跨 node/瀏覽器/小程序/React Native 使用,不得使用/包含平台特有特性。
入口文件:packages/taro-api/src/index.ts
// packages/taro-api/src/index.ts
/* eslint-disable camelcase */
import { Current, eventCenter, Events, getCurrentInstance, nextTick, options } from '@tarojs/runtime'
// 省略若干代碼
const Taro: Record<string, unknown> = {
// 省略若干代碼
Current, getCurrentInstance, options, nextTick, eventCenter, Events,
}
// 省略若干代碼
export default Taro
這個文件代碼不多,省略了一部分。默認導出Taro,其中 eventCenter,Events是從 @tarojs/runtime 引入的。
@tarojs/runtime 對應的源碼路徑是 taro/packages/taro-runtime
4.3 @tarojs/runtime Taro 運行時
Taro 運行時。在小程序端連接框架(DSL)渲染機制到小程序渲染機制,連接小程序路由和生命週期到框架對應的生命週期。在 H5/RN 端連接小程序生命週期規範到框架生命週期。
Events Taro 消息機制。
// packages/taro-runtime/src/index.ts
export * from './emitter/emitter'
// packages/taro-runtime/src/emitter/emitter.ts
import { Events, hooks } from '@tarojs/shared'
const eventCenter = hooks.call('getEventCenter', Events)!
export type EventsType = typeof Events
export { eventCenter, Events }
@tarojs/shared 對應的源碼路徑是 taro/packages/shared
4.4 @tarojs/shared 內部使用的 utils
Taro 內部使用的 utils。包含了常用的類型判斷、錯誤斷言、組件類型/聲明/參數等。
@tarojs/shared會跨 node/瀏覽器/小程序/React Native 使用,不得使用平台特有特性。引入此包的必須採用 ES6 引用單個模塊語法,且打包配置 external 不得包括此包。
// packages/shared/src/index.ts
// Events 導出的位置
export * from './event-emitter'
// hooks 導出的位置
export * from './runtime-hooks'
5. class Events 的具體實現
終於在 packages/shared/src/event-emitter.ts 找到了 class Events 的實現代碼。
// packages/shared/src/event-emitter.ts
type EventName = string | symbol
type EventCallbacks = Record<EventName, Record<'next' | 'tail', unknown>>
export class Events {
protected callbacks?: EventCallbacks
static eventSplitter = ',' // Note: Harmony ACE API 8 開發板不支持使用正則 split 字符串 /\s+/
constructor (opts?) {
this.callbacks = opts?.callbacks ?? {}
}
// 省略這幾個方法具體實現,拆分到下方講述
on(){}
once(){}
off(){}
trigger(){}
}
eventSplitter 事件分割符 ,。
callbacks 對象存儲事件名和回調函數。
5.1 on 事件監聽
on (eventName: EventName, callback: (...args: any[]) => void, context?: any): this {
let event: EventName | undefined, tail, _eventName: EventName[]
// 如果沒傳 callback 函數,則直接返回 this
if (!callback) {
return this
}
// 支持 symbol 事件名寫法
// 也支持 事件名1,事件名2,事件名3 的寫法
if (typeof eventName === 'symbol') {
_eventName = [eventName]
} else {
// 事件名1,事件名2,事件名3 分割成數組
_eventName = eventName.split(Events.eventSplitter)
}
this.callbacks ||= {}
const calls = this.callbacks
// 遍歷事件名數組
while ((event = _eventName.shift())) {
const list = calls[event]
const node: any = list ? list.tail : {}
node.next = tail = {}
node.context = context
node.callback = callback
calls[event] = {
tail,
next: list ? list.next : node
}
}
// return this 支持鏈式調用
return this
}
這裏可能有點抽象,我們舉個例子調試下:
我們直接找到打包後的代碼,路徑:taro/packages/shared/dist/event-emitter.js,註釋// export { Events };,追加如下代碼:
function fn1(){}
function fn2(){}
function fn3(){}
function fn4(){}
const events = new Events()
.on('eventName1', fn1)
.on('eventName1', fn2)
.on('eventName1', fn3)
.on('eventName2', fn4);
console.log(events.callbacks);
打開終端,新建 JavaScript Debug Terminal 調試,運行:
node packages/shared/dist/event-emitter.js
調試截圖如下:
複製監視的 events.callbacks 的值,它的對象存儲如下結構:
{
eventName1: {
tail: {
},
next: {
next: {
next: {
next: {
},
context: undefined,
callback: function fn3(){},
},
context: undefined,
callback: function fn2(){},
},
context: undefined,
callback: function fn1(){},
},
},
eventName2: {
tail: {
},
next: {
next: {
},
context: undefined,
callback: function fn4(){},
},
},
}
也就是鏈表形式。同名的事件名,會追加到鏈表的 next 節點。所以同名的事件名,可以觸發多個 callback 函數。
5.2 once 事件監聽只執行一次
once (events: EventName, callback: (...r: any[]) => void, context?: any): this {
const wrapper = (...args: any[]) => {
callback.apply(this, args)
this.off(events, wrapper, context)
}
// 執行一次後,調用 off 方法移除事件
this.on(events, wrapper, context)
return this
}
執行一遍後,調用 off 方法移除事件。
5.3 off 事件移除
off (events?: EventName, callback?: (...args: any[]) => void, context?: any) {
let event: EventName | undefined, calls: EventCallbacks | undefined, _events: EventName[]
if (!(calls = this.callbacks)) {
return this
}
if (!(events || callback || context)) {
delete this.callbacks
return this
}
// 如果是 symbol 事件名,組成數組
if (typeof events === 'symbol') {
_events = [events]
} else {
// 事件名1,事件名2,事件名3 分割成數組
// 沒有傳事件名,則移除所有事件
_events = events ? events.split(Events.eventSplitter) : Object.keys(calls)
}
// 遍歷事件名數組
while ((event = _events.shift())) {
let node: any = calls[event]
// 刪除事件對象
delete calls[event]
if (!node || !(callback || context)) {
// 刪除,進行下一次循環
continue
}
const tail = node.tail
while ((node = node.next) !== tail) {
const cb = node.callback
const ctx = node.context
if ((callback && cb !== callback) || (context && ctx !== context)) {
this.on(event, cb, ctx)
}
}
}
return this
}
5.4 trigger 事件觸發
trigger (events: EventName, ...args: any[]) {
let event: EventName | undefined, node, calls: EventCallbacks | undefined, _events: EventName[]
// callbacks 對象不存在,則直接返回 this
if (!(calls = this.callbacks)) {
return this
}
// 如果是 symbol 事件名,組成數組 [events]
if (typeof events === 'symbol') {
_events = [events]
} else {
// 事件名1,事件名2,事件名3 分割成數組
_events = events.split(Events.eventSplitter)
}
// 遍歷事件名數組
// 遍歷鏈表,依次執行回調函數
while ((event = _events.shift())) {
if ((node = calls[event])) {
const tail = node.tail
while ((node = node.next) !== tail) {
node.callback.apply(node.context || this, args)
}
}
}
return this
}
遍歷鏈表,依次執行回調函數。
tail 結尾作為判斷,到末尾了,終止遍歷。
接着我們來學習 eventCenter 全局消息中心的實現。
6. eventCenter 全局消息中心
// packages/taro-runtime/src/emitter/emitter.ts
import { Events, hooks } from '@tarojs/shared'
const eventCenter = hooks.call('getEventCenter', Events)!
7. hooks
根據上文的信息,我們可以找到 hooks 對象的代碼位置是 packages/shared/src/runtime-hooks.ts。
// packages/shared/src/runtime-hooks.ts
import { Events } from './event-emitter'
import { isFunction } from './is'
import type { Shortcuts } from './template'
// Note: @tarojs/runtime 不依賴 @tarojs/taro, 所以不能改為從 @tarojs/taro 引入 (可能導致循環依賴)
type TFunc = (...args: any[]) => any
// hook 類型
export enum HOOK_TYPE {
SINGLE,
MULTI,
WATERFALL
}
// hook 對象
interface Hook {
type: HOOK_TYPE
initial?: TFunc | null
}
// Node 對象
interface Node {
next: Node
context?: any
callback?: TFunc
}
// TaroHook 函數
export function TaroHook (type: HOOK_TYPE, initial?: TFunc): Hook {
return {
type,
initial: initial || null
}
}
這段代碼聲明瞭一些 TS 接口和類型等,TaroHook 函數返回一個 Hook 對象。
// packages/shared/src/runtime-hooks.ts
type ITaroHooks = {
/** 小程序端 App、Page 構造對象的生命週期方法名稱 */
getMiniLifecycle: (defaultConfig: MiniLifecycle) => MiniLifecycle
// 省略若干代碼
/** 解決支付寶小程序分包時全局作用域不一致的問題 */
getEventCenter: (EventsClass: typeof Events) => Events
// 省略若干代碼
initNativeApi: (taro: Record<string, any>) => void
}
export const hooks = new TaroHooks<ITaroHooks>({
getMiniLifecycle: TaroHook(HOOK_TYPE.SINGLE, defaultConfig => defaultConfig),
// 省略若干代碼
getEventCenter: TaroHook(HOOK_TYPE.SINGLE, Events => new Events()),
// 省略若干代碼
initNativeApi: TaroHook(HOOK_TYPE.MULTI),
})
hooks 對象是 TaroHooks 的實例對象。TaroHooks 繼承自 Events。
我們來看 TaroHooks 的具體實現
8. class TaroHooks 的具體實現
// packages/shared/src/runtime-hooks.ts
export class TaroHooks<T extends Record<string, TFunc> = any> extends Events {
hooks: Record<keyof T, Hook>
constructor (hooks: Record<keyof T, Hook>, opts?) {
super(opts)
// 初始化 hooks 對象
/**
*
{
getMiniLifecycle,
getEventCenter: {
type: 0,
initial: (Events) => new Events(),
},
initNativeApi,
省略了一些其他hooks
}
*
*/
this.hooks = hooks
// 遍歷 hooks 對象,
// 如果 initial 是函數,調用 this.on 方法,監聽事件
// getEventCenter 的 type 是 SINGLE, initial 是函數 (Events) => new Events()
for (const hookName in hooks) {
const { initial } = hooks[hookName]
if (isFunction(initial)) {
this.on(hookName, initial)
}
}
}
private tapOneOrMany<K extends Extract<keyof T, string>> (hookName: K, callback: T[K] | T[K][]) {
// 列表,變量
const list = isFunction(callback) ? [callback] : callback
list.forEach(cb => this.on(hookName, cb))
}
// 省略tap、call方法,拆開到下方講述
isExist (hookName: string) {
return Boolean(this.callbacks?.[hookName])
}
}
8.1 tap 方法 - 監聽事件
tap<K extends Extract<keyof T, string>> (hookName: K, callback: T[K] | T[K][]) {
const hooks = this.hooks
const { type, initial } = hooks[hookName]
if (type === HOOK_TYPE.SINGLE) {
// 單個類型的hook,則取消監聽事件,重新監聽事件
this.off(hookName)
this.on(hookName, isFunction(callback) ? callback : callback[callback.length - 1])
} else {
// 不是,則取消監聽指定回調函數的事件,重新監聽一個或多個事件
initial && this.off(hookName, initial)
this.tapOneOrMany(hookName, callback)
}
}
tap 方法是監聽事件,hook類型是SINGLE類型時,直接取消,重新監聽。不是SINGLE類型時,則取消監聽指定回調函數的事件,重新監聽一個或多個事件。
8.2 call 方法 - 觸發事件
call<K extends Extract<keyof T, string>> (hookName: K, ...rest: Parameters<T[K]>): ReturnType<T[K]> | undefined {
// 獲取 hooks 對象
// call('getEventCenter', Events);
const hook = this.hooks[hookName]
if (!hook) return
const { type } = hook
// Events 對象 中的事件名稱、回調函數等對象
const calls = this.callbacks
if (!calls) return
const list = calls[hookName] as { tail: Node, next: Node }
if (list) {
const tail = list.tail
let node: Node = list.next
let args = rest
let res
// 遍歷鏈表,依次執行回調函數
// 判斷條件,節點不等於列表的末尾
while (node !== tail) {
res = node.callback?.apply(node.context || this, args)
// 如果是 waterfall,則 args 是 [res]
if (type === HOOK_TYPE.WATERFALL) {
const params: any = [res]
args = params
}
node = node.next
}
return res
}
}
因為 TaroHooks 是繼承自 Events,所以 call 實現和 Events 的 trigger 觸發事件類似。
都是遍歷鏈表,依次執行回調函數。還有一個判斷,就是 type 是 HOOK_TYPE.WATERFALL 的時候,將返回值作為參數傳給下一個回調。
換句話説
// 調用 call 方法,觸發事件
import { Events, hooks } from '@tarojs/shared'
const eventCenter = hooks.call('getEventCenter', Events)!
/**
*
getEventCenter: {
type: 0,
initial: (Events) => new Events(),
}
*
*/
簡化之後,其實就是:
const events = new Events();
events.on('getEventCenter', (Events) => new Events())
events.trigger('getEventCenter', Events));
const eventCenter = new Events();
單從 getEventCenter 函數來看,好像有些多此一舉,為啥要這樣寫呢?可能是為了修復多個平台的bug。暫時不知道好處是啥,知道的讀者朋友也可以反饋告知。
9. 總結
我們通過文檔Taro 消息機制,瞭解到 Taro 提供了Events 和 Taro.eventCenter 對象,用於發佈訂閲。我們根據文檔也實現了。
我們在茫茫源碼中,尋找 class Events 的實現,依次在 @tarojs/taro => @tarojs/api => @tarojs/runtime => @tarojs/shared 層層查找,我們終於在 packages/shared/src/event-emitter.ts 找到了 class Events 的實現代碼。
class Events 用鏈表存儲事件,實現了 on、once、off、trigger 等方法,用於發佈訂閲。
Taro.eventCenter 對象其實也是 class Events 的實例,只不過繞了一圈,用 TaroHooks 實例 hooks.call('getEventCenter', Events) 來獲取。
如果看完有收穫,歡迎點贊、評論、分享、收藏支持。你的支持和肯定,是我寫作的動力。也歡迎提建議和交流討論。
作者:常以若川為名混跡於江湖。所知甚少,唯善學。若川的博客,github blog,可以點個 star 鼓勵下持續創作。
最後可以持續關注我@若川,歡迎關注我的公眾號:若川視野。從 2021 年 8 月起,我持續組織了好幾年的每週大家一起學習 200 行左右的源碼共讀活動,感興趣的可以點此掃碼加我微信 ruochuan02 參與。另外,想學源碼,極力推薦關注我寫的專欄《學習源碼整體架構系列》,目前是掘金關注人數(6k+人)第一的專欄,寫有幾十篇源碼文章。