The profile
Vue3’s ReActivity is a separate package, which is a big change, with all the response-related implementations in it, and that’s what I’m talking about.
Knowledge to prepare
1. Proxy: es6 proxy implementation method 2. Reflect: Put some object objects that are obviously language internal methods on Reflect, 3. WeakMap: weakMap key can only be object type. 4. WeakSet: weakSet object is a collection of some object values, and each object value can only appear once. Reactive brief implementation We used to write reactive data like this
data () {
return {
count: 0
}
}Copy the code
Then vu3’s new responsive writing style (compatible with old ones)
setup() { const state = { count: 0, double: computed(() => state.count * 2) } function increment() { state.count++ }
onMounted(() => { console.log(state.count) }) watch(() => { document.title = `count ${state.count}` Copy the code
Copy the code
})
return {
state,
increment
}
}Unlike the React hooks, the setup() function is called only once. The setup() function is called only once. So the new response data can be declared in two ways: 1.Ref Prerequisite: declare a type Ref
export interface Ref<T> { [refSymbol]: true value: UnwrapNestedRefs<T> }Copy the code
Ref () function
function ref(raw: Unknown) {if (isRef(raw)) {return raw} Raw = convert(raw) const r = {_isRef: Track (r, operationtypes.get, ") return raw}, Set value(newVal) {raw = convert(newVal) // trigger Trigger (r, operationtypes.set, '')}} return r as Ref}Copy the code
Take a look at convert
const convert = val => isObject(val) ? reactive(val) : valCopy the code
As you can see, the ref type wraps only the outermost layer, and the internal object is ultimately called Reactive to generate the Proxy object for the reactive Proxy. Why not use proxy for all internal objects and use Ref for the outermost object? The reason may be simple: proxies are objects, and for primitive data types, function passes, or object structures, references to the original data are lost. Official explanation:
However, the problem with going reactive-only is that the consumer of a composition function must keep the reference to the returned object at all times in order to retain reactivity. The object cannot be destructured or spread:
2.Reactive premise: First understand weakMap
WeakMaps that store {raw <-> observed} pairs. Const rawToReactive = new WeakMap<any, any>() // Key: original object value: Proxy const reactiveToRaw = new WeakMap<any, any>() const rawToReadonly = new WeakMap<any, any>() const readonlyToRaw = new WeakMap<any, any>() reactive(target)Copy the code
Note: Target must be an object, otherwise a warning will be generated
Function reactive(target) {if (readonlytoraw. has(target)) {return target} If (readonlyvalues.has (target)) {return readonly(target)} // go ----> step2 return CreateReactiveObject (target, rawToReactive reactiveToRaw, mutableHandlers, / / attention transfer mutableCollectionHandlers)}Copy the code
createReactiveObject(target,toProxy,toRaw,baseHandlers,collectionHandlers)
function createReactiveObject( target: unknown, toProxy: WeakMap<any, any>, toRaw: WeakMap<any, any>, baseHandlers: <any>, collectionHandlers: ProxyHandler<any>) {// If (! isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)} ')} return target} let observed = toProxy (target) if (observed! == void 0) {return observed} // If (toraw. has(target)) {return target} // If (toraw. has(target)) {return target} If (! CanObserve (target) {return target} const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : ----> Step3 Observed = new Proxy(target, 2 weakMap store target Observed toproxy. set(target, observed) Toraw. set(target, observed) if (! targetMap.has(target)) { targetMap.set(target, new Map()) } return observed }Copy the code
Handlers are as follows, and the handles of a new Proxy(target, handles) are this object
export const mutableHandlers = { get: createGetter(false), set, deleteProperty, has, ownKeys }Copy the code
CreateGetter (false) {reactive(res);} createGetter(false) {reactive(res);
reactive(target) -> createReactiveObject(target,handlers) -> new Proxy(target, handlers) -> createGetter(readonly) -> get() -> res -> isObject(res) ? reactive(res) : res
function createGetter(isReadonly: Boolean) {// isReadonly is used to distinguish whether the data is read-only and responsive // Receiver is a proxy object created. string | symbol, receiver: Const res = reflect. get(target, key, Receiver) if (isSymbol(key) && builtinsymbols.has (key)) {return res} if (isRef(res)) {return res.value} // Collect dependencies Track (target, operationtypes.get, key) // Call reactive and pass the res. Return isObject(res)? isReadonly ? // need to lazy access readonly and reactive here to avoid // circular dependency readonly(res) : reactive(res) : res } }Copy the code
One of the main functions of a set set is to trigger a listener to try to update, but notice that it controls when the view actually needs to be updated, right
function set( target: object, key: string | symbol, value: unknown, receiver: object ): OldValue = (target as any)[key] const oldValue = (target as any)[key] // oldValue = (target as any)[key] And returns if (isRef(oldValue) &&! isRef(value)) { oldValue.value = value return true } const hadKey = hasOwn(target, Key) const result = reflect. set(target, key, value, receiver) // If it is a data operation on the original prototype chain, do nothing to trigger the listener function. If (target === toRaw(receiver)) {// Update two conditions // 1. // 2. Old and new values are not equal if (! hadKey) { trigger(target, OperationTypes.ADD, key) } else if (hasChanged(value, oldValue)) { trigger(target, OperationTypes.SET, key) } } return result }Copy the code
(1) Let a = [1], a.paush (2); (2) Let a = [1], a.paush (2); (3) Set length 1; 2. Set value 2 (traps) If the set key is length, we can determine that the array already has this property, so we do not need to update it. If the new value is the same as the old value, we do not need to update it.
Problem 3: There is a target === toRaw(receiver) condition before continuing to operate on the trigger update view, which is exposed as a target! == toRaw(receiver) Receiver: the object that is called initially. This is usually the proxy itself, but the handler’s set method can also be called indirectly (and therefore not necessarily the proxy itself) on the prototype chain or in other ways
// don’t trigger if target is something up in the prototype chain of original
If our operation is a data operation on the raw data prototype chain, target is not equal to toRaw(Receiver). == toRaw(receiver) Example:
Const child = new Proxy({}, {// Other traps omit set(target, key, value, receiver) {reflect. set(target, key, value, receiver) receiver) console.log('child', receiver) return true } } )
const parent = new Proxy( { a: 10}, {// Other traps omit set(target, key, value, receiver) {reflect. set(target, key, value, receiver) {// Other traps omit set(target, key, value, receiver) {Reflect. receiver) console.log('parent', receiver) return true } } )
Object.setPrototypeOf(child, parent) // child.proto === parent true
child.a = 4
Copy the code
// parent Proxy {a: 4} // Proxy {a: 4}
In theory, the parent’s set should not fire, but it does
Target: {a: 10} receiver: Proxy {a: 4} in VUe3 toRaw(receiver): {a: 4}Copy the code
Why do we need a Ref when we have a proxy to do reactive? Because Proxy cannot hijack the basic data type, we designed such an object — Ref. In fact, there are still many design details, I will not go into details. The official website also gives them the differences, you can learn about them.