[TOC]
* * * * *
## 1 源代碼文件
### 1-1 解析實現
~~~
src\strategy\lexer.js
~~~
### 1-2 解析生成
~~~
src\vdom\
~~~
## 2 流程分析
### 2-1 節點內容解析
~~~
src\strategy\lexer.js
function lexer(text, recursive) {
// 所有生成節點數組
var nodes = []
// recursive參數使用 true 解析子節點時調用
if (recursive && !rbind.test(text)) {
return nodes
}
// recursive參數使用 false 文本數組存儲??1,??2,這里實現嵌套
if (!recursive) {
text = text.replace(rstring, dig)
}
// 反復迭代解析
do {
// 初始化解析狀態outerHTML,node
var outerHTML = ''
var node = false
//創建文本虛擬節點 VText
var match = text.match(rtext)
if (match) {
outerHTML = match[0]
node = new VText(outerHTML.replace(rfill, fill))
}
//創建注釋虛擬節點 VComment
if (!node) {
match = text.match(rcomment)
if (match) {
outerHTML = match[0]
node = new VComment(match[1].replace(rfill, fill))
// 移除緊挨著<!--ms-for-end:xxxx-->前的空白節點
// 注釋節點空白清除
var nodeValue = node.nodeValue
if (rspBeforeForEnd.test(nodeValue)) {
var sp = nodes[nodes.length - 1]
if (sp && sp.type === '#text' && rsp.test(sp.nodeValue)) {
nodes.pop()
}
}
}
}
//創建閉標簽虛擬節點
if (!node) {
// 閉合節點
match = text.match(rfullTag)
if (match) {
//貪婪匹配 outerHTML,可能匹配過多
outerHTML = match[0]
//節點類型
var type = match[1].toLowerCase()
outerHTML = clipOuterHTML(outerHTML, type)
//節點屬性
match = outerHTML.match(rvoidTag)
var props = {}
if (match[2]) {
handleProps(match[2], props)
}
//節點innerHTML
var innerHTML = outerHTML.slice(match[0].length,
(type.length + 3) * -1)
// 節點組織
node = {
type: type,
props: props,
template: innerHTML.replace(rfill, fill).trim(),
children: []
}
node = modifyProps(node, innerHTML, nodes)
}
}
//創建自閉合標簽虛擬節點
if (!node) {
match = text.match(rvoidTag)
if (match) {
//
outerHTML = match[0]
// 節點類型
type = match[1].toLowerCase()
// 節點屬性
props = {}
if (match[2]) {
handleProps(match[2], props)
}
// 節點結點
node = {
type: type,
props: props,
template: '',
children: [],
isVoidTag: true
}
modifyProps(node, '', nodes)
}
}
if (node) {
// 創建節點成功 保存到數組
nodes.push(node)
text = text.slice(outerHTML.length)
if (node.type === '#comment' && rspAfterForStart.test(node.nodeValue)) {
node.signature = makeHashCode('for')
//移除緊挨著<!--ms-for:xxxx-->后的空白節點
text = text.replace(rleftSp, '')
}
} else {
// 創建節點失敗 直接跳出
break
}
} while (1);
// 非遞歸的情況
if (!recursive) {
maps = {}
}
return nodes
}
~~~
> text:待解析節點內容
> recursive:是否屬于遞歸解析,節點內解析子節點
* * * * *
> 準備返回節點數組
> 檢查recursive,是否屬于子節點解析
> 循環解析主體
> 1 虛擬文本節點匹配解析Vtext
> 2 虛擬注釋節點匹配解析VComment
> 3 雙標簽生成虛擬節點VElement
> 4 單標簽生成虛擬節點VElement
> 5 元素節點解析的modifyProps()遞歸調用lexer()解析子節點
### 2-2 虛擬節點創建
~~~
var match = text.match(rtext)
node = new VText(outerHTML.replace(rfill, fill))
~~~
> 虛擬文本節點
~~~
match = text.match(rcomment)
node = new VComment(match[1].replace(rfill, fill))
~~~
> 虛擬注釋節點
~~~
match = text.match(rfullTag)
node = modifyProps(node, innerHTML, nodes)
match = text.match(rvoidTag)
modifyProps(node, '', nodes)
~~~
> 虛擬元素節點
### 2-3 handleProps(str, props)屬性操作
~~~
var ramp = /&/g
var rnowhite = /\S+/g
var rquote = /"/g
var rnogutter = /\s*=\s*/g
function handleProps(str, props) {
str.replace(rnogutter, '=').replace(rnowhite, function (el) {
var arr = el.split('='), value = arr[1] || '',
name = arr[0].toLowerCase()
if (arr.length === 2) {
if (value.indexOf('??') === 0) {
value = value.replace(rfill, fill).
slice(1, -1).
replace(ramp, '&').
replace(rquote, '"')
}
}
props[name] = value
})
}
~~~
### 2-4 modifyProps(node, innerHTML, nodes)元素節點修正與遞歸解析
~~~
function modifyProps(node, innerHTML, nodes) {
// 節點類型
var type = node.type
if (node.props['ms-skip']) {
// 包含ms-skip屬性的節點
node.skipContent = true
} else {
// 非包含ms-skip屬性的節點
// 根據節點屬性操作
switch (type) {
case 'style':
case 'script':
case 'noscript':
case 'template':
case 'textarea':
node.skipContent = true
if (type === 'textarea') {
node.props.type = 'textarea'
}
break
case 'input':
if (!node.props.type) {
node.props.type = 'text'
}
case 'xmp':
node.children.push(new VText(node.template))
break
case 'option':
node.children.push(new VText(trimHTML(node.template)))
break
default:
// 遞歸解析子節點
if (!node.isVoidTag) {
var childs = lexer(innerHTML, true)
node.children = childs
// 如果是table節點
if (type === 'table') {
addTbody(node.children)
}
}
break
}
// for類型表達式
var forExpr = node.props['ms-for']
if (forExpr) {
nodes.push({
type: '#comment',
nodeValue: 'ms-for:' + forExpr,
signature: makeHashCode('for')
})
delete node.props['ms-for']
nodes.push(node)
node = {
type: '#comment',
nodeValue: 'ms-for-end:'
}
}
}
return node
}
~~~
### 2-5 clipOuterHTML(matchText, type) 截取OuterHTML
~~~
var openStr = '(?:\\s+[^>=]*?(?:=[^>]+?)?)*>'
var tagCache = {}// 緩存所有匹配開標簽閉標簽的正則
var rchar = /./g
var regArgs = avalon.msie < 9 ? 'ig' : 'g'//IE6-8,標簽名都是大寫
function clipOuterHTML(matchText, type) {
var opens = []
var closes = []
// 開標簽與閉標簽的緩存
var ropen = tagCache[type + 'open'] ||
(tagCache[type + 'open'] = new RegExp('<' + type + openStr, regArgs))
var rclose = tagCache[type + 'close'] ||
(tagCache[type + 'close'] = new RegExp('<\/' + type + '>', regArgs))
/* jshint ignore:start */
//注意,頁面有時很長,b的數值就很大,如
//000000000<000000011>000000041<000000066>000000096<000000107>
matchText.replace(ropen, function (_, b) {
//取得所有開標簽的位置
opens.push(('0000000000' + b + '<').slice(-10))
return _.replace(rchar, '1')
}).replace(rclose, function (_, b) {
//取得所有閉標簽的位置
closes.push(('0000000000' + b + '>').slice(-10))
})
/* jshint ignore:end */
/*<div>
<div>01</div>
<div>02</div>
</div>
<div>222</div>
<div>333</div>
會變成
000<
005<
012>
018<
025>
031>
037<
045>
051<
059>
再變成
<<><>>
<>
<>
最后獲取正確的>的索引值,這里為<<><>>的最后一個字符,*/
var pos = opens.concat(closes).sort()
var gtlt = pos.join('').replace(rnumber, '')
var k = 0, last = 0
for (var i = 0, n = gtlt.length; i < n; i++) {
var c = gtlt.charAt(i)
if (c === '<') {
k += 1
} else {
k -= 1
}
if (k === 0) {
last = i
break
}
}
// (</>為三個字符)
var findex = parseFloat(pos[last]) + type.length + 3
//取得正確的outerHTML
return matchText.slice(0, findex)
}
~~~
### 2-7 addTbody(nodes) table修正
~~~
function addTbody(nodes) {
var tbody, needAddTbody = false, count = 0, start = 0, n = nodes.length
for (var i = 0; i < n; i++) {
var node = nodes[i]
if (!tbody) {
if (node.type === 'tr') {
tbody = {
type: 'tbody',
template: '',
children: [],
props: {}
}
tbody.children.push(node)
needAddTbody = true
if (start === 0)
start = i
nodes[i] = tbody
}
} else {
if (node.type !== 'tr' && node.type.charAt(0) !== "#") {
tbody = false
} else {
tbody.children.push(node)
count++
nodes[i] = 0
}
}
}
if (needAddTbody) {
for (i = start; i < n; i++) {
if (nodes[i] === 0) {
nodes.splice(i, 1)
i--
count--
if (count === 0) {
break
}
}
}
}
}
~~~
## 4 總結
### 4-1 意義
解析dom內容為虛擬dom
### 4-2 思路
遞歸調用lexer()解析節點
### 4-3 其他操作
[附:虛擬DOM](http://www.hmoore.net/zmwtp/avalon2/137892)
- 概述
- 框架目錄
- 組件目錄(components\)
- 生成目錄(dist\)
- 測試目錄(karma\)
- 示例目錄(perf\)
- 主體目錄(src)
- 其他文件
- 框架流程
- 前:章節說明
- 主:模板掃描(avalon.scan())
- 主:VM創建(avalon.define())
- 主:同步刷新(avalon.batch())
- 附:節點解析(avalon.lexer())
- 附:虛擬DOM(avalon.vdomAdaptor())
- 附:渲染函數(avalon.render())
- 附:VM生成(avalon.masterFactory())
- 附:節點diff(avalon.diff())
- 主:界面事件(test)
- 框架工具
- 另:全局函數
- 另:全局正則
- 另:事件接口
- 另:組件接口
- 另:DOMApi
- 框架驅動
- D : 指令實現
- D:兼容處理
- 使用范例
- 基礎原理
- js模塊
- js對象
- js函數
- js數組
- js字符串
- dom接口
- 框架心得
- 心:總體思路