您當前位置>首頁 » 新聞資(zī)訊 » 小程序相關(guān) >
鴻蒙 “JS 小程序” 數據綁定原理詳解
發表時間:2021-1-4
發布人:葵宇科技
浏覽次數:31
在幾天前開源的華為 HarmonyOS (鴻蒙)中(zhōng),提供了一種“微信小程序”式的跨平台開發框架,通(tōng)過 Toolkit 将應用代碼編譯打包成 JS Bundle,解析并生成原生 UI 組件。
按照入門文(wén)檔,很容易就能跑通(tōng) demo,唯一需要注意的是彈出網頁登錄時用 chrome 浏覽器(qì)可(kě)能無法成功:
JS 應用框架部分的代碼主要在 ace_lite_jsfwk 倉庫 中(zhōng),其模塊組成如(rú)下(xià)圖所示:
其中(zhōng)為了實現聲明式 API 開發中(zhōng)的單向數據綁定機制,在 ace_lite_jsfwk 代碼倉庫的 packages/runtime-core/src 目錄中(zhōng)實現了一個(gè) ViewModel 類來完成數據劫持。
這部分的代碼總體上并不複雜,在國内開發社區已經很習慣 Vue.js 和(hé)微信小程序開發的情況下(xià),雖有不得已而為之的倉促,但也算水到渠成的用一套清晰的開源方案實現了類似的開發體驗,也為更廣泛的開發者快速入場豐富 HarmonyOS 生态開了個(gè)好頭。
本文(wén)範圍局限在 ace_lite_jsfwk 代碼倉庫中(zhōng),且主要談論 JS 部分。為叙述方便,對私有方法/作用域内部函數等名詞不做嚴格區分。
ViewModel 類
packages/runtime-core/src/core/index.js
複制代碼
構造函數
主要工作就是依次解析唯一參數 options 中(zhōng)的屬性字段:
-
對于 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 中(zhōng)其他的自定義方法,直接綁定到 vm 上
else if (typeof value =http://www.wxapp-union.com/=='function') { vm[key] = value.bind(vm); }
options.data
同樣在構造函數中(zhōng),對于最主要的 options.data,做了兩項處理:
-
首先,遍曆 data 中(zhōng)的屬性字段,通(tōng)過 Object.defineProperty 代理 vm 上對應的每個(gè)屬性, 使得對 vm.foo = 123 這樣的操作實際上是背後 options.data.foo 的代理:
/**
- proxy data
- @param {ViewModel} target - 即 vm 實例
- @param {Object} source - 即 data
- @param {String} key - data 中(zhōng)的 key
*/ function proxy(target, source, key) { Object.defineProperty(target, key, { enumerable: false, configurable: true, get() { return source[key]; }, set(value) { source[key] = value; } }); }
-
其次,通(tōng)過 Subject.of(data) 将 data 注冊為被觀察的對象,具體邏輯後面會解釋。
組件的 $watch 方法
作為文(wén)檔中(zhōng)唯一提及的組件“事件方法”,和(hé) watch,組件渲染過程中(zhōng)也會自動(dòng)觸發,比如(rú)處理屬性時的調用順序:
-
Component::Render()
-
Component::ParseOptions()
-
在 Component::ParseAttrs(attrs) 中(zhōng)求出 newAttrValue = http://www.wxapp-union.com/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 = http://www.wxapp-union.com/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 = http://www.wxapp-union.com/jerryx_get_property_str(watcher,"_lastValue"); } jerry_release_value(options); return propValue; }? 複制代碼
在上面的代碼中(zhōng),通(tōng)過 InsertWatcherCommon 間接實例化一個(gè) 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);
}
複制代碼
通(tōng)過 ParseExpression 中(zhōng)的 propValue = http://www.wxapp-union.com/jerryx_get_property_str(watcher,"_lastValue") 一句,結合 JS 部分 ViewModel 類的源碼可(kě)知,C++ 部分的 watcher 概念對應的正是 JS 中(zhōng)的 observer:
// packages/runtime-core/src/core/index.js
ViewModel.prototype.$watch = function(getter, callback, meta) {
return new Observer(this, getter, callback, meta);
};
複制代碼
下(xià)面就來看看 Observer 的實現。
Observer 觀察者類
packages/runtime-core/src/observer/observer.js
複制代碼
構造函數和(hé) update()
主要工作就是将構造函數的幾個(gè)參數存儲為實例私有變量,其中(zhōng)
-
_ctx 上下(xià)文(wén)變量對應的就是一個(gè)要觀察的 ViewModel 實例,參考上面的 $watch 部分代碼
-
同樣,_getter、_fn、_meta 也對應着 $watch 的幾個(gè)參數
-
構造函數的最後一句是 this._lastValue = http://www.wxapp-union.com/this._get(),這就涉及到了 _lastValue 私有變量、_get() 私有方法,并引出了與之相關(guān)的 update() 實例方法等幾個(gè)東西。
-
顯然,對 _lastValue 的首次賦值是在構造函數中(zhōng)通(tōng)過 _get() 的返回值完成的:
Observer.prototype._get = function() { try { ObserverStack.push(this); return this._getter.call(this._ctx); } finally { ObserverStack.pop(); } };
稍微解釋一下(xià)這段乍看有些恍惚的代碼 -- 按照 ECMAScript Language 官方文(wén)檔中(zhōng)的規則,簡單來說就是會按照 “執行 try 中(zhōng) return 之前的代碼” --> “執行并緩存 try 中(zhōng) return 的代碼” --> “執行 finally 中(zhōng)的代碼” --> “返回緩存的 try 中(zhōng) return 的代碼” 的順序執行:
比如(rú)有如(rú)下(xià)代碼:
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
複制代碼
了解這個(gè)概念就好了,後面我們會在運行測試用例時看到更具體的效果。
-
其後,_lastValue 再次被賦值就是在 update() 中(zhōng)完成的了:
Observer.prototype.update = function() { const lastValue = http://www.wxapp-union.com/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 等一并給組件中(zhōng)傳入等 callback 調用。
新舊值的比較就是用很典型的辦法,也就是經過判斷後可(kě)被觀察的 Object 類型對象,直接用 !== 嚴格相等性比較,同樣,這由 JS 本身按照 ECMAScript Language 官方文(wén)檔中(zhōng)的相關(guān)計算方法執行就好了:
# 7.2.13 SameValueNonNumeric ( x, y )
...
8. If x and y are the same Object value, return true. Otherwise, return false.
複制代碼
另外我們可(kě)以了解到,該 update() 方法隻有 Subject 實例會調用,這個(gè)同樣放到後面再看。
訂閱/取消訂閱
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);
};
複制代碼
-
通(tōng)過 subject.attach(key, this) 記錄當前 observer 實例
-
上述調用返回一個(gè)函數并暫存在 observer 實例本身的 _detaches 數組中(zhōng),用以在将來取消訂閱
Observer.prototype.unsubscribe = function() { const detaches = this._detaches; if (!detaches) { return void 0; } while (detaches.length) { detaches.pop()(); // 注意此處的立即執行 } };? 複制代碼
unsubscribe 的邏輯就很自然了,執行動(dòng)作的同時,也會影響到 observer/subject 中(zhōng)各自的私有數組。
順便查詢一下(xià)可(kě)知,隻有 Subject 類裡面的一處調用了訂閱方法:
經過了上面這些分析,Subject 類的邏輯也呼之欲出。
Subject 被觀察主體類
packages/runtime-core/src/observer/subject.js
作者:HarmonyOS技術(shù)社區
來源:掘金
著作權歸作者所有。商(shāng)業(yè)轉載請聯系作者獲得授權,非商(shāng)業(yè)轉載請注明出處。