Facebook Componentkit 概况了解


官网文档

[TOC]

一、概况

Components are immutable objects that specify how to configure views.
A simple analogy is to think of a component as a stencil: 
a fixed description that can be used to paint a view but that is not a view itself.
A component is often composed of other components, 
building up a component hierarchy that describes a user interface.

理解:
Component(组件)是不可变的对象,用来告诉我们如何初始化view。
Component(组件)是一个固定的描述,这个描述可以用来打印view。
简单恰当的比喻:
Component(组件)相当于stencil(镂空板),是一个固定的图案,用来打印view,但不是view。

Component(组件)相当于镂空板

镂空版印刷(stencil printing),指在木片、纸板、金属或塑料等片材上刻划出图文,并挖空制成镂空版,通过刷涂或喷涂方法使油墨透过通孔附着于承印物上。

也可以理解成印章

Component(组件)相当于印章
view相当于印章盖出来的图案

一个组件通常由其他组件组合而成,组件的层级结构可以用来描述用户界面。

1.1Components 三大特性:

demo代码

@implementation ArticleComponent

+ (instancetype)newWithArticle:(ArticleModel *)article
{
  return [super newWithComponent:
          [CKFlexboxComponent
           newWithView:{}
           size:{}
           style:{
             .direction = CKFlexboxDirectionVertical,
           }
           children:{
             {[HeaderComponent newWithArticle:article]},
             {[MessageComponent newWithMessage:article.message]},
             {[FooterComponent newWithFooter:article.footer]},
           }];
}

@end

声明式 Declarative:

Instead of implementing -sizeThatFits: and -layoutSubviews and positioning subviews manually, you declare the subcomponents of your component (here, we say “stack them vertically”).
相比原生设置UI需要手动设置位置,声明式的特性只需要我们做一个声明描述即可,比如垂直排列元素。
非常方便。

函数式Functional:

Data flows in one direction.
Methods take data models and return totally immutable components.
数据单向流动,数据流向UI
根据Data获取对应的不可变的Components组件

When state changes, ComponentKit re-renders from the root and reconciles the two component trees from the top with as few changes to the view hierarchy as possible.

状态改变时,ComponentKit会重新绘制。这时候有oldState和newState,会仔细核对两个组件树(Component tree),从root node到top node 尽量使用最少的重绘来更新view层级结构。

组合式Composable:

Here FooterComponent is used in an article, but it could be reused for other UI with a similar footer.
Reusing it is a one-liner.
CKFlexboxComponent is inspired by the flexbox model of the web and can easily be used to implement many layouts.
比如上文demo中的FooterComponent可以复用在别的组件里。
CKFlexboxComponent是类似于flexbox的Component。

1.2、Components 优缺点

Strengths 优点

  • Simple and Declarative 简单和声明式: 比较类似 React.

  • Scroll Performance 滚动流畅: 布局都在后台线程,保证了主线程的流畅度。

  • View Recycling 视图复用

  • Composability 组合式使用:Component可以组合起来使用

Considerations 可改进的地方

  • 列表式的界面支持比较好,不是列表式的界面支持不够理想。
  • ComponentKit is fully native and compiled. 全Native,不支持跨端。
  • ComponentKit 基于 Objective-C++. 不支持Swift,有学习成本。

二、相关API

2.1 Component类(避免直接继承CKComponent类)

@interface CKComponent : NSObject

/** Returns a new component. */
+ (instancetype)newWithView:(const CKComponentViewConfiguration &)view
                       size:(const CKComponentSize &)size;

@end

component 是不可变的,没有addSubcomponent方法 。
component可以在任意线程创建,因此所有的布局计算可以避免阻塞主线程。The Objective-C 使用 +newWith... 便利构造器保持代码可读

2.2 Composite Components(复合组件,可以直接继承此类)

避免直接继承 CKComponent 类。
可以直接继承CKCompositeComponent.
composite component包含了另一个component,对外隐藏了它的实现。
比如需要做一个分享按钮,可以制作一个ShareButtonComponent 复合组件,里面包含CKButtonComponent组件

@implementation ShareButtonComponent

+ (instancetype)newWithArticle:(ArticleModel *)article
{
  return [super newWithComponent:
          [CKButtonComponent
           newWithAction:@selector(shareTapped)
           options:{...}]];
}

- (void)shareTapped
{
  // Share the article
}

@end

2.3 Views(视图)

使用 newWithView:size: class method:来创建component组件

+ (instancetype)newWithView:(const CKComponentViewConfiguration &)view
                       size:(const CKComponentSize &)size;

关于CKComponentViewConfiguration

struct CKComponentViewConfiguration {
  CKComponentViewClass viewClass;//使用[UIImageView class] 或者 [UIButton class]
  std::unordered_map<CKComponentViewAttribute, id> attributes;//属性map
};

使用newWithView

[CKComponent 
 newWithView:{
   [UIImageView class],
   {
     {@selector(setImage:), image},
     {@selector(setContentMode:), @(UIViewContentModeCenter)} // Wrapping into an NSNumber
   }
 }
 size:{image.size.width, image.size.height}];

ComponentKit 会做如下的事情:

1.当component被挂载时候自动创建或复用 UIImageView
2.自动调用 setImage: and setContentMode: 使用给定的值
3.如果更新view tree时候value没有变化就跳过不调用setImage: or setContentMode:方法

2.4 Layout(布局)

UIView 实例在属性里面存储 position 和 size 信息。
当约束条件变化的时候Core Animation 通过调用layoutSubviews来更新这些属性。

CKComponent 实例并不存储position 和 size 信息。
ComponentKit 使用给定的约束,调用 layoutThatFits: 方法 ,
component 会返回 CKComponentLayout,包含自身的size和children component的size和position信息

struct CKComponentLayout {
  CKComponent *component;
  CGSize size;
  std::vector<CKComponentLayoutChild> children;
};

struct CKComponentLayoutChild {
  CGPoint position;
  CKComponentLayout layout;
};

Layout Components

  • CKFlexboxComponent 基于简化版的 CSS flexbox.
    允许垂直或者水平排列元素,各种对齐等。

  • CKInsetComponent 应用内边距的布局

  • CKBackgroundLayoutComponent 背景布局

  • CKOverlayLayoutComponent 覆盖布局

  • CKCenterLayoutComponent 中心布局

  • CKRatioLayoutComponent 比例布局

  • CKStaticLayoutComponent 固定布局

2.5 Responder Chain(响应者链)

响应者链
  1. component的下一级响应者是它自己的 controller(如果有的话)
  2. component的 controller 的下一级响应者是component的父级component.
  3. 如果 component 没有controller, 它的下一级响应者是自己的父级component
  4. 根 component的下一级响应者是它所被添加的view
  5. 一般来说 view的下一级响应者是它的superview.
  6. 最终,view会找到和component层级结构一样的根view
  7. 如果要使用CKComponentActionSend,可以手动桥接view responder chain 和 the component responder chain。

注意component 并不是UIResponder的子类,不能成为 first responder.
但是component也是实现了nextResponder 和 targetForAction:withSender:方法.

Tap点击的实现

使用CKComponentActionAttributeUIControl 上实现Tap点击

@implementation SomeComponent

+ (instancetype)new
{
  return [self newWithView:{
    [UIButton class],
    {CKComponentActionAttribute(@selector(didTapButton))}
  }];
}

- (void)didTapButton
{
  // Aha! The button has been tapped.
}

@end

手势的实现

使用CKComponentTapGestureAttribute可以在任何UIView上实现

@implementation SomeComponent

+ (instancetype)new
{
  return [self newWithView:{
    [UIView class],
    {CKComponentTapGestureAttribute(@selector(didTapView))}
  }];
}

- (void)didTapView
{
  // The view has been tapped.
}

@end

2.6 Actions(子component和父component通信)

一般来说子components 需要和.父component进行通信。
比如一个按钮component需要告诉父component它被点击了。
Component actions 可以实现这个目的。

Component Actions是什么?

CKAction<T...> 是 Objective-C++ 类,包含 一个 SEL (Objective-C的方法名), 和一个 target.
CKAction<T...> 允许你指定参数传给指定方法。
CKActionsend方法可以带着发送者component和参数传递给receiver

由于历史原因,CKComponentActionSend可带action,sender,和一个可选对象。
循着响应者链,找到一个响应者响应对应的方法,把参数传给它。

CKComponentActionSend 只能在主线程被调用!

@implementation SampleComponent
+ (instancetype)new
{
  CKComponentScope scope(self);
  return [super newWithComponent:
          [CKButtonComponent
           newWithAction:{scope, @selector(someAction:event:)}
           options:{}]];
}

- (void)someAction:(CKButtonComponent *)sender event:(UIEvent *)event
{
  // Do something
}
@end

@implementation SampleOtherComponentThatDoesntCareAboutEvents
+ (instancetype)new
{
  CKComponentScope scope(self);
  return [super newWithComponent:
          [CKButtonComponent
           newWithAction:{scope, @selector(someAction:)}
           options:{}]];
}

- (void)someAction:(CKButtonComponent *)sender
{
  // Do something
}
@end

@implementation SampleOtherComponentThatDoesntCareAboutAnyParameters
+ (instancetype)new
{
  CKComponentScope scope(self);
  return [super newWithComponent:
          [CKButtonComponent
           newWithAction:{scope, @selector(someAction)}
           options:{}]];
}

- (void)someAction
{
  // We don't take any arguments in this example.
}
@end

@interface SampleControllerDelegatingComponentController : CKComponentController
/** Component actions may be implemented either on the component, or the controller for that component. */
- (void)someAction;
@end

@implementation SampleControllerDelegatingComponent
+ (instancetype)new
{
  CKComponentScope scope(self);
  return [super newWithComponent:
          [CKButtonComponent
           newWithAction:{scope, @selector(someAction)}
           options:{}]];
}
@end

@implementation SampleControllerDelegatingComponentController
- (void)someAction
{
  // Do something
}
@end

如何传递Action

简单规则: 方法应该在它们被引用的地方在一个文件中
下面的例子父component和子component耦合比较厉害,如果别的component想要使用子component,或者父类改了方法名,运行的时候就会崩溃。

//有隐患
@implementation ParentComponent
+ (instancetype)new
{
  return [super newWithComponent:[ChildComponent new]];
}
- (void)someAction:(CKComponent *)sender
{
  // Do something
}
@end

@implementation ChildComponent
+ (instancetype)new
{
  return [super newWithComponent:
          [CKButtonComponent
           newWithAction:@selector(someAction:)]];
}
@end

从父component传递方法到子component,子component只知道需要一个action,耦合比较小。

//正确的例子
@implementation ParentComponent
+ (instancetype)new
{
  CKComponentScope scope(self);

  return [super newWithComponent:
          [ChildComponent
           newWithAction:{scope, @selector(someAction:)}]];
}

- (void)someAction:(CKComponent *)sender
{
  // Do something
}
@end

@implementation ChildComponent
+ (instancetype)newWithAction:(CKTypedComponentAction<>)action
{
  return [super newWithComponent:
          [CKButtonComponent
           newWithAction:action]];
}
@end

2.7 State(对应React的State)

ComponentKit是受 React启发的.
React components 拥有下面两个元素

  • props: 从parent传过来。在ComponentKit中类似+new方法传给子component的参数

  • state: 父component不用关心,这是子component内部实现。
    Thinking in React 有相关的讨论。

CKComponent 的 state.

@interface CKComponent
- (void)updateState:(id (^)(id))updateBlock mode:(CKUpdateMode)mode;
@end

继续阅读... 展开的demo

#import "CKComponentSubclass.h" // import to expose updateState:
@implementation MessageComponent

+ (id)initialState
{
  return @NO;
}

+ (instancetype)newWithMessage:(NSAttributedString *)message
{
  CKComponentScope scope(self);
  NSNumber *state = scope.state();
  return [super newWithComponent:
          [CKTextComponent
           newWithAttributes:{
             .attributedString = message,
             .maximumNumberOfLines = [state boolValue] ? 0 : 5,
           }
           viewAttributes:{}
           accessibilityContext:{}]];
}

- (void)didTapContinueReading
{
  [self updateState:^(id oldState){ return @YES; } mode:CKUpdateModeAsynchronous];
}

@end

2.8 Scopes(用来作为component的id)

如下图,如果item没有id 就没法进行区分


无法区分

如下图,每个item都有自己的id,这样可以具体区分


有了id可以区分

Scopes给component提供了一个永久的身份标识符。不管component被创建过多少次,scope都是一样的。

  1. component 有 state,那么必须要定义一个 scope
  2. component 有component controller,那么必须要定义一个 scope
  3. component的子component有 state 或者 component controllers 那么必须要定义一个 scope

定义 Scope

使用 CKComponentScope+new方法中

@implementation ListItemComponent

+ (instancetype)newWithListItem:(ListItem *)listItem
{
  // Defines a scope that is uniquely identified by the component's class (i.e. ListItemComponent) and the provided identifier.
  CKComponentScope scope(self, listItem.uniqueID);
  const auto c = /* ... */;
  return [super newWithComponent:c];
}

@end

component 没有 model object

@implementation ListComponent

+ (instancetype)newWithList:(List *)list
{
  // Defines a scope that is uniquely identified by the component's class (i.e. ListComponent).
  CKComponentScope scope(self);
  const auto c = /* ... */;
  return [super newWithComponent:c];
}

@end
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi阅读 7,289评论 0 10
  • 今朝何欲别 徭忆锦清发 欲知相思梦 垂别泪钧越
    Luxun_Kerouac阅读 116评论 0 1
  • 谁没有一刻觉得自己百无一用 恨不得死去 天亮之后还是紧紧胳膊 抱住了自己 我们都曾幻想毕业之后永远不分离 都曾幻想...
    少女小偷阅读 144评论 0 1
  • 上中学的时候,我们被逼迫读过很多古文,我记忆最深的是《出师表》,因为那时我迷恋于一款叫《三国志》的街机游戏,因为这...
    我承认我是有点懒阅读 155评论 0 0
  • 背景 当我们比较移动浏览器和桌面浏览器的时候,它们最显而易见的不同就是屏幕尺寸。 为桌面浏览器所设计的网站在移动浏...
    进击的小铁阅读 568评论 7 5