<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入了解 Vue3 模板編譯原理

          共 365字,需瀏覽 1分鐘

           ·

          2020-11-29 20:17

          Vue 的編譯模塊包含 4 個目錄:

          compiler-corecompiler-dom // 瀏覽器compiler-sfc // 單文件組件compiler-ssr // 服務(wù)端渲染

          其中 compiler-core 模塊是 Vue 編譯的核心模塊,并且是平臺無關(guān)的。而剩下的三個都是在 compiler-core 的基礎(chǔ)上針對不同的平臺作了適配處理。

          Vue 的編譯分為三個階段,分別是:parse、transform、codegen。

          其中 parse 階段將模板字符串轉(zhuǎn)化為語法抽象樹 AST。transform 階段則是對 AST 進(jìn)行了一些轉(zhuǎn)換處理。codegen 階段根據(jù) AST 生成對應(yīng)的 render 函數(shù)字符串。

          Parse

          Vue 在解析模板字符串時,可分為兩種情況:以?<?開頭的字符串和不以?<?開頭的字符串。

          不以?<?開頭的字符串有兩種情況:它是文本節(jié)點(diǎn)或?{{ exp }}?插值表達(dá)式。

          而以?<?開頭的字符串又分為以下幾種情況:

          1.元素開始標(biāo)簽?

          2.元素結(jié)束標(biāo)簽?
          3.注釋節(jié)點(diǎn)?4.文檔聲明?

          用偽代碼表示,大概過程如下:

          while (s.length) {    if (startsWith(s, '{{')) {        // 如果以 '{{' 開頭        node = parseInterpolation(context, mode)    } else if (s[0] === '<') {        // 以 < 標(biāo)簽開頭        if (s[1] === '!') {            if (startsWith(s, '

          AST 節(jié)點(diǎn)

          所有的 AST 節(jié)點(diǎn)定義都在 compiler-core/ast.ts 文件中,下面是一個元素節(jié)點(diǎn)的定義:

          export interface BaseElementNode extends Node {  type: NodeTypes.ELEMENT // 類型  ns: Namespace // 命名空間 默認(rèn)為 HTML,即 0  tag: string // 標(biāo)簽名  tagType: ElementTypes // 元素類型  isSelfClosing: boolean // 是否是自閉合標(biāo)簽 例如 

          props: Array // props 屬性,包含 HTML 屬性和指令 children: TemplateChildNode[] // 字節(jié)點(diǎn)}

          一些簡單的要點(diǎn)已經(jīng)講完了,下面我們再從一個比較復(fù)雜的例子來詳細(xì)講解一下 parse 的處理過程。

          {{ test }}

          一個文本節(jié)點(diǎn)
          good job!

          上面的模板字符串假設(shè)為 s,第一個字符 s[0] 是?<?開頭,那說明它只能是剛才所說的四種情況之一。這時需要再看一下 s[1] 的字符是什么:

          1.如果是?!,則調(diào)用字符串原生方法?startsWith()?看看是以?'

          {{ test }}

          一個文本節(jié)點(diǎn)
          good job!

          注釋文本和普通文本節(jié)點(diǎn)解析規(guī)則都很簡單,直接截斷,生成節(jié)點(diǎn)。注釋文本調(diào)用?parseComment()?函數(shù)處理,文本節(jié)點(diǎn)調(diào)用?parseText()?處理。

          雙花插值的字符串處理邏輯稍微復(fù)雜點(diǎn),例如示例中的?{{ test }}

          1.先將雙花括號中的內(nèi)容提取出來,即?test,再對它執(zhí)行?trim(),去除空格。2.然后會生成兩個節(jié)點(diǎn),一個節(jié)點(diǎn)是?INTERPOLATION,type 為 5,表示它是雙花插值。3.第二個節(jié)點(diǎn)是它的內(nèi)容,即?test,它會生成一個?SIMPLE_EXPRESSION?節(jié)點(diǎn),type 為 4。

          return {  type: NodeTypes.INTERPOLATION, // 雙花插值類型  content: {    type: NodeTypes.SIMPLE_EXPRESSION,    isStatic: false, // 非靜態(tài)節(jié)點(diǎn)    isConstant: false,    content,    loc: getSelection(context, innerStart, innerEnd)  },  loc: getSelection(context, start)}

          剩下的字符串解析邏輯和上文的差不多,就不解釋了,最后這個示例解析出來的 AST 如下所示:

          3fbb17a5d7e82491b00c3751ac26087a.webp

          從 AST 上,我們還能看到某些節(jié)點(diǎn)上有一些別的屬性:

          1.ns,命名空間,一般為 HTML,值為 0。2.loc,它是一個位置信息,表明這個節(jié)點(diǎn)在源 HTML 字符串中的位置,包含行,列,偏移量等信息。3.{{ test }}?解析出來的節(jié)點(diǎn)會有一個 isStatic 屬性,值為 false,表示這是一個動態(tài)節(jié)點(diǎn)。如果是靜態(tài)節(jié)點(diǎn),則只會生成一次,并且在后面的階段一直復(fù)用同一個,不用進(jìn)行 diff 比較。

          另外還有一個 tagType 屬性,它有 4 個值:

          export const enum ElementTypes {  ELEMENT, // 0 元素節(jié)點(diǎn)  COMPONENT, // 1 組件  SLOT, // 2 插槽  TEMPLATE // 3 模板}

          主要用于區(qū)分上述四種類型節(jié)點(diǎn)。

          Transform

          在 transform 階段,Vue 會對 AST 進(jìn)行一些轉(zhuǎn)換操作,主要是根據(jù)不同的 AST 節(jié)點(diǎn)添加不同的選項(xiàng)參數(shù),這些參數(shù)在 codegen 階段會用到。下面列舉一些比較重要的選項(xiàng):

          cacheHandlers

          如果 cacheHandlers 的值為 true,則表示開啟事件函數(shù)緩存。例如?@click="foo"?默認(rèn)編譯為?{ onClick: foo },如果開啟了這個選項(xiàng),則編譯為

          { onClick: _cache[0] || (_cache[0] = e => _ctx.foo(e)) }

          hoistStatic

          hoistStatic 是一個標(biāo)識符,表示要不要開啟靜態(tài)節(jié)點(diǎn)提升。如果值為 true,靜態(tài)節(jié)點(diǎn)將被提升到?render()?函數(shù)外面生成,并被命名為?_hoisted_x?變量。

          例如?一個文本節(jié)點(diǎn)?生成的代碼為?const _hoisted_2 = /*#__PURE__*/_createTextVNode(" 一個文本節(jié)點(diǎn) ")

          下面兩張圖,前者是?hoistStatic = false,后面是?hoistStatic = true。大家可以在網(wǎng)站[1]上自己試一下。

          fada6b933927af9f361efd5ec3ceea43.webp

          fe4e6df023e1947fd734d6af7a5ed800.webp

          prefixIdentifiers

          這個參數(shù)的作用是用于代碼生成。例如?{{ foo }}?在 module 模式下生成的代碼為?_ctx.foo,而在 function 模式下是?with (this) { ... }。因?yàn)樵?module 模式下,默認(rèn)為嚴(yán)格模式,不能使用 with 語句。

          PatchFlags

          transform 在對 AST 節(jié)點(diǎn)進(jìn)行轉(zhuǎn)換時,會打上 patchflag 參數(shù),這個參數(shù)主要用于 diff 比較過程。當(dāng) DOM 節(jié)點(diǎn)有這個標(biāo)志并且大于 0,就代表要更新,沒有就跳過。

          我們來看一下 patchflag 的取值范圍:

          export const enum PatchFlags {  // 動態(tài)文本節(jié)點(diǎn)  TEXT = 1,
          // 動態(tài) class CLASS = 1 << 1, // 2
          // 動態(tài) style STYLE = 1 << 2, // 4
          // 動態(tài)屬性,但不包含類名和樣式 // 如果是組件,則可以包含類名和樣式 PROPS = 1 << 3, // 8
          // 具有動態(tài) key 屬性,當(dāng) key 改變時,需要進(jìn)行完整的 diff 比較。 FULL_PROPS = 1 << 4, // 16
          // 帶有監(jiān)聽事件的節(jié)點(diǎn) HYDRATE_EVENTS = 1 << 5, // 32
          // 一個不會改變子節(jié)點(diǎn)順序的 fragment STABLE_FRAGMENT = 1 << 6, // 64
          // 帶有 key 屬性的 fragment 或部分子字節(jié)有 key KEYED_FRAGMENT = 1 << 7, // 128
          // 子節(jié)點(diǎn)沒有 key 的 fragment UNKEYED_FRAGMENT = 1 << 8, // 256
          // 一個節(jié)點(diǎn)只會進(jìn)行非 props 比較 NEED_PATCH = 1 << 9, // 512
          // 動態(tài) slot DYNAMIC_SLOTS = 1 << 10, // 1024
          // 靜態(tài)節(jié)點(diǎn) HOISTED = -1,
          // 指示在 diff 過程應(yīng)該要退出優(yōu)化模式 BAIL = -2}

          從上述代碼可以看出 patchflag 使用一個 11 位的位圖來表示不同的值,每個值都有不同的含義。Vue 在 diff 過程會根據(jù)不同的 patchflag 使用不同的 patch 方法。

          下圖是經(jīng)過 transform 后的 AST:

          5cc89f0e14d4810be33231433944eef8.webp

          可以看到 codegenNode、helpers 和 hoists 已經(jīng)被填充上了相應(yīng)的值。codegenNode 是生成代碼要用到的數(shù)據(jù),hoists 存儲的是靜態(tài)節(jié)點(diǎn),helpers 存儲的是創(chuàng)建 VNode 的函數(shù)名稱(其實(shí)是 Symbol)。

          在正式開始 transform 前,需要創(chuàng)建一個 transformContext,即 transform 上下文。和這三個屬性有關(guān)的數(shù)據(jù)和方法如下:

          helpers: new Set(),hoists: [],
          // methodshelper(name) { context.helpers.add(name) return name},helperString(name) { return `_${helperNameMap[context.helper(name)]}`},hoist(exp) { context.hoists.push(exp) const identifier = createSimpleExpression( `_hoisted_${context.hoists.length}`, false, exp.loc, true ) identifier.hoisted = exp return identifier},

          我們來看一下具體的 transform 過程是怎樣的,用?

          {{ test }}

          ?來做示例。

          這個節(jié)點(diǎn)對應(yīng)的是?transformElement()?轉(zhuǎn)換函數(shù),由于?p?沒有綁定動態(tài)屬性,沒有綁定指令,所以重點(diǎn)不在它,而是在?{{ test }}?上。{{ test }}?是一個雙花插值表達(dá)式,所以將它的 patchFlag 設(shè)為 1(動態(tài)文本節(jié)點(diǎn)),對應(yīng)的執(zhí)行代碼是?patchFlag |= 1。然后再執(zhí)行?createVNodeCall()?函數(shù),它的返回值就是這個節(jié)點(diǎn)的 codegenNode 值。

          node.codegenNode = createVNodeCall(    context,    vnodeTag,    vnodeProps,    vnodeChildren,    vnodePatchFlag,    vnodeDynamicProps,    vnodeDirectives,    !!shouldUseBlock,    false /* disableTracking */,    node.loc)

          createVNodeCall()?根據(jù)這個節(jié)點(diǎn)添加了一個?createVNode?Symbol 符號,它放在 helpers 里。其實(shí)就是要在代碼生成階段引入的幫助函數(shù)。

          // createVNodeCall() 內(nèi)部執(zhí)行過程,已刪除多余的代碼context.helper(CREATE_VNODE)
          return { type: NodeTypes.VNODE_CALL, tag, props, children, patchFlag, dynamicProps, directives, isBlock, disableTracking, loc}

          hoists

          一個節(jié)點(diǎn)是否添加到 hoists 中,主要看它是不是靜態(tài)節(jié)點(diǎn),并且需要將 hoistStatic 設(shè)為 true。

          // 屬性靜態(tài)節(jié)點(diǎn)

          {{ test }}

          一個文本節(jié)點(diǎn) // 靜態(tài)節(jié)點(diǎn)
          good job!
          // 靜態(tài)節(jié)點(diǎn)

          可以看到,上面有三個靜態(tài)節(jié)點(diǎn),所以 hoists 數(shù)組有 3 個值。并且無論靜態(tài)節(jié)點(diǎn)嵌套有多深,都會被提升到 hoists 中。

          type 變化

          74a23dfe826ba0cdafe90eb1c0ee5240.webp

          從上圖可以看到,最外層的 div 的 type 原來為 1,經(jīng)過 transform 生成的 codegenNode 中的 type 變成了 13。這個 13 是代碼生成對應(yīng)的類型?VNODE_CALL。另外還有:

          // codegenVNODE_CALL, // 13JS_CALL_EXPRESSION, // 14JS_OBJECT_EXPRESSION, // 15JS_PROPERTY, // 16JS_ARRAY_EXPRESSION, // 17JS_FUNCTION_EXPRESSION, // 18JS_CONDITIONAL_EXPRESSION, // 19JS_CACHE_EXPRESSION, // 20

          剛才提到的例子?{{ test }},它的 codegenNode 就是通過調(diào)用?createVNodeCall()?生成的:

          return {  type: NodeTypes.VNODE_CALL,  tag,  props,  children,  patchFlag,  dynamicProps,  directives,  isBlock,  disableTracking,  loc}

          可以從上述代碼看到,type 被設(shè)置為 NodeTypes.VNODE_CALL,即 13。

          每個不同的節(jié)點(diǎn)都由不同的 transform 函數(shù)來處理,由于篇幅有限,具體代碼請自行查閱。

          Codegen

          代碼生成階段最后生成了一個字符串,我們把字符串的雙引號去掉,看一下具體的內(nèi)容是什么:

          const _Vue = Vueconst { createVNode: _createVNode, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode } = _Vue
          const _hoisted_1 = { name: "test" }const _hoisted_2 = /*#__PURE__*/_createTextVNode(" 一個文本節(jié)點(diǎn) ")const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "good job!", -1 /* HOISTED */)
          return function render(_ctx, _cache) { with (_ctx) { const { createCommentVNode: _createCommentVNode, toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
          return (_openBlock(), _createBlock("div", _hoisted_1, [ _createCommentVNode(" 這是注釋 "), _createVNode("p", null, _toDisplayString(test), 1 /* TEXT */), _hoisted_2, _hoisted_3 ])) }}

          代碼生成模式

          可以看到上述代碼最后返回一個?render()?函數(shù),作用是生成對應(yīng)的 VNode。

          其實(shí)代碼生成有兩種模式:module 和 function,由標(biāo)識符 prefixIdentifiers 決定使用哪種模式。

          function 模式的特點(diǎn)是:使用?const { helpers... } = Vue?的方式來引入幫助函數(shù),也就是是?createVode()?createCommentVNode()?這些函數(shù)。向外導(dǎo)出使用?return?返回整個?render()?函數(shù)。

          module 模式的特點(diǎn)是:使用 es6 模塊來導(dǎo)入導(dǎo)出函數(shù),也就是使用 import 和 export。

          靜態(tài)節(jié)點(diǎn)

          另外還有三個變量是用?_hoisted_?命名的,后面跟著數(shù)字,代表這是第幾個靜態(tài)變量。再看一下 parse 階段的 HTML 模板字符串:

          {{ test }}

          一個文本節(jié)點(diǎn)
          good job!

          這個示例只有一個動態(tài)節(jié)點(diǎn),即?{{ test }},剩下的全是靜態(tài)節(jié)點(diǎn)。從生成的代碼中也可以看出,生成的節(jié)點(diǎn)和模板中的代碼是一一對應(yīng)的。靜態(tài)節(jié)點(diǎn)的作用就是只生成一次,以后直接復(fù)用。

          細(xì)心的網(wǎng)友可能發(fā)現(xiàn)了?_hoisted_2?和?_hoisted_3?變量中都有一個?/*#__PURE__*/?注釋。

          這個注釋的作用是表示這個函數(shù)是純函數(shù),沒有副作用,主要用于 tree-shaking。壓縮工具在打包時會將未被使用的代碼直接刪除(shaking 搖掉)。

          再來看一下生成動態(tài)節(jié)點(diǎn)?{{ test }}?的代碼:?_createVNode("p", null, _toDisplayString(test), 1 /* TEXT */)

          其中?_toDisplayString(test)?的內(nèi)部實(shí)現(xiàn)是:

          return val == null    ? ''    : isObject(val)      ? JSON.stringify(val, replacer, 2)      : String(val)

          代碼很簡單,就是轉(zhuǎn)成字符串輸出。

          而?_createVNode("p", null, _toDisplayString(test), 1 /* TEXT */)?最后一個參數(shù) 1 就是 transform 添加的 patchflag 了。

          幫助函數(shù) helpers

          在 transform、codegen 這兩個階段,我們都能看到 helpers 的影子,到底 helpers 是干什么用的?

          // Name mapping for runtime helpers that need to be imported from 'vue' in// generated code. Make sure these are correctly exported in the runtime!// Using `any` here because TS doesn't allow symbols as index type.export const helperNameMap: any = {  [FRAGMENT]: `Fragment`,  [TELEPORT]: `Teleport`,  [SUSPENSE]: `Suspense`,  [KEEP_ALIVE]: `KeepAlive`,  [BASE_TRANSITION]: `BaseTransition`,  [OPEN_BLOCK]: `openBlock`,  [CREATE_BLOCK]: `createBlock`,  [CREATE_VNODE]: `createVNode`,  [CREATE_COMMENT]: `createCommentVNode`,  [CREATE_TEXT]: `createTextVNode`,  [CREATE_STATIC]: `createStaticVNode`,  [RESOLVE_COMPONENT]: `resolveComponent`,  [RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,  [RESOLVE_DIRECTIVE]: `resolveDirective`,  [WITH_DIRECTIVES]: `withDirectives`,  [RENDER_LIST]: `renderList`,  [RENDER_SLOT]: `renderSlot`,  [CREATE_SLOTS]: `createSlots`,  [TO_DISPLAY_STRING]: `toDisplayString`,  [MERGE_PROPS]: `mergeProps`,  [TO_HANDLERS]: `toHandlers`,  [CAMELIZE]: `camelize`,  [CAPITALIZE]: `capitalize`,  [SET_BLOCK_TRACKING]: `setBlockTracking`,  [PUSH_SCOPE_ID]: `pushScopeId`,  [POP_SCOPE_ID]: `popScopeId`,  [WITH_SCOPE_ID]: `withScopeId`,  [WITH_CTX]: `withCtx`}
          export function registerRuntimeHelpers(helpers: any) { Object.getOwnPropertySymbols(helpers).forEach(s => { helperNameMap[s] = helpers[s] })}

          其實(shí)幫助函數(shù)就是在代碼生成時從 Vue 引入的一些函數(shù),以便讓程序正常執(zhí)行,從上面生成的代碼中就可以看出來。而 helperNameMap 是默認(rèn)的映射表名稱,這些名稱就是要從 Vue 引入的函數(shù)名稱。

          另外,我們還能看到一個注冊函數(shù)?registerRuntimeHelpers(helpers: any(),它是干什么用的呢?

          我們知道編譯模塊 compiler-core 是平臺無關(guān)的,而 compiler-dom 是瀏覽器相關(guān)的編譯模塊。為了能在瀏覽器正常運(yùn)行 Vue 程序,就得把瀏覽器相關(guān)的 Vue 數(shù)據(jù)和函數(shù)導(dǎo)入進(jìn)來。?registerRuntimeHelpers(helpers: any()?正是用來做這件事的,從 compiler-dom 的 runtimeHelpers.ts 文件就能看出來:

          registerRuntimeHelpers({  [V_MODEL_RADIO]: `vModelRadio`,  [V_MODEL_CHECKBOX]: `vModelCheckbox`,  [V_MODEL_TEXT]: `vModelText`,  [V_MODEL_SELECT]: `vModelSelect`,  [V_MODEL_DYNAMIC]: `vModelDynamic`,  [V_ON_WITH_MODIFIERS]: `withModifiers`,  [V_ON_WITH_KEYS]: `withKeys`,  [V_SHOW]: `vShow`,  [TRANSITION]: `Transition`,  [TRANSITION_GROUP]: `TransitionGroup`})

          它運(yùn)行?registerRuntimeHelpers(helpers: any(),往映射表注入了瀏覽器相關(guān)的部分函數(shù)。

          helpers 是怎么使用的呢?

          在 parse 階段,解析到不同節(jié)點(diǎn)時會生成對應(yīng)的 type。

          在 transform 階段,會生成一個 helpers,它是一個 set 數(shù)據(jù)結(jié)構(gòu)。每當(dāng)它轉(zhuǎn)換 AST 時,都會根據(jù) AST 節(jié)點(diǎn)的 type 添加不同的 helper 函數(shù)。

          例如,假設(shè)它現(xiàn)在正在轉(zhuǎn)換的是一個注釋節(jié)點(diǎn),它會執(zhí)行?context.helper(CREATE_COMMENT),內(nèi)部實(shí)現(xiàn)相當(dāng)于?helpers.add('createCommentVNode')。然后在 codegen 階段,遍歷 helpers,將程序需要的函數(shù)從 Vue 里導(dǎo)入,代碼實(shí)現(xiàn)如下:

          // 這是 module 模式`import { ${ast.helpers  .map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)  .join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`

          如何生成代碼?

          從 codegen.ts 文件中,可以看到很多代碼生成函數(shù):

          generate() // 代碼生成入口文件genFunctionExpression() // 生成函數(shù)表達(dá)式genNode() // 生成 Vnode 節(jié)點(diǎn)...

          生成代碼則是根據(jù)不同的 AST 節(jié)點(diǎn)調(diào)用不同的代碼生成函數(shù),最終將代碼字符串拼在一起,輸出一個完整的代碼字符串。

          老規(guī)矩,還是看一個例子:

          const _hoisted_1 = { name: "test" }const _hoisted_2 = /*#__PURE__*/_createTextVNode(" 一個文本節(jié)點(diǎn) ")const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "good job!", -1 /* HOISTED */)

          看一下這段代碼是怎么生成的,首先執(zhí)行?genHoists(ast.hoists, context),將 transform 生成的靜態(tài)節(jié)點(diǎn)數(shù)組 hoists 作為第一個參數(shù)。genHoists()?內(nèi)部實(shí)現(xiàn):

          hoists.forEach((exp, i) => {    if (exp) {        push(`const _hoisted_${i + 1} = `);        genNode(exp, context);        newline();    }})

          從上述代碼可以看到,遍歷 hoists 數(shù)組,調(diào)用?genNode(exp, context)genNode()?根據(jù)不同的 type 執(zhí)行不同的函數(shù)。

          const _hoisted_1 = { name: "test" }

          這一行代碼中的?const _hoisted_1 =?由?genHoists()?生成,{ name: "test" }?由?genObjectExpression()?生成。同理,剩下的兩行代碼生成過程也是如此,只是最終調(diào)用的函數(shù)不同。

          References

          [1]?網(wǎng)站:?https://vue-next-template-explorer.netlify.app/#%7B%22src%22%3A%22%5Cr%5Cn%22%2C%22options%22%3A%7B%22mode%22%3A%22module%22%2C%22prefixIdentifiers%22%3Afalse%2C%22optimizeImports%22%3Afalse%2C%22hoistStatic%22%3Afalse%2C%22cacheHandlers%22%3Afalse%2C%22scopeId%22%3Anull%2C%22ssrCssVars%22%3A%22%7B%20color%20%7D%22%2C%22bindingMetadata%22%3A%7B%22TestComponent%22%3A%22setup%22%2C%22foo%22%3A%22setup%22%2C%22bar%22%3A%22props%22%7D%7D%7D


          瀏覽 42
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  AA级亚洲电影 | 黄色美女操逼视频 | 麻豆久久成人 | 裸体美女黄网 | 伊人婷婷五月天 |