源码阅读:Vue的响应式原理(一)

前言

  1. observer部分完整的源码注释放在github上了,有兴趣的可以去看看,如果发现有误情不吝赐教!observer
  2. 这篇文很长长长长长长长长,而且比较费脑,我也整理了很久,如果对这篇文感兴趣,请自带☕️和无限的耐心~
  3. 这篇文只是Vue的响应式原理的一部分,后面还有很多很多很多的知识本文没有涉及到。

Vue的双向数据绑定和Angular很不一样。
Angular采用的是“脏检查”的方法,当我们触发了某些事件(定时,异步请求,事件触发等),执行完事件之后,Angular会对所有“注册”过的值进行一遍“全面检查”,也就是遍历所有的值,判断是否和之前的一致。这种方法效率不高,因为我们修改一个小地方都会带来两次以上的全面检查,如果我们绑定的view比较多,就可能会存在比较明显的性能问题了。
而Vue的处理方式则不同,它结合观察者模式发布-订阅模式,当我们改变了一个数值,它会主动通知与它相关的订阅者,告诉他们可以进行相关的操作了,这种方法和“脏检查”相比,更加优雅,效率会更高。

1. 响应式的基石:Object.defineProperty(obj, prop, descriptor)

MDN : Object.defineProperty(obj, prop, descriptor)

我们都知道对象有两种属性,一种是数据属性,一种是访问器属性。
数据属性有4个特性:configurable, enumerable, value, writable
访问器属性也有4个特性:configurable, enumerable, get, set

平时我们通过普通赋值的方法(比如:obj.a - 'a')添加的属性都是数据属性,且默认configurable,和enumerable都为true
而用Object.defineProperty()可以添加数据属性或者访问器属性,默认configurableenumerable都为false

关于getter/setter
访问器属性是没有value的,但是他们可以用来劫持对另一个数据的访问,举个例子:

var log = console.log.bind(console);
var obj = {
    _year: 2017
};
Object.defineProperty(obj, 'year', {
    get: function getter () {
        return this._year;
    },
    set: function setter (value) {
        this._year = value;
    }
});
log(obj.year); // 2017
obj.year = 2018;
log(obj._year); // 2018

这个例子中我们访问obj.year,会返回obj._year的值,我们修改obj.year,会修改obj._year的值。

根据这个特性,我们可以实现视图-数据双向绑定:

<body>
    <p id="test-p">lalal</p>
</body>
<script>
    var log = console.log.bind(console);
    var obj = {}
    Object.defineProperty(obj, 'test-p', {
        get: function getter () {
            return document.getElementById('test-p').innerHTML;
        },
        set: function setter (value) {
            document.getElementById('test-p').innerHTML = value;
        }
    });
    log(obj['test-p']);
    setTimeout(function changeData () {
        obj['test-p'] = 'hahah';
    }, 3000);
</script>

是不是特别好玩?

2. 观察者模式

维基百科:观察者模式

在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。
一图胜千言,我画了张简单的流程图,应该很容易看懂:

观察者模式

详细一点讲,流程大概是这样的:

目标对象有这么几个方法:

  1. setState:设置对象的状态,该函数调用了NotifyObserver方法
  2. getState:取得对象当前的状态
  3. addObserver:添加观察者
  4. removeObserver:删除观察者
  5. NotifyObserver:通知观察者:我的状态改变了,该方法会调用各个观察者的Notify方法

观察者对象有个方法:
Notify:该方法会会调用目标对象的getState方法,然后对目标对象的新值作出一些反应,比如说,打印出来之类的。

如果我们写一个最简单的观察者模式,那可能是这样的:

var log = console.log.bind(console);

function Oberser (target, cb) {
  (function(){ // 添加到目标对象
    target.addOberser && target.addOberser(this);
    console.log('oberser added')
  }).call(this);
  
  this.notify = cb;
}

var target = {
  _value: 2017,
  obersers: [],
  addOberser: function (oberser) { // 添加观察者
    this.obersers.push(oberser);
  },
  removeOberser: function (oberser) { // 删除观察者
    // ...
  },
  notifyOberser: function () { // 通知观察者
    this.obersers.map(oberser => oberser.notify && oberser.notify());
  }
}

Object.defineProperties(target, {
  value: {
    get: function () {
      return this._value;
    },
    set: function (newValue) {
      this._value = newValue;
      this.notifyOberser(); // 调用notifyOberser
    }
  }
});

var oberser1 = new Oberser(target, function () {
    log(`I'm observer1, the value of my target is ${target.value}`);
});
var oberser2 = new Oberser(target, function () {
    log(`I'm observer2, the value of my target is ${target.value}`);
});

target.value = 2018;
/*
oberser added
oberser added
I'm observer1, the value of my target is 2018
I'm observer2, the value of my target is 2018
*/

当然了如果我们要观察同一个对象中的多个属性,就不能用这种方法了,因为我们总不能一个属性更新,所有观察者都全部调用一遍吧?最好是每一个属性都能有自己的观察者。

3. 正题

怎么给每个对象都维护一个观察者的列表呢?Vue是这样做的:
Vue在观察者模式中结合发布-订阅模式,其中涉及到了三个重要的对象:Observer, Dep, Watcher
Observer负责观察目标数据的变化,如果数据变化了,那么通知Dep。
Dep负责维护一个订阅者列表(收集依赖),当接收到Observer的通知时,他就通知所有订阅者:目标数据更新了。
Watcher维护一个回调函数,当接收到Dep的通知时,执行回调函数。

Vue响应式原理

可以这么理解:Observer是教师,Dep是教学在线,Watcher是学生。教师不必维护自己的学生列表,教务处帮他维护。学生不必维护自己的课表,因为教务处也会帮他维护。每次教师布置了新作业等(比喻不是很恰当),他只需要跟教学在线说一声就可以了,教学在线就发邮件告诉每一个上了这门课的学生:有新作业了。学生就可以分别对这个新作业作出不同的反应。

原理已经了解得差不多了,接下来看一下源码吧。
先看一下 Observer类,Vue会给每一个响应式的数据添加一个observer,这个observer就负责观察这个数据有没有发生变化。
中文注释是我加的,英文注释是作者加的,不要漏了英文注释,很重要!

export class Observer {
  value: any; // 被观察的对象,比如vue的根属性data,在vue实例初始化的时候,vue会为data属性添加一个observer对象,介时observer对象的value属性指向data,而data的__ob__属性指向observer对象
  dep: Dep; // 每一个observer对象都有一个dep,负责收集依赖和通知
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this) // value的__ob__属性指向这个observer对象本身,比如L38注释中说到的data属性
    if (Array.isArray(value)) { // 如果value为数组,那么增强这个数组
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value) // 往下递归数组,如果数组中有元素为对象或者数组,也会给其添加observer
    } else { // 如果value为对象,那么往下递归对象,如果对象中有属性为对象或者数组,也会给其添加observer
      this.walk(value)
    }
  }

  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) { // 遍历对象,把对象中的属性都转化为getter/setter对
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]) // 很重要!
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) { // 遍历数组,如果数组中有元素为对象或者数组,也会给其添加observer
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

Observer类的constructor函数中,可以看到对于数组和对象,Vue的处理是不一样的,为什么呢?
数组是没有Object.defineProperty(obj, prop, descriptor)这个方法的,这就意味着我们没有办法监听数组中属性的添加,删除。
如果你在Vue中处理过数组,你应该知道,在Vue中,数组只有7个常用方法可以触发视图的更新:push(), pop(), shift(), unshift(), splice(), sort(), reverse()。这是因为Vue对这些方法进行了增强,原理很简单,类似于这样(当然实际上要严谨一些,这里只是帮助理解):

var log = console.log.bind(console);
var arrayMethods = Object.create(Array.prototype); // 继承自Array.prototype,保留了数组原本的特性
arrayMethods.unshift = function (value) { // 重写方法
    Array.prototype.unshift.call(this, value); // 调用原来的方法
     notify(); // 并进行通知
}
function notify () {
    console.log('unshift');
}
var arr = [1, 2, 3];
arr.__proto__ = arrayMethods;
arr.unshift(0);
log(arr)
/*
[ 1, 2, 3 ]
unshift
[ 0, 1, 2, 3 ]
*/

上面的代码截断了数组的原型链,我们新创建了一个对象arrayMethods,这个对象继承自Array.prototype,然后改写里面的unshift()方法。这样既保证我们保留了数组的length等属性,有可能使用自己定义的unshift()方法,我们在unshift()方法中调用了notify()函数。

Vue是怎么给每一个对象都加上一个Observer对象的?上面代码中,在处理数组的函数observeArray里,可以看到Vue遍历了一遍数组,并对每一个元素调用了observe()函数。
而在处理对象的函数walk中,对每一个属性都调用了defineReactive函数(这个函数非常重要,后面再说),这个defineReactive函数内部也对属性都调用了一遍observe()函数。
也就是说,Vue是通过observe()函数来给对象添加observer的。看一下observe()函数:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) { // 只有对象或数组才会进入这个函数
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // value已经有了自己的observer
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value) // value为对象且没有自己的observer,那么为他新建一个observer,注意这里说明了Vue对每一层的属性或元素递归添加了observer
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

注意到里面有一行代码:ob = new Observer(value),也就是说,Vue递归遍历了每一层的属性或元素,如果这个元素/属性的类型为对象/数组,那么它也会有一个自己的observer。

好,现在我们已经明白了Vue怎么遍历数组来把数组转化为响应式的了,那接下来再看看Vue如何处理对象属性:
高能预警!

export function defineReactive ( // 每个属性都转化为getter/setter,并且每个类型为对象(包括数组)的属性都会拥有自己的observer
  obj: Object, 
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean // shallow为true的话,属性不会有自己的observer,也就是该属性将不具备响应性
) {
  const dep = new Dep() // 注意这个函数将会出现两个dep,这里第一个dep,将会被闭包进getter/setter函数中

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key] // 注意,这个val也会被闭包进getter/setter方法中,我之前还疑惑把属性都转化为getter/setter值是怎么存储数据的,就是把这个val闭包进去的
  }

  let childOb = !shallow && observe(val) // 每一个observer会有一个dep属性,所以这里有了第二个dep,这个dep会在该属性的属性被增删的时候通知订阅者
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val // 执行属性原本自己有的getter
      if (Dep.target) { // 如果存在 Dep.target 这个全局变量不为空,表示是在新建 Watcher 的时候调用的
        dep.depend() // 这里是第一个dep,当Dep.target依赖于这个属性的时候,他会调用该属性的getter,这是dep.depend()就会把Dep.target添加进自己的订阅列表,这样在属性的setter被调用的时候,这个dep就可以通知Dep.target了
        if (childOb) {
          childOb.dep.depend() // 第二个dep也会收集依赖,那么该属性的属性被添加或者删除的时候,这个dep就可以通知这个属性的订阅者了
          if (Array.isArray(value)) { // 如果value为一个数组,那么是无法通过getters来窃听对数组元素的访问的,所以要向下遍历数组,给里面的元素都收集依赖
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) { // 没有变化/newVal为NaN/value为NaN
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

这个函数逻辑比较复杂,让我们好好来捋一下思路:
1. 闭包的妙用
如果你好好观察一下属性的getter/setter方法,你会发现他们闭包了这几个变量:
getter, setter, val, dep,childOb

其中,getter, setter两个变量可能是我们自己定义的getter/setter方法,因为我们有时候也会有需要访问器属性的时候。

val是我们原本使用自己的getter/setter想要访问的值,比如这篇文章第一个代码块的_year属性。
我之前还在疑惑,因为访问器属性是没有自己的值的,Vue把对象的属性转化为访问器属性之后,要怎么维护之前的值,原来是闭包进来了!
看一下源码,当我们调用属性的setter方法的时候,最后修改的是这个val的值,而我们调用getter方法的时候,返回的也是这个val的值。

dep, childOb在第3小节一起讲。

2.Dep
是时候介绍一下Dep类了,不然后面的讲不下去。
Dep的结构很简单,大概长这样:

export default class Dep { // dep是dependence的缩写,他负责收集依赖,以及通知订阅者。每一个Observer对象有其自己的的dep
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>; // 订阅者列表,

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) { // 添加订阅者
    this.subs.push(sub)
  }
 
  removeSub (sub: Watcher) { // 删除订阅者
    remove(this.subs, sub)
  }

  depend () { // 添加依赖,也就是把当前Dep.target添加到这个dep实例的subs列表中
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () { // 通知watcher,执行所有watcher的.update()方法,更新watcher的数据
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

可以看到,一个Dep类会有一个自己的id,维护自己的一个订阅者列表,并切可以添加,删除,通知订阅者。
Dep类中有一个静态属性Dep.target,学过C++的同学应该知道,静态属性也就是类属性,是所有实例共享的。这个target是干什么用的呢?

Vue在处理一个watcher的时候,就会把Dep.target的值设为当前的watcher,举个例子,这是我们的Vue实例(该例子从官网中复制过来的):

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 计算属性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 实例
      return this.message.split('').reverse().join('')
    }
  }
})

现在假设处理到了reversedMessage,先把Dep.target指向它。很明显reversedMessage依赖了message,我们是不是要在message对应的dep的订阅者列表中加上reversedMessage?问题来了,我们要怎么添加这个依赖?

首先因为reversedMessage会访问到message,也就是会调用message的getter方法,那我们可以在getter方法中进行依赖收集,但是getter方法是没办法传参的,所以它也没办法知道谁订阅了它。

这时候Dep.target就起作用了,我们前面已经说过,Vue处理到了哪个watcher,就会把Dep.target指向它,那么此时的Dep.target肯定就是reversedMessage,我们只需要在getter函数中把Dep.target添加进订阅者列表就可以了!
那么这时,当我们改变message的值时,会调用其setter函数,setter函数中dep就会调用dep.notify()方法,通知reversedMessage:我更新了!

真的太妙了!

3. 两个dep
源码中我在注释中也提醒过了,这个函数中出现了两个dep,一个是在函数开头就新建的dep,另一个是属性自己的observer中的dep。
dep的作用是什么?收集依赖并在适当的时候通知订阅者:目标数据更新了。

在源码中,属性的getter方法中,给dep, childOb都添加了依赖,为什么在setter方法中,只通知了dep?或者说,childOb的意义在哪里呢?

先看一个例子:

var obj = {
  _a: { aa: 1}
};
Object.defineProperty(obj, 'a', {
  configurable: true,
  enumerable: true,
  get: function () {
    log('get a:' + this._a);
    return this._a;
  },
  set: function (newV) {
    log('set a:' + newV);
    this._a = newV;
  }
});
obj.a; // get a:[object Object]
  obj.a.bb = 2; // get a:[object Object]
delete obj.a;
// 删除该属性的时候,没有调用getter/setter函数!

可以很明显看出getter/setter的缺陷:只能监听到属性的更改,不能监听到属性的删除与添加。

我们都知道Vue提供了内置的Vue.set(), Vue.delete()方法来让我们响应式的添加和删除数组的元素或对象的属性。
官方文档
官方文档是这么说的:

Vue.set()这个方法主要用于避开 Vue 不能检测属性被添加的限制。
Vue.delete()这个方法主要用于避开 Vue 不能检测到属性被删除的限制。

我们前面已经证明了,setter是不会在属性被删除或者添加的时候调用的,那么Vue是怎么在删除和添加的时候通知watcher的?其实跟数组方法的增强事同一个套路,把Vue.delete()的源码简化简化再简化之后:

function del () {
  delete obj.a;
  childOb.notify(); // 通知obj的watcher:我有一个属性删除了。
}

所以为什么在getter方法中要添加childOb的依赖,就是为了在删除或者添加属性的时候进行通知。

4. 如何向下收集依赖
是这样的,假设数据是这样的let data = {a: {b: {c: {d: {e: 1}}}}},有一个模板引用了{{a.b.c}},那么我们修改a.b.c.d.e,这时watcher会被通知到吗?

答案是不会。为什么?一步一步来看。

  1. 首先我们知道每一层的属性,也就是a, b, c, d, e,都有自己的observer,而且watcher订阅observer是通过getter方法来实现的,没有getter方法就没法订阅。

  2. 它调用了c的getter方法,因此c更改了(整个对象被替换),会有通知,c删除了,也会有通知。

  3. 我们修改了a.b.c.d.e

  4. 模板引用的是{{a.b.c}},那么它没有调用到e的getter方法。

  5. 因此我们修改了e,watcher就没办法知道了。

Vue是怎么解决这个问题的呢?

它是这样做的:当模板引用了{{a.b.c}}时,此时Dep.target是这个模板,然后,Vue从c开始往下遍历,对每个属性都"touch"一下,也就是强行调用一下getter方法,这样,模板就加入了所有属性的订阅者列表中。

有兴趣的同学可以自己去看一下Vue源码中的traverse.js

5. dependArray函数
终于要进入尾声了🙂️,看源码真的很费心神,但是收获真的超级大呀!
defineReactive函数中的getter方法中,对数组有一个额外的处理过程:如果value为数组,那么对其执行dependArray函数。

想了好久才想明白为什么要进一步的处理。

回到最开始,我们给一个对象添加一个observer,那么他会遍历所有的属性,把属性都转化为getter / setter。

但是给数组添加一个observer,他只是添加了8个具有响应性的方法。(当然他也会给子对象添加observer)

这时我们push,pop数组,是响应式的,数组的dep知道他要通知订阅者们。

但是如果我们改变的是数组的元素,比如,对于一个数组var arr = [1, 2, 3, {a: 4}],现在我们这样操作arr[0] = 0,数组是不会有响应的。

这也是为什么vue给数组加了两个方法Vue.set, Vue.delete来添加和删除元素的原因。

再回到这个问题上,我们已经有了Vue.set, Vue.delete两个方法,那我们操作基本类型的元素基本没啥问题了,但是如果是像arr[3].a = 5这种呢?Vue的解决方法就是递归遍历数组,遇到类型为object的元素,就把当前的Dep.target添加到它的订阅者列表中,这时它的变化就可以被监听了。

这一切的根本原因,就是数组没法通过getter/setter对象来监听元素的变化。

最后附上dependArray的源码。

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

推荐阅读更多精彩内容