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

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


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


其中为了实现声明式 API 开发中的单向数据绑定机制,在 ace_lite_jsfwk 代码仓库的 packages/runtime-core/src 目录中实现了一个 ViewModel 类来完成数据劫持。

这部分的代码总体上并不复杂,在国内开发社区已经很习惯 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 上

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. (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.logtest 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.logtest 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 totalTests:       16 skipped, 1 passed, 17 totalSnapshots:   0 totalTime:        1.309 s

总结

在 runtime-core 中,用非常简单而不失巧妙的代码,完成了 ViewModel 类最基础的功能,为响应式开发提供了比较完整的基本支持。

参考资料

  • ?OpenHarmony开发者文档 - JS应用开发框架
  • ?JS API 参考 - 文件组织
  • ?逐行分析鸿蒙系统的 JavaScript 框架
  • ?全面梳理JS对象的访问控制及代理反射

--End--

查看更多前端好文
请搜索 fewelife 关注公众号转载请注明出处

js src 变量_人人都能看懂的鸿蒙 “JS 小程序” 数据绑定原理相关推荐

  1. em算法 实例 正态分布_人人都能看懂的EM算法推导

    ↑ 点击蓝字 关注极市平台作者丨August@知乎(已授权)来源丨https://zhuanlan.zhihu.com/p/36331115编辑丨极市平台 极市导读 EM算法到底是什么,公式推导怎么去 ...

  2. dns迭代查询配置_人人都能看懂-关于dns服务基本知识

    一.DNS: Domain Name Service 应用层协议(C/S,53/udp, 53/tcp) 域名又称网域,是由一串用点分隔的名字组成的上某一台或计算机组的名称,用于在数据传输时对计算机的 ...

  3. 人人都能看懂的Spring源码解析,Spring如何解决循环依赖

    人人都能看懂的Spring源码解析,Spring如何解决循环依赖 原理解析 什么是循环依赖 循环依赖会有什么问题? 如何解决循环依赖 问题的根本原因 如何解决 为什么需要三级缓存? Spring的三级 ...

  4. 人人都能看懂LSTM

    这是在看了台大李宏毅教授的深度学习视频之后的一点总结和感想.看完介绍的第一部分RNN尤其LSTM的介绍之后,整个人醍醐灌顶.本篇博客就是对视频的一些记录加上了一些个人的思考. 0. 从RNN说起 循环 ...

  5. 人人都能看懂的LSTMGRU

    看过的讲的最简单明了的: LSTM:人人都能看懂的LSTM GRU:人人都能看懂的GRU 自己对LSTM的理解与代码解释:https://blog.csdn.net/Strive_For_Future ...

  6. 人人都能看懂的Spring底层原理,看完绝对不会懵逼

    人人都能看懂的Spring原理,绝对不会懵逼 为什么要使用Spring? Spring的核心组件 Spring是如何实现IOC和DI的? 定义了BeanDefinition 扫描加载BeanDefin ...

  7. 语言线性拟合线对称_文科生都能看懂的机器学习教程:梯度下降、线性回归、逻辑回归...

    [新智元导读]虽然在Coursera.MIT.UC伯克利上有很多机器学习的课程,包括吴恩达等专家课程已非常经典,但都是面向有一定理科背景的专业人士.本文试图将机器学习这本深奥的课程,以更加浅显易懂的方 ...

  8. 人人都能看懂的EM算法推导

    作者丨August@知乎(已授权) 来源丨https://zhuanlan.zhihu.com/p/36331115 编辑丨极市平台 估计有很多入门机器学习的同学在看到EM算法的时候会有种种疑惑:EM ...

  9. 黑苹果电池电量补丁_小白都能看懂的DSDT电量显示补丁教程

    [TOC] 简介 知识储备DSDT 正则匹配(不懂就依葫芦画瓢) 背景 由于普通PC的电池设备并不兼容与苹果的SMbus设备,所以,对于黑苹果,只能够通过ACPI来获取电池状态.为了解决电量显示我可是 ...

  10. java基础代码实例_全网都在找的Python简单基础小程序的实例代码

    这篇文章主要介绍了Python简单基础小程序的实例代码,非常不错,具有一定的参考借鉴价值 ,需要的朋友可以参考下. 1 九九乘法表 3 4 5 6 7for i in range(9):#从0循环到8 ...

最新文章

  1. Mysql分页order by数据错乱重复
  2. Spring 的@Scheduled注解实现定时任务运行和调度
  3. k8s源码架构目录分析
  4. Cisco OSPF常见问题
  5. RocketMQ-初体验RocketMQ(07)-使用API操作RocketMQ_顺序消息 ordermessage
  6. R语言:expand.grid() 函数解析
  7. Andriod开发 --插件安装、环境配置、问题集锦
  8. python识别pdf文字_Python 神工具包!翻译、文字识别、语音转文字统统搞定
  9. 三星U-Boot-1.1.6源码分析lowlevel_init.S (board\samsung\smdk6410)
  10. UIcollectionView 加入尾部视图
  11. 成为编程高手的八大奥秘
  12. 【经验】聊自己非计算机专业做程序员的经验
  13. 之前做设计收集的部分网站
  14. NCS初探--基于nRF5340的blinky
  15. (附源码)springboot学生宿舍管理系统 毕业设计 211955
  16. 《第五项修炼》读书笔记
  17. ROS远程连接Turtlebot3并进行简单的移动控制
  18. eclipse -javaEE 和jdk版本对应
  19. 人脸识别系列(十七):ArcFace/Insight Face
  20. 收集的一些GIS数据网站

热门文章

  1. 181212每日一句
  2. Atitit 锁的不同层级 app锁 vm锁 os锁 硬件锁 目录 1. 在硬件层面,CPU提供了原子操作、关中断、锁内存总线的机制 1 1.1. test and set指令 1 1.2. 锁内
  3. Atitit.软件开发概念说明--io系统区--特殊文件名称保存最佳实践文件名称编码...filenameEncode 1.1. 不个网页title保存成个个文件的时候儿有无效字符的问题... 1
  4. Atitit mybatis快速开发 的sql api接口
  5. paip.软件版本完善计划VC423
  6. WSL:ssh 本地与阿里云数据交互
  7. 流程机器人 RPA:AI落地的接盘侠 | 甲子光年
  8. (转liigo)Rust 1.0发布一周年,发展回顾与总结
  9. Julia: Beginning deep learning with 500 lines of Julia
  10. 大搜车:云上多地域高可用消息系统的构建 | 凌云时刻