人人都能看懂的鸿蒙 “JS 小程序” 数据绑定原理 | 解读鸿蒙源码

简介: 人人都能看懂的鸿蒙 “JS 小程序” 数据绑定原理 | 解读鸿蒙源码

在几天开源的华为 HarmonyOS (鸿蒙)中,提供了一种“微信小程序”式的跨平台开发框架,通过 Toolkit 将应用代码编译打包成 JS Bundle,解析并生成原生 UI 组件。

按照入门文档,很容易就能跑通 demo,唯一需要注意的是弹出网页登录时用 chrome 浏览器可能无法成功

image.png

JS 应用框架部分的代码主要在 ace_lite_jsfwk 仓库 中,其模块组成如下图所示:

image.png

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,组件渲染过程中也会自动触发,比如处理属性时的调用顺序:

  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:

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


# 7.2.13 SameValueNonNumeric ( x, y )
...
8. If x and y are the same Object value, return true. Otherwise, return false.

另外我们可以了解到,该 update() 方法只有 Subject 实例会调用,这个同样放到后面再看。

image.png

订阅/取消订阅


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 类里面的一处调用了订阅方法:

image.png

经过了上面这些分析,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 对象,再次确认,也的确就是被这两个类的实例引用过:

image.png

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 类最基础的功能,为响应式开发提供了比较完整的基本支持。

参考资料



相关文章
|
18天前
|
机器学习/深度学习 JavaScript 前端开发
JS进阶教程:递归函数原理与篇例解析
通过对这些代码示例的学习,我们已经了解了递归的原理以及递归在JS中的应用方法。递归虽然有着理论升华,但弄清它的核心思想并不难。举个随手可见的例子,火影鸣人做的影分身,你看到的都是同一个鸣人,但他们的行为却能在全局产生影响,这不就是递归吗?雾里看花,透过其间你或许已经深入了递归的魅力之中。
68 19
|
2月前
|
小程序 Java 关系型数据库
weixin163基于微信小程序的校园二手交易平台系统设计与开发ssm(文档+源码)_kaic
本文介绍了一款基于微信小程序的校园二手物品交易平台的开发与实现。该平台采用Java语言开发服务端,使用MySQL数据库进行数据存储,前端以微信小程序为载体,支持管理员和学生两种角色操作。管理员可管理用户、商品分类及信息、交易记录等,而学生则能注册登录、发布购买商品、参与交流论坛等。系统设计注重交互性和安全性,通过SSM框架优化开发流程,确保高效稳定运行,满足用户便捷交易的需求,推动校园资源共享与循环利用。
|
3月前
|
小程序 Java 关系型数据库
weixin116大学生就业平台微信小程序+ssm(文档+源码)_kaic
本文介绍了一款大学生就业平台微信小程序的开发过程,涵盖开发环境、系统设计、实现与测试等方面。该小程序基于微信平台特性,采用MYSQL数据库存储数据,确保系统稳定与安全,同时满足学生、企业和管理员不同权限用户的功能需求。通过简化操作流程,实现了招聘信息查看、简历投递等实用功能,旨在为用户提供便捷高效的求职体验,符合“操作简单,功能实用”的设计理念。
|
2月前
|
小程序 关系型数据库 Java
weixin168“返家乡”高校暑期社会实践微信小程序设计与开发ssm(文档+源码)_kaic
本文探讨高校暑期社会实践微信小程序的开发与应用,旨在通过信息化手段提升活动管理效率。借助微信小程序技术、SSM框架及MySQL数据库,实现信息共享、流程规范和操作便捷。系统涵盖需求分析、可行性研究、设计实现等环节,确保技术可行、操作简便且经济合理。最终,该小程序可优化活动发布、学生信息管理和心得交流等功能,降低管理成本并提高工作效率。
|
3月前
|
小程序 Java 关系型数据库
weixin030英语学习交流平台小程序+ssm(文档+源码)_kaic
本文介绍了英语学习交流平台小程序的开发全过程,包括系统分析、设计与实现。该小程序基于Java的SSM框架进行后端管理开发,使用MySQL作为数据库,并借助微信开发者工具确保系统稳定性。小程序设有管理员和用户两个角色,功能涵盖个人中心、每日打卡、学习计划、论坛交流等,具有操作简单、界面清晰、功能齐全的特点。通过技术可行性、经济可行性和操作可行性分析,证明了系统的实用性和高效性,为英语学习者提供了一个便捷的交流平台。
|
4月前
|
小程序 JavaScript Java
基于SpringBoot的智慧停车场微信小程序源码分享
智慧停车场微信小程序主要包含管理端和小程序端。管理端包括停车场管理,公告信息管理,用户信息管理,预定信息管理,用户反馈管理等功能。小程序端包括登录注册,预约停车位,停车导航,停车缴费,用户信息,车辆信息,钱包充值,意见反馈等功能。
193 5
基于SpringBoot的智慧停车场微信小程序源码分享
|
3月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
3月前
|
JavaScript 前端开发 Java
深入理解 JavaScript 中的 Array.find() 方法:原理、性能优势与实用案例详解
Array.find() 是 JavaScript 数组方法中一个非常实用和强大的工具。它不仅提供了简洁的查找操作,还具有性能上的独特优势:返回的引用能够直接影响原数组的数据内容,使得数据更新更加高效。通过各种场景的展示,我们可以看到 Array.find() 在更新、条件查找和嵌套结构查找等场景中的广泛应用。 在实际开发中,掌握 Array.find() 的特性和使用技巧,可以让代码更加简洁高效,特别是在需要直接修改原数据内容的情形。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一
|
3月前
|
监控 JavaScript 前端开发
MutationObserver详解+案例——深入理解 JavaScript 中的 MutationObserver:原理与实战案例
MutationObserver 是一个非常强大的 API,提供了一种高效、灵活的方式来监听和响应 DOM 变化。它解决了传统 DOM 事件监听器的诸多局限性,通过异步、批量的方式处理 DOM 变化,大大提高了性能和效率。在实际开发中,合理使用 MutationObserver 可以帮助我们更好地控制 DOM 操作,提高代码的健壮性和可维护性。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
MutationObserver详解+案例——深入理解 JavaScript 中的 MutationObserver:原理与实战案例
|
4月前
|
开发框架 监控 JavaScript
解锁鸿蒙装饰器:应用、原理与优势全解析
ArkTS提供了多维度的状态管理机制。在UI开发框架中,与UI相关联的数据可以在组件内使用,也可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,还可以在应用全局范围内传递或跨设备传递。
115 2

热门文章

最新文章