搜尋
首頁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: 'JJC' } }

第六部分

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中文網其他相關文章!

陳述
本文轉載於:juejin。如有侵權,請聯絡admin@php.cn刪除
Python vs. JavaScript:開發環境和工具Python vs. JavaScript:開發環境和工具Apr 26, 2025 am 12:09 AM

Python和JavaScript在開發環境上的選擇都很重要。 1)Python的開發環境包括PyCharm、JupyterNotebook和Anaconda,適合數據科學和快速原型開發。 2)JavaScript的開發環境包括Node.js、VSCode和Webpack,適用於前端和後端開發。根據項目需求選擇合適的工具可以提高開發效率和項目成功率。

JavaScript是用C編寫的嗎?檢查證據JavaScript是用C編寫的嗎?檢查證據Apr 25, 2025 am 12:15 AM

是的,JavaScript的引擎核心是用C語言編寫的。 1)C語言提供了高效性能和底層控制,適合JavaScript引擎的開發。 2)以V8引擎為例,其核心用C 編寫,結合了C的效率和麵向對象特性。 3)JavaScript引擎的工作原理包括解析、編譯和執行,C語言在這些過程中發揮關鍵作用。

JavaScript的角色:使網絡交互和動態JavaScript的角色:使網絡交互和動態Apr 24, 2025 am 12:12 AM

JavaScript是現代網站的核心,因為它增強了網頁的交互性和動態性。 1)它允許在不刷新頁面的情況下改變內容,2)通過DOMAPI操作網頁,3)支持複雜的交互效果如動畫和拖放,4)優化性能和最佳實踐提高用戶體驗。

C和JavaScript:連接解釋C和JavaScript:連接解釋Apr 23, 2025 am 12:07 AM

C 和JavaScript通過WebAssembly實現互操作性。 1)C 代碼編譯成WebAssembly模塊,引入到JavaScript環境中,增強計算能力。 2)在遊戲開發中,C 處理物理引擎和圖形渲染,JavaScript負責遊戲邏輯和用戶界面。

從網站到應用程序:JavaScript的不同應用從網站到應用程序:JavaScript的不同應用Apr 22, 2025 am 12:02 AM

JavaScript在網站、移動應用、桌面應用和服務器端編程中均有廣泛應用。 1)在網站開發中,JavaScript與HTML、CSS一起操作DOM,實現動態效果,並支持如jQuery、React等框架。 2)通過ReactNative和Ionic,JavaScript用於開發跨平台移動應用。 3)Electron框架使JavaScript能構建桌面應用。 4)Node.js讓JavaScript在服務器端運行,支持高並發請求。

Python vs. JavaScript:比較用例和應用程序Python vs. JavaScript:比較用例和應用程序Apr 21, 2025 am 12:01 AM

Python更適合數據科學和自動化,JavaScript更適合前端和全棧開發。 1.Python在數據科學和機器學習中表現出色,使用NumPy、Pandas等庫進行數據處理和建模。 2.Python在自動化和腳本編寫方面簡潔高效。 3.JavaScript在前端開發中不可或缺,用於構建動態網頁和單頁面應用。 4.JavaScript通過Node.js在後端開發中發揮作用,支持全棧開發。

C/C在JavaScript口譯員和編譯器中的作用C/C在JavaScript口譯員和編譯器中的作用Apr 20, 2025 am 12:01 AM

C和C 在JavaScript引擎中扮演了至关重要的角色,主要用于实现解释器和JIT编译器。1)C 用于解析JavaScript源码并生成抽象语法树。2)C 负责生成和执行字节码。3)C 实现JIT编译器,在运行时优化和编译热点代码,显著提高JavaScript的执行效率。

JavaScript在行動中:現實世界中的示例和項目JavaScript在行動中:現實世界中的示例和項目Apr 19, 2025 am 12:13 AM

JavaScript在現實世界中的應用包括前端和後端開發。 1)通過構建TODO列表應用展示前端應用,涉及DOM操作和事件處理。 2)通過Node.js和Express構建RESTfulAPI展示後端應用。

See all articles

熱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

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

SecLists

SecLists

SecLists是最終安全測試人員的伙伴。它是一個包含各種類型清單的集合,這些清單在安全評估過程中經常使用,而且都在一個地方。 SecLists透過方便地提供安全測試人員可能需要的所有列表,幫助提高安全測試的效率和生產力。清單類型包括使用者名稱、密碼、URL、模糊測試有效載荷、敏感資料模式、Web shell等等。測試人員只需將此儲存庫拉到新的測試機上,他就可以存取所需的每種類型的清單。

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

VSCode Windows 64位元 下載

VSCode Windows 64位元 下載

微軟推出的免費、功能強大的一款IDE編輯器

MantisBT

MantisBT

Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器