preface
The above six articles explained the basic content of Vue, including declaration, invocation, and the implementation of Vue observe. This chapter will start to explain mount, including compilation, update, patch, etc. More space, patience to finish reading.
mount
The mount function was briefly mentioned in the declaration section before, and is explained in detail here. The body begins below.
$mount
Take a look at the complete $mount code:
} // {FFFFFFFF /platforms/web/entry-runtime-with-compiler.js} // save mount const mount = Vue. Prototype Vue.prototype.$mount = function (el? : string | Element, hydrating? : boolean): Component {el = el && query(el) Not allowed in the body and the root element instantiated Vue if (el = = = document. The body | | el = = = document. The documentElement) {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) } } else if (template.nodeType) { template = template.innerHTML } else { return this } } else if (el) { template = getOuterHTML(el) } if (template) { const {render, staticRenderFns} = compileToFunctions(template, { outputSourceRange: process.env.NODE_ENV ! == 'production', shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns } } return mount.call(this, el, hydrating) }Copy the code
This code checks the EL, disallows Vue instantiation on the body and root elements, and then generates Render and staticRenderFns to attach to the options object. Let’s see how render and staticRenderFns are generated.
- The first thing we get is template(using nodeType);
- Call compileToFunctions to render and staticRenderFns;
- Finally, the saved mount is called. The saved mount is here:
// @file src/platforms/web/runtime/index.js Vue.prototype.$mount = function ( el? : string | Element, hydrating? : boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }Copy the code
The mountComponent function is finally called.
compileToFunctions
CompileToFunctions this function is deeply nested, meaning looped; In fact, it is the process of calling a function with a function as an argument, and finally returning the function. The initial acquisition is in
// @file src/platforms/web/compiler/index.js
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
Copy the code
CreateCompiler generates compile and compileToFunctions; The baseOptions content is shown below:Then let’s look at createCompiler:
// @file src/compiler/index.js export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult {// Generate an abstract tree from template const ast = parse(template.trim(), options) if (options.optimize! > > optimize(ast, options)} // Render and staticRenderFns function generated by the tree options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } })Copy the code
CreateCompiler gets createCompilerCreator. CreateCompilerCreator takes a function as an argument. This function is Vue’s basic compiler function baseCompile. Render and staticRenderFns are used in the $mount function.
// @file src/compiler/create-compiler.js export function createCompilerCreator (baseCompile: Function): Function { return function createCompiler (baseOptions: CompilerOptions) { function compile ( template: string, options? : CompilerOptions ): CompiledResult { const finalOptions = Object.create(baseOptions) const errors = [] const tips = [] let warn = (msg, range, tip) => { (tip ? tips : errors).push(msg) } if (options) { // merge custom modules if (options.modules) { finalOptions.modules = (baseOptions.modules || []).concat(options.modules) } // merge custom directives if (options.directives) { finalOptions.directives = extend( Object.create(baseOptions.directives || null), options.directives ) } // copy other options for (const key in options) { if (key ! == 'modules' && key ! == 'directives') { finalOptions[key] = options[key] } } } finalOptions.warn = warn const compiled = baseCompile(template.trim(), finalOptions) // compiled = { // ast, // render: code.render, // staticRenderFns: code.staticRenderFns // } compiled.errors = errors compiled.tips = tips return compiled } return { compile, compileToFunctions: createCompileToFunctionFn(compile) } } }Copy the code
Compile createCompilerCreator (createCompilerCreator) createCompilerCreator (createCompilerCreator)
- Define finalOptions;
- Combine options.modules, options.directives, and copy the rest of options;
- Call the underlying compiler function to generate render and staticRenderFns as discussed in the previous code block;
- Finally, compiled is returned;
CreateCompilerCreator actually has two parts:
- The definition of the compile function;
- Return compile and compileToFunctions;
CompileToFunctions is defined in createCompileToFunctionFn, receives the compiler function as a parameter;
// @file src/compiler/to-function.js export function createCompileToFunctionFn (compile: Function): Const cache = object. create(null) return Function compileToFunctions (template: string, options? : CompilerOptions, vm? : Component ): CompiledFunctionResult { options = extend({}, // Get the key used by the cache const key = options.delimiters? String(options.delimiters) + template: If (cache[key]) {return cache[key]} // compile, Compiled = compile(template, Const res = {} const fnGenErrors = [] res. Render = createFunction(compiled. Render, fnGenErrors) res.staticRenderFns = compiled.staticRenderFns.map(code => { return createFunction(code, FnGenErrors)}) and return (cache[key] = res)}}Copy the code
Render and staticRenderFns are assigned to Vue options. A simple flowchart is drawn:
mountComponent
After getting the Render and staticRenderFns functions, let’s move on to the mountComponent code.
// @file src/core/instance/lifeCycle.js export function mountComponent (vm: Component, el: ? Element, hydrating? : boolean): Component { vm.$el = el if (! Vm. $options.render) {vm.$options.render = createEmptyVNode} // callHook function beforeMount callHook(vm, 'beforeMount') let updateComponent updateComponent = () => { const vnode = vm._render(); vm._update(vnode, hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && ! CallHook functions beforeUpdate callHook(vm, 'beforeUpdate')}}}, $vnode == null) {vm._isMounted = true. 'mounted') } return vm }Copy the code
Look at the code above:
- First of all, we can see from the above analysis that we have render;
- Call the hook function beforeMount;
- I defined the updateComponent function, which is the getter inside watcher;
- Create a new Watcher, the data changes and updateComponent is triggered. Before is called. Before calls the hook function beforeUpdate. The last parameter is true, vm._watcher is the current Watcher.
- Mounted;
The updateComponent function is called at new Watcher; Get the vNode by calling _render; Then call the _update function to render vNode onto the page.
_update
Call _render to get vNode before calling _update:
_render
// @file src/core/instance/render.js Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options if (_parentVnode) { vm.$scopedSlots = normalizeScopedSlots( _parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots ) } vm.$vnode = _parentVnode let vnode try { currentRenderingInstance = vm vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render`) vnode = vm._vnode } finally { currentRenderingInstance = null } if (Array.isArray(vnode) && vnode.length === 1) { vnode = vnode[0] } if (! (vnode instanceof VNode)) { vnode = createEmptyVNode() } vnode.parent = _parentVnode return vnode }Copy the code
The _render function is defined in the render file as described in the Vue declaration; Let’s look at implementation:
- Get render and _parentVnode from $options. Render is defined in the mount function at the beginning of the article.
- Judgment slot;
- $createElement = Vue; $createElement = Vue; $createElement = Vue;
- Finally return to vnode;
Update (_update);
Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const restoreActiveInstance = setActiveInstance(vm) vm._vnode = vnode if (! prevVnode) { vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false) } else { vm.$el = vm.__patch__(prevVnode, vnode) } restoreActiveInstance() if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // $vnode = parentVnode // $parent = parent if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } }Copy the code
Above is the source code of _update, the core function: patch, first analysis of the source code of _update;
- Variable VM definition, pointing to this;
- The variable prevEl refers to the current $el;
- PrevVNode refers to the previous vnode, _vnode;
- Set the current active instance to the current VM, get the setting function;
- Save the latest vnode as the previous vnode, so that you can obtain the previous vnode next time.
- For the first rendering, vm.patch(vm.$el, vnode, hydrating, false) is called; For diff rendering, vm.patch(prevVnode, vnode) is called;
- Call setup function;
- Set the previous element’s __vue__ to empty to free memory.
- Redefine __vue__ as the current instance of the current element;
- Check whether parentVnode and parent’s previous vnodes are the same. If so, assign the current EL to parent’s EL.
patch
Patch is a platform-related function. Let’s take a look at the exposed entry:
// @file src/platforms/web/runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })
Copy the code
CreatePatchFunction createPatchFunction createPatchFunction createPatchFunction createPatchFunction createPatchFunction
- NodeOps: platform-specific methods like appendChild, insertBefore, removeChild, createElement, and other native browser methods that encapsulate dom;
Modules: a combination of platform related modules and base modules, including ATTRs, class, Events, style, domProps, transtion, directives, ref, etc. Mainly the implementation of the installation, update and destruction of these instructions;
The createPatchFunction function is too long, so it finally returns the patch function. Let’s look at the source code of patch.
return function patch (oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false Const insertedVnodeQueue = [] // First, no oldVnode, If (isUndef(oldVnode)) {isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { const isRealElement = isDef(oldVnode.nodeType) if (! isRealElement && sameVnode(oldVnode, vnode)) { // dom diff patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { if (isRealElement) { // if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } } // oldVnode = emptyNodeAt(oldVnode) } // const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // create new node createElm( vnode, insertedVnodeQueue, // oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } // const insert = ancestor.data.hook.insert if (insert.merged) { // start at index 1 to avoid re-invoking component mounted hook for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }Copy the code
Parameter interpretation:
- OldVnode: The last updated vNode or el(DOM) node passed in when it was first generated;
- Vnode: vnode to be updated this time.
- Hydrating: Whether there are side effects;
- RemoveOnly: Indicates whether to delete only
Source code interpretation:
- If there is no VNode and an oldVnode exists, the current instance is destroyed, and the hook function destroy is called.
- Define variable isInitialPatch, whether it is the first comparison;
- Define an insertedVnodeQueue array to store objects that have been inserted into a VNode. These inserted hook functions will be called later.
- If oldVnode is undefined, set isInitialPatch to true and call createElm to create a new root element.
- If oldVnode has been defined, check the oldVnode nodeType. Vnode has no nodeType. If nodeType has a value, it is a native node.
- If it is a vnode, sameVnode is used to check whether it is the sameVnode. If yes, the patch operation is performed and patchVnode is invoked.
- If it is not a VNode, that is, a real node, it checks server rendering, etc., and finally calls emptyNodeAt to create a virtual node based on the node.
- Call createElm to create a new node;
- Tree traverses the ancestors, calling hooks like destroy, create, insert, etc.
- Delete the old node,
- Call insertHook;
This part of the code is not the key, focus on patchVnode and updateChildren part, let’s first look at the source of patchVnode:
// @file src/core/vdom/patch.js function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, If (oldVnode === vnode) {return} // If the vnode element is not null or undefined, If (isDef(vnode.elm) &&isdef (ownerArray)) {// Clone vNode vnode = ownerArray[index] = CloneVNode (vnode)} const elm = vnode.elm = oldvNode. elm let I // get the virtual DOM data of the vnode to be updated Const data = vnode.data // Call prepatch hook if (isDef(data) &&isdef (I = data.hook) &&isdef (I = i.patch)) { I (oldVnode, vnode)} // Current child const oldCh = oldvNode. children // new child of vnode const ch = vnode.children And vnode has _vnode; If (isDef(data) &&isPatchable (vnode)) {// Loop through the modules update hook function for (I = 0; i < cbs.update.length; If (isDef(I = data.hook) &&isdef (I = i.pdate)) I (oldVnode, Vnode)} // If the new vnode is not a text node if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh ! == ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, RemoveOnly)} else if (isDef(ch)) {if (isDef(oldvNode.text)); AddVnodes (ELM, null, ch, 0, ch. Length-1, InsertedVnodeQueue)} else if (isDef(oldCh)) {removeVnodes(oldCh, 0, oldCh) Oldch.length-1)} else if (isDef(oldvNode.text)) { Nodeops.settextcontent (elm, ")}} else if (oldvNode.text! == vnode.text) {// If it is a text node, compare it to see if it is the same, NodeOps. SetTextContent (elm, If (isDef(data)) {if (isDef(I = data.hook) &&isdef (I = i.postpatch)) I (oldVnode, vnode) } }Copy the code
The above is part of the source code of patchVnode, which has been annotated and can be read step by step. This is not code and will not be really involved in the development of the actual project, but it is one of the core, and the most core is the content of updateChildren.
updateChildren
// @file src/core/vdom/patch.js function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, vnodeToMove, refElm const canMove = ! RemoveOnly while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (isUndef(oldStartVnode)) {// No old start OldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left} else if (isUndef(oldEndVnode)) {// No old end oldEndVnode = oldCh[--oldEndIdx]} else if (sameVnode(oldStartVnode, newStartVnode)) { newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, NewEndVnode) {patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, NewEndVnode)) {// The farthest; Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) {// The nearest; Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) } if (isDef(newStartVnode.key)) { idxInOld = oldKeyToIdx[newStartVnode.key] } else { idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) } if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] } } if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { removeVnodes(oldCh, oldStartIdx, oldEndIdx) } }Copy the code
Take a look at the code above:
- I define a few variables, and then the while loop, that’s it; The but variables are particularly important;
-
- OldStartIdx is the index of the beginning of the old tree. OldEndIdx is the last index of the old tree; OldStartVnode and oldEndVnode correspond to the index.
-
- Now new, newStartIdx is the index of the start of the new tree; NewEndIdx is the last index of the new tree; NewStartVnode and newEndVnode correspond to the index.
-
- OldKeyToIdx: old tree. The key corresponding to the Vnode under the index is a map.
-
- IdxInOld: indicates the index of the new tree in the old tree.
-
- VnodeToMove: Vnode to be moved or inserted.
-
- RefElm: Stores elm that needs to be inserted into the VNode.
- Once the variable is defined, I’m going to iterate,
- OldStartVnode is judged first. If there is no oldStartVnode, oldStartIdx moves right by one unit and repeats the loop.
- If oldEndVnode is not present, oldEndIdx moves one unit to the left and repeats the loop.
- Compare the beginning of the old and new trees, oldStartVnode and newStartVnode, if the same, call patchVnode above, move the two startIdx right by one unit, repeat the loop;
- Compare the ends of the old and new trees, oldEndVnode and newEndVnode, if they are the same, call patchVnode above, move the two endIdx left one unit and repeat the loop;
- Compare old open and new tail, oldStartVnode and newEndVnode, if the same, call patchVnode above, and insert oldStartVnode in front of the next node of the old tree.
- Compare old tail and new open, oldEndVnode and newStartVnode, if the same, call patchVnode above, and insert oldEndVnode in front of the current start node of the old tree.
- The rest is the judgment of other cases, will not take IDX to operate;
- Get the keys of the old tree, get a map, and store it in oldKeyToIdx.
- If the current node newStartVnode of the new tree defines a key, the idxInOld location information is obtained from the map above. If the key is not defined, findIdxInOld is called to search for idxInOld information. Here, defining the key for an element is the most convenient and quick search.
- If idxInOld is undefined, newStartVnode does not exist in the old tree, createElm will be created.
- If idxInOld is found, that is, newStartVnode exists in the old tree and the location is idxInOld, then vnodeToMove is obtained according to the location, and vnodeToMove and newStartVnode are compared. If they are the same, call patchVnode, set idxInOld of the old tree to undefined, and insert vnodeToMove in front of oldStartVnode of the old tree.
- If they are found, but not identical, createElm will be created.
- At this point, the search for other cases ends, and newStartIdx moves one unit right, repeating the loop;
- OldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx;
To sum up, there are only three cases of breaking out of the cycle:
- The cycle is over;
- OldStartIdx > oldEndIdx, the old tree loop is finished, the new tree loop is not finished, at this point, I need to add the rest of the new tree, call addVnodes;
- NewStartIdx > newEndIdx, newStartIdx > newEndIdx, newStartIdx > newEndIdx, newStartIdx > newEndIdx, newStartIdx > newEndIdx, newStartIdx > newEndIdx, newStartIdx > newEndIdx, newStartIdx > newEndIdx.
This completes the Update Dren tutorial.
Dom diff sample
Suppose we have a DOM like this: [a, b, c, d]
The updated tree looks like this: [A, D,e, b]
Let’s look at dom Diff’s process;
- OldStartIdx = 0, newStartIdx = 0, oldEndIdx = 3, newEndIdx = 3; **[a, b, c, d]** continues the loop;
- OldStartIdx = 1, newStartIdx = 1, oldEndIdx = 3, newEndIdx = 3; [a, c, d, b] ++oldStartIdx, –newEndIdx;
- OldStartIdx = 2, newStartIdx = 1, oldEndIdx = 3, newEndIdx = 2; If the old d is the same as the new d, it moves the old D to the front of oldStartIdx, which is inserted into the second position, and becomes **[a, d, c, b]**, then –oldEndIdx, ++newStartIdx;
- OldStartIdx = 2, newStartIdx = 2, oldEndIdx = 2, newEndIdx = 2; OldKeyToIdx ={c: }, newStartvNode. key = e, idxInOld = undefined, oldKeyToIdx = undefined, createElm = e, oldStartVnode = e [a, d, e, c, b], then ++newStartIdx;
- OldStartIdx = 2, newStartIdx = 3, oldEndIdx = 2, newEndIdx = 2; OldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx] is false;
- Step 6: Call removeVnodes(oldCh, oldStartIdx, oldEndIdx) to remove the remaining parts of the old tree. Remove the old tree from the second position to the second position, which is to remove the c;
- [a, d, e, b]
“Said
At this point, the explanation of mount function is basically completed, in the DOM DIff of Vue, especially for key judgment which, key will be judged as the same, otherwise it is judged as different, so we in the development, remember to define key for child elements, improve performance.
Vue2 source code analysis is preliminary completion, from vUE declaration to vUE initialization, that is, the process of new Vue; At the same time, the call process of initState during initialization is explained in detail. A detailed interpretation of Observer, one of the cores of Vue, is made, and Watcher && Scheduler is also explained and analyzed separately. In Vue, the use of nextTick and comparison between macro tasks and micro tasks are also explained in detail. Finally, this article gives a rough explanation of the last mount part. There are also many small functions in patch, which are not explained here.
Read the source code is very boring, but for personal promotion is helpful, as the saying goes, “the book read a hundred times its meaning”, read the source code several times first, and finally in accordance with their own understanding of its interpretation and write out, more can deepen the impression, if not, welcome to point out.