الصفائف المكتوبة - بيانات ثنائية في المتصفح

مقدمة

تمّت إضافة مصفوفات النصوص البرمجية مؤخرًا إلى المتصفّحات، وذلك بسبب الحاجة إلى طريقة فعّالة لمعالجة البيانات الثنائية في WebGL. المصفوفة من النوع المحدّد هي جزء من الذاكرة يتضمّن عرضًا من النوع المحدّد، تمامًا مثل طريقة عمل المصفوفات في C. بما أنّ الصفيف من النوع المحدّد يستند إلى ذاكرة أولية، يمكن لمحرك JavaScript تمرير الذاكرة مباشرةً إلى المكتبات الأصلية بدون الحاجة إلى تحويل البيانات إلى تمثيل أصلي بشكلٍ شاق. ونتيجةً لذلك، تحقّق الصفائف المُحدَّدة أداءً أفضل بكثير من صفائف JavaScript لنقل البيانات إلى WebGL وواجهات برمجة التطبيقات الأخرى التي تتعامل مع البيانات الثنائية.

تعمل طرق عرض الصفائف المُحدَّدة النوع مثل الصفائف ذات النوع الواحد في جزء من ArrayBuffer. تتوفّر طرق عرض لجميع الأنواع الرقمية المعتادة، بأسماء ذاتية الوصف مثل Float32Array وFloat64Array وInt32Array وUint8Array. هناك أيضًا عرض خاص حلّ محلّ نوع صفيف البكسل في ImageData في Canvas: Uint8ClampedArray.

DataView هو النوع الثاني من طرق العرض وهو مخصّص لمعالجة البيانات غير المتجانسة. بدلاً من استخدام واجهة برمجة تطبيقات تشبه الصفيف، يوفّر لك عنصر DataView واجهة برمجة تطبيقات للقراءة/الكتابة من أجل قراءة أنواع بيانات عشوائية وكتابتها في أماكن عشوائية في البايت. يعمل DataView بشكل رائع لقراءة رؤوس الملفات وكتابة البيانات الأخرى التي تشبه الهياكل.

أساسيات استخدام "المصفوفات المكتوبة"

طرق عرض المصفوفات المكتوبة

لاستخدام الصفائف من النوع المحدّد، عليك إنشاء ArrayBuffer وعرض له. إنّ أسهل طريقة هي إنشاء عرض مصنّف للصفيف بالحجم والنوع المطلوبَين.

// Typed array views work pretty much like normal arrays.
var f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];

هناك عدة أنواع مختلفة من طرق عرض الصفيفات المكتوبة. تشترك جميعها في واجهة برمجة التطبيقات نفسها، لذا بعد معرفة كيفية استخدام إحدى هذه الخدمات، ستتعرّف على كيفية استخدامها جميعًا. سأنشئ في المثال التالي عرضًا واحدًا من كل عرض حالي من عروض الصفائف من النوع المحدّد.

// Floating point arrays.
var f64 = new Float64Array(8);
var f32 = new Float32Array(16);

// Signed integer arrays.
var i32 = new Int32Array(16);
var i16 = new Int16Array(32);
var i8 = new Int8Array(64);

// Unsigned integer arrays.
var u32 = new Uint32Array(16);
var u16 = new Uint16Array(32);
var u8 = new Uint8Array(64);
var pixels = new Uint8ClampedArray(64);

أما العنصر الأخير، فهو خاص نوعًا ما، إذ يحدّ من قيم الإدخال بين 0 و255. ويُعدّ ذلك مفيدًا بشكل خاص لخوارزميات معالجة الصور في Canvas، لأنّه لم يعُد عليك الآن ضبط العمليات الحسابية لمعالجة الصور يدويًا لتجنُّب تجاوز النطاق الذي يبلغ 8 بت.

على سبيل المثال، في ما يلي كيفية تطبيق عامل جاما على صورة مخزّنة في Uint8Array. ليست جميلة جدًا:

u8[i] = Math.min(255, Math.max(0, u8[i] * gamma));

باستخدام Uint8ClampedArray، يمكنك تخطّي عملية التقييد اليدوي:

pixels[i] *= gamma;

الطريقة الأخرى لإنشاء عروض صفائف من النوع هي إنشاء ArrayBuffer أولاً ثم إنشاء عروض تشير إليه. تتعامل واجهات برمجة التطبيقات التي توفّر لك بيانات خارجية عادةً مع ArrayBuffers، لذا هذه هي الطريقة التي تحصل بها على عرض مصفوفة مُحدَّدة لتلك البيانات.

var ab = new ArrayBuffer(256); // 256-byte ArrayBuffer.
var faFull = new Uint8Array(ab);
var faFirstHalf = new Uint8Array(ab, 0, 128);
var faThirdQuarter = new Uint8Array(ab, 128, 64);
var faRest = new Uint8Array(ab, 192);

يمكنك أيضًا الحصول على عدة مشاهدات لـ ArrayBuffer نفسه.

var fa = new Float32Array(64);
var ba = new Uint8Array(fa.buffer, 0, Float32Array.BYTES_PER_ELEMENT); // First float of fa.

لنسخ صفيف مكتوب إلى صفيف مكتوب آخر، أسرع طريقة هي استخدام طريقة ضبط الصفيف المكتوب. لاستخدام مشابه لـ memcpy، أنشئ صفائف Uint8Arrays في وحدات التخزين المؤقت للعروض واستخدِم set لنسخ البيانات.

function memcpy(dst, dstOffset, src, srcOffset, length) {
  var dstU8 = new Uint8Array(dst, dstOffset, length);
  var srcU8 = new Uint8Array(src, srcOffset, length);
  dstU8.set(srcU8);
};

DataView

لاستخدام ArrayBuffers التي تحتوي على بيانات بأنواع غير متجانسة، فإنّ أسهل طريقة هي استخدام DataView في المخزن المؤقت. لنفترض أنّ لدينا تنسيق ملف يحتوي على عنوان يتضمّن عددًا صحيحًا غير موقَّت بسعة 8 بت متبوعًا برقمين صحيحَين بسعة 16 بت، متبوعًا بصفيف حمولة من الأعداد الكسريّة بسعة 32 بت. يمكن قراءة هذا النص من جديد باستخدام طرق عرض القوائم المكتوبة، ولكنّ ذلك قد يكون صعبًا بعض الشيء. باستخدام DataView، يمكننا قراءة الرأس واستخدام عرض صفيف مُحدَّد لصفيف الفاصلة العائمة.

var dv = new DataView(buffer);
var vector_length = dv.getUint8(0);
var width = dv.getUint16(1); // 0+uint8 = 1 bytes offset
var height = dv.getUint16(3); // 0+uint8+uint16 = 3 bytes offset
var vectors = new Float32Array(width*height*vector_length);
for (var i=0, off=5; i<vectors.length; i++, off+=4) {
  vectors[i] = dv.getFloat32(off);
}

في المثال أعلاه، تكون كل القيم التي أقرؤها بترتيب كبار السن. إذا كانت القيم في المخزن المؤقت بالتنسيق little-endian، يمكنك تمرير المَعلمة الاختيارية littleEndian إلى دالة الحصول:

...
var width = dv.getUint16(1, true);
var height = dv.getUint16(3, true);
...
vectors[i] = dv.getFloat32(off, true);
...

يُرجى العلم أنّ طرق عرض الصفائف المكتوبة تكون دائمًا بترتيب البايت الأصلي. يهدف ذلك إلى تسريع عملية التحميل. يجب استخدام DataView لقراءة البيانات وكتابتها في حال كانت ترتيب البتات سيؤدي إلى حدوث مشكلة.

يحتوي DataView أيضًا على طرق لكتابة القيم في المخزن المؤقت. يتمّ تسمية وظائف الإعداد هذه بالطريقة نفسها التي يتمّ بها تسمية وظائف الحصول، أي "set" متبوعة بنوع البيانات.

dv.setInt32(0, 25, false); // set big-endian int32 at byte offset 0 to 25
dv.setInt32(4, 25); // set big-endian int32 at byte offset 4 to 25
dv.setFloat32(8, 2.5, true); // set little-endian float32 at byte offset 8 to 2.5

مناقشة حول ترتيب البايتات

ترتيب البايتات هو الترتيب الذي يتم به تخزين الأرقام المكوّنة من عدة بايتات في ذاكرة الكمبيوتر. يصف مصطلح big-endian بنية وحدة المعالجة المركزية التي تخزِّن البايت الأكثر أهمية أولاً، وlittle-endian، البايت الأقل أهمية أولاً. إنّ ترتيب البايتات الذي يتم استخدامه في بنية وحدة معالجة مركزية معيّنة هو أمر عشوائي تمامًا، وهناك أسباب وجيهة لاختيار أيّ منهما. في الواقع، يمكن ضبط بعض وحدات المعالجة المركزية لتتوافق مع بيانات تنسيق big-endian وlittle-endian.

لماذا عليك الاهتمام بترتيب البايتات؟ السبب بسيط. عند قراءة البيانات أو كتابتها من القرص أو الشبكة، يجب تحديد ترتيب البتات في البيانات. يضمن ذلك تفسير البيانات بشكل صحيح، بغض النظر عن ترتيب البتات في وحدة المعالجة المركزية التي تعمل معها. في عالمنا المتزايد الشبكات، من الضروري توفير التوافق بشكل صحيح مع جميع أنواع الأجهزة، سواء كانت تستخدم ترتيب البايتات الكبير أو الصغير، والتي قد تحتاج إلى العمل مع البيانات الثنائية الواردة من الخوادم أو الأجهزة المشابهة الأخرى على الشبكة.

تم تصميم واجهة DataView خصيصًا لقراءة البيانات وكتابتها من وإلى الملفات والشبكة. تعمل DataView على البيانات التي تتضمّن ترتيبًا محددًا لترتيب البايتات. يجب تحديد ترتيب البتات، سواء كان كبيرًا أو صغيرًا، عند الوصول إلى كل قيمة، ما يضمن حصولك على نتائج متسقة وصحيحة عند قراءة البيانات الثنائية أو كتابتها، بغض النظر عن ترتيب البتات في وحدة المعالجة المركزية التي يعمل عليها المتصفّح.

عادةً، عندما يقرأ تطبيقك البيانات الثنائية من خادم، عليك فحصها مرة واحدة لتحويلها إلى هياكل البيانات التي يستخدمها تطبيقك داخليًا. يجب استخدام DataView خلال هذه المرحلة. لا يُنصح باستخدام طرق عرض الصفائف المُصنَّفة التي تتألف من وحدات متعددة من البايت (Int16Array وUint16Array وما إلى ذلك) مباشرةً مع البيانات التي يتم جلبها من خلال XMLHttpRequest أو FileReader أو أي واجهة برمجة تطبيقات أخرى للإدخال/الإخراج، لأنّ طرق عرض الصفائف المُصنَّفة تستخدِم ترتيب البايتات الأصلي لوحدة المعالجة المركزية. سنقدّم المزيد من المعلومات حول هذا الموضوع لاحقًا.

لنلقِ نظرة على بعض الأمثلة البسيطة. كان تنسيق ملف Windows BMP هو التنسيق العادي لتخزين الصور في الأيام الأولى من نظام التشغيل Windows. تشير المستندات المرتبطة أعلاه بوضوح إلى أنّه يتم تخزين جميع القيم الصحيحة في الملف بتنسيق little-endian. في ما يلي مقتطف رمز تحليلي يحلّل بداية رأس BMP باستخدام مكتبة DataStream.js المصاحبة لهذه المقالة:

function parseBMP(arrayBuffer) {
  var stream = new DataStream(arrayBuffer, 0,
    DataStream.LITTLE_ENDIAN);
  var header = stream.readUint8Array(2);
  var fileSize = stream.readUint32();
  // Skip the next two 16-bit integers
  stream.readUint16();
  stream.readUint16();
  var pixelOffset = stream.readUint32();
  // Now parse the DIB header
  var dibHeaderSize = stream.readUint32();
  var imageWidth = stream.readInt32();
  var imageHeight = stream.readInt32();
  // ...
}

في ما يلي مثال آخر من العرض التوضيحي لعرض النطاق العالي الديناميكية في مشروع عيّنات WebGL. ينزِّل هذا العرض التوضيحي بيانات نقطية عائمة بترتيب الوحدات الأقل أهمية أولاً وتمثل نسيجًا بدرجة عالية من النطاق الديناميكي، ويجب تحميلها إلى WebGL. في ما يلي مقتطف من الرمز البرمجي الذي يفسّر بشكل صحيح قيم النقطة العائمة على جميع تصاميم وحدة المعالجة المركزية. لنفترض أنّ المتغيّر "arrayBuffer" هو ArrayBuffer تم تنزيله للتو من الخادم عبر XMLHttpRequest:

var arrayBuffer = ...;
var data = new DataView(arrayBuffer);
var tempArray = new Float32Array(
  data.byteLength / Float32Array.BYTES_PER_ELEMENT);
var len = tempArray.length;
// Incoming data is raw floating point values
// with little-endian byte ordering.
for (var jj = 0; jj < len; ++jj) {
  tempArray[jj] =
    data.getFloat32(jj * Float32Array.BYTES_PER_ELEMENT, true);
}
gl.texImage2D(...other arguments...,
  gl.RGB, gl.FLOAT, tempArray);

وقاعدة عامة هي: عند تلقّي بيانات ثنائية من خادم الويب، عليك إجراء جولة واحدة عليها باستخدام DataView. قراءة القيم الرقمية الفردية وتخزينها في بنية بيانات أخرى، إما عنصر JavaScript (لكميات صغيرة من البيانات المنظَّمة) أو عرض مصفوفة مكتوبة (لمجموعات كبيرة من البيانات) سيضمن ذلك عمل الرمز بشكل صحيح على جميع أنواع وحدات المعالجة المركزية. استخدِم أيضًا DataView لكتابة البيانات في ملف أو على الشبكة، واحرص على تحديد الوسيطة littleEndian بشكلٍ مناسب لأساليب set المختلفة من أجل إنشاء تنسيق الملف الذي تنشئه أو تستخدمه.

تذكَّر أنّ جميع البيانات التي يتم نقلها عبر الشبكة لها تنسيق وترتيب بتات بشكل ضمني (على الأقل لأي قيم متعددة البايتات). احرص على تحديد تنسيق جميع البيانات التي يرسلها تطبيقك عبر الشبكة وتوثيقه بوضوح.

واجهات برمجة تطبيقات المتصفّح التي تستخدِم "المصفوفات المكتوبة"

سأقدّم لك نظرة عامة موجزة على واجهات برمجة تطبيقات المتصفّحات المختلفة التي تستخدِم حاليًا "قوائم مصنّفة". تشمل المجموعة الحالية WebGL وCanvas وWeb Audio API وXMLHttpRequests وWebSockets وWeb Workers وMedia Source API وFile APIs. من قائمة واجهات برمجة التطبيقات، يمكنك ملاحظة أنّ الصفائف من النوع المحدّد مناسبة تمامًا لأعمال الوسائط المتعددة الحسّاسة للأداء، بالإضافة إلى نقل البيانات بطريقة فعّالة.

WebGL

كان أول استخدام للصفائف من النوع المحدّد في WebGL، حيث يتم استخدامها لنقل بيانات المخزن المؤقت وبيانات الصور. لضبط محتوى عنصر مخزن مؤقت في WebGL، استخدِم gl.bufferData() مع مصفوفة من النوع المحدّد.

var floatArray = new Float32Array([1,2,3,4,5,6,7,8]);
gl.bufferData(gl.ARRAY_BUFFER, floatArray);

تُستخدَم أيضًا القوائم المُصنَّفة لنقل بيانات النسيج. في ما يلي مثال أساسي على إدخال محتوى نسيج باستخدام صفيف من النوع المحدّد.

var pixels = new Uint8Array(16*16*4); // 16x16 RGBA image
gl.texImage2D(
  gl.TEXTURE_2D, // target
  0, // mip level
  gl.RGBA, // internal format
  16, 16, // width and height
  0, // border
  gl.RGBA, //format
  gl.UNSIGNED_BYTE, // type
  pixels // texture data
);

ستحتاج أيضًا إلى "قوائم أنواع" لقراءة وحدات البكسل من سياق WebGL.

var pixels = new Uint8Array(320*240*4); // 320x240 RGBA image
gl.readPixels(0, 0, 320, 240, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

Canvas 2D

تم مؤخرًا تصميم عنصر Canvas ImageData للعمل مع مواصفات الصفائف من النوع المحدّد. يمكنك الآن الحصول على تمثيل الصفائف من النوع المحدّد للبكسل في عنصر لوحة. ويُعدّ ذلك مفيدًا لأنّه يمكنك الآن أيضًا إنشاء صفائف بكسل اللوحة وتعديلها بدون الحاجة إلى العبث بعنصر اللوحة.

var imageData = ctx.getImageData(0,0, 200, 100);
var typedArray = imageData.data // data is a Uint8ClampedArray

XMLHttpRequest2

تم تحسين XMLHttpRequest باستخدام Typed Array، ويمكنك الآن تلقّي استجابة Typed Array بدلاً من الحاجة إلى تحليل سلسلة JavaScript إلى Typed Array. وهذا أمر رائع لنقل البيانات التي تم جلبها مباشرةً إلى واجهات برمجة تطبيقات الوسائط المتعددة ولتحليل الملفات الثنائية التي تم جلبها من الشبكة.

ما عليك سوى ضبط responseType لكائن XMLHttpRequest على arraybuffer.

xhr.responseType = 'arraybuffer';

تذكَّر أنّه عليك الانتباه إلى مشاكل ترتيب البايتات عند تنزيل البيانات من الشبكة. راجِع القسم الخاص بترتيب البايتات أعلاه.

واجهات برمجة تطبيقات الملفات

يمكن لـ FileReader قراءة محتوى الملفات كـ ArrayBuffer. يمكنك بعد ذلك إرفاق طرق عرض المصفوفات المكتوبة وDataViews بالذاكرة المؤقتة لتعديل محتوياتها.

reader.readAsArrayBuffer(file);

يجب أيضًا مراعاة ترتيب البتات. اطّلِع على قسم ترتيب البايتات لمعرفة التفاصيل.

العناصر القابلة للنقل

تؤدي العناصر القابلة للنقل في postMessage إلى تسريع عملية نقل البيانات الثنائية إلى النوافذ الأخرى وWeb Workers بشكل كبير. عند إرسال عنصر إلى Worker كعنصر قابل للنقل، يصبح العنصر غير قابل للوصول إليه في سلسلة المحادثات المرسَلة ويحصل Worker المستلِم على ملكية العنصر. يتيح ذلك تنفيذًا محسّنًا للغاية لا يتم فيه نسخ البيانات المُرسَلة، بل يتم فقط نقل ملكية الصفيف المُصنَّف إلى المستلِم.

لاستخدام العناصر القابلة للنقل مع Web Workers، عليك استخدام طريقة webkitPostMessage في Worker. تعمل طريقة webkitPostMessage تمامًا مثل postMessage، ولكنها تأخذ مَعلمتَين بدلاً من مَعلمة واحدة فقط. الوسيطة الثانية المُضافة هي صفيف من العناصر التي تريد نقلها إلى العامل.

worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);

لاسترداد العناصر من العامل، يمكن للعامل تمريرها مرة أخرى إلى سلسلة المهام الرئيسية بالطريقة نفسها.

webkitPostMessage({results: grand, youCanHaveThisBack: oneGBTypedArray}, [oneGBTypedArray]);

ما مِن نُسخ، رائع.

Media Source API

مؤخرًا، حصلت عناصر الوسائط أيضًا على بعض المزايا الرائعة في Typed Array في شكل Media Source API. يمكنك تمرير مصفوفة من النوع Typed Array تحتوي على بيانات الفيديو مباشرةً إلى عنصر فيديو باستخدام webkitSourceAppend. يؤدي ذلك إلى إلحاق عنصر الفيديو ببيانات الفيديو بعد الفيديو الحالي. إنّ SourceAppend رائع لإنشاء إعلانات بينية وقوائم تشغيل وعمليات بث واستخدامات أخرى قد تحتاج فيها إلى تشغيل عدة فيديوهات باستخدام عنصر فيديو واحد.

video.webkitSourceAppend(uint8Array);

WebSockets الثنائية

يمكنك أيضًا استخدام "المصفوفات من النوع المحدّد" مع WebSockets لتجنُّب الحاجة إلى تحويل جميع بياناتك إلى سلسلة. وهي رائعة لكتابة بروتوكولات فعّالة والحدّ من عدد عمليات نقل البيانات في الشبكة.

socket.binaryType = 'arraybuffer';

رائع. هذا كلّ ما يتعلّق بمراجعة واجهة برمجة التطبيقات. لننتقل إلى الاطّلاع على المكتبات التابعة لجهات خارجية لمعالجة القوائم من النوع المحدّد.

المكتبات التابعة لجهات خارجية

jDataView

ينفِّذ jDataView عنصرًا بديلاً لـ DataView في جميع المتصفّحات. كانت ميزة DataView متاحة في WebKit فقط، ولكنّها أصبحت متاحة الآن في معظم المتصفّحات الأخرى. يعمل فريق المطوّرين في Mozilla على طرح تصحيح لتفعيل DataView على Firefox أيضًا.

كتب "إريك بيدلمان" من فريق العلاقات مع المطوّرين في Chrome مثالًا صغيرًا لقارئ علامات MP3 ID3 يستخدم jDataView. في ما يلي مثال على الاستخدام من مشاركة المدونة:

var dv = new jDataView(arraybuffer);

// "TAG" starts at byte -128 from EOF.
// See http://en.wikipedia.org/wiki/ID3
if (dv.getString(3, dv.byteLength - 128) == 'TAG') {
  var title = dv.getString(30, dv.tell());
  var artist = dv.getString(30, dv.tell());
  var album = dv.getString(30, dv.tell());
  var year = dv.getString(4, dv.tell());
} else {
  // no ID3v1 data found.
}

ترميز_السلسلة

إنّ العمل مع السلاسل في القوائم من النوع المحدّد ليس أمرًا سهلاً في الوقت الحالي، ولكن هناك مكتبة ترميز السلاسل التي تساعد في ذلك. تُنفِّذ Stringencoding مواصفات ترميز سلاسل المصفوفات من النوع المقترَحة، لذا فهي أيضًا طريقة جيدة للتعرّف على الميزات القادمة.

في ما يلي مثال أساسي على استخدام ترميز السلسلة:

var uint8array = new TextEncoder(encoding).encode(string);
var string = new TextDecoder(encoding).decode(uint8array);

BitView.js

لقد كتبت مكتبة صغيرة لتعديل الوحدات النمطية في الصفيفات المُصنَّفة باسم BitView.js. وكما يشير الاسم، تعمل هذه الأداة بشكلٍ مشابه جدًا لأداة DataView، باستثناء أنّها تعمل مع الوحدات. باستخدام BitView، يمكنك الحصول على قيمة بت وضبطها عند إزاحة بت معيّنة في ArrayBuffer. تتضمّن BitView أيضًا طرقًا لتخزين وتحميل الأعداد الصحيحة ذات الـ 6 بت والـ 12 بت في أماكن عشوائية للبت.

إنّ الأعداد الصحيحة ذات الـ 12 بت مناسبة للعمل مع إحداثيات الشاشة، لأنّ الشاشات غالبًا ما تحتوي على أقل من 4096 بكسل على طول البُعد الأطول. باستخدام الأعداد الصحيحة 12 بت بدلاً من الأعداد الصحيحة 32 بت، يمكنك تقليل الحجم بنسبة% 62. في مثال أكثر تطرفاً، كنت أعمل مع ملفات Shapefile التي تستخدم أعدادًا عائمة بسعة 64 بت للإحداثيات، ولكنّني لم أكن بحاجة إلى الدقة لأنّه سيتم عرض النموذج فقط بحجم الشاشة. أدى التبديل إلى إحداثيات أساسية بسعة 12 بت مع اختلافات بسعة 6 بت لتشفير التغييرات من الإحداثيات السابقة إلى خفض حجم الملف إلى عشر الحجم السابق. يمكنك الاطّلاع على العرض التوضيحي لذلك هنا.

في ما يلي مثال على استخدام BitView.js:

var bv = new BitView(arrayBuffer);
bv.setBit(4, 1); // Set fourth bit of arrayBuffer to 1.
bv.getBit(17); // Get 17th bit of arrayBuffer.

bv.getBit(50*8 + 3); // Get third bit of 50th byte in arrayBuffer.

bv.setInt6(3, 18); // Write 18 as a 6-bit int to bit position 3 in arrayBuffer.
bv.getInt12(9); // Read a 12-bit int from bit position 9 in arrayBuffer.

DataStream.js

من أهم الميزات المثيرة للاهتمام في الصفائف المُحدَّدة النوع هي تسهيل التعامل مع الملفات الثنائية في JavaScript. بدلاً من تحليل سلسلة حرفًا حرفًا وتحويل الأحرف يدويًا إلى أرقام ثنائية وما إلى ذلك، يمكنك الآن الحصول على ArrayBuffer باستخدام XMLHttpRequest ومعالجته مباشرةً باستخدام DataView. يسهّل ذلك، على سبيل المثال، تحميل ملف MP3 وقراءة علامات البيانات الوصفية لاستخدامها في مشغّل الصوت. أو يمكنك تحميل ملف Shapefile وتحويله إلى نموذج WebGL. أو يمكنك قراءة علامات EXIF من ملف JPEG وعرضها في تطبيق عرض الشرائح.

تكمن مشكلة طلبات XHR التي تستخدم ArrayBuffer في أنّ قراءة البيانات التي تشبه البنية من المخزن المؤقت أمر صعب بعض الشيء. تناسب DataView قراءة بعض الأرقام في المرة الواحدة بطريقة آمنة لترتيب البايتات، بينما تناسب طرق عرض الصفيفات المُصنَّفة قراءة صفائف من الأرقام الأصلية لترتيب البايتات والتي تكون مُحاذية لحجم العنصر. ما كنا نعتقد أنّه غير متوفّر هو طريقة لقراءة صفائف وبنى البيانات بطريقة ملائمة وآمنة من حيث ترتيب البايتات. أدخِل DataStream.js.

DataStream.js هي مكتبة مصفوفات من النوع الذي يقرأ ويكتب القيم السلاسل والمصفوفات وبنى البيانات من ArrayBuffers بطريقة تشبه الملفات.

مثال على قراءة صفيف من الأعداد العائمة من ArrayBuffer:

// without DataStream.js
var dv = new DataView(buffer);
var f32 = new Float32Array(buffer.byteLength / 4);
var littleEndian = true;
for (var i = 0; i<f32.length; i++) {
  f32[i] = dv.getFloat32(i*4, littleEndian);
}

// with DataStream.js
var ds = new DataStream(buffer);
ds.endianness = DataStream.LITTLE_ENDIAN;
var f32 = ds.readFloat32Array(ds.byteLength / 4);

تُعدّ DataStream.js مفيدة حقًا عند قراءة بيانات أكثر تعقيدًا. لنفترض أنّ لديك طريقة لقراءة علامات JPEG:

// without DataStream.js
var dv = new DataView(buffer);
var objs = [];
for (var i=0; i<buffer.byteLength;) {
  var obj = {};
  obj.tag = dv.getUint16(i);
  i += 2;
  obj.length = dv.getUint16(i);
  i += 2;
  obj.data = new Uint8Array(obj.length - 2);
  for (var j=0; j<obj.data.length; j++,i++) {
    obj.data[j] = dv.getUint8(i);
  }
  objs.push(obj);
}

// with DataStream.js
var ds = new DataStream(buffer);
ds.endianness = ds.BIG_ENDIAN;
var objs = [];
while (!ds.isEof()) {
  var obj = {};
  obj.tag = ds.readUint16();
  obj.length = ds.readUint16();
  obj.data = ds.readUint8Array(obj.length - 2);
  objs.push(obj);
}

أو استخدِم الطريقة DataStream.readStruct لقراءة بنى البيانات. تأخذ طريقة readStruct صفيفًا لتعريف البنية يحتوي على أنواع عناصر البنية. تتضمّن دوالّ استدعاء لمعالجة الأنواع المعقّدة ومعالجة صفائف البيانات والبنى المتداخلة أيضًا:

// with DataStream.readStruct
ds.readStruct([
  'objs', ['[]', [ // objs: array of tag,length,data structs
    'tag', 'uint16',
    'length', 'uint16',
    'data', ['[]', 'uint8', function(s,ds){ return s.length - 2; }], // get length with a function
  '*'] // read in as many struct as there are
]);

كما ترى، تعريف البنية هو صفيف مسطّح من أزواج [name, type]. يتم إنشاء البنى المُدمجة من خلال إنشاء مصفوفة للنوع. يتم تعريف المصفوفات باستخدام مصفوفة من ثلاثة عناصر يكون فيها العنصر الثاني هو نوع عنصر المصفوفة والعنصر الثالث هو طول المصفوفة (إما كعدد أو كمرجع إلى حقل سبق قراءته أو كدالة استدعاء). العنصر الأول في تعريف الصفيف غير مستخدَم.

في ما يلي القيم المحتمَلة للنوع:

Number types

Unsuffixed number types use DataStream endianness.
To explicitly specify endianness, suffix the type with
'le' for little-endian or 'be' for big-endian,
e.g. 'int32be' for big-endian int32.

  'uint8' -- 8-bit unsigned int
  'uint16' -- 16-bit unsigned int
  'uint32' -- 32-bit unsigned int
  'int8' -- 8-bit int
  'int16' -- 16-bit int
  'int32' -- 32-bit int
  'float32' -- 32-bit float
  'float64' -- 64-bit float

String types

  'cstring' -- ASCII string terminated by a zero byte.
  'string:N' -- ASCII string of length N.
  'string,CHARSET:N' -- String of byteLength N encoded with given CHARSET.
  'u16string:N' -- UCS-2 string of length N in DataStream endianness.
  'u16stringle:N' -- UCS-2 string of length N in little-endian.
  'u16stringbe:N' -- UCS-2 string of length N in big-endian.

Complex types

  [name, type, name_2, type_2, ..., name_N, type_N] -- Struct

  function(dataStream, struct) {} -- Callback function to read and return data.

  {get: function(dataStream, struct) {}, set: function(dataStream, struct) {}}
  -- Getter/setter functions to reading and writing data. Handy for using the
     same struct definition for both reading and writing.

  ['', type, length] -- Array of given type and length. The length can be either
                        a number, a string that references a previously-read
                        field, or a callback function(struct, dataStream, type){}.
                        If length is set to '*', elements are read from the
                        DataStream until a read fails.

يمكنك الاطّلاع على مثال مباشر لقراءة البيانات الوصفية في ملف JPEG هنا. يستخدم الإصدار التجريبي DataStream.js لقراءة البنية على مستوى العلامة في ملف JPEG (بالإضافة إلى بعض عمليات تحليل EXIF) وjpg.js لفك ترميز صورة JPEG وعرضها في JavaScript.

تاريخ المصفوفات المكتوبة

بدأت مصفوفات النصوص في مرحلة التنفيذ المبكّرة لواجهة برمجة التطبيقات WebGL، عندما تبيّن لنا أنّ تمرير صفائف JavaScript إلى برنامج تشغيل الرسومات كان يتسبب في مشاكل في الأداء. في مصفوفات JavaScript، كان على عملية ربط WebGL تخصيص مصفوفة أصلية وملؤها من خلال التنقّل في مصفوفة JavaScript وتحويل كل كائن JavaScript في المصفوفة إلى النوع الأصلي المطلوب.

لحلّ مشكلة تحويل البيانات، كتب فلاديمير فويكيفيتش من Mozilla CanvasFloatArray: صفيف أعداد كسرية بأسلوب C مع واجهة JavaScript. يمكنك الآن تعديل CanvasFloatArray في JavaScript ونقله مباشرةً إلى WebGL بدون الحاجة إلى إجراء أي عمل إضافي في عملية الربط. في النُسخ اللاحقة، تمت إعادة تسمية CanvasFloatArray إلى WebGLFloatArray، ثم تمت إعادة تسميتها إلى Float32Array وتم تقسيمها إلى ArrayBuffer أساسي وعرض Float32Array مكتوب للوصول إلى المخزن المؤقت. تمت أيضًا إضافة أنواع لأحجام الأعداد الصحيحة والعدد الثنائي العشري الأخرى وأنواع الأرقام الموجبة/السلبية.

اعتبارات التصميم

منذ البداية، كان تصميم "المصفوفات من النوع المحدّد" مدفوعًا بالحاجة إلى تمرير البيانات الثنائية بكفاءة إلى المكتبات الأصلية. لهذا السبب، تعمل طرق عرض الصفائف من النوع على البيانات المنسَّقة في ترتيب البايتات الأصلي لوحدة المعالجة المركزية للمضيف. تتيح هذه القرارات لـ JavaScript تحقيق أقصى أداء أثناء العمليات، مثل إرسال بيانات رؤوس المضلّعات إلى بطاقة الرسومات.

تم تصميم DataView خصيصًا لنقل البيانات إلى الملفات والشبكات، حيث يكون للبيانات دائمًا ترتيب محدد لترتيب البايتات، وقد لا تكون مُحسَّنة لتحقيق أقصى أداء.

تم تقسيم التصميم بين تجميع البيانات في الذاكرة (باستخدام طرق عرض المصفوفات المُحدَّدة) وعمليات الإدخال/الإخراج (باستخدام DataView) بشكل مدروس. تعمل محرّكات JavaScript الحديثة على تحسين طرق عرض الصفيفات المكتوبة بشكل كبير، وتحقيق أداء عالٍ في العمليات الحسابية باستخدامها. تمكّنت مستويات الأداء الحالية لعروض الصفيفات المكتوبة من خلال هذا القرار التصميمي.

المراجع