An old article, published in 2017. Welcome to follow my personal wechat official account “HcySunYang”. Let’s coding for fun together!
The name of the original article was called “source code analysis”, but later thought, or use “source code learning” to the appropriate point, before not thoroughly master every letter of the source code, “analysis” is a bit of a title party. Before reading this article, it is recommended to open the 2.1.7 source code and look at it. This may be easier to understand. In addition, my level is limited, there are mistakes or inappropriate places in the article, I hope you can point out and grow together.
Addendum: Vue 2.2 has just been released. As the first of a series of articles, this article focuses on the organization of Vue code, restoration of Vue constructors, design of prototypes, and handling of parameter options as well as poorly written data binding and how to update views using the Virtual DOM. Looking at the framework as a whole, it seems that V2.1.7 doesn’t have much impact on understanding the code in V2.2. Subsequent articles in this series will start with the latest source code and hint at changes.
A long time ago, I wrote an article: JavaScript implementation of MVVM I just want to monitor the change of a common object, the beginning of the article mentioned my blog style, or the same sentence, only write efforts to let small white, even elementary school can understand the article. This will inevitably result in some ink stains for some students, so you can read it in detail or skip it according to your preferences.
Start by understanding an open source project
To look at the source code of a project, don’t just look at it at the beginning, first look at the metadata and dependencies of the project itself, in addition to the PR rules, Issue Reporting rules, etc. Especially for “front-end” open source projects, the first thing that comes to mind before looking at the source code is the package.json file.
In package.json, we should pay attention to the scripts field and devDependencies field, and the dependencies field. The devDependencies and Dependencies field give you an idea of the dependencies of the project.
With this in mind, if there are dependencies we can just NPM install to install them.
In addition to package.json, we also need to read the project’s contribution rules documentation to get started. A good open source project will definitely include this, and Vue is no exception: github.com/vuejs/vue/b… This document explains the code of conduct, PR guide, Issue Reporting guide, Development Setup, and project structure. By reading this, we can get an idea of how the project started, how it was developed, and the description of the directory. Here is a brief introduction to the important directories and files that you can read for yourself:
├ ─ ─ build ├ ─ ─ dist ├ ─ ─ examples ├ ─ ─ flow ├ ─ ─ package. The json ├ ─ ─ the test ├ ─ ─ the SRC │ ├ ─ ─ entries │ │ ├ ─ ─ web - runtime. Js │ │ ├ ─ ─ Web running-with-Compiler.js │ ├─ Web - Compiler.js │ ├─ Web -server-renderer.js │ ├─ Compiler │ ├─ parser │ ├ ─ ─ codegen │ │ ├ ─ ─ optimizer. Js │ ├ ─ ─ the core │ │ ├ ─ ─ the observer │ │ ├ ─ ─ vdom │ │ ├ ─ ─ the instance │ │ ├ ─ ─ global - API │ │ ├ ─ ─ Components │ ├─ Server │ ├─ Platforms │ ├─ SFC │ ├─ SharedCopy the code
With an overview of the important directories and files, we can look at the Common commands section of Development Setup to see how to start the project. We can see this introduction:
# watch and auto re-build dist/vue.js
$ npm run dev
# watch and auto re-run unit tests in Chrome
$ npm run dev:test
Copy the code
Now, we just need to run NPM run dev to detect file changes and automatically rebuild the output dist/vue.js, and then run NPM run dev:test to test. But for convenience, I’m going to create an example in the examples directory and then reference dist/vue.js so that we can take this example and see how we want to play with the vue source code as we go along.
Two, look at the source tips
Before I dive into the world of source code, I’d like to talk briefly about the techniques of looking at source code:
Focus on the big picture, from macro to micro
When you look at the code of a project, it is best to find a common thread, first to understand the general structure of the process, and then to dive into the details and break down items, for example Vue: If you already know that the VIRTUAL DOM is used to update the DOM after the data state changes in the Vue, then if you don’t know the Virtual DOM, take my word for it: “Don’t go into the internal implementation for the moment, because you will lose the thread.” All you need to know is that Virtual DOM has three steps:
Diff (oldNode, newNode) diff(oldNode, newNode) diff(oldNode, newNode) Apply the difference to a real DOM tree
Sometimes the second step can be combined with the third step to form a single step (patch in Vue is the case). In addition, the code in SRC/Compiler/CodeGen can be painful to read without knowing what it has written. But all you need to know is that CodeGen is used to generate the render function from the Abstract syntax tree (AST), which generates code like this:
function anonymous() {
with(this){return _c('p',{attrs:{"id":"app"}},[_v("\n "+_s(a)+"\n "),_c('my-com')])}
}
Copy the code
When we know that something exists, and we know why it exists, it’s easy to catch on to that thread, and the first article in this series focused on the general thread. Once we get to the basics, we know what each part does. For example, codeGen generates functions similar to those shown in the code posted above, so when we look at the code under CodeGen, it will be more purposeful and easier to understand.
What does the Vue constructor look like
Balabala a bunch. Let’s start with the dry stuff. The first thing we need to do is figure out exactly what the Vue constructor looks like.
We know that we need to use the new operator to call Vue, so Vue should be a constructor, so the first thing we need to do is to find the constructor. How do we find the Vue constructor? Dist /vue.js: NPM run dev: dist/vue.js: NPM run dev
"dev": "TARGET=web-full-dev rollup -w -c build/config.js",
Copy the code
First set the TARGET value to ‘web-full-dev’, then, then, then if you don’t know rollup it should be a quick look… It is simply a JavaScript module wrapper. You can think of it simply as webpack, but it has its advantages, such as tree-shaking (webPack2 also has its disadvantages in some scenarios… -w = build/config.js -w = build/config.js
...
const builds = {
...
'web-full-dev': {
entry: path.resolve(__dirname, '../src/entries/web-runtime-with-compiler.js'),
dest: path.resolve(__dirname, '../dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
...
}
function genConfig(opts){
...
}
if (process.env.TARGET) {
module.exports = genConfig(builds[process.env.TARGET])
} else {
exports.getBuild = name => genConfig(builds[name])
exports.getAllBuilds = () => Object.keys(builds).map(name => genConfig(builds[name]))
}
Copy the code
The code above is simplified. When we run NPM run dev, the value of process.env.TARGET is equal to ‘web-full-dev’, so
module.exports = genConfig(builds[process.env.TARGET])
Copy the code
This code is equivalent to:
module.exports = genConfig({ entry: path.resolve(__dirname, '.. /src/entries/web-runtime-with-compiler.js'), dest: path.resolve(__dirname, '.. /dist/vue.js'), format: 'umd', env: 'development', alias: { he: './entity-decoder' }, banner })Copy the code
Finally, the genConfig function returns a Config object, which is the Rollup configuration object. Then we can see that the entry file is:
src/entries/web-runtime-with-compiler.js
Copy the code
Let’s open this file, don’t forget our topic, we’re looking for the Vue constructor, so when we see the first line of code in this file:
import Vue from './web-runtime'
Copy the code
At this point, you should know that this file is not for you. You should open the web-runtime.js file, but when you open the file, the first line looks like this:
import Vue from 'core/index'
Copy the code
According to this thought, we finally find the location of the Vue constructor should be in the SRC/core/instance/index. The js file, in fact, we also guess guess, described above directory said: when the instance is a directory where Vue constructor design code. To summarize, our search process goes like this:
7 xlolm.com1.z0.glb.clouddn.com/vueimg2bd0d…
We look back a look at the SRC/core/instance/index. The js file, is simple:
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) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) export default VueCopy the code
The dependency is introduced, the Vue constructor is defined, five methods are called with the Vue constructor as an argument, and the Vue is exported. The five methods come from five files: init.js state.js render.js events.js and lifecycle.
Open these five files and find the corresponding methods. The purpose of these methods is to mount methods or properties on the Vue prototype. The Vue will look like this after going through these five methods:
Vue.prototype._init = function (options? : Object) {} Vue.prototype.$data Vue.prototype.$set = set Vue.prototype.$delete = del Vue.prototype.$watch = function(){} Vue.prototype.$nextTick = function (fn: Function) {} Vue.prototype._render = function (): VNode {} Vue.prototype._s = _toString Vue.prototype._v = createTextVNode Vue.prototype._n = toNumber Vue.prototype._e = createEmptyVNode Vue.prototype._q = looseEqual Vue.prototype._i = looseIndexOf Vue.prototype._m = function(){} Vue.prototype._o = function(){} Vue.prototype._f = function resolveFilter (id) {} Vue.prototype._l = function(){} Vue.prototype._t = function(){} Vue.prototype._b = function(){} Vue.prototype._k = function(){} Vue.prototype.$on = function (event: string, fn: Function): Component {} Vue.prototype.$once = function (event: string, fn: Function): Component {} Vue.prototype.$off = function (event? : string, fn? : Function): Component {} Vue.prototype.$emit = function (event: string): Component {} Vue.prototype._mount = function(){} Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) {} Vue.prototype._updateFromParent = function(){} Vue.prototype.$forceUpdate = function () {} Vue.prototype.$destroy = function () {}Copy the code
Is that the end of it? No, according to our previous route to find Vue, this is just the beginning, we retrace the route back, so the next thing to handle the Vue constructor should be the SRC /core/index.js file, we open it:
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Vue.version = '__VERSION__'
export default Vue
Copy the code
This file is also very simple. Import the Vue from instance/ Index that has methods and properties mounted on the prototype, import initGlobalAPI and isServerRendering, and pass the Vue as a parameter to initGlobalAPI. Finally, $isServer was mounted on Vue. Prototype and the Version attribute was mounted on Vue.
InitGlobalAPI is used to mount static properties and methods on the Vue constructor. After passing through initGlobalAPI, the Vue looks like this:
Vue.config
Vue.util = util
Vue.set = set
Vue.delete = del
Vue.nextTick = util.nextTick
Vue.options = {
components: {
KeepAlive
},
directives: {},
filters: {},
_base: Vue
}
Vue.use
Vue.mixin
Vue.cid = 0
Vue.extend
Vue.component = function(){}
Vue.directive = function(){}
Vue.filter = function(){}
Vue.prototype.$isServer
Vue.version = '__VERSION__'
Copy the code
Vue. Options is a slightly more complex one, and you can see that he does look like that. Next up is the web-runtime.js file. The web-runtime.js file does three things:
Override Vue. Config properties Directives and Vue.options.components install platform-specific directives and components. Define __Patch__ and $mount on vue.prototype
After passing through the web-runtime.js file, the Vue looks like this:
Vue.config.isUnknownElement = isUnknownElement
Vue.config.isReservedTag = isReservedTag
Vue.config.getTagNamespace = getTagNamespace
Vue.config.mustUseProp = mustUseProp
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
}
Vue.prototype.__patch__
Vue.prototype.$mount
Copy the code
Note the changes to vue. options. The $mount method here is simple:
Vue.prototype.$mount = function ( el? : string | Element, hydrating? : boolean ): Component { el = el && inBrowser ? query(el) : undefined return this._mount(el, hydrating) }Copy the code
Query (el) gets the element based on the browser environment, and then passes el as a parameter to this._mount().
The last file to handle Vue is the entry web-runtime-with-Compiler.js file, which does two things:
Caches the $mount function from the web-runtime.js file
const mount = Vue.prototype.$mount
Copy the code
Then overwrite Vue. Prototype.$mount
Mount compile on Vue
Vue.compile = compileToFunctions
Copy the code
The function compileToFunctions compiles the template to the render function.
At this point, we’ve restored the Vue constructor, so to summarize:
Vue. Prototype mounts properties and methods in the SRC /core/instance directory
2. Static properties and methods mounted under Vue are handled primarily by code in the SRC /core/ global-API directory
3, Web-runtime. js is used to add configuration, components, and directives specific to the Web platform. Web-runtimewith-compiler. js adds compiler compiler to Vue’s $mount method, supporting template.
4. A running example
Now that we know about the design of the Vue constructor, let’s give a round of applause for an example that runs through it:
let v = new Vue({
el: '#app',
data: {
a: 1,
b: [1, 2, 3]
}
})
Copy the code
Okay, I’ll admit that your kid could have written this code before he was a month old. This code is going to be the example that we’re going to run through, and it’s going to be the backbone of this article, and we’re going to use this code as an example, and we’re going to add options where we need to, like a computed property, of course. In the beginning, THOUGH, I only passed in two options, EL and data, “Let’s see what happens next, let’s see” — a favorite line of NBA players in interviews.
What does Vue do when we code using it as in the example?
To find out what Vue does, go to the Vue initializer and look at the Vue constructor:
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) }Copy the code
We see that the _init() method is the first method Vue calls, passing through our options argument. Before calling _init(), a safe-mode processing is done to tell the developer that Vue must be called using the new operator. According to before our arrange, _init () method should be in the SRC/core/instance/init. Defined in js file, we open the file view _init () method:
Vue.prototype._init = function (options? : Object) { const vm: Component = this vm._uid = uid++ vm._isVue = true if (options && options._isComponent) { initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } if (process.env.NODE_ENV ! == 'production') { initProxy(vm) } else { vm._renderProxy = vm } vm._self = vm initLifecycle(vm) initEvents(vm) callHook(vm, 'beforeCreate') initState(vm) callHook(vm, 'created') initRender(vm) }Copy the code
The _init() method begins by defining two properties on this: _isComponent. We do not use the _isComponent option when developing projects using Vue. The _isComponent option is used internally by Vue. We’ll do the else branch, which is this code:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
Copy the code
So here’s what Vue does in the first step: merge parameter options using policy objects
$options (vm === this), pass mergeOptions to the mergeOptions method. We take a look at, respectively, the first is: resolveConstructorOptions (vm) constructor), we take a look at this method:
export function resolveConstructorOptions (Ctor: Class<Component>) { let options = Ctor.options if (Ctor.super) { const superOptions = Ctor.super.options const cachedSuperOptions = Ctor.superOptions const extendOptions = Ctor.extendOptions if (superOptions ! == cachedSuperOptions) { Ctor.superOptions = superOptions extendOptions.render = options.render extendOptions.staticRenderFns = options.staticRenderFns extendOptions._scopeId = options._scopeId options = Ctor.options = mergeOptions(superOptions, extendOptions) if (options.name) { options.components[options.name] = Ctor } } } return options }Copy the code
This method takes an argument, Ctor, which, as we know from the passed vm.constructor, is the Vue constructor itself. So here’s the code:
let options = Ctor.options
Copy the code
Is equivalent to:
let options = Vue.options
Copy the code
Do you remember vue.options? In the finding Vue constructor section, we sorted out that Vue. Options should look like this:
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
}
Copy the code
Determine whether after defines the Vue. Super, this is used to deal with inheritance, and subsequent we speak, in this case, directly returned to the Vue resolveConstructorOptions method. The options. That is, the first argument passed to the mergeOptions method is vue.options.
The second argument passed to the mergeOptions method is the argument option when we call the Vue constructor. The third argument is vm (this object). Using Vue as in the example at the beginning of this section, the final code to run should look like this:
vm.$options = mergeOptions(
{
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
},
{
el: '#app',
data: {
a: 1,
b: [1, 2, 3]
}
},
vm
)
Copy the code
MergeOptions is defined in SRC /core/util/options.js by reference. This file might be a bit confusing at first glance, but here’s a quick look at it that should make it easier for you to understand:
import Vue from '.. /instance/index' other references... const strats = config.optionMergeStrategies strats.el = strats.propsData = function (parent, child, vm, key){} strats.data = function (parentVal, childVal, vm) config._lifecycleHooks.forEach(hook => { strats[hook] = mergeHook }) config._assetTypes.forEach(function (type) { strats[type + 's'] = mergeAssets }) strats.watch = function (parentVal, childVal) strats.props = strats.methods = strats.computed = function (parentVal: ?Object, childVal: Const defaultStrat = function (parentVal:) const defaultStrat = function (parentVal:) any, childVal: any): any { return childVal === undefined ? parentVal : childVal } export function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object { ... const options = {} let key for (key in parent) { mergeField(key) } for (key in child) { if (! hasOwn(parent, key)) { mergeField(key) } } function mergeField (key) { const strat = strats[key] || defaultStrat options[key] = strat(parent[key], child[key], vm, key) } return options }Copy the code
In the above code, I have omitted some utility functions, such as mergeHook and mergeAssets, etc. The only thing to note is this code:
config._lifecycleHooks.forEach(hook => {
strats[hook] = mergeHook
})
config._assetTypes.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
Copy the code
The config object is referenced from the SRC /core/config.js file. The end result is a merge strategy function called mergeHook with the corresponding lifecycle options added under Strats. Directives, components, filters, and other options are merged policy functions mergeAssets.
This makes it much clearer. Take our example throughout this article:
let v = new Vue({
el: '#app',
data: {
a: 1,
b: [1, 2, 3]
}
})
Copy the code
The el option is handled using the defaultStrat policy function, the data option is handled using the strats.data policy function, and according to the logic in Strats. data, the strats.data method eventually returns a function: MergedInstanceDataFn.
We won’t go into the details of each policy function here, but we will focus on the main thread to clarify the main idea. We only need to know that Vue uses a policy object to merge parent and child options when processing options. And assign the final value to the $options property under the instance: this.$options, so let’s see what the _init() method does after merging the options:
After merging the options, the second part of Vue does the initialization and design of Vue instance objects
The Vue instance object is created through the constructor. Let’s take a look at how the Vue instance object is designed. The following code is after the _init() method has merged the options:
if (process.env.NODE_ENV ! == 'production') { initProxy(vm) } else { vm._renderProxy = vm } vm._self = vm initLifecycle(vm) initEvents(vm) callHook(vm, 'beforeCreate') initState(vm) callHook(vm, 'created') initRender(vm)Copy the code
According to the code above, two properties are added to the instance in production, and the values are the instance itself:
vm._renderProxy = vm
vm._self = vm
Copy the code
Then, four init* methods are called: Initlife, initEvents, initState, initRender, and initState call beforeCreate and created respectively. InitRender is executed after the Created hook is executed, so you can see why you can’t manipulate the DOM when created. Because the actual DOM elements have not been rendered into the document at this point. Created simply represents the completion of the initialization of the data state.
According to the reference relation of the four init* methods, open the corresponding file and check the corresponding methods. We find that these methods are handling the Vue instance object and doing some initialization work. Just like cleaning the Vue constructor, I also do the same cleaning for the Vue instance attribute and method, as follows:
this._uid = uid++
this._isVue = true
this.$options = {
components,
directives,
filters,
_base,
el,
data: mergedInstanceDataFn()
}
this._renderProxy = this
this._self = this
this.$parent = parent
this.$root = parent ? parent.$root : this
this.$children = []
this.$refs = {}
this._watcher = null
this._inactive = false
this._isMounted = false
this._isDestroyed = false
this._isBeingDestroyed = false
this._events = {}
this._updateListeners = function(){}
this._watchers = []
this._data
this.$vnode = null
this._vnode = null
this._staticTrees = null
this.$slots
this.$scopedSlots
this._c
this.$createElement
Copy the code
$options._parentlisteners also call the vm._updatelisteners () method on initEvents. These are the properties and methods of an instance of Vue. Some other init methods are called in initState, as follows:
export function initState (vm: Component) {
vm._watchers = []
initProps(vm)
initMethods(vm)
initData(vm)
initComputed(vm)
initWatch(vm)
}
Copy the code
$mount(vm.$options.el); $mount(vm.$options.el);
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
Copy the code
This is why manual mount is required if the EL option is not passed.
So let’s take a look at what happens one by one, following our example at the beginning of this section and in the order of initialization. If we expand the init* method in initState, the order of execution should look like this (top to bottom) :
initLifecycle(vm)
initEvents(vm)
callHook(vm, 'beforeCreate')
initProps(vm)
initMethods(vm)
initData(vm)
initComputed(vm)
initWatch(vm)
callHook(vm, 'created')
initRender(vm)
Copy the code
InitLifecycle is the function that adds properties to the instance, initEvents is the function that adds properties to the instance because the vm.$options._parentListeners are undefined. The listeners on vm._updatelisteners are not executed, and since we only pass EL and data, initProps, initMethods, initComputed and initWatch do nothing. Only initData is executed. Finally, initRender, which, in addition to adding some properties to the instance, executes vm.$mount(vm.$options.el) since we passed the EL option.
To sum up: as written in our example, initialization consists of only two main things: initData and initRender.
See Vue data response system through initData
Vue’s data response system consists of three parts: Observer, Dep and Watcher. The data response system has been covered a lot in this article, so I’m going to keep it simple and try to make it understandable. Let’s look at the code in initData:
function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? data.call(vm) : data || {} 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 ) } const keys = Object.keys(data) const props = vm.$options.props let i = keys.length while (i--) { if (props && hasOwn(props, keys[i])) { process.env.NODE_ENV ! == 'production' && warn( `The data property "${keys[i]}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else { proxy(vm, keys[i]) } } observe(data) data.__ob__ && data.__ob__.vmCount++ }Copy the code
First, get the data: $options.data = vm.$options.data = mergedInstanceDataFn; So after getting data, it also determines whether the data type of data is’ function ‘. The final result is: data or data of the data option we passed in, i.e. :
data: {
a: 1,
b: [1, 2, 3]
}
Copy the code
You then define the _data property on the instance object, which is the same reference to data.
Then there is a while loop that proxies data on the instance object so that we can access data.a via this.a. The code is handled in the proxy function, which is very simple and simply sets the accessor property on the instance object with the same name as the data property. Then use _data to do data hijacking as follows:
function proxy (vm: Component, key: string) { if (! isReserved(key)) { Object.defineProperty(vm, key, { configurable: true, enumerable: true, get: function proxyGetter () { return vm._data[key] }, set: function proxySetter (val) { vm._data[key] = val } }) } }Copy the code
Once the data has been processed, the response system is officially entered,
observe(data)
Copy the code
As mentioned, the data response system mainly consists of three parts: Observer, Dep and Watcher. The code is stored in: Observer /index.js, observer/dep.js, observer/watcher.js, observer/index.js, observer/dep.js, observer/watcher. “Oh, that’s all.”
Suppose we have the following code:
var data = {
a: 1,
b: {
c: 2
}
}
observer(data)
new Watch('a', () => {
alert(9)
})
new Watch('a', () => {
alert(90)
})
new Watch('b.c', () => {
alert(80)
})
Copy the code
The purpose of this code is to define a data object data, then observe it through an observer, then define three observers, when the data changes, execute the corresponding method, how to implement this function using Vue originally? What does observer say? How about the Watch constructor? So let’s do that one by one.
First, the observer is used to convert attributes of the data object to accessor attributes:
class Observer { constructor (data) { this.walk(data) } walk (data) { let keys = Object.keys(data) for(let i = 0; i < keys.length; i++){ defineReactive(data, keys[i], data[keys[i]]) } } } function defineReactive (data, key, val) { observer(val) Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { return val }, set: function (newVal) { if(val === newVal){ return } observer(newVal) } }) } function observer (data) { if(Object.prototype.toString.call(data) ! == '[object Object]') { return } new Observer(data) }Copy the code
In the above code, we define an Observer method that checks if the data is a pure JavaScript object, and if so, calls the Observer class, passing data through as a parameter. In an Observer class, we use the Walk method to loop through the defineReactive method on the attributes of the data. DefineReactive is a simple method that simply converts the attributes of the data to accessor attributes and makes recursive observations on the data. Otherwise, only direct child attributes of data can be observed. This completes our first step, when we modify or retrieve the value of the data property, we can get notification via GET and set.
Let’s move on and take a look at Watch:
new Watch('a', () => {
alert(9)
})
Copy the code
Now the question is, how does Watch relate to the Observer ??????? Let’s see what Watch knows. By calling Watch above, we pass Watch two arguments. One is’ A ‘, which we can call an expression, and the other is a callback function. So for now we can only write code like this:
class Watch {
constructor (exp, fn) {
this.exp = exp
this.fn = fn
}
}
Copy the code
So how to do that? Let’s see what happens in the following code:
class Watch {
constructor (exp, fn) {
this.exp = exp
this.fn = fn
data[exp]
}
}
Copy the code
Data [exp] = data[exp] Exp = ‘a’; exp = ‘a’; data[exp] = ‘data.a’; Remember that the property under the data is already an accessor property, so this will trigger the get function for the property. We are successfully associated with the observer, but this is not enough. We are still not there, but we are infinitely close. Let’s go ahead and see if we can do this:
Since evaluating an expression in Watch triggers an Observer GET, can we collect Watch functions in GET?
The answer is yes, but that’s where Dep comes in, which is a dependency collector. The idea is that each property under data has a unique Dep object, collect dependencies only for that property in get, and then trigger all collected dependencies in set, and you’re done:
class Dep {
constructor () {
this.subs = []
}
addSub () {
this.subs.push(Dep.target)
}
notify () {
for(let i = 0; i < this.subs.length; i++){
this.subs[i].fn()
}
}
}
Dep.target = null
function pushTarget(watch){
Dep.target = watch
}
class Watch {
constructor (exp, fn) {
this.exp = exp
this.fn = fn
pushTarget(this)
data[exp]
}
}
Copy the code
We added pushTarget(this) to Watch, which sets dep. target to the Watch object. After pushTarget we evaluate the expression, and then we modify the defineReactive code as follows
function defineReactive (data, key, val) {
observer(val)
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.addSub()
return val
},
set: function (newVal) {
if(val === newVal){
return
}
observer(newVal)
dep.notify()
}
})
}
Copy the code
As marked, three new lines of code are added. We know that the evaluation of the expression in Watch will trigger the get method. We call dep.addSub in the get method and execute this code: This.subs.push (dep.target), because the dep.target value is set to a Watch object before this code is executed, the end result is a collection of Watch objects, Then we call dep.notify in the set method, so when the value of the data property changes, the dep.notify loop calls all the callbacks in the collected Watch object:
notify () {
for(let i = 0; i < this.subs.length; i++){
this.subs[i].fn()
}
}
Copy the code
In this way, observer, Dep, and Watch are linked into an organic whole, achieving our original goal. The complete code can be found here: Observer-DEP-Watch. There’s a little bit of a hole here, because we’re not dealing with observations of arrays, and since it’s complicated and not the focus of our discussion, you can poke me in this article if you want to know: If the value of exp is’ a.b ‘, then the value of Vue is’. ‘. Split expression string into array, and then iterate over it to evaluate, you can view its source. As follows:
const bailRE = /[^\w.$]/ export function parsePath (path: string): any { if (bailRE.test(path)) { return } else { 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
The evaluation code for Vue is implemented in the parsePath function in the SRC /core/util/lang.js file. To summarize the Vue dependency collection process, it looks like this:
In fact, Vue does not call addSub directly from get, but calls DEP.depend to collect the current DEP object into the Watch object. To complete the process, it would look like this: (Note that each field of the data has its own DEP object and get method.)
Vue then sets up a data response system. As we said earlier, written as our example, the initialization consists of only two main things: initData and initRender. Now that we’re done analyzing initData, let’s look at initRender
Vue render(re-render) = Vue render(re-render)
In the initRender method, since we passed the EL option in our example, the following code executes:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
Copy the code
Here, the $mount method is called. When we restored the Vue constructor, we sorted out all the methods, and the $mount method appeared in two places:
1, in the web-runtime.js file:
Vue.prototype.$mount = function ( el? : string | Element, hydrating? : boolean ): Component { el = el && inBrowser ? query(el) : undefined return this._mount(el, hydrating) }Copy the code
What it does is get the corresponding DOM element via el and then call the _mount method in the lifecycle.
2, in the web-runtime-with-compiler.js file:
const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el? : string | Element, hydrating? : boolean ): Component { el = el && query(el) if (el === document.body || el === document.documentElement) { process.env.NODE_ENV ! == 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ) return this } const options = this.$options if (! options.render) { let template = options.template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template) if (process.env.NODE_ENV ! == 'production' && ! template) { warn( `Template element not found or is empty: ${options.template}`, this ) } } } else if (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV ! == 'production') { warn('invalid template option:' + template, this) } return this } } else if (el) { template = getOuterHTML(el) } if (template) { const { render, staticRenderFns } = compileToFunctions(template, { warn, shouldDecodeNewlines, delimiters: options.delimiters }, this) options.render = render options.staticRenderFns = staticRenderFns } } return mount.call(this, el, hydrating) }Copy the code
The logic of web-runtime-with-compiler.js is as follows:
Caches the $mount method from the web-runtime.js file
Check whether the render option is passed, if it calls the $mount method directly from the web-runtime.js file
3. If render is not passed, check to see if template is available and if so, compileToFunctions to render function
4. If template is not available then check for EL and if so, compileToFunctions (template = getOuterHTML(el)) to render
5. Mount the compiled render function to the this.$options property and call the $mount method in the cached Web-runtime. js file
The mount method is called from top to bottom in a simple graph:
However, we see that the end goal of all these steps is to generate the render function and then call the _mount method in the file. lift.js file. Let’s look at what this method does.
Vue.prototype._mount = function ( el? : Element | void, hydrating? : boolean ): Component { const vm: Component = this vm.$el = el callHook(vm, 'beforeMount') vm._watcher = new Watcher(vm, () => { vm._update(vm._render(), hydrating) }, noop) if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm }Copy the code
The above code is very simple, all the comments are commented, the only thing to look at is this code:
vm._watcher = new Watcher(vm, () => {
vm._update(vm._render(), hydrating)
}, noop)
Copy the code
Does that look familiar? We usually use Watch in this way when using Vue:
This.$watch('a', (newVal, oldVal) => {}) // This.$watch(function(){return this. oldVal) => { })Copy the code
The first argument is an expression or function, the second argument is a callback function, and the third argument is an optional option. The principle is that Watch internally evaluates expressions or functions to trigger the data’s GET method to collect dependencies. The _mount method uses Watcher as the first parameter vm. We might as well look at the source of $watch function is how to implement, according to the content of the reduction in the Vue constructor before finishing it: $warch method is in the SRC/core/instance/state in the js file stateMixin method defined in the source code is as follows:
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: Function, options? : Object ): Function { const vm: Component = this options = options || {} options.user = true const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { cb.call(vm, watcher.value) } return function unwatchFn () { watcher.teardown() } }Copy the code
$warch is a wrapper around Watcher. The first argument to the internal Watcher is vm: The Vue instance object, which we can verify in the Watcher source code, opens the observer/watcher.js file to view:
export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options? : Object = {} ) { } }Copy the code
You can see that the first argument to a real Watcher is actually vm. The second argument is an expression or function, and so on, so now look at this code in _mount:
vm._watcher = new Watcher(vm, () => {
vm._update(vm._render(), hydrating)
}, noop)
Copy the code
Ignoring the first parameter vm, that is, Watcher should evaluate the second parameter internally, which is to run this function:
() => {
vm._update(vm._render(), hydrating)
}
Copy the code
. So the vm and _render () function is the first, the function in the SRC/core/instance/render. Js, the code in this method a lot, here is a simplified:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const {
render,
staticRenderFns,
_parentVnode
} = vm.$options
...
let vnode
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
...
}
vnode.parent = _parentVnode
return vnode
}
Copy the code
The _render method first deconstructs the render function from vm.$options. The vm.$options.render method is used to compile template or EL from the compileToFunctions method in the web-runtime-with-compiler.js file. After deconstructing the render function, we then execute this method:
vnode = render.call(vm._renderProxy, vm.$createElement)
Copy the code
Render proxy = vm._renderProxy; render proxy = vm._renderProxy; render proxy = vm._renderProxy; Vm. _renderProxy = vm, which is the Vue instance object itself, and passes a parameter: vm.$createElement. So what exactly does the render function do? We already know that the render function is compiled from template or EL and returns a virtual DOM object if correct. We might as well use console.log to print the render function when our template is written like this:
<ul>
<li>{{a}}</li>
</ul>
Copy the code
The render function printed looks like this:
7 xlolm.com1.z0.glb.clouddn.com/vueimgr2.pn…
We modified the template to:
<ul>
<li v-for="i in b">{{a}}</li>
</ul>
Copy the code
The render function is printed as follows:
7 xlolm.com1.z0.glb.clouddn.com/vueimgr3.pn…
Vue provides the render option as an alternative to template, and also provides JavaScript with full programming capabilities. The following two ways of writing templates are actually equivalent:
new Vue({
el: '#app',
data: {
a: 1
},
template: '<ul><li>{{a}}</li><li>{{a}}</li></ul>'
})
new Vue({
el: '#app',
render: function (createElement) {
createElement('ul', [
createElement('li', this.a),
createElement('li', this.a)
])
}
})
Copy the code
Now let’s look at the render function we printed:
function anonymous() {
with(this){
return _c('ul', {
attrs: {"id": "app"}
},[
_c('li', [_v(_s(a))])
])
}
}
Copy the code
Isn’t it similar to writing our own render function? Because the scope of the render function is bound to the Vue instance: render. Call (vm._renderProxy, vm.$createElement), the _c, _v, _S, and variable A in the above code correspond to methods and variables in the Vue instance. Do you remember where methods like _c, _v, _S are defined? We know when finishing the Vue constructor, they in the SRC/core/instance/render renderMixin methods defined in js file, in addition to these there are such as: _l, _m, _o and so on. Where _L appears when we use the V-for instruction. So now you can see why these methods are defined in the render. Js file, because they are there to construct the render function.
Now that we know what the Render function looks like, we know that the scope of the render function is the Vue instance itself: this(or VM). So when we execute the render function, a variable such as: a is equivalent to: this.a, which we know is evaluating, so this code in _mount:
vm._watcher = new Watcher(vm, () => {
vm._update(vm._render(), hydrating)
}, noop)
Copy the code
When vm._render is executed, the dependent variables are evaluated and collected as dependencies. According to watcher.js logic in Vue, not only is the callback function executed when the dependent variable changes, but it actually needs to be reevaluated, i.e. it needs to be executed again:
() => {
vm._update(vm._render(), hydrating)
}
Copy the code
This is actually re-render, because vm._update is the last step in the virtual DOM mentioned at the beginning of this article: patch
The vm_render method finally returns a vNode object, the virtual DOM, which is then passed as the first argument to vm_update. In the SRC/core/instance/lifecycle. Js file have so a piece of code:
if (! prevVnode) { vm.$el = vm.__patch__( vm.$el, vnode, hydrating, false , vm.$options._parentElm, vm.$options._refElm ) } else { vm.$el = vm.__patch__(prevVnode, vnode) }Copy the code
If you don’t already have prevVnode, you’re rendering for the first time and create the real DOM. If you already have prevVnode, this is not the first rendering, then patch algorithm is used to do the necessary DOM manipulation. This is how Vue updates the DOM. We just didn’t implement it inside the Virtual DOM.
Now let’s get our heads together when we write code like this:
new Vue({
el: '#app',
data: {
a: 1,
b: [1, 2, 3]
}
})
Copy the code
What Vue does:
1. Build a data response system and use Observer to convert data into accessor properties; Compile el into the render function, which returns the virtual DOM
_mount evaluates _update to render, which evaluates dependent variables to render, collects dependencies for the evaluated variables, and repeats _update when the variables change. To do re-render.
A more detailed diagram would look like this:
7 xlolm.com1.z0.glb.clouddn.com/vueimgdetai…
So far, we have gone through Vue from the general process and selected the key points, but there are still many details that we haven’t mentioned, such as:
1, the template to render function, is actually Mr. Into the abstract syntax tree (AST), and then the abstract syntax tree into the render function, and this set of code we did not mention, because he is complex, in fact, this part of the content is in the end of the regular.
2, we have not talked about the implementation principle of Virtual DOM in detail, there are already articles on the Internet, you can search
3. In our example, we just pass the EL and data options. You know that Vue supports a lot of options, which we didn’t talk about, but they are all by analogy. For example, it’s easy to look at the Watch option once you know how Watcher works.
This article as Vue source enlightenment article, perhaps there are a lot of defects, all when the introduction.
Welcome to follow my personal wechat official account “HcySunYang”. Let’s coding for fun together!