Vue源碼--數(shù)據(jù)驅(qū)動

VUE核心思想是數(shù)據(jù)驅(qū)動摆出,比如在使用之中 我們在界面上點擊按鈕修改數(shù)據(jù) 朗徊,在vue上只需要修改數(shù)據(jù)即可,DOM就會自動更新偎漫,而jQuery卻需要手動去操作DOM去更新爷恳。這樣有利于代碼的清晰。
第一步:new Vue()發(fā)生了什么象踊?
Vue源碼中在src/core/instance/index.js文件下有定義了一個Vue類

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options) //這個函數(shù)在旁邊init.js中聲明
}
//初始化一些Vue 為VUE對象添加方法

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

src/core/instance/init.js

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++
 
    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
        //合并對象
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

Vue 初始化主要就合并配置温亲,初始化生命周期,初始化事件中心杯矩,初始化渲染栈虚,初始化 data、props史隆、computed节芥、watcher 等等。

初始化邏輯之后再看
下面檢測到新對象如果有$el,則使用$amount掛載头镊,在調(diào)用這個函數(shù)之前 屏幕上還是{{mess}}這種行書 執(zhí)行結(jié)束之后就變成相應(yīng)的數(shù)據(jù)了蚣驼。

2、$mount是怎么渲染的相艇?
Vue 中我們是通過 $mount 實例方法去掛載 vm 的颖杏,$mount 方法在多個文件中都有定義,如 src/platform/web/entry-runtime-with-compiler.js坛芽、src/platform/web/runtime/index.js留储、src/platform/weex/runtime/index.js
$mount的實現(xiàn)方式與平臺和構(gòu)建方式有關(guān)的 接下來分析帶compiler版本的$mount的實現(xiàn)。

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el) 

  /* istanbul ignore if */ // 判斷不能把APP掛載到body下
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
//判斷沒有render下 把template和el全部轉(zhuǎn)成render函數(shù)進行渲染
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
    //這個函數(shù)轉(zhuǎn)化成render函數(shù)  后面再看其中邏輯
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating) // 最后返回函數(shù) 和不帶compile的模式的\$mount的合并
}

這段函數(shù)大概做的事情在注釋用中文標(biāo)示出來了 主要上面 1判斷不能掛在在body上 2 compileToFunction()把template和el轉(zhuǎn)化成render函數(shù) 3最后返回函數(shù)和不帶compile的版本的$mount合并

原先原型上的 $mount 方法在 src/platform/web/runtime/index.js 中定義咙轩,之所以這么設(shè)計完全是為了復(fù)用获讳,因為它是可以被 runtime only 版本的 Vue 直接使用的。

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

$mount 方法支持傳入 2 個參數(shù)活喊,第一個是 el丐膝,它表示掛載的元素,可以是字符串钾菊,也可以是 DOM 對象帅矗,如果是字符串在瀏覽器環(huán)境下會調(diào)用 query 方法轉(zhuǎn)換成 DOM 對象的。第二個參數(shù)是和服務(wù)端渲染相關(guān)煞烫,在瀏覽器環(huán)境下我們不需要傳第二個參數(shù)浑此。

由于篇幅太大 一些代碼就不貼了 貼重要的就OK
$mount 方法實際上會去調(diào)用 mountComponent 方法,這個方法定義在 src/core/instance/lifecycle.js 文件中:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
       //沒法解析template  如果需要使用帶compile的版本
        )
      } 
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) { //mark與vue運行狀況相關(guān)
    updateComponent = () => {
      //mark相關(guān)代碼
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating) //vm._render會生成vnode
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

主要是定義這個updateComponent這個函數(shù) 滞详,mark版本的那個先不看 下面這個主要就是執(zhí)行vm._update(vm._render(), hydrating) 而這個函數(shù)的執(zhí)行就是借助這個Watcher 這個就是觀察者 主要就是監(jiān)聽來執(zhí)行 這是重點 就是監(jiān)聽變化 當(dāng)有dom或者是數(shù)據(jù)什么的發(fā)生變化也就通過Watcher來執(zhí)行updateComponent()去重新掛載凛俱。
vm._update(vm._render(), hydrating)這個_update去更新 ,_render()函數(shù)是獲得Vnode作為參數(shù)傳給update去更新 接下來去看_render()
看Watcher類傳入了5個參數(shù) 第三個noop表示一個空對象 只是提一下 后面會介紹Watcher類

3料饥、接下來看_render函數(shù)

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

   // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
        } catch (e) {
          handleError(e, vm, `renderError`)
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    } finally {
      currentRenderingInstance = null
    }
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }

這段代碼最關(guān)鍵的是 render 方法的調(diào)用最冰,我們在平時的開發(fā)工作中手寫 render 方法的場景比較少,而寫的比較多的是 template 模板稀火,在之前的 mounted 方法的實現(xiàn)中,會把 template 編譯成 render 方法赌朋,但這個編譯過程是非常復(fù)雜的凰狞,我們不打算在這里展開講,之后會專門花一個章節(jié)來分析 Vue 的編譯過程沛慢。

調(diào)用render.call()的時候 vm._renderProxy這個參數(shù)在非生產(chǎn)環(huán)境下表示this 就是當(dāng)前的Vue對象 而在 生產(chǎn)環(huán)境下使用es6的proxy來一些處理

在 Vue 的官方文檔中介紹了 render 函數(shù)的第一個參數(shù)是 createElement赡若,那么結(jié)合之前的例子
我們在VUE中使用render函數(shù)時候

render: function (createElement) {
  return createElement('div', {
     attrs: {
        id: 'app'
      },
  }, this.message)
}

再回到 _render 函數(shù)中的 render 方法的調(diào)用:
vnode = render.call(vm._renderProxy, vm.$createElement)
可以看到,render 函數(shù)中的 createElement 方法就是 vm.$createElement 方法:

export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
 
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
 
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  const parentData = parentVnode && parentVnode.data

  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}

實際上团甲,vm.$createElement 方法定義是在執(zhí)行 initRender 方法的時候逾冬,可以看到除了 vm.$createElement 方法,還有一個 vm._c 方法,它是被模板編譯成的 render 函數(shù)使用身腻,而 vm.$createElement 是用戶手寫 render 方法使用的产还, 這倆個方法支持的參數(shù)相同,并且內(nèi)部都調(diào)用了 createElement 方法嘀趟。
總結(jié)
vm._render 最終是通過執(zhí)行 createElement 方法并返回的是 vnode脐区,它是一個虛擬 Node。Vue 2.0 相比 Vue 1.0 最大的升級就是利用了 Virtual DOM她按。因此在分析 createElement 的實現(xiàn)前牛隅,我們先了解一下 Virtual DOM 的概念。

Virtual DOM 就是用一個原生的 JS 對象去描述一個 DOM 節(jié)點酌泰,所以它比創(chuàng)建一個 DOM 的代價要小很多媒佣。在 Vue.js 中,Virtual DOM 是用 VNode 這么一個 Class 去描述陵刹,它是定義在 src/core/vdom/vnode.js 中的默伍。

/* @flow */

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
//省略代碼
  parent: VNode | void; // component placeholder node
  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}

export function cloneVNode (vnode: VNode): VNode {
  const cloned = new VNode( 
  )
  return cloned
}

代碼省略了很多 其實只要就可以看出這個VNode只是一個類
Vue.js 中 Virtual DOM 是借鑒了一個開源庫 snabbdom 的實現(xiàn),然后加入了一些 Vue.js 特色的東西
總結(jié)
其實 VNode 是對真實 DOM 的一種抽象描述授霸,它的核心定義無非就幾個關(guān)鍵屬性巡验,標(biāo)簽名、數(shù)據(jù)碘耳、子節(jié)點显设、鍵值等已旧,其它屬性都是都是用來擴展 VNode 的靈活性以及實現(xiàn)一些特殊 feature 的此再。由于 VNode 只是用來映射到真實 DOM 的渲染勤庐,不需要包含操作 DOM 的方法磕潮,因此它是非常輕量和簡單的责球。
Virtual DOM 除了它的數(shù)據(jù)結(jié)構(gòu)的定義捷泞,映射到真實的 DOM 實際上要經(jīng)歷 VNode 的 create秉氧、diff址儒、patch 等過程僻焚。那么在 Vue.js 中允悦,VNode 的 create 是通過之前提到的 createElement 方法創(chuàng)建的,我們接下來分析這部分的實現(xiàn)
4虑啤、createElemen的實現(xiàn)
Vue.js 利用 createElement 方法創(chuàng)建 VNode隙弛,它定義在 src/core/vdom/create-elemenet.js中:

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {  //對參數(shù)的處理  data沒有沒傳入的時候處理其他的參數(shù)的值
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

真正創(chuàng)建Vnode的函數(shù)在_createElement函數(shù)里面
_createElement 方法有 5 個參數(shù),context 表示 VNode 的上下文環(huán)境狞山,它是 Component 類型全闷;tag 表示標(biāo)簽,它可以是一個字符串萍启,也可以是一個 Component总珠;data 表示 VNode 的數(shù)據(jù)屏鳍,它是一個 VNodeData 類型,可以在 flow/vnode.js 中找到它的定義局服,這里先不展開說钓瞭;children 表示當(dāng)前 VNode 的子節(jié)點,它是任意類型的腌逢,它接下來需要被規(guī)范為標(biāo)準(zhǔn)的 VNode 數(shù)組降淮;normalizationType 表示子節(jié)點規(guī)范的類型,類型不同規(guī)范的方法也就不一樣搏讶,它主要是參考 render 函數(shù)是編譯生成的還是用戶手寫的佳鳖。

還是在上面的文件下 太多 不粘貼了
里面就是對參數(shù)以及屬性的判斷等 這里判斷render函數(shù)是用戶手寫的 還是編譯生成的 分別調(diào)用不同的函數(shù)

if (normalizationType === ALWAYS_NORMALIZE) {  //normalizationType 表示render函數(shù)的由來
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }

children 的規(guī)范化
由于 Virtual DOM 實際上是一個樹狀結(jié)構(gòu),每一個 VNode 可能會有若干個子節(jié)點媒惕,這些子節(jié)點應(yīng)該也是 VNode 的類型系吩。_createElement 接收的第 4 個參數(shù) children 是任意類型的,因此我們需要把它們規(guī)范成 VNode 類型妒蔚。
上面調(diào)用的兩個不同的函數(shù)在src/core/vdom/helpers/normalzie-children.js

export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}

simpleNormalizeChildren 方法調(diào)用場景是 render 函數(shù)當(dāng)函數(shù)是編譯生成的穿挨。理論上編譯生成的 children 都已經(jīng)是 VNode 類型的,但這里有一個例外肴盏,就是 functional component 函數(shù)式組件返回的是一個數(shù)組而不是一個根節(jié)點科盛,所以會通過 Array.prototype.concat 方法把整個 children 數(shù)組打平,讓它的深度只有一層菜皂。

normalizeChildren 方法的調(diào)用場景有 2 種贞绵,
①、一個場景是 render 函數(shù)是用戶手寫的恍飘,當(dāng) children 只有一個節(jié)點的時候榨崩,Vue.js 從接口層面允許用戶把 children 寫成基礎(chǔ)類型用來創(chuàng)建單個簡單的文本節(jié)點,這種情況會調(diào)用 createTextVNode 創(chuàng)建一個文本節(jié)點的 VNode章母;
②母蛛、另一個場景是當(dāng)編譯 slot、v-for 的時候會產(chǎn)生嵌套數(shù)組的情況乳怎,會調(diào)用 normalizeArrayChildren 方法彩郊,接下來看一下它的實現(xiàn):

export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

function isTextNode (node): boolean {
  return isDef(node) && isDef(node.text) && isFalse(node.isComment)
}

function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
  const res = []
  let i, c, lastIndex, last
  for (i = 0; i < children.length; i++) {
    c = children[i]
    if (isUndef(c) || typeof c === 'boolean') continue
    lastIndex = res.length - 1
    last = res[lastIndex]
    //  nested
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
        // merge adjacent text nodes
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        res.push.apply(res, c)
      }
    } else if (isPrimitive(c)) {
      if (isTextNode(last)) {
        // merge adjacent text nodes
        // this is necessary for SSR hydration because text nodes are
        // essentially merged when rendered to HTML strings
        res[lastIndex] = createTextVNode(last.text + c)
      } else if (c !== '') {
        // convert primitive to vnode
        res.push(createTextVNode(c))
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        // merge adjacent text nodes
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        // default key for nested array children (likely generated by v-for)
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__`
        }
        res.push(c)
      }
    }
  }
  return res
}

normalizeArrayChildren 接收 2 個參數(shù),children 表示要規(guī)范的子節(jié)點蚪缀,nestedIndex 表示嵌套的索引秫逝,因為單個 child 可能是一個數(shù)組類型。 normalizeArrayChildren 主要的邏輯就是遍歷 children椿胯,獲得單個節(jié)點 c,然后對 c 的類型判斷剃根,如果是一個數(shù)組類型哩盲,則遞歸調(diào)用 normalizeArrayChildren; 如果是基礎(chǔ)類型,則通過 createTextVNode 方法轉(zhuǎn)換成 VNode 類型;否則就已經(jīng)是 VNode 類型了廉油,如果 children 是一個列表并且列表還存在嵌套的情況惠险,則根據(jù) nestedIndex 去更新它的 key。這里需要注意一點抒线,在遍歷的過程中班巩,對這 3 種情況都做了如下處理:如果存在兩個連續(xù)的 text節(jié)點,會把它們合并成一個 text 節(jié)點嘶炭。
經(jīng)過對 children 的規(guī)范化抱慌,children 變成了一個類型為 VNode 的 Array。
接下來就是vnode的創(chuàng)建
回到 createElement 函數(shù)眨猎,規(guī)范化 children 后抑进,接下來會去創(chuàng)建一個 VNode 的實例:

let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }

這里先對 tag 做判斷,如果是 string 類型睡陪,則接著判斷如果是內(nèi)置的一些節(jié)點寺渗,則直接創(chuàng)建一個普通 VNode,如果是為已注冊的組件名兰迫,則通過 createComponent 創(chuàng)建一個組件類型的 VNode信殊,否則創(chuàng)建一個未知的標(biāo)簽的 VNode。 如果是 tag 一個 Component 類型汁果,則直接調(diào)用 createComponent 創(chuàng)建一個組件類型的 VNode 節(jié)點涡拘。對于 createComponent 創(chuàng)建組件類型的 VNode 的過程,我們之后會去介紹须鼎,本質(zhì)上它還是返回了一個 VNode鲸伴。
總結(jié)
那么至此,我們大致了解了 createElement 創(chuàng)建 VNode 的過程晋控,每個 VNode 有 children汞窗,children 每個元素也是一個 VNode,這樣就形成了一個 VNode Tree赡译,它很好的描述了我們的 DOM Tree仲吏。
回到 mountComponent 函數(shù)的過程,我們已經(jīng)知道 vm._render 是如何創(chuàng)建了一個 VNode蝌焚,接下來就是要把這個 VNode 渲染成一個真實的 DOM 并渲染出來裹唆,這個過程是通過 vm._update 完成的,接下來分析一下這個過程只洒。

5许帐、_update函數(shù)的實現(xiàn)
Vue 的 _update 是實例的一個私有方法,它被調(diào)用的時機有 2 個毕谴,一個是首次渲染成畦,一個是數(shù)據(jù)更新的時候距芬;由于我們這一章節(jié)只分析首次渲染部分,數(shù)據(jù)更新部分會在之后分析響應(yīng)式原理的時候涉及循帐。_update 方法的作用是把 VNode 渲染成真實的 DOM框仔,它的定義在 src/core/instance/lifecycle.js中:

export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

_update 的核心就是調(diào)用 vm.patch 方法,根據(jù)追溯拄养,這個方法實際上在不同的平臺离斩,比如 web 和 weex 上的定義是不一樣的,因此在 web 平臺中它的定義為patch函數(shù) 在服務(wù)器端不需要操作DOM 所以是空函數(shù)
src/platforms/web/runtime/index.js中:

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

patch方法定義在src/platforms/web/runtime/patch.js

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })

該方法的定義是調(diào)用 createPatchFunction 方法的返回值瘪匿,這里傳入了一個對象跛梗,包含 nodeOps 參數(shù)和 modules 參數(shù)。其中柿顶,nodeOps 封裝了一系列 DOM 操作的方法茄袖,modules 定義了一些模塊的鉤子函數(shù)的實現(xiàn),我們這里先不詳細介紹嘁锯,來看一下 createPatchFunction 的實現(xiàn)宪祥,它定義在 src/core/vdom/patch.js中:

不貼了 太長了

createPatchFunction 內(nèi)部定義了一系列的輔助方法,最終返回了一個 patch 方法家乘,這個方法就賦值給了 vm._update 函數(shù)里調(diào)用的 vm.patch蝗羊。
在介紹 patch 的方法實現(xiàn)之前,我們可以思考一下為何 Vue.js 源碼繞了這么一大圈仁锯,把相關(guān)代碼分散到各個目錄耀找。因為前面介紹過,patch 是平臺相關(guān)的业崖,在 Web 和 Weex 環(huán)境野芒,它們把虛擬 DOM 映射到 “平臺 DOM” 的方法是不同的,并且對 “DOM” 包括的屬性模塊創(chuàng)建和更新也不盡相同双炕。因此每個平臺都有各自的 nodeOps 和 modules狞悲,它們的代碼需要托管在 src/platforms 這個大目錄下。
而不同平臺的 patch 的主要邏輯部分是相同的妇斤,所以這部分公共的部分托管在 core 這個大目錄下摇锋。差異化部分只需要通過參數(shù)來區(qū)別,這里用到了一個函數(shù)柯里化的技巧站超,通過 createPatchFunction 把差異化參數(shù)提前固化荸恕,這樣不用每次調(diào)用 patch 的時候都傳遞 nodeOps 和 modules 了,這種編程技巧也非常值得學(xué)習(xí)死相。
在這里融求,nodeOps 表示對 “平臺 DOM” 的一些操作方法,modules 表示平臺的一些模塊算撮,它們會在整個 patch 過程的不同階段執(zhí)行相應(yīng)的鉤子函數(shù)生宛。這些代碼的具體實現(xiàn)會在之后的章節(jié)介紹施掏。
回到 patch 方法本身,它接收 4個參數(shù)茅糜,oldVnode 表示舊的 VNode 節(jié)點,它也可以不存在或者是一個 DOM 對象素挽;vnode 表示執(zhí)行 _render 后返回的 VNode 的節(jié)點蔑赘;hydrating 表示是否是服務(wù)端渲染;removeOnly 是給 transition-group 用的预明,之后會介紹缩赛。
patch 的邏輯看上去相對復(fù)雜,因為它有著非常多的分支邏輯撰糠,為了方便理解酥馍,我們并不會在這里介紹所有的邏輯,僅會針對我們之前的例子分析它的執(zhí)行邏輯阅酪。之后我們對其它場景做源碼分析的時候會再次回顧 patch 方法旨袒。

看一下簡單的例子

//這是HTML下的
<div id="app">
</div>

var app = new Vue({
  el: '#app',
  render: function (createElement) {
    return createElement('div', {
      attrs: {
        id: 'app'
      },
    }, this.message)
  },
  data: {
    message: 'Hello Vue!'
  }
})

然后我們在 vm._update 的方法里是這么調(diào)用 patch 方法的:

// initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

結(jié)合我們的例子,我們的場景是首次渲染术辐,所以在執(zhí)行 patch 函數(shù)的時候砚尽,傳入的 vm.el 對應(yīng)的是例子中 id 為 app 的 DOM 對象,這個也就是我們在 index.html 模板中寫的 <div id="app">辉词, vm.el 的賦值是在之前 mountComponent 函數(shù)做的必孤,vnode 對應(yīng)的是調(diào)用 render 函數(shù)的返回值,hydrating 在非服務(wù)端渲染情況下為 false瑞躺,removeOnly 為 false敷搪。
確定了這些入?yún)⒑螅覀兓氐?patch 函數(shù)的執(zhí)行過程幢哨,看幾個關(guān)鍵步驟赡勘。

let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
//可能括號啥的不對 代碼省略了一些 沒有對應(yīng)括號

由于我們傳入的 oldVnode 實際上是一個 DOM container,所以 isRealElement 為 true嘱么,接下來又通過 emptyNodeAt 方法把 oldVnode 轉(zhuǎn)換成 VNode 對象狮含,然后再調(diào)用 createElm 方法,這個方法在這里非常重要曼振,來看一下它的實現(xiàn):

function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    vnode.isRootInsert = !nested // for transition enter check
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
      if (process.env.NODE_ENV !== 'production') {
        if (data && data.pre) {
          creatingElmInVPre++
        }
        if (isUnknownElement(vnode, creatingElmInVPre)) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          )
        }
      }

      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)

      /* istanbul ignore if */
      if (__WEEX__) {
        // in Weex, the default insertion order is parent-first.
        // List items can be optimized to use children-first insertion
        // with append="tree".
        const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
        if (!appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
        createChildren(vnode, children, insertedVnodeQueue)
        if (appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
      } else {
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        creatingElmInVPre--
      }
    } else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else {
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
  }

createElm 的作用是通過虛擬節(jié)點創(chuàng)建真實的 DOM 并插入到它的父節(jié)點中几迄。 我們來看一下它的一些關(guān)鍵邏輯,createComponent 方法目的是嘗試創(chuàng)建子組件冰评,這個邏輯在之后組件的章節(jié)會詳細介紹映胁,在當(dāng)前這個 case 下它的返回值為 false;接下來判斷 vnode 是否包含 tag甲雅,如果包含解孙,先簡單對 tag 的合法性在非生產(chǎn)環(huán)境下做校驗坑填,看是否是一個合法標(biāo)簽;然后再去調(diào)用平臺 DOM 的操作去創(chuàng)建一個占位符元素弛姜。接下來調(diào)用 createChildren 方法去創(chuàng)建子元素

  function createChildren (vnode, children, insertedVnodeQueue) {
    if (Array.isArray(children)) {
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(children)
      }
      for (let i = 0; i < children.length; ++i) {
        createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
      }
    } else if (isPrimitive(vnode.text)) {
      nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
    }
  }

createChildren 的邏輯很簡單脐瑰,實際上是遍歷子虛擬節(jié)點,遞歸調(diào)用 createElm廷臼,這是一種常用的深度優(yōu)先的遍歷算法苍在,這里要注意的一點是在遍歷過程中會把 vnode.elm 作為父容器的 DOM 節(jié)點占位符傳入。
接著再調(diào)用 invokeCreateHooks 方法執(zhí)行所有的 create 的鉤子并把 vnode push 到 insertedVnodeQueue 中荠商。
這個函數(shù)之后再說

最后調(diào)用 insert 方法把 DOM 插入到父節(jié)點中寂恬,因為是遞歸調(diào)用,子元素會優(yōu)先調(diào)用 insert莱没,所以整個 vnode 樹節(jié)點的插入順序是先子后父初肉。來看一下 insert方法,它的定義在src/core/vdom/patch.js 上饰躲。

  function insert (parent, elm, ref) {
    if (isDef(parent)) {
      if (isDef(ref)) {
        if (nodeOps.parentNode(ref) === parent) {
          nodeOps.insertBefore(parent, elm, ref)
        }
      } else {
        nodeOps.appendChild(parent, elm)
      }
    }
  }

insert 邏輯很簡單牙咏,調(diào)用一些 nodeOps 把子節(jié)點插入到父節(jié)點中,這些輔助方法定義在 src/platforms/web/runtime/node-ops.js中:

export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}

export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}

其實就是調(diào)用原生 DOM 的 API 進行 DOM 操作嘹裂,看到這里眠寿,恍然大悟,原來 Vue 是這樣動態(tài)創(chuàng)建的 DOM焦蘑。
在 createElm 過程中盯拱,如果 vnode 節(jié)點如果不包含 tag,則它有可能是一個注釋或者純文本節(jié)點例嘱,可以直接插入到父元素中狡逢。在我們這個例子中,最內(nèi)層就是一個文本 vnode拼卵,它的 text 值取的就是之前的 this.message 的值 Hello Vue!奢浑。
再回到 patch 方法,首次渲染我們調(diào)用了 createElm 方法腋腮,這里傳入的 parentElm 是 oldVnode.elm 的父元素雀彼, 在我們的例子是 id 為 #app div 的父元素,也就是 Body即寡;實際上整個過程就是遞歸創(chuàng)建了一個完整的 DOM 樹并插入到 Body 上徊哑。
最后,我們根據(jù)之前遞歸 createElm 生成的 vnode 插入順序隊列聪富,執(zhí)行相關(guān)的 insert 鉤子函數(shù)莺丑,這部分內(nèi)容我們之后會詳細介紹。

總結(jié)
那么至此我們從主線上把模板和數(shù)據(jù)如何渲染成最終的 DOM 的過程分析完畢了,我們可以通過下圖更直觀地看到從初始化 Vue 到最終渲染的整個過程梢莽。


image.png

我們這里只是分析了最簡單和最基礎(chǔ)的場景萧豆,在實際項目中,我們是把頁面拆成很多組件的昏名,Vue 另一個核心思想就是組件化涮雷。那么下一章我們就來分析 Vue 的組件化過程。

這一章我使用一個簡單的 例子通過打斷點的方式理解了一下這個過程
首先我們通過平時在vue函數(shù)里面打斷點的方式可以知道vue執(zhí)行的源碼在node_module/vue/dist/vue.esm.js(當(dāng)然這個也是可以通過分析webpack打包的方式來獲得這個源碼地址)在里面打斷點的方式來追蹤執(zhí)行過程
總得是通過$mount函數(shù)轻局,來掛載大致總結(jié)出來如下幾個步驟
1份殿、通過一系列判斷,不論是el template 還是render 最終都會轉(zhuǎn)換成一個render函數(shù)(從代碼看出優(yōu)先級:render>template>el)本文例子中自動生成的render函數(shù)大致如下所示:

(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('h5',[_v("么么噠")]),_v("\n        "+_s(mes)+"\n    ")])}
})

vue例子

<body>
    <div id="app">
        <h5>么么噠</h5>
        {{mes}}
    </div>
    <!-- built files will be auto injected -->
</body>

//script部分   例子中main.js部分
new Vue({
  el: '#app',
  data: {
    mes: '加油'
  }
})

2嗽交、在_render函數(shù)里面調(diào)用render生成的函數(shù)(用戶手寫render函數(shù)操作不同的方法,但是也是經(jīng)過這個步驟)$createElement經(jīng)過一系列的操作將轉(zhuǎn)換成vnode返回
3颂斜、調(diào)用_update函數(shù)后調(diào)用patch函數(shù)進行更新

使用遞歸遍歷這個Vnode從子節(jié)點到父節(jié)點進行(使用原生創(chuàng)建節(jié)點的api)創(chuàng)建真實節(jié)點(文字節(jié)點也是一個節(jié)點)最后將創(chuàng)建的這個節(jié)點插入帶body上 夫壁。 這時可以通過斷點方式查看當(dāng)前文檔會發(fā)現(xiàn)有兩個節(jié)點 (原來的初始節(jié)點和新插入的節(jié)點)
image.png

4、刪除原來的節(jié)點即可

至此渲染函數(shù)$mount函數(shù)執(zhí)行完畢 頁面的上內(nèi)容也OK

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末沃疮,一起剝皮案震驚了整個濱河市盒让,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌司蔬,老刑警劉巖邑茄,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異俊啼,居然都是意外死亡肺缕,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門授帕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來同木,“玉大人,你說我怎么就攤上這事跛十⊥罚” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵芥映,是天一觀的道長洲尊。 經(jīng)常有香客問我,道長奈偏,這世上最難降的妖魔是什么坞嘀? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮惊来,結(jié)果婚禮上姆吭,老公的妹妹穿的比我還像新娘。我一直安慰自己唁盏,他們只是感情好内狸,可當(dāng)我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布检眯。 她就那樣靜靜地躺著,像睡著了一般昆淡。 火紅的嫁衣襯著肌膚如雪锰瘸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天昂灵,我揣著相機與錄音避凝,去河邊找鬼。 笑死眨补,一個胖子當(dāng)著我的面吹牛管削,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播撑螺,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼含思,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了甘晤?” 一聲冷哼從身側(cè)響起含潘,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎线婚,沒想到半個月后遏弱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡塞弊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年漱逸,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片游沿。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡虹脯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出奏候,到底是詐尸還是另有隱情循集,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布蔗草,位于F島的核電站咒彤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏咒精。R本人自食惡果不足惜镶柱,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望模叙。 院中可真熱鬧歇拆,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽输吏。三九已至权旷,卻和暖如春拄氯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背它浅。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工译柏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人姐霍。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓鄙麦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親邮弹。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,465評論 2 348

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