Reac-diff-algorithm

0 前言

React的VirtualDOM可谓是前端领域中革命性的创新,而高效的Diff算法又极大的优化了状态的对比,不同的状态对应的就是不同的UI界面。本文将简要介绍Diff算法中的精华。

1 Diff算法

1.1背景

“树”是一种常见的数据结构,对比”树”的结构在计算机相关相关领域有广泛的应用。具体到前端开发而言,Web的UI界面就是由DOM树构成,DOM作为一种有序树结构,当DOM树结构发生变化时,浏览器会重新解析渲染DOM树。造成有序树的节点变化的基本操作方式有三种:重标记(relabel),删除(delete)和插入(insert),如下图自上而下所示。

重标记(relabel L1 to L2)

重标记

删除节点(delete node L2)

删除节点

插入节点(insert L2 to L1)

插入节点

1.2 标准的Diff算法

标准的Tree-Diff算法,使用了严谨的代价函数(Cost Function),适用于很多需要树结构对比的场景,如:计算生物学、图像分析、结构化文本数据库等。然而其算法的时间复杂度为O(n^3),在浏览器环境中,要达到每次更新都可以整体刷新界面的目的,这样高的算法复杂度会有性能问题。

2 React Diff算法(reconciliation algorithm)

React开发团队结合Web界面的特点对标准的Diff算法做出了优化,使得其时间复杂度降低为O(n)。这一算法,即reconciliation algorithm,常见中文翻译为“协调算法”,个人更倾向于“权衡算法”这一更形象的翻译,因为该算法所实现的就是权衡利弊之后的简化。这种算法的基本假设如下:

  • Web UI中DOM节点跨层级的移动操作较少,可以忽略。
  • 两个相同组件产生类似的DOM结构,不同的组件产生不同的DOM结构。
  • 对于同一层次的一组相同类型的子节点,它们可以通过唯一的key进行区分。

2.1 单一节点对比

为了对比两个树结构,需要先能够比较两个节点,在React中就是Virtual DOM树(以下简称“VD树”)的节点,而VD树节点的不同又分两种情况:

  • 节点类型不同
  • 节点类型相同但属性(props)不同

2.1.1 节点类型不同

当VD树中同一节点位置前后节点类型不同时,React直接删除旧的节点,然后创建并插入新的节点,使用的节点基本操作方法为删除和插入。

1
2
3
renderA: <OldNode />
renderB: <NewNode />
=> [removeNode <OldNode />], [insertNode <NewNode />]

在React组建的生命周期方法中,老节点OldNode的componentWillUnmount()方法最先触发,之后新节点NewNode的componentWillMount()和componentDidMount()方法依次触发。

2.1.2 节点类型相同但属性不同

节点类型相同但属性不同的情况下,React会对VD数节点的属性进行重设,从而实现节点的转换,使用的节点基本操作方法为重标记。因为React组件的实例保持不变,该组件的state也会保持不变,组件的componentWillReceiveProps()、componentWillUpdate()、render()方法依次触发。

1
2
3
renderA: <Node className="old" />
renderB: <Node className="new" />
=> [replaceAttribute className "new"]

VD树节点的style属性稍有不同,因为传入的是对象而不是字符串,React会只更新改变了的样式属性。

如果开发者能够明确节点是否有变化,则可以通过shouldComponentUpdate(nextProp, nextState)方法来决定是否进行Diff运算。

2.2 逐层进行节点对比(分层求异)

React只会对同一层级(下图中相同颜色方框内)的DOM节点进行比较,即同一个父节点下的所有子节点。如果发现已经不存在的节点,则该节点及其子节点会被完全删除掉,不再作进一步的比较。这样只需要对树进行一次遍历,便能完成整个DOM树的比较。这一方法正是基于前一假设,即不同类型的组件产生不同的VD树结构。
逐层进行节点对比

例如,在下图的VD树结构转换过程中,看似应该是把整个A节点从根节点插入到D节点中,最直观的操作方式应该是:

1
Root.remove(A).find(D).append(A)


然而,React的“权衡算法”只会简单对比同一层级的节点,不同层级的节点只有删除和插入。所以,React实际的操作方式却是直接删除节点A,至于A的子节点B和C则直接被忽略了;而当发现D节点多了一个子节点A时,则会插入一个新的节点A作为自己点。过程如下:

1
2
3
4
5
A.destroy();
A = new A();
A.append(new B());
A.append(new C());
D.append(A);

这样的操作看似是把简单的问题变得复杂了,在这一例子中的确如此,毕竟根据NFL定理(no free lunch theorem),任何优化都是有代价的,Reconciliation algorithm也不例外。之所以称之为“权衡算法”,也正是因为这一方法以忽略不同类型节点的子节点的对比为代价,进而大大降低了Diff算法的时间复杂度。

鉴于这一情况,为提高性能,应尽量避免进行DOM节点跨层级的操作。此外,保持稳定的DOM结构也有助于性能提升,可以通过类名和CSS来控制节点的显示隐藏。

2.3 列表节点的对比

2.1和2.2中分别介绍了React diff算法对单一节点,和不在同一层中的节点的对比方式。与前两者不同的是,一组列表节点往往有相同的类型和属性,此外,列表节点的常见操作包含插入、删除和排序。如果每个列表节点没有唯一标识,则React无法识别每一个节点,那么只能更新所有列表节点造成效率低下。

例如,要在下图的列表节点B和C中间插入节点F,如果React无法识别每一个节点,则只能依次更新所有节点,如果节点数量很大,会有明显的性能损耗。

这时,“key”这个属性就要登场了,React通过key这个属性发现更新前后相同的列表节点,因此无需进行相同节点删除和创建,只需要将旧的节点的位置进行移动。如果有旧的节点被删除或新的节点被插入,也不会造成其他节点被重新创建。
再上图的例子中,React会通过每个节点的唯一标识(key),准确找到插入新节点F的位置,同时保留其他节点。

如果列表节点能够获取到唯一的id属性,那么选择id作为key最好不过,如果没有id或较难获取,则退而求其次选用.map(item, index)方法中的索引index作为key。

3 总结

  • React Diff 通过“权衡算法”,把复杂度O(n^3)的问题转化为复杂度O(n)的问题;
  • 通过”分层求异“策略进行优化,假设不同类型的组件产生不同的DOM树结构,主动放弃了对不同父节点的子节点的对比;
  • 开发组建时,保持稳定的DOM结构、避免跨层级移动DOM节点,有助于性能提升;
  • 通过设置唯一key的策略,对列表节点进行了diff算法优化。

by kouguandong@dtcj.com

近期状态

“前端”

回想2015年的上半年,还不知道有前端工程师”这一职业,转眼现在已经入行一整年了。做的事情从打杂到现在可以独立承担大型复杂项目,使用的技术也从jQuery、AngularJS到React全家桶。

其他

业余时间看了些Udacity上的课程,机器学习入门、d3.js数据可视化、前端性能优化等。
技术发展的方向都是进一步解放生产力,人类劳动则是从”手动板砖”到”制造搬砖机——可以批量化搬砖的机器”,再到”制造可以批量化生产搬砖机的机器”。

为什么要写博客

主要是以下几点原因

1. “输出”式学习与思考

大部分的传统学习方式都是”输入”,即从外界汲取信息的过程;而写博客则是一种强制”输出”,即把头脑中的信息整合重组,并以一种能让别人和以后的自己能够看明白的方式写出。而对”信息整合重组”的过程,就是对信息进行系统性加工学习的过程。

2. 写作表达与记录

平时的工作是以解决实际问题为导向,这类问题一般不很复杂却相对零散,往往可以通过搜索引擎或者请教他人得到解决。由于对写作表达也不够重视,加之写作能力下降,那些解决过的实际问题也就如过眼云烟了。然而之后遇到类似的问题,可能还要重新搜索和问一遍,这种情况表面上是”没有记住”的问题,实际上很可能是没有总结记录造成的。

3. 新型交流互动方式

现在人们工作越来越忙,想找个彼此都方便的时间交流并不容易。而写博客和看别人的博客则不受时间限制,甚至还能取得比其他方式更深入的交流效果。而且博客具有”一次编写,无限次阅读”的特点,对比而言,一对一的交流则可能要队同样的内容进行重复表达。当然,这并不意味着其他交流方式就不重要。

4. 可以形成正向反馈

大牛们普遍都有坚持写博客的习惯,在别人看来这是一种脚踏实地不断进步的表现;而在他们本人看来,日积月累写出的文章对自己也是一种激励,可以形成良性循环。此外,他们写的博客在技术或其他方面,也确实让很多人受益匪浅。

搭建基于Hexo和GitHub的博客

快速开始

0. 预先准备

  • 安装Node.js,前端开发者必备
  • 安装Git,最常见的版本管理工具
  • 登录GitHub账号,创建名为<github username>.github.io的代码仓库,<github username>为该账号用户名

    以上工具对大部分程序员而言都很常见,具体方法可到各自官网查看,这里不做展开

1. Hexo博客初始化

1.1 全局安装Hexo命令行工具

1
$ npm install -g hexo-cli

1.2 使用命令行”cd”到需要创建Hexo博客项目的路径下,执行

1
$ hexo init

这一操作会从GitHub上的官方Hexo项目拉取用于项目初始化的代码,其中包含名为package.json的描述性文件

1.3 依然在该路径下,执行

1
npm install

这一操作将根据package.json文件,安装Hexo博客所依赖的工具,完成时项目结构如下所示

1
2
3
4
5
6
7
8
.
├── _config.yml
├── package.json
├── scaffolds
├── source
| ├── _drafts
| └── _posts
└── themes

其中,_config.yml为配置描述文件;package.json为前述包管理工具文;scaffolds为文章的”脚手架”;source文件夹用于配置博客网站内容;themes文件夹用于文章的样式。

1.4 查看本地博客,执行

1
hexo server

即可在localhost:4000看到本地的Hexo博客网站。

2. 关联GitHub账号

2.1 安装 hexo-deployer-git 工具,用于部署博客到GitHub

1
$ npm install hexo-deployer-git --save

2.2 配置_config.yml文件,在底部 deploy部分做如下修改

1
2
3
4
5
deploy
type: git
repo: 代码仓库路径,可在settings部分点击"clone or download"看到,如:https://github.com/Kou-Guandong/kou-guandong.github.io.git
branch: master
message: 任意字符串信息,如 "deploy finished"

2.3 发布博客到线上环境,运行

1
hexo deploy

即可发布本地项目文件到远程博客”https://username.github.io",username为GitHub用户名。

3. 发布新文章与个性化配置

3.1 编写新博客

1
hexo new "blog-title"

其中,”blog-title”为博客标题。

3.2 通过修改_config.yml文件,个性化配置博客。

_config.yml文件中的都很容易看懂,这里以Site模块为例

属性名称 对应内容
title 博客大标题
subtitle 博客子标题
description 博客描述
author 作者名字
language 博客网站语言
timezone 博客网站所在时区

3.3 将本地更新(文章或配置)推到远程

生成静态资源文件

1
hexo generate

部署到 username.github.io

1
hexo deploy

更多信息可查看Hexo官方文档

化工硕士为什么选择做程序员?

原文发表于简书

1. 代码的爱恨历程

2010年春

  第一次接触编程是大一下学期的C语言课程,当时几乎是完全听不懂。等号不表示等于而成了“赋值”,开头一长串#include<stdio.h> int main(void){...}是干嘛的,#<>(){}这些符号又有啥讲究,为什么写那么一堆就为了输出个 hello, world? 做个计算要定义变量,变量还分int, float, double, long这些类型,算完了还得printf(...)才看得到, 直接放Excel写公式不是更直接吗?越学越觉得是丈二和尚摸不着头脑,“指针”难道不是仪表里面那个细长的东东么,“内存地址”又是神马?这些东西和我们的专业又有哪门子关系,计算机这种“奇技淫巧”岂能和微积分、大学物理这种根正苗红的学科相提并论?于是抱着“不挂科就行”的心态混,期末成绩也是刚好没挂科而已。

2012年秋

  再次接触编程就到了大四做实验课题的阶段,当时课题组有个被称为“大神”的学长从不做任何体力工作,只是用MATLAB做所谓的“数值模拟”。对大神的莫名崇拜,加之炫酷的3d数据可视化吸引了我的眼球,于是跑图书馆找了本MATLAB教材开撸,果然比C语言学起来体验好多了,原来写代码可以如此简单。恰好当时做课程设计,要求设计一个煅烧材料用的马弗炉,于是把它作为第一个练手的项目,一个星期废寝忘食谢了上千行,尽管现在看来太过稚嫩,当时却对自己的处女作成就感爆棚,毕竟这个程序也算帮一部分同学解决了燃眉之急。写得那么长,是因为当时完全根据计算过程一步一步来,没有采用函数封装,就连材料性质的参数也是一个一个定义,也还不晓得有“面向对象编程”这一思想。
2012年10月了解到有挪威东南大学(武汉理工的合作院校)的教授来招生,恰好还有感兴趣的专业过程技术(基本上等同于化学工程),于是果断申请和参加面试,聊得很愉快。得知录取也是意料之中,唯一的遗憾是不要求GRE成绩,此前苦战的324分没能发挥出价值。

2013-2014

  硕士阶段第一门挂科率近半的硬课”数值模拟方法”,要用到MATLAB或Python求解常微分、偏微分方程,从Euler法Crank-NicolsonRunge-Kutta,从自己写求解器到调用库中的ode45ode15s,最后还要分析各种求解方法的stability。这门课是当时投入时间精力最多的,期末成绩拿到了A,还在2014年秋季当了助教,手把手地给初次接触编程的学生debug,解释基础到不能再基础的问题。那时开始同情大一的C语言老师,对他没有足够耐心解答自己无止境的问题表示理解;更深刻同情国内非计算机专业的理工科学生,明明可以选择更人性化的Python等语言,却偏偏被安排了C作为考试内容,造成很多潜在的IT人才过早对编程失去兴趣。

2015年夏

  真正开始系统学习计算机科学,是在2015年8月,当时先从最基础的离散数学开始,学到集合论、数论、图论的时候,深感自己的认知水平一再被刷新。比如说,回想起小学数奥的分解质因式,一直不知道有什么应用,直到搞懂了RSA加密算法。随后又在EdX上学习哈佛公开课CS50,Malan教授热情洋溢,简洁风趣地阐释复杂而抽象的概念、笑点不断的师生互动,让人坐在电脑前看视频都仿佛身临其境。又不禁感慨世上居然还有如此性感的男人,能让众多讲台下和显示屏前的学生们心潮澎湃。为了完成作业中的一个breakout(打砖块)小游戏,连续在Linux虚拟机的gedit中泡上几天。学到Web开发部分,诸多脚本语言更是对开发者友好,创造力更容易得到展现和发挥,因此互联网的应用也正在渗透到社会的方方面面。

2. 化学-化工之路

中学2005-2009

  第一次接触化学是在初中三年级,当时对这门做实验像变魔术一般的学科很感兴趣。高中投入到化学竞赛的学习中,两年的省二还是没能获取保送资格。高考后报志愿选的也都是化学相关的,录取到了武汉理工的材料化学专业。(那时听到“学计算机的”,想到的就是修电脑、卖电脑,或者在网吧当个网管之类的。)

大学2009-2013

  四大化学理论在高中学竞赛阶段都有涉及,轻车熟路便学下来了。而真正接触到科研,是在2011年的“大学生创新实验设计”,名称很华丽,然而具体到自身情况,就是由本科生来做导师的子课题,题目是“微波快速萃取技术在水泥熟料中游离氧化钙分析中的应用”。主要工作就是研磨样品,配制溶液,返滴定测游离钙含量,待到酚酞指示剂变红的一刹那,关紧滴定管,记录数据,输入到Excel的公式中算出平均值和标准差。相同条件下的平行试验多次进行,之后改变实验条件中的一个参数,再次进行多组平行试验。
  课题持续了整个暑假加上几个月的周末,最终提交了的结题报告中数据真实却并不理想,并且很好的分析了数据标准差较大的原因。虽然“筛选”或修改数据,或许能发表个期刊论文,考虑到学术诚信并没有那么做。答辩结果“良好”,却并没有成就感,因为脑力似乎并没有得到施展和锻炼,专业技能也仅仅是在不断重复中操作更加熟练却没有质变。期间不止一次问自己,难道我所能做的就是这些吗,这似乎比受过训练的生产线工人仅仅多了个写论文的环节。甚至也想过能不能搞个机器来完成那些体力活,然而出于当时对C语言心存畏惧,也就只是想想而已。

硕士2013-2015

  本科四年下来最大的感触是,基于仪器药品的实验研究主要还是不可替代性较低的体力劳动,硕士阶段投入到基于数学模型的数值仿真实验,尽管不能完全取代实验室的研究,也因具有以下优点而得以发展。
   - 成本低:只需计算机和相应软件,可以尝试大量不容条件下的试验,并且没有损耗品。
   - 结果可靠:排除了操作误差、主观误差,可重复性高(不涉及随机数的情况下,同样的模型、参数和算法,结果必然相同)。
   - 规模范围大:微观可达基本粒子,宏观可达工业级别的合成路线。
   - 安全性好:不接触有毒有害物质、易燃易爆品(写代码猝死的除外)。
   - 减少重复劳动:重复性强的体力劳动交给机器自动完成,人可以腾出时间从事更具有创造性的工作。
2015年6月硕士毕业后,由于化工行业不景气,一直没能在欧洲找到工作,于是留校做助教指导4名研二的外国小伙伴学OLGA软件(后面详细介绍这一神器)。其余时间,主要用在自学计算机科学相关课程,为转行做准备。

3. 计算机对化工行业的影响

3.1 工艺研发依赖软件

  硕士论文的课题是“AICV(自动控制阀)在SAGD(重力辅助重油开采)过程中的开闭特征”,使用的即为斯伦贝谢(Schlumberger)公司开发的OLGA软件,里面整合了CFD(计算流体力学)、传送过程、热力学、流变学、油藏工程、管道工程、材料摩擦学等诸多工程领域,囊括了可能用到的数学模型、经验公式、工艺技术、物料的理化性质数据等等。操作人员并不需要掌握所有的理论、模型与公式,只要学会OLGA软件,进行以下操作即可得到所需结果。
   - GUI(图形用户界面)中画工艺流程图(搭建油藏-管道-阀门-PID控制器等)
   - 划分栅格,对连续的物料进行离散化,类似把连续的时间划分为许多时间间隔(time step)
   - 选取具体模型、经验公式、求解方法,皆有多种选择,根据需要的精度和计算时间而定。
   - 设定边界条件(温度、压强、PID控制器参数等等)、输出变量、数据存储的time step等。
   - 运行模拟,等待实验结果(中途可以plot已计算完成的数据)
  具体到每一项都有很多细节需要处理,简而言之,程序会自动把系统模型描述为上百个状态变量的偏微分方程组,不断进行上百阶矩阵的求逆运算。每次模拟需要i7处理器的电脑跑上两三天,其间CPU占满,风扇转得可以当吹风机用。模拟结果用于生产过程参考,生产现场反馈的结果又被用来优化模型,不断提升两者的匹配度。

倘若不进行数值模拟,而直接在生产现场直接做试验,人力成本和资金投入会高得离谱。在计算机模拟技术成熟之前,这种工作一度确实需要大量工程师长期投入研发。然而,现在以化工为代表的传统行业的工程师之所以找不到工作,是因为更资深的工程师和学者的智慧,都被封装到这些神器般的软件里面了,而后者又大大降低了使用者的技术门槛。

鉴于此课题结果还不错,2015年10月在瑞典林雪平大学参加了SIMS56国际会议,并讲述拙作。参会的不仅有具体工程领域的建模与模拟研究人员,最牛的当属那些开发模拟软件的Computer Scientists,当时更是深感计算机在工程应用中的巨大影响。

3.2 生产依赖嵌入式系统

  得益于挪威当地企业界管理层的校友们,读研期间教授带队参观了周边的Yara化肥厂Norcem水泥厂Noka水处理厂。诺大的厂区并没有看到任何车间工人,只有几名工程师坐在控制室里喝咖啡聊天,整个生产线的每一个设备(Reactor / Boiler / Seperator / Compressor / Pump /…)的各种状态(温度、压强、流速等数据),在诸多显示屏上一览无余。当然,工程师并不需要紧张的盯着显示屏,因为绝大部分控制工作都交给各种PID控制器了,只有当传感器的数据出现异常时,系统会自动提示操作人员进行相应的处理。随着微控制器逐渐完善,整个自动生产系统的智能程度越来越高,需要人为解决的问题也越来越少,因此不难理解这些企业为何不再招聘更多工程师。
  目前的趋势是,生产环节(可以推广到整个传统制造业)需要的人力在逐渐减少,在不远的将来,第二产业的从业人数,也会像当今发达国家第一产业人口那样降低到2%以下。推动IT技术发展的驱动力,就是用机器替代人来劳动,人力成本越高的地方,越是自动化、智能化得到重视的地方(北欧是个代表)。

3.3 企业管理与产品销售依赖IT

  化工以及其他很多传统制造业中,企业内部信息管理逐渐自动化,人员、资金、物料、仓储、物流等等,都在企业内部数据库中实时更新,销售的线上渠道占比也越来越高。此外,越来越多的企业还招聘数据挖掘方面的人才,进行市场趋势分析,进而做出产品定价、企业战略方面的决策。这方面,中国一些企业的发展走在了世界的前列。

4. 传统行业与新领域

  自己真正下定决心转行IT还是由于原专业的就业难,2015年毕业前夕不但校招的公司少得可怜,反而有很多往年就业的前辈们被裁员。部分原因是油价持续下跌,石油化工公司普遍亏损,斯塔万格也变得一片萧条。在国内,化工行业同样面临产能过剩的问题,即使没有大量裁员,薪资待遇也低的可怜。相比之下,CS相关专业则在各行各业都有很大需求。即使是斯伦贝谢、哈里伯顿这些石油化工公司,在其他部门持续裁员的同时,也仍在高薪招聘软件工程师。
  一般而言,以化工为代表的传统制造业,相比于以IT为代表的新领域,分别是资本密集型和技术密集型。对于创业者而言,传统行业最大的门槛是启动资金,而在IT领域,则是创新理念和人才;对于求职者而言,拥有人脉资源在传统行业往往起到很大作用,而想进入IT行业还是技术实力作用更为关键。尽管传统行业现在也朝着高精尖方向发展,越来越倚重创新和人才,但这一差距不是短期内能够扭转的。简要对比如下表。

行业 化工(代表的传统行业) IT(代表的新领域)
资本 非常关键 启动资金较低
技术型人才 研发方面有一定需求 非常关键
社会人脉 很重要 相对不重要
市场饱和度 基本饱和 未饱和

  

5. 总结

新技术的发展日新月异,要跟上时代必须有终生学习的能力。学生阶段所学的专业知识并不是最重要的,如果把自己局限在某个特定领域而不跳出去展望更大的世界,难免会“只见树木不见森林”。