知識(shí)
不管是網(wǎng)站,軟件還是小程序,都要直接或間接能為您產(chǎn)生價(jià)值,我們?cè)谧非笃湟曈X表現(xiàn)的同時(shí),更側(cè)重于功能的便捷,營(yíng)銷的便利,運(yùn)營(yíng)的高效,讓網(wǎng)站成為營(yíng)銷工具,讓軟件能切實(shí)提升企業(yè)內(nèi)部管理水平和效率。優(yōu)秀的程序?yàn)楹笃谏?jí)提供便捷的支持!
您當(dāng)前位置>首頁(yè) » 新聞資訊 » 小程序相關(guān) >
人人都能看懂的鴻蒙 “JS 小程序” 數(shù)據(jù)綁定原理
發(fā)表時(shí)間:2021-1-5
發(fā)布人:葵宇科技
瀏覽次數(shù):52
在幾天前開源的華為 HarmonyOS (鴻蒙)中,提供了一種“微信小程序”式的跨平臺(tái)開發(fā)框架,通過 Toolkit 將應(yīng)用代碼編譯打包成 JS Bundle,解析并生成原生 UI 組件。
按照 :point_right: 入門文檔,很容易就能跑通 demo, 唯一需要注意的是彈出網(wǎng)頁(yè)登錄時(shí)用 chrome 瀏覽器可能無法成功 :
:point_right: JS 應(yīng)用框架部分的代碼主要在 :point_right: ace_lite_jsfwk 倉(cāng)庫(kù) 中,其模塊組成如下圖所示:
其中為了實(shí)現(xiàn)聲明式 API 開發(fā)中的單向數(shù)據(jù)綁定機(jī)制,在 ace_lite_jsfwk
代碼倉(cāng)庫(kù)的 packages/runtime-core/src
目錄中實(shí)現(xiàn)了一個(gè) ViewModel 類來完成數(shù)據(jù)劫持。
這部分的代碼總體上并不復(fù)雜,在國(guó)內(nèi)開發(fā)社區(qū)已經(jīng)很習(xí)慣 Vue.js 和微信小程序開發(fā)的情況下,雖有不得已而為之的倉(cāng)促,但也算水到渠成的用一套清晰的開源方案實(shí)現(xiàn)了類似的開發(fā)體驗(yàn),也為更廣泛的開發(fā)者快速入場(chǎng)豐富 HarmonyOS 生態(tài)開了個(gè)好頭。
本文范圍局限在 ace_lite_jsfwk
代碼倉(cāng)庫(kù)中,且主要談?wù)?JS 部分。為敘述方便,對(duì)私有方法/作用域內(nèi)部函數(shù)等名詞不做嚴(yán)格區(qū)分。
ViewModel 類
packages/runtime-core/src/core/index.js
構(gòu)造函數(shù)
主要工作就是依次解析唯一參數(shù) options 中的屬性字段:
-
對(duì)于 options.render,賦值給
vm.$render
后,在運(yùn)行時(shí)交與“JS 應(yīng)用框架”層的 C++ 代碼生成的原生 UI 組件,并由其渲染方法調(diào)用:
// 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;
}
-
對(duì)于 options.styleSheet,也是直接把樣式丟給由
src/core/stylemgr/app_style_manager.cpp
定義的 C++ 類 AppStyleManager 去處理 -
對(duì)于 options 中其他的自定義方法,直接綁定到 vm 上
else if (typeof value === 'function') {
vm[key] = value.bind(vm);
}
options.data
同樣在構(gòu)造函數(shù)中,對(duì)于最主要的 options.data,做了兩項(xiàng)處理:
-
首先,遍歷 data 中的屬性字段,通過 Object.defineProperty 代理 vm 上對(duì)應(yīng)的每個(gè)屬性 , 使得對(duì)
vm.foo = 123
這樣的操作實(shí)際上是背后 options.data.foo 的代理:
/**
* proxy data
* @param {ViewModel} target - 即 vm 實(shí)例
* @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 注冊(cè)為被觀察的對(duì)象,具體邏輯后面會(huì)解釋。
組件的 $watch 方法
作為文檔中唯一提及的組件“事件方法”,和 $render()
及組件生命周期等方法一樣,也是直接由 C++ 實(shí)現(xiàn)。除了可以在組件實(shí)例中顯式調(diào)用 this.$watch
,組件渲染過程中也會(huì)自動(dòng)觸發(fā),比如處理屬性時(shí)的調(diào)用順序:
-
Component::Render()
-
Component::ParseOptions()
-
在
Component::ParseAttrs(attrs)
中求出newAttrValue = http://www.wxapp-union.com/ParseExpression(attrKey, attrValue)
-
ParseExpression 的實(shí)現(xiàn)為:
// 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;
}
在上面的代碼中,通過 InsertWatcherCommon 間接實(shí)例化一個(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 中的 propValue = http://www.wxapp-union.com/jerryx_get_property_str(watcher,"_lastValue")
一句,結(jié)合 JS 部分 ViewModel 類的源碼可知,C++ 部分的 watcher 概念對(duì)應(yīng)的正是 JS 中的 observer:
// packages/runtime-core/src/core/index.js
ViewModel.prototype.$watch = function(getter, callback, meta) {
return new Observer(this, getter, callback, meta);
};
下面就來看看 Observer 的實(shí)現(xiàn)。
Observer 觀察者類
packages/runtime-core/src/observer/observer.js
構(gòu)造函數(shù)和 update()
主要工作就是將構(gòu)造函數(shù)的幾個(gè)參數(shù)存儲(chǔ)為實(shí)例私有變量,其中
-
_ctx
上下文變量對(duì)應(yīng)的就是一個(gè)要觀察的 ViewModel 實(shí)例,參考上面的 $watch 部分代碼 -
_getter _fn _meta
構(gòu)造函數(shù)的最后一句是 this._lastValue = http://www.wxapp-union.com/this._get()
,這就涉及到了 _lastValue 私有變量 、 _get() 私有方法 ,并引出了與之相關(guān)的 update() 實(shí)例方法 等幾個(gè)東西。
-
顯然,對(duì)
_lastValue
的首次賦值是在構(gòu)造函數(shù)中通過_get()
的返回值完成的:
Observer.prototype._get = function() {
try {
ObserverStack.push(this);
return this._getter.call(this._ctx);
} finally {
ObserverStack.pop();
}
};
稍微解釋一下這段乍看有些恍惚的代碼 -- 按照 :point_right: ECMAScript Language 官方文檔中的規(guī)則,簡(jiǎn)單來說就是會(huì)按照 “執(zhí)行 try 中 return 之前的代碼” --> “執(zhí)行并緩存 try 中 return 的代碼” --> “執(zhí)行 finally 中的代碼” --> “返回緩存的 try 中 return 的代碼” 的順序執(zhí)行:
比如有如下代碼:
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);
輸出結(jié)果為:
[result] tryreturn tryfinally
了解這個(gè)概念就好了,后面我們會(huì)在運(yùn)行測(cè)試用例時(shí)看到更具體的效果。
-
其后,
_lastValue
再次被賦值就是在update()
中完成的了:
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;
邏輯簡(jiǎn)單清晰,對(duì)新舊值做比較,并取出 context/meta 等一并給組件中傳入等 callback 調(diào)用。
新舊值的比較就是用很典型的辦法,也就是經(jīng)過判斷后可被觀察的 Object 類型對(duì)象,直接用 !==
嚴(yán)格相等性比較,同樣,這由 JS 本身按照 :point_right: ECMAScript Language 官方文檔中的相關(guān)計(jì)算方法執(zhí)行就好了:
# 7.2.13 SameValueNonNumeric ( x, y )
...
8. If x and y are the same Object value, return true. Otherwise, return false.
另外我們可以了解到,該 update()
方法只有 Subject 實(shí)例會(huì)調(diào)用,這個(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);
};
-
通過
subject.attach(key, this)
記錄當(dāng)前 observer 實(shí)例 -
上述調(diào)用返回一個(gè)函數(shù)并暫存在 observer 實(shí)例本身的
_detaches
數(shù)組中,用以在將來取消訂閱
Observer.prototype.unsubscribe = function() {
const detaches = this._detaches;
if (!detaches) {
return void 0;
}
while (detaches.length) {
detaches.pop()(); // 注意此處的立即執(zhí)行
}
};
unsubscribe 的邏輯就很自然了,執(zhí)行動(dòng)作的同時(shí),也會(huì)影響到 observer/subject 中各自的私有數(shù)組。
順便查詢一下可知,只有 Subject 類里面的一處調(diào)用了訂閱方法:
經(jīng)過了上面這些分析,Subject 類的邏輯也呼之欲出。
Subject 被觀察主體類
packages/runtime-core/src/observer/subject.js
Subject.of() 和構(gòu)造函數(shù)
正如在 ViewModel 構(gòu)造函數(shù)中最后部分看到的,用靜態(tài)方法 Subject.of() 在事實(shí)上提供 Subject 類的實(shí)例化 -- 此方法只是預(yù)置了一些可行性檢測(cè)和防止對(duì)同一目標(biāo)重復(fù)實(shí)例化等處理。
真正的構(gòu)造函數(shù)完成兩項(xiàng)主要任務(wù):
-
將 subject 實(shí)例本身指定到 目標(biāo)(也就是 ViewModel 實(shí)例化時(shí)的 options.data) 的一個(gè)私有屬性(即
data["__ob__"]
)上 -
調(diào)用私有方法 hijack(),再次(第一次是在 ViewModel 構(gòu)造函數(shù)中)遍歷目標(biāo) data 中的屬性,而這主要是為了
-
在 getter 中觸發(fā)棧頂(也就是
ObserverStack.top()
)的 observer 的訂閱 -
在 setter 中通過 notify() 方法通知所有訂閱了此屬性的 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);
},
});
}
當(dāng)然邏輯中還考慮了嵌套數(shù)據(jù)的情況,并對(duì)數(shù)組方法做了特別的劫持,這些不展開說了。
attach(key, observer) 函數(shù)
-
subject 對(duì)象的
_obsMap
對(duì)象中,每個(gè) key 持有一個(gè)數(shù)組保存訂閱該 key 的 observer 們 -
正如前面在 Observer 的訂閱方法中所述,傳入的 observer 實(shí)例按 key 被推入
_obsMap
對(duì)象中的子數(shù)組里 -
返回一個(gè)和傳入 observer 實(shí)例對(duì)應(yīng)的取消訂閱方法,供 observer.unsubscribe() 調(diào)用
notify() 函數(shù)
Subject.prototype.notify = function (key) {
...
this._obsMap[key].forEach((observer) => observer.update());
};
唯一做的其實(shí)就是構(gòu)造函數(shù)中分析的,在被劫持屬性 setter 被觸發(fā)時(shí)調(diào)用每個(gè) observer.update()
。
ObserverStack 觀察者棧對(duì)象
packages/runtime-core/src/observer/utils.js
在 Observer/Subject 的介紹中,已經(jīng)反復(fù)提及過 ObserverStack 對(duì)象,再次確認(rèn),也的確就是被這兩個(gè)類的實(shí)例引用過:
ObserverStack 對(duì)象作為 observer 實(shí)例動(dòng)態(tài)存放的地方,并以此成為每次 get 數(shù)據(jù)時(shí)按序執(zhí)行 watcher 的媒介。其實(shí)現(xiàn)也平平無奇非常簡(jiǎn)單:
export const ObserverStack = {
stack: [],
push(observer) {
this.stack.push(observer);
},
pop() {
return this.stack.pop();
},
top() { // 實(shí)際上是將數(shù)組“隊(duì)尾”當(dāng)作棧頂方向的
return this.stack[this.stack.length - 1];
}
};
理解 VM 執(zhí)行過程
光說不練假把式,光練不說傻把式, 連工帶料,連盒兒帶藥,您吃了我的大力丸,甭管你讓刀砍著、斧剁著、車軋著、馬趟著、牛頂著、狗咬著、鷹抓著、鴨子踢著 下面我們就插入適當(dāng)?shù)淖⑨?,并?shí)際運(yùn)行一個(gè)自帶的測(cè)試用例,來看看這部分實(shí)際的執(zhí)行效果:
// 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();
});
運(yùn)行結(jié)果:
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
總結(jié)
在 runtime-core 中,用非常簡(jiǎn)單而不失巧妙的代碼,完成了 ViewModel 類最基礎(chǔ)的功能,為響應(yīng)式開發(fā)提供了比較完整的基本支持。
相關(guān)案例查看更多
相關(guān)閱讀
- Web開發(fā)框架
- 云南網(wǎng)站建設(shè)專業(yè)品牌
- 云南網(wǎng)站開發(fā)哪家好
- 小程序用戶登錄
- 云南小程序開發(fā)費(fèi)用
- 出入小程序
- 微分銷
- 小程序分銷商城
- web開發(fā)
- 楚雄網(wǎng)站建設(shè)公司
- 汽車拆解管理系統(tǒng)
- 北京小程序制作
- 小程序模板開發(fā)公司
- 百度小程序開發(fā)
- 云南軟件定制公司
- 云南網(wǎng)站建設(shè)首頁(yè)
- 海報(bào)插件
- 昆明小程序開發(fā)
- 云南網(wǎng)站建設(shè)靠譜公司
- 網(wǎng)站收錄
- 云南網(wǎng)站建設(shè)首選公司
- 全國(guó)前十名小程序開發(fā)公司
- 昆明網(wǎng)站設(shè)計(jì)
- 網(wǎng)站建設(shè)選
- 智慧農(nóng)貿(mào)市場(chǎng)
- 專業(yè)網(wǎng)站建設(shè)公司
- 昆明網(wǎng)站制作
- 云南小程序制作
- 報(bào)廢車回收管理軟件
- 網(wǎng)站建設(shè)特性