redux简介(二)源码解析

写在开头

前置知识内容,闭包高阶函数函数式编程思想redux核心概念

git clone https://github.com/reduxjs/redux.git

本文对应redux版本为redux v4.0.1

一、结构

redux源码结构

在redux的src目录下可以清楚的看到redux的几个核心文件和js工具函数。通过阅读index.js文件可以清楚的看到redux导出的5个核心方法

文件 功能
index redux的入口文件 用法
createStore 提供核心APIcreateStore根据reducer,preState,applyMiddleware。创建并返回store
combineReducers 提供核心APIcombineReducers,用于合并拆分的reducer
bindActionCreators 提供核心APIbindActionCreators 可以简化dispatch action的调用方法。
applyMiddleware 提供核心APIapplyMiddleware
compose
actionTypes.js 内置的action.type。
isPlainObject.js 判断是否是简单对象。
warning.js 用于输出警告信息。

二、工具文件

2.1 actionTypes

源码截取

const randomString = () =>
  Math.random()
    .toString(36)
    .substring(7)
    .split('')
    .join('.');

const ActionTypes = {
  INIT: `@@redux/INIT${randomString()}`,
  REPLACE: `@@redux/REPLACE${randomString()}`,
  PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
};

export default ActionTypes

actionTypes文件主要封装了redux内置的actionType,其中ActionTypes.INIT主要用于初始化store所使用。REPLACE PROBE_UNKNOWN_ACTION为替换reducer的actionType。

2.2isPlainObject

源码截取

export default function isPlainObject(obj) {
  if (typeof obj !== 'object' || obj === null) return false

  let proto = obj
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }

  return Object.getPrototypeOf(obj) === proto
}

本段函数主要用于对 reducer函数的action参数进行校验。函数用于判断一个对象是否是一个简单对象,简单对象是指直接使用对象字面量{}或者new Object()Object.create(null)所创建的对象。jQuerylodash等JavaScript函数均对此有所实现.redux的老期版本使用了ladash的实现版本,后改为自己实现。

2.3warning

源码截取:

export default function warning(message) {
  if (typeof console !== 'undefined' && typeof console.error === 'function') {
    console.error(message)
  }
  try {
    // This error was thrown as a convenience so that if you enable
    // "break on all exceptions" in your console,
    // it would pause the execution at this line.
    throw new Error(message)
  } catch (e) {
  }
}

本函数主要用于对代码执行过程中所遇到的错误进行统一处理,在控制台打印错误原因。使用throw new Error(message)是为了方便调试时中断执行

三、核心文件

3.1 createStore.js

使用场景

// reducer是必传的函数主要用于响应action对store做处理。
// preloadedState是一个可选对象,用于指定redux中store的默认值,使用场景比如说,服务端渲染是redux数据的注入,或者应用程序页面刷新时redux数据的保留。
// enhancer 用于增强redux的功能,比如处理异步,打印log等等
createStore(reducer, [preloadedState], enhancer)

creeateStore.js主要暴露了一个函数,即createStore函数。整个函数除去注释,约为180行左右。
下面将对此函数进行分析。此函数的参数为(rducer,preloadState,enhancer)经过处理返回一个store对象,store对象的值如下。

{
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
}

createStore函数主要有5个变量用来存储信息,通过变量名,我们可以很容易的知道每个变量的含义。createStore通过闭包将这些变量存储起来,通过store的属性来操作数据。

let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false

源代码行数过多,在这里就不体现出来了,点击这里查看

下面来逐行分析createStore函数的执行过程
1.校验函数传入的参数
函数执行的第一步是函数校验,如果校验未通过,直接抛出错误。
函数的第一个参数是必传的函数(reducers),函数的额第二个参数是可选的除函数外的任意类型参数preloadState,参数的第三个参数是可选的函数enhancer(applyMiddleWare函数返回的函数组成的数组)。也可以传递两个参数reducersenhancer
函数校验了四个地方
第一步,由于函数可以支持多个中间件函数,但是多个中间件必须要通过compose函数包装之后在可以传入。函数首先对此进行了校验。判断如果函数的第二个和第三个参数都为函数。或者函数的第三个参数和第四个参数都为函数。则可以猜测到,开发者可能是为使用compose包装多个中间件,所导致,所以此时函数会抛出错误信息。提示出当前可能的错误原因。
第二步,对于函数只传入reducersenhancer进行了处理。判断传入的第二个参数是函数,并且没有传递第三个参数,此时将preloadState赋值给enhancer。将preloadState置空。是实参与形参相对应。
第三步,判断enhancer是否存在,如果不是函数,就抛出错误。如果是函数则直接返回enhancer(createStore)(reducer, preloadedState),这里可能不好理解,需要参照applyMiddleWare函数部分源码。
第四步,判断传入的reducer参数的类型是否是函数,如果不是函数,抛出错误。
到这里,函数参数的校验就结束了。
2.定义函数内的局部变量来存储传入的参数和后续会用到的状态等信息

let currentReducer = reducer //传入的reducer参数
let currentState = preloadedState //当前store里的state
let currentListeners = []//当前监听函数数组
let nextListeners = currentListeners //接下来的监听函数数组
let isDispatching = false //是否正在dispatch

3.定义六个函数来对来处理store的数据交互
第一个函数:ensureCanMutateNextListeners

function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
        nextListeners = currentListeners.slice()
    }
}

此函数的功能较为简单,就不在说明
第二个函数:getState

function getState() {
    if (isDispatching) {
        throw new Error('msg')
    }
    return currentState
}

此函数就是最终暴露出来的store.dispatch函数,函数首先判断当前是不是处于dispatching状态。是的话直接抛出错误。否则就将函数的局部变量current返回。
第三个函数:subscribe

function subscribe(listener) {
    if (typeof listener !== 'function') {
        throw new Error('msg')
    }
    if (isDispatching) {
        throw new Error('msg')
    }
    let isSubscribed = true;
    ensureCanMutateNextListeners();
    nextListeners.push(listener);
    return function unsubscribe() {
        if (!isSubscribed) {
            return
        }
        if (isDispatching) {
            throw new Error('msg')
        }
        isSubscribed = false;
        ensureCanMutateNextListeners();
        const index = nextListeners.indexOf(listener);
        nextListeners.splice(index, 1)
    }
}

此函数也是最终暴露出来的store.subscribe函数,此函数用于添加一个订阅state变化的函数。传入参数为函数,如果传入非函数,或者正在dispatching时调用,会抛出错误。函数返回了一个函数用于取消订阅。

接下来逐步分析此函数的执行过程,首先参数和当前状态的校验。然后定义局部变量标记是否已经订阅并标记为true,然后调用之前的ensureCanMutateNextListeners()函数。以确保nextListeners与currentListeners相同。然后将传入的listener函数添加到nextListeners数组。随后返回一个新的函数用于取消订阅。

在返回的新的函数中,首先做了状态的判断,如果正在dispatching,那么抛出错误,通过之前定义的是否订阅标记判断,当前的listener是否还在被订阅。如果已经取消了订阅,那么直接返回,(这里主要处理了取消订阅函数可以被多次调用从而产生错误的情况。这里使用了闭包)否则,将取消订阅的标记置为false。再此执行ensureCanMutateNextListeners(),原因同上。然后使用数组的indexOf方法在当前的
nextListeners数组中找出对应的索引。使用数组的splice方法将其删除。

第四个函数:dispatch

function dispatch(action) {
    if (!isPlainObject(action)) {
        throw new Error('msg' )
    }

    if (typeof action.type === 'undefined') {
        throw new Error('msg')
    }

    if (isDispatching) {
        throw new Error('msg')
    }

    try {
        isDispatching = true;
        currentState = currentReducer(currentState, action)
    } finally {
        isDispatching = false
    }

    const listeners = (currentListeners = nextListeners);
    for (let i = 0; i < listeners.length; i++) {
        const listener = listeners[i];
        listener()
    }
    return action
}

这个函数也是最终暴露出来的store.dispatch方法。主要用于触发action以修改state,是非常重要的几个函数之一。函数执行过程中。首先校验参数action必须为普通javaScript对象,且action.type必须不能为undefined,并且当前不能处于dispatching状态。否则就会抛出错误。校验通过后。将isDispatching置为true,通过执行currentState = currentReducer(currentState, action)更改state,此时便成功的触发了一个action。在执行完成之后,将isDispatching置为false。随后依次执行订阅此store的函数。代码如下

const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {
    const listener = listeners[i];
    listener()
}

最后将action参数返回
第五个函数:replaceReducer

function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
        throw new Error('Expected the nextReducer to be a function.')
    }
    currentReducer = nextReducer;
    dispatch({ type: ActionTypes.REPLACE })
}

此函数也是直接暴露出来的store.replaceReducer函数,一般在实际开发中使用较少。 主要用于动态加载reducer

第六个函数:observable

function observable() {
    const outerSubscribe = subscribe;
    return {
        subscribe(observer) {
            if (typeof observer !== 'object' || observer === null) {
                throw new TypeError('Expected the observer to be an object.')
            }

            function observeState() {
                if (observer.next) {
                    observer.next(getState())
                }
            }

            observeState();
            const unsubscribe = outerSubscribe(observeState);
            return { unsubscribe }
        },

        [$$observable]() {
            return this
        }
    }
}

接下来createStore函数将直接返回已经创建好的函数。

return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
}

3.2 combineReducers.js

combineReducers.js文件在去除注释和错误提示的情况下,代码行数约为130行左右。查看源码。此js文件中主要有4个函数

函数名 作用 备注
getUndefinedStateErrorMessage 供redux内部调用,根绝action和key生成错误信息,例如reduce函数返回undefined时的情况
getUnexpectedStateShapeWarningMessage 供redux内部调用,用于获取警告信息,主要进行了 reducer的判空,当前的state是否为简单对象,给state中存在而reducer中不存在的属性添加缓存标识,并返回警告信息。
assertReducerShape 检测用于组合的reducer是否符合redux对顶的reducer
combineReducers 直接暴露出来,用于合并reducer

assertReducerShape,主要对于传入的reducer数组进行便利进行检验,首先调用reducer(undefined,{ type: ActionTypes.INIT })以获取initState,initState如果是undefined则抛出错误信息,提示必须返回初始state,如果不想为这个reducer设置值,要返回null而不是undefined。如果没有出错,则调用reducer(undefined, { type: ActionTypes.PROBE_UNKNOWN_ACTION() }),通过未知的action,来检测reducer能否正确处理,即返回的值类型是否为undefined,如果是则抛出错误原因。

combineReducers, 函数首先对传入的reducers对象进行遍历,将结果赋值到局部变量finalReducers。如果不是正式环境,那么对于为null的reducer进行提示,如果是正式环境,则忽略类型部位函数的reducer。然后调用assertReducerShape对finalReducers进行类型校验,并存储错误信息到局部变量shapeAssertionError。然后反对一个合并完成的reducer函数。
在新返回的函数中,首先判断shapeAssertionError,如果存在错误就抛出,对于非正式环境,使用getUnexpectedStateShapeWarningMessage进行校验,并提醒错误。定义一个标记表示state是否已经变化并置为false,定义下一个状态的结果nextState,接下来遍历执行reducer。最终将nextState返回

let hasChanged = false
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
  const key = finalReducerKeys[i]
  const reducer = finalReducers[key]
  const previousStateForKey = state[key]
  const nextStateForKey = reducer(previousStateForKey, action)
  if (typeof nextStateForKey === 'undefined') {
    const errorMessage = getUndefinedStateErrorMessage(key, action)
    throw new Error(errorMessage)
  }
  nextState[key] = nextStateForKey
  hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state

3.3 applyMiddleware.js

源码截取

import compose from './compose'

export default function applyMiddleware(...middlewares) {
    return createStore => (...args) => {
        const store = createStore(...args);
        let dispatch = () => {
            throw new Error(
                'Dispatching while constructing your middleware is not allowed. ' +
                'Other middleware would not be applied to this dispatch.'
            )
        };

        const middlewareAPI = {
            getState: store.getState,
            dispatch: (...args) => dispatch(...args)
        };
        const chain = middlewares.map(middleware => middleware(middlewareAPI));
        dispatch = compose(...chain)(store.dispatch);
        // 注意由于函数说引用类型所以此时middlewareAPI的dispatch函数也一同更改。
        return {
            ...store,
            dispatch
        }
    }
}

applyMiddleware函数主要是与中间件有关的函数,他允许我们在action到达reducer之前对action进行加工处理。

使用

// createStore中对于中间件的调用
return enhancer(createStore)(reducer, preloadedState)
//applyMiddleware函数的使用;
let store=createStore(reducer,preloadStste,applyMiddleware(thunk))
// redux-thunk源码
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

这里部分源码设计到了高阶函数,初次阅读可能不太好理解,所以将于applyMiddleware有关的内容均列在了上面。由此来梳理applyMiddleware函数的工作流程。

在创建store时,首先调用createStore方法,第三个参数为applyMiddleware函数返回的新的函数(起名字为函数1,源码中第一个箭头函数)。在createStore源码中可以看到,检测到第三个参数数为函数时将会执行此函数(函数1),并且把createStore函数作为函数1的参数。函数1的执行会返回一个新的函数(函数2,源码中第二个箭头函数)。通过createStore源码可以看到,此时再次对函数二,进行了执行,传入的参数时reducer和preloadState。此时便开始执行applyMiddleware函数的主体部分。

在applyMiddleware中可以看到,首先根据传入的reducer和preloadState创建了store。然后定义了一个dispatch函数。作用是执行时的校验,避免在执行函数时dispaching。然后创建了middlewareAPI对象供中间件函数使用。然后将middlewareAPI作为参数,便利执行中间件数组函数。将返回的结果存储在chain数组。通过thunk函数的源码使得我们可以了解到此时存储在chain书中的的每一项仍然是一个函数,函数接收的形参为next。随后将store.dispatch作为参数执行chain数组中的每一个函数,具体为首先执行第一个函数,将store.dispatch作为实参传入。将其返回的结果作为第二个函数的参数,以此类推,将最后结果赋值给dispatch。根绝thunk的源码可以看到,对于chain数组的每一项,执行后仍然会返回一个新的函数,这个新的函数的形参为action,这个新的函数恰恰就是我们要自己开发的中间件函数。最后将store和dispatch返回。此时回到createStore源码,可以看到,返回的恰恰就是最终创建的store。

现在已经梳理创建含有中间件的store的函数执行过程。下面来分析一下store.dispatch这个过程。

接着上面的来说,最终返回了dispatch函数并且作为了store的一个属性。当我们触发action的时候,显然使用的就是这个dispatch函数。现在我们来看看这个dispatch函数是什么样子的。

由于之前使用了compose函数,所以这部分呢可能不太容易理解,此时我们,假设两个中间件函数第一个为上面所提到的thunk中间件,假设第二个为打印log的中间件,经过map处理后的如下。

const logger = store => next => action => {
  console.group(action.type)
  console.info('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  console.groupEnd(action.type)
  return result
}

const thunk = ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };

为了利于理解我们首先考虑只有一个中间件的情况,此时

dispatch=componse([logger(middlewareAPI)])(store.dispatch)

//等同于
dispatch=logger(middlewareAPI)(store.dispatch)
    
// 所以

store.dispatch(action)=logger(middlewareAPI)(store.dispatch)(action)
    
// 此时发现
logger(middlewareAPI)(store.dispatch)(action) 与定义时的刚好对应

考虑有多个中间件的情况。

dispatch=componse([logger(middlewareAPI),thunk(middlewareAPI)])(store.dispatch)

执行流程分析

  1. 根据compose函数的源码可以将上面表达式转化为
dispatch=thunk(middlewareAPI)(logger((middlewareAPI)(store.dispatch)))
  1. 首先执行logger(middlewareAPI)(store.dispatch)函数,函数返回一个类似于next=>action=>{} 的函数。执行这个函数,传入的参数next的值恰好是store,dispatch。
  2. 然后执行thunk函数,函数也返回一个类似于next=>action=>{} 的函数。thunk()函数执行时传入的是logger函数返回的函数。
  3. 多个中间件以此类似
  4. 当到最后一个中间件时,返回类似于next=>action=>{}的函数。next参数是倒数第二个中间件返回的函数。最后一个函数返回的函数被赋值给了dispatch

总结:通过闭包存储了最新的store值。通过compose函数,使得每个中间件的next参数指向其后面的中间件函数。最后一个中间件指向store.dispatch。当触发action时,action会依次的经过中间件的处理。在每个中间件中可以通过store.getState()取得最新的state值,通过dispatch可以从第一个中间件触发dispatch()。通过调用next(action)触发下一个中间件函数

 dispatch = compose(...chain)(store.dispatch) 

compose函数
函数传入的数组的每一项是形如next=>action=>{}的函数。
作用

  • 每一个函数的next参数是对他之后函数的返回值
  • 最后一个函数的action是store.dispatch
  • 最后的返回值是第一个函数的返回值,赋值给dispatch
  • 每次调用dispatch,就是调用第一个中间件
  • 在中间件函数内调用next就是调用下一个中间件的(action)=>{}
  • 调用最后一个中间件的next,会调用store.dispatch,更新state。

接着从applyMiddleware函数源码compose部分开始分析。此时的dispatch函数如下

dispatch=componse([chainOne,chainTwo])(store.dispatch)

根据compose源码。

dispatch=chainTwo(chainOne(store.dispatch))

3.4 bindActionCreators.js

源码截取

function bindActionCreator(actionCreator, dispatch) {
    return function () {
        return dispatch(actionCreator.apply(this, arguments))
    }
}

export default function bindActionCreators(actionCreators, dispatch) {
    if (typeof actionCreators === 'function') {
        return bindActionCreator(actionCreators, dispatch)
    }
    
    // 校验参数必须为对象
    if (typeof actionCreators !== 'object' || actionCreators === null) {
        throw new Error(
            `bindActionCreators expected an object or a function, instead received ${
                actionCreators === null ? 'null' : typeof actionCreators
                }. ` +
            `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
        )
    }
    
    const boundActionCreators = {}
    for (const key in actionCreators) {
        const actionCreator = actionCreators[key]
        if (typeof actionCreator === 'function') {
            boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
        }
    }
    return boundActionCreators
}

在redux中改变state只能通过store.dispatch(action)的方式,这样迫使我们不得不将store.dispatch函数进行逐层传递,这样会增加一些无用的重复代码。

这时就需要使用bindActionCreators来加工actionCreators函数。bindActionCreators(actionCreators, dispatch)返回一个与原函数/对象相同的新函数。因为过闭包保存了store.dispatch并且通过apply调整了this的指向。直接执行返回的函数/对象的属性便可以触发数据的改变,使得我们不在需要将dispatch逐层传递,也使得我们可以像执行普通函数一样来触发action。

3.5 compose.js

源码截取

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

函数的作用为将多个函数组合成一个函数。
使得compose(f,g,h)(...arg)等同于(...arg)=>f(g(h(...arg)))
在源码的applyMiddleware函数中使用了此函数。

四、总结

以上这些内容就是我对于redux的部分理解。

redux简单的说就是一个状态管理工具。也可以与除了react之外的其他框架组合使用,比如说Vue.js,当然选择Vuex对于Vue是更好的选择。对于React来说redux也不是其唯一的状态管理工具,除此之外也有dva,mobox

五、写在最后

以上这些内容就是我对于redux的部分理解。从开始学习至文章产出大约一周左右。由于个人能力有限,所以可能有很多的不足之处。当然这篇文章也会不断的更新,完善。

预告下一篇不定时更新文章,redux有关部分框架的源码解析,比如react-redux,react-saga,reacr-router-redux
推荐下载源码进行阅读学习,相信会有很多的收获,加油

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

推荐阅读更多精彩内容