Tina.js source code analysis

tinajs# is a lightweight and progressive wechat applet framework, which can not only make full use of the capabilities of native applets, but also easy to debug.
This framework mainly encapsulates the two global methods of component and Page. This paper mainly introduces the paeg of [tinajs 1.0.0] () Define what has been done internally. Component.define and paeg Define is similar to paeg After defining , you will naturally understand , component define. Why explain 1.0.0? Because the code of the first version is clearer and easier to use than that of the latest version.

 

overview

In order to avoid confusing tina with some concepts of primordial, let's explain the meaning of some words first

  • Wx Page - native Page object
  • Tina page - tina/class/page
  • wxPageOptions - options for building native Page instances
  • tinaPageOptions - options for building native Tina page instances

Let's preview page at the beginning Define process

// tina/class/page.js
class Page extends Basic {
  static mixins = []
  static define(tinaPageOptions = {}) {
    // Option merge
    tinaPageOptions = this.mix(/*....*/)
    
    // Building native options objects
    let wxPageOptions = {/*.....*/}
    
    // Intercept the native onLoad and associate the Wx page object and Tina page object
    wxPageOptions = prependHooks(wxPageOptions, {
      onLoad() {
        // this is an instance of the applet Wx page
        // Instance is the Tina page instance
        let instance = new Page({ tinaPageOptions })
        // Establish association
        this.__tina_instance__ = instance
        instance.$source = this
      }
    })
    
    // Construct Wx page object
    new globals.Page({
       // ...
       ...wxPageOptions,
     })
  }
  constructor({ tinaPageOptions = {} }) {
    super()
    //.......
  }
  get data() {
   return this.$source.data
  }
}

The following explains each small process

 

mix

tina's mixin is implemented by merging objects with js, and does not use native} behaviors

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

tinaJs 1.0.0 only supports one consolidation policy, which is the same as the default consolidation policy of {vue}

  • For methods, the latter overrides the former
  • For life cycle hooks and special hooks (onPullDownRefresh, etc.), do you want to turn them into an array or execute them first
  • That is, tinapageoptions mixin s > Page. Mixins > builtin_ MIXINS

Such an object can be obtained after merging

{
  // page
  beforeLoad: [$log.beforeLoad, options.beforeLoad],
  onLoad: [$initial.onLoad, options.onLoad],
  onHide: [],
  onPageScroll: [],
  onPullDownRefresh: [],
  onReachBottom: [],
  onReady: [],
  onShareAppMessage: [],
  onShow: [],
  onUnload: [],
  // assembly
  attached: Function,
  compute: Function,
  created: $log.created,
  // Page and component sharing
  data: tinaPageOptions.data,
  methods: tinaPageOptions.methods,
  mixins: [],
}

After the merger, the Wx page object is created. As for what is done in the process of creating Wx page object, in order to facilitate the understanding of the whole process, we will skip the explanation here for the time being and explain it later in the "change execution context" section.

 

Associate Wx page and Tina page

In order to bind the Wx page object, tina added some operations in Wx onload.
prependHooks is used to append the "handlers[hookName] operation during the execution of" wxPageOptions[hookName], and ensure that the execution context of "wxPageOptions[hookName] and" handlers[hookName] is "this" of the native runtime

// tina/class/page
wxPageOptions = prependHooks(wxPageOptions, {
  onLoad() {
    // this is wxPageOptions
    // Instance is a Tina page instance
    let instance = new Page({ tinaPageOptions })
    // Establish association
    this.__tina_instance__ = instance
    instance.$source = this
  }
})


// tina/utils/helpers.js

/**
 * Add a hook before the Wx page life cycle hook
 * @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) {
    // Rewrite hook method
    result[name] = function handler (...args) {
      // When the applet is running, this is wxPageOptions
      if (isPrepend) {
        // Execute onLoad appended by tina
        handlers[name].apply(this, args)
      }
      if (typeof context[name] === 'function') {
        // Execute real onLoad
        context[name].apply(this, args)
      }
      // ...
    }
  }
  return {
    ...context,
    ...result,
  }
}

 

Build Tina page

Next, let's look at what new Page does

  constructor({ tinaPageOptions = {} }) {
    super()
    // Create Wx page options
    let members = {
      // compute is a method added by tina
      compute: tinaPageOptions.compute || function () {
        return {}
      },
      ...tinaPageOptions.methods,
      // Used for all life cycles of the agent (including the beforeLoad appended by tina)
      ...mapObject(pick(tinaPageOptions, PAGE_HOOKS), (handlers) => {
        return function (...args) {
          // Because mixin processing has been done, there will be multiple processing methods in a life cycle
          return handlers.reduce((memory, handler) => {
            const result = handler.apply(this, args.concat(memory))
            return result
          }, void 0)
        }
      }),
      // Taking beforeLoad and onLoad as examples, the life cycle processing method added after mapObject above is actually executed like this
      // beforeLoad(...args) {
      //  return [onLoad1,onLoad2,.....].reduce((memory, handler) => {
      //    return handler.apply(this, args.concat(memory))
      //  }, void 0)
      //},
      // onLoad(...args) {
      //   return [onShow1,onShow2,.....].reduce((memory, handler) => {
      //     return handler.apply(this, args.concat(memory))
      //   }, void 0)
      // },
    }

    // Tina page proxy all attributes
    for (let name in members) {
      this[name] = members[name]
    }

    return this
  }

First of all, we need to turn # tinaPageOptions # into the same structure as # wxPageOptions # because # methods # and # hooks # of wxPageOptions are in the first layer of options, so we need to pave the methods and hooks.
And because hooks has become an array after mixins processing, it needs to be traversed and executed. The second parameter of each hook is the result of the previous accumulation. Then copy all the methods to the Tina page instance through a simple attribute copy.

 

Change execution context

As mentioned above, build a tina page object with the same attributes as Wx page, so why? What is the role of a framework? I think it is to build an abstraction layer on top of the native capabilities that can improve development efficiency. Now tina is the abstraction layer,
For example, we want to use {methods When foo # is called natively, tina can use foo # methods Do more in foo. Therefore, tina needs to be associated with the primitives, so that all the things originally processed by the primitives can be transferred to the abstraction layer of tina for processing.
How did tina handle it. Let's take a look at the source code of creating 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 The change execution context in is Tina Page object
 * @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)
  })
}

The answer is in wxoptionsgenerator methods. As mentioned above, it will be bound when {onLoad}__ tina_instance__ To wx page, and the properties of wx page and Tina page are the same, so the call will be forwarded to the corresponding method of Tina. This is equivalent to Tina making an abstraction layer on wx. All passive calls are handled by Tina. And because the context is__ tina_instance__ For the sake of,
All active calls go through tina and then to wx. It will be better understood by combining the following two sections.

 

Add life cycle hook

There is a sentence "wxoptionsgenerator" when creating "wxPageOptions" above Lifecycles , code, which is used by tina to add one more , before load , life cycle hook before , onLoad , how does this function do? Let's take a look at the source code

// tina/utils/wx-options-generator

/**
 * options.methods The change execution context in is Tina Page object
 * @param {Array} hooks
 * @param {Function} getBeforeHookName
 * @return {Object}
 */
export function lifecycles(hooks, getBeforeHookName) {
  return fromPairs(hooks.map((origin) => {
    let before = getBeforeHookName(origin) // For example, 'beforeLoad'
    return [
      origin, // For example, 'load'
      function wxHook() {
        let context = this.__tina_instance__
        // Call the Tina page method, such as beforeLoad
        if (before && context[before]) {
          context[before].apply(context, arguments)
        }
        if (context[origin]) {
          return context[origin].apply(context, arguments)
        }
      }
    ]
  }))
}

In fact, it is rewriting {onLoad and calling} Tina page onLoad , call , Tina page beforeLoad. Some people may have questions about why you should add a "before load" hook, which is not the same as that in "on load".
For example, many times we have to manually "decode" after "onLoad" gets "query". We can do this at one time by using global "mixins" and "beforeLoad".

Page.mixins = [{
  beforeLoad(query) {
    // decode query
    // Yes, this$ Options to decode
  }
}]

It should also be noted that the tina source code has intercepted "onLoad" for many times, and the execution sequence is as follows

prependHooks.addHooks.handler -> wx-Page.onLoad,relation wx-Page,tinaPage -> go back to prependHooks.addHooks.handler -> lifecycles.wxHook -> tina-Page.beforeLoad -> tina-Page.onLoad

As shown in the figure below

Guangzhou packaging design companyhttp://www.maiqicn.com Computer embroidery factory ttp://www.szhdn.com

Implementation principle of compute

Because the runtime context is changed from tina to tina page, the developer calls this setData is actually the setData method of tina page. Because tina page inherits from Basic, it calls the setData method of Basic. Let's take a look at the source code of {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)
}

From the source code, you can see that every time you call # setData # to update data, this is the principle of # compute # and it's easy to understand.

As mentioned in the # mix # section above, tina will merge some built-in options. You can see that this will be called when # onLoad # SetData to initialize the compute attribute.

// mixins/index.js

function initial() {
  // To initialize the compute property
  this.setData()
  this.$log('Initial Mixin', 'Ready')
}

export const $initial = {
  // ...
  onLoad: initial,// Page loading completion tick
}

Tags: Framework

Posted by jamesxg1 on Fri, 13 May 2022 21:27:36 +0300