Weex 中別具匠心的 JS Framework
前言
Weex為了提高Native的極致性能,做了很多優化的工作
為了達到所有頁面在用戶端達到秒開,也就是網路(JS Bundle下載)和首屏渲染(展現在用戶第一屏的渲染時間)時間和小於1s。
手淘團隊在對Weex進行性能優化時,遇到了很多問題和挑戰:
JS Bundle下載慢,壓縮後60k左右大小的JS Bundle,在全網環境下,平均下載速度大於800ms(在2G/3G下甚至是2s以上)。 JS和Native通信效率低,拖慢了首屏載入時間。
最終想到的辦法就是把JSFramework內置到SDK中,達到極致優化的作用。
客戶端訪問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頭疼的拆包的問題了。整個過程中,JSFramework將整個頁面的渲染分拆成一個個渲染指令,然後通過JS Bridge發送給各個平台的RenderEngine進行Native渲染。因此,儘管在開發時寫的是 HTML / CSS / JS,但最後在各個移動端(在iOS上對應的是iOS的Native UI、在Android上對應的是Android的Native UI)渲染後產生的結果是純Native頁面。 由於JSFramework在本地SDK中,只用在初始化的時候初始化一次,之後每個頁面都無須再初始化了。也進一步的提高了與Native的通信效率。
JSFramework在客戶端的作用在前幾篇文章裡面也提到了。它的在Native端的職責有3個:
- 管理每個Weex instance實例的生命周期。
- 不斷的接收Native傳過來的JS Bundle,轉換成Virtual DOM,再調用Native的方法,構建頁面布局。
- 響應Native傳過來的事件,進行響應。
接下來,筆者從源碼的角度詳細分析一下Weex 中別具匠心的JS Framework是如何實現上述的特性的。
目錄
- 1.Weex JS Framework 初始化
- 2.Weex JS Framework 管理實例的生命周期
- 3.Weex JS Framework 構建Virtual DOM
- 4.Weex JS Framework 處理Native觸發的事件
- 5.Weex JS Framework 未來可能做更多的事情
一. Weex JS Framework 初始化
分析Weex JS Framework 之前,先來看看整個Weex JS Framework的代碼文件結構樹狀圖。以下的代碼版本是0.19.8。
weex/html5/frameworksn ├── index.jsn ├── legacy n │ ├── api // 定義 Vm 上的介面n │ │ ├── methods.js // 以$開頭的一些內部方法n │ │ └── modules.js // 一些組件的信息n │ ├── app // 頁面實例相關代碼n │ │ ├── bundle // 打包編譯的主代碼n │ │ │ ├── bootstrap.jsn │ │ │ ├── define.jsn │ │ │ └── index.js // 處理jsbundle的入口n │ │ ├── ctrl // 處理Native觸發回來方法n │ │ │ ├── index.jsn │ │ │ ├── init.jsn │ │ │ └── misc.jsn │ │ ├── differ.js // differ相關的處理方法n │ │ ├── downgrade.js // H5降級相關的處理方法n │ │ ├── index.jsn │ │ ├── instance.js // Weex實例的構造函數n │ │ ├── register.js // 註冊模塊和組件的處理方法n │ │ ├── viewport.jsn │ ├── core // 數據監聽相關代碼,ViewModel的核心代碼n │ │ ├── array.jsn │ │ ├── dep.jsn │ │ ├── LICENSEn │ │ ├── object.jsn │ │ ├── observer.jsn │ │ ├── state.jsn │ │ └── watcher.jsn │ ├── static // 一些靜態的方法n │ │ ├── bridge.jsn │ │ ├── create.jsn │ │ ├── life.jsn │ │ ├── map.jsn │ │ ├── misc.jsn │ │ └── register.jsn │ ├── util // 工具函數如isReserved,toArray,isObject等方法n │ │ ├── index.jsn │ │ └── LICENSEn │ │ └── shared.jsn │ ├── vm // 組件模型相關代碼n │ │ ├── compiler.js // ViewModel模板解析器和數據綁定操作n │ │ ├── directive.js // 指令編譯器n │ │ ├── dom-helper.js // Dom 元素的helpern │ │ ├── events.js // 組件的所有事件以及生命周期n │ │ └── index.js // ViewModel的構造器和定義n │ ├── config.jsn │ └── index.js // 入口文件n └── vanillan └── index.jsn
還會用到runtime文件夾裡面的文件,所以runtime的文件結構也梳理一遍。
weex/html5/runtimen ├── callback-manager.jsn ├── config.js n ├── handler.js n ├── index.js n ├── init.js n ├── listener.js n ├── service.js n ├── task-center.js n └── vdom n ├── comment.js n ├── document.js n ├── element-types.js n ├── element.js n ├── index.js n ├── node.js n └── operation.js n
接下來開始分析Weex JS Framework 初始化。
Weex JS Framework 初始化是從對應的入口文件是 html5/render/native/index.js
import { subversion } from ../../../package.jsonnimport runtime from ../../runtimenimport frameworks from ../../frameworks/indexnimport services from ../../services/indexnnconst { init, config } = runtimenconfig.frameworks = frameworksnconst { native, transformer } = subversionnn// 根據serviceName註冊servicenfor (const serviceName in services) {n runtime.service.register(serviceName, services[serviceName])n}nn// 調用runtime裡面的freezePrototype()方法,防止修改現有屬性的特性和值,並阻止添加新屬性。nruntime.freezePrototype()nn// 調用runtime裡面的setNativeConsole()方法,根據Native設置的logLevel等級設置相應的Consolenruntime.setNativeConsole()nn// 註冊 framework 元信息nglobal.frameworkVersion = nativenglobal.transformerVersion = transformernn// 初始化 frameworksnconst globalMethods = init(config)nn// 設置全局方法nfor (const methodName in globalMethods) {n global[methodName] = (...args) => {n const ret = globalMethods[methodName](...args)n if (ret instanceof Error) {n console.error(ret.toString())n }n return retn }n}n
上述方法中會調用init( )方法,這個方法就會進行JS Framework的初始化。
init( )方法在weex/html5/runtime/init.js裡面。
export default function init (config) {n runtimeConfig = config || {}n frameworks = runtimeConfig.frameworks || {}n initTaskHandler()nn // 每個framework都是由init初始化,n // config裡面都包含3個重要的virtual-DOM類,`Document`,`Element`,`Comment`和一個JS bridge 方法sendTasks(...args)n for (const name in frameworks) {n const framework = frameworks[name]n framework.init(config)n }nn // @todo: The method `registerMethods` will be re-designed or removed later.n ; [registerComponents, registerModules, registerMethods].forEach(genInit)nn ; [destroyInstance, refreshInstance, receiveTasks, getRoot].forEach(genInstance)nn adaptInstance(receiveTasks, callJS)nn return methodsn}n
在初始化方法裡面傳入了config,這個入參是從weex/html5/runtime/config.js裡面傳入的。
import { Document, Element, Comment } from ./vdomnimport Listener from ./listenernimport { TaskCenter } from ./task-centernnconst config = {n Document, Element, Comment, Listener,n TaskCenter,n sendTasks (...args) {n return global.callNative(...args)n }n}nnDocument.handler = config.sendTasksnnexport default confign
config裡面包含Document,Element,Comment,Listener,TaskCenter,以及一個sendTasks方法。
config初始化以後還會添加一個framework屬性,這個屬性是由weex/html5/frameworks/index.js傳進來的。
import * as Vanilla from ./vanilla/indexnimport * as Vue from weex-vue-frameworknimport * as Weex from ./legacy/indexnimport Rax from weex-rax-frameworknnexport default {n Vanilla,n Vue,n Rax,n Weexn}n
init( )獲取到config和config.frameworks以後,開始執行initTaskHandler()方法。
import { init as initTaskHandler } from ./task-centern
initTaskHandler( )方法來自於task-center.js裡面的init( )方法。
export function init () {n const DOM_METHODS = {n createFinish: global.callCreateFinish,n updateFinish: global.callUpdateFinish,n refreshFinish: global.callRefreshFinish,nn createBody: global.callCreateBody,nn addElement: global.callAddElement,n removeElement: global.callRemoveElement,n moveElement: global.callMoveElement,n updateAttrs: global.callUpdateAttrs,n updateStyle: global.callUpdateStyle,nn addEvent: global.callAddEvent,n removeEvent: global.callRemoveEventn }n const proto = TaskCenter.prototypenn for (const name in DOM_METHODS) {n const method = DOM_METHODS[name]n proto[name] = method ?n (id, args) => method(id, ...args) :n (id, args) => fallback(id, [{ module: dom, method: name, args }], -1)n }nn proto.componentHandler = global.callNativeComponent ||n ((id, ref, method, args, options) =>n fallback(id, [{ component: options.component, ref, method, args }]))nn proto.moduleHandler = global.callNativeModule ||n ((id, module, method, args) =>n fallback(id, [{ module, method, args }]))n}n
這裡的初始化方法就是往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)n updateFinish(id, [{ module: dom, method: updateFinish, args }], -1)n refreshFinish(id, [{ module: dom, method: refreshFinish, args }], -1)n createBody:(id, [{ module: dom, method: createBody, args }], -1)nn addElement:(id, [{ module: dom, method: addElement, args }], -1)n removeElement:(id, [{ module: dom, method: removeElement, args }], -1)n moveElement:(id, [{ module: dom, method: moveElement, args }], -1)n updateAttrs:(id, [{ module: dom, method: updateAttrs, args }], -1)n updateStyle:(id, [{ module: dom, method: updateStyle, args }], -1)nn addEvent:(id, [{ module: dom, method: addEvent, args }], -1)n removeEvent:(id, [{ module: dom, method: removeEvent, args }], -1)nn componentHandler(id, [{ component: options.component, ref, method, args }]))n moduleHandler(id, [{ module, method, args }]))n
回到init( )方法,處理完initTaskHandler()之後有一個循環:
for (const name in frameworks) {n const framework = frameworks[name]n framework.init(config)n }n
在這個循環裡面會對frameworks裡面每個對象調用init方法,入參都傳入config。
比如Vanilla的init( )實現如下:
function init (cfg) {n config.Document = cfg.Documentn config.Element = cfg.Elementn config.Comment = cfg.Commentn config.sendTasks = cfg.sendTasksn}n
Weex的init( )實現如下:
export function init (cfg) {n config.Document = cfg.Documentn config.Element = cfg.Elementn config.Comment = cfg.Commentn config.sendTasks = cfg.sendTasksn config.Listener = cfg.Listenern}n
初始化config以後就開始執行genInit
[registerComponents, registerModules, registerMethods].forEach(genInit)n
function genInit (methodName) {n methods[methodName] = function (...args) {n if (methodName === registerComponents) {n checkComponentMethods(args[0])n }n for (const name in frameworks) {n const framework = frameworks[name]n if (framework && framework[methodName]) {n framework[methodName](...args)n }n }n }n}n
methods默認有3個方法
const methods = {n createInstance,n registerService: register,n unregisterService: unregistern}n
除去這3個方法以外都是調用framework對應的方法。
export function registerComponents (components) {n if (Array.isArray(components)) {n components.forEach(function register (name) {n /* istanbul ignore if */n if (!name) {n returnn }n if (typeof name === string) {n nativeComponentMap[name] = truen }n /* istanbul ignore else */n else if (typeof name === object && typeof name.type === string) {n nativeComponentMap[name.type] = namen }n })n }n}n
上述方法就是註冊Native的組件的核心代碼實現。最終的註冊信息都存在nativeComponentMap對象中,nativeComponentMap對象最初裡面有如下的數據:
export default {n nativeComponentMap: {n text: true,n image: true,n container: true,n slider: {n type: slider,n append: treen },n cell: {n type: cell,n append: treen }n }n}n
接著會調用registerModules方法:
export function registerModules (modules) {n /* istanbul ignore else */n if (typeof modules === object) {n initModules(modules)n }n}n
initModules是來自./frameworks/legacy/app/register.js,在這個文件裡面會調用initModules (modules, ifReplace)進行初始化。這個方法裡面是註冊Native的模塊的核心代碼實現。
最後調用registerMethods
export function registerMethods (methods) {n /* istanbul ignore else */n if (typeof methods === object) {n initMethods(Vm, methods)n }n}n
initMethods是來自./frameworks/legacy/app/register.js,在這個方法裡面會調用initMethods (Vm, apis)進行初始化,initMethods方法裡面是註冊Native的handler的核心實現。
當registerComponents,registerModules,registerMethods初始化完成之後,就開始註冊每個instance實例的方法
[destroyInstance, refreshInstance, receiveTasks, getRoot].forEach(genInstance)n
這裡會給genInstance分別傳入destroyInstance,refreshInstance,receiveTasks,getRoot四個方法名。
function genInstance (methodName) {n methods[methodName] = function (...args) {n const id = args[0]n const info = instanceMap[id]n if (info && frameworks[info.framework]) {n const result = frameworks[info.framework][methodName](...args)nn // Lifecycle methodsn if (methodName === refreshInstance) {n services.forEach(service => {n const refresh = service.options.refreshn if (refresh) {n refresh(id, { info, runtime: runtimeConfig })n }n })n }n else if (methodName === destroyInstance) {n services.forEach(service => {n const destroy = service.options.destroyn if (destroy) {n destroy(id, { info, runtime: runtimeConfig })n }n })n delete instanceMap[id]n }nn return resultn }n return new Error(`invalid instance id "${id}"`)n }n}n
上面的代碼就是給每個instance註冊方法的具體實現,在Weex裡面每個instance默認都會有三個生命周期的方法:createInstance,refreshInstance,destroyInstance。所有Instance的方法都會存在services中。
init( )初始化的最後一步就是給每個實例添加callJS的方法
adaptInstance(receiveTasks, callJS)n
function adaptInstance (methodName, nativeMethodName) {n methods[nativeMethodName] = function (...args) {n const id = args[0]n const info = instanceMap[id]n if (info && frameworks[info.framework]) {n return frameworks[info.framework][methodName](...args)n }n return new Error(`invalid instance id "${id}"`)n }n}n
當Native調用callJS方法的時候,就會調用到對應id的instance的receiveTasks方法。
整個init流程總結如上圖。
init結束以後會設置全局方法。
for (const methodName in globalMethods) {n global[methodName] = (...args) => {n const ret = globalMethods[methodName](...args)n if (ret instanceof Error) {n console.error(ret.toString())n }n return retn }n}n
圖上標的紅色的3個方法表示的是默認就有的方法。
至此,Weex JS Framework就算初始化完成。
二. 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) {n let info = instanceMap[id]nn if (!info) {n // 檢查版本信息n info = checkVersion(code) || {}n if (!frameworks[info.framework]) {n info.framework = Weexn }nn // 初始化 instance 的 config.n config = JSON.parse(JSON.stringify(config || {}))n config.bundleVersion = info.versionn config.env = JSON.parse(JSON.stringify(global.WXEnvironment || {}))n console.debug(`[JS Framework] create an ${info.framework}@${config.bundleVersion} instance from ${config.bundleVersion}`)nn const env = {n info,n config,n created: Date.now(),n framework: info.frameworkn }n env.services = createServices(id, env, runtimeConfig)n instanceMap[id] = envnn return frameworks[info.framework].createInstance(id, code, config, data, env)n }n return new Error(`invalid instance id "${id}"`)n}n
這個方法裡面就是對版本信息,config,日期等信息進行初始化。並在Native記錄一條日誌信息:
[JS Framework] create an Weex@undefined instance from undefinedn
上面這個createInstance方法最終還是要調用html5/framework/legacy/static/create.js裡面的createInstance (id, code, options, data, info)方法。
export function createInstance (id, code, options, data, info) {n const { services } = info || {}n // 初始化targetn resetTarget()n let instance = instanceMap[id]n /* istanbul ignore else */n options = options || {}n let resultn /* istanbul ignore else */n if (!instance) {n instance = new App(id, options)n instanceMap[id] = instancen result = initApp(instance, code, data, services)n }n else {n result = new Error(`invalid instance id "${id}"`)n }n return resultn}n
new App()方法會創建新的 App 實例對象,並且把對象放入 instanceMap 中。
App對象的定義如下:
export default function App (id, options) {n this.id = idn this.options = options || {}n this.vm = nulln this.customComponentMap = {}n this.commonModules = {}nn // documentn this.doc = new renderer.Document(n id,n this.options.bundleUrl,n null,n renderer.Listenern )n this.differ = new Differ(id)n}n
其中有三個比較重要的屬性:
- id 是 JS Framework 與 Native 端通信時的唯一標識。
- vm 是 View Model,組件模型,包含了數據綁定相關功能。
- doc 是 Virtual DOM 中的根節點。
舉個例子,假設Native傳入了如下的信息進行createInstance初始化:
args:( n 0,n 「(這裡是網路上下載的JS,由於太長了,省略)」, n { n bundleUrl = "http://192.168.31.117:8081/HelloWeex.js"; n debug = 1; n }n) n
那麼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) {n console.debug([JS Framework] Intialize an instance with:n, data)n let resultnn /* 此處省略了一些代碼*/ nn // 初始化weexGlobalObjectn const weexGlobalObject = {n config: app.options,n define: bundleDefine,n bootstrap: bundleBootstrap,n requireModule: bundleRequireModule,n document: bundleDocument,n Vm: bundleVmn }nn // 防止weexGlobalObject被修改n Object.freeze(weexGlobalObject)n /* 此處省略了一些代碼*/ nn // 下面開始轉換JS Boudle的代碼n let functionBodyn /* istanbul ignore if */n if (typeof code === function) {n // `function () {...}` -> `{...}`n // not very strictn functionBody = code.toString().substr(12)n }n /* istanbul ignore next */n else if (code) {n functionBody = code.toString()n }n // wrap IFFE and use strict moden functionBody = `(function(global){nn"use strict";nn ${functionBody} nn})(Object.create(this))`nn // run code and get resultn const globalObjects = Object.assign({n define: bundleDefine,n require: bundleRequire,n bootstrap: bundleBootstrap,n register: bundleRegister,n render: bundleRender,n __weex_define__: bundleDefine, // alias for definen __weex_bootstrap__: bundleBootstrap, // alias for bootstrapn __weex_document__: bundleDocument,n __weex_require__: bundleRequireModule,n __weex_viewmodel__: bundleVm,n weex: weexGlobalObjectn }, timerAPIs, services)nn callFunction(globalObjects, functionBody)nn return resultn}n
上面這個方法很重要。在上面這個方法中封裝了一個globalObjects對象,裡面裝了define 、require 、bootstrap 、register 、render這5個方法。
也會在Native本地記錄一條日誌:
[JS Framework] Intialize an instance with: undefinedn
在上述5個方法中:
/**n * @deprecatedn */nexport function register (app, type, options) {n console.warn([JS Framework] Register is deprecated, please install lastest transformer.)n registerCustomComponent(app, type, options)n}n
其中register、render、require是已經廢棄的方法。
bundleDefine函數原型:
(...args) => defineFn(app, ...args)n
bundleBootstrap函數原型:
(name, config, _data) => {n result = bootstrap(app, name, config, _data || data)n updateActions(app)n app.doc.listener.createFinish()n console.debug(`[JS Framework] After intialized an instance(${app.id})`)n }n
bundleRequire函數原型:
name => _data => {n result = bootstrap(app, name, {}, _data)n }n
bundleRegister函數原型:
(...args) => register(app, ...args)n
bundleRender函數原型:
(name, _data) => {n result = bootstrap(app, name, {}, _data)n }n
上述5個方法封裝到globalObjects中,傳到 JS Bundle 中。
function callFunction (globalObjects, body) {n const globalKeys = []n const globalValues = []n for (const key in globalObjects) {n globalKeys.push(key)n globalValues.push(globalObjects[key])n }n globalKeys.push(body)n // 最終JS Bundle會通過new Function( )的方式被執行n const result = new Function(...globalKeys)n return result(...globalValues)n}n
最終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 發送到原生模塊渲染。
三. Weex JS Framework 構建Virtual DOM
構建Virtual DOM的過程就是編譯執行JS Boudle的過程。
先給一個實際的JS Boudle的例子,比如如下的代碼:
JS Framework拿到JS Boudle以後,會先執行bundleDefine。
export const defineFn = function (app, name, ...args) {n console.debug(`[JS Framework] define a component ${name}`)nn /*以下代碼省略*/n /*在這個方法裡面註冊自定義組件和普通的模塊*/nn}n
用戶自定義的組件放在app.customComponentMap中。執行完bundleDefine以後調用bundleBootstrap方法。
- define: 用來自定義一個複合組件
- bootstrap: 用來以某個複合組件為根結點渲染頁面
bundleDefine會解析代碼中的__weex_define__("@weex-component/")定義的component,包含依賴的子組件。並將component記錄到customComponentMap[name] = exports數組中,維護組件與組件代碼的對應關係。由於會依賴子組件,因此會被多次調用,直到所有的組件都被解析完全。
export function bootstrap (app, name, config, data) {n console.debug(`[JS Framework] bootstrap for ${name}`)nn // 1. 驗證自定義的Component的名字n let cleanNamen if (isWeexComponent(name)) {n cleanName = removeWeexPrefix(name)n }n else if (isNpmModule(name)) {n cleanName = removeJSSurfix(name)n // 檢查是否通過老的 define 方法定義的n if (!requireCustomComponent(app, cleanName)) {n return new Error(`Its not a component: ${name}`)n }n }n else {n return new Error(`Wrong component name: ${name}`)n }nn // 2. 驗證 configurationn config = isPlainObject(config) ? config : {}n // 2.1 transformer的版本檢查n if (typeof config.transformerVersion === string &&n typeof global.transformerVersion === string &&n !semver.satisfies(config.transformerVersion,n global.transformerVersion)) {n return new Error(`JS Bundle version: ${config.transformerVersion} ` +n `not compatible with ${global.transformerVersion}`)n }n // 2.2 降級版本檢查n const downgradeResult = downgrade.check(config.downgrade)nn if (downgradeResult.isDowngrade) {n app.callTasks([{n module: instanceWrap,n method: error,n args: [n downgradeResult.errorType,n downgradeResult.code,n downgradeResult.errorMessagen ]n }])n return new Error(`Downgrade[${downgradeResult.code}]: ${downgradeResult.errorMessage}`)n }nn // 設置 viewportn if (config.viewport) {n setViewport(app, config.viewport)n }nn // 3. 新建一個新的自定義的Component組件名字和數據的viewModeln app.vm = new Vm(cleanName, null, { _app: app }, null, data)n}n
bootstrap方法會在Native本地日誌記錄:
[JS Framework] bootstrap for @weex-component/677c57764d82d558f236d5241843a2a2(此處的編號是舉一個例子)n
bootstrap方法的作用是校驗參數和環境信息,如果不符合當前條件,會觸發頁面降級,(也可以手動進行,比如Native出現問題了,降級到H5)。最後會根據Component新建對應的viewModel。
export default function Vm (n type,n options,n parentVm,n parentEl,n mergedData,n externalEventsn) {n /*省略部分代碼*/n // 初始化n this._options = optionsn this._methods = options.methods || {}n this._computed = options.computed || {}n this._css = options.style || {}n this._ids = {}n this._vmEvents = {}n this._childrenVms = []n this._type = typenn // 綁定事件和生命周期n initEvents(this, externalEvents)nn console.debug(`[JS Framework] "init" lifecycle in n Vm(${this._type})`)n this.$emit(hook:init)n this._inited = truenn // 綁定數據到viewModel上n this._data = typeof data === function ? data() : datan if (mergedData) {n extend(this._data, mergedData)n }n initState(this)nn console.debug(`[JS Framework] "created" lifecycle in Vm(${this._type})`)n this.$emit(hook:created)n this._created = truenn // backward old ready entryn if (options.methods && options.methods.ready) {n console.warn("exports.methods.ready" is deprecated, +n please use "exports.created" instead)n options.methods.ready.call(this)n }nn if (!this._app.doc) {n returnn }nn // 如果沒有parentElement,那麼就指定為documentElementn this._parentEl = parentEl || this._app.doc.documentElementn // 構建模板n build(this)n}n
上述代碼就是關鍵的新建viewModel的代碼,在這個函數中,如果正常運行完,會在Native記錄下兩條日誌信息:
[JS Framework] "init" lifecycle in Vm(677c57764d82d558f236d5241843a2a2) [;n[JS Framework] "created" lifecycle in Vm(677c57764d82d558f236d5241843a2a2) [;n
同時幹了三件事情:
- initEvents 初始化事件和生命周期
- initState 實現數據綁定功能
- build模板並繪製 Native UI
1. initEvents 初始化事件和生命周期
export function initEvents (vm, externalEvents) {n const options = vm._options || {}n const events = options.events || {}n for (const type1 in events) {n vm.$on(type1, events[type1])n }n for (const type2 in externalEvents) {n vm.$on(type2, externalEvents[type2])n }n LIFE_CYCLE_TYPES.forEach((type) => {n vm.$on(`hook:${type}`, options[type])n })n}n
在initEvents方法裡面會監聽三類事件:
- 組件options裡面定義的事情
- 一些外部的事件externalEvents
- 還要綁定生命周期的hook鉤子
const LIFE_CYCLE_TYPES = [init, created, ready, destroyed]n
生命周期的鉤子包含上述4種,init,created,ready,destroyed。
(on方法是增加事件監聽者listener的。)emit方式是用來執行方法的,但是不進行dispatch和broadcast。(dispatch方法是派發事件,沿著父類往上傳遞。)broadcast方法是廣播事件,沿著子類往下傳遞。$off方法是移除事件監聽者listener。
事件object的定義如下:
function Evt (type, detail) {n if (detail instanceof Evt) {n return detailn }nn this.timestamp = Date.now()n this.detail = detailn this.type = typenn let shouldStop = falsen this.stop = function () {n shouldStop = truen }n this.hasStopped = function () {n return shouldStopn }n}n
每個組件的事件包含事件的object,事件的監聽者,事件的emitter,生命周期的hook鉤子。
initEvents的作用就是對當前的viewModel綁定上上述三種事件的監聽者listener。
2. initState 實現數據綁定功能
export function initState (vm) {n vm._watchers = []n initData(vm)n initComputed(vm)n initMethods(vm)n}n
- initData,設置 proxy,監聽 _data 中的屬性;然後添加 reactiveGetter & reactiveSetter 實現數據監聽。 (
- initComputed,初始化計算屬性,只有 getter,在 _data 中沒有對應的值。
- initMethods 將 _method 中的方法掛在實例上。
export function initData (vm) {n let data = vm._datann if (!isPlainObject(data)) {n data = {}n }n // proxy data on instancen const keys = Object.keys(data)n let i = keys.lengthn while (i--) {n proxy(vm, keys[i])n }n // observe datan observe(data, vm)n}n
在initData方法裡面最後一步會進行data的observe。
數據綁定的核心思想是基於 ES5 的 Object.defineProperty 方法,在 vm 實例上創建了一系列的 getter / setter,支持數組和深層對象,在設置屬性值的時候,會派發更新事件。
這塊數據綁定的思想,一部分是借鑒了Vue的實現,這塊打算以後寫篇文章專門談談。
3. build模板
export function build (vm) {n const opt = vm._options || {}n const template = opt.template || {}nn if (opt.replace) {n if (template.children && template.children.length === 1) {n compile(vm, template.children[0], vm._parentEl)n }n else {n compile(vm, template.children, vm._parentEl)n }n }n else {n compile(vm, template, vm._parentEl)n }nn console.debug(`[JS Framework] "ready" lifecycle in Vm(${vm._type})`)n vm.$emit(hook:ready)n vm._ready = truen}n
build構建思路如下:
compile(template, parentNode)
- 如果 type 是 content ,就創建contentNode。
- 否則 如果含有 v-for 標籤, 那麼就循環遍歷,創建context,繼續compile(templateWithoutFor, parentNode)
- 否則 如果含有 v-if 標籤,繼續compile(templateWithoutIf, parentNode)
- 否則如果 type 是 dynamic ,繼續compile(templateWithoutDynamicType, parentNode)
- 否則如果 type 是 custom ,那麼調用addChildVm(vm, parentVm),build(externalDirs),遍歷子節點,然後再compile(childNode, template)
- 最後如果 type 是 Native ,更新(id/attr/style/class),append(template, parentNode),遍歷子節點,compile(childNode, template)
在上述一系列的compile方法中,有4個參數,
- vm: 待編譯的 Vm 對象。
- target: 待編譯的節點,是模板中的標籤經過 transformer 轉換後的結構。
- dest: 當前節點父節點的 Virtual DOM。
- meta: 元數據,在內部調用時可以用來傳遞數據。
編譯的方法也分為以下7種:
- compileFragment 編譯多個節點,創建 Fragment 片段。
- compileBlock 創建特殊的Block。
- compileRepeat 編譯 repeat 指令,同時會執行數據綁定,在數據變動時會觸發 DOM 節點的更新。
- compileShown 編譯 if 指令,也會執行數據綁定。
- compileType 編譯動態類型的組件。
- compileCustomComponent 編譯展開用戶自定義的組件,這個過程會遞歸創建子 vm,並且綁定父子關係,也會觸發子組件的生命周期函數。
- compileNativeComponent 編譯內置原生組件。這個方法會調用 createBody 或 createElement 與原生模塊通信並創建 Native UI。
上述7個方法裡面,除了compileBlock和compileNativeComponent以外的5個方法,都會遞歸調用。
編譯好模板以後,原來的JS Boudle就都被轉變成了類似Json格式的 Virtual DOM 了。下一步開始繪製Native UI。
4. 繪製 Native UI
繪製Native UI的核心方法就是compileNativeComponent (vm, template, dest, type)。
compileNativeComponent的核心實現如下:
function compileNativeComponent (vm, template, dest, type) {n applyNaitveComponentOptions(template)nn let elementn if (dest.ref === _documentElement) {n // if its parent is documentElement then its a bodyn console.debug(`[JS Framework] compile to create body for ${type}`)n // 構建DOM根n element = createBody(vm, type)n }n else {n console.debug(`[JS Framework] compile to create element for ${type}`)n // 添加元素n element = createElement(vm, type)n }nn if (!vm._rootEl) {n vm._rootEl = elementn // bind event earlier because of lifecycle issuesn const binding = vm._externalBinding || {}n const target = binding.templaten const parentVm = binding.parentn if (target && target.events && parentVm && element) {n for (const type in target.events) {n const handler = parentVm[target.events[type]]n if (handler) {n element.addEvent(type, bind(handler, parentVm))n }n }n }n }nn bindElement(vm, element, template)nn if (template.attr && template.attr.append) { // backward, append prop in attrn template.append = template.attr.appendn }nn if (template.append) { // give the append attribute for ios adaptationn element.attr = element.attr || {}n element.attr.append = template.appendn }nn const treeMode = template.append === treen const app = vm._app || {}n if (app.lastSignal !== -1 && !treeMode) {n console.debug([JS Framework] compile to append single node for, element)n app.lastSignal = attachTarget(vm, element, dest)n }n if (app.lastSignal !== -1) {n compileChildren(vm, template, element)n }n if (app.lastSignal !== -1 && treeMode) {n console.debug([JS Framework] compile to append whole tree for, element)n app.lastSignal = attachTarget(vm, element, dest)n }n}n
繪製Native的UI會先繪製DOM的根,然後繪製上面的子孩子元素。子孩子需要遞歸判斷,如果還有子孩子,還需要繼續進行之前的compile的流程。
每個 Document 對象中都會包含一個 listener 屬性,它可以向 Native 端發送消息,每當創建元素或者是有更新操作時,listener 就會拼裝出制定格式的 action,並且最終調用 callNative 把 action 傳遞給原生模塊,原生模塊中也定義了相應的方法來執行 action 。
例如當某個元素執行了 element.appendChild() 時,就會調用 listener.addElement(),然後就會拼成一個類似Json格式的數據,再調用callTasks方法。
export function callTasks (app, tasks) {n let resultnn /* istanbul ignore next */n if (typof(tasks) !== array) {n tasks = [tasks]n }nn tasks.forEach(task => {n result = app.doc.taskCenter.send(n module,n {n module: task.module,n method: task.methodn },n task.argsn )n })nn return resultn}n
在上述方法中會繼續調用在html5/runtime/task-center.js中的send方法。
send (type, options, args) {n const { action, component, ref, module, method } = optionsnn args = args.map(arg => this.normalize(arg))nn switch (type) {n case dom:n return this[action](this.instanceId, args)n case component:n return this.componentHandler(this.instanceId, ref, method, args, { component })n default:n return this.moduleHandler(this.instanceId, module, method, args, {})n }n }n
這裡存在有2個handler,它們的實現是之前傳進來的sendTasks方法。
const config = {n Document, Element, Comment, Listener,n TaskCenter,n sendTasks (...args) {n return global.callNative(...args)n }n}n
sendTasks方法最終會調用callNative,調用本地原生的UI進行繪製。
四. Weex JS Framework 處理Native觸發的事件
最後來看看Weex JS Framework是如何處理Native傳遞過來的事件的。
在html5/framework/legacy/static/bridge.js裡面對應的是Native的傳遞過來的事件處理方法。
const jsHandlers = {n fireEvent: (id, ...args) => {n return fireEvent(instanceMap[id], ...args)n },n callback: (id, ...args) => {n return callback(instanceMap[id], ...args)n }n}nn/**n * 接收來自Native的事件和回調n */nexport function receiveTasks (id, tasks) {n const instance = instanceMap[id]n if (instance && Array.isArray(tasks)) {n const results = []n tasks.forEach((task) => {n const handler = jsHandlers[task.method]n const args = [...task.args]n /* istanbul ignore else */n if (typeof handler === function) {n args.unshift(id)n results.push(handler(...args))n }n })n return resultsn }n return new Error(`invalid instance id "${id}" or tasks`)n}n
在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) {n console.debug(`[JS Framework] Fire a "${type}" event on an element(${ref}) in instance(${app.id})`)n if (Array.isArray(ref)) {n ref.some((ref) => {n return fireEvent(app, ref, type, e) !== falsen })n returnn }n const el = app.doc.getRef(ref)n if (el) {n const result = app.doc.fireEvent(el, type, e, domChanges)n app.differ.flush()n app.doc.taskCenter.send(dom, { action: updateFinish }, [])n return resultn }n return new Error(`invalid element reference "${ref}"`)n}n
fireEvent傳遞過來的參數包含,事件類型,事件object,是一個元素的ref。如果事件會引起DOM的變化,那麼還會帶一個參數描述DOM的變化。
在htlm5/frameworks/runtime/vdom/document.js裡面
fireEvent (el, type, e, domChanges) {n if (!el) {n returnn }n e = e || {}n e.type = typen e.target = eln e.timestamp = Date.now()n if (domChanges) {n updateElement(el, domChanges)n }n return el.fireEvent(type, e)n }n
這裡可以發現,其實對DOM的更新是單獨做的,然後接著把事件繼續往下傳,傳給element。
接著在htlm5/frameworks/runtime/vdom/element.js裡面
fireEvent (type, e) {n const handler = this.event[type]n if (handler) {n return handler.call(this, e)n }n }n
最終事件在這裡通過handler的call方法進行調用。
當有數據發生變化的時候,會觸發watcher的數據監聽,當前的value和oldValue比較。先會調用watcher的update方法。
Watcher.prototype.update = function (shallow) {n if (this.lazy) {n this.dirty = truen } else {n this.run()n }n
update方法裡面會調用run方法。
Watcher.prototype.run = function () {n if (this.active) {n const value = this.get()n if (n value !== this.value ||n // Deep watchers and watchers on Object/Arrays should fire evenn // when the value is the same, because the value mayn // have mutated; but only do so if this is an // non-shallow update (caused by a vm digest).n ((isObject(value) || this.deep) && !this.shallow)n ) {n // set new valuen const oldValue = this.valuen this.value = valuen this.cb.call(this.vm, value, oldValue)n }n this.queued = this.shallow = falsen }n}n
run方法之後會觸發differ,dep會通知所有相關的子視圖的改變。
Dep.prototype.notify = function () {n const subs = this.subs.slice()n for (let i = 0, l = subs.length; i < l; i++) {n subs[i].update()n }n}n
相關聯的子視圖也會觸發update的方法。
還有一種事件是Native通過模塊的callback回調傳遞事件。
export function callback (app, callbackId, data, ifKeepAlive) {n console.debug(`[JS Framework] Invoke a callback(${callbackId}) with`, data,n `in instance(${app.id})`)n const result = app.doc.taskCenter.callback(callbackId, data, ifKeepAlive)n updateActions(app)n app.doc.taskCenter.send(dom, { action: updateFinish }, [])n return resultn}n
callback的回調比較簡單,taskCenter.callback會調用callbackManager.consume的方法。執行完callback方法以後,接著就是執行differ.flush,最後一步就是回調Native,通知updateFinish。
至此,Weex JS Framework 的三大基本功能都分析完畢了,用一張大圖做個總結,描繪它幹了哪些事情:
圖片有點大,大圖點這裡。
五.Weex JS Framework 未來可能做更多的事情
除了目前官方默認支持的 Vue 2.0,Rax的Framework,還可以支持其他平台的 JS Framework 。Weex還可以支持自己自定義的 JS Framework。只要按照如下的步驟來定製,可以寫一套完整的 JS Framework。
- 首先你要有一套完整的 JS Framework。
- 了解 Weex 的 JS 引擎的特性支持情況。
- 適配 Weex 的 native DOM APIs。
- 適配 Weex 的初始化入口和多實例管理機制。
- 在 Weex JS runtime 的 framework 配置中加入自己的 JS Framework 然後打包。
- 基於該 JS Framework 撰寫 JS bundle,並加入特定的前綴注釋,以便 Weex JS runtime 能夠正確識別。
如果經過上述的步驟進行擴展以後,可以出現如下的代碼:
import * as Vue from ...nimport * as React from ...nimport * as Angular from ...nexport default { Vue, React, Angular };n
這樣可以支持Vue,React,Angular。
如果在 JS Bundle 在文件開頭帶有如下格式的注釋:
// { "framework": "Vue" }n...n
這樣 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 性能穩定性極致優化推薦閱讀:
※webpack中 vue + ts + jsx應該怎麼配置?
※vue.js 能否設置某個組件不被keep-alive?
※angular+meteor 已經有團隊在做,Vue+meteor有類似的項目嗎?
※現在是學習vue1還是vue2?