Source code analysis vUE responsive principle
After a brief implementation of the vUE responsive principle (see this if you can’t open it), we have a brief understanding of how responsive is implemented through the publish-subscribe model, this article mainly from the source analysis of the VUE responsive principle implementation.
Find initData
In vue/ SRC /core/index.js, you can see import vue from ‘./instance/index’, importing vue.
In vue/SRC/core/instance/index, js,
import { initMixin } from './init'
/ /...
function Vue (options) {
/ /...
this._init(options)
}
initMixin(Vue)
// ...
export default Vue
Copy the code
As you can see, Vue is a function method that calls an initialization method called _init and passes in the options argument. The file also executes the initMixin method.
In the vue/SRC/core/instance/init. Js,
// ...
import { initState } from './state'
import { extend, mergeOptions, formatComponentName } from '.. /util/index'
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options? :Object) {
const vm: Component = this
// ...
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// ...
initState(vm)
// ...}}Copy the code
See that the _init method is defined in the initMixin method. In the _init method, the constant VM is declared and the current instance is assigned, the options are accepted and processed, and the initState method is called.
We won’t go into options in this article, but it is important to note that the object parameters passed in when instantiating Vue can be obtained here.
In the vue/SRC/core/instance/state. Js,
import {
set,
del,
observe,
defineReactive,
toggleObserving
} from '.. /observer/index'
export function initState (vm: Component) {
// ...
const opts = vm.$options
// ...
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)}// ...
}
Copy the code
When instantiating Vue, such as
new Vue({
el: '#app'.data: {
text: 'hello world',}});Copy the code
The data passed in is opts.data, and when it exists, call initData, otherwise call the observe method, and initialize the _data property as an empty object.
initData
We’ll talk about the observe method later, but we already found the initData method, so let’s see.
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
}
Copy the code
Declare the data object and assign it to vm.$options.data.
GetData (data, vm) is called when data is a function, and the result is assigned to data and vm._data. If yes, set vm._data to data. If no, set data and vm._data to empty objects.
export function getData (data: Function, vm: Component) :any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return{}}finally {
popTarget()
}
}
Copy the code
Vue/SRC/core/observer/dep. Js:
Dep.target = null
const targetStack = []
export function pushTarget (target: ? Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]}Copy the code
Call (vm, VM). When data is a function, this of the data method points to the current vue instance, calls the data method and passes in the vue instance.
Then down
function initData (vm: Component) {
// ...
// Determine whether data is an Object instance, if not, assign the value to an empty Object, and warn in non-production environments that the value returned by data must be an Object
if(! isPlainObject(data)) { data = {} process.env.NODE_ENV ! = ='production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// ...
}
Copy the code
vue/src/shared/util.js
export function isPlainObject (obj: any) :boolean {
return _toString.call(obj) === '[object Object]'
}
Copy the code
To continue down
function initData (vm: Component) {
// ...
// Get an array of property names
const keys = Object.keys(data)
/ / get props
const props = vm.$options.props
/ / get the methods
const methods = vm.$options.methods
// Iterate over attributes
let i = keys.length
while (i--) {
const key = keys[i]
// Non-production environment, method name and attribute name same name warning
if(process.env.NODE_ENV ! = ='production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
// For non-production environments, props and properties have the same name warning
if(props && hasOwn(props, key)) { process.env.NODE_ENV ! = ='production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if(! isReserved(key)) {// Filter attributes beginning with $or _ and proxy the rest to the instance's _data attribute.
proxy(vm, `_data`, key)
}
}
// Call the observe method
observe(data, true /* asRootData */)}Copy the code
vue/src/core/util/lang.js
export function isReserved (str: string) :boolean {
const c = (str + ' ').charCodeAt(0)
return c === 0x24 || c === 0x5F
}
Copy the code
vue/src/core/instance/state.js
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
observe
vue/src/core/observer/index.js
export let shouldObserve: boolean = true
export function observe (value: any, asRootData: ? boolean) :Observer | void {
// Make sure that the value passed (data above) is an object
if(! isObject(value) || valueinstanceof VNode) {
return
}
// Define an ob variable
let ob: Observer | void
// Determine whether the data object already has an __ob__ attribute and is an instance of Observer, and assign it to ob if it does
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
}
/** * otherwise judge: * When shouldObserve is true (which defaults to true and needs to be changed by toggleObserving), * and is not a service rendering, * and data is an instance of an array or Object, * and the data Object is extensible, * And the data object is not an instance of Vue * * If the above conditions are met, generate an Observer instance from the data object and assign it to ob */
else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value)
}
if (asRootData && ob) {
// In the case of root data, the ob object's vmCount property counts +1
ob.vmCount++
}
/ / returns the ob
return ob
}
Copy the code
vue/src/shared/util.js
export function isObject (obj: mixed) :boolean %checks {
returnobj ! = =null && typeof obj === 'object'
}
Copy the code
Observer constructor
import { arrayMethods } from './array'
import Dep from './dep'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
// Override the native array method on the prototype object with __proto__ as the vUE rewritten array method
function protoAugment (target, src: Object) {
target.__proto__ = src
}
// The seven array methods overwritten by the variable are defined one by one on the data object to override the native array methods on the prototype chain
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
export class Observer {
value: any;
dep: Dep;
vmCount: number; // Count the current data as the vUE instance of root data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
Define an __ob__ attribute on data using the object.defineProperty method
def(value, '__ob__'.this)
// When data is an array
if (Array.isArray(value)) {
// Check whether there is __proto__
if (hasProto) {
// Override the native array method on the prototype object with __proto__ as the vUE rewritten array method
protoAugment(value, arrayMethods)
} else {
// The seven array methods overwritten by the variable are defined one by one on the data object to override the native array methods on the prototype chain
copyAugment(value, arrayMethods, arrayKeys)
}
Call the observeArray method
this.observeArray(value)
} else {
// Call walk when data is an object
this.walk(value)
}
}
// Call defineReactive on each property of data
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// Traverse the array and call the observe method one by one
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
Copy the code
vue/src/core/util/lang.js
export function def (obj: Object, key: string, val: any, enumerable? : boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable:!!!!! enumerable,writable: true.configurable: true})}Copy the code
vue/src/core/util/env.js
export const hasProto = '__proto__' in {}
Copy the code
vue/src/core/observer/array.js
Vue overwrites seven prototype array methods ‘push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]
// Rewrite the seven array methods above so that changing an array using these seven methods triggers notify to update the view
methodsToPatch.forEach(function (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.observeArray(inserted)
ob.dep.notify()
return result
})
})
Copy the code
defineReactive
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function, shallow? : boolean) {
// Declare a constant DEp as an instance of deP, where closure is formed
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
If the property cannot be overridden, it is returned directly
if (property && property.configurable === false) {
return
}
// Cache getters and setters
const getter = property && property.get
const setter = property && property.set
// If there is no getter but only a setter, and no val value is passed in, assign the value from data to the val variable
if((! getter || setter) &&arguments.length === 2) {
val = obj[key]
}
// Shallow is false, the observe recursive child is called, and the returned Observer is assigned to childOb
letchildOb = ! shallow && observe(val)// Override attributes
Object.defineProperty(obj, key, {
enumerable: true./ / can be enumerated
configurable: true./ / can be configured
get: function reactiveGetter () {
// Execute if the original getter exists
const value = getter ? getter.call(obj) : val
/ / Dep. Target exists
if (Dep.target) {
// Call Depend to collect the observer
dep.depend()
// There are child objects that collect child object observers
if (childOb) {
childOb.dep.depend()
// If the value is an array, the depth is traversed to collect observers
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// Execute if the original getter exists
const value = getter ? getter.call(obj) : val
// The value is changed or returned directly for NaN
if(newVal === value || (newVal ! == newVal && value ! == value)) {return
}
// call the customSetter for non-production environments
if(process.env.NODE_ENV ! = ='production' && customSetter) {
customSetter()
}
// Returns with getter but no setter
if(getter && ! setter)return
// if the original setter exists
if (setter) {
setter.call(obj, newVal)
} else { // There is no direct update value
val = newVal
}
// Shallow is false, the observe recursive child is called, and the returned Observer is assigned to childObchildOb = ! shallow && observe(newVal)// Call notify on the DEP instance to publish notifications
dep.notify()
}
})
}
// Deep traversal number group, have __ob__ need to collect observer
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
Copy the code
Dep constructor
Vue/SRC/core/observer/dep. Js:
export default class Dep {
statictarget: ? Watcher;// dep. target is a static property of the constructor Dep, meaning there is only one target globally
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = [] // The subs array is used to store observer objects
}
// Add an observer
addSub (sub: Watcher) {
this.subs.push(sub)
}
// Remove the observer
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// If dep. target exists, the observer instance's addDep method is called to add the Dep to Watcher's DEps array
depend () {
if (Dep.target) {
Dep.target.addDep(this)}}// Issue a notification
notify () {
const subs = this.subs.slice()
// If the Watcher instance runs asynchronously through the observer queue, the Watcher instance's run is sorted before the Watcher instance runs asynchronously through the observer queue. If the Watcher instance runs asynchronously through the update queue, the Watcher instance's run is sorted before the Watcher instance runs
if(process.env.NODE_ENV ! = ='production' && !config.async) {
// Since the id of the Watcher instance is an increasing number, it can be sorted directly by the array sort
subs.sort((a, b) = > a.id - b.id)
}
// Call the observer update method one by one
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Copy the code
vue/src/shared/util.js
export function remove (arr: Array<any>, item: any) :Array<any> | void {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)}}}Copy the code
The Watcher constructor
vue/src/core/observer/watcher.js
A data object has an __ob__ attribute corresponding to an Observer instance. The Observer instance overrides each attribute on data and holds its own DEP array via a closure. Each DEP array, All Watcher observer instances of this property are collected, and each observer instance has its own DEPS dependency set, collecting the DEP of the closure in reverse.
So with that in mind, let’s take a look at Watcher a little bit
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object, isRenderWatcher? : boolean) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
// _watcher holds the observer instance
vm._watchers.push(this)
// options
if (options) {
this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.syncthis.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // Note that the id of the Watcher instance is incremental
this.active = true
this.dirty = this.lazy
this.deps = [] // The added dependency array
this.newDeps = [] // A cache array to hold dependencies to be added
this.depIds = new Set(a)// Add an array of dependent ids
this.newDepIds = new Set(a)// A cache array to hold the dependency ids to be added
this.expression = process.env.NODE_ENV ! = ='production'
? expOrFn.toString()
: ' '
// expOrFn may be a function or a string representation of attributes on an object, such as "a.b", which is parsed by parsePath and returned as a function
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
)
}
}
// Lazy is false to perform get initialization
this.value = this.lazy
? undefined
: this.get()
}
// Get the value of the listener attribute to collect DEP dependencies
get () {
// Change dep. target to point to the current Watcher
pushTarget(this)
let value
const vm = this.vm
try {
// Execute the getter to get the listening data property and fire the deP-dependent depend() method corresponding to the property to call addDep
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
throw e
}
} finally {
// If depth listening is set
if (this.deep) {
traverse(value) // Call traverse to recursively traverse arrays and object types, firing each getter
}
// Change dep. target to null
popTarget()
this.cleanupDeps()
}
return value
}
// Add dependencies, called in dep dependent depend(),
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
// Add the dependency to the cache
this.newDepIds.add(id)
this.newDeps.push(dep)
// Call the DEP-dependent addSub to collect the current observer
if (!this.depIds.has(id)) {
dep.addSub(this)}}}// Clear cache dependencies
cleanupDeps () {
// Iterate over the dependent array
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
// DePs that are not in the cache need to call deP-dependent removeSub to remove the current observer
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)}}// Set newDepIds and newDeps to depIds and deps and clear the cache
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/ / update
update () {
// Set lazy to true and dirty to true
if (this.lazy) {
this.dirty = true
}
// To synchronize, run is executed
else if (this.sync) {
this.run()
}
QueueWatcher is pushed asynchronously to the observer queue, which is eventually called to the run method via nextTick
else {
queueWatcher(this)}}// Update the value and perform the callback
run () {
// active Defaults to true
if (this.active) {
const value = this.get()
// If the value is unequal, or if the value is an array or object, or if it is deep listening
if( value ! = =this.value ||
isObject(value) ||
this.deep
) {
// Assign the latest value to this.value
const oldValue = this.value
this.value = value
// Execute Watcher's callback
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
// Trigger GET to set dirty to false
evaluate () {
this.value = this.get()
this.dirty = false
}
// Iterate through deps to execute each deP dependent depend method
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
// Delete the current Watcher instance
teardown () {
if (this.active) {
Removes itself from the array of observer instances of the current Vue instance when _isBeingDestroyed is false
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)}// Execute removeSub for each DEP dependency to remove the current watch
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)}// Set active to false
this.active = false}}}Copy the code
vue/src/core/util/lang.js
const bailRE = new RegExp(` [^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string) :any {
if (bailRE.test(path)) {
return
}
const segments = path.split('. ')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if(! obj)return
obj = obj[segments[i]]
}
return obj
}
}
Copy the code
Vue/SRC/core/observer/traverse by js:
const seenObjects = new Set(a)export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if((! isA && ! isObject(val)) ||Object.isFrozen(val) || val instanceof VNode) {
return
}
// Avoid repeated traversal
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
// Deeply iterate over groups and objects
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
Copy the code
QueueWatcher is a queueWatcher constructor. QueueWatcher is a queueWatcher constructor. QueueWatcher is a queueWatcher constructor.