tinaJs 源碼分析

封面

目前公司團隊小程序框架使用的是 tinaJs,這篇文章將講解這個框架的源碼。閱讀文章時能夠對照着這個小工程閱讀源碼,這個小工程主要是對 tina 加了更多的註釋及示例。git

是什麼

tinaJs 是一款輕巧的漸進式微信小程序框架,不只能充分利用原生小程序的能力,還易於調試。
這個框架主要是對 Component、Page 兩個全局方法進行了封裝,本文主要介紹 [tinaJS 1.0.0]() 的 Paeg.define 內部作了些什麼。Component.definePaeg.define類似,理解 Paeg.define 以後天然也就理解 Component.define。爲何是講解 1.0.0 ?由於第一個版本的代碼相對於最新版本主幹內容更更清晰更容易上手。github

類圖

概覽

爲了不混淆 tina 和原生的一些概念,這裏先說明一下一些詞的含義小程序

  • wx-Page - 原生 Page 對象
  • tina-Page - tina/class/page 這個類
  • wxPageOptions - 構建原生 Page 實例的 options
  • tinaPageOptions - 構建原生 tina-Page 實例的 options

開局先來預覽一下 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
  }
}

下面針對每一個小流程作講解數組

mix

tina 的 mixin 是靠 js 對對象作合併實現的,並無使用原生的 behaviors微信

tinaPageOptions = this.mix(PAGE_INITIAL_OPTIONS, [...BUILTIN_MIXINS, ...this.mixins, ...(tinaPageOptions.mixins || []), tinaPageOptions])

tinaJs 1.0.0 只支持一種合併策略,跟 Vue 的默認合併策略同樣app

  • 對於 methods 就是後面的覆蓋前面的
  • 對於生命週期勾子和特殊勾子(onPullDownRefresh 等),就是變成一個數組,仍是後面的先執行
  • 也就是 tinaPageOptions.mixins > Page.mixins(全局 mixin) > BUILTIN_MIXINS

合併後能夠獲得這樣一個對象框架

{
  // 頁面
  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-Page

爲了綁定 wx-Page 對象,tina 在 wx-onLoad 中追加了一些操做。
prependHooks 是做用是在 wxPageOptions[hookName] 執行時追加 handlers[hookName] 操做,並保證 wxPageOptions[hookName]handlers[hookName] 的執行上下文是原生運行時的 thisthis

// 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,
  }
}

構建 tina-Page

接下來再來看看 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 的 methodshooks 都是在 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,利用全局 mixinsbeforeLoad,能夠一次性把這個事情作了。

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

以下圖所示

啓動流程

compute 實現原理

由於運行時的上下文都被 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 主幹流程講完,若有疑問歡迎留言

參考

相關文章
相關標籤/搜索