首先放上項(xiàng)目地址,喜歡的話就star一個(gè)吧
GitHub: markdown365-parser
預(yù)覽地址: Demo
react的發(fā)布讓前端擺脫了使用jQuery一點(diǎn)一點(diǎn)修改DOM的歷史召夹。對(duì)于后來(lái)的很多框架都產(chǎn)生非常重要的影響腮恩。其中最為重要的概念就是Virtual DOM了氧敢,在后面vue也借鑒了這一概念,不過(guò)這兩個(gè)框架對(duì)Virtual DOM的實(shí)現(xiàn)是不一樣的。
作為一個(gè)搬磚的惰许,肯定是離不開(kāi)markdwon的,markdwon能夠用很簡(jiǎn)潔的語(yǔ)法表現(xiàn)出很好的排版樣式史辙。所以也就關(guān)注了一些markdwon的解析器汹买,閑來(lái)無(wú)事佩伤,就有了自己造一個(gè)輪子的想法。在GitHub上找了幾個(gè)比較流行的markdwon解析的庫(kù)之后選擇了marked來(lái)研究晦毙,選擇marked的原因主要是從代碼量和支持語(yǔ)法兩個(gè)方面考慮的生巡,由于自己?jiǎn)螛屍ヱR的干,所以選用的庫(kù)不能太大了(我怕搞不定)见妒,marked的源碼只有1000多行孤荣,而且代碼結(jié)構(gòu)還是比較清晰的,整個(gè)扒下來(lái)也就定義了三個(gè)類须揣,分別是Lexer(塊級(jí)語(yǔ)法解析)盐股、InlineLexer(行內(nèi)語(yǔ)法解析)和Renderer(渲染成html字符串),這里就不過(guò)多說(shuō)明marked了耻卡,其中我借鑒了Lexer和InlineLexer兩個(gè)類的實(shí)現(xiàn)疯汁。
嗯,好了卵酪,下面開(kāi)始正式安利
支持語(yǔ)法
項(xiàng)目已經(jīng)支持了比較常用的一些語(yǔ)法幌蚊,具體請(qǐng)查看Grammar.md
使用示例如下
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>markdown365-parser</title>
<script src="dist/markdown365-parser.js"></script>
</head>
<body>
<div id="previiew"></div>
<script>
const markdwon = '## markdown365-parser'
const parser = new Markdown365Parser({
gfm: true,
tables: true,
breaks: true,
pedantic: false,
smartypants: false,
base = '',
$el: document.querySelector('#previiew')
})
parser.parse(markdown)
</script>
</body>
</html>
參數(shù)說(shuō)明
gfm: GitHub flavored markdown語(yǔ)法支持. 默認(rèn):
true
tables: GFM tables語(yǔ)法支持. 必須要求
gfm
為true
. 默認(rèn):true
breaks: GFM line breaks解析規(guī)則支持. 必須要求
gfm
為true
. 默認(rèn):false
pedantic: 是否盡可能遵守
markdown.pl
的部分內(nèi)容. 不去掉一些不嚴(yán)格的內(nèi)容. 默認(rèn):false
smartypants: 是否替換特殊符號(hào). 默認(rèn):
false
base:這里是用來(lái)指定markdwon文檔中的鏈接地址、圖片地址的前置鏈接凛澎,如markdown中的說(shuō)有圖片都指向另一個(gè)域的時(shí)候霹肝,base就可以設(shè)置為指定域名。這里這個(gè)參數(shù)主要時(shí)考慮到編寫桌面markdown編輯器用的塑煎,因?yàn)榫庉嬈鞔蜷_(kāi)markdown文件時(shí)沫换,對(duì)應(yīng)的圖片的路徑要轉(zhuǎn)換為相對(duì)markdown文件所在目錄的相對(duì)路徑,具體可參考我的另一個(gè)項(xiàng)目markdown365
$el:文檔要渲染到的dom節(jié)點(diǎn)
源碼目錄結(jié)構(gòu)
src
│ index.js # 入口文件 Parser類
│ utils.js # 工具代碼
│
├─lexer # markdown解析相關(guān)代碼 把字符串解析為vnode
│ block-lexer.js # 塊級(jí)語(yǔ)法解析 BlockLexer類
│ block-rules.js # 塊級(jí)語(yǔ)法解析規(guī)則
│ index.js # Lexer類
│ inline-lexer.js # 行內(nèi)語(yǔ)法解析 InlineLexer類
│ inline-rules.js # 行內(nèi)語(yǔ)法解析規(guī)則
│
├─renderer # 渲染類 把vnode diff并渲染到真實(shí)dom中
│ index.js # Renderer類
│
└─vnode # vnode定義代碼
index.js # 傳入節(jié)點(diǎn)信息返回vnode
vnode.js # Vnode類
Markdwon解析流程介紹
下面分別介紹每一個(gè)類的作用
- Parser類:Parser類初始化后就可以調(diào)用parse方法最铁,然后就開(kāi)始執(zhí)行l(wèi)ex和render
import Lexer from './lexer'
import Renderer from './renderer'
import h from './vnode'
export default class Parser {
/**
* 掛載類的靜態(tài)方法讯赏,使得可直接調(diào)用Parser.parse
*/
static parse (src, options) {
const parser = new Parser(options)
return parser.parse(src)
}
/**
* 初始化Parser類
* @param {Object} options
*/
constructor (options) {
this.options = options
// 初始化Lexer
this.lexer = new Lexer(this.options)
// 初始化Renderer
this.renderer = new Renderer(this.options)
// 初始化vnode
this.vnode = h({
$el: $el,
tag: $el.tagName.toLowerCase(),
type: 'node',
children: []
})
}
/**
* 解析源碼并渲染到dom
* @param {String} src
* @return {Parser} this
*/
parse (src) {
/**
* 必須創(chuàng)建新的vnode
* 創(chuàng)建的vnode將和this.vnode進(jìn)行對(duì)比
* 否則render diff的時(shí)候就會(huì)失敗
* h為創(chuàng)建vnode的方法
*/
const vnode = h({
$el: this.vnode.$el,
tag: this.vnode.tag,
type: 'node',
children: this.lex(src)
})
this.render(vnode)
this.vnode = vnode
return this.vnode
}
/**
* 把源碼解析為vnode
* @param {String} src
* @return {Vnode}
*/
lex (src) {
return this.lexer.lex(src)
}
/**
* 把Vnode渲染到dom
* @param {Vnode} vnode
*/
render (vnode) {
this.renderer.patch(vnode, this.vnode)
}
}
- Lexer類:Lexer包含BlockLexer和InlineLexer兩個(gè)部分,分別用來(lái)解析塊級(jí)語(yǔ)法和行內(nèi)語(yǔ)法冷尉,其中比較重點(diǎn)的是lexInline方法漱挎,該方法中會(huì)遍歷解析塊級(jí)語(yǔ)法解析后的vnode對(duì)象,如果vnode對(duì)象的source存在就要解析雀哨,并且由于塊級(jí)節(jié)點(diǎn)解析后的會(huì)存在text類型的未解析的節(jié)點(diǎn)磕谅,即text類型的節(jié)點(diǎn)中還可以解析出一些語(yǔ)法。例如:一段文字中還包含鏈接或者斜體等雾棺,這些都是屬于行內(nèi)解析的范疇膊夹,但我們知道text類型的節(jié)點(diǎn)是不存在子接點(diǎn)的,所以解析出來(lái)的vnode對(duì)象不能掛載到原text節(jié)點(diǎn)的子節(jié)點(diǎn)上捌浩,所以就得把解析出來(lái)的節(jié)點(diǎn)掛載到原text節(jié)點(diǎn)的父節(jié)點(diǎn)上放刨,并且對(duì)應(yīng)的位置也不能錯(cuò)亂,詳細(xì)見(jiàn)下面源碼說(shuō)明
import BlockLexer from './block-lexer'
import InlineLexer from './inline-lexer'
export default class Lexer {
/**
* Static Lex Method
*/
static lex (src, options) {
const lexer = new Lexer(options)
return lexer.lex(src)
}
/**
* 初始化Lexer類
* @param {Object} options
*/
constructor ({
gfm = true,
tables = true,
pedantic = false,
breaks = false,
smartypants = false,
base = ''
} = {}) {
this.options = {
gfm,
tables,
breaks,
pedantic,
smartypants,
base
}
// 初始化塊級(jí)語(yǔ)法解析
this.blockLexer = new BlockLexer(this.options)
// 初始化行內(nèi)語(yǔ)法解析
this.inlineLexer = new InlineLexer(this.options)
// 定義vnode
this.vnode = []
}
/**
* 把源碼解析為vnode
* @param {String} src
* @return {Vnode}
*/
lex (src) {
const { vnode, links } = this.lexBlock(src)
// 設(shè)置參考式的鏈接或者圖片
this.inlineLexer.setLinks(links)
this.vnode = this.lexInline(vnode)
return this.vnode
}
/**
* 解析源碼的塊級(jí)語(yǔ)法
* @param {String} src
* @return {Vnode}
*/
lexBlock (src) {
return this.blockLexer.parser(src)
}
/**
* 解析經(jīng)過(guò)塊級(jí)語(yǔ)法解析的vnode對(duì)象
* 解析對(duì)象中未被解析的行內(nèi)語(yǔ)法
* @param {Vnode} vnodes
* @return {Vnode}
*/
lexInline (vnodes) {
let i = 0
let vnode = vnodes[i]
while (vnode) {
// 需要進(jìn)行行內(nèi)解析的情況
if (vnode.source) {
if (vnode.type === 'text') { // 為text的時(shí)候
// 此處說(shuō)明請(qǐng)參考后面的行內(nèi)解析說(shuō)明
// 把text從text node移動(dòng)到text node的父節(jié)點(diǎn)上
const children = this.inlineLexer.parser(vnode)
// 記錄原來(lái)位置的元素下標(biāo)
let oi = i
// 把節(jié)點(diǎn)加入到父節(jié)點(diǎn)中對(duì)應(yīng)的位置(相同下標(biāo)處)
while (children.length) {
const vn = children.shift()
/**
* 合并text節(jié)點(diǎn)
* 如果前一個(gè)是text尸饺,并且當(dāng)前vn也是text
* 就合并成一段文字进统,減少節(jié)點(diǎn)個(gè)數(shù)
*/
if (oi !== i && vnodes[i].type === 'text' && vn.type === 'text') {
vnodes[i].text += vn.text
} else {
vn.parent = vnode.parent
// 在原來(lái)的位置后面插入新的值
vnodes.splice(++i, 0, vn)
}
}
// 從父節(jié)點(diǎn)上移除被解析的節(jié)點(diǎn)助币,該節(jié)點(diǎn)已經(jīng)被解析為其他的節(jié)點(diǎn)替代了
i--
vnodes.splice(oi, 1)
} else { // 為node或者h(yuǎn)tml類型的時(shí)候
vnode.children = this.inlineLexer.parser(vnode)
.map(item => {
item.parent = vnode
return item
})
}
vnode.source = null
} else {
this.lexInline(vnode.children)
}
i++
vnode = vnodes[i]
}
return vnodes
}
}
行內(nèi)解析說(shuō)明,如下代碼只進(jìn)行說(shuō)明螟碎,實(shí)際情況子節(jié)點(diǎn)還有parent屬性眉菱,該屬性指向父節(jié)點(diǎn)
- 進(jìn)行行內(nèi)解析前
const vnode = {
uid: 0,
$el: null,
tag: 'li',
type: 'node',
parent: null,
attributes: {},
text: '',
source: null,
children: [
{
uid: 1,
$el: null,
tag: null,
type: 'text',
children: [],
attributes: {},
text: '',
source: '[x] [google](https://www.google.com/)'
}
]
}
- 行內(nèi)解析后應(yīng)為
const vnode = {
uid: 0,
$el: null,
tag: 'li',
type: 'node',
parent: null,
attributes: {},
text: '',
source: null,
children: [
{
uid: 1,
$el: null,
tag: 'input',
type: 'node',
attributes: {
checked: 'checked',
disabled: 'disabled',
type: 'checkbox'
},
text: '',
source: null,
children: []
},
{
uid: 2,
$el: null,
tag: 'a',
type: 'node',
attributes: {
href: 'https://www.google.com/'
},
text: '',
source: null,
children: [
{
uid: 3,
$el: null,
tag: null,
type: 'text',
attributes: {},
text: 'google',
source: null,
children: []
}
]
}
]
}
- BlockLexer類:BlockLexer類最主要的方法是lex,該方法是正真語(yǔ)法解析部分抚芦,在方法內(nèi)部使用while循環(huán)倍谜,直到src被解析完才返回vnode,并且里面的每一個(gè)解析規(guī)則的順序是不能隨意更換的叉抡,因?yàn)橐?guī)則之間會(huì)存在相互包含的關(guān)系,例如一個(gè)h1語(yǔ)法段落肯定是可以被解析為p標(biāo)簽的答毫,所以這就要求h1的解析規(guī)則放在p標(biāo)簽規(guī)則解析的前面褥民,如果不能匹配才會(huì)匹配為p標(biāo)簽。其次洗搂,對(duì)與lex的另兩個(gè)參數(shù)的作用消返,top參數(shù)主要是為了區(qū)分一段文字是解析為p標(biāo)簽還是解析為text類型的節(jié)點(diǎn),如果沒(méi)有父節(jié)點(diǎn)就解析為p標(biāo)簽耘拇,反之則為text節(jié)點(diǎn)撵颊。bq參數(shù)用來(lái)區(qū)分是否為blockquote標(biāo)簽下的內(nèi)容,在blockquote便簽下的內(nèi)容不會(huì)被解析到參考式的鏈接中去惫叛,其實(shí)也就是參考式的鏈接只能寫在頂級(jí)倡勇,否則不會(huì)生效
import block from './block-rules'
import h from '../vnode'
import { isDef } from '../utils'
/**
* Block Lexer
*/
export default class BlockLexer {
static rules = block
/**
* Static Lex Method
*/
static lex (src, options) {
const blockLexer = new BlockLexer(options)
return blockLexer.parser(src)
}
/**
* 初始化類
* @param {Object} options
*/
constructor ({
gfm = true,
tables = true,
pedantic = false,
base = ''
} = {}) {
this.options = {
gfm,
tables,
pedantic,
base
}
// 初始化解析規(guī)則
this.rules = block.normal
// 初始化參考式的鏈接或圖片存儲(chǔ)的對(duì)象
this.links = {}
// 初始化vnode
this.vnode = []
if (this.options.gfm) {
if (this.options.tables) {
this.rules = block.tables
} else {
this.rules = block.gfm
}
}
}
/**
* 解析源碼
* @param {String} src
* @return {Object} vnode links
*/
parser (src) {
src = src
.replace(/\r\n|\r/g, '\n')
.replace(/\t/g, ' ')
.replace(/\u00a0/g, ' ')
.replace(/\u2424/g, '\n')
this.vnode = this.lex(src, true)
return {
vnode: this.vnode,
links: this.links
}
}
/**
* 解析源碼
* @param {String} src
* @param {Boolean} top 是否是頂級(jí)的標(biāo)簽
* @param {Boolean} bq 是否為blockquote便簽中的元素
* @return {Vnode}
*/
lex (src, top, bq) {
src = src.replace(/^ +$/gm, '')
const vnodes = []
let token
let vnode
while (src) {
/**
* 解析各種塊級(jí)語(yǔ)法規(guī)則
* src = src - 被解析的部分
* 直到src = ''才停止解析
*/
if (src) {
throw new Error('Infinite loop on byte: ' + src.charCodeAt(0))
}
}
return vnodes
}
/**
* 解析table
* @param {Array} thead 表頭每一列
* @param {Array} tbody 表格每一行
* @param {Array} align 表格每一列對(duì)齊方式
* @return {Vnode}
*/
lexTable (thead, tbody, align) {
return h({
tag: 'table',
children: [
h({
tag: 'thead',
children: thead.map((th, index) => /** 表頭部分 */)
}),
h({
tag: 'tbody',
children: tbody.map(tr => /** 表格主體部分 */)
})
]
})
}
}
- InlineLexer類:InlineLexer中主要的方法是lex,lex有兩個(gè)參數(shù)嘉涌,其中src為帶解析的源碼字符串妻熊,parent為的當(dāng)前解析text的父元素,該參數(shù)主要用來(lái)判斷tasklink仑最,要進(jìn)入tasklink解析條件扔役,必須要在父元素為li才行,否則不會(huì)解析為tasklist
import inline from './inline-rules'
import { isDef, transformURL } from '../utils'
import h from '../vnode'
/**
* Inline Lexer & Compiler
*/
export default class InlineLexer {
static vision = process.env.VERSION
static rules = inline
/**
* Static Lexing/Compiling Method
*/
static lex (vnode, links, options) {
let inlineLexer = new InlineLexer(options, links)
return inlineLexer.parser(vnode)
}
/**
* 初始化類
* @param {Object} options
* @param {Object} links
*/
constructor ({
gfm = true,
pedantic = false,
breaks = false,
smartypants = false,
base = ''
} = {}, links = {}) {
this.options = {
gfm,
pedantic,
breaks,
smartypants,
base
}
this.rules = inline.normal
if (this.options.gfm) {
if (this.options.breaks) {
this.rules = inline.breaks
} else {
this.rules = inline.gfm
}
} else if (this.options.pedantic) {
this.rules = inline.pedantic
}
this.setLinks(links)
}
/**
* 設(shè)置參考式的鏈接對(duì)象集合
* @param {Object} links
*/
setLinks (links) {
if (typeof links !== 'object') {
throw new TypeError('`links` isn\'t a object.')
}
this.links = links
}
/**
* 解析行內(nèi)內(nèi)容
* @param {Vnode} vnode
*/
parser (vnode) {
return this.lex(vnode.source, vnode.parent)
}
/**
* Lexing/Compiling
* @param {String} src
* @param {Vnode} parent
*/
lex (src, parent = null) {
let vnodes = []
let link,
text,
href,
token
let vnode
while (src) {
/**
* 解析各種行內(nèi)語(yǔ)法規(guī)則
* src = src - 被解析的部分
* 直到src = ''才停止解析
*/
if (src) {
throw new Error('Infinite loop on byte: ' + src.charCodeAt(0))
}
}
return vnodes
}
/**
* 生成鏈接
* @param {String} cap 鏈接中子節(jié)點(diǎn)源碼
* @param {Object} link 鏈接對(duì)象
* @returns {Vnode}
*/
lexLink (cap, link) {}
/**
* Smartypants Transformations
* @param {String} text
* @return {String}
*/
smartypants (text) {
/**
* 轉(zhuǎn)義一些內(nèi)容
*/
}
}
- Vnode類:
- vnode類
export default class VNode {
static uid = 0 // 每次創(chuàng)建一個(gè)vnode就會(huì)加一警医,這是每個(gè)vnode的唯一標(biāo)識(shí)
constructor ({
$el = null,
tag = null,
type = 'node', // 可為node/text/html
parent = null,
children = [],
attributes = {},
text = null,
source = null
} = {}) {
this.uid = VNode.uid++
this.$el = $el
this.tag = tag
this.type = type
this.parent = parent
this.children = children
this.attributes = attributes
this.text = text
this.source = source
}
}
- h函數(shù):快速創(chuàng)建vnode的方法
import VNode from './vnode'
export default ({
$el = null,
tag = null,
type = 'node',
parent = null,
children = [],
attributes = {},
text = '',
source = null
} = {}) => {
const vnode = new VNode({
$el,
tag,
type,
parent,
children,
attributes,
text,
source
})
// 每個(gè)子節(jié)點(diǎn)都把parent指向當(dāng)前節(jié)點(diǎn)vnode
vnode.children.forEach(item => {
item.parent = vnode
})
return vnode
}
- Renderer類:Renderer類參考了vue中的render實(shí)現(xiàn)方法亿胸,其中主要是在對(duì)比的時(shí)候就對(duì)真實(shí)dom進(jìn)行修改,當(dāng)patch結(jié)束dom更新也就結(jié)束了
export default class Renderer {
static vision = process.env.VERSION
/**
* Static render Method
*/
static render (vnode, oldVnode) {
const renderer = new Renderer()
return renderer.patch(vnode, oldVnode)
}
/**
* 比較新舊兩個(gè)節(jié)點(diǎn)
* 并更新到dom
* @param {Vnode} vnode
* @param {Vnode} oldVnode
*/
patch (vnode, oldVnode) {}
/**
* 對(duì)比新舊節(jié)點(diǎn)屬性
* @param {Vnode} vnode
* @param {Vnode} oldVnode
*/
patchAttributes (vnode, oldVnode) {}
/**
* 對(duì)比子新舊節(jié)點(diǎn)的子節(jié)點(diǎn)列表
* @param {Array} newCh
* @param {Array} oldCh
*/
patchChildren (newCh = [], oldCh = []) {}
/**
* 創(chuàng)建新dom元素
* 并賦值給vnode.$el
* @param {Vnode} vnode
* @return {Vnode}
*/
create (vnode) {}
/**
* 追加節(jié)點(diǎn)
* @param {Vnode} parent
* @param {Vnode} vnode
*/
append (parent, vnode) {}
/**
* 在指定節(jié)點(diǎn)前插入節(jié)點(diǎn)
* @param {Vnode} parent
* @param {Vnode} vnode
* @param {Vnode} before
*/
insert (parent, vnode, before) {}
/**
* 移除節(jié)點(diǎn)
* @param {Vnode} vnode
*/
removeEl (vnode) {}
/**
* 替換舊節(jié)點(diǎn)為新的節(jié)點(diǎn)
* @param {Vnode} parent
* @param {Vnode} vnode
* @param {Vnode} oldVnode
*/
replace (parent, vnode, oldVnode) {}
}
diff原理說(shuō)明
diff原理基本和vue的思路一致预皇,只是在vue diff的基礎(chǔ)上做了簡(jiǎn)化和修改侈玄。這里可以打開(kāi)源碼對(duì)比著看,同時(shí)這里也推薦一篇對(duì)于vue diff源碼解析的文章Vue原理解析之Virtual Dom
-
單個(gè)節(jié)點(diǎn)進(jìn)行比較
- 子節(jié)點(diǎn)列表diff深啤,子節(jié)點(diǎn)相對(duì)于單個(gè)節(jié)點(diǎn)的對(duì)比就復(fù)雜很多了拗馒,會(huì)存在列表中添加節(jié)點(diǎn)、刪除節(jié)點(diǎn)溯街、節(jié)點(diǎn)位置移動(dòng)诱桂、某一個(gè)節(jié)點(diǎn)被替換這幾種基本情況洋丐。這里先放上
patchChildren
方法的源碼。
/**
* 對(duì)比子新舊節(jié)點(diǎn)的子節(jié)點(diǎn)列表
* @param {Array} newCh
* @param {Array} oldCh
*/
patchChildren (newCh = [], oldCh = []) {
let oldStartIdx = 0 // 記錄舊節(jié)點(diǎn)數(shù)組中的開(kāi)始下標(biāo)
let newStartIdx = 0 // 記錄新節(jié)點(diǎn)數(shù)組中的開(kāi)始下標(biāo)
let oldEndIdx = oldCh.length - 1 // 記錄舊節(jié)點(diǎn)沖末尾向前匹配的位置下標(biāo)
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
// 循環(huán)節(jié)點(diǎn)列表挥等,直到新列表和舊列表的每一個(gè)一個(gè)節(jié)點(diǎn)都被比較完
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode.tag === newStartVnode.tag) {
this.patch(newStartVnode, oldStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (oldEndVnode.tag === newEndVnode.tag) {
this.patch(newEndVnode, oldEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (oldStartVnode.tag === newEndVnode.tag) { // Vnode moved right
this.patch(newEndVnode, oldStartVnode)
this.insert(oldEndVnode.parent, oldStartVnode, oldCh[oldEndIdx + 1])
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (oldEndVnode.tag === newStartVnode.tag) { // Vnode moved left
this.patch(newStartVnode, oldEndVnode)
// 把舊節(jié)點(diǎn)真實(shí)dom移動(dòng)到newEndVnode的位置
this.insert(oldStartVnode.parent, oldEndVnode, oldStartVnode)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 這種情況就是一個(gè)節(jié)點(diǎn)變成了另一個(gè)節(jié)點(diǎn)的情況
this.replace(newStartVnode.parent, this.create(newStartVnode), oldStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}
}
// 以下情況為節(jié)點(diǎn)增加或者減少了的情況
if (oldStartIdx > oldEndIdx) {
// 插入新節(jié)點(diǎn)的情況
for (; newStartIdx <= newEndIdx; ++newStartIdx) {
this.insert(newCh[newStartIdx].parent, this.create(newCh[newStartIdx]), newCh[newEndIdx + 1])
}
} else if (newStartIdx > newEndIdx) {
// 移除就無(wú)用的舊節(jié)點(diǎn)
for (; oldStartIdx <= oldEndIdx; ++oldStartIdx) {
this.removeEl(oldCh[oldStartIdx])
}
}
}
下面分別對(duì)while循環(huán)中的每一個(gè)條件進(jìn)行說(shuō)明
- 如果
oldStartVnode.tag === newStartVnode.tag
友绝,那么舊認(rèn)為這兩個(gè)節(jié)點(diǎn)是匹配的,即認(rèn)為為相同節(jié)點(diǎn)肝劲,直接對(duì)比更新這兩個(gè)節(jié)點(diǎn)迁客,如:在末尾追加節(jié)點(diǎn)這種情況或者節(jié)點(diǎn)子元素變化
- 如果
oldEndVnode.tag === newEndVnode.tag
,那么就用新的節(jié)點(diǎn)去更新舊節(jié)點(diǎn)辞槐,如:第一個(gè)節(jié)點(diǎn)變?yōu)榱似渌愋偷墓?jié)點(diǎn)
- 如果
oldStartVnode.tag === newEndVnode.tag
掷漱,那么就對(duì)比這兩個(gè)節(jié)點(diǎn),并且將真實(shí)dom節(jié)點(diǎn)移動(dòng)到newEndVnode.tag所在的位置
-
oldEndVnode.tag === newStartVnode.tag
的情況基本和3一樣榄檬,只是移動(dòng)節(jié)點(diǎn)的位置要反過(guò)來(lái)卜范,這里就不放圖了 - 如果以上幾種情況都不能滿足的話,就讓節(jié)點(diǎn)
newStartVnode
取代oldStartVnode
循環(huán)結(jié)束之后的條件判斷 - 如果
oldStartIdx > oldEndIdx
鹿榜,比如在末尾追加節(jié)點(diǎn)的情況海雪,這是就需要插入新節(jié)點(diǎn)
- 如果
newStartIdx > newEndIdx
,這種情況可以用移除節(jié)點(diǎn)來(lái)做類比舱殿,所以就需要移除舊的多余的節(jié)點(diǎn)
對(duì)于整個(gè)render的diff來(lái)說(shuō)奥裸,整體的效率還是很低,還不完善沪袭,特別是對(duì)于子節(jié)點(diǎn)列表對(duì)比的方法湾宙,還有很大的優(yōu)化空間
最后
項(xiàng)目目前很多功能還不是很完善,對(duì)于markdwon的解析也只是做了比較基礎(chǔ)的支持枝恋,還有一部分語(yǔ)法沒(méi)有能夠支持创倔,特別是對(duì)于html的支持還存在BUG,并且目前對(duì)于語(yǔ)法如何擴(kuò)展也還存在問(wèn)題焚碌,當(dāng)前的代碼結(jié)構(gòu)不易于擴(kuò)展語(yǔ)法畦攘。對(duì)于diff部分也還有很多需要改進(jìn)的地方。所以如果有興趣十电,歡迎提交pr知押。
能力有限,所以以上內(nèi)容有很多不詳細(xì)的地方鹃骂,也可能存在錯(cuò)誤台盯,還請(qǐng)熱心的同學(xué)幫忙指正。