鴻蒙 “JS 小程序” 數據綁定原理詳解


在幾天前開源的華為 HarmonyOS (鴻蒙)中,提供了一種“微信小程序”式的跨平台開發框架,通過 Toolkit 將應用代碼編譯打包成 JS Bundle,解析並生成原生 UI 組件。 按照入門文檔,很容易就能跑通 demo,唯一需要注意的是彈出網頁登錄時用 chrome 瀏覽器可能無法成功:

image.png

JS 應用框架部分的代碼主要在 ace_lite_jsfwk 倉庫 中,其模塊組成如下圖所示:
image.png

其中為了實現聲明式 API 開發中的單向數據綁定機制,在 ace_lite_jsfwk 代碼倉庫的 packages/runtime-core/src 目錄中實現了一個 ViewModel 類來完成數據劫持。

這部分的代碼總體上並不復雜,在國內開發社區已經很習慣 Vue.js 和微信小程序開發的情況下,雖有不得已而為之的倉促,但也算水到渠成的用一套清晰的開源方案實現了類似的開發體驗,也為更廣泛的開發者快速入場豐富 HarmonyOS 生態開了個好頭。

本文范圍局限在 ace_lite_jsfwk 代碼倉庫中,且主要談論 JS 部分。為敘述方便,對私有方法/作用域內部函數等名詞不做嚴格區分。

image.png

ViewModel 類

packages/runtime-core/src/core/index.js
構造函數

主要工作就是依次解析唯一參數 options 中的屬性字段:

對於 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 方法

image.png

作為文檔中唯一提及的組件“事件方法”,和 $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:

image.png

// 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 的代碼” 的順序執行:

image.png

比如有如下代碼:

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 官方文檔中的相關計算方法執行就好了:

image.png

查看更多章節>>>

作者:Whyalone

想了解更多內容,請訪問: 51CTO和華為官方戰略合作共建的鴻蒙技術社區https://harmonyos.51cto.com/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM