1. Understand a few concepts
What is responsive
Before starting the responsive principle and source code analysis, you need to understand what is responsive? First, clarify a concept: responsiveness is a process , which has two participants:
Triggered by: data
Responder: Function that references data
When the data changes, the function that refers to the data will be automatically re-executed. For example, if the data is used in view rendering, the view will be automatically updated after the data changes, which completes a response process.
side effect function
Both in Vue
and React
in have the concept of side effect function, what is a side effect function? If a function refers to external data, this function will be affected by the change of external data, we say that this function has side effects , which is what we call a side effect function . It is not easy to understand the name at first, but in fact, a side effect function is a function that references data or a function associated with data . for example:
<!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>
In the above example, effect
external data is referenced in the function obj.name
. If the data changes, it will affect this function. Similar effect
functions are side effect functions.
Basic steps to implement responsiveness
In the above example, when obj.name
a change occurs, effect
we execute it manually. If we can monitor obj.name
the change and let it automatically execute the side effect function effect
, then the responsive process is realized. In fact, whether it is Vue2
or Vue3
, the core of responsiveness is the same 数据劫持/代理、依赖收集、依赖更新
, but the difference in the specific implementation is due to the difference in the way of realizing data hijacking.
Vue2
Responsive:基于Object.defineProperty()
hijacking of data achievedVue3
Responsive: based onProxy
implementing a proxy for the entire object
There is no Vue2
focus on the responsiveness here. This article focuses on Vue3
the realization of the responsiveness principle.
2. Proxy and Reflect
Before parsing Vue3
the responsive principle, you first need to understand two new ES6 APIs: Porxy
and Reflect
.
Proxy
Proxy
: Proxy, as the name suggests, is mainly used to create a proxy for the object, so as to realize the interception and customization of the basic operations of the object . It can be understood that a layer of "interception" is set up before the target object, and external access to the object must first pass through this layer of interception, so a mechanism is provided to filter and rewrite external access. Basic syntax:
let proxy = new Proxy(target, handler);
target
: The target object that needs to be interceptedhandler
: is also an object, used to customize the interception behavior. For example:
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
If it is empty here handler
, it directly points to the proxy object, and the proxy object is not . If you need more flexible interception of object operations, you need to handler
add corresponding attributes in . For example:
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)
In the above example, we defined set()
the , get()
, and attributes in the capturer, and implemented the right operation interception deleteProperty()
through the right operation . The trigger method for these properties has the following parameters:proxy
obj
target
-- is the target object, which is passed as the first parameter tonew Proxy
key
- target attribute namevalue
- the value of the target propertyreceiver
—— Points to the correct context for the current operation. If the target property is angetter
accessor property, thenreceiver
isthis
the object . Usually,receiver
this isproxy
the object itself, but if weproxy
inherit from , wereceiver
mean the object thatproxy
inheritsOf course, in addition to the above three, there are some common attribute operation methods:
has()
, intercept: in operator.ownKeys()
, to intercept:Object.getOwnPropertyNames(proxy) Object.getOwnPropertySymbols(proxy) Object.keys(proxy)
construct()
, Intercept:new
Operation etc.
Reflect
Reflect
: Reflection is to reflect the content of the agent. Reflect
As Proxy
with , new is also provided ES6
for manipulating objects API
. It provides methods for intercepting JavaScript
operations, and these methods Proxy handlers
are in one-to-one correspondence with the methods provided. As long as it is Proxy
the method of the object, Reflect
the corresponding method can be found on the object. And Reflect
is not a function object, that is, it cannot be instantiated, and all its properties and methods are static. Or the above example
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)
In the example above
Reflect.get()
instead oftarget[key]
operationReflect.set()
instead oftarget[key] = value
operationReflect.deleteProperty()
delete target[key]
Of course, in addition to the above methods, there are some commonly used methods for replacing operationsReflect
:
Reflect.construct(target, args)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
3. Reactive, ref source code analysis
After understanding Proxy
and Reflect
, let's see Vue3
how to porxy
implement responsiveness. Its core is the two methods to be introduced below: reactive
, ref
. Here it is analyzed according to the source code of Vue3.2 version.
The source code implementation of reactive
Open the source file, find the file packages/reactivity/src/reactive.ts
and view the source code.
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
)
}
At the beginning,
target
the responsive read-only judgment is made, and if it istrue
, it will be returned directlytarget
.reactive
The core method of implementation iscreateReactiveObject()
:
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()
The method has five parameters:target
: the original target object passed inisReadonly
: Is it a read-only flagbaseHandlers
:proxy
The second parameter when creating an ordinary objecthandler
collectionHandlers
: The second parameter whencollection
creating an object of typeproxy
handler
proxyMap
:WeakMap
typemap
, mainly used totarget
storeproxy
the correspondence between and his
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
}
}
As you can see from the source code, he divides objects into
COMMON
objects (Object
andArray
) andCOLLECTION
type objects (Map
,Set
,WeakMap
,WeakSet
). The main purpose of this distinction is to customize different object types according to different object types.handler
In
createReactiveObject()
the first few lines, a series of judgments are made:First judge
target
whether it is an object, if it isfalse
, directlyreturn
Determine
target
whether it is a responsive object, if sotrue
, directlyreturn
Determine whether it has been
target
createdproxy
, if sotrue
, directlyreturn
Determine
target
whether it is the 6 object types mentioned above, if yesfalse
, directlyreturn
If the above conditions are met, it is
target
createdproxy
, andreturn
thisproxy
handler
The next step is to pass in different logic processing according to different object types. The main focus is baseHandlers
that there are five attribute operation methods, which focus on analysis get
and set
methods.
Source location:
packages/reactivity/src/baseHandlers.ts
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
get
with dependency collection
You can see
mutableHandlers
that there are various hook functions we are familiar with. When weproxy
access or modify the object, we call the corresponding function for processing. First look atget
how totarget
collect the side effect functions of the access:
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
}
}
If
key
the value is__v_isReactive
,__v_isReadonly
return accordingly, ifkey==='__v_raw'
and the valueWeakMap
in is not empty, then returnkey
target
target
If
target
it is an array, override /enhance the method corresponding to the arrayCall in these methods
track()
for dependency collectionHow to find array elements :
includes、indexOf、lastIndexOf
How to modify the original array :
push、pop、unshift、shift、splice
Judging
Reflect.get()
the return value of the method, that is, the attribute value of the current data objectres
, ifres
it is an ordinary object and not read-only, calltrack()
for dependency collectionIf
res
it is a shallow response, return it directly, ifres
it isref
an object, return itsvalue
valueIf
res
it is an object type and is read-only , call itreadonly(res)
, otherwise callreactive(res)
the method recursivelyIf none of the above is satisfied, directly return the corresponding attribute value
Then the core method is if you use **
track()
** to process the dependency collection, the source code is in ``packages/reactivity/src/effect.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)
}
First, judge whether the dependency collection is in progress
const targetMap = new WeakMap<any, KeyToDepMap>()
Create atargetMap
container to save the dependent content related to the current responsive object, which itself is aWeakMap
typeThe corresponding responsive
targetMap
as the key ,targetMap
and the Value is onedepsMap
(belonging toMap
the instance ),depsMap
which stores the specific dependencieskey
correspondingdepsMap
The key is the key of the responsive data object, and the Value is andeps
instance (belonging toSet
the instance ). The reason why it is used hereSet
is to avoid repeated addition of side-effect functions and repeated calls
The above is the core process of the entire **get()** capturer and dependency collection.
set
update with dependencies
Let's go back baseHandlers
to see Set
how the dependency update is performed in the capturer
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
}
}
First save the old value
oldValue
If it is not a shallow response
target
but an ordinary object, and the old value is a responsive object, perform the assignment operation:oldValue.value = value
, returnstrue
, indicating that the assignment is successfulDetermine whether there is a corresponding key value
hadKey
Execution
Reflect.set
setting the corresponding property valueJudging that the object is the content on the original prototype chain (not added by custom), then the dependency update will not be triggered
According to the target object does not have the corresponding key, call
trigger
and update the dependency
The above is the entire baseHandlers
core process of dependency collection and dependency update .
The source code implementation of ref
We know that we ref
can define the responsiveness of basic data types and reference data types. Let's take a look at its source code implementation: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)
}
}
}
As can be seen from the above function call process,
ref
the core of the implementation is to instantiate anRefImpl
object. Why is an object instantiated hereRefImpl
? The purpose is that the target ofProxy
the proxy is also an object type, andproxy
data proxy cannot be performed by creating a basic data type. Only basic data types can be packaged as an object, and dependency collection and dependency update can be performed through customget、set
methodsLook at
RefImpl
the meaning of the object properties:_ value : used
保存ref当前值
, if the passed parameter is an object , it is used to save the value converted by the reactive function , otherwise_value
it is_rawValue
the same as_ rawValue : It is used to save the original value corresponding to the current ref value . If the passed parameter is an object , it is used to save the original value before conversion, otherwise
_value
it is_rawValue
the same as . The function of thistoRaw()
function is to convert the responsive object into a normal objectdep : It is a
Set
type of data used to store the dependencies of the currentref
value collection. As for whySet
we use the explanation above, here is the same reason_v_isRef : Mark bit, as long as it is
ref
defined, it will identify the current data as oneRef
, that is, its value is marked astrue
In addition, it can be clearly seen that the method
RefImpl类
exposed to the instance objectget、set
is value , soref
we must bring **.value** for the operation of the defined responsive data
If the value passed in is an object
convert()
type, the method will be called , andreactive()
the method will be called in this method to perform responsive processing on itRefImpl
The key to the example lies in the processing of the two functions oftrackRefValue(this)
andtriggerRefValue(this, newVal)
. We probably also know that they are dependent collection and dependent update . The principle is basicallyreactive
similar to the processing method, so I won’t elaborate here
V. Summary
For the basic data type , it can only
ref
realize its responsiveness through . The core still packs it into an object, and implements dependency collection and dependency updateRefImpl
internally through custom and .get value()
set value(newVal)
For the object type ,
ref
bothreactive
can convert it into responsive data , butref
internally, it will eventually callreactive
a function to realize the conversion.reactive
Function, mainly through创建了Proxy实例对象
, throughReflect
the acquisition and modification of data.
Some references:
https://github.com/vuejs/vue
https://zh.javascript.info/proxy#reflect
- END -
About Qi Wu Troupe
Qi Wu Troupe is the largest front-end team of 360 Group, and participates in the work of W3C and ECMA members (TC39) on behalf of the group. Qi Wu Troupe attaches great importance to talent training, and has various development directions such as engineers, lecturers, translators, business interface people, and team leaders for employees to choose from, and provides corresponding technical, professional, general, and leadership training course. Qi Dance Troupe welcomes all kinds of outstanding talents to pay attention to and join Qi Dance Troupe with an open and talent-seeking attitude.