目前公司團隊小程序框架使用的是 tinaJs,這篇文章將講解這個框架的源碼。閱讀文章時能夠對照着這個小工程閱讀源碼,這個小工程主要是對 tina 加了更多的註釋及示例。git
tinaJs 是一款輕巧的漸進式微信小程序框架,不只能充分利用原生小程序的能力,還易於調試。
這個框架主要是對 Component、Page 兩個全局方法進行了封裝,本文主要介紹 [tinaJS 1.0.0]() 的 Paeg.define
內部作了些什麼。Component.define
與 Paeg.define
類似,理解 Paeg.define
以後天然也就理解 Component.define
。爲何是講解 1.0.0 ?由於第一個版本的代碼相對於最新版本主幹內容更更清晰更容易上手。github
爲了不混淆 tina 和原生的一些概念,這裏先說明一下一些詞的含義小程序
tina/class/page
這個類開局先來預覽一下 Page.define
的流程微信小程序
// tina/class/page.js class Page extends Basic { static mixins = [] static define(tinaPageOptions = {}) { // 選項合併 tinaPageOptions = this.mix(/*....*/) // 構建原生 options 對象 let wxPageOptions = {/*.....*/} // 在原生 onLoad 時作攔截,關聯 wx-Page 對象和 tina-Page 對象 wxPageOptions = prependHooks(wxPageOptions, { onLoad() { // this 是小程序 wx-Page 實例 // instance 是這個 tina-Page 實例 let instance = new Page({ tinaPageOptions }) // 創建關聯 this.__tina_instance__ = instance instance.$source = this } }) // 構造 wx-Page 對象 new globals.Page({ // ... ...wxPageOptions, }) } constructor({ tinaPageOptions = {} }) { super() //....... } get data() { return this.$source.data } }
下面針對每一個小流程作講解數組
tina 的 mixin 是靠 js 對對象作合併實現的,並無使用原生的 behaviors
微信
tinaPageOptions = this.mix(PAGE_INITIAL_OPTIONS, [...BUILTIN_MIXINS, ...this.mixins, ...(tinaPageOptions.mixins || []), tinaPageOptions])
tinaJs 1.0.0 只支持一種合併策略,跟 Vue 的默認合併策略同樣app
合併後能夠獲得這樣一個對象框架
{ // 頁面 beforeLoad: [$log.beforeLoad, options.beforeLoad], onLoad: [$initial.onLoad, options.onLoad], onHide: [], onPageScroll: [], onPullDownRefresh: [], onReachBottom: [], onReady: [], onShareAppMessage: [], onShow: [], onUnload: [], // 組件 attached: Function, compute: Function, created: $log.created, // 頁面、組件共用 data: tinaPageOptions.data, methods: tinaPageOptions.methods, mixins: [], }
合併後是建立 wx-Page 對象,至於建立 wx-Page 對象過程作了什麼,爲了方便理解整個流程,在這裏暫時先跳過講解,放在後面 改變執行上下文
小節再講解。ide
爲了綁定 wx-Page 對象,tina 在 wx-onLoad 中追加了一些操做。
prependHooks 是做用是在 wxPageOptions[hookName]
執行時追加 handlers[hookName]
操做,並保證 wxPageOptions[hookName]
、handlers[hookName]
的執行上下文是原生運行時的 this
this
// tina/class/page wxPageOptions = prependHooks(wxPageOptions, { onLoad() { // this 是 wxPageOptions // instance 是 tina-Page 實例 let instance = new Page({ tinaPageOptions }) // 創建關聯 this.__tina_instance__ = instance instance.$source = this } }) // tina/utils/helpers.js /** * 在 wx-page 生命週期勾子前追加勾子 * @param {Object} context * @param {Array} handlers * @return {Object} */ export const prependHooks = (context, handlers) => addHooks(context, handlers, true) function addHooks (context, handlers, isPrepend = false) { let result = {} for (let name in handlers) { // 改寫 hook 方法 result[name] = function handler (...args) { // 小程序運行時, this 是 wxPageOptions if (isPrepend) { // 執行 tina 追加的 onLoad handlers[name].apply(this, args) } if (typeof context[name] === 'function') { // 執行真正的 onLoad context[name].apply(this, args) } // ... } } return { ...context, ...result, } }
接下來再來看看 new Page
作了什麼
constructor({ tinaPageOptions = {} }) { super() // 建立 wx-page options let members = { // compute 是 tina 添加的方法 compute: tinaPageOptions.compute || function () { return {} }, ...tinaPageOptions.methods, // 用於代理全部生命週期(包括 tina 追加的 beforeLoad) ...mapObject(pick(tinaPageOptions, PAGE_HOOKS), (handlers) => { return function (...args) { // 由於作過 mixin 處理,一個生命週期會有多個處理方法 return handlers.reduce((memory, handler) => { const result = handler.apply(this, args.concat(memory)) return result }, void 0) } }), // 以 beforeLoad、onLoad 爲例,以上 mapObject 後追加的生命週期處理方法實際執行時是這樣的 // beforeLoad(...args) { // return [onLoad一、onLoad二、.....].reduce((memory, handler) => { // return handler.apply(this, args.concat(memory)) // }, void 0) //}, // onLoad(...args) { // return [onShow一、onShow二、.....].reduce((memory, handler) => { // return handler.apply(this, args.concat(memory)) // }, void 0) // }, } // tina-page 代理全部屬性 for (let name in members) { this[name] = members[name] } return this }
首先是將 tinaPageOptions
變成跟 wxPageOptions
同樣的結構,由於 wxPageOptions 的 methods
和 hooks
都是在 options 的第一層的,因此須要將將 methods 和 hooks 鋪平。
又由於 hooks 通過 mixins 處理已經變成了數組,因此須要遍歷執行,每一個 hooks 的第二個參數都是以前累積的結果。而後經過簡單的屬性拷貝將全部方法拷貝到 tina-Page 實例。
上面提到構建一個屬性跟 wx-Page 如出一轍的 tina-Page 對象,那麼爲何要這樣呢?一個框架的做用是什麼?我認爲是在原生能力之上創建一個可以提升開發效率的抽象層。如今 tina 就是這個抽象層,
舉個例子來講就是咱們但願 methods.foo
被原生調用時,tina 能在 methods.foo
裏作更多的事情。因此 tina 須要與原生關聯使得全部原本由原生處理的東西轉交到 tina 這個抽象層處理。
那 tina 是如何處理的呢。咱們先來看看建立 wxPageOptions
的源碼
// tina/class/page.js let wxPageOptions = { ...wxOptionsGenerator.methods(tinaPageOptions.methods), ...wxOptionsGenerator.lifecycles( inUseOptionsHooks, (name) => ADDON_BEFORE_HOOKS[name] ), } // tina/class/page.js /** * wxPageOptions.methods 中的改變執行上下文爲 tina.Page 對象 * @param {Object} object * @return {Object} */ export function methods(object) { return mapObject(object || {}, (method, name) => function handler(...args) { let context = this.__tina_instance__ return context[name].apply(context, args) }) }
答案就在 wxOptionsGenerator.methods
。上面說過在 onLoad
的時候會綁定 __tina_instance__
到 wx-Page,同時 wx-Page 與 tina-Page 的屬性都是如出一轍的,因此調用會被轉發到 tina 對應的方法。這就至關於 tina 在 wx 之上作了一個抽象層。全部的被動調用都會被 tina 處理。並且由於上下文是 __tina_instance__
的緣故,
全部主動調用都先通過 tina 再到 wx。結合下面兩個小節會有更好的理解。
上面建立 wxPageOptions
時有這麼一句 wxOptionsGenerator.lifecycles
代碼,這是 tina 用於在 onLoad
以前加多一個 beforeLoad
生命週期勾子,這個功能是怎麼作的呢,咱們來看看源碼
// tina/utils/wx-options-generator /** * options.methods 中的改變執行上下文爲 tina.Page 對象 * @param {Array} hooks * @param {Function} getBeforeHookName * @return {Object} */ export function lifecycles(hooks, getBeforeHookName) { return fromPairs(hooks.map((origin) => { let before = getBeforeHookName(origin) // 例如 'beforeLoad' return [ origin, // 例如 'load' function wxHook() { let context = this.__tina_instance__ // 調用 tina-page 的方法,例如 beforeLoad if (before && context[before]) { context[before].apply(context, arguments) } if (context[origin]) { return context[origin].apply(context, arguments) } } ] })) }
其實就是改寫 onLoad
,在調用 tina-Page.onLoad
前先調用 tina-Page.beforeLoad
。可能有的人會有疑問,爲何要加個 beforeLoad
勾子,這跟直接 onLoad
裏不都同樣的麼。
舉個例子,不少時候咱們在 onLoad
拿到 query
以後是否是都要手動去 decode
,利用全局 mixins
和 beforeLoad
,能夠一次性把這個事情作了。
Page.mixins = [{ beforeLoad(query) { // 對 query 進行 decode // 對 this.$options 進行 decode } }]
還有一點須要注意的是,tina 源碼中了屢次對 onLoad
攔截,執行順序以下
prependHooks.addHooks.handler -> wx-Page.onLoad,關聯 wx-Page、tinaPage -> 回到 prependHooks.addHooks.handler -> lifecycles.wxHook -> tina-Page.beforeLoad -> tina-Page.onLoad
以下圖所示
由於運行時的上下文都被 tina 改成 tina-Page,因此開發者調用的 this.setData
, 實際上的 tina-Page 的 setData
方法,又由於 tina-Page 繼承自 Basic,也就調用 Basic 的 setData 方法。下面看看 setData
的源碼
setData(newer, callback = () => {}) { let next = { ...this.data, ...newer } if (typeof this.compute === 'function') { next = { ...next, ...this.compute(next), } } next = diff(next, this.data) this.constructor.log('setData', next) if (isEmpty(next)) { return callback() } this.$source.setData(next, callback) }
從源碼能夠看到就是每次 setData
的時候調用一下 compute
更新數據,這是 compute
的原理,很容易理解吧。
前面 mix
小節提到,tina 會合並一些內置選項,能夠看到在 onLoad
時會調用this.setData
,爲了初始化 compute 屬性。
// mixins/index.js function initial() { // 爲了初始化 compute 屬性 this.setData() this.$log('Initial Mixin', 'Ready') } export const $initial = { // ... onLoad: initial,// 頁面加載完成勾子 }
到此基本上把 Page.define
主幹流程講完,若有疑問歡迎留言