@[TOC]
Learning goals
- What is reactive
Vue
How do we know we’re up to date- Analog data responsiveness
- By reading
Vue2
Source code, understand Vue two-way data binding principle, can pull with the interviewer- What is the
Watcher
- How many types of Watcher are there in VUE
- What is the
Dep
The learning process
Data responsiveness simply means that the target (view) updates automatically when the dependency changes.
So, to understand data responsiveness, let’s first try to make the data change automatically.
1. Let a number change
Simulated data responsiveness, where target changes when data dependencies change. Such as:
let c = a+b; Copy the code
Here we call C target, a,b is a dependency of C, because C is derived from A and B.
So how do we get c to change when A changes or b changes?
1.1 The first thing THAT comes to my mind is to put C in a function. When a or B changes, we can call that function and let C remake it.
Let’s test my idea.
"use strict"; let a = 1 let b = 2 let c = null; function fn(){ c =a+b; } console.log('1',a,b,c) // 1- 1 2 null // c is null // Call the function to initialize c fn() console.log('2 -,a,b,c) // 2 // At this point c=3 // Dependencies change a = 9 console.log('3',a,b,c) // 3 // Here a changes, but c does not fn() console.log('4',a,b,c) // 4 // c=11 after fn is called Copy the code
Obviously at this point, calling target manually does update, but it feels weird to call target manually all the time. Is there any way to make it call automatically?
Here we have to solve two problems:
1. How to know automatic data changes 2. How to automatically call a specific functionCopy the code
1.2 The way I came up with is from the little Red BookObject.defineProperty()
, the use ofObject.defineProperty()
thegetter
As well assetter
Interception feature, let us test it.
"use strict"; function say() { console.log('hello')}function defineReactive(obj, key, val) { return Object.defineProperty(obj, key, { get() { console.log('get->', key) return val }, set(newVal) { if (newVal === val) return; console.log(`set ${key} from ${val} to ${newVal}`) // When the data changes, we call the function say() val = newVal } }) } let source = {} defineReactive(source, 'a'.1) console.log(source.a) source.a = 99 console.log(source.a) Copy the code
Results:
As you can see, our get and set on A have been identified, and our say function has been called successfully.
So how do we reproduce the example where c is equal to a plus b? As follows:
"use strict"; function defineReactive(obj, key, val) { return Object.defineProperty(obj, key, { get() { console.log('get->', key) return val }, set(newVal) { if (newVal === val) return; console.log(`set ${key} from ${val} to ${newVal}`) // When the data changes, we call the function fn() val = newVal } }) } let source = {} defineReactive(source, 'a'.1) defineReactive(source, 'b'.2) let c; function fn(){ c = source.a + source.b; console.log('c 'automatically changes to'. ',c) } // initialize c fn() source.a = 99 Copy the code
We find that fn is automatically called, but the value of c is still 3. How should we solve this problem?
After checking, it was found that there was a problem in set (fn was called first, so val had not had time to change).
set(newVal) { if (newVal === val) return; console.log(`set ${key} from ${val} to ${newVal}`) // When the data changes, we call the function val = newVal fn() } Copy the code
The above problem is solved.
2. Make an object change
Because the object operation is more complex, so we first implement the interception of the object operation, such as object acquisition and setting I know.
2.1 Intercepting the acquisition and setting of object properties
Add observe to redefine an object’s property traversal (similar to defining a data variable)
"use strict";
/** * Where defineReactive is actually a closure, the outer side references variables inside the function, causing these temporary variables to persist */
function defineReactive(obj, key, val){
Observe that val is an object. The value in the object has no response
observe(val)
// Use getter setters to intercept data
Object.defineProperty(obj,key, {
get(){
console.log('get', key)
return val
},
set(newVal){
if( newVal ! == val){console.log(`set ${val} -> ${newVal}`)
val = newVal
}
}
})
}
// 2. Observe an object and make its properties responsive
function observe(obj){
// We want to pass in an Object
if( typeofobj ! = ='object' || typeof(obj) == null) {return ;
}
Object.keys(obj).forEach(key= >{
defineReactive(obj, key, obj[key])
})
}
let o = { a: 1.b: 'hello'.c: {age:9}}
observe(o)
o.a
o.a = 2
o.b
o.b = 'world'
Copy the code
2.2 Let object properties change
To simplify the program, let’s look at only one layer of objects
"use strict";
const { log } = console;
let target = null;
let data = { a: 1.b:2 }
let c, d;
// Depending on collection, each Object key has a Dep instance
class Dep{
constructor(){
this.deps = []
}
depend(){
target && !this.deps.includes(target) && this.deps.push(target)
}
notify() {
this.deps.forEach(dep= >dep() )
}
}
Object.keys(data).forEach(key= >{
let v = data[key]
const dep = new Dep()
Object.defineProperty(data, key, {
get(){
dep.depend()
return v;
},
set(nv){
v = nv
dep.notify()
}
})
})
function watcher(fn) {
target = fn
target()
target = null
}
watcher(() = >{
c = data.a + data.b
})
watcher(() = >{
d = data.a - data.b
})
log('c=',c)
log('d=',d)
data.a = 99
log('c=',c)
log('d=',d)
/** c= 3 d= -1 c= 101 d= 97 */
Copy the code
- To summarize the process:
Intercept data with defineProperty for each key in data object, collect Dep dependencies in GET, and notify data updates in set.
In fact, the dependency collection is to add the Watcher instance to the DEPS queue, and execute the function in the queue when it receives an update notification to achieve the effect of automatic data update.
3. Read source code
While reading the source code, let’s first take a look at the official website’s interpretation of the data responsiveness for ease of entry.
The granularity of Watcher is component, that is, each component corresponds to a Watcher.
So what is Watcher? What is Dep? What does an Observer do? Let’s go to the source code to find the answer.
The test code
<! DOCTYPEhtml>
<html lang="en">
<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">
<title>Document</title>
<script src='.. /dist/vue.js'></script>
</head>
<body>
<div id="app">
{{a}}
</div>
<script>
const app = new Vue({
el:'#app'.data: {
a: 1
},
mounted(){
setInterval(() = >{
this.a ++
}, 3000)}})</script>
</body>
</html>
Copy the code
Trace call stack
A flow chart drawn by myself
talk is cheap, show me the code
In particular, to simplify the process, all source code display, have been deleted
src\core\instance\index.js
An entry file
function Vue (options) {
/** * initializes */
this._init(options)
}
/** * The following uses the vue. prototype mount method to mix in other methods */
initMixin(Vue)
/** * initMixin provides an __init method to Vue. Initialize the lifecycle initLifecycle -> initEvents -> initRender -> callHook(vm, 'beforeCreate') -> initInJections -> initState -> initProvide -> callHook(vm, 'created') -> if (vm.$options.el) { vm.$mount(vm.$options.el) } */
stateMixin(Vue)
/** stateMixin $data -> $props -> $set -> $delete -> $watch */
eventsMixin(Vue)
/** eventsMixin $on $once $off $emit */
lifecycleMixin(Vue)
/** lifecycleMixin * _update(), $forceUpdate, $destroy * */
renderMixin(Vue)
/** * $nextTick, _render, $vnode * * */
export default Vue
Copy the code
src\core\instance\init.js
/ * *
* initMixin
With this method, Vue is provided with an __init method to initialize the lifecycle
initLifecycle -> initEvents -> initRender
-> callHook(vm, ‘beforeCreate’) -> initInJections
-> initState -> initProvide
-> callHook(vm, ‘created’)
-> if (vm.$options.el) {
vm.
options.el)}
* /
export function initMixin (Vue: Class<Component>) {
$reject $delete $reject $delete $reject $reject $delete $reject $reject $delete $reject $reject $delete $reject $reject $delete $reject $reject $delete $reject
initState(vm)
/** * creates the hook function */
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
Copy the code
src\core\instance\state.js\initState
Data is preprocessed
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
/** * state * props -> methods -> data -> computed -> watch */
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) { initData(vm) }
if (opts.computed) initComputed(vm, opts.computed)
}
Copy the code
src\core\instance\state.js\initData
Observe data, getter data,setter interception data
function initData (vm: Component) {
let data = vm.$options.data
// proxy data on instance
/**
* 数据代理
*/
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
/** * @proxy */
proxy(vm, `_data`, key)
}
/** * @reactive operation */
observe(data, true /* asRootData */)}Copy the code
“src\core\instance\state.js\proxy`
const sharedPropertyDefinition = {
enumerable: true.configurable: true.get: noop,
set: noop
}
/** * proxy(vm, '_data', key) 168 lines */
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
Copy the code
src\core\observer\index.js\observe
Creating an Observer Instance
export function observe (value: any, asRootData: ? boolean) :Observer | void {
if(! isObject(value) || valueinstanceof VNode) {
return
}
/** * @observer */
let ob: Observer | void
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
Copy the code
SRC \ core \ observer \ index js \ the observer
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
/** * @why declare deP in Observer * create deP instance * Add/remove property in Object * change method in array */
this.dep = new Dep()
this.vmCount = 0
/** * Sets an __ob__ attribute referencing the current Observer instance */
/** * export function def (obj: Object, key: string, val: any, enumerable? : boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !! enumerable, writable: true, configurable: true }) } */
def(value, '__ob__'.this)
/** * Type check */
/ / array
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
/** * if the elements in the array are still objects, we need to do reactive processing */
this.observeArray(value)
} else {
// is an 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])
}
}
/** * Observe a list of Array items. */
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
Copy the code
SRC \ core, the observer, dep. Js \ dep class
Depend on the collection
Subs is a Watcher queue
/** * A DEP is an Observable that can have multiple * directives subscribing to it. * /
export default 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 () {
Dep.target && Dep.target.addDep(this)
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Copy the code
SRC \ core \ observer \ watcher js \ watcher
/** * The observer parses the expression, collects dependencies, and triggers a callback when the expression value changes. * This is used for the $watch() API and directives. * /
export default class Watcher {
constructor () {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
this.expression = process.env.NODE_ENV ! = ='production'
? expOrFn.toString()
: ' '
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop process.env.NODE_ENV ! = ='production' && warn(
`Failed watching path: "${expOrFn}"` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/** * Evaluate the getter, and re-collect dependencies. */
get () {
pushTarget(this)... popTarget()this.cleanupDeps()
return value
}
/** * Add a dependency to this directive. */
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)}}}/** * Clean up for dependency collection. */
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)}}}/** * Subscriber interface. * Will be called when a dependency changes. */
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}/** * Scheduler job interface. * Will be called by the scheduler. */
run () {
if (this.active) {
const value = this.get()
if( value ! = =this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "The ${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */
evaluate () {
this.value = this.get()
this.dirty = false
}
/** * Depend on all deps collected by this watcher. */
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/** * Remove self from all dependencies' subscriber list. */
teardown () {
if (this.active) {
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)}let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)}this.active = false}}}Copy the code
Learning outcomes
The declaration of data, unless otherwise specified, refers to the predefined data object that was defined when the component was defined.
1. What is reactive
Take C =a+b to show that when a or B changes, C will also change, which is the essence of the data response.
2. Vue
How do I know when the data is updated?
Vue2. X redefines user-defined data using Object.defineProperty before mounting it to the component. When the component retrieves or updates the data, it fires getters or setters to let the component know that the user “acted on” the data.
3. What isWatcher
?
A component in Vue corresponds to a Watcher, which we call a “subscriber.”
It is used to subscribe to data changes and perform operations (such as update views).
4. What isDep
Each key of the component data object corresponds to a Dep instance.
Dep is an observable class that Watcher can subscribe to once it is instantiated.
Vue does dependency collection in the Dep.
Dep has a class attribute, Target, that holds the Watcher subscriber, which is key to the dependency collection.
- Dep.depend () is called when the data property is got.
- In dep.depend, if dep. target exists, the corresponding Watcher is told to add the dependency
- When data’s property key is set (that is, when it is updated), the corresponding dep.notify() is called. Notify calls all Watcher subscribers to the property for uOdate updates
5. What isObserver
?
An Observer instance is created when the component calls Observe (data), which redefines data with objce. property and then mounts it to the component.
We call it “observer”, because he converts data into a responsive data by observing data. Some people joke that data has since gone through the “baptism of socialism” and has become a mature successor of socialism. It is worth noting that for each Observer instance, there is also a unique Dep instance corresponding to it.
6. How many different Watchers are there in Vue?
Since Watcher is for updates, we can think about how many scenarios in Vue need to be updated.
Such as:
- Data changes → View using data changes
- Data changes → Computational properties using data change → views using computational properties change
- Data change → the developer actively registers the watch callback function execution
Three scenarios, corresponding to three types of Watcher:
Component Watcher, i.e., render-watcher
User-defined computed attributes correspond to computed-watcher
Watcher (watch-API or Watch property) for user-defined listening properties
Interview question: Please talk about your understanding of the data responsiveness principle
First of all, we can take a look at C =a+ B. In this equation, C is our target data, and both A and B are dependent on C. When a or B changes, C will automatically update, which is the data response formula I understand.
In Vue, Dep, Watcher and Observer classes are mainly related to data responsiveness.
When we create the component, vue observes the user’s predefined data, creates an Observer instance, and redefines the data Object with the getter and setter of Object.defineProperty() so that all operations performed on the data are performed. Can be detected by the component.
And then when we get the Watcher instance, we call the get function in Watcher, and the get function pushes the currently triggered dependency into the targetStack, and then fires that dependency, the getter for obj. Key that we defined earlier, If dep. target is not empty depend is called for dependency collection.
When we change the data again, the triggered setter for obj.key calls the notify function of the corresponding Dep instance. Notify traverses the subscriber queue and calls all the update functions subscribed to the Keydependent Watcher to update the data. So as to achieve the purpose of data response.
May I ask the interviewer whether my understanding is wrong?