关于React中的声明式渲染框架问题

目录
  • 1. 命令式和声明式
    • 1.1 命令式
    • 1.2 声明式
    • 1.3 两种范式的性能和易维护性
  • 2. 虚拟DOM的性能如何
  • 3. 运行时和编译时
    • 3.1 运行时
    • 3.2 运行时 + 编译时
    • 3.3 编译时
  • 4. 总结

在学习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片段编译成命令式的代码呢?答案是可以的,这样就不支持任何运行时内容,用户的代码需要编译才能运行,它就是纯编译时框架了。目前就有一些框架把声明式的代码编译成命令式代码,例如:sveltejssolidjs等框架,它既保持了声明式的易维护特性,又保证了程序的性能。

4. 总结

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

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

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

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

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

(0)

相关推荐

  • React Native中ScrollView组件轮播图与ListView渲染列表组件用法实例分析

    本文实例讲述了React Native中ScrollView组件轮播图与ListView渲染列表组件用法.分享给大家供大家参考,具体如下: 1.Scroll View ScrollView是React Native提供的滚动视图组件,渲染一组视图,用户可以进行滑动响应交互,其常用属性如下: 滚动的偏移量:通过event.nativeEvent.contentOffset.x可以得到水平偏移量. horizontal={bool},属性为true时,所有子视图在水平方向排列,否则在纵向排列.默认为

  • 详解超简单的react服务器渲染(ssr)入坑指南

    前言 本文是基于react ssr的入门教程,在实际项目中使用还需要做更多的配置和优化,比较适合第一次尝试react ssr的小伙伴们.技术涉及到 koa2 + react,案例使用create-react-app创建 SSR 介绍 Server Slide Rendering,缩写为 ssr 即服务器端渲染,这个要从SEO说起,目前react单页应用HTML代码是下面这样的 <!DOCTYPE html> <html lang="en"> <head&g

  • 详解React的回调渲染模式

    一.一个简单的小例子 1.父组件 <Twitter username='tylermcginnis33'> {(user) => user === null ? <Loading /> : <Badge info={user} />} </Twitter> 2.子组件框架 import React, { Component, PropTypes } from 'react' import fetchUser from 'twitter' // fetc

  • 关于React中的声明式渲染框架问题

    目录 1. 命令式和声明式 1.1 命令式 1.2 声明式 1.3 两种范式的性能和易维护性 2. 虚拟DOM的性能如何 3. 运行时和编译时 3.1 运行时 3.2 运行时 + 编译时 3.3 编译时 4. 总结 在学习React源码之前,我们先搞清楚框架的范式都有哪些.框架范式主要有两种:命令式和声明式,目前大部份流行框架都采用声明式渲染,为什么都选择声明式渲染呢?对比命令式它有什么优势呢?为了搞清楚这些问题,我们先从动态渲染页面的三种方式:纯JS运算,innerHTML,虚拟DOM,分别比

  • vue.js声明式渲染和条件与循环基础知识

    vue.js声明式渲染和条件与循环的具体内容,分享给大家 绑定 DOM 元素文本值 html代码: <div id="app"> {{ message }} </div> JavaScript代码: var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } }) 运行结果:Hello Vue! 总结:数据和 DOM 已经被关联在一起,当我们改变app.message的数据,所渲染的的DOM元素

  • Vue声明式渲染详解

    Vue.js 的核心是一个允许采用简洁的模板语法来声明式的将数据渲染进 DOM,也就是将模板中的文本数据写进DOM中,使用  {{data}}  的格式写入.此代码都是Vue.js官网上的实例. 1.首先导入Vue.js <script type="text/javascript" src="vue.js"></script> 2.html和js代码 <body> <div id="id"> //i

  • Compose声明式代码语法对比React Flutter SwiftUI

    目录 前言 1.Stateless 组件 2.Stateful 组件 3. 控制流语句 4. 生命周期 5. 装饰/样式 总结 前言 Comopse 与 React.Flutter.SwiftUI 同属声明式 UI 框架,有着相同的设计理念和相似的实现原理,但是 Compose 的 API 设计要更加简洁. 本文就这几个框架在代码上做一个对比,感受一下 Compose 超高的代码效率. 1.Stateless 组件 声明式 UI 的基本特点是基于可复用的组件来构建视图,声明式 UI 的开发过程本

  • 关于Spring中声明式事务的使用详解

    目录 一.前言 二.回顾JDBC的数据库事务 三.数据库事务隔离级别 3.1 数据库事务的基本特征 3.2 详解数据库隔离级别 3.2.1 未提交读 3.2.2 读提交 3.2.3 可重复读 3.2.4 串行化 3.2.5 各个隔离级别的总结 四.数据库事务传播行为 五.Spring中的声明式事务的使用 5.1 @Transactional的配置属性 5.2 Spring的事务管理器 5.3 配置事务的传播行为和隔离级别 六.总结 一.前言 在Spring中,数据库事务是通过AOP技术来提供服务

  • React 中的列表渲染要加 key的原因分析

    目录 为什么需要 key? 列表渲染不提供 key 会怎样? 列表渲染的 key 用数组索引会怎样? 应该用什么值作为 key? 结尾 在 React 中我们经常需要渲染列表,比如展示好友列表. 常用写法是用 Arrary.prototype.map 方法,将数组形式的数据映射为 JSX.Element 数组,并嵌入到组件要返回的 JSX.Element 中,如下: function FriendList() { const [items, setItems] = useState(['我们',

  • 完美解决Spring声明式事务不回滚的问题

    疑问,确实像往常一样在service上添加了注解 @Transactional,为什么查询数据库时还是发现有数据不一致的情况,想想肯定是事务没起作用,出现异常的时候数据没有回滚.于是就对相关代码进行了一番测试,结果发现一下踩进了两个坑,确实是事务未回滚导致的数据不一致. 下面总结一下经验教训: Spring事务的管理操作方法 编程式的事务管理 实际应用中很少使用 通过使用TransactionTemplate 手动管理事务 声明式的事务管理 开发中推荐使用(代码侵入最少) Spring的声明式事

  • Spring声明式事务和@Aspect的拦截顺序问题的解决

    在使用AbstractRoutingDataSource配置多数据源时,发现使用@aspect配置的DataSourceSwitchAspect总是在声明式事务之后执行,配置了Order依然不行,经过调研发现是由于两者的aop代理方式不一致导致. 在spring内部,是通过BeanPostProcessor(<spring 攻略>一书中翻译为,后处理器)来完成自动创建代理工作的.根据匹配规则的不同大致分为三种类别: 1.匹配Bean的名称自动创建匹配到的Bean的代理,实现类BeanNameA

  • 详解React中Fragment的简单使用

    目录 react 中的 Fragment 标签渲染 Fragment 标签 Fragment 标签和 <></> 区别 react 中的 Fragment 今天说的这一小节超级简单,但是呢,不说还不行,因为在实际开发项目当中你会确确实实的发现有这样一个使用场景,很多人都会写,所以说尽管不影响我们的实际开发,但别人确实会这样操作,为了能更好的看清项目代码,稍微提一嘴吧. 标签渲染 Fragment 标签的使用啊超级简单,就 <Fragment></Fragment&

  • React路由规则定义与声明式导航及编程式导航分别介绍

    目录 1. 路由使用 2. 声明式导航 3. 编程式导航 1. 路由使用 安装路由模块: 路由模块不是react自带模块,需要安装第3方模块: yarn add react-router-dom@5 路由相关组件: 路由模式组件:包裹整个应用,一个React应用只需要使用一次 HashRouter: 使用URL的哈希值实现 (localhost:3000/#/first) BrowserRouter:使用H5的history API实现(localhost3000/first) 导航组件:用于指

随机推荐