Vue responsive principle of source code analysis
Data responsiveness is a major feature of MVVM framework. In Vue2, object.defineProperty () is used to intercept property access by defining setter/getter of Object property, while in Vue3, Proxy() is used to intercept property access. This results in a different design of data responsiveness in Vue2 and Vue3. Below we analyze the source code Vue2 and Vue3 data response is how to achieve.
Analysis on the principle of data response in Vue2
Below we will only analyze the data responsiveness in the Vue2 source code. The initialization process of Vue is not the subject of this article.
Vue2 data responsive began in SRC/core/instance/state. The js the initData () method
function initData (vm: Component) {
observe(data, true /* asRootData */)}Copy the code
Data is the data that we put in the choices
We go down to observe () function is defined, is located in the SRC/core/instance/observer/index, js
/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */
function observe (value: any, asRootData: ? boolean) :Observer | void {
let ob: Observer | void
// Create one when initializing
ob = new Observer(value)
return ob
}
Copy the code
Observe () returns an instance of an Observer. There is a new thing called an Observer class. What does an Observer class do?
class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__'.this)
// object
this.walk(value)
}
/** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
Copy the code
From the above code we can see that the Observer class accepts a value, which is the data object for which we are going to do reactive processing. There is a dep attribute inside, and a new class dep is created. Let’s look at what the new Observer() does
- An instance of Dep is created and copied to the Dep property
- Def (value, ‘ob’, this) copies the Observer instance to the __ob__ property of data
In the SRC/core/instance/observer/dep. Js We look at the Dep class do?
class Dep {
statictarget: ? Watcher; id: number; subs:Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if(process.env.NODE_ENV ! = ='production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) = > a.id - b.id)
}
// All watcher instances inside the loop
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Copy the code
The Dep class has a Depend () method for adding subscriptions and a notify method for publishing subscriptions. What was released? When do you add a subscription? When do you publish subscriptions? What exactly is the update function? We’ll talk about that later.
Again, what does the call to Observe do?
- For each object passed in, an instance of an Observer is obtained, which serves primarily as a response to the object
- DefineReactive () is called for each key in the object, responding to each property in the object
function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function, shallow? : boolean) {
// Corresponds to the current key
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if((! getter || setter) &&arguments.length === 2) {
val = obj[key]
}
// recursive traversal
// Each object has an Observer instance corresponding to it
letchildOb = ! shallow && observe(val)Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// Rely on collection
if (Dep.target) {
// Establish a relationship with the watcher of the current component
dep.depend()
if (childOb) {
// Child OB also has a relationship with the current component watcher
childOb.dep.depend()
// If it is an array, all internal items are processed responsively
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if(newVal === value || (newVal ! == newVal && value ! == value)) {return
}
/* eslint-enable no-self-compare */
if(process.env.NODE_ENV ! = ='production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if(getter && ! setter)return
if (setter) {
setter.call(obj, newVal)
} else{ val = newVal } childOb = ! shallow && observe(newVal) dep.notify() } }) }Copy the code
DefineReactive () generates an instance of Dep for each key passed in and starts relying on the collection in object.defineProperty’s get function, notifying changes in set.
What do dependencies collect?
Dep.depend () is executed if dep.target exists, as you can see from the code above, which should be collected.
What is dep.Target?
In SRC/core/instance/lifecycle. Js in another mountComponent method, this method is used to mount components
function mountComponent (vm: Component, el: ? Element, hydrating? : boolean) :Component {
new Watcher(vm, updateComponent, noop, {
before () {
if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)}Copy the code
UpdateComponent: updateComponent: updateComponent: updateComponent
class Watcher {
constructor() {
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)}}Copy the code
PushTarget assigns an instance of Watcher to dep. target, so it collects an instance of Watcher when it relies on collection
conclusion
Vue2 response involves three classes, Observer, Dep and Watcher
Observer: Each object generates an Observer instance that holds a DEP attribute for responsive processing at the object level
Dep: Collect dependencies in get of Object.defineProperty and publish subscriptions in set
Watcher: Used to hold component update functions, which are collected as dependencies
Vue3 data responsivity principle analysis
Vue2 uses Object. DefineProperty to intercept Object attributes, whereas Vue3 uses Proxy.
The disadvantages of data responsiveness in Vue2 are:
- The responsiveness of arrays requires additional implementation
- New or delete attributes cannot be listened on, use vue. set, vue. delete
- Data structures such as Map, Set, and Class are not supported
Vue3 uses Proxy() to solve this problem.
Let’s copy Vue3’s responsivity principle and implement one ourselves
Step 1: Make the data responsive
const isObject = v= > typeof v === 'object'
function reactive(obj) {
if(! isObject(obj)) {return obj
}
return new Proxy(obj, {
get(target, key) {
const res = Reflect.get(target, key)
return isObject(res) ? reactive(res) : res
},
set(target, key, val) {
const res = Reflect.set(target, key, val)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
return res
}
})
}
Copy the code
Step 2: Rely on the collection
Dependency collection in Vue3 is first understood with a diagram
- Effect (CB): If fn is passed in, the returned function will be reactive, and the internal agent’s data will change, and it will be executed again
- Track (target, key): Establishes mappings between responsive functions and the targets and keys they access
- Trigger (arget, key): according to the mapping established by track(), find the corresponding reactive function and execute it
Let’s do it in code
// Save the dependency data structure
const targetMap = new WeakMap(a)// Create side effects
function effect(fn) {
const e = createReactiveEffect(fn)
e()
return e
}
function createReactiveEffect(fn) {
const effect = function () {
try {
effectStack.push(fn)
return fn()
} finally {
effectStack.pop()
}
}
return effect
}
// Dependency collection: Establish mappings between target,key, and FN
function track(target, key){
const effect = effectStack[effectStack.length - 1]
if(effect) {
let depMap = targetMap.get(target)
if(! depMap) { depMap =new Map()
targetMap.set(target, depMap)
}
let deps = depMap.get(key)
if(! deps) { deps =new Set()
deps.set(key, deps)
}
deps.add(effect)
}
}
// Trigger side effects: get relevant FNS by target,key, and execute them
function trigger(target, key){
const depMap = targetMap.get(target)
if (depMap) {
const deps = depMap.get(key)
if (deps) {
deps.forEach(dep= > dep())
}
}
}
Copy the code
Track () is called in get() above
get(target, key) {
const res = Reflect.get(target, key)
track(target, key)
return isObject(res) ? reactive(res) : res
}
Copy the code
Trigger () is called in the set
set(target, key, val) {
const res = Reflect.set(target, key, val)
trigger(target, key)
return res
}
Copy the code
Combine the above two steps to create a working Vue3 Demo
reactive.js
const isObject = v= > typeof v === 'object'
function reactive(obj) {
if(! isObject(obj)) {return obj
}
return new Proxy(obj, {
// Target is the proxied object
get(target, key) {
const res = Reflect.get(target, key)
track(target, key)
return isObject(res) ? reactive(res) : res
},
set(target, key, val) {
const res = Reflect.set(target, key, val)
trigger(target, key)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log('deleteproperty');
trigger(target, key)
return res
}
})
}
// Temporarily store side effect functions
const effectStack = []
// Create side effects
function effect(fn) {
const e = createReactiveEffect(fn)
e()
return e
}
function createReactiveEffect(fn) {
const effect = function () {
try {
effectStack.push(fn)
return fn()
} finally {
effectStack.pop()
}
}
return effect
}
// Save the dependency data structure
const targetMap = new WeakMap(a)// Dependency collection: Establish mappings between target,key, and FN
function track(target, key){
const effect = effectStack[effectStack.length - 1]
if(effect) {
let depMap = targetMap.get(target)
if(! depMap) { depMap =new Map()
targetMap.set(target, depMap)
}
let deps = depMap.get(key)
if(! deps) { deps =new Set()
deps.set(key, deps)
}
deps.add(effect)
}
}
// Trigger side effects: get relevant FNS by target,key, and execute them
function trigger(target, key){
const depMap = targetMap.get(target)
if (depMap) {
const deps = depMap.get(key)
if (deps) {
deps.forEach(dep= > dep())
}
}
}
Copy the code
<div id="app">
<h3>{{title}}</h3>
</div>
<script src="reactive.js"></script>
<script>
const Vue = {
createApp(options) {
// Web DOM platform
const renderer = Vue.createRenderer({
querySelector(sel) {
return document.querySelector(sel)
},
insert(child, parent, anchor) {
// Do not pass anchor, equivalent to appendChild
parent.insertBefore(child, anchor || null)}})return renderer.createApp(options)
},
createRenderer({querySelector, insert}) {
// Return the renderer
return {
createApp(options) {
// The object returned is the app instance
return {
mount(selector) {
const parent = querySelector(selector)
if(! options.render) { options.render =this.compile(parent.innerHTML)
}
/ / handle the setup
if (options.setup) {
this.setupState = options.setup()
}
if (options.data) {
this.data = options.data()
}
this.proxy = new Proxy(this, {
get(target, key) {
// If there is a key in setupState, use it, otherwise use the key in data
if (key in target.setupState) {
return target.setupState[key]
} else {
return target.data[key]
}
},
set(target, key, val) {
if (key in target.setupState) {
target.setupState[key] = val
} else {
target.data[key] = val
}
},
})
this.update = effect(() = > {
// Execute render to get the view structure
const el = options.render.call(this.proxy)
parent.innerHTML = ' '
// parent.appendChild(el)
insert(el, parent)
})
},
compile(template) {
/ / compile:
// template =》 ast =》 ast => generate render()
// Pass in template, return render
return function render() {
const h3 = document.createElement('h3')
h3.textContent = this.title
return h3
}
}
}
}
}
}
}
</script>
<script>
const { createApp } = Vue
const app = createApp({
setup() {
const state = reactive({
title: 'vue3,hello! '
})
setTimeout(() = > {
state.title = 'hello'
}, 2000);
return state
}
})
app.mount('#app')
</script>
Copy the code
conclusion
Vue3 uses Proxy() for responsive data, making it more powerful and easier to understand the collection and publishing of dependencies.