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

مقدمة

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

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

DataView هو النوع الثاني من طريقة العرض وهو مخصّص لمعالجة البيانات غير المتجانسة. بدلاً من استخدام واجهة برمجة تطبيقات تشبه المصفوفة، يوفّر لك عنصر DataView واجهة برمجة تطبيقات get/set لقراءة أنواع البيانات العشوائية وكتابتها باستخدام إزاحة البايت العشوائية. يعمل 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);
}

في المثال أعلاه، جميع القيم التي أقرأها هي big-endian. إذا كانت القيم في المخزن المؤقت بترتيب 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، البايت الأقل أهمية أولاً. إنّ ترتيب البتات في بنية وحدة المعالجة المركزية (CPU) معيّنة هو أمر عشوائي تمامًا، وهناك أسباب وجيهة لاختيار أيّ منهما. ففي الواقع، يمكن تكوين بعض وحدات المعالجة المركزية (CPU) لتتوافق مع البيانات الكبيرة والصغيرة على حد سواء.

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

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

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

في ما يلي بعض الأمثلة البسيطة. كان تنسيق ملف 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 على تعزيز المصفوفة المكتوبة، ويمكنك الآن تلقي استجابة المصفوفة المكتوبة بدلاً من تحليل سلسلة جافا سكريبت إلى مصفوفة مكتوبة. وهذا أمر ممتاز لتمرير البيانات التي تم جلبها مباشرةً إلى واجهات برمجة تطبيقات الوسائط المتعددة ولتحليل الملفات الثنائية التي تم جلبها من الشبكة.

كل ما عليك فعله هو ضبط 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

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

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

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

BitView.js

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

تكمن مشكلة ArrayBuffer XHR في أن قراءة البيانات الشبيهة بالهيكل من المخزن المؤقت يعد أمرًا صعبًا. تُعدّ أداة 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 الحديثة على تحسين طرق عرض الصفيفات المكتوبة بشكل كبير، وتحقيق أداء عالٍ في العمليات الرقمية باستخدامها. تمكّنت مستويات الأداء الحالية لعروض الصفيفات المكتوبة من خلال هذا القرار التصميمي.

المراجع