vue的生命週期解析並經過表單理解MVVM(不只理論,圖文並茂)

開始前說一說

吐槽

首先, 文章有謬誤的地方, 請評論, 我會進行驗證修改。謝謝。css

vue真是個好東西,但vue的中文文檔還有很大的改進空間,有點大雜燴的意思,對於怎麼把html文件(經過<script>引入vue)簡單項目移植成vue-cli(vue init webpack my-project)構建的模塊化組件項目言之甚少,理解了vue的生命週期,才能高效簡潔的編寫組件間的數據通訊代碼,這是寫vue程序極爲重要的關鍵性的概念也沒有官方的demo。html

我以前看過facebook發行的Gytsby這個static-gen的英文文檔,整個tutorial一鼓作氣,且文章用詞把握在3000英文單詞左右,新聞撰稿的級別易懂且印象深入,說實話當時我在看這個文檔的時候是欲罷不能了,技術文檔越看越爽。
其實我想說vue是個很好的框架,發展到如今更須要的是傳播力,技術只有易於傳承才能產生更多的生產力。前端

鑑於以前vue的更新工做主要是尤大一我的做爲主力,以後但願能改善vue的官方文檔,有按部就班的demo輔助理解技術,能讓新手更順暢的學習vue, 我我的每次對vue, MVVM理解的提高基本都不是看的vue官方文檔,而是瀏覽大量開源社區的文章和項目實踐達到的。vue

這篇文章本文旨在結合簡易項目,說清vue組件生命週期,讓新人能快速的理解mvvm和vue的生命週期,跳過那個迷茫前進的過程。react

核心概念

  • 你須要明白Vue 組件都是 Vue 實例
  • 官方vue生命週期配圖

Vue 實例
lifecycle.pngwebpack

生命週期參考文章=> vue生命週期探究(一)
生命週期參考文章=> Vue2.0 探索之路——生命週期和鉤子函數的一些理解
  • MVVM
本段內容摘錄自廖雪峯老師的MVVM

什麼是MVVM?MVVM是Model-View-ViewModel的縮寫。git

要編寫可維護的前端代碼絕非易事。我因爲前端開發混合了HTML、CSS和JavaScript,並且頁面衆多,因此,代碼的組織和維護難度其實更加複雜,這就是MVVM出現的緣由。github

在瞭解MVVM以前,咱們先回顧一下前端發展的歷史。web

用JavaScript在瀏覽器中操做HTML,經歷了若干發展階段:vue-router

第一階段,直接用JavaScript操做DOM節點,使用瀏覽器提供的原生API:

var dom = document.getElementById('name');
dom.innerHTML = 'Homer';
dom.style.color = 'red';

第二階段,因爲原生API很差用,還要考慮瀏覽器兼容性,jQuery橫空出世,以簡潔的API迅速俘獲了前端開發者的芳心:

$('#name').text('Homer').css('color', 'red');

第三階段,MVC模式,須要服務器端配合,JavaScript能夠在前端修改服務器渲染後的數據。

如今,隨着前端頁面愈來愈複雜,用戶對於交互性要求也愈來愈高,想要寫出Gmail這樣的頁面,僅僅用jQuery是遠遠不夠的。MVVM模型應運而生。

在前端頁面中,把Model用純JavaScript對象表示,View負責顯示,二者作到了最大限度的分離。

把Model和View關聯起來的就是ViewModel。ViewModel負責把Model的數據同步到View顯示出來,還負責把View的修改同步回Model。

ViewModel如何編寫?須要用JavaScript編寫一個通用的ViewModel,這樣,就能夠複用整個MVVM模型了。

一個MVVM框架和jQuery操做DOM相比有什麼區別?

咱們先看用jQuery實現的修改兩個DOM節點的例子:

<!-- HTML -->
<p>Hello, <span id="name">Bart</span>!</p>
<p>You are <span id="age">12</span>.</p>

Hello, Bart!

You are 12.

用jQuery修改name和age節點的內容:

'use strict';
----
var name = 'Homer';
var age = 51;

$('#name').text(name);
$('#age').text(age);
----
// 執行代碼並觀察頁面變化, 請點擊本節開頭引用連接去該文章原創頁面更改會生效。

若是咱們使用MVVM框架來實現一樣的功能,咱們首先並不關心DOM的結構,而是關心數據如何存儲。最簡單的數據存儲方式是使用JavaScript對象:

var person = {
    name: 'Bart',
    age: 12
};

咱們把變量person看做Model,把HTML某些DOM節點看做View,並假定它們之間被關聯起來了。

要把顯示的name從Bart改成Homer,把顯示的age從12改成51,咱們並不操做DOM,而是直接修改JavaScript對象:
Hello, Bart!
You are 12.

'use strict';
----
person.name = 'Homer';
person.age = 51;
----
// 執行代碼並觀察頁面變化, 同上原創頁面更改

執行上面的代碼,咱們驚訝地發現,改變JavaScript對象的狀態,會致使DOM結構做出對應的變化!這讓咱們的關注點從如何操做DOM變成了如何更新JavaScript對象的狀態,而操做JavaScript對象比DOM簡單多了!

這就是MVVM的設計思想:關注Model的變化,讓MVVM框架去自動更新DOM的狀態,從而把開發者從操做DOM的繁瑣步驟中解脫出來!

簡易點擊開頭連接把以後的兩節單向綁定和雙向綁定看一看,我想你會受益不淺,廖老師寫的很好。


vue發音同view, vue的api, v-model雙向數據綁定 => view-model就是MVVM裏面的VM, 經過前文能夠通俗的理解爲:

v-model => viewModel: 視圖層(用戶看到的界面view)和數據層(Model模型,vue實例中的data,computed,props等都屬於數據層)之間相互"映射", 即二者任意一個發生改變都會觸發對方的變爲一致, 只關注數據(model)的變化

v-if => view-if => if和數據相關, 若是某個數據的結果爲true則渲染這個view,不然不渲染, 也是隻關注數據(model)的變化

v-show => view-show => view的顯示(diplay)與否和show的布爾值有關, 仍是隻關注數據(model)的變化

v-bind => view-bind => 和view相關的數據與另一個數據進行綁定, 顯示的是綁定的數據對應的view, 仍是隻關注數據(model)的變化

v-on => view-on => view監聽一個事件,即vue實例中對應的方法method, 其實仍是經過click等事件,出發數據的改變(data,computed,props),經過數據(model)的變化再反饋給view,仍是隻關注數據(model)的變化

v-for => view-for => 把一個數組等容器形式存在的數據(model)以for循環的方式來渲染view, 仍是隻關注數據(model)的變化。

MVVM中的VM => viewModel 實際上實現了生產力的解放, 應用的設計脫離了固有的DOM結構, 而是我有數據(model)我想把數據展現(view)出來,其餘人或服務經過看到這個試圖(view)就能夠得到數據(model),編輯數據,而不用再拘泥於形式(DOM等),vue框架封裝好了這些操做,讓編程變的高效簡潔,大道至簡。

在vue實例中的生命週期, 方法method, computed, watch, filter, props等, 都是用來處理數據, 而後"映射"到視圖(view)上, 核心就是數據層(Model), 因此, 用vue這個框架來進行前端的頁面的模塊化編程, 組件實例的做用域是孤立的, 須要解決的就是不一樣組件(父子組件和非父子組件)之間的通訊問題, 來進行數據傳遞, 而這個過程會每每伴隨這組件實例間的切換, 就有老組件實例的銷燬和新組件實例的掛載, 理解組件實例的生命週期對於數據可否精準的傳遞相當重要。


正文

1 v-if 在組件上或組件根元素上生命週期對於數據傳遞的影響

不知道你有沒有碰到過這種狀況, 有一個表單並帶有增刪改查的功能, 那麼vue-cli項目構建的方式就會須要兩個組件實例, 一個組件Table.vue做爲表單部分, 另外一個組件Crud.vue做爲添加create, research, update, delete的模態框。

那麼以MVVM的思想, 增長或修改一行數據, 點擊按鈕就會用v-if渲染出Crud.vue組件實例, 期間會把Table.vue組件實例的一行數據(data)以正確的組件通訊方式傳遞給Crud.vue組件實例, 該組件實例會把傳遞過來的數據"映射"到模態框對應的輸入框(viuw)中, 而後編輯完成之後再以一樣的方式傳回數據, Table.vue組件實例就是渲染相應的新數據信息到表單上, 這就模擬完成了一個簡單的表單編輯功能。流程圖以下:

圖片描述

這個過程模態框組件實例Crud.vue由於v-if的不一樣聲明位置而經歷不同的生命週期, 這會致使Crud.vue
的須要在不一樣的狀態下才能接收到數據。驗證demo以下:

首先, 新建一個index.html文件(章節末尾有完整代碼), 在html文件中註冊兩個全局組件
my-component-one

template: '<div>A component!</div>'

my-component-two

template: '<div v-if="isActive">A component!</div>'

且都加上了以下代碼(命名爲組件生命週期測試代碼組):

beforeRouteEnter(to, from, next) {
        console.log(this) //undefined,不能用this來獲取vue實例
        console.log('組件路由勾子:beforeRouteEnter')
        next(vm => {
          console.log(vm) //vm爲vue的實例
          console.log('組件路由勾子beforeRouteEnter的next')
        })
      },
      beforeCreate() {
        console.log('組件:beforeCreate')
        console.log("%c%s", "color:red", "el     : " + this.$el); //undefined
        console.log("%c%s", "color:red", "data   : " + this.$data); //undefined 
        console.log("%c%s", "color:red", "message: " + this.message)
      },
      created() {
        this.$nextTick(() => {
          console.log('nextTick')
        })
        console.log('組件:created')
        console.log("%c%s", "color:red", "el     : " + this.$el); //undefined
        console.log("%c%s", "color:red", "data   : " + this.$data); //已被初始化 
        console.log("%c%s", "color:red", "message: " + this.message); //已被初始化
      },
      beforeMount() {
        console.log('組件:beforeMount')
        console.log("%c%s", "color:red", "el     : " + (this.$el)); //已被初始化
        console.log("%c%s", "color:red", "data   : " + this.$data); //已被初始化  
        console.log("%c%s", "color:red", "message: " + this.message); //已被初始化
      },
      mounted() {
        console.log('組件:mounted')
      },
      beforeUpdate() {
        console.log('beforeUpdate')
      },
      updated() {
        console.log('updated')
      },
      beforeDestroy: function() {
        console.log('beforeDestroy 銷燬前狀態===============》');
        console.log("%c%s", "color:red", "el     : " + this.$el);
        console.log("%c%s", "color:red", "data   : " + this.$data);
        console.log("%c%s", "color:red", "message: " + this.message);
      },
      destroyed: function() {
        console.log('destroyed 銷燬完成狀態===============》');
        console.log("%c%s", "color:red", "el     : " + this.$el);
        console.log("%c%s", "color:red", "data   : " + this.$data);
        console.log("%c%s", "color:red", "message: " + this.message)
      },
      beforeRouteLeave(to, from, next) {
        console.log(this) //能夠訪問vue實例
        console.log('組件路由勾子:beforeRouteLeave')
        next()
      }

而後聲明組件

<div id="app" style="float: right;">
    <my-component-one v-if="activeOne"></my-component-one>
    <button v-on:click="toggleOne">IfOnComponent</button>
    <hr>
    <my-component-two v-bind:is-active="activeTwo"></my-component-two>
    <button v-on:click="toggleTwo">IfOnRootElement</button>
  </div>

組件my-component-one的v-if聲明在組件上, 對應按鈕IfOnComponent

組件my-component-two的v-if聲明在組件根元素上。對應按鈕IfOnRootElement

兩個組件的布爾值經過兩個臨近的按鈕控制(toggle),v-if初始值activeOneactiveTwo的結果都是flase

打開index.html, F12打開開發者工具。頁面剛加載時控制檯以下圖:

clipboard.png

如圖所示, 能夠看到在v-if聲明在組件根元素上, 初始值爲false, 頁面加載時該組件my-component-two的生命週期會處於mounted掛載狀態, 可是沒有被渲染在DOM節點樹上。 組件my-component-onev-if聲明在組件上, 則徹底沒有進入生命週期。

狀況1 v-if聲明在組件根元素上

點擊按鈕IfOnRootElement, 效果以下圖紅框部分:

clipboard.png

如圖所示, 點擊按鈕後activeTwo值的改變成true, 而觸發beforeUpdate => updated

再點擊按鈕IfOnRootElement, 效果以下圖紅框部分:

clipboard.png

如圖所示, 再次點擊按鈕後activeTwo值的變回false, 又觸發beforeUpdate => updated

因此,v-if聲明在組件根元素上。 組件實例數據的改變會觸發beforeUpdate => updated

狀況2 v-if聲明在組件上

頁面剛加載時, my-component-one沒有進入生命週期, 先清空控制檯, 點擊按鈕IfOnComponent, 效果以下圖紅框部分:

clipboard.png

如圖所示, 點擊按鈕後activeOne值的改變成true, 而觸發beforeCreate => create => beforeMount => Mounted, 組件實例掛載並被渲染到DOM節點樹中。

再點擊按鈕IfOnComponent, 效果以下圖紅框部分:

clipboard.png

如圖所示, 再次點擊按鈕後activeOne值的變回false, 觸發beforeDestroy => destroyed, 組件不會觸發beforeUpdate => updated而直接進入銷燬。

因此,v-if聲明在組件上。 組件實例與v-if相關數據的改變不會觸發beforeUpdate => updated

本demo完整代碼以下:

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>vue-lifecycle</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
  <div id="app" style="float: right;">
    <my-component-one v-if="activeOne"></my-component-one>
    <button v-on:click="toggleOne">IfOnComponent</button>
    <hr>
    <my-component-two v-bind:is-active="activeTwo"></my-component-two>
    <button v-on:click="toggleTwo">IfOnRootElement</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue"></script>
  <script>
    // 註冊
    Vue.component('my-component-one', {
      name: "2",
      template: '<div>A component!</div>',
      beforeRouteEnter(to, from, next) {
        console.log('組件:my-component-one')
        console.log(this) //undefined,不能用this來獲取vue實例
        console.log('組件路由勾子:beforeRouteEnter')
        next(vm => {
          console.log(vm) //vm爲vue的實例
          console.log('組件路由勾子beforeRouteEnter的next')
        })
      },
      beforeCreate() {
        console.log('組件:my-component-one')
        console.log('beforeCreate')
        console.log("%c%s", "color:red", "el     : " + this.$el); //undefined
        console.log("%c%s", "color:red", "data   : " + this.$data); //undefined 
        console.log("%c%s", "color:red", "message: " + this.message)
      },
      created() {
        this.$nextTick(() => {
          console.log('組件:my-component-one')
          console.log('nextTick')
        })
        console.log('組件:my-component-one')
        console.log('created')
        console.log("%c%s", "color:red", "el     : " + this.$el); //undefined
        console.log("%c%s", "color:red", "data   : " + this.$data); //已被初始化 
        console.log("%c%s", "color:red", "message: " + this.message); //已被初始化
      },
      beforeMount() {
        console.log('組件:my-component-one')
        console.log('beforeMount')
        console.log("%c%s", "color:red", "el     : " + (this.$el)); //已被初始化
        console.log("%c%s", "color:red", "data   : " + this.$data); //已被初始化  
        console.log("%c%s", "color:red", "message: " + this.message); //已被初始化
      },
      mounted() {
        console.log('組件:my-component-one')
        console.log('mounted')
      },
      beforeUpdate() {
        console.log('組件:my-component-one')
        console.log('beforeUpdate')
      },
      updated() {
        console.log('組件:my-component-one')
        console.log('updated')
      },
      beforeDestroy: function() {
        console.log('組件:my-component-one')
        console.log('beforeDestroy 銷燬前狀態===============》');
        console.log("%c%s", "color:red", "el     : " + this.$el);
        console.log("%c%s", "color:red", "data   : " + this.$data);
        console.log("%c%s", "color:red", "message: " + this.message);
      },
      destroyed: function() {
        console.log('組件:my-component-one')
        console.log('destroyed 銷燬完成狀態===============》');
        console.log("%c%s", "color:red", "el     : " + this.$el);
        console.log("%c%s", "color:red", "data   : " + this.$data);
        console.log("%c%s", "color:red", "message: " + this.message)
      },
      beforeRouteLeave(to, from, next) {
        console.log('組件:my-component-one')
        console.log(this) //能夠訪問vue實例
        console.log('組件路由勾子:beforeRouteLeave')
        next()
      }
    })

    Vue.component('my-component-two', {
      name: "1",
      template: '<div v-if="isActive">A component!</div>',
      props: ["isActive"],
      beforeRouteEnter(to, from, next) {
        console.log('組件:my-component-two')
        console.log(this) //undefined,不能用this來獲取vue實例
        console.log('組件路由勾子:beforeRouteEnter')
        next(vm => {
          console.log(vm) //vm爲vue的實例
          console.log('組件路由勾子beforeRouteEnter的next')
        })
      },
      beforeCreate() {
        console.log('組件:my-component-two')
        console.log('beforeCreate')
        console.log("%c%s", "color:red", "el     : " + this.$el); //undefined
        console.log("%c%s", "color:red", "data   : " + this.$data); //undefined 
        console.log("%c%s", "color:red", "message: " + this.message)
      },
      created() {
        this.$nextTick(() => {
          console.log('組件:my-component-two')
          console.log('nextTick')
        })
        console.log('組件:my-component-two')
        console.log('created')
        console.log("%c%s", "color:red", "el     : " + this.$el); //undefined
        console.log("%c%s", "color:red", "data   : " + this.$data); //已被初始化 
        console.log("%c%s", "color:red", "message: " + this.message); //已被初始化
      },
      beforeMount() {
        console.log('組件:my-component-two')
        console.log('beforeMount')
        console.log("%c%s", "color:red", "el     : " + (this.$el)); //已被初始化
        console.log("%c%s", "color:red", "data   : " + this.$data); //已被初始化  
        console.log("%c%s", "color:red", "message: " + this.message); //已被初始化
      },
      mounted() {
        console.log('組件:my-component-two')
        console.log('mounted')
      },
      beforeUpdate() {
        console.log('組件:my-component-two')
        console.log('beforeUpdate')
      },
      updated() {
        console.log('組件:my-component-two')
        console.log('updated')
      },
      beforeDestroy: function() {
        console.log('組件:my-component-two')
        console.log('beforeDestroy 銷燬前狀態===============》');
        console.log("%c%s", "color:red", "el     : " + this.$el);
        console.log("%c%s", "color:red", "data   : " + this.$data);
        console.log("%c%s", "color:red", "message: " + this.message);
      },
      destroyed: function() {
        console.log('組件:my-component-two')
        console.log('destroyed 銷燬完成狀態===============》');
        console.log("%c%s", "color:red", "el     : " + this.$el);
        console.log("%c%s", "color:red", "data   : " + this.$data);
        console.log("%c%s", "color:red", "message: " + this.message)
      },
      beforeRouteLeave(to, from, next) {
        console.log('組件:my-component-two')
        console.log(this) //能夠訪問vue實例
        console.log('組件路由勾子:beforeRouteLeave')
        next()
      }
    })

    var vm = new Vue({
      el: "#app",
      data: {
        activeOne: false,
        activeTwo: false,
      },
      methods: {
        toggleOne() {
          this.activeOne = !this.activeOne;
        },
        toggleTwo() {
          this.activeTwo = !this.activeTwo;
        }
      },
    })
  </script>
</body>

</html>

這個demo寫下來咱們須要下面兩條結論, 來做爲接下來實際vue-cli項目的支持.
因此,v-if聲明在組件根元素上。 組件實例數據的改變會觸發beforeUpdate => updated
因此,v-if聲明在組件上。 組件實例與v-if相關數據的改變不會觸發beforeUpdate => updated

2 表單中v-if在組件上或組件根元素上生命週期對於數據傳遞的影響

我在章節1的開頭說過表單會面臨的問題, 不記得同窗能夠回去看.

若是有vue init webpck my-project 並(cnpm install)安裝好依賴模塊的項目, 建議在乾淨的項目上直接拷貝下文的代碼, 不然建議直接點擊github連接clone項目或者下載壓縮包.並按步驟啓動項目, 通過測試, 能順利運行.

本章github源碼連接

目錄結構
clipboard.png

vue init webpck my-project 構建完成項目之後,
cd my-project,

npm installcnpm install

安裝完成後, 先在src\assets文件夾下建立了css文件夾並在裏面編寫了需main.css, 代碼以下:
(先不要處理細節, copy代碼先讓項目能運行, 本文的主要關注點是生命週期和MVVM, 對於不知道怎麼把html文件中<script>引入vue完成的非腳手架項目移植到vue-cli腳手架上去的同窗, 能夠參考代碼結構)

main.css:

[v-cloak] {
    display: none;
  }
  table {
    border: 1px solid #ccc;
    padding: 0;
    border-collapse: collapse;
    table-layout: fixed;
    margin-top: 10px;
    width: 100%;
  }
  table td,
  table th {
    height: 30px;
    border: 1px solid #ccc;
    background: #fff;
    font-size: 15px;
    padding: 3px 3px 3px 8px;
    overflow: hidden;
  }
  table th:first-child {
    width: 30px;
  }
  .container,
  .st {
    width: 100%;
    margin: 10px auto 0;
    font-size: 13px;
    font-family: "Microsoft YaHei";
  }
  .container .search {
    font-size: 15px;
    padding: 4px;
  }
  .container .add {
    padding: 5px 15px;
  }
  .overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 6;
    background: rgba(0, 0, 0, 0.7);
  }
  .overlay td:first-child {
    width: 66px;
  }
  .overlay .con {
    position: absolute;
    width: 420px;
    min-height: 300px;
    background: #fff;
    left: 50%;
    top: 50%;
    -webkit-transform: translate3d(-50%, -50%, 0);
    transform: translate3d(-50%, -50%, 0);
    /*margin-top: -150px;*/
    padding: 20px;
  }

而後修改App.vue並在src\component目錄下編寫了組件Table.vueCrud.vue, 代碼以下:

App.vue:

<template>
  <div id="app">
    <img src="./assets/logo.png">
    <router-view/>
  </div>
</template>

<script>
export default {
  name: "App",
  beforeRouteEnter(to, from, next) {
    console.log("組件路由勾子:beforeRouteEnter, 下一行打印自身this");
    console.log(this); //undefined,不能用this來獲取vue實例
    next(vm => {
      console.log("組件路由勾子beforeRouteEnter的next");
      console.log(vm); //vm爲vue的實例
    });
  },
  beforeCreate() {
    console.log("beforeCreate");
  },
  created() {
    this.$nextTick(() => {
      console.log("nextTick");
    });
    console.log("created");
  },
  beforeMount() {
    console.log("beforeMount");
  },
  mounted() {
    console.log("mounted");
  },
  beforeUpdate() {
    console.log("beforeUpdate");
  },
  updated() {
    console.log("updated");
  },
  beforeDestroy: function() {
    console.log("beforeDestroy 銷燬前狀態===============》");
  },
  destroyed: function() {
    console.log("destroyed 銷燬完成狀態===============》");
  },
  beforeRouteLeave(to, from, next) {
    console.log("組件路由勾子:beforeRouteLeave, 下一行打印自身this");
    console.log(this); //能夠訪問vue實例
    next();
  }
};
</script>

<style>
  #app {
    font-family: "Avenir", Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
    margin-top: 60px;
  }
</style>

Table.vue:

<template>
  <div class="container">
    <input type="button" value="新增" class="add" @click="add">
    <div>
      <table>
        <thead>
          <tr>
            <th v-for="head in header" v-bind:key="head.id" v-cloak>{{ head }}</th>
          </tr>          
        </thead>
        <tbody>
          <tr v-for="(student, index) in students" v-bind:key="student.index" v-cloak>
            <td>{{index+1}}</td>
            <td v-for="value in student" v-bind:key="value.id">{{ value.toString() }}</td>
            <td><button v-on:click="edit(index)">修改</button><button @click="del(index)">刪除</button></td>
          </tr>
        </tbody>
      </table>
    </div>
    
    <!-- 外部組件,做爲子組件,與Crud組件的關係是父子組件關係, modifylist用kebab-case(短橫線分隔式命名),由於
    HTML 特性是不區分大小寫的。因此,當使用的不是字符串模板時,camelCase (駝峯式命名) 的 prop 須要轉換爲相對應的 kebab-case (短橫線分隔式命名): -->
      <crud class="just-try-something-and-press-f12" data-would-be-add-to-root-component-attr="true" v-bind:modify-list="selectedList" v-bind:is-active="isActive" v-on:edit="editOne" v-on:cancel="foobar = arguments[0];isActive=!isActive" v-cloak></crud>
    <!-- class="just-try-something-and-press-f12" data-would-be-add-to-root-component-attr="true"
    這兩段話是爲了驗證組件標籤上編寫的的非prop特性會被添加到組件到組件的根元素上去, 和表單無關.
    foobar = arguments[0] 是爲了驗證子組件經過$emit與父組件通訊時接受參數的形式, 嘗試直接把$on監聽到的數據直接賦值給父組件的model, 打開F12 vue devtool能夠看到能成功-->
  </div>
</template>

<script>
// https://www.xiabingbao.com/vue/2017/07/10/vue-curd.html
import Crud from "./Crud";

export default {
  name: "Table",
  components: { Crud },
  data() {
    return {
      foobar: 0,
      selectedList: {},
      isActive: false,
      header: ["id", "用戶名", "email", "性別", "省份", "愛好", "編輯"],
      students: [
        {
          username: "李明",
          email: "li@qq.com",
          sex: "男",
          province: "北京市",
          hobby: ["籃球", "編程"]
        },
        {
          username: "韓紅",
          email: "han@163.com",
          sex: "女",
          province: "河北省",
          hobby: ["彈琴", "插畫"]
        }
      ]
    };
  },
  filters: {
    filter: function(array) {
      // 數組的toString方法能輸出
      return array.toString();
    }
  },
  methods: {
    del(index) {
      this.students.splice(index, 1);
    },
    add() {
      var init = {
        username: "",
        email: "",
        sex: "",
        province: "",
        hobby: []
      };
      this.selectedList = JSON.parse(JSON.stringify(init));
      this.toggleEdit();
    },
    editOne(student) {
      var isduplicated;
      // 爲了簡單, 添加和編輯都是用這個方法, 判斷保存的username是否重複, 若是重複則覆蓋, 不重複則新建一行,經過isduplicated若是爲true來判斷.
      isduplicated = this.students.some(aStudent => {
        return aStudent.username === student.username;
      });
      console.log(isduplicated);
      if (isduplicated) {
        this.students.forEach((element, index) => {
          if (student.username == element.username) {
            this.students[index] = student;
          }
        });
      } else {
        this.students.push(student);
      }

      this.toggleEdit();
    },
    cancleOne() {
      this.toggleEdit();
    },
    edit(index) {
      // 必定要分別嘗試這兩斷代碼的區別!!! 兩端代碼要作同一件事就是傳遞表單的對象給子組件
      // 下面這一句經過轉換之後, 傳遞的selectedList對象是持有內存中新的引用的student
      this.selectedList = JSON.parse(JSON.stringify(this.students[index]));

      // 而下面這句持有的引用和表單中的student的引用是一致的, 即傳遞給子組件的student對象在進行屬性編輯的時候, 也會實時改變父組件表單中的內容, 就算點擊取消按鈕也不會還原修改, 運行實踐.
      // this.selectedList = this.students[index]
      this.toggleEdit();
    },
    toggleEdit() {
      this.isActive = !this.isActive;
    }
  },
  beforeRouteEnter(to, from, next) {
    console.log("組件路由勾子:beforeRouteEnter, 下一行打印自身this");
    console.log(this); //undefined,不能用this來獲取vue實例
    next(vm => {
      console.log("組件路由勾子beforeRouteEnter的next");
      console.log(vm); //vm爲vue的實例
    });
  },
  beforeCreate() {
    console.log("beforeCreate");
  },
  created() {
    this.$nextTick(() => {
      console.log("nextTick");
    });
    console.log("created");
  },
  beforeMount() {
    console.log("beforeMount");
  },
  mounted() {
    console.log("mounted");
  },
  beforeUpdate() {
    console.log("beforeUpdate");
  },
  updated() {
    console.log("updated");
  },
  beforeDestroy: function() {
    console.log("beforeDestroy 銷燬前狀態===============》");
  },
  destroyed: function() {
    console.log("destroyed 銷燬完成狀態===============》");
  },
  beforeRouteLeave(to, from, next) {
    console.log("組件路由勾子:beforeRouteLeave, 下一行打印自身this");
    console.log(this); //能夠訪問vue實例
    next();
  }
};
</script>

<style>
  @import "../assets/css/main.css";
</style>

Crud.vue:

<template>
  <div class="overlay" v-if="isActive">
    <div class="con">
      <h2 class="title">新增 | 修改</h2>
      <div class="content">
        <table>
          <tr>
            <td>用戶名</td>
            <td><input type="text" v-model="list.username"></td>
          </tr>
          <tr>
            <td>郵箱</td>
            <td><input type="text" v-model="list.email"></td>
          </tr>
          <tr>
            <td>性別</td>
            <td>
              <label><input type="radio" name="sex" value="男" v-model="list.sex">男</label>
              <label><input type="radio" name="sex" value="女" v-model="list.sex">女</label>
              <label><input type="radio" name="sex" value="未知" v-model="list.sex">未知</label>
            </td>
          </tr>
          <tr>
            <td>省份</td>
            <td>
              <select name="" id="" v-model="list.province">
                <option value="北京市">北京市</option>
                <option value="河北省">河北省</option>
                <option value="河南省">河南省</option>
                <option value="重慶市">重慶市</option>
                <option value="廣東省">廣東省</option>
                <option value="遼寧省">遼寧省</option>
              </select>
            </td>
          </tr>
          <tr>
            <td>愛好</td>
            <td>
              <label><input type="checkbox" v-model="list.hobby" value="籃球">籃球</label>
              <label><input type="checkbox" v-model="list.hobby" value="讀書">讀書</label>
              <label><input type="checkbox" v-model="list.hobby" value="插畫">插畫</label>
              <label><input type="checkbox" v-model="list.hobby" value="編程">編程</label>
              <label><input type="checkbox" v-model="list.hobby" value="彈琴">彈琴</label>
            </td>
          </tr>
        </table>
        <p>
          <input type="button" @click="cancelModify" value="取消">
          <input type="button" @click="modify" value="保存">
        </p>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "Crud",
  data() {
    return {
      list: {}
    };
  },
  methods: {
    cancelModify() {
      this.$emit("cancel", 123);
    },
    modify() {
      this.$emit("edit", this.list);
    }
  },
  props: ["modifyList", "isActive"],
  computed: {
    selectedList: function() {
      return this.modifyList;
    }
  },
  updated() {
    console.log("updated");
    this.list = this.modifyList;
  },
  beforeRouteEnter(to, from, next) {
    console.log("組件路由勾子:beforeRouteEnter");
    console.log(this); //undefined,不能用this來獲取vue實例
    next(vm => {
      console.log("組件路由勾子beforeRouteEnter的next");
      console.log(vm); //vm爲vue的實例
    });
  },
  beforeCreate() {
    console.log("beforeCreate");
  },
  created() {
    this.$nextTick(() => {
      console.log("nextTick");
    });
    console.log("created");
  },
  beforeMount() {
    console.log("beforeMount");
  },
  mounted() {
    console.log("mounted");
  },
  beforeUpdate() {
    console.log("beforeUpdate");
  },
  beforeDestroy: function() {
    console.log("beforeDestroy 銷燬前狀態===============》");
  },
  destroyed: function() {
    console.log("destroyed 銷燬完成狀態===============》");
  },
  beforeRouteLeave(to, from, next) {
    console.log("組件路由勾子:beforeRouteLeave");
    console.log(this); //能夠訪問vue實例
    next();
  }
};
</script>

<style>

</style>

最後後修改了router目錄下的index.js文件, 代碼以下:
index.js:

import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Table from '@/components/Table'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/helloWorld',
      name: 'HelloWorld',
      component: HelloWorld
    },
    {
      path: "/",
      name: "Table",
      component: Table
    }
  ]
})

這裏插一句, 分配路由的時候, 每一個name對應字符串, 會出如今F12的vue devtools的組建實例結構中, 以下圖所示.

clipboard.png

<Root>組件是組件, 其子組件是<App>就是, <Table>表單組件是<App>的路由組件, <Crud>是編輯模態框存在表單中.

export default {
  name: "Crud",
  data() {
    ...
  },

如上圖所示, 因爲Crud.vue定義的name是Crud因此在dev tools中顯示的是<Crud>, 相似的<App>, <Table>等組件的組件名字均可以自定義, 最好使命名能有自描述性.

my-project項目目錄的根目錄, 輸入命令npm start便可在本地8080打開項目.

如上組件層級圖所示, 這個項目一共三個組件來模擬表單操做。即:

App.vue項目自帶組件, 顯示一張Vue的圖片, 路由的根組件。
Table.vue是表單組件。以下圖所示:

clipboard.png

Crud.vue是一個編輯模態框以下圖所示:

clipboard.png

如今咱們應該對模塊化表單組件結構有了必定理解了, 先無視代碼細節, 來關注和第一章同樣的v-if聲明位置對於組件生命週期的影響,以表單的編輯功能爲例。

思考一下, 如今須要一個具備增刪改查功能的表單, 有表格主體, 有一個能添加一列信息的表格, 有一個能添加新信息和編輯修改信息模態框, 還有刪除按鈕, 和搜索框。

爲了編寫可複用的組件, 把一個簡易的表單分紅兩個組件, 一個是展現表單信息的表格Table.vue, 還有一個模態框Crud.vue, 當須要刪除一行數據的時候刪除對應行的數據(model), 搜索框忽略, 增長和編輯信息須要用到模態框, 那麼就涉及組件之間的通訊問題來傳遞數據(model), 那麼問題來了。

這兩個組件的組合方式是同級組件(非父子組件方式通訊), 仍是父子組件呢? 這你能夠自行設計。

本項目採用的父子組件的方式來組合兩個組件, 即在Table.vue中有以下代碼:

引入Crud.vue
<script>
import Crud from "./Crud";

export default {
  ...
  components: { Crud },
  ...
}
</script>

聲明該組件到模板中

<template>
  <div class="container">
  ...
  ...
    <crud ...></crud>
  </div>
</template>

這樣寫Crud.vue就是Table.vue的子組件。

Table.vue初始渲染數據(假設是從數據庫取出來的)以下:

<script>
export default {
  name: "Table",
  components: { Crud },
  data() {
    return {
      selectedList: {},
      isActive: false,
      header: ["id", "用戶名", "email", "性別", "省份", "愛好", "編輯"],
      students: [
        {
          username: "李明",
          email: "li@qq.com",
          sex: "男",
          province: "北京市",
          hobby: ["籃球", "編程"]
        },
        {
          username: "韓紅",
          email: "han@163.com",
          sex: "女",
          province: "河北省",
          hobby: ["彈琴", "插畫"]
        }
      ]
    };
  },
  ...
  ...
}
</script>

打開項目顯示以下:

clipboard.png

狀況1 v-if聲明在組件根元素上

<template>
  <div ... v-if="isActive">
  ...
  ...
  </div>
</template>

還記得以前的結論麼, v-if聲明在組件根元素上, 組件會在頁面渲染完成後處於mounted狀態!

讓咱們來看一看打開項目的初始生命週期:

clipboard.png

頁面的渲染順序是, 全部組件先從最高父級組件至最低子組件都先經歷beforeCreate => created => beforeMount => 暫停, 下一個組件! (App.vue => Table.vue => Crud.vue)

當最後一個子組件達到beforeMount,而後所有組件以相反的順序進入掛載狀態mounted。 (Crud.vue => Table.vue => App.vue)

事件循環的順序(nextTick)是父 => 子

瞭解nextTick

如今我想要編輯第一行學生名字叫作李明的數據, 點擊編輯按鈕拿到李明的數據, 要怎麼喚醒組件Crud而後傳遞李明的數據進行編輯呢?

記住, Props向下傳遞, 事件向上傳遞

props-events.png

Crud聲明


<script>
export default {
  ...
  props: ["modifyList", "isActive"],
  ...
} 
</script>

Table經過v-bind命令傳遞數據給子組件

<template>
  <div class="container">
  ...
  ...
    <crud ...v-bind:modify-list="selectedList" v-bind:is-active="isActive"...></crud>
  </div>
</template>

<script>
export default {
  ...
  selectedList: {},
  isActive: false
  ...
}
</script>

注意, HTML 特性是不區分大小寫的。因此,當使用的不是字符串模板時,camelCase (駝峯式命名) 的 prop 須要轉換爲相對應的 kebab-case (短橫線分隔式命名)。 例如, html標籤上的modify-list等價於props中的'modifyList'。

點擊編輯, 拿到李明對象數據以後賦值給selectedList, 父組件的selectedList發生了變化, 且isActive變化爲true, v-bind 來動態地將 prop(modifyList, isActive) 綁定到父組件的數據。每當父組件的數據變化時,該變化也會傳導給子組件Crud.vue, 子組件有段代碼this.list = this.modifyList, list是和模態框輸入框input進行雙向綁定v-model的數據。

<script>
export default {
  name: "Crud",
  data() {
    return {
      list: {}
    };
  },
 ...
 ...
}
</script>

子組件的isAcitve布爾值轉換爲true, 模態框顯示出來。

重點來了, 假設咱們不瞭解vue實例的生命週期, 不瞭解不一樣v-if聲明位置的生命週期, 那麼Crud.vue組件接收到的李明對象selectedListisActive的數據該在什麼哪一個生命週期進行賦值呢, created,mounted 仍是updated, 一個一個的試? 這對於高效簡潔的編程是有阻礙的, 在這個思惟胡亂的過程當中, 怎麼能寫出模塊化高的程序?怎麼保證程序沒有bug?

好了, 如今咱們知道了v-if聲明在組件根元素上在打開頁面時就出在生命週期的mounted狀態, 若是在created或mounted以前的生命週期會接收不到Table父組件傳遞的數據。 list爲空對象, 因此視圖顯示以下:

clipboard.png

mounted以後的生命週期beforeUpdate和updated能夠接收到數據。視圖顯示以下:
clipboard.png

而後點擊保存按鈕, 把Crud組件修改的數據list經過$emit事件發出數據, 父組件Table經過$on監聽並接收收到的數據list, Table組件根據list中的名字更新相應行的數據(model), 而後經過雙向綁定v-model把更新的樹反應在表格視圖中。

Crud.vue:

modify() {
      this.$emit("edit", this.list);
    }
Table.vue:

<template>
   ...
  <crud ... v-on:edit="editOne"...></crud>
</template>

<script>
...
editOne(student) {
      var isduplicated;
      // 爲了簡單, 添加和編輯都是用這個方法, 判斷保存的username是否重複, 若是重複則覆蓋, 不重複則新建一行,經過isduplicated若是爲true來判斷.
      isduplicated = this.students.some(aStudent => {
        return aStudent.username === student.username;
      });
      console.log(isduplicated);
      if (isduplicated) {
        this.students.forEach((element, index) => {
          if (student.username == element.username) {
            this.students[index] = student;
          }
        });
      } else {
        this.students.push(student);
      }

      this.toggleEdit();
    }
<script>
...

那麼有人要問了, 可不能夠把代碼該成這樣:

v-model="list.username" => v-model="modifyList.username"
v-model="list.email" => v-model="modifyList.email"
v-model="list.sex" => v-model="modifyList.sex"
v-model="list.province" => v-model="modifyList.province"
v-model="list.hobby" => v-model="modifyList.hobby"

這樣就不用理會生命週期, 直接顯示並修改prop數據modifyList, 並返回給父組件。 首先, 這樣在技術上時可行的, 不會出現問題。 可是, Prop 是單向綁定的, 不該該在子組件內部改變 prop, 請遵照規範。

單向數據流

Prop 是單向綁定的:當父組件的屬性變化時,將傳導給子組件,可是反過來不會。這是爲了防止子組件無心間修改了父組件的狀態,來避免應用的數據流變得難以理解。

另外,每次父組件更新時,子組件的全部 prop 都會更新爲最新值。這意味着你不該該在子組件內部改變 prop。若是你這麼作了,Vue 會在控制檯給出警告。

在兩種狀況下,咱們很容易忍不住想去修改 prop 中數據:

Prop 做爲初始值傳入後,子組件想把它看成局部數據來用;

Prop 做爲原始數據傳入,由子組件處理成其它數據輸出。

對這兩種狀況,正確的應對方式是:

......
......

以上單向數據流內容, 來自官方文檔組件章節。

還有一個組件間傳遞的數據爲對象的問題, 當點擊編輯按鈕時, 以這句代碼this.selectedList = this.students[index]傳遞李明的對象數據, 表單會出現一個問題, 點擊修改李明行的按鈕, 狀況正常, 以下圖:

clipboard.png

而後隨意修改數據,以下圖

clipboard.png

注意到, 表格中的用戶名也跟着變化。 再來1張圖:

clipboard.png

關鍵的是, 點擊美容會被修改:

clipboard.png

這是由於在 JavaScript 中對象和數組是引用類型,指向同一個內存空間,若是 prop 是一個對象或數組,在子組件內部改變它會影響父組件的狀態。

目前的解決方法是克隆一個內存空間中新生成對象李明, 在進行數據傳遞就不會出現問題。

this.selectedList = JSON.parse(JSON.stringify(this.students[index]))

即先把組件Table的李明對象轉成JSON字符串, 在轉回來, 達到"克隆"。 還有其餘方法麼? 歡飲討論。

問題(待解決): 在vue的編碼規範中有以下聲明:

傳遞過於複雜的對象使得咱們不可以清楚的知道哪些屬性或方法被自定義組件使用,這使得代碼難以重構和維護。

因此, 我考慮Table.vue傳遞李明的字符串, edit()方法修改以下:

edit(index) {
   this.selectedList = JSON.stringify(this.students[index]);
   this.toggleEdit();
}

而後再在Crud.vue中解析成對象, 修改以下:

updated() {
  this.list = JSON.parse(this.modifyList);
}

此時點擊一行數據進行編輯, 瀏覽器會進入死循環, 卡死。 解析放到beforeupdate

beforeupdate() {
  this.list = JSON.parse(this.modifyList);
}

點擊編輯, 循壞100來此報錯:

clipboard.png

哪位大神能詳盡的解釋一下麼? updated狀態下進行解析生成新對象, 組件Crud.vue又會進入beforeUpdate => updated狀態又成新解析的對象, 無限循環直到內存溢出, 那麼爲何解析放在updated中回掉瀏覽器器會直接卡死, 而beforeUpdated中遞歸會停止並報錯?

狀況2 v-if聲明在組件上

<template>
  <div class="container">
  ...
  ...
    <crud ...v-if="isActive"...></crud>
  </div>
</template>

還要刪除組件Crud.vue根組件, <div>元素標籤上的v-if="isActive"

那麼還記得以前的結論麼, v-if聲明在組件上, 組件會在v-iftrue後頁面纔開始渲染顯示到DOM節點樹, 並顯示相應的視圖, 且組件關閉後後被銷燬

讓咱們來看一看打開項目的初始生命週期:

clipboard.png

能夠看到沒有組件Crud.vue的控制檯數據。

如今, 傳遞的數據給list能夠放在created beforeMount mounted任一輩子命週期中, 項目就能順利運行, 可是不能放在beforeUpdate updated生命週期中, 由於數據動態綁定的關係, 組件Crud.vue渲染到生命週期的created狀態時props中定義的數據就已經被傳遞數據並賦值了, 因此點擊編輯打開模態框並不會觸發beforeUpdate updated的生命週期, 在裏面進行list是賦值行爲是沒有意義的。

如今能夠遵照規範, 讓組件Table.vue能夠傳遞李明的字符串, 再在Crud.vue中解析成對象, 由於上述原理, 解析的新對象不會遞歸出發組件beforeUpdate updated的生命週期而進入死循環。 爲了理解更深你不妨試試。

vue組件實例的生命週期還有須要探究的地方。

本章github源碼連接

相關文章
相關標籤/搜索