编者按:本文作者 Berwin,W3C性能工作组成员,360导航高级前端工程师。Vue.js早期用户,《深入浅出Vue.js》(正在出版)作者。
最近看到一篇国外的文章,说现代JS框架存在的根本原因是保持UI与状态同步、这其实与我这篇文章的思想是一致的,同时也认证了我对现代前端框架的认知是正确的。
现在前端界有三大框架横行,Vue,React,Angular,几乎是所有身为一名前端工程师所必备的一项技能。
但是我不知道有多少人仔细思考过为什么会这样?
现在的一些应届生和刚入行的人们,在刚一踏入前端这个行业起就会面临着是学习Vue还是学习React又或者是学习Angular等这样的选择问题。
事实上在早几年是没有这个问题的,我们不需要选择,那时候我们写前端就是jQuery一把梭,就是干,干就完了。
那为什么现在人们需要选择各种框架了呢?
其实之所以现在我们需要选择框架,本质上是因为我们面临的需求变了。大家肯定都明白如果我们只写一个纯展示信息的页面,没有任何交互功能的页面,其实即便是现在,我们也是不需要选择框架的,我们只需要写几行CSS和HTML就可以完成任务。
所以是因为我们面临的需求变得复杂了,我们的应用经常需要在运行时做一些交互。
这里面有三个很重要的字我标了粗体,叫做运行时(Runtime)。现代的前端开发,我们开发的应用经常需要在运行时来做一些交互,这些交互在早期只是个幻灯片或者Tab切换下拉菜单等一些简单的交互,这些交互用jQuery实现完全没什么问题。但现代的前端我们的目标是用Web去PK原生应用,去和Native进行PK。
那这个时候我们会发现用jQuery来开发应用,我们的代码变得很难以维护,那为什么使用现代框架比如Vue,React等就变得容易维护了呢?
这里面请容我讲一个故事,一个小插曲,前几天我在一个微信群里面有人讨论,Vue和jQuery的区别是什么,有人非常强烈的说什么差别是Vue有组件,有什么这个那个的一些特性。
当时我在微信群里说了我的观点,我说Vue和jQuery之间的区别只有一点,声明式与命令式。
我们可以想一下,我们用jQuery去操作DOM的目的是什么?是为了局部更新视图,换句话说是为了局部重新渲染。
jQuery是命令式的操作DOM,命令式的局部更新视图,而现代主流框架Vue,React,Angular等都是声明式的,声明式的局部更新视图。
为什么声明式的操作DOM就可以让应用变得好维护了呢?
弄明白这个问题首先我们先简单介绍下什么是命令式,什么是声明式。
命令式
命令式,像jQuery,我们都是想干什么然后就去干就完了,例如下面的代码:
代码语言:javascript复制
$('.box') .append('<p>Test</p>') .xxx() .yyy() .jjj() ...
命令式就是想干什么就直接去调用方法直接干就完了,简单直接粗暴。
试想一个很简单的场景,比如一个toggle效果,点击一个按钮,切换颜色。
用命令式写,我们肯定是这样写,如果当前是什么颜色就让它变成另外一个颜色。
如果你仔细思考,其实这里面可以细分成两个行为,一个是对状态判断,另一个是操作DOM。
那什么是声明式??
声明式
声明式是通过描述状态与视图之间的映射关系,然后通过这样的一个映射关系来操作DOM,或者说具体点是用这样的映射关系来生成一个DOM节点插入到页面去。比如Vue中的模板。模板的作用就用是来描述状态与DOM的映射关系。
同样的场景,我们用Vue中的模板来实现,当我们用模板描述了映射关系之后,我们在点击按钮时,我们只需要对颜色这个变量进行修改就可以完成需求。
看到区别了么?
仔细思考下,用Vue来实现同样的需求,如果细分来看,我们在逻辑上只有一个行为,只有状态。而jQuery是两个行为,状态 DOM操作。
所以声明式为什么可以简化维护应用代码的复杂度?
因为它让我们可以把关注点只放在状态的维护上。这样一来当应用复杂后,其实我们的思维,我们管理代码的方式只在状态上,所有的DOM操作都不用关心了,可以说大大降低代码维护的复杂度。
我们不再需要关注怎么操作DOM,因为框架会帮我们自动去做,我们只关注状态就好了。
但是如果应用特别特别复杂,我们会发现即便是我们只关注状态的维护,依然很难,即便只维护状态也很难,所以才出现了Vuex,Redux等技术解决方案。
什么是渲染?
经过前面的介绍,你会发现其实现代主流框架要解决的最本质的问题依然是渲染,只是不同框架之间的解决方案有差异,那么什么是渲染?
现在开发前端,我们的应用在运行时需要不断的进行各种交互,现代主流框架让我们把关注点放在了状态的维护上,也就是说应用在运行时,应用内部的状态会不断的发生变化。
而将状态生成DOM插入到页面展示在用户界面上,这一套流程叫做渲染。
现代前端框架对渲染的处理
当应用在运行时,内部状态会不断的发生变化,这时用户页面的某个局部区域需要不停的重新渲染。
如何重新渲染?
最简单粗暴的解决方式,也是我平时在没有使用任何框架的项目里写的一些简单的功能时最常用的方式是用状态生成一份新的DOM,然后用innerHTML把旧DOM替换了。
我写的小功能块用这种方式没问题,因为功能涉及到的DOM标签少,状态变的时候,几乎就是我这个功能块的所有标签都需要变,所以即便是用innerHTML也不会有太大的性能浪费,是在可接受范围内的。
但是框架不行,框架如果用innerHTML这样去替换,那就不是局部重新渲染了,而是整个页面整体刷新,这性质就变了,那么框架如何做到局部重新渲染?
解决这个问题,需要一些技术方案来解决,可以是VirtualDOM,但并不一定必须是VirtualDOM,也可以是Angular中的脏检测的流程,也可以是细粒度的绑定,像Vue1.0就是使用细粒度的绑定来实现的。
什么是细粒度绑定?
细粒度的绑定意思是说,当某个状态,与之绑定的是页面中的某个具体的标签。就是说,如果模板中有十个标签使用了某个变量,那么与这个变量所绑定的就是10个具体的标签。
相对比较React和Angular粒度都比较粗,他们的变化侦测其实不知道具体哪个状态变量,所以需要一个暴力的比对,比对后才知道需要对视图中的哪个部分进行更新。
而Vue这种细粒度的绑定其实在状态发生变化的那一个瞬间,立刻就知道哪个状态变了,而且还知道有哪些具体的标签使用了这个状态,那么事情就变的简单的多了,直接把与这个状态所绑定的这些具体的标签进行更新就能达到局部更新的目的。
但是这样做其实也有一定的代价,因为粒度太细,会有一定的依赖追踪的开销。所以Vue2.0开始采取了一个折中的方案,就是把绑定调整为中等粒度。
一个状态对应某个组件,而不再是具体标签,这样做有一个好处是可以大大降低依赖的数量,毕竟组件的数量与DOM中的具体标签比,数量要少的多。但是这样就需要多一个操作,当状态发生变化只通知到组件,那么组件内部如何知道具体更新哪个DOM标签??
答案是VirtualDOM。
也就是说,当粒度调整为中等之后,需要多一个操作就是在组件内部使用VirtualDOM去重新渲染。
Vue很聪明地通过变化侦测 VirtualDOM这两种技术方案,提升了框架运行的性能问题。
所以说,Vue2.0引入VirtualDOM并不是因为VirtualDOM有多好,而是恰好VirtualDOM结合变化侦测可以将绑定调整成中等粒度来解决依赖追踪的开销问题。
关于变化侦测我专门写过文章1来介绍Vue是如何实现变化侦测的。
所以变化侦测的方式,在一定程度上就已经决定了框架如何进行渲染。
关于VirtualDOM的实现原理我写过一个PPT2,有兴趣的可以看看。
还有一个是模板编译,其实前面对于模板编译这个问题并没有说太多,模板的作用是描述状态与DOM之间的映射关系,通过模板可以编译出一个渲染函数,执行这个渲染函数可以得到VirtualDOM中所提供的VNode,事实上你看过我前面介绍VirtualDOM原理的PPT你就会知道VirtualDOM对节点进行diff其实是对VNode进行diff。关于模板编译的实现原理我专门写过一篇文章3介绍过。
最后
最后我想说的话是,现在的前端我个人感觉有点浮躁,很多人都在追新,每天关注一些今天出了一个新特性,明天出了一个新框架什么的,对于这些我是赞成的,但是我更希望在追新的同时,要看到它的本质。
所有技术解决方案的终极目标都是在解决问题,都是先有问题,然后在有解决方案,解决方案可能并不完美,可能解决方案有很多种,那么他们之间都有哪些优缺点?解决问题的同时各自都做了哪些权衡和取舍?
我们要透过现象看本质才不至于被表面所迷惑。
文内链接
- https://github.com/berwin/Blog/issues/17
- https://ppt.baomitu.com/d/2afbd5b9
- https://github.com/berwin/Blog/issues/18