Vue2.0 source code analysis
If you think it is good, please send me a Star at GitHub
Vue2.0 source code analysis: responsive principle (on) Next: Vue2.0 source code analysis: componentization (on)
Deep into the responsivity principle
After introducing props, Data, Watch, and computed, we have a preliminary understanding of the responsive principle. In this chapter, we review the responsive principle again to explore its implementation principle.
In previous chapters, we have introduced: Vue. Js defines responsive objects using the object.defineProperty (obj, key, descriptor) method, which Can be found on Can I Use. IE8 does not support this method. This is the real reason vue.js does not support IE8 and below.
On the MDN website, we can see that this method supports a number of parameters, of which descriptor supports a number of optional properties. The most important for vue.js implementations of reactive objects are the get and set properties.
let val = 'msg'
const reactiveObj = {}
Object.defineProperty(reactiveObj, msg, {
get: function () {
// called when reactiveobj.msg is accessed
return val
},
set: function (newVal) {
// this is called when reactiveobj.msg is set
val = newVal
}
})
Copy the code
In Vue’s reactive object, it collects dependencies in getters and dispatches updates in setters. We’ll cover the collection of dependencies in getters and dispatches updates in setters in separate sections.
After introducing Object.defineProperty, let’s answer the question, what is a reactive Object? DefineProperty () is defined with both get and set options. We can call it a reactive Object.
When ue. Js is instantiated, props, data, and computed are turned into responsive objects. When we introduce responsive objects, we will focus on the processing of props and data, which occurs in initState(VM) in the this._init() method.
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
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
Let’s start by looking at how initProps handles the props logic:
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
constisRoot = ! vm.$parent// root instance props should be converted
if(! isRoot) { toggleObserving(false)}for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if(process.env.NODE_ENV ! = ='production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () = > {
if(! isRoot && ! isUpdatingChildComponent) { warn(`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if(! (keyin vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)}Copy the code
In analyzing the overall flow of initProps, we know that initProps mainly does three things: props verification and evaluation, props responsiveness, and props proxy. This is very simple for the props agent and its main function is to allow us to evaluate.
The proxy agent
The proxy () method is defined in SRC/core/instance/state. Js file:
const sharedPropertyDefinition = {
enumerable: true.configurable: true.get: noop,
set: noop
}
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
Code analysis:
noop
: it stands for empty function, and empty function stands for doing nothing.target
: it is the target proxy object inVue.js
Is in theVue
Instance.sourceKey
: it is the source property inprops
What is passed in the proxy is_props
Internal private properties.key
: it is the property to be proxied inprops
Is the variety that we writeprops
Properties.sharedPropertyDefinition
: it isObject.defineProperty(obj, key, descriptor)
methodsdescriptor
Parameter, as you can see from the code above, inprops
In the proxy it providesenumerable
,configurable
,get
andset
These are the choices.
Suppose we have the following Vue instance:
export default {
props: ['msg'.'age']}Copy the code
MSG and this._props. Age instead of this._props. MSG and this._props.
/ / agent before
const msg = this._props.msg
console.log(msg)
// The value of props cannot be modified as long as the demo is performed
this._props.msg = Const MSG = this.msg console.log(MSG) // const MSG = this.msg console.log(MSG) //new msg'
Copy the code
The above is the analysis of the props agent process. It is the same for the data agent.
defineReactive
The defineReactive method is defined in the index.js entry file of its SRC /core/observer directory
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function, shallow? : boolean) {
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]
}
letchildOb = ! shallow && observe(val)Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
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
Code analysis:
defineReactive
In fact, yesObject.defineProperty()
Method of wrapping a layer, mainly is treatedgetter
andsetter
Relevant logic.defineReactive
First of all byObject.getOwnPropertyDescriptor()
Method gets the currentobj.key
Attribute description, if its attributeconfigurable
forfalse
, cannot be defined as a responsive object, so forobj.key
Component updates are not triggered by any assignment, such as:
export default {
data () {
return {
obj: {}
}
},
created () {
const obj = {}
Object.defineProperty(obj, 'msg', {
configurable: false.value: 'msg'
})
this.obj = obj
setTimeout(() = > {
// this.obj.msg is not a responsive object and changes to it will not trigger component updates
this.obj.msg = 'new msg'
}, 3000)}}Copy the code
Observe and Observer
We can see the observe(val) code in defineReactive, so let’s introduce the observe() method and the Observer class. The observe() method definition is defined in the same file as the defineReactive() method, with the following code:
export function observe (value: any, asRootData: ? boolean) :Observer | void {
if(! isObject(value) || valueinstanceof VNode) {
return
}
let ob: Observer | void
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 ) { ob =new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
Copy the code
Code analysis:
- First of all on the transfer
value
Type check, not object orVNode
Instance when no operation is performedVNode
Is a class that will be used when generating the virtual DOM, which we’ll cover later,isObject
Is a definition insrc/shared/utils.js
Tool methods in the file.
export function isObject (obj: mixed) :boolean {
returnobj ! = =null && typeof obj === 'object'
}
Copy the code
- Then the
value
usehasOwn
Determine if there is__ob__
Properties and__ob__
forObserver
Example, this property is added to prevent repeated observations (to avoid redefining reactive types), i.e., return if the object is already reactive, otherwise proceed to the next step.hasOwn
Is a definition insrc/shared/utils.js
Tool methods in the file:
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<*>, key: string) :boolean {
return hasOwnProperty.call(obj, key)
}
Copy the code
- The last
value
Some conditions are judged, among which the two most important conditions areArray.isArray
andisPlainObject
, they judge separatelyvalue
Whether it’s an array, whether it’s an ordinary object, the other boundary conditions are not going to be discussed. Among themisPlainObject
Is a definition insrc/shared/utils.js
Tool methods in the file:
export function isPlainObject (obj: any) :boolean {
return _toString.call(obj) === '[object Object]'
}
Copy the code
Next, we need to look at the implementation of the Observer class:
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
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__'.this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
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
Code analysis:
def
As defined in thesrc/core/utils/lang.js
A tool method in the file,def
And that’s essentially trueObject.defineProperty()
Method for a layer of wrapping, usingdef
define__ob__
The purpose of the__ob__
Cannot be enumerated during object property traversal.
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
- in
Vue.js
The responsive handling of pure objects and arrays is different in the code firstvalue
Whether it is an array. If it is not an array, thenwalk()
Methods.walk()
A method is essentially a recursive traversal of object properties and then a calldefineReactive()
For example:
const nestedObj = {
a: {
b: {
c: 'c'}}}// recursive call
defineReactive(nestedObj)
defineReactive(a)
defineReactive(b)
defineReactive(c)
Copy the code
If it is an array, the observeArray() method is called. ObserveArray is also a recursive call, except that the array is iterated instead of the object’s property keys. Then we also see that a hasProto judgment is made before the observeArray() method is called, and then different actions are taken based on that judgment. HasProto is a constant defined in SRC /core/util/env.js to determine whether the current browser supports the __proto__ attribute:
export const hasProto = '__proto__' in {}
Copy the code
As we all know, due to some limitations of the native API, vue.js provides variant method support for seven methods that can change their own arrays:
const methodsToPatch = [
'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]
Copy the code
The variation of the seven method processing logic in the SRC/core/ovserver/array. The js file:
import { def } from '.. /util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
]
/** * Intercept mutating methods and emit events */
methodsToPatch.forEach(function (method) {
// cache 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.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
Copy the code
Code analysis:
- First of all to
Array.prototype
The prototype creates a new variable that will be displayed in theprotoAugment
orcopyAugment
Method. - Then go through the seven methods and use
def
To redefine a wrap method. That is, when we call any of these seven methods, we call our wrapper method first and then our native array method in the wrapper method, so that we can do our own thing in the wrapper method, for examplenotify
, this process can be described using the following pseudocode example:
// array.prototype. push method as an example
function mutatorFunc (value) {
const result = Array.prototype.push(value)
// do something
return result
}
export default {
data () {
return {
arr: []
}
},
created () {
this.arr.push('123')
/ / equivalent to
mutatorFunc(123)}}Copy the code
Then we next look at the implementation of protoAugment and copyAugment, first the simplest protoAugment:
/ / define
const arr = []
export const arrayMethods = Object.create(arrayProto)
function protoAugment (target, src: Object) {
target.__proto__ = src
}
/ / call
protoAugment(arr, arrayMethods)
/ / after the call
arr.__proto__ = {
// omit others
push: function () {},
pop: function () {},
shift: function () {},
unshift: function () {},
splice: function () {},
sort: function () {},
reverse: function () {}
}
arr.push()
arr.pop()
arr.shift()
arr.unshift()
arr.splice()
arr.sort()
arr.reverse()
Copy the code
Code analysis: When the browser supports the __proto__ attribute, direct __proto__ to the arrayMethods variable we created, which contains the seven variation methods we defined above.
When the __proto__ attribute is not supported by the browser, we call the copyAugment method:
/ / define
const arr = []
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export const arrayMethods = Object.create(arrayProto)
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])
}
}
/ / call
copyAugment(value, arrayMethods, arrayKeys)
/ / after the call
arr = {
// omit others
push: function () {},
pop: function () {},
shift: function () {},
unshift: function () {},
splice: function () {},
sort: function () {},
reverse: function () {}
}
arr.push()
arr.pop()
arr.shift()
arr.unshift()
arr.splice()
arr.sort()
arr.reverse()
Copy the code
Code analysis: As we can see from the code, when the browser does not support __proto__, we will iterate over and assign all keys from the arrayMethods variable we created to the value array.
Depend on the collection
In this section, we introduce dependency collection, but before that we need to know what it is and what it is for.
Q: What is dependency collection? What is the purpose of dependency collection? A: Dependency collection is the Watcher collection of changes to subscription data. The goal is to know which subscribers should be notified to do the logical processing when reactive data changes and their setters are triggered. For example, when a reactive variable is used in the Template template, the Render Watcher dependency should be collected for the reactive variable at the first rendering of the component, and the Render Watcher should be notified to rerender the component when its data changes and the setter is triggered.
As we mentioned earlier, dependency collection takes place in the getter of Object.defineProperty(), so let’s review the defineReactive() code:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function, shallow? : boolean) {
// omit the code
const dep = new Dep()
// 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]
}
letchildOb = ! shallow && observe(val)Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
}
})
}
Copy the code
As you can see from the code, when the getter is fired, it first checks whether dep.target exists, and if it does, it calls dep.depend(). Dep.depend () is where the dependencies are actually collected. After reading the code above, we might have a few questions:
Dep
What is?Dep.target
What is?dep.depend
How is dependency collection done? How is dependency removal done?
Dep
The Dep class is a class defined in the Dep. Js file in the Observer directory. The observer directory structure is as follows:
|-- observer
| |-- array.js
| |-- dep.js
| |-- index.js
| |-- scheduler.js
| |-- traverse.js
| |-- watcher.js
Copy the code
Then, let’s look at the definition of the Dep class:
let uid = 0
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 () {
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)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Copy the code
Code analysis:
Dep
Class first defines a static propertytarget
, it isDep.target
We’ll talk about it later. Then we define two instance properties,id
isDep
The primary key of thesubs
Is a store of variousWatcher
The array. For example,render watcher
,user watcher
andcomputed watcher
And so on.addSub
andremoveSub
That corresponds to PIsubs
Array to add and remove variousWatcher
.depend
Is the dependency collection process.notify
Triggered when data changessetter
There is a code like this:dep.notify()
Its purpose is to notify when the responsive data changessubs
All the different things insidewatcher
And then execute itupdate()
Methods. This is part of the process of distributing updates, which we will cover in a later section.
With these properties and methods covered, we have a concrete idea of what A Dep is and what it does.
Dep. Target and Watcher
Let’s move on to the second question, what is dep.Target? Dep.target is an example of various Watcher types, as illustrated by the following code:
<tempalte>
<div>{{msg}}</div>
</template>
<script>
export default {
data () {
return {
msg: 'Hello, Vue.js'
}
}
}
</script>
Copy the code
When the component first renders, it takes the MSG value and executes pushTarget(this), which represents the current Watcher instance. PushTarget () is a method defined in the dep.js file, along with a method called popTarget. Their code looks like this:
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
In pushTarget, we pass the target argument as the Watcher instance, and when pushTarget executes, it dynamically sets the Dep static property dep.target. After examining the code for pushTarget, we can see why Dep.target is an instance of Watcher.
Then, we have a new problem: How is the Watcher class defined? This is a class defined in the watcher.js file with the following key code:
let uid = 0
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
}
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 // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set(a)this.newDepIds = new Set(a)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()
}
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
}
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)
}
}
}
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)}}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
}
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
Copy the code
When we look at the Watcher class from a dependency collection perspective, we need to pay attention to the following four properties in its constructor:
this.deps = [] // Old DEP list
this.newDeps = [] // New DEP list
this.depIds = new Set(a)// Old DEP ID collection
this.newDepIds = new Set(a)// New deP ID collection
Copy the code
The use of these four attributes will be described in more detail in the addDep and cleanupDeps sections. In this section, we will focus on the constructor of Watcher and the implementation of the Get () method.
In the constructor of the Watcher class, when instantiated, the deps and newDeps arrays and the depIds and newDepIds collections are initialized to empty arrays and empty collections, respectively. At the end of the constructor, it is determined that, if computed Watcher (note: If only the lazy attribute is true for computed watcher), the this.get() function is called immediately to evaluate.
Next, let’s look at the implementation of this.get() and a scenario where pushTarget and popTarget are used together.
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
}
Copy the code
The get() method starts with a call to pushTarget(this), which pushes the current Watcher instance into the target array. Then set dep. target to the current Watcher instance.
Dep.target = null
const targetStack = []
export function pushTarget (target: ? Watcher) {
targetStack.push(target)
Dep.target = target
}
Copy the code
This. Getter is then called to evaluate, using the following example of a calculated property:
export default {
data () {
return {
age: 23}},computed: {
newAge () {
return this.age + 1
}
}
}
value = this.getter.call(vm, vm)
/ / equivalent to
value = newAge()
Copy the code
For computed Watcher, its getter property is the computed property method we wrote, and the procedure for calling this.getter is the procedure for performing the computed property method we wrote.
At the end of the this.get() method, popTarget() is called, which removes the last of the current target stack array and sets dep.target to the next-to-last.
Dep.target = null
const targetStack = []
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]}Copy the code
After analyzing pushTarget and popTarget, one question might be why does pushTarget/popTarget exist, and what purpose does it serve?
This is done because components can be nested, and the purpose of using stack arrays to push/unload components is to maintain proper dependencies during component rendering, as shown in the following code:
// child component
export default {
name: 'ChildComponent'.template: '<div>{{childMsg}}</div>',
data () {
return {
childMsg: 'child msg'}}}export default {
name: 'ParentComponent'.template: `
{{parentMsg}}
`.components: {
ChildComponent
}
data () {
return {
parentMsg: 'parent msg'}}}Copy the code
As we all know, when a component is rendering, when the parent component has a child component, the child component will be rendered first, and the parent component will not be rendered until all the child components are rendered. Therefore, the execution sequence of the component rendering hook functions is as follows:
parent beforeMount()
child beforeMount()
child mounted()
parent mounted()
Copy the code
According to the above rendering steps, when parent beforeMount() starts, parent Render Watcher is instantiated and this.get() is called, the dep.target dependency is parent Render Watcher, The target stack array is:
// This is an example of Watcher
const targetStack = ['parent render watcher']
Copy the code
When child beforeMount is executed, child Render Watcher is instantiated and this.get() is called. Dep.target is dependent on Child Render Watcher and the target stack array is:
// This is an example of Watcher
const targetStack = ['parent render watcher'.'child render watcher']
Copy the code
When child Mounted () is mounted, this. Getter () is called, and popTarget() is called.
// This is an example of Watcher
const targetStack = ['parent render watcher']
Dep.target = 'parent render watcher'
Copy the code
Parent Mounted () = this.getter(); popTarget() = popTarget();
// This is an example of Watcher
const targetStack = []
Dep.target = undefined
Copy the code
From the above example analysis, we can understand why there is a dependent push/push step and the purpose of doing so. Next, let’s examine the logic of addDep and cleanupDeps during dependency collection.
AddDep and cleanupDeps
addDep
In the depend() method of the Dep class, we introduced the code implementation, which calls addDep(Dep) :
export default Dep {
// Omit other code
depend () {
if (Dep.target) {
Dep.target.addDep(this)}}}Copy the code
Dep.target.adddep (this) is equivalent to:
const watcher = new Watcher()
watcher.addDep(this)
Copy the code
Next, let’s look at the implementation logic of the addDep method in the Watcher class:
export default Watcher {
// Simplify the code
constructor () {
this.deps = [] // Old DEP list
this.newDeps = [] // New DEP list
this.depIds = new Set(a)// Old DEP ID collection
this.newDepIds = new Set(a)// New deP ID collection
}
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)}}}}Copy the code
Dep = deP = deP = deP = deP = deP = deP = deP = deP = deP = deP Adds the current Watcher instance to the SUBs array of the DEP instance.
Rigid analysis of the source code is not very convenient for us to understand addDep code logic, we use the following code example:
{{MSG}}</p> <script> export default {name: {{MSG}}</p> </template> 'App', data () { return { msg: 'msg' } } } </script>Copy the code
Process analysis:
- Instantiation occurs when the component is first rendered
render watcher
For the time of,Dep.target
forrender watcher
:
const updateComponent = () = > {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)
Copy the code
- First compile read
msg
When a reactive variable is triggeredgetter
fordep.depend()
Rely on the collection, and then calladdDep()
Method, becausedeps
,newDeps
,depIds
andnewDepIds
Initialize to an empty array or an empty set, so theta at this pointdep
Is added to thenewDepIds
,newDeps
And will be executeddep.addSub(this)
, can be represented by the following code:
// instantiate Dep
const dep = {
id: 1.subs: []}// Add to newDepIds, newDeps
this.newDepIds.push(1)
this.newDeps.push(dep)
/ / call addSub
dep.addSub(this)
console.log(dep) // { id: 1, subs: [new Watcher()] }
Copy the code
- When the second compilation reads
msg
When a reactive variable is triggeredgetter
fordep.depend
Rely on collection becausedep
isdefineReactive
Function in the closure variable, so twice triggeredgetter
Is the samedep
Instance. When callingaddDep
Judge at this timenewDepIds
In the collectiondep.id
for1
It already exists, so skip it.
You may notice that when parsing the code in the getter, we deliberately omit the following code:
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
Copy the code
You may be wondering: What is this code for? What does it do? So now, for example:
<template>
<p>{{obj.msg}}</p>
</template>
<script>
export default {
name: 'App',
data () {
return {
obj: {
msg: 'msg'
}
}
}
}
</script>
Copy the code
Process analysis:
- When it’s called the first time
defineReactive
When, at this timedefineReactive
The first parameterobj
andkey
Are:
obj = {
obj: {
msg: 'msg'
}
}
key = 'obj'
Copy the code
At the beginning of defineReactive, a closure DEP instance is instantiated and we assume that the instantiated DEP looks like this:
const dep = new Dep()
console.log(dep) // { id: 1, subs: [] }
Copy the code
Observe (val); observe(val); Observe (val); Observe (val); Observe (val); observe(val); observe(val);
this.dep = new Dep()
Copy the code
It instantiates another DEp and assigns the instantiated DEp to this.dep. Let’s assume that the instantiated DEP looks like this:
const dep = new Dep()
console.log(dep) // { id: 2, subs: [] }
Copy the code
Because obj = {MSG: ‘MSG’} is an object, this.walk() iterates through the obj object’s properties and then calls defineReactive again to instantiate a closure DEP instance. We assume that the instantiated DEP looks like this:
const dep = new Dep()
console.log(dep) // { id: 3, subs: [] }
Copy the code
We now have three instances of DEP, two of which are the closure instance DEP in the defineReactive function and one of which is the attribute DEP of childOb(Observer instance).
- When the component starts rendering, the reactive principle adds us in
template
Reads theobj.msg
Variable, so it fires firstobj
The object’sgetter
At this time,dep
forid=1
The closure variable ofdep
. At this timeDep.target
forrender watcher
And then proceeddep.depend()
Rely on the collection when going toaddDep
Method, because the four properties we care about are all empty arrays or empty collections, we will change the value ofdep
I’m going to add to thatdep
It is expressed as follows:
const dep = {
id: 1.subs: [new Watcher()]
}
Copy the code
- in
dep.depend()
Dependencies are determined after collection is completechildOb
Because thechildOb
forObserver
Is called, so the condition is judged to be truechildOb.dep.depend()
. When performing theaddDep()
At this timedep
forid=2
theObserver
Instance attributesdep
And do notnewDepIds
anddepIds
, so it will be added to, at this pointdep
It is expressed as follows:
const dep = {
id: 2.subs: [new Watcher()]
}
Copy the code
- When the response variable
obj
thegetter
When the trigger is complete, it will triggerobj.msg
thegetter
For the time of,dep
forid=3
The closure variable ofdep
. At this timeDep.target
Is stillrender watcher
And then proceeddep.depend()
Depend on the collection, this process and
objthe
getterThe process for doing dependency collection is basically the same when
addDep()Method is executed at this time
Dep ‘is denoted as follows:
const dep = {
id: 3.subs: [new Watcher()]
}
Copy the code
The only difference is that childOb is undefined, and childob.dep.depend () will not be called for dependency collection of child attributes.
After analyzing the above code, it’s easy to answer the question: q: what does childob.dep.depend () do? What does it do? A: Childob.dep.depend () does dependency collection for child attributes so that when an object or one of its attributes changes, its dependencies can be notified to act accordingly.
<template> <p>{{obj. MSG}}</p> <button @click="change"> </button> <button @click="add"> </button> </template> <script> import Vue from 'vue' export default { name: 'App', data () { return { obj: { msg: 'msg' } } }, methods: { change () { this.obj.msg = 'new msg' }, add () { this.$set(this.obj, 'age', 23) } }, watch: { obj: { handler () { console.log(this.obj) }, deep: true } } } </script>Copy the code
Take the example above:
- When there is
childOb.dep.depend()
Collect subproperty dependencies when we modify them either waymsg
The value of is addedage
New properties, all fireuser watcher
, that is, printingthis.obj
The value of the. - When there is no
childOb.dep.depend()
When collecting subproperty dependencies, we modify themmsg
The value of, although will be notifiedrender watcher
Component rerendering is performed without notificationuser watcher
printthis.obj
The value of the.
cleanupDeps
In this section, our goal is to figure out why and how dependency cleanup is done.
Let’s look at the implementation of cleanupDeps in the Watcher class:
export default Watcher {
// Simplify the code
constructor () {
this.deps = [] // Old DEP list
this.newDeps = [] // New DEP list
this.depIds = new Set(a)// Old DEP ID collection
this.newDepIds = new Set(a)// New deP ID collection
}
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)}}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}}Copy the code
As an example, suppose we have the following components:
<template>
<p v-if="count < 1">{{msg}}</p>
<p v-else>{{age}}</p>
<button @click="change">Add</button>
</template>
<script>
import Vue from 'vue'
export default {
name: 'App',
data () {
return {
count: 0,
msg: 'msg',
age: 23
}
},
methods: {
change () {
this.count++
}
}
}
</script>
Copy the code
Process analysis:
- When the component is rendered for the first time,
render watcher
The instancenewDeps
There are two arraysdep
Example, one of which is incount
Responsive variablegetter
The other one was collected when triggeredmsg
Responsive variablegetter
Collected when triggered (age
becausev-if/v-else
The cause of the command is not triggered when the component is first renderedage
thegetter
), we use the following code to represent:
this.deps = []
this.newDeps = [
{ id: 1.subs: [new Watcher()] },
{ id: 2.subs: [new Watcher()] }
]
Copy the code
- When we click the button to proceed
this.count++
Is triggered to re-update the component becausecount < 1
The condition is false, so it also fires during component re-renderingage
Of reactive variablesgetter
Do dependency collection. When performing theaddDep
After this timenewDeps
Have changed:
this.deps = [
{ id: 1.subs: [new Watcher()] },
{ id: 2.subs: [new Watcher()] }
]
this.newDeps = [
{ id: 1.subs: [new Watcher()] },
{ id: 3.subs: [new Watcher()] }
]
this.depIds = new Set([1.2])
this.newDepIds = new Set([1.3])
Copy the code
On the last call to this.get(), the this.cleanupdeps () method is called, which first iterates through the old dependency list DEps, and if one of the dePs is not found in the new dependency ID collection newDepIds, Call dep.removesub (this) to remove the dependency. In the component rendering process, this stands for Render Watcher. When we call this method and change the MSG variable value, the component will not be rerendered. After iterating through the DEPS array, the values of DEps and newDeps, depIds and newDepIds are swapped, and newDeps and newDepIds are emptied.
After analyzing the examples above, we can see why dependency cleanup is necessary: to avoid repeating rendering of components with unrelated dependencies.
Distributed update
Following the introduction of dependency collection, we will examine the distribution of updates. In this section, our goal is to figure out what updates do and how they are implemented.
Let’s answer the first question: Q: What do you do when you distribute updates? A: Distributive update means notifying all Watcher(Dep dependent) subscribers to update when responsive data changes. In the case of The Render Watcher, update triggers the component to re-render; For computed Watcher, update means reevaluating a computed property; In the case of user Watcher custom watcher, update means invoking the user-provided callback function.
scenario
Most people analyze the update scenario and only indicate that the setter in object.defineProperty () method will issue the update when it is triggered. In fact, there are four places to issue the update. The other three are:
Vue.js
When the seven array variation methods are called, an update is distributed.
const methodsToPatch = ['push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse']
methodsToPatch.forEach(function (method) {
def(arrayMethods, method, function mutator (. args) {
// Simplify the code
ob.dep.notify()
return result
})
})
Copy the code
Vue.set
orthis.$set
, will be distributed updates.
export function set (target: Array<any> | Object, key: any, val: any) :any {
// Simplify the code
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
Copy the code
Vue.delete
orthis.$delete
, will be distributed updates.
export function del (target: Array<any> | Object, key: any) {
// Simplify the code
delete target[key]
if(! ob) {return
}
ob.dep.notify()
}
Copy the code
The above three dispatched updates are slightly different from those dispatched when setters are triggered in the Object.defineProperty() method, where the deP is a closure variable defined in the defineReactive method, Means it can only serve the defineReactive method. The dep of the former is taken from the this.__ob__ object. The this.__ob__ attribute is defined when the Observer is instantiated and refers to the Observer instance, as we have described earlier. This unique processing mode facilitates us to read deP dependencies in the above three scenarios, and then distribute updates of dependencies.
process
In the code above, we learned about the various times when dep.notify() is called. In this section we need to look at the process of sending out updates.
When dep.notify() is called, it executes the code in notify(). Let’s look at the implementation of this method in the DEP class:
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
Copy the code
As you can see, the main thing notify does is iterate through the subs array and call the Update method. Next, let’s look at the code implementation of the Update method in the Watcher class:
import { queueWatcher } from './scheduler'
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}Copy the code
When the update method is executed, two attributes, this.lazy and this.sync, are evaluated first, where this.lazy is a sign of computed properties in a computed watcher. This.sync is not the focus of this section on distributing updates, so I won’t cover it too much.
Let’s focus on queueWatcher, which is a method written in an observer/scheduler.js file:
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
let index = 0
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if(! flushing) { 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)
}
}
}
Copy the code
There are several variables defined at the top of the code. Some of the most important variables are as follows:
queue
: all kinds ofWatcher
Execute the queue, eitherrender watcher
,user watcher
orcomputed watcher
“As long as it’s not repetitiveWatcher
Will eventually be pushed intoqueue
Queue array.has
: Prevents repeated additionsWatcher
Flag object of:
// indicates that the Watcher instance with id 1,2 has been added to queue
// The same Watcher instance will not be added to the queue repeatedly
const has = {
1: true.2: true
}
Copy the code
index
: Current traversalWatcher
The instance index, which isflushSchedulerQueue
Method.for
To iterate overqueue
Queue arrayindex
.
With the above important variables covered, let’s examine queueWatcher’s process:
- The code starts by getting the current
Watcher
Since it increasesid
, judge in the mark objecthas
Exists in, if not, for thisid
Mark it and assign it totrue
. - And then determine whether it is
flushing
State, if not, means we can normally put the currentWatcher
Push toqueue
Queue array. - And then I decided whether it was
waiting
Status, if not, it indicates that the command can be executedqueue
Queue array, and then setwaiting
fortrue
, and finally callnextTick(flushSchedulerQueue)
.nextTick
Method isVue.js
A utility function that handles asynchronous logic, as long as we know:nextTick
The function argument in thetick
The execution.
FlushSchedulerQueue = flushSchedulerQueue = flushSchedulerQueue
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) = > a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if(process.env.NODE_ENV ! = ='production'&& has[id] ! =null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break}}}// 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
A cursory look at flushSchedulerQueue shows that it does several things: restoring flushing, sorting queues, traversing queues, restoring states, and firing component hook functions. Let’s follow these steps and explain them respectively:
- Reduction flushing stateIn:
flushSchedulerQueue
First of all,flushing
The restore is done so that it does not affect the executionqueue
In the queue, there areWatcher
Push toqueue
In the queue. - Sort queue: uses arrays
sort
The method,queue
In the queueWatcher
According to the increasingid
Is sorted from smallest to largest in order to ensure the following three scenarios:
- As we all know, component updates start with the parent component and then go to the child component. When a component is rendered, it will start rendering from the parent component, which is when the parent component is created
render watcher
, suppose that at this pointparent render watcher
Since the increaseid
for1
, and then render the child component, instantiating the child component’srender watcher
, suppose that at this pointchild render watcher
Since the increaseid
for2
. forqueue.sort()
After sorting,id
The values are sorted to the front of the array, so that inqueue
So when you iterate, you’re guaranteed to do it firstparent render watcher
And then deal with itchild render watcher
- Because it’s user defined
Watcher
Can be created prior to component rendering, therefore for user customizationWatcher
In terms of, need takes precedence overrender watcher
The execution.
<template> <p>{{msg}}</p> <button @click="change">Add</button> </template> <script> export default { data () { return { count: 0, msg: 'msg', age: $this.$watch(' MSG ', () => {console.log(this.msg)})}, methods: { change () { this.msg = Math.random() } } } </script>Copy the code
- If a child component executes in the parent component
queueWatcher
The process is destroyed, then all of the sub-componentsWatcher
Execution should be skipped.
- Through the queue: in the use of
for
When we iterate, we need to pay attention to the iterate condition, which is true firstqueue
The length is evaluated, and then the loop condition is judged, because in traversalqueue
In the array,queue
Elements in the array may change. During traversal, the current is first releasedWatcher
inhas
Flags the state in the object, and then callswatcher.run()
Methods.run
Is defined in theWatcher
A method in the class:
export default class Watcher {
// Simplify the code
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) {
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)
}
}
}
}
}
Copy the code
Run method code is not very complex, it is to different treatment of different Watcher, if is to render the Watcher, in the execution of the enclosing the get () will perform this. In the process of the getter, enclosing the getter method corresponding to the following:
updateComponent = () = > {
// Component rendering methods
vm._update(vm._render(), hydrating)
}
Copy the code
If user watcher is true, this.cb.call() will be called, in which case this.cb will be the user callback written by the user:
export default {
data () {
return {
msg: 'msg'
}
},
created () {
// user callback
// this.cb = userCallback
const userCallback = () = > {
console.log(this.msg)
}
this.$watch(this.msg, userCallback)
}
}
Copy the code
If it is computed watcher and its this.user value is false, this.cb.call() is called, and this.cb is the method we provide for calculating attributes:
export default {
data () {
return {
msg: 'msg'}},computed: {
// this.cb = newMsg () {}
newMsg () {
return this.msg + '!!!!!! '}}}Copy the code
- Reducing state: call
resetSchedulerState
The purpose of the function is whenqueue
When all queues are executed, restore all relevant states to their initial state, which includesqueue
,has
andindex
Such as:
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if(process.env.NODE_ENV ! = ='production') {
circular = {}
}
waiting = flushing = false
}
Copy the code
- Triggers the component hook function: call
callActivatedHooks
andcallUpdatedHooks
The difference is to trigger the componentactivated
andupdated
The hook function, whereactivated
Is with thekeep-alive
Associated hook functions.
Infinite loop
When developing with vue.js, we sometimes accidentally write code with an infinite loop, for example:
<template>
<p>{{msg}}</p>
<button @click="change">Add</button>
</template>
<script>
export default {
data () {
return {
msg: 'msg'
}
},
methods: {
change () {
this.msg = Math.random()
}
},
watch: {
msg () {
this.msg = Math.random()
}
}
}
</script>
Copy the code
When we click the button and call the change method to modify the value of this. MSG, because we used watch to monitor the update of MSG value, the watch listener function will be executed. However, we changed the value of this. MSG in the watch listener function, which will cause the listener function we wrote to be called all the time. There is an endless loop. In vue.js, it does something special to avoid an infinite loop that crashes the browser.
In queueWatcher, we didn’t parse the following else code:
export const MAX_UPDATE_COUNT = 100
let circular: { [key: number]: number } = {}
if(! flushing) { 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)
}
Copy the code
Let’s examine the following code from the above example:
- When we click the button to modify
this.msg
When the value is triggeredmsg
thesetter
And then proceeddep.notify
Send out the update, then callqueueWatcher
At this time,msg
There are twoDep
Dependency, one isrender watcher
And the other one isuser watcher
, sothis.subs
Phi is a length of phi2
theWatcher
The array. At the beginning timequeueWatcher
The time,flushing
The status offalse
Because theuser watcher
thanrender watcher
Create it first, so at this pointuser watcher
I’m going to push it in firstqueue
The queue, and then the queuerender watcher
:
// Display the use of the actual Watcher instance
const queue = ['user watcher'.'render watcher']
Copy the code
- And then it will execute
watch
Listen on the function, execute againqueueWatcher
At this timeflushing
forfalse
Go,else
Branching logic,while
The main function of the loop is to find out what should bequeue
Where is the new array insertedwatcher
, such as:
const queue = [
{ id: 1.type: 'user watcher' },
{ id: 2.type: 'render watcher'},]// When the watch listener is executed, the watcher should be inserted into the second item in the array
const queue = [
{ id: 1.type: 'user watcher' },
{ id: 1.type: 'user watcher' },
{ id: 2.type: 'render watcher'},]Copy the code
Because of the special example we wrote, the queue array pushes the user watcher continuously, and vue.js terminates this behavior prematurely when the number in the queue exceeds the limit (some watcher is traversed more than 100 times). Vue.js uses circular token objects to count, It marks the number of times each Watcher is traversed, for example:
// Watcher with id 1 is traversed 101 times
// Watcher with id 2 is traversed once
const circular = {
1: 101.2: 1
}
Copy the code
Circular counts updates and terminations in the flushSchedulerQueue function:
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if(process.env.NODE_ENV ! = ='production'&& has[id] ! =null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break}}}Copy the code
So for the example above, vue.js prints an error message on the console:
// You may have an infinite update loop in watcher with expression "msg"
Copy the code
Overall flow chart
After analyzing the above process of distributing updates, we can get the following flow chart.
Implementation principle of nextTick
When developing with vue.js, if we want to manipulate the correct DOM based on data state, we must have dealt with the nextTick() method, which is one of the core methods in vue.js. In this section, we introduce how nextTick is implemented in vue.js.
Asynchronous knowledge
Because nextTick involves a lot of asynchrony, we’ll introduce asynchrony to make it easier to learn.
Event Loop
We all know that JavaScript is single-threaded and is executed based on an Event Loop that follows certain rules: All synchronous tasks are executed in the main thread, forming an execution stack. All asynchronous tasks are temporarily put into a task queue. When all synchronous tasks are completed, the task queue is read and put into the execution stack to start execution. The above is a single execution mechanism. The main thread repeats this process over and over again to form an Event Loop.
The above is a general introduction to Event Loop, but there are still some details we need to master when executing Event Loop.
We mentioned tick in the update section, so what is tick? The tick is a single execution of the main thread. All asynchronous tasks are scheduled by task queue, which stores tasks. According to the specification, these tasks are divided into Macro task and micro task. There is a subtle relationship between macro tasks and Micro Tasks: After each Macro task is executed, all micro Tasks are cleared.
Macro Task and Micro Task correspond as follows in the browser environment:
macro task
Macro task:MessageChannel
,postMessage
,setImmediate
andsetTimeout
.micro task
Micro tasks:Promise.then
andMutationObsever
.
MutationObserver
In the MDN documentation, we can see the detailed use of MutationObserver, which is not very complicated. It is used to create and return a new Instance of MutationObserver, which is called when the specified DOM changes.
Let’s write an example according to the documentation:
const callback = () = > {
console.log('text node data change')}const observer = new MutationObserver(callback)
let count = 1
const textNode = document.createTextNode(count)
observer.observe(textNode, {
characterData: true
})
function func () {
count++
textNode.data = count
}
func() // text node data change
Copy the code
Code analysis:
- First of all, we define
callback
The callback function andMutationObserver
Object, where the constructor passes arguments that are ourscallback
. - It then creates a text node and passes in the initial text of the text node, followed by the call
MutationObserver
The instanceobserve
Method, passing in the text node we created and aconfig
Observe the configuration object, wherecharacterData:true
We have to observetextNode
The text of the node changes.config
There are other option properties that you can use in theMDN
You can view it in the document. - And then, let’s define one
func
Function, the main thing that this function does is modifytextNode
The text content in the text node, when the text content changes,callback
Is automatically called, so the outputtext node data change
.
Now that we know how to use MutationObserver, let’s take a look at how the nextTick method uses MutationObserver:
import { isIE, isNative } from './env'
// omit the code
else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () = > {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
}
Copy the code
As you can see, nextTick determines that a non-INTERNET Explorer browser is used only when MutationObserver is available and is a native MutationObserver. For determining the non-ie situation, you can look at issue#6466 of vue.js to see why.
SetImmediate and setTimeout
SetTimeout is a very common timer method for most people, so we won’t cover it too much.
In the nextTick method implementation, it uses setImmediate, an API method that is only available in advanced Internet Explorer and low Edge browsers, as noted on Can I Use.
Then why is this method used? It is because of the issue we mentioned before: MutationObserver is not reliable in Internet Explorer, so in Internet Explorer you level down to using setImmediate, which we can think of as similar to setTimeout.
setImmediate(() = > {
console.log('setImmediate')},0)
/ / is approximately equal to
setTimeout(() = > {
console.log('setTimeout')},0)
Copy the code
NextTick implementation
After introducing the knowledge related to nextTick and asynchrony, let’s analyze the implementation of nextTick method. The first thing to say is: asynchronous degradation.
let timerFunc
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise! = ='undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () = > {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () = > {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () = > {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () = > {
setTimeout(flushCallbacks, 0)}}Copy the code
We introduced the Event Loop in the previous section. Due to the special execution mechanism of Macro Task and Micro Task, we first determine whether the current browser supports promises. If not, we then demoted to determine whether MutationObserver is supported. It continues to demote to determining whether or not setImmediate is supported, and finally to using setTimeout.
After introducing asynchronous degradation, let’s look at the nextTick implementation code:
const callbacks = []
let pending = false
export function nextTick (cb? :Function, ctx? :Object) {
let _resolve
callbacks.push(() = > {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')}}else if (_resolve) {
_resolve(ctx)
}
})
if(! pending) { pending =true
timerFunc()
}
// $flow-disable-line
if(! cb &&typeof Promise! = ='undefined') {
return new Promise(resolve= > {
_resolve = resolve
})
}
}
Copy the code
The real code for nextTick is not complicated. It collects incoming CB’s and then executes the timerFunc method when pending is false, where timeFunc is defined during asynchronous demotion. The nextTick method also makes a final judgment that if no CB is passed in and a Promise is supported, it will return a Promise, so we can use nextTick in two ways:
const callback = () = > {
console.log('nextTick callback')}/ / way
this.$nextTick(callback)
2 / / way
this.$nextTick().then(() = > {
callback()
})
Copy the code
Finally, we’ll look at an implementation of the flushCallbacks method that wasn’t mentioned before:
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
Copy the code
The flushCallbacks method returns the pending state to false and executes the methods in the callbacks array.
Note for change detection
Although the Object.defineProperty() method works well, there are exceptions where changes to these exceptions do not trigger setters. In this case, we classify objects and arrays.
object
Suppose we have the following example:
export default {
data () {
return {
obj: {
a: 'a'
}
}
},
created () {
// 1. Add attribute b, attribute B is not reactive, does not trigger setter for obj
this.obj.b = 'b'
// 2.delete delete existing property, cannot trigger setter for obj
delete this.obj.a
}
}
Copy the code
From the above examples, we can see:
- When a new property is added to a responsive object, the new property is not reactive and cannot be triggered by any subsequent changes to the new property
setter
. To solve this problem,Vue.js
Provides a globalVue.set()
Methods and Examplesvm.$set()
Method, they’re all really the sameset
Method, which we will cover globally in relation to responsiveness in a later sectionAPI
The implementation of the. - This is not triggered when a reactive object deletes an existing property
setter
. To solve this problem,Vue.js
Provides a globalvue.delete()
Methods and Examplesvm.$delete()
Method, they’re all really the samedel
Method, which we will cover globally in relation to responsiveness in a later sectionAPI
The implementation of the.
An array of
Suppose we have the following example:
export default {
data () {
return {
arr: [1.2.3]
}
},
created () {
// 1. Cannot capture array changes through index changes.
this.arr[0] = 11
// 2. Cannot capture array changes by changing the array length.
this.arr.length = 0}}Copy the code
From the above examples, we can see:
- Modifying an array directly through an index does not capture changes to the array.
- Changes to the array cannot be caught by changing the array length.
For the first case, we can use the aforementioned vue.set or vm.$set, and for the second, we can use the array splice() method.
In the latest version of Vue3.0, Proxy is used to replace Object.defineProperty() to achieve responsiveness. All the above problems can be solved after Proxy is used. However, Proxy belongs to ES6, so it has certain requirements for browser compatibility.
Change detection API implementation
In the previous section, we looked at some of the problems with change detection. In this section, we’ll look at how vue.js implements the API to solve these problems.
Vue. Set to achieve
Vue. Set and vm.$set refer to a set method defined in observer/index.js:
export function set (target: Array<any> | Object, key: any, val: any) :any {
if(process.env.NODE_ENV ! = ='production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key intarget && ! (keyin Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if(target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV ! = ='production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if(! ob) { target[key] = valreturn val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
Copy the code
Before analyzing the code, let’s review the use of vue. set or vm.$set:
export default {
data () {
return {
obj: {
a: 'a'
},
arr: []
}
},
created () {
// Add a new attribute to the object
this.$set(this.obj, 'b'.'b')
console.log(this.obj.b) // b
// Add a new element to the array
this.$set(this.arr, 0.'AAA')
console.log(this.arr[0]) // AAA
// Modify array elements by index
this.$set(this.arr, 0.'BBB')
console.log(this.arr[0]) // BBB}}Copy the code
Code analysis:
set
Method first on the incomingtarget
Parameters are verified, whereisUndef
Determine whetherundefined
.isPrimitive
Determine whetherJavaScript
Raw value, an error message is displayed in the development environment if one of the conditions is met.
export default {
created () {
// Error message
this.$set(undefined.'a'.'a')
this.$set(1.'a'.'a')
this.$set('1'.'a'.'a')
this.$set(true.'a'.'a')}}Copy the code
-
IsArray () is used to check whether target is an Array. If it is a valid Array index, isValidArrayIndex is used to check whether target is a valid Array index. If it is a valid Array index, splice is used to set the value to the specified position in the Array. We also reset the length property of the array because the index we passed might be larger than the length of the existing array.
-
Then determine if it is an object and if the current key is already on that object. If it is, then we just need to copy it again.
-
Finally, add a property to the reactive object using the defineReactive method, which was introduced earlier and won’t be covered here. When defineReactive is executed, an update is sent immediately to inform the dependencies of reactive data to be updated immediately. The following two pieces of code are the core of the set method:
defineReactive(ob.value, key, val)
ob.dep.notify()
Copy the code
Vue. Delete
Delete and vm.$delete use the same delete method as defined in the observer/index.js file: Vue.
export function del (target: Array<any> | Object, key: any) {
if(process.env.NODE_ENV ! = ='production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)}if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
if(target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV ! = ='production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
if(! hasOwn(target, key)) {return
}
delete target[key]
if(! ob) {return
}
ob.dep.notify()
}
Copy the code
Before analyzing the code, let’s review the following use of vue. delete or vm.$delete:
export default {
data () {
return {
obj: {
a: 'a'
},
arr: [1.2.3]
}
},
created () {
// Delete object properties
this.$delete(this.obj, 'a')
console.log(this.obj.a) // undefined
// Delete an array element
this.$delete(this.arr, 1)
console.log(this.arr) / / [1, 3]}}Copy the code
Code analysis:
- The object to be deleted is determined first
target
Can’t forundefined
Or a raw value, if so, an error is displayed in the development environment.
export default {
created () {
// Error message
this.$delete(undefined.'a')
this.$delete(1.'a')
this.$delete('1'.'a')
this.$delete(true.'a')}}Copy the code
- Then through the
Array.isArray()
Method to determinetarget
Whether it is an array, and if so, pass againisValidArrayIndex
Check if it is a valid array index. If it is, variation is usedsplice
Method to remove the element at the specified location. - Then determine whether the current attribute to be deleted is in
target
Object, if it’s not there, it just returns, doing nothing. - Finally, through
delete
The operator deletes an attribute on the object, and thenob.dep.notify()
Notifies dependencies on responsive objects to be updated.
Vue. Observables
Vue.observable is a global method available in Vue2.6+ that makes an object responsive:
const obj = {
a: 1.b: 2
}
const observeObj = Vue.observable(obj)
console.log(observeObj.a) / / triggers the getter
observeObj.b = 22 / / triggers the setter
Copy the code
This global method is defined in the initGlobalAPI, which we’ve already covered, not covered here:
export default function initGlobalAPI (Vue) {
// ...
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T= > {
observe(obj)
return obj
}
// ...
}
Copy the code
Observable implementation is very simple, just calling the observe method inside the method and returning the obj. The code implementation of Observe has been covered in the previous section, so there is no further explanation here:
export function observe (value: any, asRootData: ? boolean) :Observer | void {
if(! isObject(value) || valueinstanceof VNode) {
return
}
let ob: Observer | void
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 ) { ob =new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
Copy the code
Vue2.0 source code analysis: responsive principle (on) Next: Vue2.0 source code analysis: componentization (on)