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 Vue
como React
in 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, effect
se hace referencia a los datos externos en la función obj.name
. Si los datos cambian, afectará a esta función. Las effect
funciones similares son funciones de efectos secundarios.
Pasos básicos para implementar la capacidad de respuesta
En el ejemplo anterior, cuando obj.name
ocurre un cambio, effect
lo ejecutamos manualmente. Si podemos monitorear obj.name
el cambio y dejar que ejecute automáticamente la función de efecto secundario effect
, entonces se realiza el proceso receptivo. De hecho, ya sea Vue2
o 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.
Vue2
Responsivo:基于Object.defineProperty()
secuestro de datos logradoVue3
Responsivo: basado enProxy
la implementación de un proxy para todo el objeto
Aquí no Vue2
nos centramos en la capacidad de respuesta, sino que este artículo se centra en Vue3
la realización del principio de capacidad de respuesta.
2. Proxy y reflejo
Antes de analizar Vue3
el principio receptivo, primero debe comprender dos nuevas API de ES6: Porxy
y 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 interceptadohandler
: 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
objProxy
Si 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 handler
atributos 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:proxy
obj
target
-- es el objeto de destino, que se pasa como el primer parámetro anew Proxy
key
- nombre de atributo de destinovalue
- el valor de la propiedad de destinoreceiver
—— Apunta al contexto correcto para la operación actual. Si la propiedad de destino es una propiedadgetter
de acceso , entoncesreceiver
esthis
el objeto . Por lo general,receiver
este esproxy
el objeto en sí mismo, pero siproxy
heredamos de , nosreceiver
referimos al objeto queproxy
heredaPor 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:new
Operación, etc.
Reflejar
Reflect
: La reflexión es reflejar el contenido del agente. Reflect
Al igual que Proxy
con , new también se proporciona ES6
para manipular objetos API
. Proporciona métodos para interceptar JavaScript
operaciones, y estos métodos Proxy handlers
están en correspondencia uno a uno con los métodos proporcionados Siempre que sea Proxy
un método de objeto, Reflect
el método correspondiente se puede encontrar en el objeto. Y Reflect
no 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 detarget[key]
operaciónReflect.set()
en lugar detarget[key] = value
operaciónReflect.deleteProperty()
delete target[key]
Por supuesto, además de los métodos anteriores, existen algunos métodos comúnmente utilizados para reemplazar operacionesReflect
:
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 Proxy
y Reflect
, veamos Vue3
cómo porxy
implementar la capacidad de respuesta. Su núcleo son los dos métodos que se presentarán a continuación: reactive
Aquí ref
se 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.ts
y 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,
target
se realiza el juicio receptivo de solo lectura y, si lo estrue
, se devolverá directamentetarget
.reactive
El método principal de implementación escreateReactiveObject()
:
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 pasadoisReadonly
: ¿Es un indicador de solo lectura?baseHandlers
:proxy
El segundo parámetro al crear un objeto ordinario.handler
collectionHandlers
: El segundo parámetro alcollection
crear un objeto de tipoproxy
handler
proxyMap
:WeakMap
tipomap
, utilizado principalmente paratarget
almacenarproxy
la 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
COMMON
objetos (Object
yArray
) yCOLLECTION
objetos 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
target
si es un objeto, si lo esfalse
, directamentereturn
Determine
target
si es un objeto sensible, si es asítrue
, directamentereturn
Determinar si se ha
target
creadoproxy
, en caso afirmativotrue
, directamentereturn
Determine
target
si se trata de los 6 tipos de objetos mencionados anteriormente, en caso afirmativofalse
, directamentereturn
Si se cumplen las condiciones anteriores, se
target
creaproxy
, yreturn
esteproxy
handler
El siguiente paso es pasar un procesamiento lógico diferente de acuerdo con los diferentes tipos de objetos. El enfoque principal es baseHandlers
que hay cinco métodos de operación de atributos, que se enfocan en el análisis get
y set
los métodos.
Ubicación de la fuente:
packages/reactivity/src/baseHandlers.ts
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
get
con colección de dependencia
Puede ver
mutableHandlers
que hay varias funciones de enlace con las que estamos familiarizados. Cuandoproxy
accedemos o modificamos el objeto, llamamos a la función correspondiente para su procesamiento. Primero observeget
cómotarget
recopilar 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
key
el valor es__v_isReactive
,__v_isReadonly
regrese en consecuencia, sikey==='__v_raw'
y el valorWeakMap
en no está vacío, luego regresekey
target
target
Si
target
es una matriz, invalide /mejore el método correspondiente a la matrizLlame a estos métodos
track()
para la recopilación de dependenciasCó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 actualres
, sires
es un objeto ordinario y no de solo lectura, solicitartrack()
la recopilación de dependenciasSi
res
es una respuesta superficial, devuélvela directamente, sires
esref
un objeto, devuelve suvalue
valorSi
res
es un tipo de objeto y es de solo lectura , llámelo ; de lo contrario, llame al métodoreadonly(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 untargetMap
contenedor para guardar el contenido dependiente relacionado con el objeto receptivo actual, que en sí mismo es unWeakMap
tipoUse el objeto receptivo
targetMap
como la clave ,targetMap
y el valor es unodepsMap
(perteneciente aMap
la instancia ),depsMap
que almacena las dependencias específicaskey
correspondientesdepsMap
La clave es la clave del objeto de datos de respuesta, y el Valor es unadeps
instancia (que pertenece aSet
la instancia ). La razón por la que se usa aquíSet
es 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.
set
actualizar con dependencias
Volvamos baseHandlers
a ver Set
có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 anterior
oldValue
Si no es una respuesta superficial
target
sino un objeto ordinario, y el valor anterior es un objeto receptivo, realice la operación de asignación:oldValue.value = value
, devuelvetrue
, indicando que la asignación fue exitosaDeterminar si hay un valor de clave correspondiente
hadKey
Ejecución
Reflect.set
estableciendo el valor de propiedad correspondienteSi 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
trigger
y actualice la dependencia
Lo anterior es todo el proceso baseHandlers
central de recopilación de dependencias y actualización de dependencias .
La implementación del código fuente de ref.
Sabemos que ref
podemos 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,
ref
el núcleo de la implementación es instanciar unRefImpl
objeto. ¿Por qué se crea una instancia de un objeto aquíRefImpl
? El propósito es que el objetivo delProxy
proxy también sea un tipo de objeto, yproxy
el 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 deget、set
métodosMira
RefImpl
el 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_rawValue
lo 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_rawValue
lo mismo que. La función de estatoRaw()
función es convertir el objeto receptivo en un objeto normal.dep : Es un
Set
tipo de dato utilizado para almacenar las dependencias de laref
colección de valores actual. En cuanto a por quéSet
usamos la explicación anterior, aquí está la misma razón_v_isRef : Marcar bit, siempre que esté
ref
definido identificará el dato actual como unoRef
, es decir su valor se marca comotrue
Además, se puede ver claramente que el método
RefImpl类
expuesto al objeto de instanciaget、set
es valor , por lo queref
debemos 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 yreactive()
se llamará al método en este método para realizar un procesamiento receptivo en él.RefImpl
La clave del ejemplo radica en el procesamiento de las dos funciones detrackRefValue(this)
ytriggerRefValue(this, newVal)
. Probablemente también sabemos que son recopilación dependiente y actualización dependiente . El principio es básicamentereactive
similar 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
ref
darse 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 dependenciasRefImpl
internamente a través de custom y .get value()
set value(newVal)
Para el tipo de objeto ,
ref
ambosreactive
pueden convertirlo en datos receptivos , peroref
internamente, eventualmente llamaráreactive
a una función para realizar la conversión.reactive
Función, principalmente a través de创建了Proxy实例对象
, a través deReflect
la 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.