用vnode造一個(gè)markdown輪子

首先放上項(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)疯汁。

talk is cheap show me the code

嗯,好了卵酪,下面開(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ǔ)法支持. 必須要求gfmtrue. 默認(rèn): true

  • breaks: GFM line breaks解析規(guī)則支持. 必須要求gfmtrue. 默認(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è)類的作用

  1. 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)
  }
}
  1. 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: []
        }
      ]
    }
  ]
}
  1. 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 => /** 表格主體部分 */)
        })
      ]
    })
  }
}
  1. 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)容
     */
  }
}
  1. 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
}
  1. 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

  1. 單個(gè)節(jié)點(diǎn)進(jìn)行比較


    diff原理
  2. 子節(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ō)明

  1. 如果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)子元素變化
    1.png
  2. 如果oldEndVnode.tag === newEndVnode.tag,那么就用新的節(jié)點(diǎn)去更新舊節(jié)點(diǎn)辞槐,如:第一個(gè)節(jié)點(diǎn)變?yōu)榱似渌愋偷墓?jié)點(diǎn)
    2.png
  3. 如果oldStartVnode.tag === newEndVnode.tag掷漱,那么就對(duì)比這兩個(gè)節(jié)點(diǎn),并且將真實(shí)dom節(jié)點(diǎn)移動(dòng)到newEndVnode.tag所在的位置
    3.png
  4. oldEndVnode.tag === newStartVnode.tag的情況基本和3一樣榄檬,只是移動(dòng)節(jié)點(diǎn)的位置要反過(guò)來(lái)卜范,這里就不放圖了
  5. 如果以上幾種情況都不能滿足的話,就讓節(jié)點(diǎn)newStartVnode取代oldStartVnode
    循環(huán)結(jié)束之后的條件判斷
  6. 如果oldStartIdx > oldEndIdx鹿榜,比如在末尾追加節(jié)點(diǎn)的情況海雪,這是就需要插入新節(jié)點(diǎn)
    4.png
  7. 如果newStartIdx > newEndIdx,這種情況可以用移除節(jié)點(diǎn)來(lái)做類比舱殿,所以就需要移除舊的多余的節(jié)點(diǎn)
    5.png

對(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é)幫忙指正。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末畏线,一起剝皮案震驚了整個(gè)濱河市静盅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌寝殴,老刑警劉巖蒿叠,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件明垢,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡市咽,警方通過(guò)查閱死者的電腦和手機(jī)痊银,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)施绎,“玉大人溯革,你說(shuō)我怎么就攤上這事」茸恚” “怎么了己莺?”我有些...
    開(kāi)封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵佃蚜,是天一觀的道長(zhǎng)捐川。 經(jīng)常有香客問(wèn)我匾南,道長(zhǎng)兼雄,這世上最難降的妖魔是什么阔墩? 我笑而不...
    開(kāi)封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任蛇数,我火速辦了婚禮鹦倚,結(jié)果婚禮上躺酒,老公的妹妹穿的比我還像新娘押蚤。我一直安慰自己,他們只是感情好羹应,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布揽碘。 她就那樣靜靜地躺著,像睡著了一般园匹。 火紅的嫁衣襯著肌膚如雪雳刺。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天裸违,我揣著相機(jī)與錄音掖桦,去河邊找鬼。 笑死供汛,一個(gè)胖子當(dāng)著我的面吹牛枪汪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播怔昨,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼雀久,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了趁舀?” 一聲冷哼從身側(cè)響起赖捌,我...
    開(kāi)封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎矮烹,沒(méi)想到半個(gè)月后越庇,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體罩锐,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年悦荒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了唯欣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡搬味,死狀恐怖境氢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情碰纬,我是刑警寧澤萍聊,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站悦析,受9級(jí)特大地震影響寿桨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜强戴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一亭螟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧骑歹,春花似錦预烙、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至最域,卻和暖如春谴分,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背镀脂。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工牺蹄, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人狗热。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓钞馁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親匿刮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子僧凰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 寫在前面 這篇文章算是對(duì)最近寫的一系列Vue.js源碼的文章(https://github.com/answers...
    染陌同學(xué)閱讀 2,136評(píng)論 0 14
  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內(nèi)容,還有我對(duì)于 Vue 1.0 印象不深的內(nèi)容熟丸。關(guān)于...
    云之外閱讀 5,045評(píng)論 0 29
  • 轉(zhuǎn)載說(shuō)明 一训措、介紹 瀏覽器可以被認(rèn)為是使用最廣泛的軟件,本文將介紹瀏覽器的工作原理,我們將看到绩鸣,從你在地址欄輸入g...
    17碎那年閱讀 2,440評(píng)論 0 22
  • 早上一個(gè)許久未聯(lián)系的貨代怀大,在微信上發(fā)信息給我,然后互通了電話呀闻,雖然因?yàn)橐郧耙恍┕ぷ魃系牟挥淇炀蜎](méi)再聯(lián)系化借,但還是常...
    哈哈野閱讀 72評(píng)論 0 0
  • 郭芳艷 焦點(diǎn)網(wǎng)絡(luò)初級(jí)五期 堅(jiān)持原創(chuàng)分享第219天 昨天堅(jiān)持不批評(píng)小高同學(xué),希望能去找到他進(jìn)步的地方捡多,...
    冰山藍(lán)鷹閱讀 148評(píng)論 0 0