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

مقدمة

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

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

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

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

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

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

// 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 للمخازن الاحتياطية لطرق العرض واستخدم مجموعة لنسخ البيانات.

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

في المثال أعلاه، جميع القيم التي أقرأها كبيرة النهاية. إذا كانت القيم الموجودة في المخزن المؤقت صغيرة النهاية، فيمكنك تمرير المعلمة newEndian الاختيارية إلى getter:

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

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

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

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

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

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

لنلقِ نظرة على مثالين بسيطين. كان تنسيق ملف Windows BMP هو التنسيق العادي لتخزين الصور في الأيام الأولى من نظام Windows. تشير الوثائق المرتبطة أعلاه بوضوح إلى أن جميع قيم الأعداد الصحيحة في الملف مخزنة بتنسيق صغير. في ما يلي مقتطف من الرمز يحلّل بداية عنوان 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. في ما يلي مقتطف الرمز الذي يفسر قيم النقطة العائمة بشكل صحيح في جميع بُنى وحدة المعالجة المركزية (CPU). لنفترض أنّ المتغيّر "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 (للكميات الصغيرة من البيانات المنظَّمة) أو طريقة عرض صفيف مكتوب (للكتل الكبيرة من البيانات). وسيضمن ذلك عمل الرمز بشكلٍ صحيح على جميع أنواع وحدات المعالجة المركزية (CPU). استخدِم أيضًا 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);

لوحة الرسم ثنائية الأبعاد

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

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

XMLHttpRequest2

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

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

xhr.responseType = 'arraybuffer';

تذكر أنه يجب أن تكون على دراية بمشكلات النهاية عند تنزيل البيانات من الشبكة! راجع القسم الذي يتناول عملية التحويل أعلاه.

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

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

reader.readAsArrayBuffer(file);

يجب أيضًا أن تضعوا الاهتمام في الاعتبار هنا أيضًا. للحصول على التفاصيل، يمكنك الاطلاع على قسم تقنية المعلومات.

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

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

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

worker.webkitPostMessage(oneGBTypedArray, [oneGBTypedArray]);

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

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

ليس هناك أي نسخ، يا للروعة!

واجهة برمجة تطبيقات مصدر الوسائط

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

video.webkitSourceAppend(uint8Array);

مآخذ الويب الثنائية

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

socket.binaryType = 'arraybuffer';

واو! هذا يختتم مراجعة واجهة برمجة التطبيقات. لننتقل إلى مكتبات الجهات الخارجية للتعامل مع الصفائف المكتوبة.

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

jDataView

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

كتب "إريك بيدلمان" ضمن فريق علاقات مطوّري برامج Chrome مثالاً صغيرًا لقارئ علامات ID3 ID3 بتنسيق MP3 يستخدم 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.
}

ترميز السلاسل النصية

يمثّل العمل على السلاسل في Typed Arrays إزعاجًا في الوقت الحالي، ولكن هناك مكتبة ترميز السلسلة التي تساعدك في هذا الأمر. ينفِّذ 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%. للحصول على مثال أكثر شمولية، كنت أعمل على ملفات الأشكال التي تستخدم أعداد عشرية 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 للوصول إلى المخزن المؤقت. تمّت إضافة الأنواع أيضًا لأحجام الأعداد الصحيحة والنقاط العائمة والصيغ الأخرى الموقَّعة/غير الموقَّعة.

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

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

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

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

المراجع