聊一聊React(一):思想

2,673 阅读11分钟

原文地址(需翻墙,建议Chrome打开):simplify20.github.io/blog/_posts…

An image

源起

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操作问题
在复杂的应用场景下,如果任何环节都可能修改状态,当出现一个应用状态时,我们便难以知道应用状态改变的源头。Flux实现了单向数据流,而React利用虚Dom提高渲染效率,利用组件组织UI,接下来会着重介绍介绍React的设计思想,对于其细节不会深究。

思想

数学概念里,函数是一种特殊映射,而映射关系也可以称之为变换(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 NativeAndroid ViewiOS 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:。