首页 web前端 js教程 解剖式分析 鸿蒙系统的JavaScript框架

解剖式分析 鸿蒙系统的JavaScript框架

Sep 17, 2020 pm 05:04 PM
javascript 鸿蒙系统

解剖式分析 鸿蒙系统的JavaScript框架

相关学习推荐:javascript

我在前文中曾经介绍过鸿蒙的 Javascript 框架,这几天终于把 JS 仓库编译通过了,期间踩了不少坑,也给鸿蒙贡献了几个 PR。今天我们就来逐行分析鸿蒙系统中的 JS 框架。

文中的所有代码都基于鸿蒙的当前最新版(版本为 677ed06,提交日期为 2020-09-10)。

鸿蒙系统使用 JavaScript 开发 GUI 是一种类似于微信小程序、轻应用的模式。而这个 MVVM 模式中,V 其实是由 C++ 来承担的。JavaScript 代码只是其中的 ViewModel 层。

鸿蒙 JS 框架是零依赖的,只在开发打包过程中使用到了一些 npm 包。打包完之的代码是没有依赖任何 npm 包的。我们先看一下使用鸿蒙 JS 框架写出来的 JS 代码到底长什么样。

export default {
  data() {    return { count: 1 };
  },
  increase() {
    ++this.count;
  },
  decrease() {
    --this.count;
  },
}复制代码
登录后复制

如果我不告诉你这是鸿蒙,你甚至会以为它是 vue 或小程序。如果单独把 JS 拿出来使用(脱离鸿蒙系统),代码是这样:

const vm = new ViewModel({
  data() {    return { count: 1 };
  },
  increase() {
    ++this.count;
  },
  decrease() {
    --this.count;
  },
});console.log(vm.count); // 1vm.increase();console.log(vm.count); // 2vm.decrease();console.log(vm.count); // 1复制代码
登录后复制

仓库中的所有 JS 代码实现了一个响应式系统,充当了 MVVM 中的 ViewModel。

下面我们逐行分析。

src 目录中一共有 4 个目录,总计 8 个文件。其中 1 个是单元测试。还有 1 个性能分析。再除去 2 个 index.js 文件,有用的文件一共是 4 个。也是本文分析的重点。

src
├── __test__
│   └── index.test.js
├── core
│   └── index.js
├── index.js
├── observer
│   ├── index.js
│   ├── observer.js
│   ├── subject.js
│   └── utils.js
└── profiler
    └── index.js复制代码
登录后复制

首先是入口文件,src/index.js,只有 2 行代码:

import { ViewModel } from './core';export default ViewModel;复制代码
登录后复制

其实就是重新导出。

另一个类似的文件是 src/observer/index.js,也是 2 行代码:

export { Observer } from './observer';export { Subject } from './subject';复制代码
登录后复制

observer 和 subject 实现了一个观察者模式。subject 是主题,也就是被观察者。observer 是观察者。当 subject 有任何变化时需要主动通知被观察者。这就是响应式。

这 2 个文件都使用到了 src/observer/utils.js,所以我们先分析一下 utils 文件。分 3 部分。

第一部分

export const ObserverStack = {  stack: [],
  push(observer) {    this.stack.push(observer);
  },
  pop() {    return this.stack.pop();
  },
  top() {    return this.stack[this.stack.length - 1];
  }
};复制代码
登录后复制

首先是定义了一个用来存放观察者的栈,遵循后进先出的原则,内部使用 stack 数组来存储。

  • 入栈操作 push,和数组的 push 函数一样,在栈顶放入一个观察者 observer。
  • 出栈操作 pop,和数组的 pop 函数一样,在将栈顶的观察者删除,并返回这个被删除的观察者。
  • 取栈顶元素 top,和 pop 操作不同,top 是把栈顶元素取出来,但是并不删除。

第二部分

export const SYMBOL_OBSERVABLE = '__ob__';export const canObserve = target => typeof target === 'object';复制代码
登录后复制

定义了一个字符串常量 SYMBOL_OBSERVABLE。为了后面用着方便。

定义了一个函数 canObserve,目标是否可以被观察。只有对象才能被观察,所以使用 typeof 来判断目标的类型。等等,好像有什么不对。如果 targetnull 的话,函数也会返回 true。如果 null 不可观察,那么这就是一个 bug。(写这篇文章的时候我已经提了一个 PR,并询问了这种行为是否是期望的行为)。

第三部分

export const defineProp = (target, key, value) => {  Object.defineProperty(target, key, { enumerable: false, value });
};复制代码
登录后复制

这个没有什么好解释的,就是 Object.defineProperty 代码太长了,定义一个函数来避免代码重复。

下面再来分析观察者 src/observer/observer.js,分 4 部分。

第一部分

export function Observer(context, getter, callback, meta) {  this._ctx = context;  this._getter = getter;  this._fn = callback;  this._meta = meta;  this._lastValue = this._get();
}复制代码
登录后复制

构造函数。接受 4 个参数。

context 当前观察者所处的上下文,类型是 ViewModel。当第三个参数 callback 调用时,函数的 this 就是这个 context

getter 类型是一个函数,用来获取某个属性的值。

callback 类型是一个函数,当某个值变化后执行的回调函数。

meta 元数据。观察者(Observer)并不关注 meta 元数据。

在构造函数的最后一行,this._lastValue = this._get()。下面来分析 _get 函数。

第二部分

Observer.prototype._get = function() {  try {
    ObserverStack.push(this);    return this._getter.call(this._ctx);
  } finally {
    ObserverStack.pop();
  }
};复制代码
登录后复制

ObserverStack 就是上面分析过的用来存储所有观察者的栈。将当前观察者入栈,并通过 _getter 取得当前值。结合第一部分的构造函数,这个值存储在了 _lastValue 属性中。

执行完这个过程后,这个观察者就已经初始化完成了。

第三部分

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;
  }
};复制代码
登录后复制

这部分实现了数据更新时的脏检查(Dirty checking)机制。比较更新后的值和当前值,如果不同,那么就执行回调函数。如果这个回调函数是渲染 UI,那么则可以实现按需渲染。如果值相同,那么再检查设置的新值是否可以被观察,再决定到底要不要执行回调函数。

第四部分

Observer.prototype.subscribe = function(subject, key) {  const detach = subject.attach(key, this);  if (typeof detach !== 'function') {    return;
  }  if (!this._detaches) {    this._detaches = [];
  }  this._detaches.push(detach);
};

Observer.prototype.unsubscribe = function() {  const detaches = this._detaches;  if (!detaches) {    return;
  }  while (detaches.length) {
    detaches.pop()();
  }
};复制代码
登录后复制

订阅与取消订阅。

我们前面经常说观察者和被观察者。对于观察者模式其实还有另一种说法,叫订阅/发布模式。而这部分代码则实现了对主题(subject)的订阅。

先调用主题的 attach 方法进行订阅。如果订阅成功,subject.attach 方法会返回一个函数,当调用这个函数就会取消订阅。为了将来能够取消订阅,这个返回值必需保存起来。

subject 的实现很多人应该已经猜到了。观察者订阅了 subject,那么 subject 需要做的就是,当数据变化时即使通知观察者。subject 如何知道数据发生了变化呢,机制和 vue2 一样,使用 Object.defineProperty 做属性劫持。

下面再来分析观察者 src/observer/subject.js,分 7 部分。

第一部分

export function Subject(target) {  const subject = this;
  subject._hijacking = true;
  defineProp(target, SYMBOL_OBSERVABLE, subject);  if (Array.isArray(target)) {
    hijackArray(target);
  }  Object.keys(target).forEach(key => hijack(target, key, target[key]));
}复制代码
登录后复制

构造函数。基本没什么难点。设置 _hijacking 属性为 true,用来标示这个对象已经被劫持了。Object.keys 通过遍历来劫持每个属性。如果是数组,则调用 hijackArray

第二部分

两个静态方法。

Subject.of = function(target) {  if (!target || !canObserve(target)) {    return target;
  }  if (target[SYMBOL_OBSERVABLE]) {    return target[SYMBOL_OBSERVABLE];
  }  return new Subject(target);
};

Subject.is = function(target) {  return target && target._hijacking;
};复制代码
登录后复制

Subject 的构造函数并不直接被外部调用,而是封装到了 Subject.of 静态方法中。

如果目标不能被观察,那么直接返回目标。

如果 target[SYMBOL_OBSERVABLE] 不是 undefined,说明目标已经被初始化过了。

否则,调用构造函数初始化 Subject。

Subject.is 则用来判断目标是否被劫持过了。

第三部分

Subject.prototype.attach = function(key, observer) {  if (typeof key === 'undefined' || !observer) {    return;
  }  if (!this._obsMap) {    this._obsMap = {};
  }  if (!this._obsMap[key]) {    this._obsMap[key] = [];
  }  const observers = this._obsMap[key];  if (observers.indexOf(observer) < 0) {
    observers.push(observer);    return function() {
      observers.splice(observers.indexOf(observer), 1);
    };
  }
};复制代码
登录后复制

这个方法很眼熟,对,就是上文的 Observer.prototype.subscribe 中调用的。作用是某个观察者用来订阅主题。而这个方法则是“主题是怎么订阅的”。

观察者维护这一个主题的哈希表 _obsMap。哈希表的 key 是需要订阅的 key。比如某个观察者订阅了 name 属性的变化,而另一个观察者订阅了 age 属性的变化。而且属性的变化还可以被多个观察者同时订阅,因此哈希表存储的值是一个数组,数据的每个元素都是一个观察者。

第四部分

Subject.prototype.notify = function(key) {  if (    typeof key === &#39;undefined&#39; ||
    !this._obsMap ||
    !this._obsMap[key]
  ) {    return;
  }  this._obsMap[key].forEach(observer => observer.update());
};复制代码
登录后复制

当属性发生变化是,通知订阅了此属性的观察者们。遍历每个观察者,并调用观察者的 update 方法。我们上文中也提到了,脏检查就是在这个方法内完成的。

第五部分

Subject.prototype.setParent = function(parent, key) {  this._parent = parent;  this._key = key;
};

Subject.prototype.notifyParent = function() {  this._parent && this._parent.notify(this._key);
};复制代码
登录后复制

这部分是用来处理属性嵌套(nested object)的问题的。就是类似这种对象:{ user: { name: &#39;JJC&#39; } }

第六部分

function hijack(target, key, cache) {  const subject = target[SYMBOL_OBSERVABLE];  Object.defineProperty(target, key, {    enumerable: true,
    get() {      const observer = ObserverStack.top();      if (observer) {
        observer.subscribe(subject, key);
      }      const subSubject = Subject.of(cache);      if (Subject.is(subSubject)) {
        subSubject.setParent(subject, key);
      }      return cache;
    },
    set(value) {
      cache = value;
      subject.notify(key);
    }
  });
}复制代码
登录后复制

这一部分展示了如何使用 Object.defineProperty 进行属性劫持。当设置属性时,会调用 set(value),设置新的值,然后调用 subject 的 notify 方法。这里并不进行任何检查,只要设置了属性就会调用,即使属性的新值和旧值一样。notify 会通知所有的观察者。

第七部分

劫持数组方法。

const ObservedMethods = {  PUSH: &#39;push&#39;,  POP: &#39;pop&#39;,  UNSHIFT: &#39;unshift&#39;,  SHIFT: &#39;shift&#39;,  SPLICE: &#39;splice&#39;,  REVERSE: &#39;reverse&#39;};const OBSERVED_METHODS = Object.keys(ObservedMethods).map(    key => ObservedMethods[key]
);复制代码
登录后复制

ObservedMethods 定义了需要劫持的数组函数。前面大写的用来做 key,后面小写的是需要劫持的方法。

function hijackArray(target) {
  OBSERVED_METHODS.forEach(key => {    const originalMethod = target[key];

    defineProp(target, key, function() {      const args = Array.prototype.slice.call(arguments);
      originalMethod.apply(this, args);      let inserted;      if (ObservedMethods.PUSH === key || ObservedMethods.UNSHIFT === key) {
        inserted = args;
      } else if (ObservedMethods.SPLICE) {
        inserted = args.slice(2);
      }      if (inserted && inserted.length) {
        inserted.forEach(Subject.of);
      }      const subject = target[SYMBOL_OBSERVABLE];      if (subject) {
        subject.notifyParent();
      }
    });
  });
}复制代码
登录后复制

数组的劫持和对象不同,不能使用 Object.defineProperty

我们需要劫持 6 个数组方法。分别是头部添加、头部删除、尾部添加、尾部删除、替换/删除某几项、数组反转。

通过重写数组方法实现了数组的劫持。但是这里有一个需要注意的地方,数据的每一个元素都是被观察过的,但是当在数组中添加了新元素时,这些元素还没有被观察。因此代码中还需要判断当前的方法如果是 pushunshiftsplice,那么需要将新的元素放入观察者队列中。

另外两个文件分别是单元测试和性能分析,这里就不再分析了。

想了解更多编程学习,敬请关注php培训栏目!

以上是解剖式分析 鸿蒙系统的JavaScript框架的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

Java教程
1663
14
CakePHP 教程
1419
52
Laravel 教程
1313
25
PHP教程
1263
29
C# 教程
1236
24
鸿蒙系统支持哪些手机 鸿蒙系统支持哪些手机 Mar 20, 2024 pm 03:01 PM

鸿蒙系统是华为自主研发的分布式操作系统,为用户提供流畅、安全、智能的全场景体验。自发布以来,鸿蒙系统不断更新迭代,支持的手机型号范围逐步扩大,包括华为旗舰机型Mate系列、P系列,以及nova系列等。随着鸿蒙系统开放给其他品牌手机进行适配,未来可能有更多手机支持鸿蒙系统。此外,鸿蒙系统还支持平板、智能穿戴、智慧屏等多种终端设备,实现快速连接、能力互助、资源共享。

如何使用WebSocket和JavaScript实现在线语音识别系统 如何使用WebSocket和JavaScript实现在线语音识别系统 Dec 17, 2023 pm 02:54 PM

如何使用WebSocket和JavaScript实现在线语音识别系统引言:随着科技的不断发展,语音识别技术已经成为了人工智能领域的重要组成部分。而基于WebSocket和JavaScript实现的在线语音识别系统,具备了低延迟、实时性和跨平台的特点,成为了一种被广泛应用的解决方案。本文将介绍如何使用WebSocket和JavaScript来实现在线语音识别系

WebSocket与JavaScript:实现实时监控系统的关键技术 WebSocket与JavaScript:实现实时监控系统的关键技术 Dec 17, 2023 pm 05:30 PM

WebSocket与JavaScript:实现实时监控系统的关键技术引言:随着互联网技术的快速发展,实时监控系统在各个领域中得到了广泛的应用。而实现实时监控的关键技术之一就是WebSocket与JavaScript的结合使用。本文将介绍WebSocket与JavaScript在实时监控系统中的应用,并给出代码示例,详细解释其实现原理。一、WebSocket技

鸿蒙系统好用吗 鸿蒙系统好用吗 Mar 18, 2024 pm 02:15 PM

鸿蒙系统是一款好用且值得考虑的操作系统。它拥有简洁直观的用户界面、流畅稳定的系统性能、丰富的功能应用以及完善的生态支持。从用户体验、系统性能、功能丰富度到生态支持,鸿蒙系统在各方面都表现出色,能够满足用户的日常使用需求,为用户带来便捷高效的体验。

荣耀手机鸿蒙系统升级全攻略 荣耀手机鸿蒙系统升级全攻略 Mar 22, 2024 pm 09:18 PM

荣耀手机自面市以来,凭借着出色的性能表现和颇具创新的设计理念,一直备受消费者的青睐。而随着鸿蒙系统的发布,荣耀手机再次引起了广泛关注。鸿蒙系统作为一款全新的自研操作系统,极大地提升了荣耀手机的使用体验,更为用户带来了全新的功能和操控方式。在这样的背景下,荣耀手机用户们迫不及待想要升级自己的手机至鸿蒙系统。为了帮助大家更好地进行升级,本文将为您提供一份全面的荣

鸿蒙系统和安卓系统数据互通吗 鸿蒙系统和安卓系统数据互通吗 Mar 18, 2024 pm 02:21 PM

鸿蒙系统和安卓系统,基于Linux内核,在底层架构上存在相似性,为数据互通提供了基础。然而,两系统在设计理念、系统架构和功能实现方面存在差异,导致数据互通存在挑战。实现鸿蒙系统和安卓系统的数据互通需要解决兼容性、数据格式转换等技术问题,以及制定统一的数据交换标准、建立可靠的数据传输机制等问题。同时,还需要应用开发者和生态系统合作伙伴的积极参与和支持,共同推动两系统之间的数据互通。

用户口碑揭秘:鸿蒙系统到底好不好用? 用户口碑揭秘:鸿蒙系统到底好不好用? Mar 25, 2024 am 09:39 AM

鸿蒙系统,作为华为推出的全新操作系统,在推出之初就引起了广泛的关注和热议。作为华为公司自主研发的操作系统,鸿蒙系统被寄予了厚望,被认为有望与目前流行的Android和iOS等操作系统一较高下。然而,随着鸿蒙系统逐渐在市场上推出和普及,用户们开始对其使用体验、功能性、稳定性等方面展开了各种讨论和评价。那么,鸿蒙系统到底好不好用?用户的口碑又是如何?接下来,将从

鸿蒙系统适配手机品牌解析 鸿蒙系统适配手机品牌解析 Mar 23, 2024 pm 09:42 PM

鸿蒙系统,作为华为公司推出的全新操作系统,一经问世便引起了广泛关注。作为一款面向全场景智能终端的分布式操作系统,鸿蒙系统被认为有望成为未来手机、平板、智能穿戴等设备的新选择。随着鸿蒙系统的逐渐成熟和推广,越来越多的手机品牌开始积极响应并适配这一系统,以期在市场竞争中获得更多的优势。华为手机品牌首当其冲要说到的当然是华为手机品牌自家的产品。作为鸿蒙系统的开发者

See all articles