Facebook Componentkit 概況瞭解

GabrielPanda發表於2018-05-11

官網文件

[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點選的實現

使用CKComponentActionAttribute在 UIControl 上實現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...> 允許你指定引數傳給指定方法。 CKAction的 send方法可以帶著傳送者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
複製程式碼

相關文章