Writing in the front
Since I am very interested in vue. js, and the technology stack I work in is also vue. js, I have spent some time studying the source code of vue. js in these months, and made a summary and output. The original address of this article: github.com/answershuto… . In the process of learning, I added the Chinese annotation github.com/answershuto… , hope can be helpful to other want to learn Vue source partners. There may be some deviation in understanding, please mention the issue to point out that we should learn together and make progress together.
VNode
In the slash-and-burn era, we needed to manipulate the DOM directly in each event method to modify the view. But when the application is large, it becomes difficult to maintain.
Can we abstract the real DOM tree into an abstract tree composed of JavaScript objects, and then transform the abstract tree into a real DOM and redraw it on the page after modifying the data of the abstract tree? Thus comes the virtual DOM, which is a layer of abstraction from the real DOM and uses properties to describe the properties of the real DOM. When it changes, it changes the view.
But this JavaScript manipulation of the DOM to redraw the entire view layer is quite performance intensive, so can we just update it with changes each time? Therefore, viee. js abstracts THE DOM into a virtual DOM tree with JavaScript objects as nodes, and uses VNode nodes to simulate the real DOM. Operations such as node creation, node deletion, and node modification can be performed on the abstract tree, and the real DOM is not required in the process. You only need to manipulate JavaScript objects, which greatly improves performance. After modification, diff algorithm is used to get some minimum units to be modified, and then the view of these small units is updated. Doing so reduces a lot of unnecessary DOM manipulation and greatly improves performance.
Vue uses such an abstract node, VNode, which is a layer of abstraction from the real Dom and does not depend on a certain platform. It can be the browser platform or WEEX, and even the Node platform can also create, delete, modify and other operations on such an abstract Dom tree, which also provides the possibility of front and back end isomorphism.
For details about VNode, see VNode.
Modify the view
As you know, Vue changes views through data binding. When a data is modified, the set method causes the Dep in the closure to call notify to all subscribers Watcher. Watcher executes vm._update(vm._render(), hydrating) using the get method.
Look at the _update method here
Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) {
const vm: Component = this
/* If the component is already mounted, it means that this step is an update process, triggering the beforeUpdate hook */
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')}const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
/* Back end rendering vue.prototype. __patch__ is used as an entry */
if(! prevVnode) {// initial render
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// update __vue__ reference
/* Update the new instance object's __vue__*/
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}Copy the code
The first argument to the _update method is a VNode object, which is internally patched with the old VNode object.
What is patch?
patch
Patch compares the old and new VNodes, and then changes the view in the smallest unit according to the comparison result, instead of redrawing the whole view according to the new VNode. The core of Patch lies in the DIff algorithm, which can efficiently compare changes of vitURL DOM and obtain changes to modify views.
So how does Patch work?
Firstly, let’s talk about patch’s core DIff algorithm. Diff algorithm is a way of comparing tree nodes of the same layer rather than searching and traversing the tree layer by layer, so the time complexity is only O(n), and it is a very efficient algorithm.
The two graphs represent the patch process of the old VNode and the new VNode. They only compare vnodes of the same level to get the changes (the blocks with the same color in the second graph represent the VNode nodes that are compared with each other), and then modify the changed view, so it is very efficient.
Let’s look at the code for Patch.
/* Return value of the createPatchFunction, a patch function */
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
/* If the vnode does not exist, call the destruct hook
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
/* if oldVnode is not defined, create a new node */
isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
/* Indicates whether the old VNode has nodeType*/
/*Github:https://github.com/answershuto*/
const isRealElement = isDef(oldVnode.nodeType)
if(! isRealElement && sameVnode(oldVnode, vnode)) {// patch existing root node
/* Modify the existing node */ when it is the same node
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
/* If the old VNode is a rendered element on the server, hydrating is true*/
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
/* Need to merge into the real DOM */
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
/* Call insert hook */
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if(process.env.NODE_ENV ! = ='production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.')}}// either not server-rendered, or hydration failed.
// create an empty node and replace it
/* If not server-side rendering or merging into the real DOM fails, create an empty VNode to replace it */
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
/* Replace existing element */
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
if (isDef(vnode.parent)) {
// component root element replaced.
// update parent placeholder node element, recursively
/* The root node of the component is replaced and the parent node element*/ is iterated
let ancestor = vnode.parent
while (ancestor) {
ancestor.elm = vnode.elm
ancestor = ancestor.parent
}
if (isPatchable(vnode)) {
/* Call the create callback */
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode.parent)
}
}
}
if (isDef(parentElm)) {
/* Remove the old node */
removeVnodes(parentElm, [oldVnode], 0.0)}else if (isDef(oldVnode.tag)) {
/*Github:https://github.com/answershuto*/
/* Call the destroy hook */
invokeDestroyHook(oldVnode)
}
}
}
/* Call insert hook */
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}Copy the code
It is not difficult to find from the code that patchVnode is performed only when oldVnode and vnode are in sameVnode, that is, the process of patchVnode is performed only when the new and oldVnode nodes are determined to be the same node; otherwise, a new DOM is created and the old DOM is removed.
What is a sameVnode?
sameVnode
Let’s take a look at the sameVnode implementation.
/* To determine whether two vnodes are the same, the following conditions must be met: same key, same tag (the name of the tag of the current node), same isComment (whether it is a comment node), same data (the object corresponding to the current node, contains specific data information, is a VNodeData type, When the tag is , the type must be the same */
function sameVnode (a, b) {
return (
a.key === b.key &&
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
)
}
// Some browsers do not support dynamically changing type for <input>
// so they need to be treated as different nodes
Some browsers do not support dynamically changing types, so they are treated as different types */
function sameInputType (a, b) {
if(a.tag ! = ='input') return true
let i
const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
return typeA === typeB
}Copy the code
If the tag, key, and isComment of two VNodes are the same, and data is defined or not, and the label is input, the type must be the same. In this case, the two VNodes are considered samevnodes and can be directly operated as patchVnode.
patchVnode
Let’s look at the code of patchVnode first.
/*patch VNode */
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
/* If two vnodes are the same, return */
if (oldVnode === vnode) {
return
}
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
/* If the new VNode is static and its key is the same, and the new VNode is clone or marked once, then replace elm and componentInstance. * /
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
vnode.elm = oldVnode.elm
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
/* I = data.hook. Prepatch, if present, see "./ creation-component componentVNodeHooks". * /
i(oldVnode, vnode)
}
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
/* Call the update callback and update hook */
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
/* if the VNode has no text */
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
/* If both the old and new nodes have children, diff the children nodes and call updateChildren*/
if(oldCh ! == ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) }else if (isDef(ch)) {
/* If the old node has no children and the new node has children, clean the elm text and add children */ to the current node
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, ' ')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
/* If the new node has no children and the old node has children, remove all ele children */
removeVnodes(elm, oldCh, 0, oldCh.length - 1)}else if (isDef(oldVnode.text)) {
/* If the new node does not exist, the text */ will be removed
nodeOps.setTextContent(elm, ' ')}}else if(oldVnode.text ! == vnode.text) {/* If the new node is different from the old node, replace the text */
nodeOps.setTextContent(elm, vnode.text)
}
/* Call the Postpatch hook
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}Copy the code
The rules for patchVnode are as follows:
If the new VNode is static and its key is the same, and the new VNode is clone or marked once (marked v-once, only once), then only need to replace elm and componentInstance.
2. Both the new and old nodes have children nodes, diff is performed on the child nodes and updateChildren is called, which is also the core of DIFF.
3. If the old node has no child nodes and the new node has child nodes, clear the text of the DOM of the old node and add a child node to the current DOM node.
4. If the new DOM node has no children and the old DOM node has children, remove all the children of the DOM node.
5. When the old and new nodes have no child nodes, only text replacement.
updateChildren
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, elmToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
constcanMove = ! removeOnlywhile (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
/* In the first four cases, when the key is specified, the VNode is determined to be the same. Then, direct patchVnode is used to compare the two nodes of oldCh and newCh. 2*2=4 cases */
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
/* Create a hash table where the key corresponds to the old VNode key (this is only generated when undefined first time). For example, childre looks like this: [{xx: xx, key: 'key0'}, {xx: Xx, key: 'key1'}, {xx: xx, key: 'key2'}] beginIdx = 0 endIdx = 2 Result {key0:0, key1:1, key2:2} */
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
NewStartVnode Returns the idxInOld of the new VNode if the new VNode has a key and the key can be found in oldVnode */
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
if (isUndef(idxInOld)) { // New element
/*newStartVnode does not have a key or the key is not found in the old node to create a new node */
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
/* Get the old node with the same key */
elmToMove = oldCh[idxInOld]
/* istanbul ignore if */
if(process.env.NODE_ENV ! = ='production' && !elmToMove) {
/* If elmToMove does not exist, it indicates that a new node has been placed in the DOM of this key before, indicating that there may be duplicate keys, and ensuring that the item has a unique key value */ when v-for is used
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.')}if (sameVnode(elmToMove, newStartVnode)) {
/*Github:https://github.com/answershuto*/
/* Run patchVnode*/ if the new VNode is the same as the obtained VNode with the same key
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
/* If there is a new node with the same key as the patchVnode, it will indicate that there is a duplicate key*/
oldCh[idxInOld] = undefined
/* canMove can be inserted directly in front of oldStartVnode */
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
// same key but different element. treat as new element
/* Create a new VNode if the new VNode is not a sameVNode with the same key as the VNode found (e.g. with a different tag or input tag of a different type) */
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
}
if (oldStartIdx > oldEndIdx) {
OldStartIdx > oldEndIdx = oldStartIdx > oldEndIdx = oldStartIdx > oldEndIdx = oldStartIdx > oldEndIdx = oldStartIdx > oldEndIdx = oldStartIdx > oldEndIdx
refElm = isUndef(newCh[newEndIdx + 1])?null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
NewStartIdx > newEndIdx = newStartIdx > newEndIdx = newStartIdx > newEndIdx = newStartIdx > newEndIdx
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}Copy the code
Looking directly at the source code may be difficult to filter out the relationships, but let’s take a look at the diagram.
First, there is a variable marker on both the left and right sides of the new and old VNode nodes, and these variables will converge to the middle during the traversal. The loop ends when oldStartIdx <= oldEndIdx or newStartIdx <= newEndIdx.
Mapping between indexes and vNodes: oldStartIdx => oldStartVnode oldEndIdx => oldEndVnode newStartIdx => newStartVnode newEndIdx => newEndVnode
In the traversal, if the key exists and sameVnode is satisfied, the DOM node is reused, otherwise a new DOM node is created.
First of all, oldStartVnode and oldEndVnode are compared with newStartVnode and newEndVnode in pairs, and there are altogether 2*2=4 comparison methods.
SameVnode (oldStartVnode, newStartVnode) or sameVnode(oldEndVnode, newEndVnode) Perform patchVnode on the VNode.
If oldStartVnode and newEndVnode satisfy sameVnode, it is sameVnode(oldStartVnode, newEndVnode).
OldStartVnode has run behind oldEndVnode. When running patchVnode, you need to move the real DOM node to the back of oldEndVnode.
If oldEndVnode and newStartVnode satisfy sameVnode, it is sameVnode(oldEndVnode, newStartVnode).
This indicates that oldEndVnode runs in front of oldStartVnode, and the real DOM node moves in front of oldStartVnode during patchVnode.
If none of the above is true, createKeyToOldIdx will give you an oldKeyToIdx containing a hash table with the old VNode key and the value of the corresponding index sequence. From this hash table, you can find out if there are old VNode nodes with the same key as newStartVnode, if sameVnode is also satisfied, PatchVnode moves the real DOM (elmToMove) to the front of the real DOM of oldStartVnode.
It is also possible that newStartVnode cannot find a consistent key in the old VNode, or that even if the key is the same, it is not a sameVnode. In this case, createElm will be called to create a new DOM node.
At this point, the loop is over, leaving us with an extra or insufficient number of real DOM nodes to deal with.
When oldStartIdx > oldEndIdx is finished, the old VNode has been traversed, but the new VNode has not been traversed. Add the remaining vNodes to the real DOM. Call addVnodes (call createElm interface in batches to add these vNodes to the real DOM).
2. Similarly, when newStartIdx > newEndIdx, the new VNode is traversed, but the old VNode is left, indicating that the real DOM nodes are redundant and need to be removed from the document. In this case, call removeVnodes to remove these redundant real DOM nodes.
DOM manipulation
Since Vue uses the virtual DOM, the virtual DOM can operate on any platform that supports JavaScript language. For example, the current browser platform or WEEX supported by Vue is consistent in the implementation of the virtual DOM. So how does the virtual DOM finally map to the real DOM nodes?
Vue do a layer of adaptation layer for platform, platform/browser platforms/web/runtime/node – ops. Js and weex platform/platforms/weex/runtime/node – ops. Js. Different platforms provide the same interface externally through the adaptation layer. When the virtual DOM operates the real DOM node, it only needs to call the interfaces of these adaptation layers. However, the internal implementation does not need to be concerned, as it will change according to the change of the platform.
Now there’s another problem. We’re just mapping the virtual DOM to the real DOM. How do you add attr, class, style and other DOM attributes to the DOM?
This depends on the life hook of the virtual DOM. The virtual DOM provides the following hook functions, which are called at different times.
const hooks = ['create'.'activate'.'update'.'remove'.'destroy']
/ * build CBS callback function, the web platform/platforms/web/runtime modules * /
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}Copy the code
Similarly, there will be different implementations based on different platforms, so let’s take the Web platform as an example. Web platform of hook function/platforms/Web/runtime/modules. It operates on the ATTr, class, props, events, style, and transition DOM properties.
In the case of ATTR, the code is simple.
/* @flow */
import { isIE9 } from 'core/util/env'
import {
extend,
isDef,
isUndef
} from 'shared/util'
import {
isXlink,
xlinkNS,
getXlinkProp,
isBooleanAttr,
isEnumeratedAttr,
isFalsyAttrValue
} from 'web/util/index'
Update attr * / / *
function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) {
/* if both the old and new VNode nodes have no attr attribute, return */
if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) {
return
}
let key, cur, old
/* Dom instance of VNode */
const elm = vnode.elm
/* attr*/ of the old VNode
const oldAttrs = oldVnode.data.attrs || {}
/* attr*/ of the new VNode
let attrs: any = vnode.data.attrs || {}
// clone observed objects, as the user probably wants to mutate it
/* Deep copy */ if the attr of the new VNode has __ob__ (indicating that the attr has been processed by the Observe)
if (isDef(attrs.__ob__)) {
attrs = vnode.data.attrs = extend({}, attrs)
}
/* Iterates over attr. If not, replace */
for (key in attrs) {
cur = attrs[key]
old = oldAttrs[key]
if(old ! == cur) { setAttr(elm, key, cur) } }// #4391: in IE9, setting type can reset value for input[type=radio]
/* istanbul ignore if */
if(isIE9 && attrs.value ! == oldAttrs.value) { setAttr(elm,'value', attrs.value)
}
for (key in oldAttrs) {
if (isUndef(attrs[key])) {
if (isXlink(key)) {
elm.removeAttributeNS(xlinkNS, getXlinkProp(key))
} else if(! isEnumeratedAttr(key)) { elm.removeAttribute(key) } } } }/ * attr * /
function setAttr (el: Element, key: string, value: any) {
if (isBooleanAttr(key)) {
// set attribute for blank value
// e.g. <option disabled>Select one</option>
if (isFalsyAttrValue(value)) {
el.removeAttribute(key)
} else {
el.setAttribute(key, key)
}
} else if (isEnumeratedAttr(key)) {
el.setAttribute(key, isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true')}else if (isXlink(key)) {
if (isFalsyAttrValue(value)) {
el.removeAttributeNS(xlinkNS, getXlinkProp(key))
} else {
el.setAttributeNS(xlinkNS, key, value)
}
} else {
if (isFalsyAttrValue(value)) {
el.removeAttribute(key)
} else {
el.setAttribute(key, value)
}
}
}
export default {
create: updateAttrs,
update: updateAttrs
}Copy the code
Attr simply updates the ATTR property of the DOM when the create and Update hooks are called.
about
Author: dye mo
Email: [email protected] or [email protected]
Github: github.com/answershuto
Blog: answershuto. Making. IO /
Zhihu column: zhuanlan.zhihu.com/ranmo
The Denver nuggets: juejin. Im/user / 289926…
OsChina:my.oschina.net/u/3161824/b…
Reproduced please indicate the source, thank you.
Welcome to follow my public account