preface
Vue3 reactivity source address
- Reactivity responsive system
- The composition API implemented is:
- reactive, shallowReactive, shallowReadonly, readonly
- ref, shallowRef
- toRef, toRefs
- effect:
- Reactivity is an internal method that is not exposed to the outside
- The effect function is executed when the data changes
shared
Vue3 shared source address
- Shared: Content shared between multiple packages
// Is it an object
export const isObject = (value) = > typeof value == 'object'&& value ! = =null
// Merge objects
export const extend = Object.assign
// Is not an array
export const isArray = Array.isArray
// is not a function
export const isFunction = (value) = > typeof value == 'function'
// Is not a number
export const isNumber = (value) = > typeof value == 'number'
// Is not a character
export const isString = (value) = > typeof value === 'string'
// Is a positive integer
export const isIntegerKey = (key) = > parseInt(key) + ' ' === key
// is not its own attribute
let hasOwnpRroperty = Object.prototype.hasOwnProperty
export const hasOwn = (target, key) = > hasOwnpRroperty.call(target, key)
// Is it the same value
export const hasChanged = (oldValue,value) = >oldValue ! == valueCopy the code
reactive
- Reactive: Proxy for data (key)
- ShallowReactive: Shallow proxy for data (that is, only focus on the first layer)
- ShallowReadonly: Shallow proxy for data and can only be read, not modified (no track collection)
- Readonly: can only be read, cannot be modified (track is not collected)
- General process (take the following examples as examples):
- Execute effect function
state.arr.length
state.son.name
state.arr[3]
Will walk the proxyget
- through
track
Collect the current effect function - When data changes, the proxy is used
set
- through
trigger
The current effect function is executed again - Only variables in the function passed by effect will collect the effect function (equivalent to watcher in VUe2)
The sample
<div id="app"></div>
<script src=".. /node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
<script>
let { effect, reactive } = VueReactivity
let state = reactive({
name: 'tom'.age: 38.son: {name: 'Bob'.age: 18},
arr: [1.2.3.4.5]
})
effect(() = > {
// app.innerHTML = state.name + state.name
app.innerHTML = `${state.arr.length}-${state.son.name}-${state.arr[3]}`
})
setTimeout(() = > {
// state.arr.push(100)
state.son.name = 'Pretty'
state.arr.length = 1
}, 2000)
</script>
Copy the code
+---------------------+ +----------------------+
| | | |
| 5-Bob-4 +--->| 1-Pretty-undefined +
| | | |
+---------------------+ +----------------------+
Copy the code
reactive.ts
import { isObject } from "@vue/shared/src"
import { mutableHandlers, shallowReactiveHandlers, readonlyHandlers, shallowReadonlyHandlers } from './baseHandlers'
/ * * *@description Intercept data */
export function reactive(target) {
return createReactiveObject(target, false, mutableHandlers)
}
/ * * *@description Intercept the first layer of data (shallow response) */
export function shallowReactive(target) {
return createReactiveObject(target, false, shallowReactiveHandlers)
}
/ * * *@description Read-only data */
export function readonly(target) {
return createReactiveObject(target, true, readonlyHandlers)
}
/ * * *@description Shallow read-only data */
export function shallowReadonly(target){
return createReactiveObject(target, true, shallowReadonlyHandlers)
}
// Create WeakMap and the response is still read-only
// For easy lookup, the stored key must be an object
// Automatic garbage collection, no memory leak
const reactiveMap = new WeakMap(a)const readonlyMap = new WeakMap(a)/ * * *@description Create data broker *@param Target The target (array or object) to intercept@param IsReadonly Whether it is read-only *@param BaseHandlers Proxy second argument object */
export function createReactiveObject(target, isReadonly, baseHandlers) {
if(! isObject) {return target }
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existProxy = proxyMap.get(target)
if (existProxy) { return existProxy }
const proxy = new Proxy(target, baseHandlers)
proxyMap.set(target, proxy)
return proxy
}
Copy the code
operators.ts
/ * * *@description Enumerate */ when collecting effects
export const enum TrackOpTypes {
GET
}
/ * * *@description Enumerate */ when publishing effect
export const enum TriggerOrTypes {
ADD,
SET
}
Copy the code
baseHandlers.ts
import { extend, hasChanged, hasOwn, isArray, isIntegerKey, isObject } from "@vue/shared/src"
import { track, trigger } from "./effect"
import { TrackOpTypes, TriggerOrTypes } from "./operators"
import { reactive, readonly } from "./reactive"
/ * * *@description Create a get *@param IsReadonly Whether it is read-only *@param Shallow is not shallow intercept */
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
// reflect.get has the return value (target[key])
// If the target value type is not Object, a TypeError is raised
const res = Reflect.get(target, key, receiver)
if(! isReadonly) {/ / collection effect
track(target, TrackOpTypes.GET, key)
}
if (shallow) { return res }
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
/ * * *@description Create sets only for non-read-only data *@param Shallow is not shallow intercept */
function createSetter(shallow = false) {
return function set(target, key, value, receiver) {
const oldValue = target[key]
let hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key)
// reflect. set has a return value (Boolean)
const result = Reflect.set(target, key, value, receiver)
if(! hadKey) {/ / new
trigger(target, TriggerOrTypes.ADD, key, value)
} else if(hasChanged(oldValue,value)) {
/ / modify
trigger(target, TriggerOrTypes.SET, key, value, oldValue)
}
return result
}
}
const get = createGetter()
const shallowGet = createGetter(false.true)
const readonlyGet = createGetter(true)
const showllowReadonlyGet = createGetter(true.true)
const set = createSetter()
const shallowSet = createSetter(true)
export const mutableHandlers = {
get,
set
}
export const shallowReactiveHandlers = {
get: shallowGet,
set: shallowSet
}
// Warning when read only set
let readonlyObj = {
set: (target, key) = > {
console.warn(`set on key ${key}Falied, currently read-only property ')}}export const readonlyHandlers = extend({
get: readonlyGet,
}, readonlyObj)
export const shallowReadonlyHandlers = extend({
get: showllowReadonlyGet,
}, readonlyObj)
Copy the code
effect.ts
When effect is passed into an array such as app.innerhtml = state.arr the current targetmap. get(state.arr) will collect the entire index length join toString call push, pop, Methods such as splice trigger the length of the array such as state.arr.push(100). The source code does limit the length collection when calling the method
import { isArray, isIntegerKey } from "@vue/shared/src"
import { TriggerOrTypes } from "./operators"
/ * * *@description Uid effect Uniquely identifies *@description ActiveEffect Indicates the current effect *@description EffectStack Stores the stack of effects, terminating each with an */
let uid = 0
let activeEffect;
const effectStack = []
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
Fn -> state. Age++
if(! effectStack.includes(effect)) {try {
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
}
effect.id = uid++
effect._isEffect = true // The flag is a responsive effect
effect.raw = fn // The original function passed in
effect.options = options
return effect
}
/ * * *@description The data is called in the function passed by the effect trigger is published again when the data changes *@param F sub n passes the function *@param Options if the options are not lazy (computed by default is not performed) */
export function effect(fn, options: any = {}) {
const effect = createReactiveEffect(fn, options)
if(! options.lazy) { effect() }return effect
}
/ * * *@description Effect The stored variable track collects effect *@description TargetMap structure * {name: 'XXX ', age: 000} = > {* name = > [effect effect], * age = > [effect effect] *}, * [1, 2, 3] = > {* 1 = >} [effect, efffect] * * /
const targetMap = new WeakMap(a)/ * * *@description Lets properties in an object collect the current corresponding effect function *@description */ is collected only when effect is executed and variables in effect are collected
export function track(target, type, key) {
// activeEffect Points to the currently executing effect
if (activeEffect === undefined) { 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 =new Set))}if(! dep.has(activeEffect)) { dep.add(activeEffect) } }/ * * *@description Find the effect for the property to execute (only arrays and objects here) *@param The target goal *@param Type New or modified *@param key key
* @param NewValue the new value@param OldValue old value * /
export function trigger(target, type, key? , newValue? , oldValue?) {
const depsMap = targetMap.get(target)
if(! depsMap)return
// Remove the effect to publish
// Save all effects to be executed in a new collection and execute them together
const effects = new Set(a)const add = (effectsToAdd) = > {
if (effectsToAdd) {
effectsToAdd.forEach(effect= > effects.add(effect))
}
}
// Modify the array length traget.length = newValue
[1,2,3,4,5] => [1,2,3,4,5]. Length = 1
if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) = > {
if (key === 'length' || key > newValue) {
add(dep)
}
})
} else {
// Can be an object
// This must be a modification, not a new one
Depmap. get(key) -> undefined
if(key ! = =undefined) {
add(depsMap.get(key))
}
// If you modify an index in the array
Arr =[1,2,3] -> arr[100]=1
switch (type) {
case TriggerOrTypes.ADD:
if (isArray(target) && isIntegerKey(key)) {
add(depsMap.get('length'))}break; }}/ / release
effects.forEach((effect: any) = > effect())
}
Copy the code
Ref and toRef
ref
- Ref is mostly for a single variable
let name = ref('tom')
Object. DefineProperty (get and set accessors) - if
let state = ref({})
Is the object that will be usedreactive({})
API - ShallowRef only does object.defineProperty for the first layer, no
reactive({})
The sample
<script src=".. /node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
<div id="app"></div>
<script>
const { ref, shallowRef, effect } = VueReactivity
let name = ref('Tom')
// let state = ref({a: 'a1', b: 'b1'})
effect(() = > {
app.innerHTML = name.value
// app.innerHTML = state.value.a
})
setTimeout(() = > {
name.value = 'Bob'
// state.value.a = 'a2'
}, 1000)
</script>
Copy the code
+---------------------+ +----------------------+
| | | |
| Tom +--->| Bob +
| | | |
+---------------------+ +----------------------+
Copy the code
toRef
- Call the proxy variable to make a layer of proxy
- ToRefs simply loops toRef and does a layer of proxying
The sample
<script src=".. /node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
<div id="app"></div>
<script>
const { effect, reactive, toRef, toRefs } = VueReactivity
let proxy = reactive({name:'Tom'.age: 100})
// let r1 = toRef(proxy, 'name')
// let r2 = toRef(proxy, 'age')
const { name, age } = toRefs(proxy)
effect(() = >{
app.innerHTML = name.value + The '-' + age.value
})
setTimeout(() = > {
proxy.name = 'Bob'
}, 2000)
</script>
Copy the code
+---------------------+ +----------------------+
| | | |
| Tom-100 +--->| Bob-100 +
| | | |
+---------------------+ +----------------------+
Copy the code
ref.ts
import { hasChanged, isArray, isObject } from "@vue/shared/src"
import { track, trigger } from "./effect"
import { TrackOpTypes, TriggerOrTypes } from "./operators"
import { reactive } from "./reactive"
/ * * *@description ref shallowRef
* @description Reactive uses defineProperty */ internally in proxy ref
export function ref(value) {
return createRef(value)
}
export function shallowRef(value) {
return createRef(value, true)}// Use reactive proxies if the value passed in is an object
const convert = (val) = > isObject(val) ? reactive(val) : val
class RefImpl {
public _value;
public __v_isRef = true // indicates a ref attribute
constructor(public rawValue, public shallow) {
this._value = shallow ? rawValue : convert(rawValue)
}
get value() {
/ / collection effect
track(this, TrackOpTypes.GET, 'value')
return this._value
}
set value(newValue) {
if (hasChanged(newValue, this.rawValue)) {
this.rawValue = newValue
this._value = this.shallow ? newValue : convert(newValue)
/ / release effect
trigger(this, TriggerOrTypes.SET, 'value', newValue)
}
}
}
function createRef(rawValue, shallow = false) {
return new RefImpl(rawValue, shallow)
}
/ * * *@description toRef toRefs
* @description To convert an object to ref is to do a layer of proxy */
class ObjectRefImpl {
public __v_isRef = true
constructor(public target, public key) {}
Track depends on collection if the original object is reactive
get value() {return this.target[this.key]
}
// Trigger triggers updates if the original object is responsive
set value(newValue) {this.target[this.key] = newValue
}
}
export function toRef(target, key) {
return new ObjectRefImpl(target, key)
}
export function toRefs(object) {
const ret = isArray(object)?new Array(object.length) : {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}
Copy the code