在幾天前開源的華爲 HarmonyOS (鴻蒙)中,提供了一種「微信小程序」式的跨平臺開發框架,經過 Toolkit 將應用代碼編譯打包成 JS Bundle,解析並生成原生 UI 組件。node
按照入門文檔,很容易就能跑通 demo,惟一須要注意的是彈出網頁登陸時用 chrome 瀏覽器可能沒法成功:chrome
JS 應用框架部分的代碼主要在 ace_lite_jsfwk 倉庫 中,其模塊組成以下圖所示:小程序
其中爲了實現聲明式 API 開發中的單向數據綁定機制,在 ace_lite_jsfwk 代碼倉庫的 packages/runtime-core/src 目錄中實現了一個 ViewModel 類來完成數據劫持。微信小程序
這部分的代碼整體上並不複雜,在國內開發社區已經很習慣 Vue.js 和微信小程序開發的狀況下,雖有不得已而爲之的倉促,但也算水到渠成的用一套清晰的開源方案實現了相似的開發體驗,也爲更普遍的開發者快速入場豐富 HarmonyOS 生態開了個好頭。數組
本文範圍侷限在 ace_lite_jsfwk 代碼倉庫中,且主要談論 JS 部分。爲敘述方便,對私有方法/做用域內部函數等名詞不作嚴格區分。瀏覽器
ViewModel 類緩存
packages/runtime-core/src/core/index.js
構造函數微信
主要工做就是依次解析惟一參數 options 中的屬性字段:app
- 對於 options.render,賦值給 vm.$render 後,在運行時交與「JS 應用框架」層的 C++ 代碼生成的原生 UI 組件,並由其渲染方法調用:
// src/core/context/js_app_context.cpp jerry_value_t JsAppContext::Render(jerry_value_t viewModel) const { // ATTR_RENDER 即 vm.$render 方法 jerry_value_t renderFunction = jerryx_get_property_str(viewModel, ATTR_RENDER); jerry_value_t nativeElement = CallJSFunction(renderFunction, viewModel, nullptr, 0); return nativeElement; }
- 對於 options.styleSheet,也是直接把樣式丟給由 src/core/stylemgr/app_style_manager.cpp 定義的 C++ 類 AppStyleManager 去處理
- 對於 options 中其餘的自定義方法,直接綁定到 vm 上
else if (typeof value === 'function') { vm[key] = value.bind(vm); }
options.data
一樣在構造函數中,對於最主要的 options.data,作了兩項處理:框架
- 首先,遍歷 data 中的屬性字段,經過 Object.defineProperty 代理 vm 上對應的每一個屬性, 使得對 vm.foo = 123 這樣的操做其實是背後 options.data.foo 的代理:
/** * proxy data * @param {ViewModel} target - 即 vm 實例 * @param {Object} source - 即 data * @param {String} key - data 中的 key */ function proxy(target, source, key) { Object.defineProperty(target, key, { enumerable: false, configurable: true, get() { return source[key]; }, set(value) { source[key] = value; } }); }
- 其次,經過 Subject.of(data) 將 data 註冊爲被觀察的對象,具體邏輯後面會解釋。
組件的 $watch 方法
做爲文檔中惟一說起的組件「事件方法」,和 $render() 及組件生命週期等方法同樣,也是直接由 C++ 實現。除了能夠在組件實例中顯式調用 this.$watch,組件渲染過程當中也會自動觸發,好比處理屬性時的調用順序:
- Component::Render()
- Component::ParseOptions()
- 在 Component::ParseAttrs(attrs) 中求出 newAttrValue = ParseExpression(attrKey, attrValue)
- ParseExpression 的實現爲:
// src/core/components/component.cpp /** * check if the pass-in attrValue is an Expression, if it is, calculate it and bind watcher instance. * if it's not, just return the passed-in attrValue itself. */ jerry_value_t Component::ParseExpression(jerry_value_t attrKey, jerry_value_t attrValue) { jerry_value_t options = jerry_create_object(); JerrySetNamedProperty(options, ARG_WATCH_EL, nativeElement_); JerrySetNamedProperty(options, ARG_WATCH_ATTR, attrKey); jerry_value_t watcher = CallJSWatcher(attrValue, WatcherCallbackFunc, options); jerry_value_t propValue = UNDEFINED; if (IS_UNDEFINED(watcher) || jerry_value_is_error(watcher)) { HILOG_ERROR(HILOG_MODULE_ACE, "Failed to create Watcher instance."); } else { InsertWatcherCommon(watchersHead_, watcher); propValue = jerryx_get_property_str(watcher, "_lastValue"); } jerry_release_value(options); return propValue; }
在上面的代碼中,經過 InsertWatcherCommon 間接實例化一個 Watcher: Watcher *node = new Watcher()
// src/core/base/js_fwk_common.h struct Watcher : public MemoryHeap { ACE_DISALLOW_COPY_AND_MOVE(Watcher); Watcher() : watcher(jerry_create_undefined()), next(nullptr) {} jerry_value_t watcher; struct Watcher *next; };
// src/core/base/memory_heap.cpp void *MemoryHeap::operator new(size_t size) { return ace_malloc(size); }
經過 ParseExpression 中的 propValue = jerryx_get_property_str(watcher, "_lastValue") 一句,結合 JS 部分 ViewModel 類的源碼可知,C++ 部分的 watcher 概念對應的正是 JS 中的 observer:
// packages/runtime-core/src/core/index.js ViewModel.prototype.$watch = function(getter, callback, meta) { return new Observer(this, getter, callback, meta); };
下面就來看看 Observer 的實現。
Observer 觀察者類
packages/runtime-core/src/observer/observer.js
構造函數和 update()
主要工做就是將構造函數的幾個參數存儲爲實例私有變量,其中
- _ctx 上下文變量對應的就是一個要觀察的 ViewModel 實例,參考上面的 $watch 部分代碼
- 一樣,_getter、_fn、_meta 也對應着 $watch 的幾個參數
- 構造函數的最後一句是 this._lastValue = this._get(),這就涉及到了 _lastValue 私有變量、_get() 私有方法,並引出了與之相關的 update() 實例方法等幾個東西。
- 顯然,對 _lastValue 的首次賦值是在構造函數中經過 _get() 的返回值完成的:
Observer.prototype._get = function() { try { ObserverStack.push(this); return this._getter.call(this._ctx); } finally { ObserverStack.pop(); } };
稍微解釋一下這段乍看有些恍惚的代碼 -- 按照 ECMAScript Language 官方文檔中的規則,簡單來講就是會按照 「執行 try 中 return 以前的代碼」 --> 「執行並緩存 try 中 return 的代碼」 --> 「執行 finally 中的代碼」 --> 「返回緩存的 try 中 return 的代碼」 的順序執行:
好比有以下代碼:
let _str = ''; function Abc() {} Abc.prototype.hello = function() { try { _str += 'try'; return _str + 'return'; } catch (ex) { console.log(ex); } finally { _str += 'finally'; } }; const abc = new Abc(); const result = abc.hello(); console.log('[result]', result, _str);
輸出結果爲:
[result] tryreturn tryfinally
瞭解這個概念就行了,後面咱們會在運行測試用例時看到更具體的效果。
- 其後,_lastValue 再次被賦值就是在 update() 中完成的了:
Observer.prototype.update = function() { const lastValue = this._lastValue; const nextValue = this._get(); const context = this._ctx; const meta = this._meta; if (nextValue !== lastValue || canObserve(nextValue)) { this._fn.call(context, nextValue, lastValue, meta); this._lastValue = nextValue; } };
// packages/runtime-core/src/observer/utils.js export const canObserve = target => typeof target === 'object' && target !== null;
邏輯簡單清晰,對新舊值作比較,並取出 context/meta 等一併給組件中傳入等 callback 調用。
新舊值的比較就是用很典型的辦法,也就是通過判斷後可被觀察的 Object 類型對象,直接用 !== 嚴格相等性比較,一樣,這由 JS 自己按照 ECMAScript Language 官方文檔中的相關計算方法執行就行了:
# 7.2.13 SameValueNonNumeric ( x, y ) ... 8. If x and y are the same Object value, return true. Otherwise, return false.
另外咱們能夠了解到,該 update() 方法只有 Subject 實例會調用,這個一樣放到後面再看。
訂閱/取消訂閱
Observer.prototype.subscribe = function(subject, key) { const detach = subject.attach(key, this); if (typeof detach !== 'function') { return void 0; } if (!this._detaches) { this._detaches = []; } this._detaches.push(detach); };
- 經過 subject.attach(key, this) 記錄當前 observer 實例
- 上述調用返回一個函數並暫存在 observer 實例自己的 _detaches 數組中,用以在未來取消訂閱
Observer.prototype.unsubscribe = function() { const detaches = this._detaches; if (!detaches) { return void 0; } while (detaches.length) { detaches.pop()(); // 注意此處的當即執行 } };
unsubscribe 的邏輯就很天然了,執行動做的同時,也會影響到 observer/subject 中各自的私有數組。
順便查詢一下可知,只有 Subject 類裏面的一處調用了訂閱方法:
通過了上面這些分析,Subject 類的邏輯也呼之欲出。
Subject 被觀察主體類
packages/runtime-core/src/observer/subject.js
做者:Whyalone
想了解更多內容,請訪問: 51CTO和華爲官方戰略合做共建的鴻蒙技術社區https://harmonyos.51cto.com/