v-model原理的深入解析(超詳細(xì))

拋出問題

我們先來看一下下面這段代碼

<template>
  <div>
    <div class="message">{{ info.message }}</div>
    <div><input v-model="info.message" type="text"></div>
    <button @click="change">click</button>
  </div>
</template>

<script>
  export default {
    data () {
      return {
        info: {}
      }
    },
    methods: {
      change () {
        this.info.message = 'hello world'
      }
    }
  }
</script>

上述代碼很簡單,就不做過多的解釋了。如果這段代碼都看不懂,那下面也沒必要再看下去了

問題重現(xiàn)步驟

我現(xiàn)在對上述代碼做兩種操作:

  1. 一進(jìn)頁面先在輸入框中輸入hello vue
  2. 一進(jìn)頁面先點擊click按鈕進(jìn)行賦值操作,再在輸入框中輸入hello vue

上述兩種情況分別會出現(xiàn)什么現(xiàn)象呢?

第一種操作,當(dāng)我們在輸入框中輸入hello vue的時候,class為message的div中會聯(lián)動出現(xiàn)hello vue,也就是說info中的message屬性是響應(yīng)式的

第二種操作,當(dāng)我們先進(jìn)行賦值操作,之后無論在輸入框中輸入什么內(nèi)容,class為message的div中都不會聯(lián)動出現(xiàn)任何值,也就是說info中的message屬性非響應(yīng)式的

問題引發(fā)的猜想

查閱vue官方文檔我們得知vue在初始化的時候會對data中所有已經(jīng)定義的對象及其子屬性進(jìn)行遍歷,給他們添加gettersetter,使得他們變成響應(yīng)式的(關(guān)于響應(yīng)式這塊之后會單開文章進(jìn)行解析),但是vue不能檢測對象屬性的添加或刪除。但是,可以使用 Vue.set(object, propertyName, value)方法向嵌套對象添加響應(yīng)式屬性

基于上述描述,我們先看第一種操作。直接在輸入框中輸入hello vue,class為message的div中會聯(lián)動出現(xiàn)hello vue。但是我們看data中只定義了info對象,其中并沒有定義message屬性,message屬于新增屬性。根據(jù)vue官方文檔中說的,vue不能檢測對象屬性的添加或刪除,所以我猜測vue底層在解析v-model指令的時候,每當(dāng)觸發(fā)表單元素的監(jiān)聽事件(例如input事件),就會有Vue.set()操作,從而觸發(fā)setter

帶著這個猜測,我們來看第二種操作。一進(jìn)頁面先點擊click按鈕,對info.message進(jìn)行賦值,message屬于新增屬性,根據(jù)官方文檔中說的,此時message并不是響應(yīng)式的,沒問題。但是我們接著在input輸入框中輸入值,class為message的div中沒有聯(lián)動出現(xiàn)任何值,根據(jù)我們對于第一種情況的猜測,當(dāng)輸入框監(jiān)聽到input事件的時候,會對info中的message進(jìn)行Vue.set()操作,所以理論上就算一開始click中是對新增屬性message直接賦值的,導(dǎo)致該屬性并非響應(yīng)式的,在經(jīng)過輸入框input事件中的Vue.set()操作之后,應(yīng)該會變成響應(yīng)式的,而現(xiàn)在呈現(xiàn)出來的情況并不是這樣的啊,這是為什么呢?

聰明的你們應(yīng)該已經(jīng)猜到在Vue.set()底層源碼中,應(yīng)該是會判斷message屬性是否一開始就在info中,如果存在就只是進(jìn)行單純的賦值,不存在的話在進(jìn)行響應(yīng)式操作,綁定gettersetter

但是光猜測肯定是不夠的,我們要用事實說話,做到有理有據(jù)。接下來我們就去看下vue源碼中v-model這塊,看看是不是如我們猜想的一樣

探索真相-源碼分析

v-model指令使用分為兩種情況:一種是在表單元素上使用,另外一種是在組件上使用。我們今天分析的是第一種情況,也就是在表單元素上使用

v-model實現(xiàn)機(jī)制

我們先簡單說下v-model的機(jī)制:v-model會把它關(guān)聯(lián)的響應(yīng)式數(shù)據(jù)(如info.message),動態(tài)地綁定到表單元素的value屬性上,然后監(jiān)聽表單元素的input事件:當(dāng)v-model綁定的響應(yīng)數(shù)據(jù)發(fā)生變化時,表單元素的value值也會同步變化;當(dāng)表單元素接受用戶的輸入時,input事件會觸發(fā),input的回調(diào)邏輯會把表單元素value最新值同步賦值給v-model綁定的響應(yīng)式數(shù)據(jù)。

v-model實現(xiàn)原理

我用來分析的源碼是在vue官網(wǎng)安裝模塊里面下載的開發(fā)版本(2.6.10),便于調(diào)試

編譯

我們今天講的內(nèi)容其實就是把模版編譯成render函數(shù)的一個流程,這里不會對每步流程都展開講解,我可以給出一個步驟實現(xiàn)的流程,大家有興趣的話可以根據(jù)這個流程來閱讀代碼,提高效率
$mount()->compileToFunctions()->compile()->baseCompile()
真正的編譯過程都是在這個baseCompile()里面執(zhí)行,執(zhí)行步驟可以分為三個過程

  1. 解析模版字符串生成AST
    const ast = parse(template.trim(), options)
  1. 優(yōu)化語法樹
    optimize(ast, options)
  1. 生成代碼
    const code = generate(ast, options)

然后我們看下generate里面的代碼,這也是我們今天講的重點

    function generate (
    ast,
    options
  ) {
    var state = new CodegenState(options);
    var code = ast ? genElement(ast, state) : '_c("div")';
    return {
      render: ("with(this){return " + code + "}"),
      staticRenderFns: state.staticRenderFns
    }
  }

generate() 首先通過 genElement()->genData$2()->genDirectives() 生成code,再把codewith(this){return ${code}}} 包裹起來,最終的到render函數(shù)。
接下來我們從genDirectives()開始講解

genDirectives

在模板的編譯階段,v-model跟其他指令一樣,會被解析到 el.directives中,之后會通過genDirectives方法處理這些指令,我們這里從genDirectives()重點開始講,至于怎么到這步,如果大家感興趣的話,可以從generate()開始看

    function genDirectives (el, state) {
        var dirs = el.directives;
        if (!dirs) { return }
        var res = 'directives:[';
        var hasRuntime = false;
        var i, l, dir, needRuntime;
        for (i = 0, l = dirs.length; i < l; i++) {
          dir = dirs[i];
          needRuntime = true;
          var gen = state.directives[dir.name];
          if (gen) {
            // compile-time directive that manipulates AST.
            // returns true if it also needs a runtime counterpart.
            needRuntime = !!gen(el, dir, state.warn);
          }
          if (needRuntime) {
            hasRuntime = true;
            res += "{name:\"" + (dir.name) + "\",rawName:\"" + (dir.rawName) + "\"" + (dir.value ? (",value:(" + (dir.value) + "),expression:" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (",arg:" + (dir.isDynamicArg ? dir.arg : ("\"" + (dir.arg) + "\""))) : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},";
          }
        }
        if (hasRuntime) {
          return res.slice(0, -1) + ']'
        }
    }

我對上面這個代碼打個斷點,結(jié)合我們上面的代碼例子,這樣子看的更清楚,如下圖:


getDirectives.png

我們可以看到傳進(jìn)來的elAst語法樹,el.directivesel上的指令,在我們這里就是el-model的相關(guān)參數(shù),然后賦值給變量dirs

往下看代碼,for循環(huán)中有段代碼:

    var gen = state.directives[dir.name];
    if (gen) {
      // compile-time directive that manipulates AST.
      // returns true if it also needs a runtime counterpart.
      needRuntime = !!gen(el, dir, state.warn);
    }

這里面的state.dirctives是什么呢?打個斷點看一下,如下圖:

genDirectives_state.png

我們可以看到state.directives里面包含了很多指令方法,model就在其中,

    var gen = state.directives[dir.name];

其實就是等價于

    var gen = state.directives[model];

所以代碼中的變量gen得到的是model()

    needRuntime = !!gen(el, dir, state.warn);

其實就是執(zhí)行了model()

model

那我們再來看看model這個方法里面做了些什么事情,先上model的代碼:

  function model (el,dir,_warn) {
    warn$1 = _warn;
    var value = dir.value;
    var modifiers = dir.modifiers;
    var tag = el.tag;
    var type = el.attrsMap.type;

    {
      // inputs with type="file" are read only and setting the input's
      // value will throw an error.
      if (tag === 'input' && type === 'file') {
        warn$1(
          "<" + (el.tag) + " v-model=\"" + value + "\" type=\"file\">:\n" +
          "File inputs are read only. Use a v-on:change listener instead.",
          el.rawAttrsMap['v-model']
        );
      }
    }

    if (el.component) {
      genComponentModel(el, value, modifiers);
      // component v-model doesn't need extra runtime
      return false
    } else if (tag === 'select') {
      genSelect(el, value, modifiers);
    } else if (tag === 'input' && type === 'checkbox') {
      genCheckboxModel(el, value, modifiers);
    } else if (tag === 'input' && type === 'radio') {
      genRadioModel(el, value, modifiers);
    } else if (tag === 'input' || tag === 'textarea') {
      genDefaultModel(el, value, modifiers);
    } else if (!config.isReservedTag(tag)) {
      genComponentModel(el, value, modifiers);
      // component v-model doesn't need extra runtime
      return false
    } else {
      warn$1(
        "<" + (el.tag) + " v-model=\"" + value + "\">: " +
        "v-model is not supported on this element type. " +
        'If you are working with contenteditable, it\'s recommended to ' +
        'wrap a library dedicated for that purpose inside a custom component.',
        el.rawAttrsMap['v-model']
      );
    }

    // ensure runtime directive metadata
    return true
  }

model方法根據(jù)傳入的參數(shù)對tag的類型進(jìn)行判斷,調(diào)用不同的處理邏輯,本demo中tag的類型為input,所以會執(zhí)行genDefaultModel方法

genDefaultModel

    function genDefaultModel (el,value,modifiers) {
        var type = el.attrsMap.type;
        {
          var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value'];
          var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type'];
          if (value$1 && !typeBinding) {
            var binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value';
            warn$1(
              binding + "=\"" + value$1 + "\" conflicts with v-model on the same element " +
              'because the latter already expands to a value binding internally',
              el.rawAttrsMap[binding]
            );
          }
        }

        var ref = modifiers || {};
        var lazy = ref.lazy;
        var number = ref.number;
        var trim = ref.trim;
        var needCompositionGuard = !lazy && type !== 'range';
        var event = lazy
          ? 'change'
          : type === 'range'
            ? RANGE_TOKEN
            : 'input';

        var valueExpression = '$event.target.value';
        if (trim) {
          valueExpression = "$event.target.value.trim()";
        }
        if (number) {
          valueExpression = "_n(" + valueExpression + ")";
        }

        var code = genAssignmentCode(value, valueExpression);
        if (needCompositionGuard) {
          code = "if($event.target.composing)return;" + code;
        }

        addProp(el, 'value', ("(" + value + ")"));
        addHandler(el, event, code, null, true);
        if (trim || number) {
          addHandler(el, 'blur', '$forceUpdate()');
        }
  }

我們對genDefaultModel()中的代碼進(jìn)行分塊解析,首先看下面這段代碼:

是否同時具有指令v-modelv-bind
    var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value'];
    var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type'];
    if (value$1 && !typeBinding) {
      var binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value';
      warn$1(
        binding + "=\"" + value$1 + "\" conflicts with v-model on the same element " +
        'because the latter already expands to a value binding internally',
        el.rawAttrsMap[binding]
      );
    }

這塊代碼其實就是解釋表單元素是否同時有指令v-modelv-bind

    var ref = modifiers || {};
    var lazy = ref.lazy;
    var number = ref.number;
    var trim = ref.trim;
修飾符

這段代碼就是獲取修飾符lazy, number及trim

  1. .lazy 取代input監(jiān)聽change事件
  2. .number 輸入字符串轉(zhuǎn)為數(shù)字
  3. .trim 輸入首尾空格過濾
var needCompositionGuard = !lazy && type !== 'range';

這里的needCompositionGuard后面再說有什么用,現(xiàn)在只用知道默認(rèn)是true就行了

    var event = lazy
      ? 'change'
      : type === 'range'
        ? RANGE_TOKEN
        : 'input';

    var valueExpression = '$event.target.value';
    if (trim) {
      valueExpression = "$event.target.value.trim()";
    }
    if (number) {
      valueExpression = "_n(" + valueExpression + ")";
    }

上面這段代碼中,event = ‘input’,定義變量valueExpression,修飾符trimnumber在我們這個demo中默認(rèn)都沒有,所以跳過往下看

genAssignmentCode
    var code = genAssignmentCode(value, valueExpression);
    if (needCompositionGuard) {
      code = "if($event.target.composing)return;" + code;
    }

這里涉及到一個函數(shù)genAssignmentCode,上源碼:

  function genAssignmentCode (
    value,
    assignment
  ) {
    var res = parseModel(value);
    if (res.key === null) {
      return (value + "=" + assignment)
    } else {
      return ("$set(" + (res.exp) + ", " + (res.key) + ", " + assignment + ")")
    }
  }

這段代碼是生成v-model綁定的value的值,看到這段代碼,我們就知道離真相不遠(yuǎn)了,因為我們看到了$set()。現(xiàn)在我們通過斷點具體分析下,如下圖:

getAssignmentCode.png

通過斷點我們可以很清楚的看到我們先執(zhí)行parseModel('info.message')獲取到一個對象res,由于我們的demo中綁定的值是路徑形式的對象,即info.message,所以此時res通過parseModel解析出來就是{exp: "info", key: "message"}。那下面的判斷就進(jìn)入else,即:

    return ("$set(" + (res.exp) + ", " + (res.key) + ", " + assignment + ")")

回到上面的getDefaultModel()中

    var code = genAssignmentCode(value, valueExpression);
    if (needCompositionGuard) {
      code = "if($event.target.composing)return;" + code;
    }

此時code獲取到genAssignmentCode()返回的字符串值"$set(info, "message", $event.target.value)"

$event.target.composing

上面我說的到變量needCompositionGuard = true,經(jīng)過拼接,最終code = “if($event.target.composing)return;$set(info, "message", $event.target.value)”

這里的$event.target.composing有什么用呢?其實就是用于判斷此次input事件是否是IME構(gòu)成觸發(fā)的,如果是IME構(gòu)成,直接return。IME 是輸入法編輯器(Input Method Editor) 的英文縮寫,IME構(gòu)成指我們在輸入文字時,處于未確認(rèn)狀態(tài)的文字。如圖:

composing.png

帶下劃線的ceshi就屬于IME構(gòu)成,它會同樣會觸發(fā)input事件,但不會觸發(fā)v-model更新數(shù)據(jù)。

繼續(xù)往下看

    addProp(el, 'value', ("(" + value + ")"));
    addHandler(el, event, code, null, true);
    if (trim || number) {
      addHandler(el, 'blur', '$forceUpdate()');
    }
addProp

先說下addProp(el, 'value', ("(" + value + ")"))

    function addProp (el, name, value, range, dynamic) {
      (el.props || (el.props = [])).push(rangeSetItem({ name: name, value: value, dynamic: dynamic }, range));
      el.plain = false;
    }

照常打個斷點看下:,如下圖


addProp.png

可以看到此方法的功能為給el添加props,首先判斷el上有沒有props,如果沒有的話創(chuàng)建props并賦值為一個空數(shù)組,隨后拼接對象并推到props中,代碼在此demo中相當(dāng)于push{name: "value", value: "(info.message)"}

如果一直往下追,可以看到這個方法其實是在input輸入框上綁定了value,對照我們的demo來看,就是將<input v-model="info.message" type="text">變成<input v-bind:value="info.message" type="text">

addHandler

同樣的,addHandler()相當(dāng)于在input上綁定了input事件,最終我們demo的模版就會被編譯成

    <input v-bind:value="info.message" v-on:input="info.message=$event.target.value">
render

后續(xù)再根據(jù)一些指令拼接,我們最終的到的render如下:

with(this) {
    return _c('div', {
        attrs: {
            "id": "app-2"
        }
    }, [_c('div', [_v(_s(info.message))]), _v(" "), _c('div', [_c('input', {
        directives: [{
            name: "model",
            rawName: "v-model",
            value: (info.message),
            expression: "info.message"
        }],
        attrs: {
            "type": "text"
        },
        domProps: {
            "value": (info.message)
        },
        on: {
            "input": function ($event) {
                if ($event.target.composing) return;
                $set(info, "message", $event.target.value)
            }
        }
    })]), _v(" "), _c('button', {
        on: {
            "click": change
        }
    }, [_v("click")])])
}

最后通過createFunction()render代碼串通過new Function的方式轉(zhuǎn)換成可執(zhí)行的函數(shù),賦值給 vm.options.render,這樣當(dāng)組件通過vm._render的時候,就會執(zhí)行這個render函數(shù)

至此,針對表單元素上的v-model指令從開始編譯到最終生成render()并執(zhí)行的過程就講解完了,我們驗證了在編譯階段,v-model會在監(jiān)聽到input事件時對我們綁定的value進(jìn)行Vue.$set()操作

還記得我們上面說的對demo第二種操作情況么?先進(jìn)行click操作賦值,那v-model中的Vue.$set()操作似乎沒有作用了。我們當(dāng)時猜測的是Vue.$set()底層源碼中有應(yīng)該是會判斷message屬性是否一開始就在info中,如果存在就只是進(jìn)行單純的賦值,不存在的話在進(jìn)行響應(yīng)式操作,綁定gettersetter

現(xiàn)在我們就去Vue.$set()中看一下

set

先上代碼:

/**
   * Set a property on an object. Adds the new property and
   * triggers change notification if the property doesn't
   * already exist.
   */
  function set (target, key, val) {
    if (isUndef(target) || isPrimitive(target)
    ) {
      warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
    }
    if (Array.isArray(target) && isValidArrayIndex(key)) {
      target.length = Math.max(target.length, key);
      target.splice(key, 1, val);
      return val
    }
    if (key in target && !(key in Object.prototype)) {
      target[key] = val;
      return val
    }
    var ob = (target).__ob__;
    if (target._isVue || (ob && ob.vmCount)) {
      warn(
        'Avoid adding reactive properties to a Vue instance or its root $data ' +
        'at runtime - declare it upfront in the data option.'
      );
      return val
    }
    if (!ob) {
      target[key] = val;
      return val
    }
    defineReactive$$1(ob.value, key, val);
    ob.dep.notify();
    return val
  }

看到這句代碼了么?這就是證據(jù),驗證我們猜想的證據(jù)

if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val
}
驗證猜想

當(dāng)我們首先點擊click的時候,執(zhí)行this.info.message = 'hello world',此時info對象中新增了一個message屬性。當(dāng)我們在input框中輸入值并觸發(fā)Vue.$set()時,key in targettrue,并且message又不是Object原型上的屬性,所以!(key in Object.prototype)也為true,此時message屬性并不是響應(yīng)式屬性,沒有綁定setter,所以僅僅進(jìn)行了單純的賦值操作。

而當(dāng)我們一進(jìn)頁面首次input中執(zhí)行輸入操作時,根據(jù)上面我們的分析input框監(jiān)聽到了input事件,先執(zhí)行了Vue.$set()操作,因為時首次,所以info中還沒有message屬性,所以上面的key in targetfalse,跳過了賦值操作,到了下面的

defineReactive$$1(ob.value, key, val);
ob.dep.notify();

這個defineReactive的作用就是為message綁定了getter()setter(),之后再對message的賦值操作都會直接進(jìn)入自身綁定的setter中進(jìn)行響應(yīng)式操作

一個意外的發(fā)現(xiàn)

我突然奇想把vue的版本換到了2.3.0,發(fā)現(xiàn)v-model不能對demo中的message屬性實現(xiàn)響應(yīng)化,跑去看了下vue更新日志,發(fā)現(xiàn)在2.5.0版本中,有這么一句話
now creates non-existent properties as reactive (non-recursive) e1da0d5, closes #5932 (See reasoning behind this change)
上面這句話的意思是從2.5.0版本開始支持將不存在的屬性響應(yīng)化,非遞歸的。
因為message屬性一開始在info中并沒有定義,在2.3.0中,還不支持將不存在的屬性響應(yīng)化的操作,所以對demo無效

總結(jié)

到這里,我們這篇文章就結(jié)束了 里面有一些細(xì)節(jié)如果大家有興趣的話可以自己再去深究一下。有時候很小的一個問題,背后牽扯到的知識點也是很多的,盡量把每個不懂背后的邏輯搞清楚,才能盡快的成為你想成為的人

參考資料

https://segmentfault.com/a/1190000015848976#articleHeader0
https://blog.csdn.net/fabulous1111/article/details/85265503

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,157評論 2 375

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

  • 主要還是自己看的,所有內(nèi)容來自官方文檔。 介紹 Vue.js 是什么 Vue (讀音 /vju?/,類似于 vie...
    Leonzai閱讀 3,369評論 0 25
  • 1. Vue 實例 1.1 創(chuàng)建一個Vue實例 一個 Vue 應(yīng)用由一個通過 new Vue 創(chuàng)建的根 Vue 實...
    王童孟閱讀 1,028評論 0 2
  • vue概述sd 在官方文檔中,有一句話對Vue的定位說的很明確:Vue.js 的核心是一個允許采用簡潔的模板語法來...
    去年的牛肉閱讀 4,072評論 0 1
  • 一、了解Vue.js 1.1.1 Vue.js是什么? 簡單小巧、漸進(jìn)式、功能強(qiáng)大的技術(shù)棧 1.1.2 為什么學(xué)習(xí)...
    蔡華鵬閱讀 3,357評論 0 3
  • VUE介紹 Vue的特點構(gòu)建用戶界面,只關(guān)注View層簡單易學(xué),簡潔、輕量、快速漸進(jìn)式框架 框架VS庫庫,是一封裝...
    多多醬_DuoDuo_閱讀 2,728評論 1 17