Weex爲了提升Native的極致性能,作了不少優化的工做javascript
爲了達到全部頁面在用戶端達到秒開,也就是網絡(JS Bundle下載)和首屏渲染(展示在用戶第一屏的渲染時間)時間和小於1s。css
手淘團隊在對Weex進行性能優化時,遇到了不少問題和挑戰:html
JS Bundle下載慢,壓縮後60k左右大小的JS Bundle,在全網環境下,平均下載速度大於800ms(在2G/3G下甚至是2s以上)。
JS和Native通訊效率低,拖慢了首屏加載時間。vue
最終想到的辦法就是把JSFramework內置到SDK中,達到極致優化的做用。html5
客戶端訪問Weex頁面時,首先會網絡請求JS Bundle,JS Bundle被加載到客戶端本地後,傳入JSFramework中進行解析渲染。JS Framework解析和渲染的過程實際上是根據JS Bundle的數據結構建立Virtual DOM 和數據綁定,而後傳遞給客戶端渲染。
因爲JSFramework在本地,因此就減小了JS Bundle的體積,每一個JS Bundle均可以減小一部分體積,Bundle裏面只保留業務代碼。每一個頁面下載Bundle的時間均可以節約10-20ms。若是Weex頁面很是多,那麼每一個頁面累計起來節約的時間就不少了。 Weex這種默認就拆包加載的設計,比ReactNative強,也就不須要考慮一直困擾ReactNative頭疼的拆包的問題了。java
整個過程當中,JSFramework將整個頁面的渲染分拆成一個個渲染指令,而後經過JS Bridge發送給各個平臺的RenderEngine進行Native渲染。所以,儘管在開發時寫的是 HTML / CSS / JS,但最後在各個移動端(在iOS上對應的是iOS的Native UI、在Android上對應的是Android的Native UI)渲染後產生的結果是純Native頁面。
因爲JSFramework在本地SDK中,只用在初始化的時候初始化一次,以後每一個頁面都無須再初始化了。也進一步的提升了與Native的通訊效率。node
JSFramework在客戶端的做用在前幾篇文章裏面也提到了。它的在Native端的職責有3個:react
接下來,筆者從源碼的角度詳細分析一下Weex 中別具匠心的JS Framework是如何實現上述的特性的。webpack
分析Weex JS Framework 以前,先來看看整個Weex JS Framework的代碼文件結構樹狀圖。如下的代碼版本是0.19.8。ios
weex/html5/frameworks
├── index.js
├── legacy
│ ├── api // 定義 Vm 上的接口
│ │ ├── methods.js // 以$開頭的一些內部方法
│ │ └── modules.js // 一些組件的信息
│ ├── app // 頁面實例相關代碼
│ │ ├── bundle // 打包編譯的主代碼
│ │ │ ├── bootstrap.js
│ │ │ ├── define.js
│ │ │ └── index.js // 處理jsbundle的入口
│ │ ├── ctrl // 處理Native觸發回來方法
│ │ │ ├── index.js
│ │ │ ├── init.js
│ │ │ └── misc.js
│ │ ├── differ.js // differ相關的處理方法
│ │ ├── downgrade.js // H5降級相關的處理方法
│ │ ├── index.js
│ │ ├── instance.js // Weex實例的構造函數
│ │ ├── register.js // 註冊模塊和組件的處理方法
│ │ ├── viewport.js
│ ├── core // 數據監聽相關代碼,ViewModel的核心代碼
│ │ ├── array.js
│ │ ├── dep.js
│ │ ├── LICENSE
│ │ ├── object.js
│ │ ├── observer.js
│ │ ├── state.js
│ │ └── watcher.js
│ ├── static // 一些靜態的方法
│ │ ├── bridge.js
│ │ ├── create.js
│ │ ├── life.js
│ │ ├── map.js
│ │ ├── misc.js
│ │ └── register.js
│ ├── util // 工具函數如isReserved,toArray,isObject等方法
│ │ ├── index.js
│ │ └── LICENSE
│ │ └── shared.js
│ ├── vm // 組件模型相關代碼
│ │ ├── compiler.js // ViewModel模板解析器和數據綁定操做
│ │ ├── directive.js // 指令編譯器
│ │ ├── dom-helper.js // Dom 元素的helper
│ │ ├── events.js // 組件的全部事件以及生命週期
│ │ └── index.js // ViewModel的構造器和定義
│ ├── config.js
│ └── index.js // 入口文件
└── vanilla
└── index.js複製代碼
還會用到runtime文件夾裏面的文件,因此runtime的文件結構也梳理一遍。
weex/html5/runtime
├── callback-manager.js
├── config.js
├── handler.js
├── index.js
├── init.js
├── listener.js
├── service.js
├── task-center.js
└── vdom
├── comment.js
├── document.js
├── element-types.js
├── element.js
├── index.js
├── node.js
└── operation.js複製代碼
接下來開始分析Weex JS Framework 初始化。
Weex JS Framework 初始化是從對應的入口文件是 html5/render/native/index.js
import { subversion } from '../../../package.json'
import runtime from '../../runtime'
import frameworks from '../../frameworks/index'
import services from '../../services/index'
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()方法,防止修改現有屬性的特性和值,並阻止添加新屬性。
runtime.freezePrototype()
// 調用runtime裏面的setNativeConsole()方法,根據Native設置的logLevel等級設置相應的Console
runtime.setNativeConsole()
// 註冊 framework 元信息
global.frameworkVersion = native
global.transformerVersion = transformer
// 初始化 frameworks
const globalMethods = init(config)
// 設置全局方法
for (const methodName in globalMethods) {
global[methodName] = (...args) => {
const ret = globalMethods[methodName](...args)
if (ret instanceof Error) {
console.error(ret.toString())
}
return ret
}
}複製代碼
上述方法中會調用init( )方法,這個方法就會進行JS Framework的初始化。
init( )方法在weex/html5/runtime/init.js裏面。
export default function init (config) {
runtimeConfig = config || {}
frameworks = runtimeConfig.frameworks || {}
initTaskHandler()
// 每一個framework都是由init初始化,
// config裏面都包含3個重要的virtual-DOM類,`Document`,`Element`,`Comment`和一個JS bridge 方法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
}複製代碼
在初始化方法裏面傳入了config,這個入參是從weex/html5/runtime/config.js裏面傳入的。
import { Document, Element, Comment } from './vdom'
import Listener from './listener'
import { TaskCenter } from './task-center'
const config = {
Document, Element, Comment, Listener,
TaskCenter,
sendTasks (...args) {
return global.callNative(...args)
}
}
Document.handler = config.sendTasks
export default config複製代碼
config裏面包含Document,Element,Comment,Listener,TaskCenter,以及一個sendTasks方法。
config初始化之後還會添加一個framework屬性,這個屬性是由weex/html5/frameworks/index.js傳進來的。
import * as Vanilla from './vanilla/index'
import * as Vue from 'weex-vue-framework'
import * as Weex from './legacy/index'
import Rax from 'weex-rax-framework'
export default {
Vanilla,
Vue,
Rax,
Weex
}複製代碼
init( )獲取到config和config.frameworks之後,開始執行initTaskHandler()方法。
import { init as initTaskHandler } from './task-center'複製代碼
initTaskHandler( )方法來自於task-center.js裏面的init( )方法。
export function init () {
const DOM_METHODS = {
createFinish: global.callCreateFinish,
updateFinish: global.callUpdateFinish,
refreshFinish: global.callRefreshFinish,
createBody: global.callCreateBody,
addElement: global.callAddElement,
removeElement: global.callRemoveElement,
moveElement: global.callMoveElement,
updateAttrs: global.callUpdateAttrs,
updateStyle: global.callUpdateStyle,
addEvent: global.callAddEvent,
removeEvent: global.callRemoveEvent
}
const proto = TaskCenter.prototype
for (const name in DOM_METHODS) {
const method = DOM_METHODS[name]
proto[name] = method ?
(id, args) => method(id, ...args) :
(id, args) => fallback(id, [{ module: 'dom', method: name, args }], '-1')
}
proto.componentHandler = global.callNativeComponent ||
((id, ref, method, args, options) =>
fallback(id, [{ component: options.component, ref, method, args }]))
proto.moduleHandler = global.callNativeModule ||
((id, module, method, args) =>
fallback(id, [{ module, method, args }]))
}複製代碼
這裏的初始化方法就是往prototype上11個方法:createFinish,updateFinish,refreshFinish,createBody,addElement,removeElement,moveElement,updateAttrs,updateStyle,addEvent,removeEvent。
若是method存在,就用method(id, ...args)方法初始化,若是不存在,就用fallback(id, [{ module: 'dom', method: name, args }], '-1')初始化。
最後再加上componentHandler和moduleHandler。
initTaskHandler( )方法初始化了13個方法(其中2個handler),都綁定到了prototype上
createFinish(id, [{ module: 'dom', method: createFinish, args }], '-1')
updateFinish(id, [{ module: 'dom', method: updateFinish, args }], '-1')
refreshFinish(id, [{ module: 'dom', method: refreshFinish, args }], '-1')
createBody:(id, [{ module: 'dom', method: createBody, args }], '-1')
addElement:(id, [{ module: 'dom', method: addElement, args }], '-1')
removeElement:(id, [{ module: 'dom', method: removeElement, args }], '-1')
moveElement:(id, [{ module: 'dom', method: moveElement, args }], '-1')
updateAttrs:(id, [{ module: 'dom', method: updateAttrs, args }], '-1')
updateStyle:(id, [{ module: 'dom', method: updateStyle, args }], '-1')
addEvent:(id, [{ module: 'dom', method: addEvent, args }], '-1')
removeEvent:(id, [{ module: 'dom', method: removeEvent, args }], '-1')
componentHandler(id, [{ component: options.component, ref, method, args }]))
moduleHandler(id, [{ module, method, args }]))複製代碼
回到init( )方法,處理完initTaskHandler()以後有一個循環:
for (const name in frameworks) {
const framework = frameworks[name]
framework.init(config)
}複製代碼
在這個循環裏面會對frameworks裏面每一個對象調用init方法,入參都傳入config。
好比Vanilla的init( )實現以下:
function init (cfg) {
config.Document = cfg.Document
config.Element = cfg.Element
config.Comment = cfg.Comment
config.sendTasks = cfg.sendTasks
}複製代碼
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之後就開始執行genInit
['registerComponents', 'registerModules', 'registerMethods'].forEach(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)
}
}
}
}複製代碼
methods默認有3個方法
const methods = {
createInstance,
registerService: register,
unregisterService: unregister
}複製代碼
除去這3個方法之外都是調用framework對應的方法。
export function registerComponents (components) {
if (Array.isArray(components)) {
components.forEach(function register (name) {
/* istanbul ignore if */
if (!name) {
return
}
if (typeof name === 'string') {
nativeComponentMap[name] = true
}
/* istanbul ignore else */
else if (typeof name === 'object' && typeof name.type === 'string') {
nativeComponentMap[name.type] = name
}
})
}
}複製代碼
上述方法就是註冊Native的組件的核心代碼實現。最終的註冊信息都存在nativeComponentMap對象中,nativeComponentMap對象最初裏面有以下的數據:
export default {
nativeComponentMap: {
text: true,
image: true,
container: true,
slider: {
type: 'slider',
append: 'tree'
},
cell: {
type: 'cell',
append: 'tree'
}
}
}複製代碼
接着會調用registerModules方法:
export function registerModules (modules) {
/* istanbul ignore else */
if (typeof modules === 'object') {
initModules(modules)
}
}複製代碼
initModules是來自./frameworks/legacy/app/register.js,在這個文件裏面會調用initModules (modules, ifReplace)進行初始化。這個方法裏面是註冊Native的模塊的核心代碼實現。
最後調用registerMethods
export function registerMethods (methods) {
/* istanbul ignore else */
if (typeof methods === 'object') {
initMethods(Vm, methods)
}
}複製代碼
initMethods是來自./frameworks/legacy/app/register.js,在這個方法裏面會調用initMethods (Vm, apis)進行初始化,initMethods方法裏面是註冊Native的handler的核心實現。
當registerComponents,registerModules,registerMethods初始化完成以後,就開始註冊每一個instance實例的方法
['destroyInstance', 'refreshInstance', 'receiveTasks', 'getRoot'].forEach(genInstance)複製代碼
這裏會給genInstance分別傳入destroyInstance,refreshInstance,receiveTasks,getRoot四個方法名。
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}"`)
}
}複製代碼
上面的代碼就是給每一個instance註冊方法的具體實現,在Weex裏面每一個instance默認都會有三個生命週期的方法:createInstance,refreshInstance,destroyInstance。全部Instance的方法都會存在services中。
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方法。
整個init流程總結如上圖。
init結束之後會設置全局方法。
for (const methodName in globalMethods) {
global[methodName] = (...args) => {
const ret = globalMethods[methodName](...args)
if (ret instanceof Error) {
console.error(ret.toString())
}
return ret
}
}複製代碼
圖上標的紅色的3個方法表示的是默認就有的方法。
至此,Weex JS Framework就算初始化完成。
當Native初始化完成Component,Module,handler以後,從遠端請求到了JS Bundle,Native經過調用createInstance方法,把JS Bundle傳給JS Framework。因而接下來的這一切從createInstance開始提及。
Native經過調用createInstance,就會執行到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,日期等信息進行初始化。並在Native記錄一條日誌信息:
[JS Framework] create an Weex@undefined instance from undefined複製代碼
上面這個createInstance方法最終仍是要調用html5/framework/legacy/static/create.js裏面的createInstance (id, code, options, data, info)方法。
export function createInstance (id, code, options, data, info) {
const { services } = info || {}
// 初始化target
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
}複製代碼
new App()方法會建立新的 App 實例對象,而且把對象放入 instanceMap 中。
App對象的定義以下:
export default function App (id, options) {
this.id = id
this.options = options || {}
this.vm = null
this.customComponentMap = {}
this.commonModules = {}
// document
this.doc = new renderer.Document(
id,
this.options.bundleUrl,
null,
renderer.Listener
)
this.differ = new Differ(id)
}複製代碼
其中有三個比較重要的屬性:
舉個例子,假設Native傳入了以下的信息進行createInstance初始化:
args:(
0,
「(這裏是網絡上下載的JS,因爲太長了,省略)」,
{
bundleUrl = "http://192.168.31.117:8081/HelloWeex.js";
debug = 1;
}
)複製代碼
那麼instance = 0,code就是JS代碼,data對應的是下面那個字典,service = @{ }。經過這個入參傳入initApp(instance, code, data, services)方法。這個方法在html5/framework/legacy/app/ctrl/init.js裏面。
export function init (app, code, data, services) {
console.debug('[JS Framework] Intialize an instance with:\n', data)
let result
/* 此處省略了一些代碼*/
// 初始化weexGlobalObject
const weexGlobalObject = {
config: app.options,
define: bundleDefine,
bootstrap: bundleBootstrap,
requireModule: bundleRequireModule,
document: bundleDocument,
Vm: bundleVm
}
// 防止weexGlobalObject被修改
Object.freeze(weexGlobalObject)
/* 此處省略了一些代碼*/
// 下面開始轉換JS Boudle的代碼
let functionBody
/* istanbul ignore if */
if (typeof code === 'function') {
// `function () {...}` -> `{...}`
// not very strict
functionBody = code.toString().substr(12)
}
/* istanbul ignore next */
else if (code) {
functionBody = code.toString()
}
// wrap IFFE and use strict mode
functionBody = `(function(global){\n\n"use strict";\n\n ${functionBody} \n\n})(Object.create(this))`
// run code and get result
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)
callFunction(globalObjects, functionBody)
return result
}複製代碼
上面這個方法很重要。在上面這個方法中封裝了一個globalObjects對象,裏面裝了define 、require 、bootstrap 、register 、render這5個方法。
也會在Native本地記錄一條日誌:
[JS Framework] Intialize an instance with: undefined複製代碼
在上述5個方法中:
/** * @deprecated */
export function register (app, type, options) {
console.warn('[JS Framework] Register is deprecated, please install lastest transformer.')
registerCustomComponent(app, type, options)
}複製代碼
其中register、render、require是已經廢棄的方法。
bundleDefine函數原型:
(...args) => defineFn(app, ...args)複製代碼
bundleBootstrap函數原型:
(name, config, _data) => {
result = bootstrap(app, name, config, _data || data)
updateActions(app)
app.doc.listener.createFinish()
console.debug(`[JS Framework] After intialized an instance(${app.id})`)
}複製代碼
bundleRequire函數原型:
name => _data => {
result = bootstrap(app, name, {}, _data)
}複製代碼
bundleRegister函數原型:
(...args) => register(app, ...args)複製代碼
bundleRender函數原型:
(name, _data) => {
result = bootstrap(app, name, {}, _data)
}複製代碼
上述5個方法封裝到globalObjects中,傳到 JS Bundle 中。
function callFunction (globalObjects, body) {
const globalKeys = []
const globalValues = []
for (const key in globalObjects) {
globalKeys.push(key)
globalValues.push(globalObjects[key])
}
globalKeys.push(body)
// 最終JS Bundle會經過new Function( )的方式被執行
const result = new Function(...globalKeys)
return result(...globalValues)
}複製代碼
最終JS Bundle是會經過new Function( )的方式被執行。JS Bundle的代碼將會在全局環境中執行,並不能獲取到 JS Framework 執行環境中的數據,只能用globalObjects對象裏面的方法。JS Bundle 自己也用了IFFE 和 嚴格模式,也並不會污染全局環境。
以上就是createInstance作的全部事情,在接收到Native的createInstance調用的時候,先會在JSFramework中新建App實例對象並保存在instanceMap 中。再把5個方法(其中3個方法已經廢棄了)傳入到new Function( )中。new Function( )會進行JSFramework最重要的事情,將 JS Bundle 轉換成 Virtual DOM 發送到原生模塊渲染。
構建Virtual DOM的過程就是編譯執行JS Boudle的過程。
先給一個實際的JS Boudle的例子,好比以下的代碼:
// { "framework": "Weex" }
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {
var __weex_template__ = __webpack_require__(1)
var __weex_style__ = __webpack_require__(2)
var __weex_script__ = __webpack_require__(3)
__weex_define__('@weex-component/916f9ecb075bbff1f4ea98389a4bb514', [], function(__weex_require__, __weex_exports__, __weex_module__) {
__weex_script__(__weex_module__, __weex_exports__, __weex_require__)
if (__weex_exports__.__esModule && __weex_exports__.default) {
__weex_module__.exports = __weex_exports__.default
}
__weex_module__.exports.template = __weex_template__
__weex_module__.exports.style = __weex_style__
})
__weex_bootstrap__('@weex-component/916f9ecb075bbff1f4ea98389a4bb514',undefined,undefined)
/***/ },
/* 1 */
/***/ function(module, exports) {
module.exports = {
"type": "div",
"classList": [
"container"
],
"children": [
{
"type": "image",
"attr": {
"src": "http://9.pic.paopaoche.net/up/2016-7/201671315341.png"
},
"classList": [
"pic"
],
"events": {
"click": "picClick"
}
},
{
"type": "text",
"classList": [
"text"
],
"attr": {
"value": function () {return this.title}
}
}
]
}
/***/ },
/* 2 */
/***/ function(module, exports) {
module.exports = {
"container": {
"alignItems": "center"
},
"pic": {
"width": 200,
"height": 200
},
"text": {
"fontSize": 40,
"color": "#000000"
}
}
/***/ },
/* 3 */
/***/ function(module, exports) {
module.exports = function(module, exports, __weex_require__){'use strict';
module.exports = {
data: function () {return {
title: 'Hello World',
toggle: false
}},
ready: function ready() {
console.log('this.title == ' + this.title);
this.title = 'hello Weex';
console.log('this.title == ' + this.title);
},
methods: {
picClick: function picClick() {
this.toggle = !this.toggle;
if (this.toggle) {
this.title = '圖片被點擊';
} else {
this.title = 'Hello Weex';
}
}
}
};}
/* generated by weex-loader */
/***/ }
/******/ ]);複製代碼
JS Framework拿到JS Boudle之後,會先執行bundleDefine。
export const defineFn = function (app, name, ...args) {
console.debug(`[JS Framework] define a component ${name}`)
/*如下代碼省略*/
/*在這個方法裏面註冊自定義組件和普通的模塊*/
}複製代碼
用戶自定義的組件放在app.customComponentMap中。執行完bundleDefine之後調用bundleBootstrap方法。
bundleDefine會解析代碼中的__weex_define__("@weex-component/")定義的component,包含依賴的子組件。並將component記錄到customComponentMap[name] = exports數組中,維護組件與組件代碼的對應關係。因爲會依賴子組件,所以會被屢次調用,直到全部的組件都被解析徹底。
export function bootstrap (app, name, config, data) {
console.debug(`[JS Framework] bootstrap for ${name}`)
// 1. 驗證自定義的Component的名字
let cleanName
if (isWeexComponent(name)) {
cleanName = removeWeexPrefix(name)
}
else if (isNpmModule(name)) {
cleanName = removeJSSurfix(name)
// 檢查是否經過老的 'define' 方法定義的
if (!requireCustomComponent(app, cleanName)) {
return new Error(`It's not a component: ${name}`)
}
}
else {
return new Error(`Wrong component name: ${name}`)
}
// 2. 驗證 configuration
config = isPlainObject(config) ? config : {}
// 2.1 transformer的版本檢查
if (typeof config.transformerVersion === 'string' &&
typeof global.transformerVersion === 'string' &&
!semver.satisfies(config.transformerVersion,
global.transformerVersion)) {
return new Error(`JS Bundle version: ${config.transformerVersion} ` +
`not compatible with ${global.transformerVersion}`)
}
// 2.2 降級版本檢查
const downgradeResult = downgrade.check(config.downgrade)
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)
}
// 3. 新建一個新的自定義的Component組件名字和數據的viewModel
app.vm = new Vm(cleanName, null, { _app: app }, null, data)
}複製代碼
bootstrap方法會在Native本地日誌記錄:
[JS Framework] bootstrap for @weex-component/677c57764d82d558f236d5241843a2a2(此處的編號是舉一個例子)複製代碼
bootstrap方法的做用是校驗參數和環境信息,若是不符合當前條件,會觸發頁面降級,(也能夠手動進行,好比Native出現問題了,降級到H5)。最後會根據Component新建對應的viewModel。
export default function Vm ( type, options, parentVm, parentEl, mergedData, externalEvents ) {
/*省略部分代碼*/
// 初始化
this._options = options
this._methods = options.methods || {}
this._computed = options.computed || {}
this._css = options.style || {}
this._ids = {}
this._vmEvents = {}
this._childrenVms = []
this._type = type
// 綁定事件和生命週期
initEvents(this, externalEvents)
console.debug(`[JS Framework] "init" lifecycle in Vm(${this._type})`)
this.$emit('hook:init')
this._inited = true
// 綁定數據到viewModel上
this._data = typeof data === 'function' ? data() : data
if (mergedData) {
extend(this._data, mergedData)
}
initState(this)
console.debug(`[JS Framework] "created" lifecycle in Vm(${this._type})`)
this.$emit('hook:created')
this._created = true
// backward old ready entry
if (options.methods && options.methods.ready) {
console.warn('"exports.methods.ready" is deprecated, ' +
'please use "exports.created" instead')
options.methods.ready.call(this)
}
if (!this._app.doc) {
return
}
// 若是沒有parentElement,那麼就指定爲documentElement
this._parentEl = parentEl || this._app.doc.documentElement
// 構建模板
build(this)
}複製代碼
上述代碼就是關鍵的新建viewModel的代碼,在這個函數中,若是正常運行完,會在Native記錄下兩條日誌信息:
[JS Framework] "init" lifecycle in Vm(677c57764d82d558f236d5241843a2a2) [;
[JS Framework] "created" lifecycle in Vm(677c57764d82d558f236d5241843a2a2) [;複製代碼
同時幹了三件事情:
export function initEvents (vm, externalEvents) {
const options = vm._options || {}
const events = options.events || {}
for (const type1 in events) {
vm.$on(type1, events[type1])
}
for (const type2 in externalEvents) {
vm.$on(type2, externalEvents[type2])
}
LIFE_CYCLE_TYPES.forEach((type) => {
vm.$on(`hook:${type}`, options[type])
})
}複製代碼
在initEvents方法裏面會監聽三類事件:
const LIFE_CYCLE_TYPES = ['init', 'created', 'ready', 'destroyed']複製代碼
生命週期的鉤子包含上述4種,init,created,ready,destroyed。
$on方法是增長事件監聽者listener的。$emit方式是用來執行方法的,可是不進行dispatch和broadcast。$dispatch方法是派發事件,沿着父類往上傳遞。$broadcast方法是廣播事件,沿着子類往下傳遞。$off方法是移除事件監聽者listener。
事件object的定義以下:
function Evt (type, detail) {
if (detail instanceof Evt) {
return detail
}
this.timestamp = Date.now()
this.detail = detail
this.type = type
let shouldStop = false
this.stop = function () {
shouldStop = true
}
this.hasStopped = function () {
return shouldStop
}
}複製代碼
每一個組件的事件包含事件的object,事件的監聽者,事件的emitter,生命週期的hook鉤子。
initEvents的做用就是對當前的viewModel綁定上上述三種事件的監聽者listener。
export function initState (vm) {
vm._watchers = []
initData(vm)
initComputed(vm)
initMethods(vm)
}複製代碼
export function initData (vm) {
let data = vm._data
if (!isPlainObject(data)) {
data = {}
}
// proxy data on instance
const keys = Object.keys(data)
let i = keys.length
while (i--) {
proxy(vm, keys[i])
}
// observe data
observe(data, vm)
}複製代碼
在initData方法裏面最後一步會進行data的observe。
數據綁定的核心思想是基於 ES5 的 Object.defineProperty 方法,在 vm 實例上建立了一系列的 getter / setter,支持數組和深層對象,在設置屬性值的時候,會派發更新事件。
這塊數據綁定的思想,一部分是借鑑了Vue的實現,這塊打算之後寫篇文章專門談談。
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)
在上述一系列的compile方法中,有4個參數,
編譯的方法也分爲如下7種:
上述7個方法裏面,除了compileBlock和compileNativeComponent之外的5個方法,都會遞歸調用。
編譯好模板之後,原來的JS Boudle就都被轉變成了相似Json格式的 Virtual DOM 了。下一步開始繪製Native UI。
繪製Native UI的核心方法就是compileNativeComponent (vm, template, dest, type)。
compileNativeComponent的核心實現以下:
function compileNativeComponent (vm, template, dest, type) {
applyNaitveComponentOptions(template)
let element
if (dest.ref === '_documentElement') {
// if its parent is documentElement then it's a body
console.debug(`[JS Framework] compile to create body for ${type}`)
// 構建DOM根
element = createBody(vm, type)
}
else {
console.debug(`[JS Framework] compile to create element for ${type}`)
// 添加元素
element = createElement(vm, type)
}
if (!vm._rootEl) {
vm._rootEl = element
// bind event earlier because of lifecycle issues
const binding = vm._externalBinding || {}
const target = binding.template
const parentVm = binding.parent
if (target && target.events && parentVm && element) {
for (const type in target.events) {
const handler = parentVm[target.events[type]]
if (handler) {
element.addEvent(type, bind(handler, parentVm))
}
}
}
}
bindElement(vm, element, template)
if (template.attr && template.attr.append) { // backward, append prop in attr
template.append = template.attr.append
}
if (template.append) { // give the append attribute for ios adaptation
element.attr = element.attr || {}
element.attr.append = template.append
}
const treeMode = template.append === 'tree'
const app = vm._app || {}
if (app.lastSignal !== -1 && !treeMode) {
console.debug('[JS Framework] compile to append single node for', element)
app.lastSignal = attachTarget(vm, element, dest)
}
if (app.lastSignal !== -1) {
compileChildren(vm, template, element)
}
if (app.lastSignal !== -1 && treeMode) {
console.debug('[JS Framework] compile to append whole tree for', element)
app.lastSignal = attachTarget(vm, element, dest)
}
}複製代碼
繪製Native的UI會先繪製DOM的根,而後繪製上面的子孩子元素。子孩子須要遞歸判斷,若是還有子孩子,還須要繼續進行以前的compile的流程。
每一個 Document 對象中都會包含一個 listener 屬性,它能夠向 Native 端發送消息,每當建立元素或者是有更新操做時,listener 就會拼裝出制定格式的 action,而且最終調用 callNative 把 action 傳遞給原生模塊,原生模塊中也定義了相應的方法來執行 action 。
例如當某個元素執行了 element.appendChild() 時,就會調用 listener.addElement(),而後就會拼成一個相似Json格式的數據,再調用callTasks方法。
export function callTasks (app, tasks) {
let result
/* istanbul ignore next */
if (typof(tasks) !== 'array') {
tasks = [tasks]
}
tasks.forEach(task => {
result = app.doc.taskCenter.send(
'module',
{
module: task.module,
method: task.method
},
task.args
)
})
return result
}複製代碼
在上述方法中會繼續調用在html5/runtime/task-center.js中的send方法。
send (type, options, args) {
const { action, component, ref, module, method } = options
args = args.map(arg => this.normalize(arg))
switch (type) {
case 'dom':
return this[action](this.instanceId, args)
case 'component':
return this.componentHandler(this.instanceId, ref, method, args, { component })
default:
return this.moduleHandler(this.instanceId, module, method, args, {})
}
}複製代碼
這裏存在有2個handler,它們的實現是以前傳進來的sendTasks方法。
const config = {
Document, Element, Comment, Listener,
TaskCenter,
sendTasks (...args) {
return global.callNative(...args)
}
}複製代碼
sendTasks方法最終會調用callNative,調用本地原生的UI進行繪製。
最後來看看Weex JS Framework是如何處理Native傳遞過來的事件的。
在html5/framework/legacy/static/bridge.js裏面對應的是Native的傳遞過來的事件處理方法。
const jsHandlers = {
fireEvent: (id, ...args) => {
return fireEvent(instanceMap[id], ...args)
},
callback: (id, ...args) => {
return callback(instanceMap[id], ...args)
}
}
/** * 接收來自Native的事件和回調 */
export function receiveTasks (id, tasks) {
const instance = instanceMap[id]
if (instance && Array.isArray(tasks)) {
const results = []
tasks.forEach((task) => {
const handler = jsHandlers[task.method]
const args = [...task.args]
/* istanbul ignore else */
if (typeof handler === 'function') {
args.unshift(id)
results.push(handler(...args))
}
})
return results
}
return new Error(`invalid instance id "${id}" or tasks`)
}複製代碼
在Weex 每一個instance實例裏面都包含有一個callJS的全局方法,當本地調用了callJS這個方法之後,會調用receiveTasks方法。
關於Native會傳遞過來哪些事件,能夠看這篇文章《Weex 事件傳遞的那些事兒》
在jsHandler裏面封裝了fireEvent和callback方法,這兩個方法在html5/frameworks/legacy/app/ctrl/misc.js方法中。
export function fireEvent (app, ref, type, e, domChanges) {
console.debug(`[JS Framework] Fire a "${type}" event on an element(${ref}) in instance(${app.id})`)
if (Array.isArray(ref)) {
ref.some((ref) => {
return fireEvent(app, ref, type, e) !== false
})
return
}
const el = app.doc.getRef(ref)
if (el) {
const result = app.doc.fireEvent(el, type, e, domChanges)
app.differ.flush()
app.doc.taskCenter.send('dom', { action: 'updateFinish' }, [])
return result
}
return new Error(`invalid element reference "${ref}"`)
}複製代碼
fireEvent傳遞過來的參數包含,事件類型,事件object,是一個元素的ref。若是事件會引發DOM的變化,那麼還會帶一個參數描述DOM的變化。
在htlm5/frameworks/runtime/vdom/document.js裏面
fireEvent (el, type, e, domChanges) {
if (!el) {
return
}
e = e || {}
e.type = type
e.target = el
e.timestamp = Date.now()
if (domChanges) {
updateElement(el, domChanges)
}
return el.fireEvent(type, e)
}複製代碼
這裏能夠發現,其實對DOM的更新是單獨作的,而後接着把事件繼續往下傳,傳給element。
接着在htlm5/frameworks/runtime/vdom/element.js裏面
fireEvent (type, e) {
const handler = this.event[type]
if (handler) {
return handler.call(this, e)
}
}複製代碼
最終事件在這裏經過handler的call方法進行調用。
當有數據發生變化的時候,會觸發watcher的數據監聽,當前的value和oldValue比較。先會調用watcher的update方法。
Watcher.prototype.update = function (shallow) {
if (this.lazy) {
this.dirty = true
} else {
this.run()
}複製代碼
update方法裏面會調用run方法。
Watcher.prototype.run = function () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated; but only do so if this is a
// non-shallow update (caused by a vm digest).
((isObject(value) || this.deep) && !this.shallow)
) {
// set new value
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue)
}
this.queued = this.shallow = false
}
}複製代碼
run方法以後會觸發differ,dep會通知全部相關的子視圖的改變。
Dep.prototype.notify = function () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}複製代碼
相關聯的子視圖也會觸發update的方法。
還有一種事件是Native經過模塊的callback回調傳遞事件。
export function callback (app, callbackId, data, ifKeepAlive) {
console.debug(`[JS Framework] Invoke a callback(${callbackId}) with`, data,
`in instance(${app.id})`)
const result = app.doc.taskCenter.callback(callbackId, data, ifKeepAlive)
updateActions(app)
app.doc.taskCenter.send('dom', { action: 'updateFinish' }, [])
return result
}複製代碼
callback的回調比較簡單,taskCenter.callback會調用callbackManager.consume的方法。執行完callback方法之後,接着就是執行differ.flush,最後一步就是回調Native,通知updateFinish。
至此,Weex JS Framework 的三大基本功能都分析完畢了,用一張大圖作個總結,描繪它幹了哪些事情:
圖片有點大,連接點這裏
除了目前官方默認支持的 Vue 2.0,Rax的Framework,還能夠支持其餘平臺的 JS Framework 。Weex還能夠支持本身自定義的 JS Framework。只要按照以下的步驟來定製,能夠寫一套完整的 JS Framework。
若是通過上述的步驟進行擴展之後,能夠出現以下的代碼:
import * as Vue from '...'
import * as React from '...'
import * as Angular from '...'
export default { Vue, React, Angular };複製代碼
這樣能夠支持Vue,React,Angular。
若是在 JS Bundle 在文件開頭帶有以下格式的註釋:
// { "framework": "Vue" }
...複製代碼
這樣 Weex JS 引擎就會識別出這個 JS bundle 須要用 Vue 框架來解析。並分發給 Vue 框架處理。
這樣每一個 JS Framework,只要:1. 封裝了這幾個接口,2. 給本身的 JS Bundle 第一行寫好特殊格式的註釋,Weex 就能夠正常的運行基於各類 JS Framework 的頁面了。
Weex 支持同時多種框架在一個移動應用中共存並各自解析基於不一樣框架的 JS bundle。
這一塊筆者暫時尚未實踐各自解析不一樣的 JS bundle,相信這部分將來也許能夠幹不少有趣的事情。
本篇文章把 Weex 在 Native 端的 JS Framework 的工做原理簡單的梳理了一遍,中間惟一沒有深究的點可能就是 Weex 是 如何 利用
Vue 進行數據綁定的,如何監聽數據變化的,這塊打算另外開一篇文章詳細的分析一下。到此篇爲止,Weex 在 Native 端的全部源碼實現就分析完畢了。
請你們多多指點。
References:
Weex 官方文檔
Weex 框架中 JS Framework 的結構
淺析weex之vdom渲染
Native 性能穩定性極致優化
本次徵文活動的連接: juejin.im/post/58d8e9…