在学习React源码之前,我们先搞清楚框架的范式都有哪些。框架范式主要有两种:命令式声明式,目前大部份流行框架都采用声明式渲染,为什么都选择声明式渲染呢?对比命令式它有什么优势呢?为了搞清楚这些问题,我们先从动态渲染页面的三种方式:纯JS运算,innerHTML,虚拟DOM,分别比较他们的性能可维护性心智负担,来阐明基于虚拟DOM声明式渲染的优势。然后会说到与声明式框架密切相关的运行时和编译时。相信看完你会对React、Vue这一类采用虚拟DOM的声明式框架有自己的理解。

1. 命令式和声明式

在对比之前,我们先了解一下什么是声明式,什么是命令式,它们各有什么优缺点。作为框架学习者,了解这两种范式的框架对学习框架思想很有帮助。 我们先看看命令式和声明式框架的概念和具体形式。

1.1 命令式

什么是命令式?早年间大范围流行的JQuery就是典型的命令式框架,命令式框架最大的特点是关注过程,例如我要做如下DOM操作:

  • 获取id为app的div元素
  • 把元素的显示文本设置为 hello world
  • 给他绑定点击事件
  • 事件内容是弹窗提示ok

用jQuery可以写出如下代码:

$("#app") // 1
.text("hello world") // 2
.on('click', function(){ // 3
    alert("ok") // 4
})

可以看到自然语言描述能够跟实际写代码一一对应起来,写代码本身就是在描 述做事的过程,这很符合日常生活的直觉和逻辑。而且整个过程没有任何其他的性能开销,因此命令式框架的性能一搬都非常不错。

1.2 声明式

什么是声明式?与命令式框架不同关注过程不同,声明式框架更 关注结果 ,所见即所得。按照框架的规范,声明出用户想要的结果,具体怎么实现无需关心,都交给框架处理。 同样上面的那个例子里面,用React这种声明式的框架可以这样写:

<div id="app" onClick={()=>alert("ok")}>hello world</div>

在react里面一般都是用JSX描述页面的dom结构。可以看到我们只需提供一个最终的“结果”,至于具体怎么实现这个结果的过程,我们并不需要关心。换句话说React框架帮我们封装了实现的过程,因此应该能够猜到React框架内部实现一定是命令式的,但是暴露给用户的是更加直观的声明式。

1.3 两种范式的性能和易维护性

我们首先抛出一个结论:声明式代码的性能不优于命令式代码的新能。为什么呢?还是拿上面的例子来说,如果要把文本内容改为:“react”,用命令式代码就很简单,因为用户明确的知道要修改的是什么,直接调用相关api即可:

app.textContent = 'react'

试想一下,有没有其他的实现方式比这个代码性能更好的?答案是没有,因为我们明确的知道是哪些地方发生了变化,直接修改变化的地方就行, 因此命令式的代码能做到极致的性能优化。但是声明式的代码目前还做不到这一点,因为它表述的是结果:

// 之前
<div id="app" onClick={()=>alert("ok")}>hello world</div>
// 之后
<div id="app" onClick={()=>alert("ok")}>react</div>

对于框架来说,为了实现最好的更新性能,框架需要找到新旧DOM的差异,并且只更新有差异的地方,最终还是用命令式的代码完成这次变更:

app.textContent = 'react'

如果把修改文本的性能消耗为A,找出新旧内容差异的性能损耗为B,那么会有如下公式:

  • 命令式的代码更新性能为:A
  • 声明式的代码更新性能为:B A

可以看出,声明式代码比命令式代码多了找出差异的性能消耗,最理想的情况是查找差异性能的消耗为0,此时命令式代码和声明式代码的性能相同,但是无法超过命令式代码。因为框架本身封装了命令式的代码才实现了面向用户的声明式,这也侧面印证了之前的结论:声明式代码的性能不优于命令式代码的新能

既然命令式代码性能这么好,又直接,为啥还有类似React,Vue这样的声明是框架呢?原因是声明式代码的可维护性更强。从之前的例子可以看出,采用命令式代码实现的时候,我们需要关注整个实现过程的每一步,包括DOM元素的创建、获取、更新、删除等操作,过程繁琐而且抽象,心智负担高。而声明式代码展示就是最终我们想要的结果,更加直观,只关注结果效率高,而实现结果的命令式的代码框架内部已经实现,不需要用户关心。

但是声明式代码在提升可读性和维护性的同时,面临的问题是性能上有一部分损耗,所以框架要做的是:保持可维护性的同时让性能损耗最小。在这种前提下,就有人提出了 虚拟节点(Virtual DOM) 这种找出新旧差异的方案,并被广泛运用于React,Vue这类框架之中。那么虚拟DOM的性能到底如何呢?

2. 虚拟DOM的性能如何

说到这里相信大家有一个基本的了解,那就是采用虚拟DOM的框架更新新时,理论上性能不会比原生JS操作dom性能更好,理论上是指用户写的命令式代码是绝对优化的。在实际场景中这很难,可能需要投入巨大的精力,所以投入产出比不高,目前只谈理论性能。
那么有没有一种办法既不需要投入太大的精力,又能保证代码程序的性能下限,不至于让应用程序性能太差。甚至经过一定的优化处理,接近命令式代码的性能呢?其实这就是虚拟DOM要解决的问题。

上文说的原生JS操作dom的命令式代码,指的是document.createElement等方法,不包括innerHTML这个方法,它比较特殊,需要单独探讨它。在早年使用JQuery或直接写原生JS代码的时候,innerHTML操作dom是非常常见的。那么我们可以考虑以下几个问题:

  • innerHTML的渲染流程是什么样的?
  • innerHTML的性能相比较虚拟DOM谁的性能好?

首先对于第一个问题,对于innerHTML创建页面,需要先构造一段HTML字符串:

let htmlStr = '<ul>'
for(let i=0; i<data.length; i  ) {
   htmlStr  = `<li>${data[i].name}</li>`
}
htmlStr  = '</ul>'

然后把这个字符串赋值给dom元素的innerHTML属性:

app.innerHTML = htmlStr

在赋值之后,由于要渲染出页面,首先要吧字符串解析成DOM树,这一步是DOM层面的计算。然而,涉及DOM的运算性能要远比JS层面的计算性能差很多,我们可以在jsbench.me这个网站上给它们跑个分,比较创建10000个js对象和10000个dom元素的性能,结果如下:

我们可以看出,纯JS运算要比操作DOM快得多,他们不在一个数量级上。基于这个前提,我们可以得出innerHTML创建页面的性能为:拼接HTML字符串的计算量 innerHTML 的 DOM计算量

我们再看第二个问题,innerHTML的性能相比较虚拟DOM谁的性能好?我们再看一下虚拟dom创建页面的过程。第一步,先创建JS对象,这个对象是对真实DOM的描述,也就是大家说的虚拟DOM,第二部是递归JS对象并创建所有对应的真实DOM。我们也可以用一个公式来表述他们的性能消耗:创建JS对象的计算量 创建真实DOM的计算量

1.比方说有这样一个虚拟dom对象:

const vdom = {
  type:'ul',
  children: {
    type: 'li',
  }
}

2.递归对象创建真实DOM渲染到页面

function render(vdom, anchor){
 const el = document.createElement(vdom.type)
 anchor.appendChild(el)
 if(vdom.children){
   render(children, el)
 }
}
render(vdom)

我们列一个表格对比一下纯JS运算、虚拟DOM和innerHTML创建页面时所消耗的性能:

纯JS运算 虚拟DOM innerHTML
  创建js对象(vdom) 渲染HTML字符串
DOM运算 创建所有DOM元素 创建所有DOM元素

我们可以看出虚拟DOM和innerHTML创建页面时流程差不多,性能两者差别不大。在相同数量级上面,基本上没有什么区别,因为都要新建所有的DOM元素。

看到这里可能有人会说,性能都差不多那还要虚拟DOM干嘛,这不是多此一举嘛。别急,上面说的的新创建DOM。在都是新创建所有的DOM元素来说虚拟DOM对比innerHTML在性能上确实没有任何优势可言。但是在我们更新页面的时候,哪怕我们只改了一个字,用innerHTML这种方式更新页面时,要先销毁之前所有的DOM元素,然后根据新的html字符串重新创建所有的DOM。我们再看看虚拟DOM是怎么更新页面的,它需要重新创建js对象(vdom),然后比较新旧虚拟DOM,找到变化的元素然后更新它。如下面这个表格所示:

纯JS运算 虚拟DOM innerHTML
  1. 创建js对象(vdom)
2. Diff找出变化的部分
渲染HTML字符串
DOM运算 只更新变化的部分DOM 1. 创建所有的新DOM元素
2. 创建所有的新DOM元素

在页面更新的时候,采用虚拟DOM更新页面,由于经过JS计算出哪些DOM元素需要更新,只需要更新对应的DOM元素即可。而采用innerHTML这种方式需要先销毁所有的DOM元素,然后又创建所有DOM。综合之前的JS运算比DOM运算的性能快的多的结论下,这时候虚拟DOM的优势就提现出来了。

此外,当页面页面更新时,影响虚拟DOM的性能因素与影响innerHTML的性能因素补贴。对于虚拟DOM来说,无论页面多大,只更新变化的内容,所以性能跟变化内容的大小有关。对innerHTML这种方式来说,就不关系变化内容的大小了,只关心要渲染性能跟html字符串的大小有关。

纯JS运算 虚拟DOM innerHTML
  1. 创建js对象(vdom)
2. Diff找出变化的部分
渲染HTML字符串
DOM运算
性能因素
1. 只更新变化的部分DOM
2. 与数据变化量相关
1. 创建所有的新DOM元素
2. 创建所有的新DOM元素
3. 与模板大小相关

基于上面的描述,我们可以总结一下原生JS(指createElement等方法)、虚拟DOM、innerHTML这三个方法在更新页面时候的性能,如下表所示:

纯JS运算 虚拟DOM innerHTML
心智负担大 心智负担小 心智负担小中等
性能最好 性能不错 性能差
可维护性差 可维护性强 可维护性一版

我们分了一个维度去考量:心智负担、可维护性、性能:

  • 对于纯JS运算,毫无疑问原生JS的DOM操作这种方式心智负担最大,因为需要手动增删改查大量的DOM元素。但它的性能是最好的,不过要承受巨大的心智负担,而且代码可能读性很差,不便于后期维护。
  • 对于innerHTML,由于有一部分是拼接字符串来实现的,有点类似于声明式的代码了,但也存在着一定的心智负担,而且其他的DOM操作(绑事件,增加属性等)还是得通过原生JS来处理。此外如果html字符串如果很大的话还可能有性能问题。
  • 对于虚拟DOM:由于虚拟DOM是声明式的,心智负担比较小,可维护性强,性能虽然比不上极致优化的原生JS,但是在页面更新的时候也有着不错的性能。

一番权衡之后,发现虚拟 DOM 是个还不错的选择。这也是大部份流行框架采用虚拟DOM的原因。

可能有的人要问了,有没有一种方法能做到:既可以声明式的描述UI结构,同时又具备原生JS的性能呢?这些问题在下一节讨论。

3. 运行时和编译时

我们先来说一下纯运行时的框架。假如我们设计了一个框架,它提供了一个Render函数,用户只要传入虚拟DOM,Render函数就会递归创建真实DOM把它插入到对应的节点,还是拿之前的代码为例:

3.1 运行时

1.虚拟dom对象:

const vdom = {
  type:'ul',
  children: {
    type: 'li',
  }
}

2.创建真实DOM:

function render(vdom, anchor){
 const el = document.createElement(vdom.type)
 anchor.appendChild(el)
 if(vdom.children){
   render(children, el)
 }
}
render(vdom)

3.2 运行时 编译时

在浏览器上运行这段代码可以看到预期的结果。但是有人会说,写这样的dom描述对象太不直观了,而且手写起来很麻烦。有没有一种方式能够支持写HTML就能得到dom描述对象呢?答案是有的,我们可以引入编译手段,将写好的HTML编译成dom描述对象,再把这个对象交给Render函数,将他渲染到页面上。流程如下:

  • 写了一个Compiler程序,将HTML声明式的代码变成了产出dom描述对象的函数:
const el = <div id="app" onClick={()=>alert("ok")}>react</div>
const vdom = Compiler(el) 
  • 上面的vdom就会编译成如下结果:
{
  type: 'div',
  props: { 
    click: () => alert("ok"),
    chilren: ['react']
  }
}
  • Render函数传入编译得到的dom描述对象就可以把之前声明式的dom节点渲染到页面上了。
Render(vdom, container)

实际上面就是 运行时 编译时 框架的基本工作流程。用户可以选择提供dom描述对象或者写HTML片段,来描述UI界面,如果是dom描述对象就直接渲染,如果是HTML片段就先编译再渲染。代码运行起来才进行编译,叫做运行编译时,这会产生一定的性能开销,所以有些框架可以在构建的时候执行Compiler将提供的内容 提前编译好,运行的时候就无需编译了,这对程序性能也有一部分提升。像React,Vue就是这么做的。

3.3 编译时

有人可能会问了,既然能把能把HTML片段编译成dom描述对象,那为啥不直接HTML片段编译成命令式的代码呢?答案是可以的,这样就不支持任何运行时内容,用户的代码需要编译才能运行,它就是纯编译时框架了。目前就有一些框架把声明式的代码编译成命令式代码,例如:sveltejs、solidjs等框架,它既保持了声明式的易维护特性,又保证了程序的性能。

4. 总结

我们先讨论了命令式和声明式这两种范式的差异,其中命令式更加关注过程,而 声明式更加关注结果。命令式在理论上可以做到极致优化,但是用户要承受巨大的心智负担;而 声明式能够有效减轻用户的心智负担,但是性能上有一定的牺牲,框架要想办法尽量使性 能损耗最小化。

后面,我们讨论了虚拟 DOM 的性能,并给出了一个公式:声明式的更新性能消耗 = 找出 差异的性能消耗 + 直接修改的性能消耗。虚拟DOM 的意义就在于使找出差异的性能消耗最小 化。我们发现,用原生JavaSoript操作DOM 的方法(如 document.createElement )、虚拟 DOM 和 tnnerHTML 三者操作页面的性能,不可以简单地下定论,这与页面大小、变更部分的大小都有关 系,除此之外,与创建页面还是更新页面也有关系,选择哪种更新策略,需要我们结合心智负担、 可维护性等因素综合考虑。

再后面了解了运行时和编译时的相关知识和各自的特点。

下一节我们着重来说一下React声明式框架是如何将JSX创建虚拟DOM,以及虚拟DOM是怎么渲染到页面上的。

到此这篇关于React中的声明式渲染框架的文章就介绍到这了,更多相关React渲染框架内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

关于React中的声明式渲染框架问题的更多相关文章

  1. xcode6.1 – Xcode 6.1中项目模板中缺少类前缀

    项目模板上曾经有一个类前缀字段,这有助于区分项目类和框架类.Xcode6.1项目模板中不再提供此功能.这背后的意图是什么?

  2. ios – 伞框架

    错误.应用程序,通常位于…错误仍然存在你也可以在这里添加(子)框架的路径.

  3. ios – UIView框架大小的问题

    我正在开发一个iPad项目,目前正在使用Landscape视图.我试着这样做:为什么这总是返回960?虽然在景观中,视图本身的高度尺寸应为768对吗?

  4. 安装自定义cocoa框架的最佳方法

    我有一个自定义框架,遵循Apple的框架编程指南>>中的建议.Installingyourframework我在/Library/Frameworks中安装.我通过使用以下脚本添加RunScript构建阶段来完成此操作:在我的项目中,我然后链接/Library/Frameworks/MyFramework并将其导入我的类中,如下所示:这非常有效,除了我总是在调试器控制台中看到以下消息:Loadin

  5. ios – React native链接到另一个应用程序

    如果是错误的,有人知道如何调用正确的吗?

  6. ios – 在设备上构建和运行时,仅将嵌入式框架与其他动态框架链接失败

    TL;博士将您的嵌入式框架与其他框架链接,并且不将其他框架与您的应用程序链接,导致Build&在设备上运行.描述:建立:我的设置非常简单(Swift2.3&XcodeXcode8.0;Build版本8S162m):>使用Carthage(0.17.2)我用xcodebuild8.0和TOOLCHAINS=com.apple.dt.toolchain.Swift_2_3carthagebui

  7. iOS 8嵌入式框架中的头文件

    我正在尝试创建一个用于iOS8的嵌入式框架.在创建一个名为SampleKit(BTW;这里有任何约定,我应该使用前缀吗?)之后,它包含一个令我困惑的头文件:我知道FOUNDATION_EXPORT是extern或extern“C”的宏,但我不确定这两个常量.我应该在哪里为他们设定价值?解决方法项目>构建设置>版本控制>当前项目版本:

  8. 在Monotouch上模拟.NET的框架?

    有没有人使用过他们发现与Monotouch兼容的.NET模拟框架?在尝试使用之前,我很好奇与NMock,NSubstitute,Moq和其他框架的兼容性.Xamarin刚刚加强了它的单元测试支持,但没有提到模拟框架.仅供参考,我希望在VS2010上为非UI位做很多开发,并在UI进入时移动到iOS平台.谢谢您的帮助.解决方法我建议只使用手动模拟:如果我不得不猜测RhinoMocks,Moq等大量使用Reflection.Emit(你怎么能做他们能做的疯狂?),这将无法在MonoTouch上使用AOT编译器运

  9. 在ios上使用来自框架的boost :: filesysystem路径

    我一直在使用Boost作为PeteGoodliffe脚本构建的框架已有一段时间了.效果很好.最近我遇到了一个问题,可以通过将以下代码放入另一个全新的XCode项目中的视图控制器的viewDidLoad中来重现:当路径对象被销毁时会导致EXC_BAD_ACCESS.有没有其他人遇到这个问题?

  10. ios – 在约束依赖于框架的自定义视图中使用自动布局

    我正在编写一个以编程方式初始化的自定义视图.我重写updateConstraints以添加此视图所需的所有约束.:问题是self.bounds返回CGRectZero的等价物.我做了我的研究并根据这个objc.ioarticle,这是预期的,因为在调用layoutSubviews之前框架不会被设置.它也提到了Toforcethesystemtoupdatethelayoutofaviewtreei

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部