Hablando sobre el principio de respuesta de Vue3 y la interpretación del código fuente

1. Comprender algunos conceptos

que es responsivo

Antes de comenzar con el principio receptivo y el análisis del código fuente, debe comprender qué es receptivo. Primero, aclarar un concepto: la capacidad de respuesta es un proceso , que tiene dos participantes:

  • Activado por: datos

  • Respondedor: función que hace referencia a los datos

Cuando los datos cambien, la función que hace referencia a los datos se volverá a ejecutar automáticamente. Por ejemplo, si los datos se utilizan en la representación de la vista, la vista se actualizará automáticamente después de que cambien los datos, lo que completa un proceso de respuesta.

función de efectos secundarios

Tanto in Vuecomo Reactin tienen el concepto de función de efectos secundarios, ¿qué es una función de efectos secundarios? Si una función se refiere a datos externos, esta función se verá afectada por el cambio de datos externos, decimos que esta función tiene efectos secundarios , que es lo que llamamos una función de efecto secundario . No es fácil entender el nombre al principio, pero de hecho, una función de efecto secundario es una función que hace referencia a datos o una función asociada con datos . Por ejemplo:

<!DOCTYPE html>
<html lang="">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
</head>

<body>
    <div id="app"></div>
    <script>
        const obj = {
            name: 'John',
        }
        // 副作用函数 effect
        function effect() {
            app.innerHTML = obj.name
            console.log('effect', obj.name)
        }

        effect()

        setTimeout(() => {
            obj.name = 'ming'
            // 手动执行 effect 函数
            effect()
        }, 1000);
    </script>
</body>
</html>

En el ejemplo anterior, effectse hace referencia a los datos externos en la función obj.name. Si los datos cambian, afectará a esta función. Las effectfunciones similares son funciones de efectos secundarios.

Pasos básicos para implementar la capacidad de respuesta

En el ejemplo anterior, cuando obj.nameocurre un cambio, effectlo ejecutamos manualmente. Si podemos monitorear obj.nameel cambio y dejar que ejecute automáticamente la función de efecto secundario effect, entonces se realiza el proceso receptivo. De hecho, ya sea Vue2o Vue3, el núcleo de la capacidad de respuesta es el mismo 数据劫持/代理、依赖收集、依赖更新, pero la diferencia en la implementación específica se debe a la diferencia en la forma de realizar el secuestro de datos.

  • Vue2Responsivo: 基于Object.defineProperty()secuestro de datos logrado

  • Vue3Responsivo: basado en Proxyla implementación de un proxy para todo el objeto

Aquí no Vue2nos centramos en la capacidad de respuesta, sino que este artículo se centra en Vue3la realización del principio de capacidad de respuesta.

2. Proxy y reflejo

Antes de analizar Vue3el principio receptivo, primero debe comprender dos nuevas API de ES6: Porxyy Reflect.

Apoderado

Proxy: Proxy, como sugiere su nombre, se utiliza principalmente para crear un proxy para el objeto, a fin de realizar la interceptación y personalización de las operaciones básicas del objeto . Se puede entender que se configura una capa de "intercepción" antes del objeto de destino, y el acceso externo al objeto debe pasar primero a través de esta capa de intercepción, por lo que se proporciona un mecanismo para filtrar y reescribir el acceso externo. Sintaxis básica:

let proxy = new Proxy(target, handler);
  • target: El objeto de destino que necesita ser interceptado

  • handler: también es un objeto, que se utiliza para personalizar el comportamiento de intercepción. Por ejemplo:

const obj = {
    name: 'John',
    age: 16
}

const objProxy = new Proxy(obj,{})
objProxy.age = 20
console.log('obj.age',obj.age);
console.log('objProxy.age',objProxy.age);
console.log('obj与objProxy是否相等',obj === objProxy);
// 输出
[Log] obj.age – 20
[Log] objProxy.age – 20 
[Log] obj与objProxy是否相等 – false

objProxySi está vacío aquí handler, apunta directamente al objeto proxy, y el objeto proxy no es necesita una intercepción más flexible de las operaciones de objetos, debe agregar los handleratributos correspondientes en . Por ejemplo:

const obj = {
    name: 'John',
    age: 16
}

const handler = {
    get(target, key, receiver) {
        console.log(`获取对象属性${key}值`)
        return target[key]
    },
    set(target, key, value, receiver) {
        console.log(`设置对象属性${key}值`)
        target[key] = value
    },
    deleteProperty(target, key) {
        console.log(`删除对象属性${key}值`)
        return delete target[key]
    },
}

const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)

// 输出
[Log] 获取对象属性age值 (example01.html, line 22)
[Log] 16 (example01.html, line 36)
[Log] 设置对象属性age值 (example01.html, line 26)
[Log] 删除对象属性age值 (example01.html, line 30)
[Log] true (example01.html, line 38)

En el ejemplo anterior, definimos set()los atributos get(), y en el capturador e implementamos la intercepción de la operación correcta deleteProperty()a través de la operación correcta . El método de disparo para estas propiedades tiene los siguientes parámetros:proxyobj

  • target-- es el objeto de destino, que se pasa como el primer parámetro anew Proxy

  • key- nombre de atributo de destino

  • value- el valor de la propiedad de destino

  • receiver—— Apunta al contexto correcto para la operación actual. Si la propiedad de destino es una propiedad getterde acceso , entonces receiveres thisel objeto . Por lo general, receivereste es proxyel objeto en sí mismo, pero si proxyheredamos de , nos receiverreferimos al objeto que proxyhereda

  • Por supuesto, además de los tres anteriores, existen algunos métodos de operación de atributos comunes:

    • has(), intercepción: en operador.

    • ownKeys(), para interceptar:Object.getOwnPropertyNames(proxy) Object.getOwnPropertySymbols(proxy) Object.keys(proxy)

    • construct(), Intercepción: newOperación, etc.

Reflejar

Reflect: La reflexión es reflejar el contenido del agente. ReflectAl igual que Proxycon , new también se proporciona ES6para manipular objetos API. Proporciona métodos para interceptar JavaScriptoperaciones, y estos métodos Proxy handlersestán en correspondencia uno a uno con los métodos proporcionados Siempre que sea Proxyun método de objeto, Reflectel método correspondiente se puede encontrar en el objeto. Y Reflectno es un objeto función, es decir, no se puede instanciar, y todas sus propiedades y métodos son estáticos. O el ejemplo anterior

const obj = {
    name: 'John',
    age: 16
}

const handler = {
    get(target, key, receiver) {
        console.log(`获取对象属性${key}值`)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        console.log(`设置对象属性${key}值`)
        return Reflect.set(target, key, value, receiver)
    },
    deleteProperty(target, key) {
        console.log(`删除对象属性${key}值`)
        return Reflect.deleteProperty(target, key)
    },
}

const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)

En el ejemplo anterior

  • Reflect.get()en lugar de target[key]operación

  • Reflect.set()en lugar de target[key] = valueoperación

  • Reflect.deleteProperty()delete target[key]Por supuesto, además de los métodos anteriores, existen algunos métodos comúnmente utilizados para reemplazar operaciones Reflect:

Reflect.construct(target, args)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)

3. Análisis reactivo del código fuente ref.

Después de comprender Proxyy Reflect, veamos Vue3cómo porxyimplementar la capacidad de respuesta. Su núcleo son los dos métodos que se presentarán a continuación: reactiveAquí refse analiza de acuerdo con el código fuente de la versión Vue3.2.

La implementación del código fuente de reactivo

Abra el archivo fuente, busque el archivo packages/reactivity/src/reactive.tsy vea el código fuente.

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}
  • Al principio, targetse realiza el juicio receptivo de solo lectura y, si lo es true, se devolverá directamente target. reactiveEl método principal de implementación es createReactiveObject():

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only a whitelist of value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}
  • createReactiveObject()El método tiene cinco parámetros:

    • target: el objeto de destino original pasado

    • isReadonly: ¿Es un indicador de solo lectura?

    • baseHandlers: proxyEl segundo parámetro al crear un objeto ordinario.handler

    • collectionHandlers: El segundo parámetro al collectioncrear un objeto de tipoproxyhandler

    • proxyMap: WeakMaptipo map, utilizado principalmente para targetalmacenar proxyla correspondencia entre y su

function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}
  • Como puede ver en el código fuente, divide los objetos en COMMONobjetos ( Objecty Array) y COLLECTIONobjetos de tipo ( Map, Set, WeakMap, WeakSet). El objetivo principal de esta distinción es personalizar diferentes tipos de objetos de acuerdo con diferentes tipos de objetos.handler

  • En createReactiveObject()las primeras líneas se hacen una serie de juicios:

    • Primero juzga targetsi es un objeto, si lo es false, directamentereturn

    • Determine targetsi es un objeto sensible, si es así true, directamentereturn

    • Determinar si se ha targetcreado proxy, en caso afirmativo true, directamentereturn

    • Determine targetsi se trata de los 6 tipos de objetos mencionados anteriormente, en caso afirmativo false, directamentereturn

    • Si se cumplen las condiciones anteriores, se targetcrea proxy, y returnesteproxy

handlerEl siguiente paso es pasar un procesamiento lógico diferente de acuerdo con los diferentes tipos de objetos. El enfoque principal es baseHandlersque hay cinco métodos de operación de atributos, que se enfocan en el análisis gety setlos métodos.

Ubicación de la fuente:packages/reactivity/src/baseHandlers.ts

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
getcon colección de dependencia
  • Puede ver mutableHandlersque hay varias funciones de enlace con las que estamos familiarizados. Cuando proxyaccedemos o modificamos el objeto, llamamos a la función correspondiente para su procesamiento. Primero observe getcómo targetrecopilar las funciones de efectos secundarios del acceso:

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
            ? shallowReactiveMap
            : reactiveMap
        ).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)

    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}
  • Si keyel valor es __v_isReactive, __v_isReadonlyregrese en consecuencia, si key==='__v_raw'y el valor WeakMapen no está vacío, luego regresekeytargettarget

  • Si targetes una matriz, invalide /mejore el método correspondiente a la matriz

    Llame a estos métodos track()para la recopilación de dependencias

    • Cómo encontrar elementos de matriz :includes、indexOf、lastIndexOf

    • Cómo modificar la matriz original :push、pop、unshift、shift、splice

  • Juzgar Reflect.get()el valor de retorno del método, es decir, el valor del atributo del objeto de datos actual res, si reses un objeto ordinario y no de solo lectura, solicitar track()la recopilación de dependencias

  • Si reses una respuesta superficial, devuélvela directamente, si reses refun objeto, devuelve su valuevalor

  • Si reses un tipo de objeto y es de solo lectura , llámelo ; de lo contrario, llame al método readonly(res)recursivamentereactive(res)

  • Si no se cumple nada de lo anterior, devolver directamente el valor del atributo

Luego, el método central es si usa ** track()** para procesar la colección de dependencias, el código fuente está en `` paquetes/reactividad/src/efecto.ts`

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!isTracking()) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = createDep()))
  }

  const eventInfo = __DEV__
    ? { effect: activeEffect, target, type, key }
    : undefined

  trackEffects(dep, eventInfo)
}
  • Primero, juzgue si la recopilación de dependencia está en progreso

  • const targetMap = new WeakMap<any, KeyToDepMap>()Cree un targetMapcontenedor para guardar el contenido dependiente relacionado con el objeto receptivo actual, que en sí mismo es un WeakMaptipo

  • Use el objeto receptivotargetMap como la clave , targetMapy el valor es uno depsMap(perteneciente a Mapla instancia ), depsMapque almacena las dependencias específicas keycorrespondientes

  • depsMapLa clave es la clave del objeto de datos de respuesta, y el Valor es una depsinstancia (que pertenece a Setla instancia ). La razón por la que se usa aquí Setes para evitar la adición repetida de funciones de efectos secundarios y llamadas repetidas.

Lo anterior es el proceso central de todo el capturador **get()** y la colección de dependencias.

setactualizar con dependencias

Volvamos baseHandlersa ver Setcómo se realiza la actualización de dependencias en el capturador

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value)
      oldValue = toRaw(oldValue)
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}
  • Primero guarde el valor anterioroldValue

  • Si no es una respuesta superficial targetsino un objeto ordinario, y el valor anterior es un objeto receptivo, realice la operación de asignación: oldValue.value = value, devuelve true, indicando que la asignación fue exitosa

  • Determinar si hay un valor de clave correspondientehadKey

  • Ejecución Reflect.setestableciendo el valor de propiedad correspondiente

  • Si se considera que el objeto es el contenido de la cadena de prototipo original (no agregado por encargo), la actualización de dependencia no se activará

  • Según el objeto de destino no tiene la clave correspondiente, llame triggery actualice la dependencia

Lo anterior es todo el proceso baseHandlerscentral de recopilación de dependencias y actualización de dependencias .

La implementación del código fuente de ref.

Sabemos que refpodemos definir la capacidad de respuesta de los tipos de datos básicos y los tipos de datos de referencia. Echemos un vistazo a su implementación de código fuente:packages/reactivity/src/ref.ts

export function ref(value?: unknown) {
  return createRef(value)
}

function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T, public readonly _shallow = false) {
    this._rawValue = _shallow ? value : toRaw(value)
    this._value = _shallow ? value : convert(value)
  }

  get value() {
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    newVal = this._shallow ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      triggerRefValue(this, newVal)
    }
  }
}
  • Como se puede ver en el proceso de llamada de función anterior, refel núcleo de la implementación es instanciar un RefImplobjeto. ¿Por qué se crea una instancia de un objeto aquí RefImpl? El propósito es que el objetivo del Proxyproxy también sea un tipo de objeto, y proxyel proxy de datos no se puede realizar creando un tipo de datos básico. Solo los tipos de datos básicos se pueden empaquetar como un objeto, y la recopilación de dependencias y la actualización de dependencias se pueden realizar a través de get、setmétodos

  • Mira RefImplel significado de las propiedades del objeto:

    • _ valor : se usa 保存ref当前值, si el parámetro pasado es un objeto , se usa para guardar el valor convertido por la función reactiva , de lo contrario _value, es _rawValuelo mismo que

    • _ rawValue : se usa para guardar el valor original correspondiente al valor de referencia actual.Si el parámetro pasado es un objeto , se usa para guardar el valor original antes de la conversión, de lo contrario _value, es _rawValuelo mismo que. La función de esta toRaw()función es convertir el objeto receptivo en un objeto normal.

    • dep : Es un Settipo de dato utilizado para almacenar las dependencias de la refcolección de valores actual. En cuanto a por qué Setusamos la explicación anterior, aquí está la misma razón

    • _v_isRef : Marcar bit, siempre que esté refdefinido identificará el dato actual como uno Ref, es decir su valor se marca comotrue

    • Además, se puede ver claramente que el método RefImpl类expuesto al objeto de instancia get、setes valor , por lo que refdebemos traer **.valor** para la operación de los datos de respuesta definidos

  • Si el valor pasado es un tipo de objeto , se llamará convert()al método y reactive()se llamará al método en este método para realizar un procesamiento receptivo en él.

  • RefImplLa clave del ejemplo radica en el procesamiento de las dos funciones de trackRefValue(this)y triggerRefValue(this, newVal). Probablemente también sabemos que son recopilación dependiente y actualización dependiente . El principio es básicamente reactivesimilar al método de procesamiento, por lo que no daré más detalles aquí.

V. Resumen

  • Para el tipo de datos básico , solo puede refdarse cuenta de su capacidad de respuesta a través de El núcleo aún lo empaqueta en un objeto e implementa la recopilación de dependencias y la actualización de dependencias RefImplinternamente a través de custom y . get value()set value(newVal)

  • Para el tipo de objeto , refambos reactivepueden convertirlo en datos receptivos , pero refinternamente, eventualmente llamará reactivea una función para realizar la conversión. reactiveFunción, principalmente a través de 创建了Proxy实例对象, a través de Reflectla adquisición y modificación de datos.

Algunas referencias:

https://github.com/vuejs/vue

https://zh.javascript.info/proxy#reflect

- FIN -

Acerca de Qi Wu Troupe

Qi Wu Troupe es el equipo front-end más grande de 360 ​​Group y participa en el trabajo de los miembros de W3C y ECMA (TC39) en nombre del grupo. Qi Wu Troupe otorga gran importancia a la capacitación de talentos y tiene varias direcciones de desarrollo, como ingenieros, profesores, traductores, personas de interfaz comercial y líderes de equipo para que los empleados elijan, y brinda el curso de capacitación técnica, profesional, general y de liderazgo correspondiente. Qi Dance Troupe da la bienvenida a todo tipo de talentos destacados para que presten atención y se unan a Qi Dance Troupe con una actitud abierta y de búsqueda de talentos.

4e6f39033c9c9821f060ffa32caf7fda.png

Supongo que te gusta

Origin blog.csdn.net/qiwoo_weekly/article/details/131078699
Recomendado
Clasificación