百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

鸿蒙OS里的JS开发框架是怎么样的?

ccwgpt 2024-10-13 01:36 28 浏览 0 评论

作为一名老程序员,看到JavaScript被鸿蒙应用开发框架采用为开发语言,我简直是“喜不自胜”啊,感觉自己这是要逆袭了??果然才是世界最好的语言啊。。。

这个源码被托管在国内知名的开源平台上,我也下载认真的研究了一晚上这个源码,顺便写了个 hello world 程序,今天就跟大家一起逐行分析一下鸿蒙系统里的JS框架。


那鸿蒙的JavaScript框架是怎么样的??

我相信不管是我还是其他开发者最感兴趣的就是鸿蒙的JS框架ace_lite_jsfwk,这一看就是个非常轻量级的框架。等我看完它的源代码后发现它确实很轻,它的核心代码只有5个js文件,大致有300-400行代码。(无单元测试境况下)

  • runtime-core\src\core\index.js
  • runtime-core\src\observer\observer.js
  • runtime-core\src\observer\subject.js
  • runtime-core\src\observer\utils.js
  • runtime-core\src\profiler\index.js

这些代码呢实现了一个观察的模式,意识就是说,它实现了一个非常轻量级的 MVVM 模式,通过使用和 vue2 相似的属性劫持技术实现了响应式系统。这应该是目前培训班的“三大自己实现”之一了吧。(自己实现 Promise,自己实现 vue,自己实现 react)

utils里定义了一个Observer栈,里面存放了观察者。subject定义被观察者。所以当我们观察某个对象的时候,也就是劫持这个对象属性的操作,也包含一些数组函数,像push、pop等等。这个文件应该是代码最多的,有160行吧。observer的代码就更简单了,大概有五六十行。

当我们进行开发的时候,可以通过Toolkit讲开发者编写的HML、CSS、JS文件编译打包成JS Bundle。然后把JS Bundle 解析运行成C++native UI的View组件进行渲染。

在 src\core\base\framework_min_js.h 文件中,这一段编译好的js被编译到runtime里,编译完的 js 文件不到 3K,这确实很轻量。

js runtime 没有使用 V8,同样也没有使用到jscore选择了JerryScript。JerryScript用于物联网的超轻量 JavaScript 引擎。能够在内存少于64KB的设备上执行ECMAScript 5.1 源代码。这就说明了为什么在文档里鸿蒙JS框架支持ECMAScript 5.1 的原因。

从整体上看js框架大概使用了96%的C/C++代码。在htm文件中写的组件会被编译成原生组件。然而app_style_manager.cpp和同级的七八个文件则用来解析css,最终生成原生布局。从整体上看,我觉得比我预想中的会要好一些。

唠嗑完鸿蒙的JavaScript框架,那我现在就给大家逐行分析下鸿蒙系统的 JavaScript 框架,为了这我也是踩了不上坑,给鸿蒙贡献了好几个PR(所有的代码都是基于鸿蒙当前最新版的)。

鸿蒙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); // 1
vm.increase();console.log(vm.count); // 2
vm.decrease();console.log(vm.count); // 1

仓库里所有的JS代码石祥路一个响应系统,充当MVVM中的ViewModel。

接下来我们来逐行分析下。

scr目录中一共有4个目录,攻击8个文件。其中一个是单元测试,还有一个就是性能测试,除去两个index.js 文件,有用的文件共四个。这也是我们要分析的重点。

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,它也是两行代码:

export { Observer } from './observer';export { Subject } from './subject';

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

可以看到这两个文件里都用到了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,目标是否可以被观察。只有对象才能被观察,所以可以使用typeoftrue。如果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();}

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

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

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

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

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

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

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

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

第三部分:这一部分实现数据更新时的脏检查机制。

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;  }};

对比更新后的值和现在的值,如果两者不同,那么久执行回调函数。如果这个回调函数是渲染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()();  }};

上面我们经常提到观察者和被观察者,对于观察者模式我们还有另一种说法,即是订阅/发布模式。这个部分的代码则实现了对主题的订阅。

先调用主题的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静态方法中。

若目标不能被观察,就直接返回目标。

不然的话,调用构造函数初始化Subject。

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

第三部分:是前面的Observer.prototype.subscribe中调用的。作用是某个观察者用来订阅主题。而这个方法则是“主题怎么订阅”。

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);    };  }};

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

第四部分:当属性发生变化时,通知订阅此属性的观察者,遍历每个观察者,且调用观察者的update方法。上面我们提到过,脏检查就是这个方法内完成的。

Subject.prototype.notify = function(key) {  if (    typeof key === 'undefined' ||    !this._obsMap ||    !this._obsMap[key]  ) {    return;  }  this._obsMap[key].forEach(observer => observer.update());};

第五部分:是用来处理属性嵌套的问题,就像这种对象{ user: { name: 'JJC' } }

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

第六部分:展示了怎么使用Object.defineProperty 进行属性劫持。当设置属性时,会调用set(value),设置新的值,之后调用subject 的 notify 方法。

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);    }  });}

这里不进行任何的检查,只要设置属性就可以调用了,即使属性的新值跟旧值一样。notify会通知所有的观察者。

第七部分:劫持数组方法。

const ObservedMethods = {  PUSH: 'push',  POP: 'pop',  UNSHIFT: 'unshift',  SHIFT: 'shift',  SPLICE: 'splice',  REVERSE: 'reverse'};
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个数组方法。分别是同步添加、头部删除、尾部添加、尾部删除、替换/删除某几项、数组反转。

通过重写数组方法实现数组的劫持。不过这有一个要注意的地方,数据的每个元素都是被观察过的,但在数组里添加新元素时,这些元素哈没有被观察,所以代码中仍需要判断当前的方法如果是push、unshift、splice。那需要把新的元素投放在观察者队伍里。

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

作为JavaScript曾经的程序员,我当然是很支持鸿蒙采用JavaScript作为开发语言,这也增强所有熟悉 JavaScript 的前端开发者们对前端的道路自信、理论自信、文化自信和技术栈自信。不过,如果你要做一些底层的开发,你可能还需要用其他编程语言。


——————

原创:老王丨鸿蒙hms开发者高级认证持证人!学习更多鸿蒙OS相关开发技术可以关注我的公众号:鸿蒙开发者老王

相关推荐

NestJS入门教程系列一

介绍Nest(NestJS)是用于构建高效,可扩展的Node.js服务器端应用程序的框架。它使用渐进式JavaScript,内置并完全支持TypeScript(但开发人员仍然能够使用JavaScrip...

【推荐】一个网盘资源搜索与转存工具,支持移动端与PC端!

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!项目介绍CloudSaver是一个基于Vue3和Express的网盘资源搜索与转存开源实用工具。它支持...

Appium原理精讲

目前使用Appium新版本和旧版本的企业数目都很多,而两个版本的安装过程和api的使用又有较大的区别。但是无论表面上的东东如何变化,内部原理都是一样的。在这里我给大家介绍一下appium的核心,增进大...

Kubernetes最小部署单元Pod

一、Kubernetes与Pod简介在当今云计算和容器化技术盛行的时代,Kubernetes已然成为容器编排领域的中流砥柱。它是一个开源的容器编排平台,由Google基于其内部使用的Bo...

最常用的四种跨域解决方案

前置知识什么是跨域?浏览器发送的请求地址(URL)与所在页面的地址不同(端口/协议/域名其一不同)。简言之,浏览器发出的请求url,与其所在页面的url不一样。此时,同源策略会让浏览器拒收服务器...

Bolt.New —— 全栈AI Web自动编程

Bolt.New是由StackBlitz公司推出的,全栈AI工具,代码编辑、运行、部署,通通一站式搞定。它使用WebContainers技术,无需任何本地安装或配置,在浏览器中,就可以运行完整的No...

Nodejs Express新手教程&amp;高手进阶

NodejsExpress新手教程&高手进阶Express是一个NodeJS平台的框架,主要用于构于Web服务器项目。本文将通过示例介绍适合新手入门的Express基础使用,以及高手进阶知识,如:c...

Express.js 创建Node.js Web应用

Express.js是一个基于Node.js的Web应用框架,框架的设计目的是构建应用的架构和简化应用的开发。框架会解决一些通用的问题,在Express.js中,Express框架会处理如:中间件、代...

JavaScript 的 Express.js 功能及应用场景详解

Express.js是一个基于Node.js的轻量级Web应用框架,主要用于快速构建服务器端应用和API。它的核心功能包括以下关键点:1.路由管理URL路径与HTTP方法映射:通过...

nodejs的express4文件下载

在nodejs的express框架中,下载变得非常简单,就一个方法,res.download()首先express命令行生成项目基本框架:不会的看这里:http://blog.csdn.net/zz...

Express 系列:快速生成一个项目

系列预告本系列将以一个项目入手结合相关技术细节来带领大家一起学习Express这个基于Node.js的后端框架。本文首先将介绍:如何快速的生成一个具有一定结构的Express项目。Express项目结...

nodejs的express自动生成项目框架

nodejs版本为:4.X,express版本为4.X1.全局安装2个模块express、express-generator在命令行输入:npminstall-gexpressnpminsta...

express开发(一)简介与搭建

上周末去了趟上海书城,不愧是上海数得上号的书城,流行的科技书应有尽有,话不多说直接上图。最经典的C语言O(∩_∩)O最流行的java(づ ̄3 ̄)づ超酷的R语言/(ㄒoㄒ)/~~然而,身为一个坚定的前...

Vue+Echarts可视化大屏系统后端框架搭建(附代码)

各位同学,大家好。上节课,前面我们讲解了Vue+Echarts前端部分的设计方法。这节课程,我们开始讲解使用Express进行后端设计的方法。01项目相关理论介绍什么是expressExpress是...

Shopify电商API接口开发

Shopify电商API接口开发上线流程主要包括以下步骤。北京木奇移动技术有限公司,专业的软件外包开发公司,欢迎洽谈合作。前期准备-注册Shopify账号:在Shopify官网注册,用于后续开发测试...

取消回复欢迎 发表评论: