人(rén)人(rén)都能看懂的(de)鴻蒙 “JS 小程序” 數據綁定原理
發表時(shí)間:2021-1-5
發布人(rén):融晨科技
浏覽次數:52
在(zài)幾天前開源的(de)華爲(wéi / wèi) HarmonyOS (鴻蒙)中,提供了(le/liǎo)一種“微信小程序”式的(de)跨平台開發框架,通過 Toolkit 将應用代碼編譯打包成 JS Bundle,解析并生成原生 UI 組件。
按照 :point_right: 入門文檔,很容易就(jiù)能跑通 demo, 唯一需要(yào / yāo)注意的(de)是(shì)彈出(chū)網頁登錄時(shí)用 chrome 浏覽器可能無法成功 :
:point_right: JS 應用框架部分的(de)代碼主要(yào / yāo)在(zài) :point_right: ace_lite_jsfwk 倉庫 中,其模塊組成如下圖所示:
其中爲(wéi / wèi)了(le/liǎo)實現聲明式 API 開發中的(de)單向數據綁定機制,在(zài) ace_lite_jsfwk
代碼倉庫的(de) packages/runtime-core/src
目錄中實現了(le/liǎo)一個(gè) ViewModel 類來(lái)完成數據劫持。
這(zhè)部分的(de)代碼總體上(shàng)并不(bù)複雜,在(zài)國(guó)内開發社區已經很習慣 Vue.js 和(hé / huò)微信小程序開發的(de)情況下,雖有不(bù)得已而(ér)爲(wéi / wèi)之(zhī)的(de)倉促,但也(yě)算水到(dào)渠成的(de)用一套清晰的(de)開源方案實現了(le/liǎo)類似的(de)開發體驗,也(yě)爲(wéi / wèi)更廣泛的(de)開發者快速入場豐富 HarmonyOS 生态開了(le/liǎo)個(gè)好頭。
本文範圍局限在(zài) ace_lite_jsfwk
代碼倉庫中,且主要(yào / yāo)談論 JS 部分。爲(wéi / wèi)叙述方便,對私有方法/作用域内部函數等名詞不(bù)做嚴格區分。
ViewModel 類
packages/runtime-core/src/core/index.js
構造函數
主要(yào / yāo)工作就(jiù)是(shì)依次解析唯一參數 options 中的(de)屬性字段:
-
對于(yú) options.render,賦值給
vm.$render
後,在(zài)運行時(shí)交與“JS 應用框架”層的(de) C++ 代碼生成的(de)原生 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;
}
-
對于(yú) options.styleSheet,也(yě)是(shì)直接把樣式丢給由
src/core/stylemgr/app_style_manager.cpp
定義的(de) C++ 類 AppStyleManager 去處理 -
對于(yú) options 中其他(tā)的(de)自定義方法,直接綁定到(dào) vm 上(shàng)
else if (typeof value === 'function') {
vm[key] = value.bind(vm);
}
options.data
同樣在(zài)構造函數中,對于(yú)最主要(yào / yāo)的(de) options.data,做了(le/liǎo)兩項處理:
-
首先,遍曆 data 中的(de)屬性字段,通過 Object.defineProperty 代理 vm 上(shàng)對應的(de)每個(gè)屬性 , 使得對
vm.foo = 123
這(zhè)樣的(de)操作實際上(shàng)是(shì)背後 options.data.foo 的(de)代理:
/**
* proxy data
* @param {ViewModel} target - 即 vm 實例
* @param {Object} source - 即 data
* @param {String} key - data 中的(de) 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 注冊爲(wéi / wèi)被觀察的(de)對象,具體邏輯後面會解釋。
組件的(de) $watch 方法
作爲(wéi / wèi)文檔中唯一提及的(de)組件“事件方法”,和(hé / huò) $render()
及組件生命周期等方法一樣,也(yě)是(shì)直接由 C++ 實現。除了(le/liǎo)可以(yǐ)在(zài)組件實例中顯式調用 this.$watch
,組件渲染過程中也(yě)會自動觸發,比如處理屬性時(shí)的(de)調用順序:
-
Component::Render()
-
Component::ParseOptions()
-
在(zài)
Component::ParseAttrs(attrs)
中求出(chū)newAttrValue = http://www.wxapp-union.com/ParseExpression(attrKey, attrValue)
-
ParseExpression 的(de)實現爲(wéi / wèi):
// 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;
}
在(zài)上(shàng)面的(de)代碼中,通過 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);
}
通過 ParseExpression 中的(de) propValue = http://www.wxapp-union.com/jerryx_get_property_str(watcher,"_lastValue")
一句,結合 JS 部分 ViewModel 類的(de)源碼可知,C++ 部分的(de) watcher 概念對應的(de)正是(shì) JS 中的(de) observer:
// packages/runtime-core/src/core/index.js
ViewModel.prototype.$watch = function(getter, callback, meta) {
return new Observer(this, getter, callback, meta);
};
下面就(jiù)來(lái)看看 Observer 的(de)實現。
Observer 觀察者類
packages/runtime-core/src/observer/observer.js
構造函數和(hé / huò) update()
主要(yào / yāo)工作就(jiù)是(shì)将構造函數的(de)幾個(gè)參數存儲爲(wéi / wèi)實例私有變量,其中
-
_ctx
上(shàng)下文變量對應的(de)就(jiù)是(shì)一個(gè)要(yào / yāo)觀察的(de) ViewModel 實例,參考上(shàng)面的(de) $watch 部分代碼 -
_getter _fn _meta
構造函數的(de)最後一句是(shì) this._lastValue = http://www.wxapp-union.com/this._get()
,這(zhè)就(jiù)涉及到(dào)了(le/liǎo) _lastValue 私有變量 、 _get() 私有方法 ,并引出(chū)了(le/liǎo)與之(zhī)相關的(de) update() 實例方法 等幾個(gè)東西。
-
顯然,對
_lastValue
的(de)首次賦值是(shì)在(zài)構造函數中通過_get()
的(de)返回值完成的(de):
Observer.prototype._get = function() {
try {
ObserverStack.push(this);
return this._getter.call(this._ctx);
} finally {
ObserverStack.pop();
}
};
稍微解釋一下這(zhè)段乍看有些恍惚的(de)代碼 -- 按照 :point_right: ECMAScript Language 官方文檔中的(de)規則,簡單來(lái)說(shuō)就(jiù)是(shì)會按照 “執行 try 中 return 之(zhī)前的(de)代碼” --> “執行并緩存 try 中 return 的(de)代碼” --> “執行 finally 中的(de)代碼” --> “返回緩存的(de) try 中 return 的(de)代碼” 的(de)順序執行:
比如有如下代碼:
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);
輸出(chū)結果爲(wéi / wèi):
[result] tryreturn tryfinally
了(le/liǎo)解這(zhè)個(gè)概念就(jiù)好了(le/liǎo),後面我們會在(zài)運行測試用例時(shí)看到(dào)更具體的(de)效果。
-
其後,
_lastValue
再次被賦值就(jiù)是(shì)在(zài)update()
中完成的(de)了(le/liǎo):
Observer.prototype.update = function() {
const lastValue = http://www.wxapp-union.com/this._lastValue;
const nextValue = http://www.wxapp-union.com/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;
邏輯簡單清晰,對新舊值做比較,并取出(chū) context/meta 等一并給組件中傳入等 callback 調用。
新舊值的(de)比較就(jiù)是(shì)用很典型的(de)辦法,也(yě)就(jiù)是(shì)經過判斷後可被觀察的(de) Object 類型對象,直接用 !==
嚴格相等性比較,同樣,這(zhè)由 JS 本身按照 :point_right: ECMAScript Language 官方文檔中的(de)相關計算方法執行就(jiù)好了(le/liǎo):
# 7.2.13 SameValueNonNumeric ( x, y )
...
8. If x and y are the same Object value, return true. Otherwise, return false.
另外我們可以(yǐ)了(le/liǎo)解到(dào),該 update()
方法隻有 Subject 實例會調用,這(zhè)個(gè)同樣放到(dào)後面再看。
訂閱/取消訂閱
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 實例 -
上(shàng)述調用返回一個(gè)函數并暫存在(zài) observer 實例本身的(de)
_detaches
數組中,用以(yǐ)在(zài)将來(lái)取消訂閱
Observer.prototype.unsubscribe = function() {
const detaches = this._detaches;
if (!detaches) {
return void 0;
}
while (detaches.length) {
detaches.pop()(); // 注意此處的(de)立即執行
}
};
unsubscribe 的(de)邏輯就(jiù)很自然了(le/liǎo),執行動作的(de)同時(shí),也(yě)會影響到(dào) observer/subject 中各自的(de)私有數組。
順便查詢一下可知,隻有 Subject 類裏面的(de)一處調用了(le/liǎo)訂閱方法:
經過了(le/liǎo)上(shàng)面這(zhè)些分析,Subject 類的(de)邏輯也(yě)呼之(zhī)欲出(chū)。
Subject 被觀察主體類
packages/runtime-core/src/observer/subject.js
Subject.of() 和(hé / huò)構造函數
正如在(zài) ViewModel 構造函數中最後部分看到(dào)的(de),用靜态方法 Subject.of() 在(zài)事實上(shàng)提供 Subject 類的(de)實例化 -- 此方法隻是(shì)預置了(le/liǎo)一些可行性檢測和(hé / huò)防止對同一目标重複實例化等處理。
真正的(de)構造函數完成兩項主要(yào / yāo)任務:
-
将 subject 實例本身指定到(dào) 目标(也(yě)就(jiù)是(shì) ViewModel 實例化時(shí)的(de) options.data) 的(de)一個(gè)私有屬性(即
data["__ob__"]
)上(shàng) -
調用私有方法 hijack(),再次(第一次是(shì)在(zài) ViewModel 構造函數中)遍曆目标 data 中的(de)屬性,而(ér)這(zhè)主要(yào / yāo)是(shì)爲(wéi / wèi)了(le/liǎo)
-
在(zài) getter 中觸發棧頂(也(yě)就(jiù)是(shì)
ObserverStack.top()
)的(de) observer 的(de)訂閱 -
在(zài) setter 中通過 notify() 方法通知所有訂閱了(le/liǎo)此屬性的(de) observer 們
/**
* observe object
* @param {any} target the object to be observed
* @param {String} key the key to be observed
* @param {any} cache the cached value
*/
function hijack(target, key, cache) {
const subject = target[SYMBOL_OBSERVABLE]; // "__ob__"
Object.defineProperty(target, key, {
enumerable: true,
get() {
const observer = ObserverStack.top();
if (observer) {
console.log('[topObserver.subscribe in Subject::hijack]');
observer.subscribe(subject, key);
}
...
return cache;
},
set(value) {
cache = value;
subject.notify(key);
},
});
}
當然邏輯中還考慮了(le/liǎo)嵌套數據的(de)情況,并對數組方法做了(le/liǎo)特别的(de)劫持,這(zhè)些不(bù)展開說(shuō)了(le/liǎo)。
attach(key, observer) 函數
-
subject 對象的(de)
_obsMap
對象中,每個(gè) key 持有一個(gè)數組保存訂閱該 key 的(de) observer 們 -
正如前面在(zài) Observer 的(de)訂閱方法中所述,傳入的(de) observer 實例按 key 被推入
_obsMap
對象中的(de)子(zǐ)數組裏 -
返回一個(gè)和(hé / huò)傳入 observer 實例對應的(de)取消訂閱方法,供 observer.unsubscribe() 調用
notify() 函數
Subject.prototype.notify = function (key) {
...
this._obsMap[key].forEach((observer) => observer.update());
};
唯一做的(de)其實就(jiù)是(shì)構造函數中分析的(de),在(zài)被劫持屬性 setter 被觸發時(shí)調用每個(gè) observer.update()
。
ObserverStack 觀察者棧對象
packages/runtime-core/src/observer/utils.js
在(zài) Observer/Subject 的(de)介紹中,已經反複提及過 ObserverStack 對象,再次确認,也(yě)的(de)确就(jiù)是(shì)被這(zhè)兩個(gè)類的(de)實例引用過:
ObserverStack 對象作爲(wéi / wèi) observer 實例動态存放的(de)地(dì / de)方,并以(yǐ)此成爲(wéi / wèi)每次 get 數據時(shí)按序執行 watcher 的(de)媒介。其實現也(yě)平平無奇非常簡單:
export const ObserverStack = {
stack: [],
push(observer) {
this.stack.push(observer);
},
pop() {
return this.stack.pop();
},
top() { // 實際上(shàng)是(shì)将數組“隊尾”當作棧頂方向的(de)
return this.stack[this.stack.length - 1];
}
};
理解 VM 執行過程
光說(shuō)不(bù)練假把式,光練不(bù)說(shuō)傻把式, 連工帶料,連盒兒帶藥,您吃了(le/liǎo)我的(de)大(dà)力丸,甭管你讓刀砍着、斧剁着、車軋着、馬趟着、牛頂着、狗咬着、鷹抓着、鴨子(zǐ)踢着 下面我們就(jiù)插入适當的(de)注釋,并實際運行一個(gè)自帶的(de)測試用例,來(lái)看看這(zhè)部分實際的(de)執行效果:
// packages/runtime-core/src/__test__/index.test.js
test.only('04_watch_basic_usage', (done) => {
const vm = new ViewModel({
data: function () {
return { count: 1 };
},
increase() {
++this.count;
},
decrease() {
--this.count;
},
});
console.log('test step 1 =========================');
expect(vm.count).toBe(1);
console.log('test step 2 =========================');
const watcher = vm.$watch(
() => vm.count,
(value) => {
expect(value).toBe(2);
watcher.unsubscribe();
done();
}
);
console.log('test step 3 =========================');
vm.increase();
});
運行結果:
PASS src/__test__/index.test.js
ViewModel
? 04_watch_basic_usage (32 ms)
○ skipped 01_proxy_data
○ skipped 02_data_type
○ skipped 03_handler
○ skipped 05_watch_nested_object
○ skipped 06_watch_array
○ skipped 07_observed_array_push
○ skipped 08_observed_array_pop
○ skipped 09_observed_array_unshift
○ skipped 10_observed_array_shift
○ skipped 11_observed_array_splice
○ skipped 12_observed_array_reverse
○ skipped 13_watch_multidimensional_array
○ skipped 14_watch_multidimensional_array
○ skipped 15_change_array_by_index
○ skipped 15_watch_object_array
○ skipped 99_lifecycle
console.log
test step 1 =========================
at Object. (src/__test__/index.test.js:66:13)
console.log
[proxy in VM] count
at ViewModel.count (src/core/index.js:102:15)
console.log
[get in Subject::hijack]
key: count,
stack length: 0
at Object.get [as count] (src/observer/subject.js:144:15)
console.log
test step 2 =========================
at Object. (src/__test__/index.test.js:68:13)
console.log
[new in Observer]
at new Observer (src/observer/observer.js:29:11)
console.log
[_get ObserverStack.push(this) in Observer]
stack length: 1
at Observer._get (src/observer/observer.js:36:13)
console.log
[proxy in VM] count
at ViewModel.count (src/core/index.js:102:15)
console.log
[get in Subject::hijack]
key: count,
stack length: 1
at Object.get [as count] (src/observer/subject.js:144:15)
console.log
[topObserver.subscribe in Subject::hijack]
at Object.get [as count] (src/observer/subject.js:151:17)
console.log
[subscribe in Observer]
key: count,
typeof detach: function
at Observer.subscribe (src/observer/observer.js:67:11)
console.log
[_get ObserverStack.pop() in Observer]
stack length: 0
at Observer._get (src/observer/observer.js:45:13)
console.log
test step 3 =========================
at Object. (src/__test__/index.test.js:77:13)
console.log
[proxy in VM] count
at ViewModel.get (src/core/index.js:102:15)
console.log
[get in Subject::hijack]
key: count,
stack length: 0
at Object.get [as count] (src/observer/subject.js:144:15)
console.log
[set in Subject::hijack]
key: count,
value: 2,
cache: 1,
stack length: 0
at Object.set [as count] (src/observer/subject.js:163:15)
console.log
[update in Observer]
at Observer.update (src/observer/observer.js:54:11)
at Array.forEach ()
console.log
[_get ObserverStack.push(this) in Observer]
stack length: 1
at Observer._get (src/observer/observer.js:36:13)
at Array.forEach ()
console.log
[proxy in VM] count
at ViewModel.count (src/core/index.js:102:15)
at Array.forEach ()
console.log
[get in Subject::hijack]
key: count,
stack length: 1
at Object.get [as count] (src/observer/subject.js:144:15)
at Array.forEach ()
console.log
[topObserver.subscribe in Subject::hijack]
at Object.get [as count] (src/observer/subject.js:151:17)
at Array.forEach ()
console.log
[subscribe in Observer]
key: count,
typeof detach: undefined
at Observer.subscribe (src/observer/observer.js:67:11)
console.log
[_get ObserverStack.pop() in Observer]
stack length: 0
at Observer._get (src/observer/observer.js:45:13)
at Array.forEach ()
Test Suites: 1 passed, 1 total
Tests: 16 skipped, 1 passed, 17 total
Snapshots: 0 total
Time: 1.309 s
總結
在(zài) runtime-core 中,用非常簡單而(ér)不(bù)失巧妙的(de)代碼,完成了(le/liǎo) ViewModel 類最基礎的(de)功能,爲(wéi / wèi)響應式開發提供了(le/liǎo)比較完整的(de)基本支持。