目前公司团队小程序框架使用的是 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
主干流程讲完,若有疑问欢迎留言