React Native 按鈕的實(shí)現(xiàn)與源碼解析

[圖片上傳中...(pic1.png-835f42-1510063522595-0)]
iOS與安卓的原生實(shí)現(xiàn)實(shí)際都已經(jīng)提供了UIButtonButton的的按鈕組件唤蔗。但是我們發(fā)現(xiàn)RN并沒有基于這兩個(gè)組件實(shí)現(xiàn)統(tǒng)一的按鈕組件餐弱。那如何在RN中實(shí)現(xiàn)按鈕組件呢?其內(nèi)部實(shí)現(xiàn)又是如何處理的唠帝?本文將從設(shè)計(jì)按鈕,使用按鈕建芙,具體實(shí)現(xiàn)没隘,三部分解析整個(gè)按鈕的實(shí)現(xiàn)邏輯懂扼。

依賴

  • RN的版本 0.38
  • 系統(tǒng):iOS

按鈕設(shè)計(jì)

MyCustomButton.js

import React, {Component} from 'react';
import { 
    StyleSheet,
    Text,
    TouchableHighlight
} from 'react-native';

class MyCustomButton extends React.Component {
  props: Props;

  constructor(props: Props) {
    super(props);
  }

  render() {
    return (
      <TouchableHighlight
        style={styles.button}
        underlayColor="#a5a5a5"
        onPress={this.props.onPress}>
        <Text style={styles.buttonText}>{this.props.text}</Text>
      </TouchableHighlight>
    );
  }
}

const styles = StyleSheet.create({
    button: {
        borderWidth: 1,
    },
    buttonText: {
        fontSize: 18,
        color: 'red',
        backgroundColor:'transparent',
        alignSelf:'center'
    },
    text: {
        fontSize: 16,
        marginBottom:20
    },
});

module.exports = MyCustomButton;

具體使用

import React, { Component } from 'react';
import { 
    StyleSheet,
    View
} from 'react-native';
import MyCustomButton from './MyCustomButton'

class TestScreen extends Component{
    _onPress(){
        alert('按鈕點(diǎn)擊');
    }
    
    render(){
        return (<View style={styles.container}>
          <View
            style={styles.buttonWrap}
          >
          <MyCustomButton
            onPress={this._onPress}
            text={"按鈕"}
          ></MyCustomButton>
          </View>
        </View>)
    }
}

let styles = StyleSheet.create({
  container: {
    marginTop: 20,
    flex: 1
  },
  buttonWrap:{
    height:100,
    width:50
  }
});

module.exports = TestScreen;

主要想分析按鈕實(shí)現(xiàn)禁荸,因此該Demo例子只是簡(jiǎn)單滿足按鈕的要求。

TouchableHighlight及相關(guān)組件

我們查看上述例子中的TouchableHighlight的實(shí)現(xiàn)代碼中以下這一段阀湿,由此可以推斷是在TouchableWithoutFeedback的基礎(chǔ)上做的擴(kuò)展赶熟。

var TouchableHighlight = React.createClass({
  propTypes: {
    ...TouchableWithoutFeedback.propTypes,
  }
});

我們看看TouchableWithoutFeedback以及其他幾種擴(kuò)展的效果:

組件 描述 效果圖
TouchableWithoutFeedback 響應(yīng)點(diǎn)擊事件,無任何反饋
TouchableHighlight 點(diǎn)擊狀態(tài)背景變暗
TouchableOpacity 點(diǎn)擊狀態(tài)改變背景的透明度
TouchableNativeFeedback 此組件只支持Android陷嘴,不作分析 -

組件API的調(diào)用此處就不作具體介紹映砖,可以查看React Native的官方文檔

Native與jS端按鈕一塊的交互邏輯

下面我們來具體分析RN中按鈕的內(nèi)部實(shí)現(xiàn),從Native切入考慮灾挨。想到iOS端能處理手勢(shì)事件的類--UIGestureRecognizer邑退。

我們進(jìn)入到node_modules目錄執(zhí)行grep "UIGestureRecognizer" -rn .得到如下結(jié)果:

不難看出RN中繼承UIGestureRecognizer實(shí)現(xiàn)了自己的手勢(shì)處理的派生類RCTTouchHandler。按鈕的點(diǎn)擊功能的實(shí)現(xiàn)便從RCTTouchHandler開始分析劳澄。

Native端的處理

RCTTouchHandler入口

全局搜索RCTTouchHandler我們發(fā)現(xiàn)RN頁(yè)面的承載容器RCTRootView中的子組件RCTRootContentView存在RCTTouchHandler的屬性:

@interface RCTRootContentView : RCTView <RCTInvalidating>

@property (nonatomic, readonly) BOOL contentHasAppeared;
@property (nonatomic, readonly, strong) RCTTouchHandler *touchHandler;
@property (nonatomic, assign) BOOL passThroughTouches;

- (instancetype)initWithFrame:(CGRect)frame
                       bridge:(RCTBridge *)bridge
                     reactTag:(NSNumber *)reactTag
               sizeFlexiblity:(RCTRootViewSizeFlexibility)sizeFlexibility NS_DESIGNATED_INITIALIZER;
@end

@implementation RCTRootContentView
{
  __weak RCTBridge *_bridge;
  UIColor *_backgroundColor;
}

- (instancetype)initWithFrame:(CGRect)frame
                       bridge:(RCTBridge *)bridge
                     reactTag:(NSNumber *)reactTag
               sizeFlexiblity:(RCTRootViewSizeFlexibility)sizeFlexibility
{
  if ((self = [super initWithFrame:frame])) {
    _bridge = bridge;
    self.reactTag = reactTag;
    
    // 注意此處_touchHandler手勢(shì)實(shí)例初始化完成地技,然后添加到contentView上,這樣contentView便可以處理手勢(shì)事件了
    _touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge];
    [self addGestureRecognizer:_touchHandler];
    [_bridge.uiManager registerRootView:self withSizeFlexibility:sizeFlexibility];
    self.layer.backgroundColor = NULL;
  }
  return self;
}
// 省略部分代碼段
@end

RCTTouchHandler初始化邏輯

以下是RCTTouchHandler的初始化邏輯秒拔,

- (instancetype)initWithBridge:(RCTBridge *)bridge
{
  RCTAssertParam(bridge);

  // 初始化綁定事件的處理函數(shù)
  if ((self = [super initWithTarget:self action:@selector(handleGestureUpdate:)])) {

    _eventDispatcher = [bridge moduleForClass:[RCTEventDispatcher class]];
    _dispatchedInitialTouches = NO;
    _nativeTouches = [NSMutableOrderedSet new];
    _reactTouches = [NSMutableArray new];
    _touchViews = [NSMutableArray new];

    // `cancelsTouchesInView` is needed in order to be used as a top level
    // event delegated recognizer. Otherwise, lower-level components not built
    // using RCT, will fail to recognize gestures.
    self.cancelsTouchesInView = NO;
  }
  return self;
}

由此得出按鈕的點(diǎn)擊必將觸發(fā)handleGestureUpdate函數(shù)莫矗。

RCTTouchHandler中手勢(shì)處理

我們都知道手勢(shì)觸發(fā)會(huì)先執(zhí)行如下的函數(shù):

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

因此我們?cè)?code>RCTTouchHandler中的handleGestureUpdate函數(shù)以及上述函數(shù)中加入斷點(diǎn)調(diào)試分析。根據(jù)代碼執(zhí)行順序來看一下具體實(shí)現(xiàn)邏輯砂缩。

開始觸摸topTouchStart代碼執(zhí)行流程:

// 1.點(diǎn)擊按鈕時(shí)觸發(fā)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 
 
    // 2.記錄點(diǎn)擊view,點(diǎn)擊事件touch,點(diǎn)擊事件對(duì)應(yīng)的reactTouch
    - (void)_recordNewTouches:(NSSet<UITouch *> *)touches 
    
    // 3.監(jiān)聽手勢(shì)觸發(fā)的函數(shù)
    - (void)handleGestureUpdate:(__unused UIGestureRecognizer *)gesture
    
    // 4.更新reactTouch作谚,生成touchEvent,使用_eventDispatcher事件將該觸發(fā)事件發(fā)送給js端
    - (void)_updateAndDispatchTouches:(NSSet<UITouch *> *)touches
                        eventName:(NSString *)eventName
                  originatingTime:(__unused CFTimeInterval)originatingTime
        // 5.將native統(tǒng)計(jì)的touch信息同步到react的touch中       
        - (void)_updateReactTouchAtIndex:(NSInteger)touchIndex
        
         // 6.將touch通過發(fā)送事件的方式通知給js
        RCTTouchEvent *event = [[RCTTouchEvent alloc] initWithEventName:eventName
                                                             reactTag:self.view.reactTag
                                                         reactTouches:reactTouches
                                                       changedIndexes:changedIndexes
                                                        coalescingKey:_coalescingKey];
        [_eventDispatcher sendEvent:event];             

上述的代碼段6中,self.view.reactTag,reactTag是表示reactView的唯一標(biāo)識(shí)。是由js端的ReactNativeTagHandles.allocateTag()生成庵芭,有興趣可以自己研究妹懒,此處不作擴(kuò)展分析。

topTouchEnd的流程與上述的類似不作額外分析双吆。

js端的處理

Touchable手勢(shì)處理類

上面我們看了Native端是如何觸發(fā)按鈕點(diǎn)擊事件的眨唬,如何將native的觸摸事件傳給js端。下面我們將從js繼續(xù)分析按鈕點(diǎn)擊事件的整個(gè)流程:PRESSIN -> PRESSEND伊诵。
我們查看TouchableWithoutFeedback中如下一段代碼:


// mixins 模式的使用使得TouchableWithoutFeedback擁有Touchable的屬性與方法
const TouchableWithoutFeedback = React.createClass({
  mixins: [TimerMixin, Touchable.Mixin],
});

/**
 * `Touchable.Mixin` self callbacks. The mixin will invoke these if  they are
 * defined on your component.
 */
touchableHandlePress: function(e: Event) {
this.props.onPress && this.props.onPress(e);
},

顯然的按鈕的點(diǎn)擊事件onPress實(shí)際是觸發(fā)Touchable中的onPress函數(shù)的執(zhí)行单绑。下面我們具體看看Touchable的處理邏輯。

Touchable手勢(shì)處理流程圖

 * ======= State Machine =======
 *
 * +-------------+ <---+ RESPONDER_RELEASE
 * |NOT_RESPONDER|
 * +-------------+ <---+ RESPONDER_TERMINATED
 *     +
 *     | RESPONDER_GRANT (HitRect)
 *     v
 * +---------------------------+  DELAY   +-------------------------+  T + DELAY     +------------------------------+
 * |RESPONDER_INACTIVE_PRESS_IN|+-------->|RESPONDER_ACTIVE_PRESS_IN| +------------> |RESPONDER_ACTIVE_LONG_PRESS_IN|
 * +---------------------------+          +-------------------------+                +------------------------------+
 *     +            ^                         +           ^                                 +           ^
 *     |LEAVE_      |ENTER_                   |LEAVE_     |ENTER_                           |LEAVE_     |ENTER_
 *     |PRESS_RECT  |PRESS_RECT               |PRESS_RECT |PRESS_RECT                       |PRESS_RECT |PRESS_RECT
 *     |            |                         |           |                                 |           |
 *     v            +                         v           +                                 v           +
 * +----------------------------+  DELAY  +--------------------------+               +-------------------------------+
 * |RESPONDER_INACTIVE_PRESS_OUT|+------->|RESPONDER_ACTIVE_PRESS_OUT|               |RESPONDER_ACTIVE_LONG_PRESS_OUT|
 * +----------------------------+         +--------------------------+               +-------------------------------+
 *
 * T + DELAY => LONG_PRESS_DELAY_MS + DELAY
 *

從中我們能簡(jiǎn)單分析到按鈕點(diǎn)擊事件的幾個(gè)狀態(tài)變化:NOT_RESPONDER -> [RESPONDER_GRANT] -> RESPONDER_INACTIVE_PRESS_IN -> [LEAVE_PRESS_RECT] -> RESPONDER_ACTIVE_PRESS_OUT曹宴。我們開啟RN的調(diào)試模式利用Chrome瀏覽器驗(yàn)證一下搂橙。

Touchable touchableHandleResponderGrant

由上述的流程圖我們將斷點(diǎn)加入到當(dāng)前的函數(shù),點(diǎn)擊按鈕(先不釋放),我們看到如下的調(diào)試結(jié)果:

,初步驗(yàn)證了猜測(cè)区转,我們看看touchableHandleResponderGrant中的處理:

touchableHandleResponderGrant: function(e) {
    var dispatchID = e.currentTarget;
    // Since e is used in a callback invoked on another event loop
    // (as in setTimeout etc), we need to call e.persist() on the
    // event to make sure it doesn't get reused in the event object pool.
    
    // 1.標(biāo)記為已經(jīng)處理苔巨,避免該event被重復(fù)處理
    e.persist();
    
    // 2.清理掉pressOutDelayTimeout 
    this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout);
    this.pressOutDelayTimeout = null;
    
    // 3.初始化當(dāng)前的touchState為States.NOT_RESPONDER;
    this.state.touchable.touchState = States.NOT_RESPONDER;
    this.state.touchable.responderID = dispatchID;
    
    // 4.接收觸發(fā)開始信號(hào),處理邏輯見下
    this._receiveSignal(Signals.RESPONDER_GRANT, e);
    
    // 5.設(shè)置點(diǎn)擊事件有效的時(shí)間間隔废离,執(zhí)行_handleDelay函數(shù)
    var delayMS =
      this.touchableGetHighlightDelayMS !== undefined ?
      Math.max(this.touchableGetHighlightDelayMS(), 0) : HIGHLIGHT_DELAY_MS;
    delayMS = isNaN(delayMS) ? HIGHLIGHT_DELAY_MS : delayMS;
    if (delayMS !== 0) {
      this.touchableDelayTimeout = setTimeout(
        this._handleDelay.bind(this, e),
        delayMS
      );
    } else {
      this._handleDelay(e);
    }

    // 6.設(shè)置長(zhǎng)按事件的觸發(fā)時(shí)間間隔侄泽,執(zhí)行_handleLongDelay函數(shù)
    var longDelayMS =
      this.touchableGetLongPressDelayMS !== undefined ?
      Math.max(this.touchableGetLongPressDelayMS(), 10) : LONG_PRESS_DELAY_MS;
    longDelayMS = isNaN(longDelayMS) ? LONG_PRESS_DELAY_MS : longDelayMS;
    this.longPressDelayTimeout = setTimeout(
      this._handleLongDelay.bind(this, e),
      longDelayMS + delayMS
    );
  },

Touchable _receiveSignal

/**
   * Receives a state machine signal, performs side effects of the transition
   * and stores the new state. Validates the transition as well.
   *
   * @param {Signals} signal State machine signal.
   * @throws Error if invalid state transition or unrecognized signal.
   * @sideeffects
   */
  _receiveSignal: function(signal, e) {
    var responderID = this.state.touchable.responderID;
    var curState = this.state.touchable.touchState;
    
    // 1.Transitions是全局維護(hù)的字典:state ->(singal) -> nextState,
    // 具體可以自己查看Transitions定義
    var nextState = Transitions[curState] && Transitions[curState][signal];
    if (!responderID && signal === Signals.RESPONDER_RELEASE) {
      return;
    }
    if (!nextState) {
      throw new Error(
        'Unrecognized signal `' + signal + '` or state `' + curState +
        '` for Touchable responder `' + responderID + '`'
      );
    }
    if (nextState === States.ERROR) {
      throw new Error(
        'Touchable cannot transition from `' + curState + '` to `' + signal +
        '` for responder `' + responderID + '`'
      );
    }
    if (curState !== nextState) {
    
      // 2.根據(jù)state,nextState蜻韭,singal來判斷當(dāng)前的操作狀態(tài),改變按鈕的狀態(tài)悼尾,執(zhí)行相關(guān)回調(diào)
      this._performSideEffectsForTransition(curState, nextState, signal, e);
      this.state.touchable.touchState = nextState;
    }
  },

我們?cè)?code>_performSideEffectsForTransition中看到了如下的代碼段:

if (IsPressingIn[curState] && signal === Signals.RESPONDER_RELEASE) {
  var hasLongPressHandler = !!this.props.onLongPress;
  var pressIsLongButStillCallOnPress =
    IsLongPressingIn[curState] && (    // We *are* long pressing..
      !hasLongPressHandler ||          // But either has no long handler
      !this.touchableLongPressCancelsPress() // or we're told to ignore it.
    );

  var shouldInvokePress =  !IsLongPressingIn[curState] || pressIsLongButStillCallOnPress;
  if (shouldInvokePress && this.touchableHandlePress) {
    if (!newIsHighlight && !curIsHighlight) {
      // we never highlighted because of delay, but we should highlight now
      this._startHighlight(e);
      this._endHighlight(e);
    }
    // 此處是真正觸發(fā)onPress函數(shù)的調(diào)用
    this.touchableHandlePress(e);
  }
}

因此我們大膽的猜測(cè),當(dāng)按鈕點(diǎn)擊完成即觸摸離開時(shí)觸發(fā)Signals.RESPONDER_RELEASE的行為肖方,完成整個(gè)的按鈕點(diǎn)擊的操作闺魏。搜索全局查看到如下代碼段,加入斷點(diǎn)分析俯画。

/**
 * Place as callback for a DOM element's `onResponderRelease` event.
 */
touchableHandleResponderRelease: function(e) {
   this._receiveSignal(Signals.RESPONDER_RELEASE, e);
},

當(dāng)我們松開按鈕時(shí)析桥,我們看到如下的調(diào)試結(jié)果:

進(jìn)而印證了猜想。

Native中的event到j(luò)s端處理

上述的流程分別分析了Native與js端針對(duì)按鈕點(diǎn)擊事件的處理艰垂,尚且留下一個(gè)疑問就是以下這段代碼,即Native中的event到j(luò)s端具體的處理流程是什么泡仗?

RCTTouchEvent *event = [[RCTTouchEvent alloc] initWithEventName:eventName
                                                     reactTag:self.view.reactTag
                                                 reactTouches:reactTouches
                                               changedIndexes:changedIndexes
                                                coalescingKey:_coalescingKey];
[_eventDispatcher sendEvent:event];

查看了sendEvent:函數(shù)的實(shí)現(xiàn),按鈕點(diǎn)擊的整個(gè)js的調(diào)用棧如下圖猜憎,有點(diǎn)嚇到娩怎,RN中的額event的設(shè)計(jì)也不是幾句話分析清楚的,后續(xù)將寫一篇博文重點(diǎn)介紹這一塊的設(shè)計(jì)拉宗。本文就只需要知道Native的按鈕觸摸的信息是通過事件(event)的方式傳送給js端的就行了峦树,當(dāng)然你有興趣也可以自己研究。

整個(gè)React Native中按鈕的設(shè)計(jì)到具體實(shí)現(xiàn)基本告一段落旦事,其中部分細(xì)節(jié)未展開分析魁巩,包括按鈕高亮狀態(tài),禁用狀態(tài)姐浮,長(zhǎng)按事件等谷遂,有興趣可以自己分析。后續(xù)將繼續(xù)展開分析event的實(shí)現(xiàn)卖鲤,歡迎關(guān)注肾扰。文章中有錯(cuò)誤的地方歡迎指正,謝謝蛋逾。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末集晚,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子区匣,更是在濱河造成了極大的恐慌偷拔,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異莲绰,居然都是意外死亡欺旧,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門蛤签,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辞友,“玉大人,你說我怎么就攤上這事震肮〕屏” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵钙蒙,是天一觀的道長(zhǎng)茵瀑。 經(jīng)常有香客問我间驮,道長(zhǎng)躬厌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任竞帽,我火速辦了婚禮扛施,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘屹篓。我一直安慰自己疙渣,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布堆巧。 她就那樣靜靜地躺著妄荔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪谍肤。 梳的紋絲不亂的頭發(fā)上啦租,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天,我揣著相機(jī)與錄音荒揣,去河邊找鬼篷角。 笑死,一個(gè)胖子當(dāng)著我的面吹牛系任,可吹牛的內(nèi)容都是我干的恳蹲。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼俩滥,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼嘉蕾!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起霜旧,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤错忱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體航背,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡喉悴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了玖媚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片箕肃。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖今魔,靈堂內(nèi)的尸體忽然破棺而出勺像,到底是詐尸還是另有隱情,我是刑警寧澤错森,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布吟宦,位于F島的核電站,受9級(jí)特大地震影響涩维,放射性物質(zhì)發(fā)生泄漏殃姓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一瓦阐、第九天 我趴在偏房一處隱蔽的房頂上張望蜗侈。 院中可真熱鬧,春花似錦睡蟋、人聲如沸踏幻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)该面。三九已至,卻和暖如春信卡,著一層夾襖步出監(jiān)牢的瞬間隔缀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工坐求, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蚕泽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓桥嗤,卻偏偏與公主長(zhǎng)得像须妻,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子泛领,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容