Weec JS Framework(四)

版权声明:本文为博主原创文章,未经博主允许不得转载。技术交流可邮:[email protected] https://blog.csdn.net/cjh94520/article/details/75530295


上图取自weex github 上的图

1.JS Framework初始化

Weex JS Framework 初始化是从对应的入口文件是 html5/render/native/index.js

import setup from './setup'
import frameworks from '../../frameworks/index'

setup(frameworks)

framework来自framework/index.js,系统默认支持4种的框架

export default {
  Vanilla,
  Vue,
  Rax,
  Weex  
}

跟进去setup(frameworks),观察如何进行初始化

export default function (frameworks) {
  const { init, config } = runtime
  config.frameworks = frameworks
  const { native, transformer } = subversion

  // 根据serviceName注册service
  for (const serviceName in services) {
    runtime.service.register(serviceName, services[serviceName])
  }

  //全部属性冻结,防止修改
  runtime.freezePrototype()
  //console设置相关level
  runtime.setNativeConsole()

  // register framework meta info
  //将框架、转换器对应的版本号注册到global
  global.frameworkVersion = native
  global.transformerVersion = transformer

  // init frameworks
  //主要初始化手段,初始化全部的方法,配合下边的循环设置到global  
  const globalMethods = init(config)

  // set global methods
  for (const methodName in globalMethods) {
    global[methodName] = (...args) => {
      const ret = globalMethods[methodName](...args)
      if (ret instanceof Error) {
        console.error(ret.toString())
      }
      return ret
    }
  }
}

解释如上代码,基本上所有的操作都是围绕global对象,将对应的方法全部注册到global对象当中。
前面都是获取frameworkVersion 和transformerVersion 字段,重点方法在init(config),跟进:

runtime\init.js:


export default function init (config) {
  runtimeConfig = config || {}
  frameworks = runtimeConfig.frameworks || {}
  initTaskHandler()

  // Init each framework by `init` method and `config` which contains three
  // virtual-DOM Class: `Document`, `Element` & `Comment`, and a JS bridge method:
  // `sendTasks(...args)`.
  for (const name in frameworks) {
    const framework = frameworks[name]
    framework.init(config)
  }

  // @todo: The method `registerMethods` will be re-designed or removed later.
  ; ['registerComponents', 'registerModules', 'registerMethods'].forEach(genInit)

  ; ['destroyInstance', 'refreshInstance', 'receiveTasks', 'getRoot'].forEach(genInstance)

  adaptInstance('receiveTasks', 'callJS')

  return methods
}

framework就是我们一开始初始化的几个定义好的框架,通过各自的init方法进行初始化,而且带上config,原注解也提醒了,config有3个重要的virtual-DOM,DocumentElementComment和一个JS bridge 方法sendTasks(…args)

我们观察weex的init过程,其他框架类似:

export function init (cfg) {
  config.Document = cfg.Document
  config.Element = cfg.Element
  config.Comment = cfg.Comment
  config.sendTasks = cfg.sendTasks
  config.Listener = cfg.Listener
}

也就是将config对应的class复制到framework中。初始化config以后就开始执行genInit

  ; ['registerComponents', 'registerModules', 'registerMethods'].forEach(genInit)

将上文这三个方法名依次入参调用genInit()方法,那么接下来我们跟进去:

function genInit (methodName) {
  methods[methodName] = function (...args) {
    if (methodName === 'registerComponents') {
      checkComponentMethods(args[0])
    }
    for (const name in frameworks) {
      const framework = frameworks[name]
      if (framework && framework[methodName]) {
        framework[methodName](...args)
      }
    }
  }
}

上面代码比较简单,设置这3个方法到methods对象上,也是调用frameworks对应的方法,而methods对象需要关注,我们收集的所有方法,最后都是交给这个对象,然后统一返回给之前我们提到的global对象。
而methods原本就带有三个方法:

const methods = {
  createInstance,
  registerService: register,
  unregisterService: unregister
}
function genInstance (methodName) {
  methods[methodName] = function (...args) {
    const id = args[0]
    const info = instanceMap[id]
    if (info && frameworks[info.framework]) {
      const result = frameworks[info.framework][methodName](...args)

      // Lifecycle methods
      if (methodName === 'refreshInstance') {
        services.forEach(service => {
          const refresh = service.options.refresh
          if (refresh) {
            refresh(id, { info, runtime: runtimeConfig })
          }
        })
      }
      else if (methodName === 'destroyInstance') {
        services.forEach(service => {
          const destroy = service.options.destroy
          if (destroy) {
            destroy(id, { info, runtime: runtimeConfig })
          }
        })
        delete instanceMap[id]
      }

      return result
    }
    return new Error(`invalid instance id "${id}"`)
  }
}

上面是设置’destroyInstance’, ‘refreshInstance’, ‘receiveTasks’, ‘getRoot’这4个方法的genInstance 。receiveTasks和getRoot走的还是framework路线,而destroyInstance和refreshInstance则交给service 处理。

init( )初始化的最后一步就是给每个实例添加callJS的方法

adaptInstance('receiveTasks', 'callJS')
function adaptInstance (methodName, nativeMethodName) {
  methods[nativeMethodName] = function (...args) {
    const id = args[0]
    const info = instanceMap[id]
    if (info && frameworks[info.framework]) {
      return frameworks[info.framework][methodName](...args)
    }
    return new Error(`invalid instance id "${id}"`)
  }
}

当Native调用callJS方法的时候,就会调用到对应id的instance的receiveTasks方法。

至此,所有方法全部设置到methods上,之后返回交给global,初始化结束.

2.JS Bundle加载

从客户端发起WXSDKInstance.render(),到IWxBridge发出“createInstance”指令,JS Framework受到指令后,就会触发html5/runtime/init.js里面的function createInstance (id, code, config, data)方法。

function createInstance (id, code, config, data) {
  let info = instanceMap[id]

  if (!info) {
    // 检查版本信息
    info = checkVersion(code) || {}
    if (!frameworks[info.framework]) {
      info.framework = 'Weex'
    }

    // 初始化 instance 的 config.
    config = JSON.parse(JSON.stringify(config || {}))
    config.bundleVersion = info.version
    config.env = JSON.parse(JSON.stringify(global.WXEnvironment || {}))
    console.debug(`[JS Framework] create an ${info.framework}@${config.bundleVersion} instance from ${config.bundleVersion}`)

    const env = {
      info,
      config,
      created: Date.now(),
      framework: info.framework
    }
    env.services = createServices(id, env, runtimeConfig)
    instanceMap[id] = env

    return frameworks[info.framework].createInstance(id, code, config, data, env)
  }
  return new Error(`invalid instance id "${id}"`)
}

这个方法里面就是对版本信息,config,日期等信息进行初始化。主要代码是:

所以整个逻辑回到legacy/static/create.js

export function createInstance (id, code, options, data, info) {
  const { services } = info || {}
  resetTarget()
  let instance = instanceMap[id]
  /* istanbul ignore else */
  options = options || {}
  let result
  /* istanbul ignore else */
  if (!instance) {
    instance = new App(id, options)
    instanceMap[id] = instance
    result = initApp(instance,
                     code,
                     data,
                     services)
  }
  else {
    result = new Error(`invalid instance id "${id}"`)
  }
  return result
}
  1. info就是上文提及的Env,将其中的key services取出来;
  2. 主要是中间的new App,在JS Framewrork,Js Bundle 对应的是一个App对象,同时instanceMap会保存这个App对象。
  3. 然后再执行初始化方法initApp();

initApp()表示legacy/app/ctrl/init.js中的init()方法

export function init (app, code, data, services) {
  let result

  // prepare app env methods
  const bundleDefine = (...args) => defineFn(app, ...args)
  const bundleBootstrap = (name, config, _data) => {
    result = bootstrap(app, name, config, _data || data)
    updateActions(app)
    app.doc.listener.createFinish()
  }

  const bundleVm = Vm
  /* istanbul ignore next */
  const bundleRegister = (...args) => register(app, ...args)
  /* istanbul ignore next */
  const bundleRender = (name, _data) => {
    result = bootstrap(app, name, {}, _data)
  }
  /* istanbul ignore next */
  const bundleRequire = name => _data => {
    result = bootstrap(app, name, {}, _data)
  }
  const bundleDocument = app.doc
  /* istanbul ignore next */
  const bundleRequireModule = name => app.requireModule(removeWeexPrefix(name))

  const weexGlobalObject = {
    config: app.options,
    define: bundleDefine,
    bootstrap: bundleBootstrap,
    requireModule: bundleRequireModule,
    document: bundleDocument,
    Vm: bundleVm
  }

  Object.freeze(weexGlobalObject)

   /**
   *  主要作用是封装准备解析的Js Bundle代码
   * */
  let functionBody
  /* istanbul ignore if */
  if (typeof code === 'function') {
    functionBody = code.toString().substr(12)
  }
  else if (code) {
    functionBody = code.toString()
  }
  functionBody = `(function(global){\n\n"use strict";\n\n ${functionBody} \n\n})(Object.create(this))`

  // run code and get result
  const { WXEnvironment } = global
  const timerAPIs = {}

  if (WXEnvironment && WXEnvironment.platform !== 'Web') {
    const timer = app.requireModule('timer')
    Object.assign(timerAPIs, {
      setTimeout: (...args) => {
        const handler = function () {
          args[0](...args.slice(2))
        }
        timer.setTimeout(handler, args[1])
        return app.doc.taskCenter.callbackManager.lastCallbackId.toString()
      },
      setInterval: (...args) => {
        const handler = function () {
          args[0](...args.slice(2))
        }
        timer.setInterval(handler, args[1])
        return app.doc.taskCenter.callbackManager.lastCallbackId.toString()
      },
      clearTimeout: (n) => {
        timer.clearTimeout(n)
      },
      clearInterval: (n) => {
        timer.clearInterval(n)
      }
    })
  }

   /**
   * 上面这个方法很重要。在上面这个方法中封装了一个globalObjects对象,里面装了define 、require 、bootstrap 、register 、render这5个方法。
   */
  const globalObjects = Object.assign({
    define: bundleDefine,
    require: bundleRequire,
    bootstrap: bundleBootstrap,
    register: bundleRegister,
    render: bundleRender,
    __weex_define__: bundleDefine, // alias for define
    __weex_bootstrap__: bundleBootstrap, // alias for bootstrap
    __weex_document__: bundleDocument,
    __weex_require__: bundleRequireModule,
    __weex_viewmodel__: bundleVm,
    weex: weexGlobalObject
  }, timerAPIs, services)

    /**
     * 为了适配code cache,新增的callFunctionNative()用来调用native端编译bundle的函数compileAndRunBundle()。
     * 如果native端成功编译bundle,则不会采用jsfm中的callFunction(),如果native端无法编译bundle,那么还是fallback到
     * callFunction()。
     */
    if (!callFunctionNative(globalObjects, functionBody)) {
    // If failed to compile functionBody on native side,
    // fallback to callFunction.
    callFunction(globalObjects, functionBody)
  }

  return result
}

具体的js code cache可以看ATA大神解释:
https://www.atatech.org/articles/79137
callFunctionNative()方法会尝试调用native级别compileAndRunBundle
() 去解析这个bundle。

function callFunctionNative (globalObjects, body) {
  let fn = void 0
  let isNativeCompileOk = false
  let script = '(function ('
  //省略script部分代码
  try {
    const weex = globalObjects.weex || {}
    const config = weex.config || {}
    fn = compileAndRunBundle(script,
                             config.bundleUrl,
                             config.bundleDigest,
                             config.codeCachePath)
    if (fn && typeof fn === 'function') {
      fn(...globalValues)
      isNativeCompileOk = true
    }
  }
  return isNativeCompileOk
}

所以在具体解析bundle阶段,会调用我们上面给globalObjects对象定义好的几个方法,分别执行

  const globalObjects = Object.assign({
    define: bundleDefine,
    require: bundleRequire,
    bootstrap: bundleBootstrap,
    register: bundleRegister,
    render: bundleRender,
    __weex_define__: bundleDefine, // alias for define
    __weex_bootstrap__: bundleBootstrap, // alias for bootstrap
    __weex_document__: bundleDocument,
    __weex_require__: bundleRequireModule,
    __weex_viewmodel__: bundleVm,
    weex: weexGlobalObject
  }, timerAPIs, services)

3.V-Dom构建

从JS 引擎开始解析JS Bundle开始

方法名 作用
define 注册自定义的组件,并将component记录到customComponentMap[name] = exports数组中
bootstrap 校验参数和环境信息,降级判断,新建viewModel

最重要的看bootstrap,有创建ViewModel的功能,这个ViewModel相当于这个JS Bundle对应的根元素:
from(legacy/app/bundle/bootstrap.js)

export function bootstrap (app, name, config, data) {
  //省略部分代码...
  //进行降级版本检查
  const downgradeResult = downgrade.check(config.downgrade)
  /* istanbul ignore if */
  if (downgradeResult.isDowngrade) {
    app.callTasks([{
      module: 'instanceWrap',
      method: 'error',
      args: [
        downgradeResult.errorType,
        downgradeResult.code,
        downgradeResult.errorMessage
      ]
    }])
    return new Error(`Downgrade[${downgradeResult.code}]: ${downgradeResult.errorMessage}`)
  }

  //设置viewport宽高信息
  if (config.viewport) {
    setViewport(app, config.viewport)
  }

  //创建Vm对象
  app.vm = new Vm(cleanName, null, { _app: app }, null, data)
}

重点关注Vm(legacy\vm\index.js)

export default function Vm (
  type,
  options,
  parentVm,
  parentEl,
  mergedData,
  externalEvents
) {
   //...省略相关代码

    /**
     *  1.绑定相关事件以及生命周期
     */
  initEvents(this, externalEvents)

  this.$emit('hook:init')
  this._inited = true


  this._data = typeof data === 'function' ? data() : data
  if (mergedData) {
    extend(this._data, mergedData)
  }
    /**
     * 2.相关状态设置(属性+方法)
     */
  initState(this)

  this.$emit('hook:created')
  this._created = true

  if (!this._app.doc) {
    return
  }

  // if no parentElement then specify the documentElement
  this._parentEl = parentEl || this._app.doc.documentElement
    /**
     * 3.创建模板
     */
  build(this)
}

VM相当于WxInstance对应的根元素,从这里开始讲所有的V-Dom元素一个个设置,具体表现在3个方法:
1. initEvents()。绑定元素对应的事件,以及通用的生命周期
2. initState()。相关状态设置(属性+方法)里面具体细分各个设置方法:

export function initState (vm) {
  vm._watchers = []
  initData(vm)  //数据绑定,设置属性的观察者
  initComputed(vm) //初始化计算属性
  initMethods(vm) //方法绑定
}

3.build()。new出每个dom对象,并通知客户端绘制native ui
build的过程实际就是解析dom的各个元素的属性的过程。由于需要兼容vue.js很多特性,所以具体会compile走不同的路线。

export function build (vm) {
  const opt = vm._options || {}
  const template = opt.template || {}

  if (opt.replace) {
    if (template.children && template.children.length === 1) {
      compile(vm, template.children[0], vm._parentEl)
    }
    else {
      compile(vm, template.children, vm._parentEl)
    }
  }
  else {
    compile(vm, template, vm._parentEl)
  }

  console.debug(`[JS Framework] "ready" lifecycle in Vm(${vm._type})`)
  vm.$emit('hook:ready')
  vm._ready = true
}

按注解提示走:

/**
 * build()
 *   compile(template, parentNode)
 *     if (type is content) create contentNode
 *     else if (dirs have v-for) foreach -> create context
 *       -> compile(templateWithoutFor, parentNode): diff(list) onchange
 *     else if (dirs have v-if) assert
 *       -> compile(templateWithoutIf, parentNode): toggle(shown) onchange
 *     else if (type is dynamic)
 *       -> compile(templateWithoutDynamicType, parentNode): watch(type) onchange
 *     else if (type is custom)
 *       addChildVm(vm, parentVm)
 *       build(externalDirs)
 *       foreach childNodes -> compile(childNode, template)
 *     else if (type is native)
 *       set(dirs): update(id/attr/style/class) onchange
 *       append(template, parentNode)
 *       foreach childNodes -> compile(childNode, template)
 */
function compileNativeComponent (vm, template, dest, type) {
  applyNaitveComponentOptions(template)

  let element
    /**
     * 判断_documentElement属性走createBody,表示创建根元素
     * createElement走普通元素
     */
  if (dest.ref === '_documentElement') {
    element = createBody(vm, type)
  }
  else {
    element = createElement(vm, type)
  }

    /**
     * 绑定相关属性
     */
  bindElement(vm, element, template)

}
export function bindElement (vm, el, template) {
  setId(vm, el, template.id, vm)
  setAttr(vm, el, template.attr)
  setClass(vm, el, template.classList)
  setStyle(vm, el, template.style)
  bindEvents(vm, el, template.events)
}

至此V-DOm渲染完毕。

猜你喜欢

转载自blog.csdn.net/cjh94520/article/details/75530295