Why write this Vue analysis article?
For the silly front-end (me), reading the source code is not easy. After all, sometimes you can’t understand the articles that read source code analysis. Every time I see the bosses who have used vue for 1 to 2 years can master the principles and even master the source code. When I see that I have been using it for several years, I am always ashamed. If you have always been satisfied with basic business development, you may have to stay at the primary level. Therefore, I hope to record knowledge points while learning the source code, so that my understanding and memory can be deeper and easier to read in the future.
Directory structure
This article uses the first commit a879ec06 of vue as the analysis version
├── build │ └── // `rollup` package configuration├── dist │ └── ├── ├── src // vue source code directory│ ├── compiler // Convert vue-template into render function│ │ ├── // Recursive ast extraction instructions, classify attr, style, class, and generate render functions│ │ ├── // Convert html string to ast via regular matching│ │ ├── // compile main entrance│ │ └── // Compile {{}}│ ├── // Global configuration file for vue│ ├── // Main entrance│ ├── // Unknown (it should be the main entrance in umd format)│ ├── instance // vue instance function│ │ └── // Contains the initialization of vue instances, compile, data proxy, methods proxy, watch data, and rendering│ ├── observer // Implementation of data subscription release│ │ ├── // Implement the array mutation method, $set $remove implementation│ │ ├── // Watch execution queue collection, execution│ │ ├── // Subscription Center Implementation│ │ ├── // Implementation of data hijacking to collect subscribers│ │ └── // watch implementation, subscriber│ ├── util // Tool function│ │ ├── │ │ ├── │ │ ├── │ │ ├── // nexttick implementation│ │ ├── │ │ ├── │ │ └── │ └── vdom │ ├── // dom operation encapsulation│ ├── // Node data analysis (element node, text node)│ ├── // vdom main entrance│ ├── modules // Different attribute processing functions│ │ ├── // Normal attr attribute processing│ │ ├── // class processing│ │ ├── // Event processing│ │ ├── // Props processing│ │ └── // style processing│ ├── // Rendering of node tree, including the addition and subtraction of node update processing, and the corresponding attr processing│ └── // Return the final node data└── // webpack configuration
Analysis of the process from template to html
Our code starts with new Vue(), and the constructor of Vue is as follows:
constructor (options) { // options is our configuration for vue this.$options = options this._data = // Get the element html, that is, template const el = this._el = () // Compile template -> render function const render = compile(getOuterHTML(el)) this._el.innerHTML = '' // Instance proxy data ().forEach(key => this._proxy(key)) // Point this method to the instance if () { ().forEach(key => { this[key] = [key].bind(this) }) } // Data Observation this._ob = observe() this._watchers = [] // watch data and update this._watcher = new Watcher(this, render, this._update) // Rendering function this._update(this._watcher.value) }
When we initialize the project, the constructor will be executed, which shows us the main line of vue initialization:Compile template string => Proxy data/methods this binding => Data observation => Create watch and update rendering
1. Compile template string
const render = compile(getOuterHTML(el))
The implementation of compile is as follows:
export function compile (html) { html = () // Cache the compilation result const hit = cache[html] // The parse function is defined in parse-html. Its function is to convert the html string we get into ast through regular matching, and the output is as follows {tag: 'div', attrs: {}, children: []} return hit || (cache[html] = generate(parse(html))) }
Next, look at the generate function. Ast generates a function that builds node html through the conversion of genElement. In genElement, if for and so on will be judged and converted (the specific processing of the instruction will be analyzed later, focus on the main process code first), and finally the genData function will be executed.
// Generate the node main functionexport function generate (ast) { const code = genElement(ast) // Execute the code code and use this as the global object of the code. So our variable in template will point to the attribute {{name}} -> return new Function (`with (this) { return $[code]}`) } // parse a single node -> genDatafunction genElement (el, key) { let exp // The implementation of the instruction is actually implemented when the template is compiled. if (exp = getAttr(el, 'v-for')) { return genFor(el, exp) } else if (exp = getAttr(el, 'v-if')) { return genIf(el, exp) } else if ( === 'template') { return genChildren(el) } else { // are the tag's own attributes and child node data respectively return `__h__('${ }', ${ genData(el, key) }, ${ genChildren(el) })` } }
We can see what is done in genData. The parse function above converts the html string into ast, while in genData further processes the attrs data of the node, such as class -> renderClass style class props attr classification. Here you can see the implementation of the bind directive, that is, by regular matching: and bind. If it matches, the corresponding value value will be converted into (value) form, while the non-match will be converted into a string ('value'). Finally, the (key-value) of attrs is output. The object obtained here is in the form of a string. For example, (value) and so on are just the variable name, and in generate, the variable value is further obtained through () through new Function.
function genData (el, key) { // No attribute returns empty object if (!) { return '{}' } // key let data = key ? `{key:${ key },` : `{` // class processing if ([':class'] || ['class']) { data += `class: _renderClass(${ [':class'] }, "${ ['class'] || '' }"),` } // attrs let attrs = `attrs:{` let props = `props:{` let hasAttrs = false let hasProps = false for (let i = 0, l = ; i < l; i++) { let attr = [i] let name = // bind attribute if ((name)) { name = (bindRE, '') if (name === 'class') { continue // style processing } else if (name === 'style') { data += `style: ${ },` // Props property processing } else if ((name)) { hasProps = true props += `"${ name }": (${ }),` // Other properties } else { hasAttrs = true attrs += `"${ name }": (${ }),` } // on directive, not implemented } else if ((name)) { name = (onRE, '') // Normal attributes } else if (name !== 'class') { hasAttrs = true attrs += `"${ name }": (${ () }),` } } if (hasAttrs) { data += (0, -1) + '},' } if (hasProps) { data += (0, -1) + '},' } return (/,$/, '') + '}' }
As for genChildren, we can guess that it is to traverse the children in ast to call genElement, which actually includes processing of text nodes here.
// traversal of child nodes -> genNodefunction genChildren (el) { if (!) { return 'undefined' } // Flattening of children return '__flatten__([' + (genNode).join(',') + '])' } function genNode (node) { if () { return genElement(node) } else { return genText(node) } } // parse {{}}function genText (text) { if (text === ' ') { return '" "' } else { const exp = parseText(text) if (exp) { return 'String(' + escapeNewlines(exp) + ')' } else { return escapeNewlines((text)) } } }
genText processes text and line breaks, uses regular parsing {{}} in the parseText function to output strings in the form of strings (value).
Now let's take a look at the __h__ function in __h__('${ }', ${ genData(el, key) }, ${ genChildren(el) })
// h function uses the node data obtained above to obtain vNode object => Virtual domexport default function h (tag, b, c) { var data = {}, children, text, i if ( === 3) { data = b if (isArray(c)) { children = c } else if (isPrimitive(c)) { text = c } } else if ( === 2) { if (isArray(b)) { children = b } else if (isPrimitive(b)) { text = b } else { data = b } } if (isArray(children)) { // Recursive processing of child nodes for (i = 0; i < ; ++i) { if (isPrimitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]) } } // svg processing if (tag === 'svg') { addNS(data, children) } // The child node is a text node return VNode(tag, data, children, text, undefined) }
So far, we analyzed how to deal with const render = compile(getOuterHTML(el)), from el's html string to render function.
2. Proxy data/methods this binding
// Instance proxy data().forEach(key => this._proxy(key)) // Point this method to the instanceif () { ().forEach(key => { this[key] = [key].bind(this) }) }
The implementation of instance proxy data is relatively simple, which is to use the object's setter and getter, return data data when reading this data, and set data data synchronously when setting this data
_proxy (key) { if (!isReserved(key)) { // need to store ref to self here // because these getter/setters might // be called by child scopes via // prototype inheritance. var self = this (self, key, { configurable: true, enumerable: true, get: function proxyGetter () { return self._data[key] }, set: function proxySetter (val) { self._data[key] = val } }) } }
3. Implementation of Obaerve
The implementation principle of Observe is analyzed in many places, mainly using () to establish subscriptions for data changes, which is also called data hijacking in many places. Let’s learn to establish such a data subscription and publishing system from scratch.
Starting from the simplest point, we hope there is a function that can help us listen to the changes in data, and execute a specific callback function whenever the data changes.
function observe(data, callback) { if (!data || typeof data !== 'object') { return } // traverse key (data).forEach((key) => { let value = data[key]; // Recursively traverse the listening depth changes observe(value, callback); // Listen to individual changes (data, key, { configurable: true, enumerable: true, get() { return value; }, set(val) { if (val === value) { return } value = val; // Listen to new data observe(value, callback); // Callback for data changes callback(); } }); }); } // Use the observe function to listen to dataconst data = {}; observe(data, () => { ('data modification'); })
Above we implemented a simple observe function. As long as we pass the compiled function in as a callback, the callback function will be triggered every time the data changes. However, we cannot set up listening and callback functions for separate keys, we can only listen for changes in the entire object to execute callbacks. Below we improve the function to set listening and callbacks for a certain key. At the same time, a scheduling center is established to make the entire subscription publishing model clearer.
// First of all, the subscription centerclass Dep { constructor() { = []; // Subscriber array } addSub(sub) { // Add subscriber (sub); } notify() { // Notice ((sub) => { (); }); } } // Current subscriber, tagged in getter = null; // Subscribersclass Watch { constructor(express, cb) { = cb; if (typeof express === 'function') { = express; } else { = () => { return new Function(express)(); } } (); } get() { // Use the current subscriber to save = this; // Execute expression -> Trigger getter -> Add subscriber in getter (); // Set aside in time = null; } update() { // renew (); } addDep(dep) { // Add a subscription (this); } } // Observer Establish observationclass Observe { constructor(data) { if (!data || typeof data !== 'object') { return } // traverse key (data).forEach((key) => { // key => dep corresponding const dep = new Dep(); let value = data[key]; // Recursively traverse the listening depth changes const observe = new Observe(value); // Listen to individual changes (data, key, { configurable: true, enumerable: true, get() { if () { const watch = ; (dep); } return value; }, set(val) { if (val === value) { return } value = val; // Listen to new data new Observe(value); // Callback for data changes (); } }); }); } } // Listen to changes in a key in the dataconst data = { name: 'xiaoming', age: 26 }; const observe = new Observe(data); const watch = new Watch('', () => { ('age update'); }); = 22
Now we implement subscription center, subscriber, observer. The observer monitors the update of data, and the subscriber subscribes to the update of data through the subscription center. When the data is updated, the observer will tell the subscription center, and the subscription center will notify all subscribers to execute the update function one by one. So far, we can roughly guess the implementation principle of vue:
- Create new Observe to observe changes in data data (new Observe)
- When compiling, when a code snippet or node depends on data data, subscribers are recommended to subscribe to updates of certain data in data (new watch) for that node.
- When dada data is updated, the data update is notified through the subscription center, the node update function is executed, and the node is created or updated (())
The above is our guess on the basic implementation of the subscription and release mode of the vue implementation principle and the compilation to the update process. Now we will analyze the implementation of the vue source code:
Initialization of the instance
// ... // Create data observations for datathis._ob = observe() this._watchers = [] // Add a subscriber Execution render will trigger getter subscriber subscription updates, and data changes will trigger setter Subscription Center notifies subscribers to execute updatethis._watcher = new Watcher(this, render, this._update) // ...
Implementation of data observation in vue
// observe functionexport function observe (value, vm) { if (!value || typeof value !== 'object') { return } if ( hasOwn(value, '__ob__') && value.__ob__ instanceof Observer ) { ob = value.__ob__ } else if ( shouldConvert && (isArray(value) || isPlainObject(value)) && (value) && !value._isVue ) { // Create observers for data ob = new Observer(value) } //Storing associated vm if (ob && vm) { (vm) } return ob } // => Observe functionexport function Observer (value) { = value // Useful in array mutation methods = new Dep() // Observer instance exists in __ob__ def(value, '__ob__', this) if (isArray(value)) { var augment = hasProto ? protoAugment : copyAugment // Array traversal, add variant array method augment(value, arrayMethods, arrayKeys) // Call the observe function for each option of the array (value) } else { // walk -> convert -> defineReactive -> setter/getter (value) } } // => walk = function (obj) { var keys = (obj) for (var i = 0, l = ; i < l; i++) { (keys[i], obj[keys[i]]) } } // => convert = function (key, val) { defineReactive(, key, val) } // Focus on defineReactiveexport function defineReactive (obj, key, val) { // Subscription center corresponding to the key var dep = new Dep() var property = (obj, key) if (property && === false) { return } // Compatible with original setter/getter // cater for pre-defined getter/setters var getter = property && var setter = property && // Implement recursive listening attribute val = obj[key] // Depth priority traversal. Set reactive for subproperties first var childOb = observe(val) // Set getter/setter (obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? (obj) : val // is the current watch instance if () { // dep is the scheduler corresponding to obj[key] Add the current wtcher instance to the scheduler () if (childOb) { // Dep for the observer instance corresponding to the obj[key] value val // Subscription of the mutation method and $set method that implement array () } // TODO: The function here is unknown? if (isArray(value)) { for (var e, i = 0, l = ; i < l; i++) { e = value[i] e && e.__ob__ && e.__ob__.() } } } return value }, set: function reactiveSetter (newVal) { var value = getter ? (obj) : val // Get val through getter to determine whether it changes if (newVal === value) { return } if (setter) { (obj, newVal) } else { val = newVal } // Set reactive for new values childOb = observe(newVal) // Notify the key corresponding to the subscription center update () } }) }
Implementation of Subscription Center
let uid = 0 export default function Dep () { = uid++ // Subscribe to the watch array of the dispatch center = [] } // Current watch instance = null // Add subscriber = function (sub) { (sub) } // Remove subscribers = function (sub) { .$remove(sub) } // Subscribe = function () { // (this) => () => () (this) } // Notification update = function () { // stablize the subscriber list first var subs = () for (var i = 0, l = ; i < l; i++) { // subs[i].update() => () subs[i].update() } }
Subscriber implementation
export default function Watcher (vm, expOrFn, cb, options) { // mix in options if (options) { extend(this, options) } var isFn = typeof expOrFn === 'function' = vm // vm's _watchers contains all watches vm._watchers.push(this) = expOrFn = cb = ++uid // uid for batching = true = // for lazy watchers // deps A watch instance can correspond to multiple deps = [] = [] = (null) = null = null // for async error stacks // parse expression for getter/setter if (isFn) { = expOrFn = undefined } else { warn('vue-lite only supports watching functions.') } = ? undefined : () = = false } = function () { () var scope = || var value try { // Execute expOrFn, getter => () will be triggered to add the watch instance to the dep corresponding to obj[key] value = (scope, scope) } if () { // Depth watch // The getter watch instance that triggers each key will correspond to multiple dep traverse(value) } // ... () return value } // Trigger getter to implement subscription = function () { = this = (null) = 0 } // Add a subscription = function (dep) { var id = if (![id]) { // Add the newly appeared dep to newDeps [id] = true (dep) // If you are already in the dispatch center, no longer add it repeatedly if (![id]) { // Add watch to the array in the dispatch center (this) } } } = function () { // Getter contact for excision of key = null var i = while (i--) { var dep = [i] if (![]) { // Remove subscriptions for watches in dep that are not associated in expOrFn expression (this) } } = var tmp = = // TODO: Since newDeps will eventually be empty, the meaning of assignment here is? = tmp } // Subscription center notification message update = function (shallow) { if () { = true } else if ( || !) { () } else { // if queued, only overwrite shallow with non-shallow, // but not the other way around. = ? shallow ? : false : !!shallow = true // record before-push error stack in debug mode /* istanbul ignore if */ if (.NODE_ENV !== 'production' && ) { = new Error('[vue] async stack trace') } // Add to the pool to be executed pushWatcher(this) } } // Execute update callback = function () { if () { var value = () if ( ((isObject(value) || ) && !) ) { // set new value var oldValue = = value var prevError = // ... (, value, oldValue) } = = false } } = function () { var i = while (i--) { [i].depend() } }
Watch callback execution queue
We can find above that watch performs update when it receives the update information. If pushWatcher(this) is executed in asynchronous situation and push the instance into the execution pool, then when will the callback function be executed and how? Let's take a look at the implementation of pushWatcher.
// var queueIndex var queue = [] var userQueue = [] var has = {} var circular = {} var waiting = false var internalQueueDepleted = false // Reset the execution poolfunction resetBatcherState () { queue = [] userQueue = [] // has avoid duplication has = {} circular = {} waiting = internalQueueDepleted = false } // Execution execution queuefunction flushBatcherQueue () { runBatcherQueue(queue) internalQueueDepleted = true runBatcherQueue(userQueue) resetBatcherState() } // Batch executionfunction runBatcherQueue (queue) { for (queueIndex = 0; queueIndex < ; queueIndex++) { var watcher = queue[queueIndex] var id = // After execution is set to null has[id] = null () // in dev build, check and stop circular updates. if (.NODE_ENV !== 'production' && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > config._maxUpdateCount) { warn( 'You may have an infinite update loop for watcher ' + 'with expression "' + + '"', ) break } } } } // Add to execution poolexport function pushWatcher (watcher) { var id = if (has[id] == null) { if (internalQueueDepleted && !) { // an internal watcher triggered by a user watcher... // let's run it immediately after current user watcher is done. (queueIndex + 1, 0, watcher) } else { // push watcher into appropriate queue var q = ? userQueue : queue has[id] = (watcher) // queue the flush if (!waiting) { waiting = true // Execute in nextick nextTick(flushBatcherQueue) } } } }
4. patch implementation
The above is the implementation principle of data-driven in vue. Next, we will return to the main process. After executing the watch, we will execute this._update(this._watcher.value) to start node rendering.
// _update => createPatchFunction => patch => patchVnode => (dom api) // vtree is the result of the execution of the render function compiled by the compile function, returning the object currently representing the current dom structure (virtual node tree)_update (vtree) { if (!this._tree) { // First rendering patch(this._el, vtree) } else { patch(this._tree, vtree) } this._tree = vtree } // When processing nodes, different processing needs to be done for class, props, style, attrs, events, and different processing needs// Inject here the processing functions for different attributesconst patch = createPatchFunction([ _class, // makes it easy to toggle classes props, style, attrs, events ]) // => createPatchFunction returns the patch function. The patch function adds, deletes and updates the nodes by comparing the differences between virtual nodes.// Finally, call the native dom API to update htmlreturn function patch (oldVnode, vnode) { var i, elm, parent var insertedVnodeQueue = [] // pre hook for (i = 0; i < ; ++i) [i]() if (isUndef()) { oldVnode = emptyNodeAt(oldVnode) } if (sameVnode(oldVnode, vnode)) { // someNode can patch patchVnode(oldVnode, vnode, insertedVnodeQueue) } else { // Normally, no reuse remove insert elm = parent = (elm) createElm(vnode, insertedVnodeQueue) if (parent !== null) { (parent, , (elm)) removeVnodes(parent, [oldVnode], 0, 0) } } for (i = 0; i < ; ++i) { insertedVnodeQueue[i].(insertedVnodeQueue[i]) } // hook post for (i = 0; i < ; ++i) [i]() return vnode }
Ending
The above analyzes the general implementation of vue from template to node rendering. Of course, there are some places where there is no comprehensive analysis. The template resolution ast is mainly implemented through regular matching, and the patch process of node rendering and update is mainly implemented through node operation comparison. However, we have a relatively complete understanding of the general process of compiling template strings => proxy data/methods this binding => data observation => establishing watch and updating rendering.
This is all about this article about the first commit analysis of vue. For more related vue commit content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!