深入浅出React(二):React开发神器Webpack

编者按:自2013年Facebook发布以来,React吸引了越来越多的开发者,基于它的衍生技术,如React Native、React Canvas等也层出不穷。InfoQ精心策划“深入浅出React”系列文章,为读者剖析React开发的技术细节。 上一篇我们对React有了一个总体的认识,在介绍其中的技术细节之前,我们首先来了解一下用于React开发和模块管理的主流工具Webpack。称之为React开发神器有点标题党了,不过Webpack确实是笔者见过的功能最为强大的前端模块管理和打包工具。虽然Webpack是一个通用的工具,并不只适合于React,但是很多React的文章或者项目都使用了Webpack,尤其是react-hot-loader这样的神器存在,让Webpack成为最主流的React开发工具。 CommonJS和AMD是用于JavaScript模块管理的两大规范,前者定义的是模块的同步加载,主要用于NodeJS;而后者则是异步加载,通过requirejs等工具适用于前端。随着npm成为主流的JavaScript组件发布平台,越来越多的前端项目也依赖于npm上的项目,或者自身就会发布到npm平台。因此,让前端项目更方便的使用npm上的资源成为一大需求。于是诞生了类似browserify这样的工具,代码中可以使用require函数直接以同步语法形式引入npm模块,打包后再由浏览器执行。 Webpack其实有点类似browserify,出自Facebook的Instagram团队,但功能比browserify更为强大。其主要特性如下: 同时支持CommonJS和AMD模块(对于新项目,推荐直接使用CommonJS); 串联式模块加载器以及插件机制,让其具有更好的灵活性和扩展性,例如提供对CoffeeScript、ES6的支持; 可以基于配置或者智能分析打包成多个文件,实现公共模块或者按需加载; 支持对CSS,图片等资源进行打包,从而无需借助Grunt或Gulp; 开发时在内存中完成打包,性能更快,完全可以支持开发过程的实时打包需求; 对sourcemap有很好的支持,易于调试。 Webpack将项目中用到的一切静态资源都视之为模块,模块之间可以互相依赖。Webpack对它们进行统一的管理以及打包发布,其官方主页用下面这张图来说明Webpack的作用: 可以看到Webpack的目标就是对项目中的静态资源进行统一管理,为产品的最终发布提供最优的打包部署方案。本文就将围绕React对其相关用法做一个总体介绍,从而能让你将其应用在自己的实际项目之中。 安装Webpack,并加载一个简单的React组件 Webpack一般作为全局的npm模块安装:

之后便有了全局的webpack命令,直接执行此命令会默认使用当前目录的webpack.config.js作为配置文件。如果要指定另外的配置文件,可以执行:

尽管Webpack可以通过命令行来指定参数,但我们通常会将所有相关参数定义在配置文件中。一般我们会定义两个配置文件,一个用于开发时,另外一个用于产品发布。生产环境下的打包文件不需要包含sourcemap等用于开发时的代码。配置文件通常放在项目根目录之下,其本身也是一个标准的CommonJS模块。 一个最简单的Webpack配置文件webpack.config.js如下所示:

其中entry参数定义了打包后的入口文件,数组中的所有文件会按顺序打包。每个文件进行依赖的递归查找,直到所有相关模块都被打包。output参数定义了输出文件的位置,其中常用的参数包括: path: 打包文件存放的绝对路径 publicPath: 网站运行时的访问路径 filename: 打包后的文件名 现在来看如何打包一个React组件。假设有如下项目文件夹结构:

其中Hello.js定义了一个简单的React组件,使用ES6语法:

entry.js是入口文件,将一个Hello组件输出到界面:

index.html的内容如下:

在这里Hello.js和entry.js都是JSX组件语法,需要对它们进行预处理,这就要引入webpack的JSX加载器。因此在配置文件中加入如下配置:

加载器的概念稍后还会详细介绍,这里只需要知道它能将JSX编译成JavaScript并加载为Webpack模块。这样在当前目录执行webpack命令之后,在assets目录将生成bundle.js,打包了entry.js的内容。当浏览器打开当前服务器上的index.html,将显示“Hello Nate!”。这是一个非常简单的例子,演示了如何使用Webpack来进行最简单的React组件打包。 加载AMD或CommonJS模块 在实际项目中,代码以模块进行组织,AMD是在CommonJS的基础上考虑了浏览器的异步加载特性而产生的,可以让模块异步加载并保证执行顺序。而CommonJS的require函数则是同步加载。在Webpack中笔者更加推荐CommonJS方式去加载模块,这种方式语法更加简洁直观。即使在开发时,我们也是加载Webpack打包后的文件,通过sourcemap去进行调试。 除了项目本身的模块,我们也需要依赖第三方的模块,现在比较常用的第三方模块基本都通过npm进行发布,使用它们已经无需单独下载管理,需要时执行npm install即可。例如,我们需要依赖jQuery,只需执行:

更多情况下我们是在项目的package.json中进行依赖管理,然后通过直接执行npm install来安装所有依赖。这样在项目的代码仓库中并不需要存储实际的第三方依赖库的代码。 安装之后,在需要使用jquery的模块中需要在头部进行引入:

可以看到,这种以CommonJS的同步形式去引入其它模块的方式代码更加简洁。浏览器并不会实际的去同步加载这个模块,require的处理是由Webpack进行解析和打包的,浏览器只需要执行打包后的代码。Webpack自身已经可以完全处理JavaScript模块的加载,但是对于React中的JSX语法,这就需要使用Webpack的扩展加载器来处理了。 Webpack开发服务器 除了提供模块打包功能,Webpack还提供了一个基于Node.js Express框架的开发服务器,它是一个静态资源Web服务器,对于简单静态页面或者仅依赖于独立服务的前端页面,都可以直接使用这个开发服务器进行开发。在开发过程中,开发服务器会监听每一个文件的变化,进行实时打包,并且可以推送通知前端页面代码发生了变化,从而可以实现页面的自动刷新。 Webpack开发服务器需要单独安装,同样是通过npm进行:

之后便可以运行webpack-dev-server命令来启动开发服务器,然后通过localhost:8080/webpack-dev-server/访问到页面了。默认情况下服务器以当前目录作为服务器目录。在React开发中,我们通常会结合react-hot-loader来使用开发服务器,因此这里不做太多介绍,只需要知道有这样一个开发服务器可以用于开发时的内容实时打包和推送。详细配置和用法可以参考官方文档。 Webpack模块加载器(Loaders) Webpack将所有静态资源都认为是模块,比如JavaScript,CSS,LESS,TypeScript,JSX,CoffeeScript,图片等等,从而可以对其进行统一管理。为此Webpack引入了加载器的概念,除了纯JavaScript之外,每一种资源都可以通过对应的加载器处理成模块。和大多数包管理器不一样的是,Webpack的加载器之间可以进行串联,一个加载器的输出可以成为另一个加载器的输入。比如LESS文件先通过less-load处理成css,然后再通过css-loader加载成css模块,最后由style-loader加载器对其做最后的处理,从而运行时可以通过style标签将其应用到最终的浏览器环境。 对于React的JSX也是如此,它通过jsx-loader来载入。jsx-loader专门用于载入React的JSX文件,Webpack的加载器支持参数,jsx-loader就可以添加?harmony参数使其支持ES6语法。为了让Webpack识别什么样的资源应该用什么加载器去载入,需要在配置文件进行配置:通过正则表达式对文件名进行匹配。例如:

可以看到,该使用什么加载器完全取决于这里的配置,即使对于JSX文件,我们也可以用js作为后缀,从而所有的JavaScript都可以通过jsx-loader载入,因为jsx本身就是完全兼容JavaScript的,所以即使没有JSX语法,普通JavaScript模块也可以使用jsx-loader来载入。 加载器之间的级联是通过感叹号来连接,例如对于LESS资源,写法为style-loader!css-loader!less-loader。对于小型的图片资源,也可以将其进行统一打包,由url-loader实现,代码中url-loader?limit=8192含义就是对于所有小于8192字节的图片资源也进行打包。这在一定程度上可以替代Css Sprites方案,用于减少对于小图片资源的HTTP请求数量。 除了已有加载器,你也可以自己实现自己的加载器,从而可以让Webpack统一管理项目特定的静态资源。现在也已经有很多第三方的加载器实现常见静态资源的打包管理,可以参考Webpack主页上的加载器列表。 React开发神器:react-hot-loader Webpack本身具有运行时模块替换功能,称之为Hot Module Replacement (HMR)。当某个模块代码发生变化时,Webpack实时打包将其推送到页面并进行替换,从而无需刷新页面就实现代码替换。这个过程相对比较复杂,需要进行多方面考虑和配置。而现在针对React出现了一个第三方react-hot-loader加载器,使用这个加载器就可以轻松实现React组件的热替换,非常方便。其实正是因为React的每一次更新都是全局刷新的虚拟DOM机制,让React组件的热替换可以成为通用的加载器,从而极大提高开发效率。 要使用react-hot-loader,首先通过npm进行安装:

之后,Webpack开发服务器需要开启HMR参数hot,为了方便,我们创建一个名为server.js的文件用以启动Webpack开发服务器:

为了热加载React组件,我们需要在前端页面中加入相应的代码,用以接收Webpack推送过来的代码模块,进而可以通知所有相关React组件进行重新Render。加入这个代码很简单:

需要注意的是,这里的client?http://127.0.0.1:3000需要和在server.js中启动Webpack开发服务器的地址匹配。这样,打包生成的文件就知道该从哪里去获取动态的代码更新。下一步,我们需要让Webpack用react-hot-loader去加载React组件,如上一节所介绍,这通过加载器配置完成:

做完这些配置之后,使用Node.js运行server.js:

即可启动开发服务器并实现React组件的热加载。为了方便,我们也可以在package.json中加入一节配置:

从而通过npm start命令即可启动开发服务器。示例代码也上传在Github上,大家可以参考。 这样,React的热加载开发环境即配置完成,任何修改只要以保存,就会在页面上立刻体现出来。无论是对样式修改,还是对界面渲染的修改,甚至事件绑定处理函数的修改,都可以立刻生效,不得不说是提高开发效率的神器。 将Webpack开发服务器集成到已有服务器 尽管Webpack开发服务器可以直接用于开发,但实际项目中我们可能必须使用自己的Web服务器。这就需要我们能将Webpack的服务集成到已有服务器,来使用Webpack提供的模块打包和加载功能。要实现这一点其实非常容易,只需要在载入打包文件时指定完整的URL地址,例如:

这就告诉当前页面应该去另外一个服务器获得脚本资源文件,在之前我们已经在配置文件中指定了开发服务器的地址,因此打包后的文件也知道应该通过哪个地址去建立Socket […]

龙生   29 Jul 2017
View Details

深入浅出React(三):理解JSX和组件

编者按:自2013年Facebook发布以来,React吸引了越来越多的开发者,基于它的衍生技术,如React Native、React Canvas等也层出不穷。InfoQ精心策划“深入浅出React”系列文章,为读者剖析React开发的技术细节。 通过前两篇文章的介绍,相信大家对JSX和组件已经有了一定的了解。JSX这种混合使用JavaScript和XML的语言第一眼看上去很“丑”,也很神奇,但是其语法和背后的逻辑却极其简单。相信读完本文你就可以对JSX和组件有一个全面的了解,并能够用JSX来直观的构造用户界面。 什么是JSX React的核心机制之一就是虚拟DOM:可以在内存中创建的虚拟DOM元素。React利用虚拟DOM来减少对实际DOM的操作从而提升性能。类似于真实的原生DOM,虚拟DOM也可以通过JavaScript来创建,例如:

使用这样的机制,我们完全可以用JavaScript构建完整的界面DOM树,正如我们可以用JavaScript创建真实DOM。但这样的代码可读性并不好,于是React发明了JSX,利用我们熟悉的HTML语法来创建虚拟DOM:

这两段代码是完全等价的,后者将XML语法直接加入到JavaScript代码中,让你能够高效的通过代码而不是模板来定义界面。之后JSX通过翻译器转换到纯JavaScript再由浏览器执行。在实际开发中,JSX在产品打包阶段都已经编译成纯JavaScript,JSX的语法不会带来任何性能影响。另外,由于JSX只是一种语法,因此JavaScript的关键字class, for等也不能出现在XML中,而要如例子中所示,使用className, htmlFor代替,这和原生DOM在JavaScript中的创建也是一致的。 因此,JSX本身并不是什么高深的技术,可以说只是一个比较高级但很直观的语法糖。它非常有用,却不是一个必需品,没有JSX的React也可以正常工作:只要你乐意用JavaScript代码去创建这些虚拟DOM元素。 为什么使用JSX 前端界面的最基本功能在于展现数据,为此大多数框架都使用了模板引擎,例如在AngularJS中:

在EmberJS中:

在Knockoutjs中:

模板可以直观的定义UI来展现Model中的数据,你不必手动的去拼出一个很长的HTML字符串,几乎每种框架都有自己的模板引擎。传统MVC框架强调界面展示逻辑和业务逻辑的分离,因此为了应对复杂的展示逻辑需求,这些模板引擎几乎都不可避免的需要发展成一门独立的语言,如上面代码所示,每个框架都有自己的模板语言语法。而这无疑增加了框架的门槛和复杂度。 如果说掌握一种模板语言并不是很大的问题,那么其实由模板带来的架构复杂性则是让框架也变得复杂的重要原因之一,例如: 模板需要对应数据模型,即上下文,如何去绑定和实现? 模板可以嵌套,不同部分界面可能来自不同数据模型,如何处理? 模板语言终究是一个轻量级语言,为了满足项目需求,你很可能需要扩展模板引擎的功能。 为了解决这些复杂度,框架本身需要精心的设计,以及创造新的概念(例如Angular的Directive)。这些都会让框架变得复杂和难以掌握,不仅增加了开发成本,各种难以调试的Bug还会降低开发质量。 正因为如此,React直接放弃了模板而发明了JSX。看上去很像模板语言,但其本质是通过代码来构建界面,这使得我们不再需要掌握一门新的语言就可以直观的去定义用户界面:掌握了JavaScript就已经掌握了JSX。这里不妨再引用之前文章举过的例子,在展示一个列表时,模板语言通常提供名为Repeat的语法,例如在Angular中:

而使用JSX,则代码如下:

可以看到,JSX完美利用了JavaScript自带的语法和特性,我们只要记住HTML只是代码创建DOM的一种语法形式,就很容易理解JSX。而这种使用代码构建界面的方式,完全消除了业务逻辑和界面元素之间的隔阂,让代码更加直观和易于维护。 JSX的语法 JSX本身就和XML语法类似,可以定义属性以及子元素。唯一特殊的是可以用大括号来加入JavaScript表达式,例如:

一般每个组件都定义了一组属性(props,properties的简写)接收输入参数,这些属性通过XML标记的属性来指定。大括号中的语法就是纯JavaScript表达式,返回值会赋予组件的对应属性,因此可以使用任何JavaScript变量或者函数调用。上述代码经过JSX编译后会得到:

对于子元素也是类似,大括号中使用JavaScript表达式来返回需要展现的元素,例如文章开头提到的例子使用JSX可以写成:

既然大括号中是JavaScript,而JSX又允许在JavaScript中使用XML,因此在大括号中仍然可以使用XML来声明组件,不断递归使用。 如果需要展现一组子节点,只需表达式返回一个JavaScript数组,数组的每个元素都是一个React组件,例如上一节的例子,其中lis就是有多个“li”元素的数组。:

在JSX中使用事件 如果你在90年代写过HTML,那么也许会有点怀念那时的事件绑定是多么的直观和简单:

那时的JavaScript应用范围非常有限,最有用的也许就是做表单有效性验证。因为逻辑都很简单,直接写到HTML中并没有问题,而且这种方式非常直观易读。但是现在因为Web程序变的越来越复杂,我们就需要使用JavaScript来绑定事件,例如在jQuery中:

在看到这段事件绑定和验证逻辑之前,你无法直观的看到有事件绑定在某个元素上,这种隐藏的界面元素和业务逻辑的耦合是很多Bug和内存泄露产生的根源。幸运的是,现在JSX可以让事件绑定返璞归真:

和原生HTML定义事件的唯一区别就是JSX采用驼峰写法来描述事件名称,大括号中仍然是标准的JavaScript表达式,返回一个事件处理函数。在JSX中你不需要关心什么时机去移除事件绑定,因为React会在对应的真实DOM节点移除时就自动解除了事件绑定。 React并不会真正的绑定事件到每一个具体的元素上,而是采用事件代理的模式:在根节点document上为每种事件添加唯一的Listener,然后通过事件的target找到真实的触发元素。这样从触发元素到顶层节点之间的所有节点如果有绑定这个事件,React都会触发对应的事件处理函数。这就是所谓的React模拟事件系统。 尽管整个事件系统由React管理,但是其API和使用方法与原生事件一致。这种机制确保了跨浏览器的一致性:在所有浏览器(IE8及以上)都可以使用符合W3C标准的API,包括stopPropagation(),preventDefault()等等。对于事件的冒泡(bubble)和捕获(capture)模式也都完全支持。 在JSX中使用样式 尽管在大部分场景下我们应该将样式写在独立的CSS文件中,但是有时对于某个特定组件而言,其样式相当简单而且独立,那么也可以将其直接定义在JSX中。在JSX中使用样式和真实的样式也很类似,通过style属性来定义,但和真实DOM不同的是,属性值不能是字符串而必须为对象,例如:

乍一看,这段JSX中的大括号是双的,有点奇怪,但实际上里面的大括号只是标准的JavaScript对象表达式,外面的大括号是JSX的语法。所以,样式你也可以先赋值给一个变量,然后传进去,代码会更易读:

在JSX中可以使用所有的的样式,基本上属性名的转换规范就是将其写成驼峰写法,例如“background-color”变为“backgroundColor”, “font-size”变为“fontSize”,这和标准的JavaScript操作DOM样式的API是一致的。 使用自定义组件 在JSX中,我们不仅可以使用React自带div, input…这些虚拟DOM元素,还可以自定义组件。组件定义之后,也都可以利用XML语法去声明,而能够使用的XML Tag就是在当前JavaScript上下文的变量名,这一点非常好用,你不必再去考虑某个Tag是如何对应到相应的组件实现。例如React官方教程中的例子:

其中声明了一个名为HelloWorld的组件,那么就可以在XML中使用<HellWorld />,这个Tag就是JavaScript变量名,我们可以用任意变量名:

甚至,我们还可以引入命名空间:

这些语法看上去有点怪,但是如果我们记住JSX语法只是JavaScript语法的一个语法映射,那么这些就非常容易理解了。 组件的概念和生命周期 React使用组件来封装界面模块,整个界面就是一个大组件,开发过程就是不断优化和拆分界面组件、构造整个组件树的过程。可以认为组件类似于其他框架中Widget(或Control)的概念,但又有所不同。React中的界面一切皆为组件,而Widget一般只是嵌入到界面中为完成某个功能的独立模块。 如下图,整个页面是一个大的组件,然后再将其拆分成很多小的组件。组件机制加上JSX的语法,让你在构造界面时就像有一套符合项目需求的HTML标记,界面定义变得非常直观。 组件自身定义了一组props作为对外接口,展示一个组件时只需要指定props作为XML节点的属性。组件很少需要对外公开方法,唯一的交互途径就是props。这使得使用组件就像使用函数一样简单,给定一个输入,组件给定一个界面输出。当给予的参数一定时,那么输出也是一定的。而传统控件通常提供很多方法让你在外部改变控件的状态和行为,当控件的状态在不同场景不同逻辑中可以被随意控制时,开发和调试也会变得复杂。 而React组件通过唯一的props接口避免了逻辑复杂性,让开发测试都更加容易。这种特性完全得益于虚拟DOM机制,让你可以每次props改变都能以整体刷新页面的思路去考虑界面展现逻辑。 如果整个项目完全采用React,那么界面上就只有一个组件根节点;如果局部使用React,那么每个局部使用的部分都有一个根节点。在Render时,根节点由React.render函数去触发:

而所有的子节点则都是通过父节点的render方法去构造的。每个组件都会有一个render方法,这个方法返回组件的实例,最终整个界面得到一个虚拟DOM树,再由React以最高效的方式展现在界面上。 除了props之外,组件还有一个很重要的概念:state。组件规范中定义了setState方法,每次调用时都会更新组件的状态,触发render方法。需要注意,render方法是被异步调用的,这可以保证同步的多个setState方法只会触发一次render,有利于提高性能。和props不同,state是组件的内部状态,除了初始化时可能由props来决定,之后就完全由组件自身去维护。在组件的整个生命周期中,React强烈不推荐去修改自身的props,因为这会破坏UI和Model的一致性,props只能够由使用者来决定。 对于自定义组件,唯一必须实现的方法就是render(),除此之外,还有一些方法会在组件生命周期中被调用,如下图所示: 图中的方法几乎已经包括了React的所有API,自定义组件时根据需要在组件生命周期的不同阶段实现不同的逻辑。除了必须的render方法之外,其它常用的方法包括: componentDidMount: 在组件第一次render之后调用,这时组件对应的DOM节点已被加入到浏览器。在这个方法里可以去实现一些初始化逻辑。 componentWillUnmount: 在DOM节点移除之后被调用,这里可以做一些相关的清理工作。 shouldComponentUpdate: 这是一个和性能非常相关的方法,在每一次render方法之前被调用。它提供了一个机会让你决定是否要对组件进行实际的render。例如:

当此函数返回false时,组件就不会调用render方法从而避免了虚拟DOM的创建和内存中的Diff比较,从而有助于提高性能。当返回true时,则会进行正常的render的逻辑。 […]

龙生   29 Jul 2017
View Details

深入浅出React(四):虚拟DOM Diff算法解析

React中最神奇的部分莫过于虚拟DOM,以及其高效的Diff算法。这让我们可以无需担心性能问题而”毫无顾忌”的随时“刷新”整个页面,由虚拟DOM来确保只对界面上真正变化的部分进行实际的DOM操作。React在这一部分已经做到足够透明,在实际开发中我们基本无需关心虚拟DOM是如何运作的。然而,作为有态度的程序员,我们总是对技术背后的原理充满着好奇。理解其运行机制不仅有助于更好的理解React组件的生命周期,而且对于进一步优化React程序也会有很大帮助。 什么是DOM Diff算法 Web界面由DOM树来构成,当其中某一部分发生变化时,其实就是对应的某个DOM节点发生了变化。在React中,构建UI界面的思路是由当前状态决定界面。前后两个状态就对应两套界面,然后由React来比较两个界面的区别,这就需要对DOM树进行Diff算法分析。 即给定任意两棵树,找到最少的转换步骤。但是标准的的Diff算法复杂度需要O(n^3),这显然无法满足性能要求。要达到每次界面都可以整体刷新界面的目的,势必需要对算法进行优化。这看上去非常有难度,然而Facebook工程师却做到了,他们结合Web界面的特点做出了两个简单的假设,使得Diff算法复杂度直接降低到O(n) 两个相同组件产生类似的DOM结构,不同的组件产生不同的DOM结构; 对于同一层次的一组子节点,它们可以通过唯一的id进行区分。 算法上的优化是React整个界面Render的基础,事实也证明这两个假设是合理而精确的,保证了整体界面构建的性能。 不同节点类型的比较 为了在树之间进行比较,我们首先要能够比较两个节点,在React中即比较两个虚拟DOM节点,当两个节点不同时,应该如何处理。这分为两种情况:(1)节点类型不同 ,(2)节点类型相同,但是属性不同。本节先看第一种情况。 当在树中的同一位置前后输出了不同类型的节点,React直接删除前面的节点,然后创建并插入新的节点。假设我们在树的同一位置前后两次输出不同类型的节点。

当一个节点从div变成span时,简单的直接删除div节点,并插入一个新的span节点。这符合我们对真实DOM操作的理解。 需要注意的是,删除节点意味着彻底销毁该节点,而不是再后续的比较中再去看是否有另外一个节点等同于该删除的节点。如果该删除的节点之下有子节点,那么这些子节点也会被完全删除,它们也不会用于后面的比较。这也是算法复杂能够降低到O(n)的原因。 上面提到的是对虚拟DOM节点的操作,而同样的逻辑也被用在React组件的比较,例如:

当React在同一个位置遇到不同的组件时,也是简单的销毁第一个组件,而把新创建的组件加上去。这正是应用了第一个假设,不同的组件一般会产生不一样的DOM结构,与其浪费时间去比较它们基本上不会等价的DOM结构,还不如完全创建一个新的组件加上去。 由这一React对不同类型的节点的处理逻辑我们很容易得到推论,那就是React的DOM Diff算法实际上只会对树进行逐层比较,如下所述。 逐层进行节点比较 提到树,相信大多数同学立刻想到的是二叉树,遍历,最短路径等复杂的数据结构算法。而在React中,树的算法其实非常简单,那就是两棵树只会对同一层次的节点进行比较。如下图所示: React只会对相同颜色方框内的DOM节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个DOM树的比较。 例如,考虑有下面的DOM结构转换: A节点被整个移动到D节点下,直观的考虑DOM Diff操作应该是

但因为React只会简单的考虑同层节点的位置变换,对于不同层的节点,只有简单的创建和删除。当根节点发现子节点中A不见了,就会直接销毁A;而当D发现自己多了一个子节点A,则会创建一个新的A作为子节点。因此对于这种结构的转变的实际操作是:

可以看到,以A为根节点的树被整个重新创建。 虽然看上去这样的算法有些“简陋”,但是其基于的是第一个假设:两个不同组件一般产生不一样的DOM结构。根据React官方博客,这一假设至今为止没有导致严重的性能问题。这当然也给我们一个提示,在实现自己的组件时,保持稳定的DOM结构会有助于性能的提升。例如,我们有时可以通过CSS隐藏或显示某些节点,而不是真的移除或添加DOM节点。 由DOM Diff算法理解组件的生命周期 在上一篇文章中介绍了React组件的生命周期,其中的每个阶段其实都是和DOM Diff算法息息相关的。例如以下几个方法: constructor: 构造函数,组件被创建时执行; componentDidMount: 当组件添加到DOM树之后执行; componentWillUnmount: 当组件从DOM树中移除之后执行,在React中可以认为组件被销毁; componentDidUpdate: 当组件更新时执行。 为了演示组件生命周期和DOM Diff算法的关系,笔者创建了一个示例:https://supnate.github.io/react-dom-diff/index.html ,大家可以直接访问试用。这时当DOM树进行如下转变时,即从“shape1”转变到“shape2”时。我们来观察这几个方法的执行情况: 浏览器开发工具控制台输出如下结果:

可以看到,C节点是完全重建后再添加到D节点之下,而不是将其“移动”过去。如果大家有兴趣,也可以fork示例代码:https://github.com/supnate/react-dom-diff 。从而可以自己添加其它树结构,试验它们之间是如何转换的。 相同类型节点的比较 第二种节点的比较是相同类型的节点,算法就相对简单而容易理解。React会对属性进行重设从而实现节点的转换。例如:

虚拟DOM的style属性稍有不同,其值并不是一个简单字符串而必须为一个对象,因此转换过程如下:

列表节点的比较 上面介绍了对于不在同一层的节点的比较,即使它们完全一样,也会销毁并重新创建。那么当它们在同一层时,又是如何处理的呢?这就涉及到列表节点的Diff算法。相信很多使用React的同学大多遇到过这样的警告: 这是React在遇到列表时却又找不到key时提示的警告。虽然无视这条警告大部分界面也会正确工作,但这通常意味着潜在的性能问题。因为React觉得自己可能无法高效的去更新这个列表。 列表节点的操作通常包括添加、删除和排序。例如下图,我们需要往B和C直接插入节点F,在jQuery中我们可能会直接使用$(B).after(F)来实现。而在React中,我们只会告诉React新的界面应该是A-B-F-C-D-E,由Diff算法完成更新界面。 这时如果每个节点都没有唯一的标识,React无法识别每一个节点,那么更新过程会很低效,即,将C更新成F,D更新成C,E更新成D,最后再插入一个E节点。效果如下图所示: 可以看到,React会逐个对节点进行更新,转换到目标节点。而最后插入新的节点E,涉及到的DOM操作非常多。而如果给每个节点唯一的标识(key),那么React能够找到正确的位置去插入新的节点,入下图所示: 对于列表节点顺序的调整其实也类似于插入或删除,下面结合示例代码我们看下其转换的过程。仍然使用前面提到的示例:https://supnate.github.io/react-dom-diff/index.html ,我们将树的形态从shape5转换到shape6: 即将同一层的节点位置进行调整。如果未提供key,那么React认为B和C之后的对应位置组件类型不同,因此完全删除后重建,控制台输出如下:

而如果提供了key,如下面的代码:

那么控制台输出如下:

可以看到,对于列表节点提供唯一的key属性可以帮助React定位到正确的节点进行比较,从而大幅减少DOM操作次数,提高了性能。 小结 本文分析了React的DOM Diff算法究竟是如何工作的,其复杂度控制在了O(n),这让我们考虑UI时可以完全基于状态来每次render整个界面而无需担心性能问题,简化了UI开发的复杂度。而算法优化的基础是文章开头提到的两个假设,以及React的UI基于组件这样的一个机制。理解虚拟DOM Diff算法不仅能够帮助我们理解组件的生命周期,而且也对我们实现自定义组件时如何进一步优化性能具有指导意义。   from:http://www.infoq.com/cn/articles/react-dom-diff

龙生   29 Jul 2017
View Details

深入浅出React(五):使用Flux搭建React应用程序架构

前面几篇文章介绍了React相关的基本概念和运行原理,可以看到React是一个完全面向View的解决方案,它让我们能以一种新的思路去实现View,让很多复杂的场景可以用一种简单的方法去解决。然而在一个完整的应用程序中,除了实现View之外,我们还需要考虑如何同服务器通信、View之间如何交互以及View背后的数据模型如何去设计。那么Flux正是Facebook提出的解决这些问题的方案。 简单来说,Flux定义了一种单向数据流的方式,来实现View和Model之间的数据流动。它更像是一种设计模式而非一个正式的框架,以至于官方的Flux参考实现只有一个文件,区区100多行源代码。所以Flux继承了React的简单、直观的设计思想,让人一眼就能看明白其背后的运行原理。当然,要用好Flux,还是要正确理解其概念和背后的出发点,官方则是提供了两个具体的例子供大家参考。 Flux的标准实现非常简单,因此还衍生出了很多第三方实现,比较著名的包括Redux,Reflux,Fluxmm。而如今最为火热的应该属于Redux,它采用了函数式编程的思想来维护整个应用程序的状态。其实无论哪一种框架,都是以Flux的架构为基础而做的演变,其核心都是单向数据流和单向数据绑定。因而本文只会介绍官方的Flux,理解了标准实现之后也会更容易理解其他的实现方式。大家可以按照自己的兴趣和认可程度选择最适合自己的实现。 Flux 要解决的问题 在传统MVC框架中,通常使用双向绑定的方式来将Model的数据展现到View。当Model中的数据发生变化时,一个或多个View会发生变化;当View接受了用户输入时,Model中的数据则会发生变化。在实际的应用中,当一个Model中的数据发生变化时,也有可能另一个相关的Model中的数据会被同步更新。这样,很容易出现的一个现象就是连锁更新(Cascading Update),Model可以更新Model,Model可以更新View,View也可以更新Model。你很难去推断一个界面的变化究竟是由哪个局部的功能代码引起。如下图所示, Model 和 View 之间的关系错综复杂,导致出现问题时很难调试;实现新功能时也需要时刻注意代码是否会产生副作用。 对此问题,Flux的解决方案是让数据流变成单向,引入Store、Action、Action Creators和Dispatcher等概念来管理信息流。如下图所示: 可以看到,数据流变成单向的。同时,数据如何被处理也被明确的定义了。在MVC中,数据如何处理通常由Controller来完成,在Controller中实现大部分的业务逻辑来处理数据。而现在则被清晰的定义在Store或者Action Creators中。当然,上图隐藏了一些细节,更为全面的架构图则如下所示: 在Flux中,View完全是Store的展现形式,Store的更新则完全由Action触发。得益于React的View每次更新都是整体刷新的思路,我们可以完全不必关心Store的变化细节,只需要监听Store的onChange事件,每次变化都触发View的re-render。从而也可以看到,尽管Flux架构可以离开React单独使用,但无疑两者结合是一个更加和谐的方案,能够各发挥所长。 看一个具体的例子 为了对Flux有一个总体的印象,我们先考虑一个简单的使用场景:在文章评论页面提交一条评论。为此,我们需要向服务器发送一个请求提交新的评论,同时要将新的评论显示在列表中。这样的场景如果使用Flux去实现,大概需要实现以下几个部分: React组件用于显示评论列表以及评论框,并绑定到Store; 一个Store用于存储评论数据; Action Creator用于向服务器发送请求; Store中监听Action并进行处理,从而对Store自身进行更新。 整个架构如下图所示: (点击放大图像) 整个流程的运行大概如下: 用户点击提交按钮,Action Creator负责向服务器发送请求; 请求如果成功,那么将评论本身被添加到Store; 请求如果失败,那么在Store中标记一个特别的错误状态; View监听了Store的onChange的事件,因此,无论请求成功和失败,Store都会触发onChange事件,这时View就会进行整体更新。 可以看到,无论请求成功和失败,都是去修改组件之外的Store,由Store通知UI进行变化。在这样一个架构中,Store中存储的是整个或者一部分应用程序的状态,React实现的View只需要监听Store的变化,而无需知道变化的细节,这也是由React组件的特点决定的。这样,我们就使用Flux完成了评论功能,不同于双向绑定,在Flux的流程中,数据如何流转和变化,变得非常清晰明确。虽然可能需要写更多的代码,但是带来了更清楚的架构。下面,我们来具体看其中的每个具体组件的概念和用法。 View和Store 在Flux架构中,View即React的组件,而Store则存储的是应用程序的状态。在前面的文章中我们已经介绍过,React是完全面向View的解决方案,它提供了一种始终都是整体刷新的思路来构建界面。在React的思路中,UI就是一个状态机,每个确定的状态对应着一个确定的界面。对于一个小的组件,它的状态可能是在其内部进行维护;而对于多个组件组成的应用程序,如果某些状态需要在组件之间进行共享,则可以将这部分状态放到Store中进行维护。在Flux中,Store并不是一个复杂的机制,甚至Flux的官方实现中并没有任何Store相关的机制和接口,而是仅仅通过示例来描述了一个Store应该是什么样的数据结构。例如,在官方提供的TodoMVC例子( https://github.com/facebook/flux/tree/master/examples/flux-todomvc/ )中,Store的实现如下:

可以看到,一个Flux的Store就是一个能触发onChange事件的对象,能够让其它对象订阅(addChangeListener)或者取消订阅(removeChangeListener)。同时,它提供了一些API供View来获取自己需要的状态。因此,也可以将Store理解为需要被不同View共享的公用状态。 那么,已经有了Store,React的组件(View)该如何使用它们呢?其实很简单,只需要在Store每次变化时都去获取一下最新的数据即可。我们可以看下TodoMVC中的实现:

可以看到,在组件的componentDidMount方法中,开始监听Store的onChange事件,在componentWillUnmount方法中,取消监听onChange事件。在Store的每次变化后,都去重新获取自己需要的状态数据:getTodoState()。 通过这样一种很简单的机制,我们建立了从Store到View的数据绑定,每当Store发生变化,View也会进行相应的更新。那么底下我们需要关心当View接收用户交互,需要将新的状态存入到Store中,应该如何去实现。这就需要引入Flux的另外两个概念Dispatcher和Action。 Dispatcher,Action 顾名思义,Dispatcher就是负责分发不同的Action。在一个Flux应用中,只有一个中心的Dispatcher,所有的Action都通过它来分发。而Facebook的官方Flux实现其实就仅仅是提供了Dispatcher。使用Dispatcher只需要将其作为npm模块引入:

典型的,Dispatcher有两个方法: dispatch:分发一个Action; register:注册一个Action处理函数。 这样,当View接受了一个用户的输入之后,通过Dispatcher来分发一个特定的Action,而对应的Action处理函数会负责去更新Store。这个流程在文章开始的图中可以清楚的看到。因此,通常来说Action的处理函数会和Store放在一起,因为Store的更新都是由Action处理函数来完成的。例如在TodoMVC中,TodoStore中会处理如下Action:

无论是添加、删除还是修改一个Todo项,都是由Action来触发的。在Action处理函数中,不仅对Store进行了更新,还触发了Store的onChange事件,从而让所有监听组件能够得到通知。完整的代码可以参考:https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/stores/TodoStore.js 。 通过Dispatcher和Action,实现了从View到Store的数据流,进而实现了整个Flux的单向数据流循环。从这里可以看到,Dispatcher是全局唯一的,相当于是所有Action的总hub,而每个Action处理函数都能够收到所有的Action,至于需要对哪些进行处理,则由处理函数自己决定。例子中是通过switch来判断Action的type属性来决定如何进行处理。因此,虽然不是必须,但是一般Action都会有一个type属性来标识其类型。 Action Creators 有了上述概念和机制,基本上就已经有了Flux的整个架构的模型。那么Action Creators又是什么呢?顾名思义,Action Creators即Action的创建者。它们负责去创建具体的Action。一个Action可以由一个界面操作产生,也可以由一个Ajax请求的返回结果产生。为了让这部分逻辑更加清晰,让View更少的去关心数据流的细节,于是有了Action Creators。例如,对于一个TodoItem组件,当用户点击其中的Checkbox时,会产生一个COMPLETE_TODO的Action,直观的看,完全可以在View的内部去实现Dispatcher.dispatch({type: ‘COMPLETE_TODO’, payload: {…}),而为了保持View的简单和直观,通常会在独立的Action Creators去封装这部分逻辑,例如:

这里的TodoActions就是一个Action Creator,它把具体分发Action的动作封装成具有语义的方法,供View去使用,那么在一个TodoItem的组件中,其界面JSX可能就是:

通过将Action的创建逻辑放到Action Creators,可以让View更加简单和纯粹,View不需要知道背后是否有Flux,而只需要知道调用某个方法来实现某个功能,从而让View的开发更加流畅和直观 。完整代码可以参考:https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/components/TodoItem.react.js 。 小结 本文介绍了Facebook提出的面向React的一种新的应用架构模式Flux,这种架构也已经在Facebook内部被广泛使用,其概念和原理虽然很简单直观,但是确实被证明有能力去组织一个完整的大型应用。然而Flux的中心Dispatcher的模式,以及Action作为全局可知的数据流的方式,仍然有一些争论。因此,社区中也是出现了非常多的类Flux实现,如今最成熟的莫过于Redux,其最大的特点在于Action能够分层次的去负责一个全局Store的不同部分,从而更容易去模块化应用状态的管理。理解了本文介绍的Flux概念,对于理解Redux也会有很大的帮助。   from:http://www.infoq.com/cn/articles/react-flux

龙生   29 Jul 2017
View Details

Java中存在着两种Random函数

一、java.lang.Math.Random; 调用这个Math.Random()函数能够返回带正号的double值,该值大于等于0.0且小于1.0,即取值范围是[0.0,1.0)的左闭右开区间,返回值是一个伪随机选择的数,在该范围内(近似)均匀分布。例子如下:

二、java.util.Random 下面Random()的两种构造方法: Random():创建一个新的随机数生成器。 Random(long seed):使用单个 long 种子创建一个新的随机数生成器。 我们可以在构造Random对象的时候指定种子(这里指定种子有何作用,请接着往下看),如:Random r1 = new Random(20); 或者默认当前系统时间的毫秒数作为种子数:Random r1 = new Random(); 需要说明的是:你在创建一个Random对象的时候可以给定任意一个合法的种子数,种子数只是随机算法的起源数字,和生成的随机数的区间没有任何关系。如下面的Java代码:

初始化时25并没有起直接作用(注意:不是没有起作用),rand.nextInt(100);中的100是随机数的上限,产生的随机数为0-100的整数,不包括100。 具体用法如下例:

备注:下面是Java.util.Random()方法摘要: protected int next(int bits):生成下一个伪随机数。 boolean nextBoolean():返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的boolean值。 void nextBytes(byte[] bytes):生成随机字节并将其置于用户提供的 byte 数组中。 double nextDouble():返回下一个伪随机数,它是取自此随机数生成器序列的、在0.0和1.0之间均匀分布的 double值。 float nextFloat():返回下一个伪随机数,它是取自此随机数生成器序列的、在0.0和1.0之间均匀分布float值。 double nextGaussian():返回下一个伪随机数,它是取自此随机数生成器序列的、呈高斯(“正态”)分布的double值,其平均值是0.0标准差是1.0。 int nextInt():返回下一个伪随机数,它是此随机数生成器的序列中均匀分布的 int 值。 int nextInt(int n):返回一个伪随机数,它是取自此随机数生成器序列的、在(包括和指定值(不包括)之间均匀分布的int值。 long nextLong():返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的 long 值。 void setSeed(long seed):使用单个 long 种子设置此随机数生成器的种子。 下面给几个例子: 生成[0,1.0)区间的小数:double d1 = r.nextDouble(); 生成[0,5.0)区间的小数:double d2 = r.nextDouble() * 5; 生成[1,2.5)区间的小数:double d3 = r.nextDouble() * 1.5 + 1; 生成-231到231-1之间的整数:int n = r.nextInt(); 生成[0,10)区间的整数: int n2 = r.nextInt(10);//方法一 n2 = Math.abs(r.nextInt() % 10);//方法二   参考资料: http://blog.sina.com.cn/s/blog_93dc666c0101h3gd.html http://blog.csdn.net/wpjava/article/details/6004492

龙生   29 Jul 2017
View Details

在eclipse中使用maven创建springMVC项目

一、在eclipse中创建maven-archetype-webapp项目: 1.新建项目选择maven项目 2.默认,下一步 3.选择maven-archetype-webapp,其他保持默认即可 4.如下填写完成后,点击完成即可 5.创建完成后的maven项目结构如下 其中index.jsp报错,错误信息:Multiple annotations found at this line: – The superclass "javax.servlet.http.HttpServlet" was not found on the Java   意思是缺少servlet包,我们可以导入javax.servlet-api-3.1.0.jar包,我们可以用两种方式来处理:     1> 在pom.xml中的dependencies中加入依赖包     

2> 可以在build path中添加tomcat 库,如下 点击next出现下面界面,如下操作 至此,一个正常的maven web项目已经建好,如下: 二、配置springMVC 1.在pom.xml中添加对spring的依赖 pom.xml

保存后会下载对应的jar文件 2.编辑web.xml文件 web.xml内容

  3.创建springContext.xml文件,在src/main/resources包中创建springContext.xml文件,如图: springContxt.xml内容  

在springContext.xml中,base-package是指定spring控制器控件的包,前缀指定的是视图目录,被设置为/WEB-INF/views,即视图目录被放到WEB-INF下。后缀指定的是视图的扩展名。例如,"hellospring"视图,将被放到/WEB-INF/views/hellospring.jsp。   4. 创建Spring控制器和视图 创建HelloSpringController.java类,在src/main/java包中,如下图: HelloSpringController.java

在上面的代码中,@Controller注解为Spring标注前置控制器的方式,@RequestMapping注解映射web请求到具体要操作的类或者方法上面,@RequestMapping注解既可以用到类上,也可以用到方法上,在此不再详述,如有疑问,可以百度。@RequestParam注解为请求指定参数。这种方式提供了一个一致 的编程风格。 另外上述代码中ModelAndView类指定具体的视图,这里是"hellospring",由于我们在springContext.xml配置了视图的前后缀,所以在这里只需要写出视图的具体名称即可,其具体指定的就是:前缀+视图名称+后缀,即完整的视图路径/WEB-INF/views/hellospring.jsp,也就是所要展示的视图的位置。 项目首页index.jsp内容  

上述代码中,点击跳转 的链接其实就是我们HelloSpringController.java中定义的控制器的一个@RequestMapping注解方法,name=zhangsan为showMessage接受的参数。 在/WEB-INF/views/下创建hellospring.jsp视图,如图: hellospring.jsp

上述代码中显示我们在HelloSpringController.java的showMessage方法中添加的两个参数message和name,如图:     好,至此我们使用maven搭建springMVC的操作已经全部完成,完成后整个项目的结构如下图: 三、将项目部署到tomcat服务器运行 首页 http://localhost:8080/HelloSpringMVC/  ,其中也可以使用 http://localhost:8080/HelloSpringMVC/index.jsp 两个效果是一样的 点击跳转 页   注意:若是跳转后页面直接显示${message} ${name}说明jstl表达式不起作用,我们可以在pom.xml中添加如下依赖:

from:http://www.cnblogs.com/qixing/p/qixing.html

龙生   28 Jul 2017
View Details

如何在linux系统中设置静态ip地址

在终端中输入:vi /etc/sysconfig/network-scripts/ifcfg-eth0 开始编辑,填写ip地址、子网掩码、网关、DNS等。其中“红框内的信息”是必须得有的。 编辑完后,保存退出。 重启网络服务。service network restart或/etc/init.d/network restart ping网关,ping外网进行测试。都能ping通表示网络正常。 摘要: —修改ip地址— 即时生效: # ifconfig eth0 192.168.1.155 netmask 255.255.255.0 重启生效: 修改/etc/sysconfig/network-scripts/ifcfg-eth0 —修改default gateway— 即时生效: # route add default gw 192.168.1.1 重启生效: 修改/etc/sysconfig/network-scripts/ifcfg-eth0 —修改dns— 修改/etc/resolv.conf 修改后即时生效,重启同样有效 —修改host name— 即时生效: # hostname test1 重启生效: 修改/etc/sysconfig/network from:http://jingyan.baidu.com/article/455a99508be7cda167277865.html

龙生   27 Jul 2017
View Details

Maven .m2文件夹创建

settings.xml存在于两个地方: 1.安装的地方:$M2_HOME/conf/settings.xml 2.用户的目录:${user.home}/.m2/settings.xml 前者又被叫做全局配置,后者被称为用户配置。如果两者都存在,它们的内容将被合并,并且用户范围的settings.xml优先。 如果你偶尔需要创建用户范围的settings,你可以简单的copy Maven安装路径下的settings到目录${user.home}/.m2。Maven默认的settings.xml是一个包含了注释和例子的模板,你可以快速的修改它来达到你的要求。 发现很多第三方的项目默认的setting配置都是用户目录/.m2/settings.xml 所以为了方便,需要自己创建.m2文件夹,并在其中配置settings.xml 网上的教程就是使用命令

使用之后,发现并没有生成.m2文件夹 查找很多之后发现,必须把默认的maven里面的本地存储设置为默认的,就是不要设置

这一行注释或取消掉,再执行mvn help:system命令就OK了。   http://www.cnblogs.com/yakov/archive/2011/11/26/maven2_settings.html

龙生   27 Jul 2017
View Details

centos7安装java1.8

安装方式:rpm安装,因为可能部分机子不连接外网,所以统一用rpm安装 软件:jdk-8u45-linux-x64.rpm 下载地址:http://www.oracle.com/technetwork/java/javase/downloads/index.html   查看自带java信息 [root@zck ~]# java -version java version "1.7.0_" OpenJDK Runtime Environment (IcedTea6 1.11.1) (rhel-1.45.1.11.1.el6-x86_64) OpenJDK 64-Bit Server VM (build 20.0-b12, mixed mode) 进一步查看JDK信息: [root@localhost ~]#  rpm -qa | grep java javapackages-tools-3.4.1-6.el7_0.noarch tzdata-java-2014i-1.el7.noarch java-1.7.0-openjdk-headless-1.7.0.71-2.5.3.1.el7_0.x86_64 java-1.7.0-openjdk-1.7.0.71-2.5.3.1.el7_0.x86_64 python-javapackages-3.4.1-6.el7_0.noarch 卸载OpenJDK,执行以下操作: [root@localhost ~]# rpm -e --nodeps tzdata-java-2014i-1.el7.noarch [root@localhost ~]# rpm -e --nodeps java-1.7.0-openjdk-headless-1.7.0.71-2.5.3.1.el7_0.x86_64 [root@localhost ~]# rpm -e --nodeps java-1.7.0-openjdk-1.7.0.71-2.5.3.1.el7_0.x86_64   安装JDK 上传新的jdk-8u45-linux-x64.rpm软件到/usr/local/执行以下操作: [root@localhost ~]# cd /usr/local/ [root@localhost local]# rpm -ivh jdk-8u45-linux-x64.rpm JDK默认安装在/usr/java中。   执行以下操作,查看信息是否正常: [root@localhost ~]# java [root@localhost ~]# javac [root@localhost ~]# java -version http://www.centoscn.com/image-text/install/2015/0609/5625.html

龙生   27 Jul 2017
View Details
1 236 237 238 432