上图取自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,Document
,Element
,Comment
和一个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
}
- info就是上文提及的Env,将其中的key services取出来;
- 主要是中间的new App,在JS Framewrork,Js Bundle 对应的是一个App对象,同时instanceMap会保存这个App对象。
- 然后再执行初始化方法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渲染完毕。