原文地址(需翻墙,建议Chrome打开):simplify20.github.io/blog/_posts…
源起
Tech(
name:'React',
since:'2013'
)
在相当长的一段时间里,Facebook网站的聊天栏有一个bug:当用户看到了新消息提示,点开后却没有新消息。开发们多次尝试修复,在以为自己已经修复的情况下,问题却又再次出现。
消息处理函数很庞大且有大量DOM操作,而一个页面里会有多个聊天会话,因为使用了双向数据绑定,就很难跟踪数据流。
为了解决这个问题,Facebook提出了Flux应用架构,并开发了React库,Flux基于单向数据流(one-way data flow)思想,状态是事实的唯一源头(The state is a single source of truth),React会监听状态的变化,当状态发生改变,相应地重绘组件。
基于上面的背景,我们可以推测,Flux应用架构及React库之所以诞生可能是为了解决两个问题:
- 应用状态问题
- Dom操作问题
思想
数学概念里,函数是一种特殊映射,而映射关系也可以称之为变换(Transformation),y = f(x),f就是一种变换,我们只关注单值函数,即给定一个x,就有唯一的y与之对应,那么相同输入,一定就有相同输出。
0x01 变换(Transformation)
React认为UI是一种数据形式到另一种数据形式的变换(Transformation),即UI = f(data),同样的输入必定有同样的结果,而对应到计算机概念里,就是纯函数。
有一天你接到这样一个需求:把充钱的用户,名字突出展示,比如这样:王老五、马爸爸、路人甲、路人乙,你刚好在看React设计思想,这个展示效果,可以用函数表示,只要是充钱的用户就给他突出展示
//充钱用户专属
function VipBox(name) {
return { fontWeight: 'bold', labelContent: name };
}
//白嫖用户专属
function NormalBox(name) {
return { labelContent: name };
}
'马爸爸' ->
{ fontWeight: 'bold', labelContent: '马爸爸' };
'路人甲' ->
{ labelContent: '路人甲' };
我对这种通过函数表示UI的方式的理解是,给定数据,生成UI界面的描述
0x02 抽象(Abstraction)
UI界面里可能有若干View及Layout,你不可能仅用一个函数就能实现复杂的UI,这时候就需要把UI抽象成多个隐藏内部细节,又可复用的函数。通过在一个函数中调用另一个函数来实现复杂的 UI,这就是抽象
当你自信满满地完成上述功能,准备下班时,产品在蹲坑时产生了一个新的灵感:加个框,才更突出。你心里有一万只草泥马,手起刀落......
两分钟后,新的代码出来了(还好VipBox可以复用,嘿嘿):function FancyVipBox(user) {
return {
borderStyle: '1px solid blue',
childContent: [
'Name: ',
VipBox(user.chineseName + '(' + user.englishName + ')')
]
};
}
{ chineseName: '马爸爸', englishName: 'Jack Ma' } ->
{
borderStyle: '1px solid blue',
childContent: [
'Name: ',
{ fontWeight: 'bold', labelContent: '马爸爸(Jack Ma)' }
]
};
0x03 组合(Composition)
第二天早上,产品来到了你的桌前,面色凝重,就像这一天的天气。
“什么?照片也要加边框,太丑了吧!” “老板说的” “好,我马上加” 边框盒子,可以包裹任何抽象的内容,不一定要是VipBox啊,所以可以这样改:function FancyBox(children) {
return {
borderStyle: '1px solid blue',
children: children
};
}
这样很多需要边框的场景都可以复用了,在展示充钱用户场景,可以这样写:
function FancyUserBox(user) {
return FancyBox([
'Name: ',
VipBox(user.chineseName + '(' + user.englishName + ')')
]);
}
在展示充钱用户图片的场景,可以这样写:
function FancyImageBox(url) {
return FancyBox([
Image(url)
]);
}
可以看到VipBox,FancyBox都是对一种展示效果的抽象,与具体业务场景无关,而FancyBox是通过对前者的组合,创建出的新的抽象:
为了真正达到重用的特性,只重用叶子然后每次都为他们创建一个新的容器是不够的。你可以将其他抽象的容器再次进行组合。我理解的“组合”就是将两个或者多个不同的抽象组织起来,成为一个新的抽象
0x04 状态(State)
UI 不单单是对服务器端或业务逻辑状态的复制。实际上还有很多状态是针对具体的渲染目标。举个例子,在一个 text field 中打字。它不一定要复制到其他页面或者你的手机设备。滚动位置这个状态是一个典型的你几乎不会复制到多个渲染目标的。我们倾向于使用不可变的数据模型。我们把可以改变 state 的函数串联起来作为原点放置在顶层。
下面的例子中,likes这个状态不会传递到所有渲染目标,仅仅针对LikeBox,而LikeButton可以产生更改likes的行为。
function FancyUserBox(user, likes, onClick) {
return FancyBox([
'Name: ', VipBox(user.chineseName + '(' + user.englishName + ')'),
'Likes: ', LikeBox(likes),
LikeButton(onClick)
]);
}
//实现细节
var likes = 0;
function addOneMoreLike() {
likes++;
rerender();
}
FancyUserBox(
{ chineseName: '马爸爸', englishName: 'Jack Ma' },
likes,
addOneMoreLike
);
用户点击LikeButton的事件交给上层处理,这样的好处是,下层只管无脑展示,不改变状态,保持不可变性。上层实现细节会更新likes,之后会触发rerender。状态应该是不可变的(所有改变状态的行为都可以称之为副作用),上例直接修改了状态(应该生成一个新的状态)是为了简化示例,实际编码中不推荐
0x05 记忆化(Memoization)
记忆化 是编程中常常采用的一种提高程序运行速度的优化技术,对于纯函数,相同的输入,必会有相同输出,因此可以把相应参数的执行结果缓存下来,下次有相同输入时,直接返回缓存结果,减少函数调用开销。
当我们知道一个函数是纯函数时,反复的调用是一种浪费,我们可以创建一个具有记忆能力的函数版本:总是缓存最近的输入和输出,如果输入没有变化,返回缓存结果,就没必要再执行了
function memoize(fn) {
var cachedArg;
var cachedResult;
return function(arg) {
if (cachedArg === arg) {
return cachedResult;
}
cachedArg = arg;
cachedResult = fn(arg);
return cachedResult;
};
}
var MemoizedVipBox = memoize(VipBox);
function NameAndAgeBox(user, currentTime) {
return FancyBox([
'Name: ',
MemoizedVipBox(user.chineseName + '(' + user.englishName + ')'),
'Age in milliseconds: ',
currentTime - user.dateOfBirth
]);
}
上例中,对于同一个user,MemoizedVipBox的参数是不变的,VipBox只会执行一次。memoize函数,简单的理解就是一个wrapper或者delegate。
上面着重介绍了React的几种核心思想,参考自中文翻译及原文,为方便理解,我对部分例子进行了修改,更多内容可自行查看React 设计思想(中文版)、React 设计思想(原文)react.js介绍
react.js是React设计思想的一种实现,下面是经典的HelloWorld程序,基于React.js实现
index.js
import React from 'react';
import ReactDOM from 'react-dom';
function App(){
return (
<div>
<p>Hello,World</p>
</div>
);
}
ReactDOM.render(
<App/>,
document.getElementById('root')
);
App方法中出现了类似于html的标签,这种写法称之为JSX,一种JS语法扩展,JSX代码会通过编译器(Babel。)转为标准JS语法,转换过程如下:
jsx
function App(){
return (
<div>
<p>Hello,World</p>
</div>
);
}
经过Babel编译后的标准js
function App() {
return React.createElement(
"div",
null,
React.createElement("p", null, "Hello,World")
);
}
React最终返回一个Js对象
function App() {
return {
type: 'div',
props: {
children: {
type: 'p',
props: {
children: 'Hello,World'
}
}
}
};
}
这样看来,和React思想介绍中的例子是不是就差不多了。
以下是App的另一种实现,继承React.Component,React.Component会提供状态更新、组件生命周期相关的方法,上面在介绍React思想时,为了便于理解举的例子都很具体,所有就有很多名称各异的方法,如果使用React.Component,那代表UI的方法就只有一个,即render
class App extends React.Component{
render(){
return (
<div>
<p>Hello,World</p>
</div>
);
}
}
React认为渲染逻辑(让UI展示)和UI逻辑(UI怎么布置)的耦合是天然的:事件之处理,状态之变化,数据之展示等,与其将标签和逻辑放在不同的文件中管理,不如通过“组件”的方式来管理,即组件同时包含了标签和逻辑,通过一个个独立的组件单元实现separates concerns。此外,在代码里写标签对于很多人是一种直观的编程体验。更多介绍参考:Pete Hunt: React: Rethinking best practices -- JSConf EU,视频里介绍了React为什么要用JSX,为什么要使用组件管理UI和逻辑,实现separates concerns
react.js体现记忆化(Memoization)思想的地方在于使用了Virtual DOM技术,App.render方法虽然返回了类似html的标签结构,但那不是html,经过Babel的编译,会成为ReactNode节点树,全是原生JS对象,而非Dom元素。关于Virtual DOM,可以参考深度剖析:如何实现一个 Virtual DOM 算法,其核心是:
- 步骤一:用JS对象模拟DOM树
- 步骤二:比较两棵虚拟DOM树的差异
- 步骤三:把差异应用到真正的DOM树上
后续
React-Native
Tech(
name:'React-Native',
since:'2014'
)
React Native是Facebook开发的开源移动应用开发框架,基于React,支持平台特性,React Native的设计思想是基于React的,就不展开讲了。相较于React,React Native不会通过虚Dom操作Dom,而是将组件翻译成平台对应的UI控件,因此UI渲染是原生渲染,所以性能会比WebView渲染高。
| React Native | Android View | iOS View |
|---|---|---|
<View> | <ViewGroup> | <UIView> |
<Text> | <TextView> | <UITextView> |
<Image> | <ImageView> | <UIImageView> |
React Native版的HelloWorld程序,注意React Native版的HelloWorld程序没有导入ReactDOM依赖
index.js
import React, { Component } from 'react';
import {AppRegistry,Text } from 'react-native';
class App extends Component{
render(){
return <Text>Hello,World</Text>;
}
}
AppRegistry.registerComponent('HelloWorld', () => App);
Flutter
Tech(
name:'Flutter',
since:'2015'
)
Flutter在2015 Dart developer summit首次发布,是一个跨平台应用开发框架, 目前支持Android, iOS, Linux, Mac, Windows, Google Fuchsia及Web等多个平台
Flutter版的HelloWorld程序main.dart
import 'package:flutter/material.dart';
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Text(
'Hello,World',
textDirection: TextDirection.ltr,
),
);
}
}
void main() {
runApp(App());
}
在Flutter中Widget是对于UI的描述(A widget is an immutable description of part of a user interface),是一种配置,这种配置被Element使用。在上例中,App的build方法每次被调用,都会创建新的Center和Text对象(都是Widget),因为Widget本质是一种配置,可以理解为数据类,所以创建的开销很小,Widget描述的UI效果要真正展示在屏幕上,还需要经过Element树和RenderObject树的配合,这个过程中,Widget的创建,不一定伴随Element的创建,后者可能只是更新而已。Flutter的设计思想显然与React如出一格。
JetPack Compose
Tech(
name:'JetPack Compose',
since:'2019'
)
Jetpack Compose是近年由Google推出的基于Kotlin的用于构建原生 Android 界面的工具包,支持在代码中通过声明式的方式创建UI组件
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
App()
}
}
//应用组件
@Composable
fun App(){
Greeting("World")
}
//打招呼组件
@Composable
fun Greeting(name: String) {
//文本组件
Text (text = "Hello $name!")
}
//预览组件,@Preview注释的组件会在AS中实时渲染
@Preview
@Composable
fun PreviewGreeting() {
App()
}
}
Jetpack Compose框架似乎深刻吸收了React的核心思想,将UI理解为一种数据到另一种数据的映射,用纯函数来表现UI,再利用抽象和组合复用已有组件实现复杂的功能。Google官方说Jetpack Compose是Android未来的UI开发工具包,看来是对其寄予厚望的。目前Google基于LiveData和ViewMode的MVVM架构,写起来总觉得少点什么,没错就是UI数据的自动绑定,虽然data-binding早就出来了,但是在xml中写逻辑总觉得不爽,有了Jetpack Compose,声明式的数据绑定就很容易实现。
总结
虽然是一篇以React为题的文章,但本文着重探讨了React的设计思想及其对后续一些应用开发框架/架构产生的影响,可以看到web开发技术/架构是如何向原生开发渗透的,其实Android最近比较热的MVI架构也是源于于web。React/React-Native/Flutter/Jetpack Compose,总体看起来是如此相像,我们再写UI时,只要按照各个框架的规则编写声明式的UI组件即可,底层对我们来说几乎透明,然而,各个框架底层为我们做了大量的事,react.js的虚拟Dom,Flutter的三棵树,除了保证UI更新的效率,还要把UI描述转换为UI渲染指令,最终通过底层图形框架的合成,展示到屏幕,对于底层原理我们也要心知肚明。此外,限于篇幅,本文没有介绍react组件的状态,后续文章将会介绍,另外会介绍单向数据流的应用,敬请期待 :sunny:。