人人都能看懂的鴻蒙 「JS 小程序」 數據綁定原理 | 解讀鴻蒙源碼

在幾天前開源的華爲 HarmonyOS (鴻蒙)中,提供了一種「微信小程序」式的跨平臺開發框架,經過 Toolkit 將應用代碼編譯打包成 JS Bundle,解析並生成原生 UI 組件。java

按照入門文檔,很容易就能跑通 demo,惟一須要注意的是彈出網頁登陸時用 chrome 瀏覽器可能沒法成功node

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

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

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

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

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 上bash

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,組件渲染過程當中也會自動觸發,好比處理屬性時的調用順序:

  1. Component::Render()
  2. Component::ParseOptions()
  3. Component::ParseAttrs(attrs) 中求出 newAttrValue = ParseExpression(attrKey, attrValue)
  4. 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

Subject.of() 和構造函數

正如在 ViewModel 構造函數中最後部分看到的,用靜態方法 Subject.of() 在事實上提供 Subject 類的實例化 -- 此方法只是預置了一些可行性檢測和防止對同一目標重複實例化等處理。

真正的構造函數完成兩項主要任務:

  1. 將 subject 實例自己指定到 目標(也就是 ViewModel 實例化時的 options.data) 的一個私有屬性(即 data["__ob__"])上
  2. 調用私有方法 hijack(),再次(第一次是在 ViewModel 構造函數中)遍歷目標 data 中的屬性,而這主要是爲了
    • 在 getter 中觸發棧頂(也就是 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);
    },
  });
}

固然邏輯中還考慮了嵌套數據的狀況,並對數組方法作了特別的劫持,這些不展開說了。

attach(key, observer) 函數

  • subject 對象的 _obsMap 對象中,每一個 key 持有一個數組保存訂閱該 key 的 observer 們
  • 正如前面在 Observer 的訂閱方法中所述,傳入的 observer 實例按 key 被推入 _obsMap 對象中的子數組裏
  • 返回一個和傳入 observer 實例對應的取消訂閱方法,供 observer.unsubscribe() 調用

notify() 函數

Subject.prototype.notify = function (key) {
  ...
  this._obsMap[key].forEach((observer) => observer.update());
};

惟一作的其實就是構造函數中分析的,在被劫持屬性 setter 被觸發時調用每一個 observer.update()

ObserverStack 觀察者棧對象

packages/runtime-core/src/observer/utils.js

在 Observer/Subject 的介紹中,已經反覆說起過 ObserverStack 對象,再次確認,也的確就是被這兩個類的實例引用過:

ObserverStack 對象做爲 observer 實例動態存放的地方,並以此成爲每次 get 數據時按序執行 watcher 的媒介。其實現也平平無奇很是簡單:

export const ObserverStack = {
  stack: [],
  push(observer) {
    this.stack.push(observer);
  },
  pop() {
    return this.stack.pop();
  },
  top() { // 其實是將數組「隊尾」看成棧頂方向的
    return this.stack[this.stack.length - 1];
  }
};

理解 VM 執行過程

光說不練假把式,光練不說傻把式,連工帶料,連盒兒帶藥,您吃了個人大力丸,甭管你讓刀砍着、斧剁着、車軋着、馬趟着、牛頂着、狗咬着、鷹抓着、鴨子踢着 下面咱們就插入適當的註釋,並實際運行一個自帶的測試用例,來看看這部分實際的執行效果:

// 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.<anonymous> (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.<anonymous> (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.<anonymous> (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 (<anonymous>)

  console.log
    [_get ObserverStack.push(this) in Observer]
            stack length: 1

      at Observer._get (src/observer/observer.js:36:13)
          at Array.forEach (<anonymous>)

  console.log
    [proxy in VM] count

      at ViewModel.count (src/core/index.js:102:15)
          at Array.forEach (<anonymous>)

  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 (<anonymous>)

  console.log
    [topObserver.subscribe in Subject::hijack]

      at Object.get [as count] (src/observer/subject.js:151:17)
          at Array.forEach (<anonymous>)

  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 (<anonymous>)

Test Suites: 1 passed, 1 total
Tests:       16 skipped, 1 passed, 17 total
Snapshots:   0 total
Time:        1.309 s

總結

在 runtime-core 中,用很是簡單而不失巧妙的代碼,完成了 ViewModel 類最基礎的功能,爲響應式開發提供了比較完整的基本支持。

參考資料

本文參與了「解讀鴻蒙源碼」技術徵文,歡迎正在閱讀的你也加入。

相關文章
相關標籤/搜索