The article original intention
We know that Vue2.0 uses Object.defineProperty to hijack the get and set methods of objects. And MVVM uses observer mode, this time to see hand in hand exactly where the hijacking is taking place? Where exactly is the observer created?
For fear of children’s shoes that you can’t understand, you can have a look firstSimulate the MVVM article implementing VUE, this article will reveal its content in the corresponding Observe, Dep, Watcher, defineRactive source, for further explanation
Listen to Vue source code analysis for a long time. These two smallpox a few hours time to visit the vue source, the feeling is semantic written well! The details are indeed more, and indeed around.
This article is a bit long, but for those of you who have sat through it, I’m sure you’ve already started looking at the source code for any of the NPM libraries yourself
introduce
- Version: the 2.6.10
- Download source address: github.com/vuejs/vue/t…
- Type: the runtime
The order of this viewing
Before you read: how do I find the file entry of an NPM package when it is referenced? The entry point is the file that “mian” points to in package.json
Let’s start with package.json and take a look at the mechanics of MVVM implementation for the pure reader, pure novice
The specific order is as follows:
- package.json
- scripts\config.js
- src\platforms\web\entry-runtime.js
- src\platforms\web\runtime\index.js
- src\core\index.js
- src\core\instance\index.js
- src\core\instance\init.js
- SRC \core\instance\state.js – the most important file, most of the source code is here
- src\core\observer\dep.js
- src\core\observer\watcher.js
package.json
."main": "dist/vue.runtime.common.js"."scripts": {
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"."dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev"."dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm"."dev:test": "karma start test/unit/karma.dev.config.js"."dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:web-server-renderer",}Copy the code
The rollup file is vue.runtime.common.js. The rollup file is scripts/config.js
scripts/config.js
.const builds = {
// Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
'web-runtime-cjs-dev': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.dev.js'),
format: 'cjs'.env: 'development',
banner
},
'web-runtime-cjs-prod': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.prod.js'),
format: 'cjs'.env: 'production',
banner
},
...
}
Copy the code
If I skip the rest, This section you can see our vue.runtime.com mon. Js a dev version of mon * * vue.runtime.com. Dev. Js and a prod version * * * * vue.runtime.com mon. Prod. Js * *.
OK, so let’s look at the dev version and see that its entry is ‘web/ entry-Runtime.js’
src\platforms\web\entry-runtime.js
/* @flow */
import Vue from './runtime/index'
export default Vue
Copy the code
/runtime/index to see what Vue is./runtime/index
src\platforms\web\runtime\index.js
import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'. Vue.prototype.$mount =function ( //mount the entry to the render functionel? : string | Element, hydrating? : boolean) :Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating) // Hang in the component's entry function to create the watcher internally}...export default Vue
Copy the code
Another Vue – -0 from somewhere else. Keep looking
src\core\index.js
import Vue from './instance/index'.Copy the code
Found no? He’s fucking for nothing! Keep looking!
src\core\instance\index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '.. /util/index'
function Vue (options) {
if(process.env.NODE_ENV ! = ='production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')}this._init(options)} initMixin(Vue) # init stateMixin(Vue)/ / $data, $props, $watch with
eventsMixin(Vue) $on,$once,$off,$emit, etc
lifecycleMixin(Vue) // $update,$forceUpdate,$destroy
renderMixin(Vue) // $nextTick,_render Internal _render function for the mountComponent mentioned above
export default Vue
Copy the code
OK, I finally see a Vue who doesn’t pay. Vue declares here that he is a function. Then there is the inhuman mixing of Vue’s prototype chain.
- We see that the _init function is executed internally in Vue. This is equivalent to your code that runs immediately after new Vue({el:’#app’}).
- {el:’#app’} == options in init(options)
- Next, _init, which is the Vue that initMix bestow
src\core\instance\init.js
.export function initMixin (Vue: Class<Component>) {... initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm,'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')... }...Copy the code
Pick the key paragraph, here we see the familiar life cycle beforeCreate,created. So this is where they were triggered! There’s also a lot of initialization
Of course, keep your eye on our target defineProperty and go into the initState function
SRC \core\instance\state.js – the most important file, most of the source code is here
.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
Observe the observe object controls the default data null {} object when opts. Data is null. Non-null, implemented in initData in the same file
With the file
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
...
// observe data
observe(data, true /* asRootData */)}Copy the code
Observe data with Observe in initData.
With the file
export function observe (value: any, asRootData: ? boolean) :Observer | void {... ob =new Observer(value)
...
return ob
}
Copy the code
Observe the implementation of Observe, which is actually the object of the Observer.
With the file
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.dep = new Dep()
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
Copy the code
Here, the essence is everywhere. First, every OB has a DEP.
- If data is an array, all its arrayMethods are listened on, and arrayMethods include
‘push’, ‘pop’, ‘shift’ and ‘unshift’, ‘splice’, ‘sort of’ reverse ‘.
- Perform the walk method if data is an object. Inside the walk method is listening for the first key of the data.
DefineReactive with file
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function, shallow? : boolean) {
const dep = new Dep()
...
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.get: function reactiveGetter () {
if (Dep.target) {
...
dep.depend()
...
}
return value
},
set: function reactiveSetter (newVal) {... dep.notify() } }) }Copy the code
This is where we find the expected hijacking of the setter and getter for the corresponding key value passed in to the data by defineProperty.
- When our data data.a.b.c changes, we go to the setter and execute Watcher’s update method to get the latest state of the data (implemented in DEP.notify). The vm.$watch(‘ A.B.C ‘,cb) and the data bound by instructions and interpolation syntax in the template are all based on Watcher.
- The dependency collection dep.depend() is done in get.
- Update notification dep.notify() in set
- Next, how does Dep implement dependency collection and
src\core\observer\dep.js
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
Dep.target is a Watcher type. For objects, dependencies are collected in getters and triggered in setters. Where are the dependencies stored? Here the Dep class manages the dependencies, and for each key value of the response data object, there is an array to store the dependencies.
src\core\observer\watcher.js
export default class Watcher {
cb: Function;
deps: Array<Dep>;
constructor () {
this.cb = cb
this.deps = []
this.newDeps = []
this.depIds = new Set(a)this.newDepIds = new Set()}/** * Add a dependency to this directive. */
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)}}}/** * Clean up for dependency collection. */
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)}}}/** * Subscriber interface. * Will be called when a dependency changes. */
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}/** * Scheduler job interface. * Will be called by the scheduler. */
run () {
this.cb.call(this.vm, value, oldValue)
}
}
Copy the code
The addDep and update methods inside watcher actually trigger dep.addSub and the dependencies that are collected
At the end
So far, see Vue to MVVM initialization process, of course, some of the middle lifecycle initialization, event initialization are mentioned, interested partners can continue to in-depth oh.