1. Publish subscribe vs. observer
1.1 Publish and Subscribe
- The subscriber
- The publisher
- The signal center
Let us suppose that there is a “signal center” to which a task “publishes” a signal when it is completed, and to which other tasks can “subscribe” to know when they are ready to perform. This is called a publish-subscribe pattern.
1.1.1 Publish/subscribe applications
- Vue User-defined events
let vm = new Vue()
// { 'click': [fn1, fn2], 'change': [fn] }
// Register event (subscription message)
vm.$on('dataChange'.() = > {
console.log('dataChange')
})
vm.$on('dataChange'.() = > {
console.log('dataChange1')})// Trigger event (publish message)
vm.$emit('dataChange')
Copy the code
- eventBus
// Event center
let eventHub = new Vue()
// ComponentA. vue
/ / publisher
addTodo: function () {
// Publish a message (event)
eventHub.$emit('add-todo', { text: this.newTodoText })
this.newTodoText = ' '
}
// ComponentB.vue
/ / subscriber
created: function () {
// Subscribe to message (event)
eventHub.$on('add-todo'.this.addTodo)
}
Copy the code
1.1.2 Publish subscribe implementation
class EventEmitter {
constructor () {
// { eventType: [ handler1, handler2 ] }
this.subs = Object.create(null)}// Subscribe to notifications
$on (eventType, handler) {
this.subs[eventType] = this.subs[eventType] || []
this.subs[eventType].push(handler)
}
// Issue a notification
$emit (eventType) {
if (this.subs[eventType]) {
this.subs[eventType].forEach(handler= > {
handler()
})
}
}
}
/ / test
var bus = new EventEmitter()
// Register the event
bus.$on('click'.function () {
console.log('click')
})
bus.$on('click'.function () {
console.log('click1')})// Triggers the event
bus.$emit('click')
Copy the code
1.2 Observer Mode
The observer pattern follows the design principle that the subject and the observer are separate, not actively triggered but passively listening, and decouple
// Subject, receives state changes, triggers each observer
class Subject {
constructor() {
this.state = 0
this.observers = []
}
getState() {
return this.state
}
setState(state) {
this.state = state
this.notifyAllObservers()
}
// Subscribe to notifications
attach(observer) {
this.observers.push(observer)
}
// Execute the notification
notifyAllObservers() {
this.observers.forEach(observer= > {
observer.update()
})
}
}
// The observer, waiting to be triggered
class Observer {
constructor(name, subject) {
this.name = name
this.subject = subject
this.subject.attach(this)}update() {
console.log(`The ${this.name} update, state: The ${this.subject.getState()}`)}}// Test the code
let s = new Subject()
let o1 = new Observer('o1', s)
let o2 = new Observer('o2', s)
let o3 = new Observer('o3', s)
s.setState(1)
s.setState(2)
s.setState(3)
Copy the code
1.3 Differences between publish subscribe and observer patterns
Conclusion:
- The observer pattern is scheduled by specific targets, such as when an event is triggered, Dep will call the observer method, so there is a dependency between the observer pattern subscribers and publishers.
- The publish/subscribe pattern is invoked by a unified scheduling center, so publishers and subscribers do not need to be aware of each other’s existence.
1.4 Observer mode in Vue
// Target (publisher)
// Dependency
class Dep {
constructor () {
// Store all observers
this.subs = []
}
// Add an observer
addSub (sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// Notify all observers
notify () {
this.subs.forEach(sub= > {
sub.update()
})
}
}
// User (user)
class Watcher {
update () {
console.log('update')}}/ / test
let dep = new Dep()
let watcher = new Watcher()
dep.addSub(watcher)
dep.notify()
Copy the code
- Observer (subscriber) – Watcher
- Update (): Specifies what to do when an event occurs
- Target (publisher) — Dep
- Subs array: Stores all observers
- AddSub (): adds an observer
- Notify (): When an event occurs, call the update() method on all observers
- No event center
2. Vue MVVM simple implementation
2.1 the Vue
- function
- Responsible for receiving initialization parameters (options)
- Responsible for injecting properties from data into Vue instances, turning them into getters/setters
- Is responsible for calling an observer to listen for changes to all properties in the data
- Responsible for calling compiler parsing instructions/difference expressions
- structure
In this case, we mount the data attribute to the Vue instance so that we can use the data, instead of this.data. Can be
class Vue {
constructor (options) {
// 1. Save the data of the option through the attribute
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 2. Convert data members into getters and setters and inject them into vue instance
this._proxyData(this.$data)
// 3. Call the Observer object to listen for data changes
new Observer(this.$data)
// 4. Call the Compiler object to parse the instructions and difference expressions
new Compiler(this)}_proxyData(data) {
Object.keys(data).forEach(key= > {
Object.defineProperty(this, key, {
enumerable: true.configurable: true.get() {
return data[key]
},
set(newValue) {
if(newValue ! == data[key]) { data[key] = newValue } } }) }) } }Copy the code
2.2 the Observer
- function
- Is responsible for converting the properties in the Data option to responsive data
- A property in data is also an object, and that property is converted to reactive data
- Data change notification is sent
- implementation
Here we need to note that in get we return val instead of data[key], because using data[key] triggers the getter and becomes an infinite loop.
In addition, we modify val in set, and get returns val. Val is locked in defineReactive, so we can modify val in both get and set
class Observer {
constructor (data) {
this.walk(data)
}
walk(data) {
// 1. Check whether data is an object
if(! data ||typeofdata ! = ='object') {
return
}
Object.keys(data).forEach(key= > {
this.defineReactive(data, key, data[key])
})
}
defineReactive(data, key, val) {
let _self = this
let dep = new Dep()
this.walk(val)
Object.defineProperty(data, key, {
enumerable: true.configurable: true.get() {
Dep.target && dep.addSub(Dep.target) // The observer mode adds an observer
return val
},
set(newValue) {
if(newValue ! == val) { val = newValue _self.walk(newValue) dep.notify()// The observer mode publishes notifications}}}Copy the code
3.3 Dep
- function
- Collect dependencies, add Watcher
- Inform all observers
- implementation
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
if(sub && sub.update) {
this.subs.push(sub)
}
}
notify() {
this.subs.forEach(sub= > {
sub.update()
})
}
remove() {
if(arr.length) {
const index = arr.indexOf(item)
if(index > -1) {
return arr.splice(index, 1)}}}}Copy the code
3.4 watcher
- function
- When data changes trigger dependencies, DEP informs all Watcher instances to update the view
- Adds itself to the DEP object when it instantiates itself
- implementation
We set dep. target = this, Then call this.oldValue = VM [key], which triggers a getter, i.e. the get method in defineReactive does dep.target && dep.addsub (dep.target). Add an observer, and then we empty target dep. target = null
class Watcher {
constructor (vm, key, cb) {
this.vm = vm
// Attribute name in data
this.key = key
// Call cb to update the view when the data changes
this.cb = cb
// Record the current watcher object on the static property of the Dep and add the watcher to the SUBs of the Dep when data is accessed
Dep.target = this
// Trigger a getter that tells DEp to record watcher for the current key
this.oldValue = vm[key]
/ / clear the target
Dep.target = null
}
update () {
const newValue = this.vm[this.key]
if (this.oldValue === newValue) {
return
}
this.cb(newValue)
}
}
Copy the code
3.5 call watcher
Create a watcher object in compiler.js for each instruction/interpolation to monitor data changes
// because it is used in textUpdater, etc
this updaterFn && updaterFn.call(this, node, this.vm[key], key)
// Update method of the v-text directive
textUpdater (node, value, key) {
node.textContent = value // Create a watcher for each instruction to watch the data change
new Watcher(this.vm, key, value= > {
node.textContent = value
})
}
Copy the code
View changes update data
// Update method of the V-model directive
modelUpdater (node, value, key) {
node.value = value // Create a watcher for each instruction to watch the data change
new Watcher(this.vm, key, value= > {
node.value = value
})// Listen for changes to the view
node.addEventListener('input'.() = > {
this.vm[key] = node.value
})
}
Copy the code
3.6 the batch
Function:
- Handle view rendering asynchronously, so that when we modify data multiple times, we only render the last time
- Because of asynchronous rendering, the data we get here must be the last data.
- We can conclude that the modification data is synchronous and the rendering page is asynchronous
class Batcher {
constructor () {
this.has = {}
this.queue = []
this.waiting = false
}
push(job) {
let id = job.id
// If there is no id, proceed to the next step
if (!this.has[id]) {
this.queue.push(job)
// Set the ID of the element
this.has[id] = true
if (!this.waiting) {
this.waiting = true
if ("Promise" in window) {
Promise.resolve().then( () = > {
this.flush()
})
} else {
setTimeout(() = > {
this.flush()
}, 0)}}}}flush() {
this.queue.forEach((job) = > {
job.cb(job.newValue)
})
this.reset()
}
reset() {
this.has = {}
this.queue = []
this.waiting = false}}Copy the code
3.7 Overall Process
Source: gitee.com/zxhnext/fed…
4. Vue source MVVM
Let’s look at the source code to find answers to the following questions
vm.msg = { count: 0 }
, re-assign a value to the property, is it responsive?vm.arr[0] = 4
To assign a value to an array element, whether the view will be updatedvm.arr.length = 0
To change the length of the array and whether the view will be updatedvm.arr.push(4)
, whether the view will be updated
4.1 initState
In the Vue initialization phase, _init method performs, executes initState (vm) method, it is defined in SRC/core/instance/state. Js This will determine whether the data function, Call the methods () method after determining whether it has the same name as methods/props/
- Order of execution:
- props
- methods
- data
- computed
- watch
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
// Check if data is a function and has the same name as methods/props/, and then call observer().
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)
if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) }Copy the code
The initState method initializes the props, methods, data, computed, and Wathcer properties
There are two main things to do here: one is to iterate over the object returned by defining the data function, and proxy each value vm._data.xxx to vm.xxx. The other is to call the observe method to observe the entire data change, making data also responsive
4.2 Reactive objects
1. observer
The observer in/SRC/core/observer/index. Js
- Check whether value is an object or vNode instance. If yes, return value directly
- If the value is
__ob__
(Observer object) property, indicating that the object already exists - Create an Observer object
export function observe (value: any, asRootData: ? boolean) :Observer | void {
// Check whether value is an object or a VNode instance
if(! isObject(value) || valueinstanceof VNode) {
return
}
let ob: Observer | void
// If value has an __ob__(observer object) attribute, the object already exists
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) && ! value._isVue// Cannot be a vue instance
) {
// Create an Observer object
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
Copy the code
2. Observer
- src\core\observer\index.js
- Add an instance of itself to the data object value by executing the def function
__ob__
attribute - Reactive processing of the object
- Responding to arrays (a separate section, which I’ll skip here)
- walk(obj)
- Walk through all the properties of OBj, calling defineReactive() for each property, setting the getter/setter
- Add an instance of itself to the data object value by executing the def function
export class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
// Initialize the instance vmCount to 0
this.vmCount = 0
// Mount the instance to the __ob__ attribute of the observed object
def(value, '__ob__'.this)
// Array response processing
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// Create an observer instance for each object in the array
this.observeArray(value)
} else {
// Iterates over each property in the object, turning it into a setter/getter
this.walk(value)
}
}
walk (obj: Object) {
// Get each property of the observed object
const keys = Object.keys(obj)
// Iterate over each attribute and set it to responsive data
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
Copy the code
3. defineReactive()
- src\core\observer\index.js
- defineReactive(obj, key, val, customSetter, shallow)
- Define a responsive property for an object, each property corresponding to a DEP object
- If the object is not configurable, do nothing
- If the value of the property is an object, the call to Observe continues
- If a new value is assigned to the property, the call to observe continues
- Create dependencies in GET
- Set sends notifications if data is updated
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function, shallow? : boolean) {
// Create an instance of the dependent object
const dep = new Dep()
// Get the property descriptor object for obj. If the object is not configurable, return it directly
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
if((! getter || setter) &&arguments.length === 2) {
val = obj[key]
}
// Determine whether to recursively observe the child object, and turn the child object properties into getters/setters, returning the child observation object
letchildOb = ! shallow && observe(val)Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get: function reactiveGetter () {
// Value equals the return value of the getter call if a predefined getter exists
// Otherwise, give the attribute value directly
const value = getter ? getter.call(obj) : val
// Create a dependency if the current dependency target, a watcher object, exists
if (Dep.target) {
dep.depend()
// If the subobject of observation exists, establish dependencies between the subobjects
if (childOb) {
childOb.dep.depend()
// If the property is an array, the special treatment is to collect array object dependencies
if (Array.isArray(value)) {
dependArray(value)
}
}
}
// Return the attribute value
return value
},
set: function reactiveSetter (newVal) {
// Value equals the return value of the getter call if a predefined getter exists
// Otherwise, give the attribute value directly
const value = getter ? getter.call(obj) : val
// Do not execute if the new value is equal to the old value or if the old value is NaN
/* 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()
}
Return if there is no setter
// #7981: for accessor properties without setter
if(getter && ! setter)return
// Call if the predefined setter exists, otherwise update the new value directly
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// If the new value is an object, observe the child object and return the child observer objectchildOb = ! shallow && observe(newVal)// Distribute updates (publish change notices)
dep.notify()
}
})
}
Copy the code
4.3 Collecting Dependencies
1. dep
- Dep is actually a kind of management for Watcher
- addSub
- notify
Dep.target
Only one watcher can be used at a time
export default class Dep {
// Static property, watcher object
statictarget: ? Watcher;// Dep Instance Id
id: number;
// Watcher object/subscriber array corresponding to the dep instance
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
// Add a new subscriber watcher object
addSub (sub: Watcher) {
this.subs.push(sub)
}
// Create dependencies between the observed objects and the watcher
depend () {
if (Dep.target) {
// If the target exists, add the dep object to the watcher dependency
Dep.target.addDep(this)}}// Issue a notification
notify () {
const subs = this.subs.slice()
// Call each subscriber's update method to implement the update
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// dep. target is used to store the watcher currently in use
// Only one watcher can be used at a time
Dep.target = null
const targetStack = []
// Push the stack and assign the current watcher to dep.target
// When nesting parent components, first push the watcher corresponding to the parent component.
// Remove the watcher of the parent component from the stack and continue
export function pushTarget (target: ? Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
// Exit the stack
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]}Copy the code
2. watcher
- get
- PushTarget Pushes the stack and assigns the current watcher to dep. target
- Value = this.getter.call(VM, VM) // Read value, trigger get
- PopTarget: The popTarget is the next target assigned to the stack
- update
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}Copy the code
3. Register watcher
updateComponent = () = > {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')}}},true /* isRenderWatcher */)
Copy the code
4.4 Array Processing
1. Get the array prototype arrayMethods
Create a variable called arrayMethods, which inherits from Array.prototype and has all of its functionality. In the future, we’ll use arrayMethods to override Array.prototype.
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto) ; ['push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
].forEach(function(method) {
// Cache the original method
const original = arrayProto[method]
Object.defineProperty(arrayMethods, method, {
value:function mutator(. args) {
return original.apply(this, args)
},
enumerable:false.writable:true.configurable: true})})Copy the code
2. Cover responsive dataArray.prototype
We override array. prototype with arrayMethods. While we can’t override directly because that would pollute the global Array, we want the interceptor to override only the data whose changes are detected. That is, we want the interceptor to override only the prototype of the responsive Array. Turning a data into reactive data requires an Observer, so we only need to use interceptors in the Observer to override the prototypes that are going to be converted into reactive Array data
export class Observer {
constructor(value){
this.value = value
if(Array.isArray(value)){
value.__proto__ = arrayMethods/ / new
} else {
this.walk(value)
}
}
}
Copy the code
__proto__ = arrayMethods is used to assign an interceptor (an arrayMethods that has been processed to intercept) to value.__proto__ is a clever way to override the value stereotype
3. No__proto__
In the case
When __proto__ cannot be used, Vue does a pretty rough job of simply setting arrayMethods methods on the array being detected if __proto__ cannot be used
//_proto_ Whether available
const hasProto = '__proto__' in {}
const arrayKeys = Object.getOMnPropertyNames(arrayMethods)
export class Observer {
constructor(value){
this.value = value
if (Array.isArray(value)) {
/ / modify
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value)
}
}
}
function protoAugment(target, src, keys) {
target.__proto__ = src
}
function copyAugment(target, src, keys) {
for(let i=0,l = keys.length; i<l; i++) {
const key = keys[i]
def(target,key,src[key])
}
}
Copy the code
4. Add an array__ob__
__ob__ is used not only to access the Observer instance in the interceptor, but also to indicate whether the current value has been converted to responsive data by the Observer. That is, all data whose changes are detected will have an __ob__ attribute to indicate that they are responsive. If value is responsive, __ob__ is returned; If it is not responsive, the new Observer is used to transform the data into responsive data. When the value is marked __ob__, the observer instance can be accessed through the value. In the case of an Array interceptor, because the interceptor is a prototype method, the Observer instance can be accessed directly through this.__ob__
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto) ; ['push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
].forEach(function(method) {
// Cache the original method
const original= arrayProto[method]
def(arrayMethods,method, function mutator(. args){
const result = original.apply(this,args)
const ob = this.__ob__
ob.dep.notify() // Send a message to the dependency
return result
})
})
Copy the code
5. Detects array element changes
export class Observer {
constructor(value){
this.value = value
def(value, '__ob__'.this)
if (Array.isArray(value)) {
/ / modify
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
this.observerArray(value)
} else {
this.walk(value)
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
Copy the code
6. Listen for new elements in the array
; ['push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
].forEach(function(method) {
// Cache the original method
const original= arrayProto[method]
def(arrayMethods,method, function mutator(. args){
const result = original.apply(this,args)
const ob = this.__ob__
let inserted
switch(method){
case 'push':
case 'unshift':
inserted= args
break
case 'splice':
inserted = args.slice(2)
break
}
if(inserted) ob.observerArray(inserted)
ob.dep.notify() // Send a message to the dependency
return result
})
})
Copy the code
If method is push, unshift, or splice, then the new element is removed from args and placed in inserted. Next, an Observer is used to turn the elements in inserted into responsive.
4.5 Update the render queue asynchronously
-
The has object is guaranteed to be added to the same Watcher only once
-
Flushing indicates whether the queue is being processed, adding directly to the queue if not, or inserting into the queue otherwise
-
Waiting indicates whether the current queue is executing. If not, call flushSchedulerQueue, ensuring that nextTick(flushSchedulerQueue) is called only once
-
Purpose of queue
-
- Components are updated in order from parent to child, because the parent component is created before the child component is created
-
- User custom Watcher is executed before render watcher; Because the user custom Watcher is created before the watcher is rendered
-
- If a component is destroyed during the execution of the parent’s watcher, its watcher execution can be skipped, so the parent’s watcher should be executed first
-
-
Traverse the watcher
- Do not cache the queue length because the queue is added dynamically
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
// Whether the queue is being processed
if(! flushing) {// Add to queue
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1.0, watcher)
}
// queue the flush
if(! waiting) { waiting =true
if(process.env.NODE_ENV ! = ='production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// The purpose of sorting
// 1. The component update sequence is from parent to child, because the parent component is created first and then the child component is created
// 2. The component's user watcher should run before rendering the watcher
// 3. If a parent component is destroyed before a component is run, skip this update
queue.sort((a, b) = > a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
// Do not cache the queue length because it is added dynamically
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')}}Copy the code