Implementación de APIs de Composición de Vue 3: reactive, ref y más

Este artículo explora la implementación manual de varias APIs de composición clave en Vue 3, como reactive, ref, shallowReactive, shallowRef, readonly, shallowReadonly, isRef, isReactive y isReadonly. El objetivo es comprender los mecanismos subyacentes que permiten a Vue 3 lograr la reactividad.

  1. Implementando shallowReactive y reactive

Estas funciones se encargan de la reactividad de objetos y arrays. Utilizamos Proxy para interceptar las operaciones de obtención y establecimiento de propiedades. reactive aplica la reactividad de forma recursiva a todos los niveles del objeto, mientras que shallowReactive solo la aplica al nivel superior.

El manejador reactiveHandler define el comportamiento:

  • get: Devuelve el valor de la propiedad. Si la clave es '_is_reactive', devuelve true para identificarlo como un objeto reactivo.
  • set: Actualiza el valor de la propiedad y regsitra un mensaje indicando que los datos han sido actualizados y la interfaz de usuario debería ser notificada.
  • deleteProperty: Elimina la propiedad y registra un mensaje similar.

const reactiveHandler = {
  get (target, key) {
    if (key === '_is_reactive') return true;
    return Reflect.get(target, key);
  },
  set (target, key, value) {
    const result = Reflect.set(target, key, value);
    console.log('Data updated, triggering UI refresh...');
    return result;
  },
  deleteProperty (target, key) {
    const result = Reflect.deleteProperty(target, key);
    console.log('Data deleted, triggering UI refresh...');
    return result;
  },
};

function shallowReactive(obj) {
  // Solo aplica el proxy al objeto principal
  return new Proxy(obj, reactiveHandler);
}

function reactive(target) {
  if (target && typeof target === 'object') {
    if (Array.isArray(target)) {
      // Recorre el array y aplica reactive a cada elemento
      target.forEach((item, index) => {
        target[index] = reactive(item);
      });
    } else {
      // Recorre las propiedades del objeto y aplica reactive a cada valor
      Object.keys(target).forEach(key => {
        target[key] = reactive(target[key]);
      });
    }
    // Aplica el proxy al objeto o array principal
    const proxy = new Proxy(target, reactiveHandler);
    return proxy;
  }
  return target; // Devuelve el valor si no es un objeto/array
}

// Prueba de shallowReactive
const shallowProxy = shallowReactive({ a: { b: 3 } });
shallowProxy.a = { b: 4 }; // Se intercepta
shallowProxy.a.b = 5;     // No se intercepta, solo el nivel superior es reactivo

// Prueba de reactive
const objToMakeReactive = {
  a: 'initial string',
  b: [{ x: 1 }],
  c: { x: [11] },
};
const deeplyReactiveObj = reactive(objToMakeReactive);
console.log('Deeply reactive object:', deeplyReactiveObj);
deeplyReactiveObj.b[0].x += 1; // Modificación anidada
deeplyReactiveObj.c.x[0] += 1; // Modificación anidada
console.log('After nested modifications:', deeplyReactiveObj);

  1. Implementando shallowRef y ref

Las funciones ref y shallowRef se utilizan para crear referencias reactivas. Un ref envuelve un valor, y los cambios en .value desencadenan actualizaciones. ref aplica reactividad profunda a los objetos envueltos, mientras que shallowRef solo crea una referencia reactiav al objeto en sí, sin hacerlo profundo.

Ambas implementaciones crean un objeto con una propiedad value para acceder y modificar el dato, y una bandera _is_ref para identificación.


function shallowRef(target) {
  const refObject = {
    _value: target, // Almacena el valor internamente
    _is_ref: true,  // Identificador de ref
    get value() {
      return this._value;
    },
    set value(val) {
      this._value = val;
      console.log('shallowRef value updated, triggering UI refresh...');
    }
  };
  return refObject;
}

function ref(target) {
  // Si el valor es un objeto, lo hace reactivo con reactive()
  if (target && typeof target === 'object') {
    target = reactive(target);
  }

  const refObject = {
    _value: target,
    _is_ref: true,
    get value() {
      return this._value;
    },
    set value(val) {
      // Si el nuevo valor es un objeto, también lo hace reactivo
      this._value = (val && typeof val === 'object') ? reactive(val) : val;
      console.log('ref value updated, triggering UI refresh...');
    }
  };
  return refObject;
}

// Prueba de shallowRef
const shallowRefExample = shallowRef({ a: 'abc' });
shallowRefExample.value = 'xxx'; // Se actualiza la referencia
shallowRefExample.value.a = 'yyy'; // No se intercepta, solo el objeto principal es reactivo

// Prueba de ref
const simpleRef = ref(0);
const complexRef = ref({
  a: 'initial complex',
  b: [{ x: 1 }],
  c: { x: [11] },
});

simpleRef.value++; // Actualiza el valor simple
complexRef.value.b[0].x++; // Modifica un objeto anidado (gracias a reactive dentro de ref)
complexRef.value.c.x[0]++; // Modifica un array anidado
console.log('Simple ref:', simpleRef);
console.log('Complex ref:', complexRef);

  1. Implementando shallowReadonly y readonly

Estas funciones crean versiones de solo lectura de objetos. readonly hace que todos los niveles de un objeto sean de solo lectura, mientras que shallowReadonly solo lo aplica al nivel superior.

El manejador readonlyHandler intercepta las operaciones set y deleteProperty, emitiendo advertencias y devolviendo true sin realizar cambios.


const readonlyHandler = {
  get (target, key) {
    if (key === '_is_readonly') return true; // Identificador
    return Reflect.get(target, key);
  },
  set () {
    console.warn('Attempted to set property on a readonly object.');
    return true; // Indica éxito, aunque no se modifica
  },
  deleteProperty () {
    console.warn('Attempted to delete property from a readonly object.');
    return true;
  },
};

function shallowReadonly(obj) {
  // Aplica el proxy solo al nivel superior
  return new Proxy(obj, readonlyHandler);
}

function readonly(target) {
  if (target && typeof target === 'object') {
    if (Array.isArray(target)) {
      // Aplica readonly recursivamente a los elementos del array
      target.forEach((item, index) => {
        target[index] = readonly(item);
      });
    } else {
      // Aplica readonly recursivamente a las propiedades del objeto
      Object.keys(target).forEach(key => {
        target[key] = readonly(target[key]);
      });
    }
    // Aplica el proxy al objeto/array principal
    const proxy = new Proxy(target, readonlyHandler);
    return proxy;
  }
  return target;
}

// Prueba de shallowReadonly
const shallowReadonlyObj = shallowReadonly({ a: { b: 1 } });
shallowReadonlyObj.a = { b: 2 };     // Advertencia, no se modifica
shallowReadonlyObj.a.b = 3;         // Advertencia, no se modifica

// Prueba de readonly
const fullyReadonlyObj = readonly({ a: { b: 1 } });
fullyReadonlyObj.a = { b: 2 };     // Advertencia, no se modifica
fullyReadonlyObj.a.b = 3;         // Advertencia, no se modifica
console.log('Shallow readonly:', shallowReadonlyObj);
console.log('Fully readonly:', fullyReadonlyObj);

  1. Implementando isRef, isReactive y isReadonly

Estas funciones de utilidad permiten verificar el tipo de objeto reactivo.


function isRef(obj) {
  // Verifica la existencia del objeto y la bandera '_is_ref'
  return obj !== null && typeof obj === 'object' && obj._is_ref === true;
}

function isReactive(obj) {
  // Verifica la existencia del objeto y la bandera '_is_reactive'
  return obj !== null && typeof obj === 'object' && obj._is_reactive === true;
}

function isReadonly(obj) {
  // Verifica la existencia del objeto y la bandera '_is_readonly'
  return obj !== null && typeof obj === 'object' && obj._is_readonly === true;
}

/* 
Función auxiliar para verificar si es un proxy de reactive o readonly
*/
function isProxy(obj) {
  return isReactive(obj) || isReadonly(obj);
}

// Pruebas de las funciones de verificación
const reactiveObj = reactive({});
const refObj = ref({});
const readonlyObj = readonly({});

console.log('Is reactive (reactiveObj)?', isReactive(reactiveObj));       // true
console.log('Is ref (refObj)?', isRef(refObj));                         // true
console.log('Is readonly (readonlyObj)?', isReadonly(readonlyObj));     // true
console.log('Is proxy (reactiveObj)?', isProxy(reactiveObj));           // true
console.log('Is proxy (readonlyObj)?', isProxy(readonlyObj));         // true
console.log('Is reactive (refObj)?', isReactive(refObj));               // false
console.log('Is readonly (reactiveObj)?', isReadonly(reactiveObj));     // false

Etiquetas: vue.js JavaScript reactive ref Composition API

Publicado el 6-24 00:58