First published at: github.com/USTB-musion…
Writing in the front
Js is the technology stack adopted in the recent project, and I am very interested in the underlying operation mechanism of vue.js, so I spend some time every day recently to summarize the source code of vue.js for about one or two months. Here I summarize the process of template and data rendering into the final DOM.
In the process of looking at the source code, there may be their own understanding of the deviation or we have a different understanding of the place, you are welcome to comment or private letter me, common learning progress.
A global overview of the vue.js operating mechanism
This is a global overview of the vue.js mechanism I found on the Internet. For some of you, this is a little blurry when you first see it. I hope the students who are vague will feel enlightened when they look back at this picture after reading the following analysis.
Vue. Js source directory design
When we look at vue.js source code, it is very necessary to understand the directory design of the source code. This is a sample of vue.js source directory, their general functions are as follows:
folder | function |
---|---|
compiler | Compile related (parsing templates into AST syntax trees, AST syntax tree optimization, code generation) |
core | Core functionality related (VUE instantiation, global API encapsulation, virtual DOM, detection of changes, etc.) |
platforms | Support for different platforms (including Web and WEEX) |
server | Server side render related (server side render related logic) |
sfc | Parsing.vue file correlation (parsing the contents of a.vue file into a javascript object) |
shared | Sharing code across platforms (defining some globally shared tool methods) |
From the point of view of source directory design, the authors split the source code into independent modules, and the relevant logic is maintained in a special directory, so that the readability and maintainability of the code becomes very clear. | |
## Start from the entrance of Vue |
The real initialization is in SRC /core/index.js:
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
Vue.version = '__VERSION__'
export default Vue
Copy the code
There are two key pieces of code here, import Vue from ‘./instance/index.js’ and initGlobalAPI(Vue), which defines and initializes the global API by passing Vue as an argument to initGlobalAPI.
Start with the definition of the Vue, look at the SRC/core/instance/index. The js files:
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 Vue
Copy the code
As you can see, Vue is actually a Class implemented with Function, which is why we instantiate it with new Vue().
InitGlobalAPI () is defined in SRC /core/global-api/index.js. It extends global methods on the Vue object itself. These global apis can be found on the Vue website.
Start with new Vue()
new Vue({
el: '#app'.data() {
return {
message: '11'}},mounted() {
console.log(this.message)
},
methods: {}});Copy the code
May be a lot of people write vUE write code, more or less have such a question?
1. What's going on behind New Vue?
In mounted, a message defined in data can be printed using this.message.
3. How are the templates and data rendered into the final DOM?
With these questions in mind, let’s take a look at what’s going on inside New Vue.
Vue is actually a class that is defined in SRC/core/instance/index in js:
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
As you can see, the _init method is called when the Vue is initialized with the new keyword. The method is defined in SRC/core/instance/init. In js:
Vue.prototype._init = function (options? :Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if(process.env.NODE_ENV ! = ='production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
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')
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
Copy the code
As you can see, the _init method does two main things:
1. Merge configurations, initialize life cycles, initialize events, initialize Render, initialize Data, computed, Methods, Wacther, etc.
2. At the end of initialization, if an EL attribute is detected, the vm.$mount method is called to mount the VM and mount the component.
BeforeCreate and created the life cycle will call initState to initialize state, initState () method defined in SRC/core/instance/state in js:
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
During this process, props, methods, data, computed, and watch are initialized successively. This is the process of vue.js performing “responsiveness” (bidirectional binding) on the data in options. In the initData method:
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, 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
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if(process.env.NODE_ENV ! = ='production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if(props && hasOwn(props, key)) { process.env.NODE_ENV ! = ='production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if(! isReserved(key)) { proxy(vm,`_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)}Copy the code
Finally, observer() is called, and Observe binds the Object in data bidirectionally via defineReactive and sets setter and getter methods on the Object via Object.defineProperty. Getter methods are used primarily for dependency collection. Setter methods fire when the object is modified (Vue. Set is used to add properties when no property is added), and the setter notifies the Dep in the closure, which has Watcher observer objects subscribed to the object’s changes, and the Dep notifies the Watcher object to update the view.
Proxy (vm, _data, key); attach attributes from data to vm;
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
The proxy implements the proxy through defineProperty, changing the read/write of target[sourceKey][key] to the read/write of target[key]. Mounted {this.message} Mounted {this.message} mounted {this.message} mounted {this.message}
Back to the implementation of the mount component in the _init method. Platforms /platforms/web/entry-runtime-with-compiler.js:
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
) :Component {
el = el && query(el)
/* istanbul ignore if */
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
// resolve template/el and convert to render function
if(! options.render) {let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) = = =The '#') {
template = idToTemplate(template)
/* istanbul ignore if */
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) {
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
mark('compile')}const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
mark('compile end')
measure(`vue The ${this._name} compile`.'compile'.'compile end')}}}return mount.call(this, el, hydrating)
}
Copy the code
This code first caches the $mount method on the prototype. First, we restrict EL from mounting Vue on nodes like HTML or body, and then we convert el or template strings to render methods if we don’t define render methods, because in ve2. X, All Vue components will eventually need the render method, and at the end of the code there’s vue.compile = compileToFunctions, which compileToFunctions the template into render functions.
How is template compiled into the Render function?
Vue is available in two versions, one Runtime+Compiler version and one Runtime only version. Runtime+Compiler contains the compiled code, which can be done at Runtime. Runtime only does not include compiled code, so you need to use webpack’s vue-loader to compile the template into the render function.
In real development, we usually write templates in components. So how is template compiled? Take a look at the compiler entry, defined in SRC /compiler/index.js:
export const createCompiler = createCompilerCreator(function baseCompile (template: string, options: CompilerOptions) :CompiledResult {
const ast = parse(template.trim(), options)
if(options.optimize ! = =false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
Copy the code
There are three main procedures for compiling:
1. Parse the template string to generate the AST
- AST (in computer science, an Abstract syntax tree (or AST for short), or syntax tree, is a tree-like representation of the abstract syntax structure of source code, specifically the source code of a programming language.)
const ast = parse(template.trim(), options)
Copy the code
Parse parses data such as instructions, classes, and styles in the Template template using regexes to form an AST tree. The AST describes the entire template in the form of Javascript objects. The entire parse process uses regular expressions to parse the template in sequence. When the template is parsed to the start tag, the close tag, and the text, the response callback function will be executed respectively to achieve the purpose of constructing the AST tree.
Here’s an example:
<div :class="c" class="demo" v-if="isShow">
<span v-for="item in sz">{{item}}</span>
</div>
Copy the code
After a series of regular parsing, the resulting AST is as follows:
{
/* A map of the tag attributes, which record the tag attributes */
'attrsMap': {
':class': 'c'.'class': 'demo'.'v-if': 'isShow'
},
/* Class */
'classBinding': 'c'./* The label attribute v-if */
'if': 'isShow'./* v-if condition */
'ifConditions': [{'exp': 'isShow'}]./* The tag attribute class */
'staticClass': 'demo'./* Tag */
'tag': 'div'./* Sublabel array */
'children': [{'attrsMap': {
'v-for': "item in sz"
},
/* Arguments to the for loop */
'alias': "item"./* The object for the loop */
'for': 'sz'./* Flag bit for whether the for loop has been processed */
'forProcessed': true.'tag': 'span'.'children': [{/* expression, _s is a function that converts strings */
'expression': '_s(item)'.'text': '{{item}}'}]}]}Copy the code
Once the AST is constructed, it’s time to optimize the AST tree.
2. Optimize: Optimize the AST syntax tree
optimize(ast, options)
Copy the code
Why is there an optimization process here? We know that Vue is data-driven and responsive, but not all of the data in the Template is responsive, and many of the data will not change after initial rendering, so the DOM corresponding to this part of the data will not change. There will be a process of update interface later, in which there will be a patch process, and the DIff algorithm will directly skip the static node, thus reducing the comparison process and optimizing the patch performance.
The optimize code is defined in SRC/Compiler /optimize.js:
export function optimize (root: ? ASTElement, options: CompilerOptions) {
if(! root)return
isStaticKey = genStaticKeysCached(options.staticKeys || ' ')
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
markStatic(root)
// second pass: mark static roots.
markStaticRoots(root, false)}Copy the code
As you can see, Optimize actually does two things, one calling markStatic() to mark the static node and the other markStaticRoots() to mark the static root node.
3. Codegen: Converts the optimized AST tree into executable code.
const code = generate(ast, options)
Copy the code
The template template goes through the parse->optimize-> codeGen process to get the render function.
The last
Look at the picture above, is there a general vein? This article is I wrote the first vue.js source learning article, there may be a lot of defects, I hope in the future learning to explore slowly improved it.
You can pay attention to my public account “Muchen classmates”, goose factory code farmers, usually record some trivial bits, technology, life, perception, grow together.