1. Virtual DOM implementation principle
The realization principle of virtual DOM mainly includes the following three parts:
- Use JavaScript objects to simulate the real DOM tree and abstract the real DOM;
- diff algorithm — compare the differences between two virtual DOM trees;
- The pach algorithm — applies the difference of two virtual DOM objects to the real DOM tree.
The following details are reproduced
1. Truth DOM
and its analysis process
In this section, we mainly introduce the DOM
real parsing process, and by introducing its parsing process and existing problems, we will draw out why virtualization is needed DOM
. A picture is worth a thousand words, the following figure is webkit
the rendering engine workflow
All browser rendering engine workflows are roughly divided into 5 steps: create DOM
tree —> create Style Rules
—> build Render
tree —> layout Layout
—> draw Painting
.
- The first step is to build a DOM tree: use an HTML analyzer to analyze HTML elements and build a DOM tree;
- The second step is to generate a style sheet: use a CSS analyzer to analyze the inline style on CSS files and elements, and generate a style sheet for the page;
- The third step is to build the Render tree: associate the DOM tree with the style sheet to build a Render tree (Attachment). Each DOM node has an attach method that accepts style information and returns a render object (also known as renderer), and these render objects will eventually be built into a Render tree;
- The fourth step is to determine the node coordinates: according to the Render tree structure, determine a precise coordinate that appears on the display screen for each node on the Render tree;
- The fifth step is to draw the page: display the coordinates according to the Render tree and nodes, and then call the paint method of each node to draw them.
important point:
1. DOM
Does the construction of the tree start when the document is loaded? Building DOM
the tree is a gradual process. In order to achieve a better user experience, the rendering engine will display the content on the screen as soon as possible. It does not have to wait until the entire HTML
document is parsed before starting to build render
the tree and layout.
2. Render
The tree DOM
is CSS
built after the tree and the style sheet are built? These three processes are not completely independent when they are actually carried out, but there will be crossover, and they will be loaded, parsed, and rendered at the same time.
3. CSS
What are the points for attention in the analysis? CSS
The parsing is reversed from right to left, the more nested tags, the slower the parsing.
4. What is the realJS
price of the operation ? DOM
Using our traditional development mode, when native JS
or operatingJQ
, the browser will execute the process from beginning to end starting from building the DOM tree. DOM
In one operation, I need to update 10 DOM
nodes . DOM
After the browser receives the first request, it does not know that there are 9 more update operations, so it will execute the process immediately, and finally execute 10 times. For example, after the first calculation, immediately after the next DOM
update request, the coordinate value of this node will change, and the previous calculation is useless. Calculating DOM
node coordinate values, etc. are all wasted performance. Even if the computer hardware has been iteratively updated, the cost DOM
of is still expensive, and frequent operations will still cause page freezes, affecting user experience
2. Virtual-DOM
Basic
2.1, the benefits DOM
of
Virtual DOM
is designed to solve browser performance problems. As before, if there are 10 DOM
update , the virtual DOM
will not operate immediately DOM
, but save diff
the content to a local JS
object, and finally put this JS
object attch
on DOM
the tree at one time, and then perform subsequent operations , to avoid a large amount of unnecessary calculations. Therefore, the advantage of using JS
objects to simulate DOM
nodes is that all page updates can be reflected on JS
objects (virtual DOM
) first, and the speed of operating JS
objects is obviously faster. After the update is completed, the final JS
objects are mapped to real DOM
, to be drawn by the browser.
2.2. Algorithm implementation
2.2.1 JS
. Simulating DOM
trees with objects
(1) How JS
to simulate DOM
a tree with an object
For example a real DOM
node is as follows:
<div id="virtual-dom">
<p>Virtual DOM</p>
<ul id="list">
<li class="item">Item 1</li>
<li class="item">Item 2</li>
<li class="item">Item 3</li>
</ul>
<div>Hello World</div>
</div>
We use JavaScript
objects to represent DOM
nodes, and use the properties of the object to record the type, attributes, child nodes, etc. of the node.
element.js
Indicates that the node object code is as follows:
/**
* Element virdual-dom 对象定义
* @param {String} tagName - dom 元素名称
* @param {Object} props - dom 属性
* @param {Array<Element|String>} - 子节点
*/
function Element(tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
// dom 元素的 key 值,用作唯一标识符
if(props.key){
this.key = props.key
}
var count = 0
children.forEach(function (child, i) {
if (child instanceof Element) {
count += child.count
} else {
children[i] = '' + child
}
count++
})
// 子元素个数
this.count = count
}
function createElement(tagName, props, children){
return new Element(tagName, props, children);
}
module.exports = createElement;
According to the setting of element
the object , the above DOM
structure can be simply expressed as:
var el = require("./element.js");
var ul = el('div',{id:'virtual-dom'},[ el('p',{},['Virtual DOM']),
el('ul', { id: 'list' }, [ el('li', { class: 'item' }, ['Item 1']),
el('li', { class: 'item' }, ['Item 2']),
el('li', { class: 'item' }, ['Item 3'])
]),
el('div',{},['Hello World'])
])
Now ul
is the structureJavaScript
we represent with the object , and we output and view the corresponding data structure as follows:DOM
ul
(2) JS
Render DOM
the object represented by
But there is no such structure on the page. In the next step, we will introduce how to ul
render into the real DOM
structure on the page. The relevant rendering functions are as follows:
/**
* render 将virdual-dom 对象渲染为实际 DOM 元素
*/
Element.prototype.render = function () {
var el = document.createElement(this.tagName)
var props = this.props
// 设置节点的DOM属性
for (var propName in props) {
var propValue = props[propName]
el.setAttribute(propName, propValue)
}
var children = this.children || []
children.forEach(function (child) {
var childEl = (child instanceof Element)
? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
: document.createTextNode(child) // 如果字符串,只构建文本节点
el.appendChild(childEl)
})
return el
}
By looking at the above render
method will tagName
build a real DOM
node according to , then set the properties of this node, and finally recursively build our own child nodes.
We will add the built DOM
structure to the page body
, as follows:
ulRoot = ul.render();
document.body.appendChild(ulRoot);
In this way body
, there is a real DOM
structure in the page, and the effect is shown in the following figure:
2.2.2. Comparing the difference between two virtual DOM
trees — diff
algorithm
diff
The algorithm is used to compare the differences between two Virtual DOM
trees . If the complete comparison of two trees is required, diff
the time complexity of the algorithm is O(n^3)
. But in the front end, you rarely move DOM
elements , so Virtual DOM
will only compare elements of the same level, as shown in the figure below, div
will only div
compare , and the second level will only compare with the second level In contrast, the complexity of the algorithm can be achieved O(n)
.
(1) Depth-first traversal, recording differences
In the actual code, a depth-first traversal will be performed on the old and new trees, so that each node will have a unique tag:
During depth-first traversal, every time a node is traversed, the node is compared with the new tree. If there is a difference, it is recorded in an object.
// diff 函数,对比两棵树
function diff(oldTree, newTree) {
var index = 0 // 当前节点的标志
var patches = {} // 用来记录每个节点差异的对象
dfsWalk(oldTree, newTree, index, patches)
return patches
}
// 对两棵树进行深度优先遍历
function dfsWalk(oldNode, newNode, index, patches) {
var currentPatch = []
if (typeof (oldNode) === "string" && typeof (newNode) === "string") {
// 文本内容改变
if (newNode !== oldNode) {
currentPatch.push({ type: patch.TEXT, content: newNode })
}
} else if (newNode!=null && oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
// 节点相同,比较属性
var propsPatches = diffProps(oldNode, newNode)
if (propsPatches) {
currentPatch.push({ type: patch.PROPS, props: propsPatches })
}
// 比较子节点,如果子节点有'ignore'属性,则不需要比较
if (!isIgnoreChildren(newNode)) {
diffChildren(
oldNode.children,
newNode.children,
index,
patches,
currentPatch
)
}
} else if(newNode !== null){
// 新节点和旧节点不同,用 replace 替换
currentPatch.push({ type: patch.REPLACE, node: newNode })
}
if (currentPatch.length) {
patches[index] = currentPatch
}
}
From the above it can be concluded that, patches[1]
said p
, patches[3]
said ul
, and so on.
(2) Difference type
DOM
The types of discrepancies caused by actions include the following:
- Node replacement: the node has changed, for example,
div
replace the above withh1
; - Sequence exchange: move, delete, and add child nodes, such as the child nodes
div
above , exchange the order ofp
and ;ul
- Attribute change: modify the attributes of the node, such as deleting the
li
aboveclass
style class; - Text change: change the text content of the text node, for example, change the text content of the above
p
node to "Real Dom
";
Several difference types described above are defined in the code as follows:
var REPLACE = 0 // 替换原先的节点
var REORDER = 1 // 重新排序
var PROPS = 2 // 修改了节点的属性
var TEXT = 3 // 文本内容改变
(3) List comparison algorithm
The comparison algorithm of child nodes, for example p, ul, div
, the order of is changed div, p, ul
. How should this be compared? If they are compared sequentially at the same level, they will all be replaced. If p
the div
is tagName
different from , p
it will be replaced div
by . Eventually, all three nodes will be replaced, DOM
which can be very expensive. In fact, there is no need to replace the node, but only need to move through the node. We only need to know how to move.
Abstracting this problem is actually the minimum edit distance problem of strings ( Edition Distance
), the most common solution is Levenshtein Distance
a Levenshtein Distance
string metric that measures the difference between two character sequences, between two words Levenshtein Distance
is a The minimum number of single-character edits (insertions, deletions, or substitutions) required to convert a word into another word. Levenshtein Distance
It was invented by Soviet mathematician Vladimir Levenshtein in 1965. Levenshtein Distance
Also known as the edit distance ( Edit Distance
), solved by dynamic programming , the time complexity is O(M*N)
.
Definition: For two strings a、b
, their Levenshtein Distance
is :
Example: strings a
and b
, a=“abcde” ,b=“cabef”
according to the calculation formula given above, Levenshtein Distance
their calculation process is as follows:
In this demo
paper , the plug-in list-diff2
algorithm is used for comparison. The time complexity of the algorithm is great O(n*m)
. Although the algorithm is not the optimal algorithm, it is sufficient for routine operations on dom
elements . The specific implementation process of the algorithm will not be introduced in detail here. For the specific introduction of the algorithm, please refer to: github.com/livoras/lis…
(4) Example output
The two virtual DOM
objects are shown in the figure below, where ul1
represents the original virtual DOM
tree and ul2
represents the changed virtual DOM
tree
var ul1 = el('div',{id:'virtual-dom'},[ el('p',{},['Virtual DOM']),
el('ul', { id: 'list' }, [ el('li', { class: 'item' }, ['Item 1']),
el('li', { class: 'item' }, ['Item 2']),
el('li', { class: 'item' }, ['Item 3'])
]),
el('div',{},['Hello World'])
])
var ul2 = el('div',{id:'virtual-dom'},[ el('p',{},['Virtual DOM']),
el('ul', { id: 'list' }, [ el('li', { class: 'item' }, ['Item 21']),
el('li', { class: 'item' }, ['Item 23'])
]),
el('p',{},['Hello World'])
])
var patches = diff(ul1,ul2);
console.log('patches:',patches);
We look at the difference object between the two output virtual DOM
objects as shown in the figure below, we can get DOM
what changes have been made between the two virtual objects through the difference object, and then patches
change the original real DOM
structure , thus Change DOM
the structure of the page .
2.2.3. Apply the difference of two virtual DOM
objects to the real DOM
tree
(1) Depth-first traversal of DOM
the tree
Because the JavaScript
object render
has the same information and structure as the real DOM
tree . Therefore, we can also perform depth-first traversal on that DOM
tree . When traversing patches
, find out the difference between the currently traversed nodes from the object generated in step 2, as shown in the following related code:
function patch (node, patches) {
var walker = {index: 0}
dfsWalk(node, walker, patches)
}
function dfsWalk (node, walker, patches) {
// 从patches拿出当前节点的差异
var currentPatches = patches[walker.index]
var len = node.childNodes
? node.childNodes.length
: 0
// 深度遍历子节点
for (var i = 0; i < len; i++) {
var child = node.childNodes[i]
walker.index++
dfsWalk(child, walker, patches)
}
// 对当前节点进行DOM操作
if (currentPatches) {
applyPatches(node, currentPatches)
}
}
(2 DOM
) DOM
Operate the original tree
We perform different DOM
operations , for example, if the node is replaced, the node DOM
replacement operation is performed; if the node text is changed, the text replacement DOM
operation ; and sub-node rearrangement, attribute changes, etc. OperationDOM
, the related code is applyPatches
shown :
function applyPatches (node, currentPatches) {
currentPatches.forEach(currentPatch => {
switch (currentPatch.type) {
case REPLACE:
var newNode = (typeof currentPatch.node === 'string')
? document.createTextNode(currentPatch.node)
: currentPatch.node.render()
node.parentNode.replaceChild(newNode, node)
break
case REORDER:
reorderChildren(node, currentPatch.moves)
break
case PROPS:
setProps(node, currentPatch.props)
break
case TEXT:
node.textContent = currentPatch.content
break
default:
throw new Error('Unknown patch type ' + currentPatch.type)
}
})
}
(3) DOM structure changes
By applying the difference between the two DOM
objects to the first (previous) DOM
structure, we can see that DOM
the structure undergoes the expected change, as shown in the following figure:
2.3. Conclusion
The relevant code implementation has been put on github. Interested students can clone to run the experiment. The github address is: github.com/fengshi123/…
Virtual DOM
The algorithm mainly implements the above three steps to achieve:
-
Simulate a tree with
JS
an object—DOM
element.js
<div id="virtual-dom"> <p>Virtual DOM</p> <ul id="list"> <li class="item">Item 1</li> <li class="item">Item 2</li> <li class="item">Item 3</li> </ul> <div>Hello World</div> </div> 复制代码
-
Compare the differences of two virtual
DOM
trees—diff.js
-
Apply the difference of two dummy
DOM
objects to a realDOM
tree—patch.js
function applyPatches (node, currentPatches) { currentPatches.forEach(currentPatch => { switch (currentPatch.type) { case REPLACE: var newNode = (typeof currentPatch.node === 'string') ? document.createTextNode(currentPatch.node) : currentPatch.node.render() node.parentNode.replaceChild(newNode, node) break case REORDER: reorderChildren(node, currentPatch.moves) break case PROPS: setProps(node, currentPatch.props) break case TEXT: node.textContent = currentPatch.content break default: throw new Error('Unknown patch type ' + currentPatch.type) } }) }
3. Brief analysis ofVue
source codeVirtual-DOM
From the second chapter ( Virtual-DOM
Basic), we have Virtual DOM
mastered the definition, , and other processes ofDOM
rendering it into a real one , so the analysis of the source code in this chapter is also briefly analyzed according to these processes.VNode
diff
patch
Vue
3.1. VNode
Simulation DOM
tree
3.1.1, VNode
class brief analysis
Vue.js
In , Virtual DOM
use VNode
this Class
to describe, and it is defined in src/core/vdom/vnode.js
, as you can see from the following code block Vue.js
, Virtual DOM
the definition of in is more complicated, because it contains Vue.js
many features of . Vue.js
In fact, the middle Virtual DOM
is borrowed from the implementation of an open source library snabbdom , and then added some features Vue.js
of the middle.
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
}
Don't be scared by VNode
such , or grit your teeth to find out the meaning of each attribute. In fact, we only need to understand a few core key attributes, for example:
tag
The attribute isvnode
the label attribute of thisdata
The attribute contains the , , and bound eventsdom
on the node after the final rendering into a real nodeclass
attribute
style
children
attribute isvnode
a child node oftext
attribute is a text attributeelm
The attribute is thevnode
corresponding realdom
nodekey
Attributes arevnode
markers thatdiff
can improvediff
efficiency in the process
3.1.2. Source code creation VNode
process
(1) Initialize vue
We are instantiating an vue
instance , new Vue( )
that is, actually src/core/instance/index.js
executing Function
the function defined in .
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)
}
By looking at Vue
the function
, we know Vue
that can only new
be initialized by the keyword, and then call this._init
the method , which is src/core/instance/init.js
defined in .
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// 省略一系列其它初始化的代码
if (vm.$options.el) {
console.log('vm.$options.el:',vm.$options.el);
vm.$mount(vm.$options.el)
}
}
(2) Vue
instance mount
Vue
is mounted through $mount
the instance method dom
, below we compiler
analyze mount
the implementation of the version, and the relevant source code is defined in the directory src/platforms/web/entry-runtime-with-compiler.js
file:.
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
// 省略一系列初始化以及逻辑判断代码
return mount.call(this, el, hydrating)
}
We found that in the end, the method on the original prototype is called and mounted , and the method $mount
on the original prototype is defined in .$mount
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)
}
We found that $mount
the method actually calls mountComponent
the method, which is defined in src/core/instance/lifecycle.js
the file
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 省略一系列其它代码
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
// 生成虚拟 vnode
const vnode = vm._render()
// 更新 DOM
vm._update(vnode, hydrating)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
return vm
}
As can be seen from the above code, mountComponent
the core is to instantiate a rendering first Watcher
, and the method will be called in its callback function updateComponent
. In this method, the calling vm._render
method first generates a virtual Node, and finally calls vm._update
the update DOM
.
(3) Create a virtual Node
Vue
The _render
method is a private method of the instance, which is used to render the instance as a virtual Node
. Its definition is in src/core/instance/render.js
the file:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
let vnode
try {
// 省略一系列代码
currentRenderingInstance = vm
// 调用 createElement 方法来返回 vnode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`){}
}
// set parent
vnode.parent = _parentVnode
console.log("vnode...:",vnode);
return vnode
}
Vue.js
Created using _createElement
the method VNode
, which is defined in src/core/vdom/create-elemenet.js
:
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 省略一系列非主线代码
if (normalizationType === ALWAYS_NORMALIZE) {
// 场景是 render 函数不是编译生成的
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 场景是 render 函数是编译生成的
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// 创建虚拟 vnode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
_createElement
The method has 5 parameters, context
which represent the context of the VNode, which is Component
a type; tag
represent the label, which can be a string or one Component
; represent the data
data of the VNode, which is a VNodeData
type, and flow/vnode.js
its definition can be found in children
The child node of the current VNode, which is of any type, needs to be standardized as a standard VNode
array ;
3.1.3. Instance view
In order to see more intuitively how Vue
the code VNode
is represented by a class, we have a deeper understanding through the conversion of an instance.
For example, to instantiate a Vue
instance :
var app = new Vue({
el: '#app',
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app',
class: "class_box"
},
}, this.message)
},
data: {
message: 'Hello Vue!'
}
})
We print out its corresponding VNode
representation :
3.2. diff
Process
3.2.1. CallVue.js
logic of source codediff
Vue.js
The source code instantiates one watcher
, and this ~ is added to the dependencies of the variables bound in the template. model
Once responsive data in changes, dep
the array maintained by these responsive data will call dep.notify()
the method to complete all dependency traversal The work performed, which includes the update of the view, that is, the invocation of updateComponent
the method . watcher
and updateComponent
methods are defined in src/core/instance/lifecycle.js
the file.
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 省略一系列其它代码
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
// 生成虚拟 vnode
const vnode = vm._render()
// 更新 DOM
vm._update(vnode, hydrating)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
return vm
}
Completing the update of the view is actually calling vm._update
the method. The first parameter received by this method is just generated , and the method Vnode
to call is defined in .vm._update
src/core/instance/lifecycle.js
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) {
// 第一个参数为真实的node节点,则为初始化
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 如果需要diff的prevVnode存在,那么对prevVnode和vnode进行diff
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
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
}
}
The most critical method in this method is vm.__patch__
the method , which is also virtual-dom
the most core method in the whole. It mainly completes the process ofprevVnode
and and opens according to the nodes . Finally, a new real node and the update of the view is completed.vnode
diff
vdom
patch
dom
Next, let's look at the logical process vm.__patch__
of , vm.__patch__
the method is defined src/core/vdom/patch.js
in .
function patch (oldVnode, vnode, hydrating, removeOnly) {
......
if (isUndef(oldVnode)) {
// 当oldVnode不存在时,创建新的节点
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// 对oldVnode和vnode进行diff,并对oldVnode打patch
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
......
}
}
复制代码
In patch
the method , we see that there are two situations, one is that when oldVnode
does not exist, a new node will be created; the other is that it already exists oldVnode
, oldVnode
then the process of and will be vnode
performed diff
on and . Thepatch
method will be called in the process to compare the basic attributes of the two incoming , only when the basic attributes are the same, it is considered that the two are only partially updated, and then the two will be updated , if 2 If there is an inconsistency in the basic attributes of a , then the process of is skipped directly, and a real node is created , and the old node .patch
sameVnode
vnode
vnode
vnode
diff
vnode
diff
vnode
dom
dom
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)
)
}
复制代码
diff
The process is mainly carried out by calling patchVnode
the method :
function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
......
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
// 如果vnode没有文本节点
if (isUndef(vnode.text)) {
// 如果oldVnode的children属性存在且vnode的children属性也存在
if (isDef(oldCh) && isDef(ch)) {
// updateChildren,对子节点进行diff
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
// 如果oldVnode的text存在,那么首先清空text的内容,然后将vnode的children添加进去
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 删除elm下的oldchildren
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// oldVnode有子节点,而vnode没有,那么就清空这个节点
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 如果oldVnode和vnode文本属性不同,那么直接更新真是dom节点的文本元素
nodeOps.setTextContent(elm, vnode.text)
}
......
}
From the above code we know that,
diff
There are several situations in the process, which are child nodes ofoldCh
and child nodes of :oldVnode
ch
Vnode
- First, judge the text node, if so
oldVnode.text !== vnode.text
, then directly replace the text node; - In
vnode
the case of no text node, enter the child nodediff
; - When
oldCh
bothch
exist and are not the same, call is performed on theupdateChildren
child nodediff
; - If
oldCh
does not exist,ch
exists, first clear the text nodeoldVnode
of , and calladdVnodes
the method toch
add it toelm
the realdom
node ; - If
oldCh
exists ,ch
does not exist, thenelm
deleteoldCh
the child node under the real node; - If
oldVnode
there is a text node butvnode
not, then the text node is cleared.
3.2.2. Sub-node diff
process analysis
(1) Vue.js
Source code
Here we focus on analyzing updateChildren
the method, which is also the most important link in the whole diff
process . The following is Vue.js
the source code process of . In order to understand diff
the process , we give relevant schematic diagrams to explain it.
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// 为oldCh和newCh分别建立索引,为之后遍历的依据
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
// 直到oldCh或者newCh被遍历完后跳出循环
while (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)) {
patchVnode(oldStartVnode, 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)) { // 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)) { // 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)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: 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(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
diff
Before starting to traverse , first assign a andoldCh
to each of and as indexes for traversal, and when or is traversed (the condition for traversing is or ), stop the process of and . Next, let's take a look at the whole ( the case without in the node attribute).newCh
startIndex
endIndex
oldCh
newCh
oldCh
newCh
startIndex >= endIndex
oldCh
newCh
diff
diff
key
(2) key
No diff
process
We explain the above code process through the following diagram:
(2.1) First start the comparison from the first node, no matter whether it is oldCh
or newCh
the start or end node does not exist sameVnode
, and there is no key
mark , so after the first round of diff
is completed, newCh
the startVnode
is added oldStartVnode
in front of , Simultaneously newStartIndex
move forward one bit;
(2.2) diff
In , it is satisfied sameVnode(oldStartVnode, newStartVnode)
, so the two vnode
are carried out diff
, and finally patch
hit oldStartVnode
on , and both oldStartVnode
and newStartIndex
move forward one bit;
(2.3) diff
In , if it is satisfied, then first perform sameVnode(oldEndVnode, newStartVnode)
on oldEndVnode
and , and perform on , and complete the shift operation, and finally move one bit forward and one bit backward;newStartVnode
diff
oldEndVnode
patch
oldEndVnode
newStartIndex
oldStartVnode
(2.4) diff
In , the process is the same as step 3;
(2.5) diff
In , the same as process 1;
(2.6) After the traversal process ends, newStartIdx > newEndIdx
, indicating that oldCh
there are , then these redundant nodes need to be deleted in the end.
(3) key
Existing diff
process
vnode
In key
the case of without , each diff
round is compared 起始
with the node until or is traversed. And when the attribute is introduced for , in each round of the process , when the and nodes are not found , then judge whether there is in the attribute of and whether the corresponding node is found in :结束
oldCh
newCh
vnode
key
diff
起始
结束
sameVnode
newStartVnode
key
oldKeyToIndx
- If this does not exist
key
, then create thisnewStartVnode
as a new node and insert it intoroot
the child node of the original ; - If there is this
key
, then take out what existsoldCh
inkey
thisvnode
, and thendiff
proceed ;
Through the above analysis, after vdom
adding key
the attribute to , diff
in the process of traversing , when the search for the starting point , the end point , and are still unable to match, it will be used as the unique identifier to proceed , so that the efficiency .diff
key
diff
diff
The process with Key
the attribute can be seen in the following figure:vnode
diff
(3.1) First start the comparison from the first node, whether it is oldCh
or newCh
the start or end node does not exist sameVnode
, but the node attribute is key
marked , and then oldKeyToIndx
find the corresponding node in , so that diff
after oldCh
round B节点
of Removed, but newCh
the on propertyB节点
on the keeps a reference to the on .elm
oldCh
B节点
elm
(3.2) diff
In , it is satisfied sameVnode(oldStartVnode, newStartVnode)
, so the two vnode
are carried out diff
, and finally patch
hit oldStartVnode
on , and both oldStartVnode
and newStartIndex
move forward one bit;
(3.3) diff
In , if it is satisfied, then first perform sameVnode(oldEndVnode, newStartVnode)
on oldEndVnode
and , and perform on , and complete the shift operation, and finally move forward by one bit and move back by one bit;newStartVnode
diff
oldEndVnode
patch
oldEndVnode
newStartIndex
oldStartVnode
(3.4) In the fourth round diff
, the process is the same as step 2;
(3.5) In the fifth round diff
, because oldStartIndex
is already oldEndIndex
, the remaining Vnode
queue inserted into the queue at the end.
3.3. patch
Process
Through diff
the process , we will see that nodeOps
the related methods operate on the real DOM
structure , nodeOps
which is defined in src/platforms/web/runtime/node-ops.js
, which is a basic DOM
operation , and will not be introduced in detail here.
export function createElementNS (namespace: string, tagName: string): Element {
return document.createElementNS(namespaceMap[namespace], tagName)
}
export function createTextNode (text: string): Text {
return document.createTextNode(text)
}
export function createComment (text: string): Comment {
return document.createComment(text)
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
export function removeChild (node: Node, child: Node) {
node.removeChild(child)
}
复制代码
3.4. Summary
Through the brief analysis of the first three sections, we have completed the analysis of how to render the template and data into the DOM
final . We can see Vue
the whole process from initialization to final rendering more intuitively through the figure below.