起因

最近在写一个博客小网站,使用markdown作为编写语言。纯文本模式下,markdown预览效果实时渲染基本所有的流行markdown渲染库都能做到。但我打算在博客中加入类似LaTeX数学公式,甘特图,EChart图表等组件,这时候就发现传统的全局渲染延迟过大,特别是添加了图后,快速连续输入几个字符,整个预览界面就会出现卡顿,用户体验确实不好。于是花了几天魔改了一下markdown-it,重新实现了渲染逻辑。

相关代码可以在这里查看,也欢迎来我的博客网站rgan.work上体验体验(注册账号啥的随便填)。虽然小破站目前挺简陋的,但起码它能动…

具体流程

0、准备工作

注册几个即将用到的渲染库,目前支持echart,mermaid,flowchart,katex库

// markdown-it
mdRender
// echart
echartRender
// mermaid
mermaidRender
// flowchart
flowchartRender
// katex
katexRender

1、利用Markdown-it,分析生成抽象语法树(AST)

这一步利用Markdown-it的parse功能,分析markdown字符串。

AST上的每个节点都包含以下字段。很多字段我们都是用不到的,只需要关心attrs(生成的html表情的属性),children(该节点的子节点),content(节点内容),tag(html节点标签),nesting(层级),markup(标记符),type(节点类型)就好了

class AstNode {attrs: anyblock: anychildren: anycontent: anyhidden: anyinfo: anylevel: anymap: anymarkup: anymeta: anynesting: anytag: anytype: any
}

实现代码非常简单,一行写完

// 定义
parse (code) : Array<AstNode> {return this.mdRender.parse(code, {})
}
// 使用
let ast : Array<AstNode> = this.parse(code)

2、利用nesting字段,将解析得到的AST进行分块

这里实现比较简单,根据文章的大段落进行分段。nesting会根据解析得到的节点标签进行赋值,如果遇到开标签,如<p>,nesting会在前一个节点的nesting值的基础上+1,反之,遇到闭标签,如</p>,nesting会在前一个节点的nesting值的基础上-1。基于此,我们可以知道,当nesting的累和为0时,说明一个大段落结束,以此进行分块操作

let astBlockArray : Array<Array<AstNode>> = []
let nesting : number = 0
let blocks : Array<AstNode> = []// ast分块
for (let i = 0; i < ast.length; i++) {let astNode : AstNode = ast[i]nesting += astNode.nestingif (nesting > 0) {blocks.push(astNode)} else {blocks.push(astNode)astBlockArray.push(blocks)blocks = []}
}

3.根据分块后的段落进行段落签名的计算

段落签名这块还没想好怎么生成比较好,目前是直接使用了渲染后的html字符串当做段落签名

// ast分块生成签名
// 签名
let signArray : Array<string> = []
// 渲染后的html
let codeArray : Array<string> = []
// 遍历段落
for (let i = 0; i < astBlockArray.length; i++) {let block = astBlockArray[i]let codeStrArr : Array<string> = []let codeStr : string = ''let parentTags : Array<Object> = []// 根据节点属性渲染节点,得到渲染后的字符串for (let node of block) {// 渲染过程比较冗长,这里为了直观,使用renderNode进行替代codeStrArr.push(renderNode(node))}codeStr = codeStrArr.join('')// 生成的签名(直接使用html字符串)signArray.push(codeStr)// 生成的htmlcodeArray.push(codeStr)
}

4、比较新旧节点的段落签名,确定发生变化的段落

let oriChangeNodes : Array<number> = []
let newChangeNodes : Array<number> = []
let newLen = signArray.length
let hisLen = this.historySignArray.lengthif (hisLen === 0) {oriChangeNodes = [0, -1]newChangeNodes = newLen === 0? [0, 0] : [0, newLen - 1]
} else if (newLen === 0) {oriChangeNodes = hisLen === 0? [0, 0] : [0, hisLen - 1]newChangeNodes = [0, -1]
} else if (newLen !== hisLen) {// 渲染块数量不一致,说明新建或移除了段落let newFrontPtr : number = 0let hisFrontPtr : number = 0// 从前往后遍历新旧签名数组,找到第一个不同点的下标while (newFrontPtr < newLen && hisFrontPtr < hisLen && signArray[newFrontPtr] === this.historySignArray[hisFrontPtr]) {newFrontPtr++hisFrontPtr++}// 移除前面找到的相同段落let newArr = signArray.filter((item, index) => (index >= newFrontPtr))let hisArr = this.historySignArray.filter((item, index) => (index >= hisFrontPtr))// 从后往前遍历新旧签名数组,找到第一个不同点的下标let newBackPtr : number = newArr.length - 1let hisBackPtr : number = hisArr.length - 1while (newBackPtr > 0 && hisBackPtr > 0 && newArr[newBackPtr] === hisArr[hisBackPtr]) {newBackPtr--hisBackPtr--}// 映射回原签名数组对应位置newBackPtr += newFrontPtrhisBackPtr += hisFrontPtr// 将修改部分记录下来oriChangeNodes = [hisFrontPtr, hisBackPtr]newChangeNodes = [newFrontPtr, newBackPtr]
} else {// 渲染块数量一致,说明段落内容出现变化for (let i = 0; i < newLen; i++) {if (signArray[i] !== this.historySignArray[i]) {oriChangeNodes = [i, i]newChangeNodes = [i, i]break}}
}
// 没有找到变化位置,退出
if (!(oriChangeNodes.length > 0 && newChangeNodes.length > 0)) {return
}
// changePos:第一个变化段落的下标,changeNum:后续发生变化的相邻段落的数量
let changePos = oriChangeNodes[0]
let changeNum = newChangeNodes[1] - oriChangeNodes[1]

5、根据找到的变化段落,更新Dom节点

// 获取当前的渲染Dom节点列表
let blockDom = document.querySelectorAll(`.${this.CONTAINER_CLASS_NAME}`)
let container = document.getElementById(DomId)
// 将新的节点渲染在文档片段(DocumentFragment)中
let frag = document.createDocumentFragment()
for (let idx = 0; idx <= changeNum && (idx + changePos) < codeArray.length; idx ++) {let b = document.createElement('div')b.className = this.CONTAINER_CLASS_NAMEb.innerHTML = codeArray[idx + changePos]this._renderNode(b)frag.appendChild(b)
}
// 更新预览
if (blockDom.length === 0 || changePos >= blockDom.length) {// 假如当前文档为空,或修改位置在文档末尾,直接添加container.appendChild(frag)
} else {if (changeNum < 0) {// 假如段落数减少了// 先将原有的节点删掉for (let i = 0; i < -changeNum; i++) {container.removeChild(blockDom[changePos + i])}blockDom = document.querySelectorAll(`.${this.CONTAINER_CLASS_NAME}`)if (newChangeNodes[1] >= 0 && newChangeNodes[0] >= 0) {// 直接修改旧节点的内容for (let i = newChangeNodes[0]; i <= newChangeNodes[1]; i++) {if (signArray[i] !== this.historySignArray[i]) {blockDom[i].innerHTML = codeArray[i]this._renderNode(blockDom[i])}}} else {// 假如原文档已被清空,直接插入新文档内容container.insertBefore(frag, blockDom[newChangeNodes[1]])}} else {// 段落数增加或无变化,直接将原节点覆盖container.replaceChild(frag, blockDom[changePos])}this.historySignArray = signArray
}

以上就是整个渲染引擎的框架实现了。由于篇幅关系,某些具体的实现,例如节点的渲染,EChart等图表的渲染在这里就略过,有兴趣可以直接到GitHub上查看相应的代码。这个东西近期打算重新编写成一个独立的库,欢迎各位大佬围观,如果能顺手star一下那就更好了(雾)。也欢迎大家关注一下我的小破站摸鱼好地方,提个issue啥的,共建一个好用的技术资源分享社区

markdown实时分块渲染引擎相关推荐

  1. 【渲染引擎】Blender的2021年最佳渲染引擎(上)

    Blender最终摆脱了"古怪的孩子"的装束,并穿上了更为严肃和受人尊敬的" 3D强者". 它已在业界获得广泛认可,许多工作室和艺术家正在将其纳入他们的产品线. ...

  2. ue4渲染速度太慢_推介飞向月球纪录片基于Unreal实时渲染引擎的三维流程化制作...

    作者:中央电视台 葛小丁 2019年1月3日上午10点26分,"嫦娥四号"探测器成功着陆在月球背面东经177.6度.南纬45.5度附近的预选着陆区,并通过"鹊桥" ...

  3. Filament 实时渲染引擎介绍~~

    作者:_子宽 来源: https://blog.csdn.net/u010281174/article/details/107847966 摘要 Filament是一款Google开发的跨平台的实时渲 ...

  4. opengl游戏引擎源码_跨平台渲染引擎之路:拨云见日

    前言 最近在工作中越来越多地接触到一些3D以及相比常见特性更酷炫的效果,因此萌发了想要自己从0开始打造一个渲染引擎的念头,一方面是为了更好地实现公司业务的需求,另一方面则是可以学到整个渲染流水线上的方 ...

  5. 美摄云非编系统——网页端实时编辑渲染方案

    美摄云非编是一款新型网页端非线性编辑工具,应用WebAssembly技术实现网页端直接渲染图像.本次LiveVideoStackCon 2020线上峰会我们邀请到了北京美摄网络科技有限公司的研发总监黄 ...

  6. (一)WaveDrom 数字时序图渲染引擎

    专栏:WaveDrom 下一篇:(二)WaveDrom Editor使用教程 WaveDrom 数字时序图渲染引擎 1. WaveDrom介绍 WaveDrom官网 https://wavedrom. ...

  7. 【专访蓝景科技】5G+实时云渲染赋能数字孪生,共建元宇宙

    2021年伊始,元宇宙 概念不断扩展探讨与深入,国内外科技巨头扎堆布局元宇宙. 元宇宙第一股------ROBLOX 上市.字节跳动 90亿收购国内TOP1VR厂商PICO.FACEBOOK改名为ME ...

  8. 专访深职院XR专家 | 实时云渲染赋能虚拟仿真实训,打造5G+XR智慧教育平台

    "职业教育与普通教育是不同教育类型,具有同等重要地位,是国民教育体系和人力资源开发的重要组成部分,是培养多样化人才.传承技术技能.促进就业创业的重要途径."--<中华人民共和 ...

  9. 【我的渲染技术进阶之旅】基于Filament渲染引擎绘制一个不停旋转的彩色矩形

    一.绘制三角形回顾 在上一篇博客 [我的渲染技术进阶之旅]Google开源的基于物理的实时渲染引擎Filament源码分析:Android版本的Filament第一个示例:sample-hello-t ...

  10. Filament 渲染引擎简介

    Filament 是一款基于物理的实时渲染引擎.引擎核心主要使用C++开发完成.支持平台包括 Android,IOS,Linux,macOS, Windows, and WebGL.尤其对Androi ...

最新文章

  1. 最近面试一个6年 Java程序员,一个问题都答不上!
  2. SAP MM 条件类型中PB00的‘Group Cond.‘标记的作用?
  3. MyBatis-25MyBatis缓存配置【集成Redis】
  4. java精准查询mysql时间_在mysql查询中查找与指定日期时间最接近的日期时间
  5. php 建立自己的框架,利用 Composer 一步一步构建自己的 PHP 框架(一)——基础准备...
  6. Redis底层实现--字符串
  7. Linux上构建一个RADIUS服务器详解
  8. leetcode[232]用栈实现队列/Implement Queue using Stacks
  9. 纪念学海生涯的最后一次盲审抽签
  10. win10右下角的天气怎么关闭
  11. word中取消链接上一节在哪_在WORD中怎样取消与上一节相同
  12. 程序员必知之浮点数运算原理详解
  13. ubuntu借助windows的网络共享上网
  14. 逻辑推理:张老师的生日
  15. 数据中心100G主流应用技术分析
  16. [pytorch]yolov3.cfg参数详解(每层输出及route、yolo、shortcut层详解)
  17. python使用tkinter库,封装操作excel为GUI程序
  18. 重邮大学计算机基础考试试题及答案,重庆邮电大学《大学计算机基础(2015》考试试卷.pdf...
  19. 鬼影病毒和浏览器锁狼狈为奸,用户浏览器遭强行劫持
  20. 低温烹饪过程中真空压力的自动控制

热门文章

  1. 虚拟机安装教程(VM15.5+Ubuntu16.04)
  2. 二阶系统响应指标图_频率响应介绍_二阶系统的频率响应
  3. 解决plsql使用无法导出DMP
  4. 初中计算机课堂游戏设计方案,初中信息技术教案设计
  5. 使用 ReportLab 绘制 PDF
  6. CS61a-2020fall学习笔记
  7. python 使用 .qrc文件
  8. 【51单片机】矩阵键盘
  9. 信源编码程序设计实验C语言实现,霍夫曼信源编码实验报告.docx
  10. python控制51单片机的红绿灯_基于51单片机的交通灯控制设计