鸿蒙 “JS 小程序” 数据绑定原理详解

标签:程序,数据,原理,详解 发布时间:2021年02月28日 点击18

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

按照入门文档北京人事考试信息网,很容易就能跑通 demo,唯一必要细致的是弹出网页登录时用 chrome 欣赏器可能无法成功:

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

鸿蒙 “JS 小程序” 数据绑定原理详解

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

这部分的代码总体上并不复杂,在国内开发社区已经很风俗 Vue.js 和微信小程序开发的情况下,虽有不得已而为之的仓促,但也算水到渠成的用一套清晰的开源方案实现了类似的开发体验,也为更广泛的开发者快速入场雄厚 HarmonyOS 生态开了个好头。

本文范围局限在 ace_lite_jsfwk 代码仓库中,且重要评论辩论 JS 部分。为叙述方便,对私有方法/作用域内部函数等名词不做严酷区分。

鸿蒙 “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 方法

鸿蒙 “JS 小程序” 数据绑定原理详解

作为文档中唯一提及的组件“事件方法”,和 render()及组件生命周期等方法一样,也是直接由C++实现。除了可以在组件实例中显式调用this.render() 及组件生命周期等方法一样,也是直接由 C++ 实现。除了可以在组件实例中显式调用 this.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:

鸿蒙 “JS 小程序” 数据绑定原理详解

// packages/runtime-core/src/core/index.js

ViewModel8693totype.$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() 的返回值完成的:

    Observer8693totype._get = function() { try { ObserverStack.push(this); return this._getter.call(this._ctx); } finally { ObserverStack.pop(); } };

稍微诠释一下这段乍看有些恍惚的代码 -- 按照 ECMAScript Language 官方文档中的规则,简单来说就是会按照 “实行 try 中 return 之前的代码” --> “实行并缓存 try 中 return 的代码” --> “实行 finally 中的代码” --> “返回缓存的 try 中 return 的代码” 的顺序实行:

鸿蒙 “JS 小程序” 数据绑定原理详解比如有如下代码:

let _str = '';

function Abc() {}
Abc8693totype.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() 中完成的了:

    Observer8693totype.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 官方文档中的相干计算方法实行就好了:

鸿蒙 “JS 小程序” 数据绑定原理详解

# 7.2.13 SameValueNonNumeric ( x, y )

...

8. If x and y are the same Object value, return true. Otherwise, return false.
复制代码

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

鸿蒙 “JS 小程序” 数据绑定原理详解

订阅/取消订阅

Observer8693totype.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 数组中,用以在未来取消订阅

    Observer8693totype.unsubscribe = function() {
      const detaches = this._detaches;
      if (!detaches) {
        return void 0;
      }
      while (detaches.length) {
        detaches.pop()(); // 细致此处的立即实行
      }
    };?
    复制代码

unsubscribe 的逻辑就很天然了,实行动作的同时上海调查公司,也会影响到 observer/subject 中各自的私稀有组。

顺便查询一下可知,只有 Subject 类里面的一处调用了订阅方法:

鸿蒙 “JS 小程序” 数据绑定原理详解

经过了上面这些分析河北人事考试网,Subject 类的逻辑也呼之欲出。

Subject 被观察主体类

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

作者:HarmonyOS技术社区
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
手机网站建设