[TOC]
*****
## 1 vdom意義
### 1-1 傳統界面操作
>[info] (1)傳統web界面操作中需要使用js操作DOM,
隨著應用程序的復雜,dom操作復雜度提升。
為了有效的組織這種復雜操作,提出了mvc,mvvm等結果
### 1-2 vdom的意義
>[info] (2)在mvvm中通過數據綁定可以實現視圖與數據的互動效果
不需要手動更新頁面。只需要在模板中聲明視圖組件和綁定數據。
雙向綁定引擎vm可以自動實現數據與視圖的同步更新
>[info] (3)mvvm只是簡化了數據與視圖的關系。為了簡化模板引擎渲染,
可以使用vdom。生成新的視圖替換舊的視圖。
>[info] (4)vdom的核心是維護數據與視圖的關系
## 2 vdom思路
>[info] (1)原生dom包含屬性較多。
直接操作dom可能導致頁面重排,影響渲染性能
可以將dom解析為js對象,操作js對象則簡單
~~~
;模板
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>
;js對象
var element = {
tagName: 'ul', // 節點標簽名
props: { // DOM的屬性,用一個對象存儲鍵值對
id: 'list'
},
children: [ // 該節點的子節點
{tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
]
}
~~~
>[info] (2) 這樣就可以將數據的變化直接修改js對象,
對比修改后js的對象,記錄下需要對頁面真正的dom操作。
然后將其應用到真正的dom樹,實現頁面的更新
視圖的結構是整個全新渲染,最后操作dom只是修改局部
>[info] (3) 核心步驟如下
~~~
1 將dom樹轉換為js對象結構,生成真正dom樹,插入到文檔中
2 數據發生變化后,重新生成虛擬dom樹,進行比較
3 將比較結果保存到dom樹上
~~~
>[info] (4) 可以將js對象看做真實dom的緩存部分。
## 3 vdom之element
>[info] 使用js對象記錄dom節點,只需要記錄節點類型,屬性和子節點
~~~
;element.js
function Element (tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
}
module.exports = function (tagName, props, children) {
return new Element(tagName, props, children)
}
~~~
>[info] element.js簡單使用
~~~
var el = require('./element')
var ul = el('ul', {id: 'list'}, [
el('li', {class: 'item'}, ['Item 1']),
el('li', {class: 'item'}, ['Item 2']),
el('li', {class: 'item'}, ['Item 3'])
])
~~~
>[info] element的生成真正dom
~~~
Element.prototype.render = function () {
var el = document.createElement(this.tagName) // 根據tagName構建
var props = this.props
for (var propName in props) { // 設置節點的DOM屬性
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
}
~~~
>[info] 添加渲染結果到文檔
~~~
var ulRoot = ul.render()
document.body.appendChild(ulRoot)
~~~
## 4 vdom之diff
>[info] 對比兩個樹的diff是vdom算法的核心之一,
只是對同層的元素進行對比

>[info] 對新舊兩個樹遍歷,記錄差異。

>[info] 遍歷節點把節點對比信息存儲到對象
~~~
// diff 函數,對比兩棵樹
function diff (oldTree, newTree) {
var index = 0 // 當前節點的標志
var patches = {} // 用來記錄每個節點差異的對象
dfsWalk(oldTree, newTree, index, patches)
return patches
}
// 對兩棵樹進行深度優先遍歷
function dfsWalk (oldNode, newNode, index, patches) {
// 對比oldNode和newNode的不同,記錄下來
patches[index] = [...]
diffChildren(oldNode.children, newNode.children, index, patches)
}
// 遍歷子節點
function diffChildren (oldChildren, newChildren, index, patches) {
var leftNode = null
var currentNodeIndex = index
oldChildren.forEach(function (child, i) {
var newChild = newChildren[i]
currentNodeIndex = (leftNode && leftNode.count) // 計算節點的標識
? currentNodeIndex + leftNode.count + 1
: currentNodeIndex + 1
dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍歷子節點
leftNode = child
})
}
~~~
>[info] 上面的記錄信息
~~~
patches[0] = [{difference}, {difference}, ...] // 用數組存儲新舊節點的不同
~~~
同理p是patches[1],ul是patches[3],類推。
>[info] 差異類型
對于節點的對比結果可能分為以下幾種類型
替換掉原來的節點,
移動刪除新增子節點
修改節點屬性
修改節點文本
~~~
var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3
~~~
>[info] 差異類型舉例
~~~
;替換差異
patches[0] = [{
type: REPALCE,
node: newNode // el('section', props, children)
}]
;屬性修改
patches[0] = [{
type: REPALCE,
node: newNode // el('section', props, children)
}, {
type: PROPS,
props: {
id: "container"
}
}]
;文本修改
patches[2] = [{
type: TEXT,
content: "Virtual DOM2"
}]
~~~
>[info] 子節點列表對比算法
~~~
;舊節點順序
a b c d e f g h i
;新節點順序
a b c h d f g h i j
~~~
>[info] 列表中節點的操作可以看做移動 插入 刪除三種
移動可以看出刪除和插入的合并。因此,可以簡化為插入和刪除操作。
抽象出來就是字符串的最小編輯距離問題。
簡單實現如下
~~~
patches[0] = [{
type: REORDER,
moves: [{remove or insert}, {remove or insert}, ...]
}]
~~~
## 5 vdom之patch
>[info] 可以對js對象進行對比后結果patches對象中
轉換為對應的dom操作實現dom樹的更新
~~~
function patch (node, patches) {
var walker = {index: 0}
dfsWalk(node, walker, patches)
}
function dfsWalk (node, walker, patches) {
var currentPatches = patches[walker.index] // 從patches拿出當前節點的差異
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)
}
if (currentPatches) {
applyPatches(node, currentPatches) // 對當前節點進行DOM操作
}
}
function applyPatches (node, currentPatches) {
currentPatches.forEach(function (currentPatch) {
switch (currentPatch.type) {
case REPLACE:
node.parentNode.replaceChild(currentPatch.node.render(), 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)
}
})
}
~~~
## 6 總結
>[info] vdom算法主要包含以上三個函數element,diff,patch
使用思路如下
~~~
// 1. 構建虛擬DOM
var tree = el('div', {'id': 'container'}, [
el('h1', {style: 'color: blue'}, ['simple virtal dom']),
el('p', ['Hello, virtual-dom']),
el('ul', [el('li')])
])
// 2. 通過虛擬DOM構建真正的DOM
var root = tree.render()
document.body.appendChild(root)
// 3. 生成新的虛擬DOM
var newTree = el('div', {'id': 'container'}, [
el('h1', {style: 'color: red'}, ['simple virtal dom']),
el('p', ['Hello, virtual-dom']),
el('ul', [el('li'), el('li')])
])
// 4. 比較兩棵虛擬DOM樹的不同
var patches = diff(tree, newTree)
// 5. 在真正的DOM元素上應用變更
patch(root, patches)
~~~
## 7 參考
[vdom算法](https://segmentfault.com/a/1190000004029168)
[vdom完整代碼](https://github.com/livoras/simple-virtual-dom)
- 概述
- 框架結構
- 編譯入口(\entries)
- web-compiler.js(web編譯)
- web-runtime.js(web運行時)
- web-runtime-wih-compiler.js(web編譯運行)
- web-server-renderer.js(web服務器渲染)
- 核心實現 (\core)
- index.js(核心入口)
- config.js(核心配置)
- core\util(核心工具)
- core\observer(雙向綁定)
- core\vdom(虛擬DOM)
- core\global-api(核心api)
- core\instance(核心實例)
- 模板編譯(\compiler)
- compiler\parser(模板解析)
- events.js(事件解析)
- helper.js(解析助手)
- directives\ref.js(ref指令)
- optimizer.js(解析優化)
- codegen.js(渲染生成)
- index.js(模板編譯入口)
- web渲染(\platforms\web)
- compiler(web編譯目錄)
- runtime(web運行時目錄)
- server(web服務器目錄)
- util(web工具目錄)
- 服務器渲染(\server)
- render-stream.js(流式渲染)
- render.js(服務器渲染函數)
- create-renderer.js(創建渲染接口)
- 框架流程
- Vue初始化
- Vue視圖數據綁定
- Vue數據變化刷新
- Vue視圖操作刷新
- 框架工具
- 基礎工具(\shared)
- 模板編譯助手
- 核心實例工具
- Web渲染工具
- 基礎原理
- dom
- string
- array
- function
- object
- es6
- 模塊(Module)
- 類(Class)
- 函數(箭頭)
- 字符串(擴展)
- 代理接口(Proxy)
- 數據綁定基礎
- 數據綁定實現
- mvvm簡單實現
- mvvm簡單使用
- vdom算法
- vdom實現
- vue源碼分析資料