当前位置: 首页 > 科技观察

HarmonyOS系统JS开发框架

时间:2023-03-16 15:36:03 科技观察

更多内容请访问:HarmonyOS技术社区https://harmonyos.51cto.com/#zzHarmonyOS已经开源一个多月了,源码托管在码云,国内知名开源平台,https://gitee.com/openharmony。我最感兴趣的是JS框架ace_lite_jsfwk。从名字就可以看出这是一个非常轻量级的框架。官方介绍是“轻量级的JS核心开发框架”。看源码的时候,发现真的很轻。src目录下一共有4个目录,一共8个文件。1个用于单元测试,1个用于分析。去掉2个index.js文件后,共有4个关键文件,大概300-400行代码。src├──__test__│└──index.test.js├──core│└──index.js├──index.js├──observer│├──index.js│├──observer.js│├──subject.js│└──utils.js└──profiler└──index.js顾名思义,这些代码实现了观察者模式。也就是说,它实现了一个非常轻量级的MVVM模式。使用类似vue2的属性劫持技术实现响应式系统。utils中定义了一个Observer栈,存放观察者。主题定义可观察的。我们观察一个对象,就是劫持这个对象的属性的操作,包括一些数组函数,比如push,pop等。这个文件应该是代码最多的,160行。observer的代码就更简单了,五十、六十行。我们在开发时,将开发者编写的HML、CSS、JS文件通过Toolkit编译打包成JSBundle,然后将JSBundle解析运行到C++原生UI的View组件中进行渲染。“通过支持第三方开发者使用声明式API进行应用开发,数据驱动视图变更,避免大量视图操作,大大降低应用开发难度,提升开发者开发体验。”基本上是小程序式的开发体验。在src\core\base\framework_min_js.h文件中,将这个编译好的js编译到runtime中。编译后的js文件不到3K,真是轻量级。js运行时不使用V8,也不使用jscore。相反,选择JerryScript。JerryScript是用于物联网的超轻量级JavaScript引擎。它能够在内存小于64KB的设备上执行ECMAScript5.1源代码。总的来说,这个js框架使用了大约96%的C/C++代码和1.8%的JS代码。在htm文件中编写的组件将被编译为本机组件。而app_style_manager.cpp和同级的七八个文件就是用来解析css,最终生成nativelayout的。虽然SDK里面有好几个weex包,但是我们也发现了react的影子。但是C/C++代码中并没有瑜伽相关的内容(全局搜索没找到)。SDK中的package只是作为loader使用,大概是webpack打包时解析htm组件的。将htm的模板编译成js代码。下面我们逐行分析。首先是入口文件src/index.js,只有2行代码:import{ViewModel}from'./core';exportdefaultViewModel;实际上是再出口。另一个类似的文件是src/observer/index.js,也是2行代码:export{Observer}from'./observer';export{Subject}from'./subject';observer和subject实现了一个观察者模式。subject就是主体,也就是观察者。观察者就是观察者。当主体有任何变化时,需要主动通知观察者。这是有反应的。第一部分exportconstObserverStack={stack:[],push(observer){this.stack.push(observer);},pop(){returnthis.stack.pop();},top(){returnthis.stack[this.stack.length-1];}};首先,它定义了一个存放观察者的栈,遵循后进先出的原则,内部使用一个栈数组进行存储。栈操作push,和数组的push函数一样,在栈顶放一个观察者。pop操作pop,和数组的pop函数一样,删除栈顶的观察者,并返回删除的观察者。取栈顶元素top与pop操作不同。top取出栈顶元素,但不删除它。第二部分exportconstSYMBOL_OBSERVABLE='__ob__';exportconstcanObserve=target=>typeoftarget==='object';定义一个字符串常量SYMBOL_OBSERVABLE。为了以后方便。定义了一个函数canObserve,目标是否可以被观察到。只能观察对象,所以使用typeof来判断目标的类型。等等,似乎有什么不对。如果目标为空,该函数也返回真。如果null不可观察,那么它就是一个错误。(我在写这篇文章时提出了PR,并询问是否需要这种行为)。第三部分exportconstdefineProp=(target,key,value)=>{Object.defineProperty(target,key,{enumerable:false,value});};这个没有解释,但是Object.defineProperty的代码太长了,自己定义一个函数,避免代码重复。第一部分exportfunctionObserver(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就是this上下文。getter类型是用于获取属性值的函数。回调类型是在值更改时执行的函数。元元数据。观察者不关注元元数据。在构造函数的最后一行,this._lastValue=this._get()。我们来分析一下_get函数。第二部分Observer.prototype._get=function(){try{ObserverStack.push(this);returnthis._getter.call(this._ctx);}finally{ObserverStack.pop();}};ObserverStack是上面分析的用于存放所有观察者的栈。将当前观察者压入栈中,通过_getter获取当前值。结合构造函数的第一部分,这个值存储在_lastValue属性中。执行完这个过程后,观察者就已经初始化好了。第三部分Observer.prototype.update=function(){constlastValue=this._lastValue;constnextValue=this._get();constcontext=this._ctx;constmeta=this._meta;if(nextValue!==lastValue||canObserve(nextValue)){this._fn.call(context,nextValue,lastValue,meta);this._lastValue=nextValue;}};这部分实现了数据更新时的脏检查(Dirtychecking)机制。将更新后的值与当前值进行比较,如果不同则执行回调函数。如果这个回调函数是渲染UI,那么就可以实现按需渲染。如果值相同,则检查是否可以观察到新的值集,然后决定是否执行回调函数。第四部分Observer.prototype.subscribe=function(subject,key){constdetach=subject.attach(key,this);if(typeofdetach!=='function'){return;}if(!this._detaches){this._detaches=[];}this._detaches.push(detach);};Observer.prototype.unsubscribe=function(){constdetaches=this._detaches;if(!detaches){return;}while(detaches.length){分离.pop()();}};订阅和退订。我们经常谈论观察者和被观察者。观察者模式其实还有一种说法,叫做订阅/发布模式。而这部分代码实现了对主题的订阅。先调用主题的attach方法进行订阅。如果订阅成功,则subject.attach方法返回一个函数,该函数在被调用时取消订阅。为了将来能够取消订阅,必须保存此返回值。subject的实现应该很多人都猜到了。观察者订阅了主题,那么主题需要做的就是在数据发生变化时通知观察者。主体如何知道数据已更改?机制和vue2一样,使用Object.defineProperty作为属性劫持。第一部分exportfunctionSubject(target){constsubject=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)){returntarget;}if(target[SYMBOL_OBSERVABLE]){returntarget[SYMBOL_OBSERVABLE];}returnnewSubject(target);};Subject.is=函数(目标){returntarget&&target._hijacking;};Subject的构造函数不是直接从外部调用,而是封装到Subject.of静态方法中。如果无法观察到目标,则直接返回目标。如果target[SYMBOL_OBSERVABLE]未定义,则目标已经初始化。否则,调用构造函数来初始化Subject。Subject.is用于判断目标是否被劫持。第三部分Subject.prototype.attach=function(key,observer){if(typeofkey==='undefined'||!observer){return;}if(!this._obsMap){this._obsMap={};}if(!this._obsMap[key]){this._obsMap[key]=[];}constoobservers=this._obsMap[key];if(observers.indexOf(observer)<0){observers.push(observer);returnfunction(){observers.splice(observers.indexOf(observer),1);};}};这个方法很熟悉,没错,就是上面Observer.prototype.subscribe中调用的。该角色是观察者订阅主题。而这个方法就是“如何订阅主题”。观察者维护一个该主题的哈希表_obsMap。哈希表的key就是需要订阅的key。例如,一个观察者订阅了name属性的变化,而另一个观察者订阅了age属性的变化。而且属性的变化也可以被多个观察者同时订阅,所以哈??希表中存储的值是一个数组,数据的每个元素都是一个观察者。第四部分Subject.prototype.notify=function(key){if(typeofkey==='undefined'||!this._obsMap||!this._obsMap[key]){return;}this._obsMap[key]。forEach(observer=>observer.update());};当属性发生变化时,通知订阅该属性的观察者。遍历每个观察者并调用观察者的更新方法。上面我们也提到了脏检查是在这个方法中完成的。第五部分Subject.prototype.setParent=function(parent,key){this._parent=parent;this._key=key;};Subject.prototype.notifyParent=function(){this._parent&&this._parent.notify(this._钥匙);};这部分用来处理嵌套对象的问题。它是这样一个对象:{user:{name:'JJC'}}。第六部分functionhijack(target,key,cache){constsubject=target[SYMBOL_OBSERVABLE];Object.defineProperty(target,key,{enumerable:true,get(){constobserver=ObserverStack.top();if(observer){observer.subscribe(subject,key);}constsubSubject=Subject.of(cache);if(Subject.is(subSubject)){subSubject.setParent(subject,key);}returncache;},set(value){cache=value;subject.notify(key);}});}本节展示如何使用Object.defineProperty进行属性劫持。设置属性时,调用set(value),设置新值,调用主体的notify方法。此处不执行任何检查,只要设置属性,就会调用它,即使属性的新值与旧值相同。通知将通知所有观察者。第七部分劫持数组方法。constObservedMethods={PUSH:'push',POP:'pop',UNSHIFT:'unshift',SHIFT:'shift',SPLICE:'splice',REVERSE:'reverse'};constOBSERVED_METHODS=Object.keys(ObservedMethods).map(key=>ObservedMethods[key]);ObservedMethods定义了一组需要被劫持的函数。前面的大写作为key,后面的小写是需要劫持的方法。functionhijackArray(target){OBSERVED_METHODS.forEach(key=>{constooriginalMethod=target[key];defineProp(target,key,function(){constargs=Array.prototype.slice.call(arguments);originalMethod.apply(this,args);letinserted;if(ObservedMethods.PUSH===key||ObservedMethods.UNSHIFT===key){inserted=args;}elseif(ObservedMethods.SPLICE===key){inserted=args.slice(2);}if(inserted&&inserted.length){inserted.forEach(Subject.of);}constsubject=target[SYMBOL_OBSERVABLE];if(subject){subject.notifyParent();}});});}数组劫持不同于对象,不能使用Object.defineProperty。我们需要劫持6个数组方法。它们是头部添加、头部删除、尾部添加、尾部删除、某些项目的替换/删除和数组反转。数组劫持是通过覆盖数组方法来实现的。但是这里有一点需要注意,数据的每一个元素都被观察到了,但是当新的元素加入到数组中时,这些元素并没有被观察到。因此,代码中还需要判断,如果当前方法是push、unshift或splice,则需要将新元素放入观察者队列。另外两个文件是单元测试和性能分析,这里就不分析了。总的来说,它比我预期的要好。了解更多请访问:与华为官方共建鸿蒙科技社区https://harmonyos.51cto.com/#zz