1. 程式人生 > >vue系列---理解Vue中的computed,watch,methods的區別及原始碼實現(六)

vue系列---理解Vue中的computed,watch,methods的區別及原始碼實現(六)

閱讀目錄

  • 一. 理解Vue中的computed用法
  • 二:computed 和 methods的區別?
  • 三:Vue中的watch的用法
  • 四:computed的基本原理及原始碼實現
回到頂部

一. 理解Vue中的computed用法

computed是計算屬性的; 它會根據所依賴的資料動態顯示新的計算結果, 該計算結果會被快取起來。computed的值在getter執行後是會被快取的。如果所依賴的資料發生改變時候, 就會重新呼叫getter來計算最新的結果。

下面我們根據官網中的demo來理解下computed的使用及何時使用computed。

computed設計的初衷是為了使模板中的邏輯運算更簡單, 比如在Vue模板中有很多複雜的資料計算的話, 我們可以把該計算邏輯放入到computed中去計算。

下面我們看下官網中的一個demo如下:

<!DOCTYPE html>
<html>
<head>
  <title>vue</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    {{ msg.split('').reverse().join('') }}
  </div>
  <script type="text/javascript">
    new Vue({
      el: '#app',
      data: {
        msg: 'hello'
      }
    });
  </script>
</body>
</html>

如上程式碼, 我們的data屬性中的msg預設值為 'hello'; 然後我們在vue模板中會對該資料值進行反轉操作後輸出資料, 因此在頁面上就會顯示 'olleh'; 這樣的資料。這是一個簡單的運算, 但是如果頁面中的運算比這個還更復雜的話, 這個時候我們可以使用computed來進行計算屬性值, computed的目的就是能使模板中的運算邏輯更簡單。因此我們現在需要把上面的程式碼改寫成下面如下程式碼:

<!DOCTYPE html>
<html>
<head>
  <title>vue</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <p>原來的資料: {{ msg }}</p>
    <p>反轉後的資料為: {{ reversedMsg }}</p>
  </div>
  <script type="text/javascript">
    var vm = new Vue({
      el: '#app',
      data: {
        msg: 'hello'
      },
      computed: {
        reversedMsg() {
          // this 指向 vm 例項
          return this.msg.split('').reverse().join('')
        }
      }
    });
  </script>
</body>
</html>

如上程式碼, 我們在computed中聲明瞭一個計算屬性 reversedMsg。我們提供的 reversedMsg 函式, 將用作屬性 vm.reversedMsg 的getter函式; 我們可以在上面例項化後代碼中, 列印如下資訊:

console.log(vm);

列印資訊如下所示, 我們可以看到 vm.reversedMsg = 'olleh'; 

我們也可以開啟控制檯, 當我們修改 vm.msg 的值後, vm.reversedMsg 的值也會發生改變,如下控制檯列印的資訊可知:

如上列印的資訊我們可以看得到, 我們的 vm.reversedMsg 的值依賴於 vm.msg 的值,當vm.msg的值發生改變時, vm.reversedMsg 的值也會得到更新。

computed 應用場景

1. 適用於一些重複使用資料或複雜及費時的運算。我們可以把它放入computed中進行計算, 然後會在computed中快取起來, 下次就可以直接獲取了。

2. 如果我們需要的資料依賴於其他的資料的話, 我們可以把該資料設計為computed中。

回到頂部

二:computed 和 methods的區別?

如上demo程式碼, 如果我們通過在表示式中呼叫方法也可以達到同樣的效果, 現在我們把程式碼改成方法, 如下程式碼:
<!DOCTYPE html>
<html>
<head>
  <title>vue</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <p>原來的資料: {{ msg }}</p>
    <p>反轉後的資料為: {{ reversedMsg() }}</p>
  </div>
  <script type="text/javascript">
    var vm = new Vue({
      el: '#app',
      data: {
        msg: 'hello'
      },
      /*
      computed: {
        reversedMsg() {
          // this 指向 vm 例項
          return this.msg.split('').reverse().join('')
        }
      }
      */
      methods: {
        reversedMsg() {
          // this 指向 vm 例項
          return this.msg.split('').reverse().join('')
        }
      }
    });
    console.log(vm);
  </script>
</body>
</html>

如上程式碼, 我們反轉後的資料在模板中呼叫的是方法 reversedMsg(); 該方法在methods中也定義了。那麼也可以實現同樣的效果, 那麼他們之間到底有什麼區別呢?

區別是:

1. computed 是基於響應性依賴來進行快取的。只有在響應式依賴發生改變時它們才會重新求值, 也就是說, 當msg屬性值沒有發生改變時, 多次訪問 reversedMsg 計算屬性會立即返回之前快取的計算結果, 而不會再次執行computed中的函式。但是methods方法中是每次呼叫, 都會執行函式的, methods它不是響應式的。
2. computed中的成員可以只定義一個函式作為只讀屬性, 也可以定義成 get/set變成可讀寫屬性, 但是methods中的成員沒有這樣的。

我們可以再看下如下demo:

<!DOCTYPE html>
<html>
<head>
  <title>vue</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <div>第一次呼叫computed屬性: {{ reversedMsg }}</div>
    <div>第二次呼叫computed屬性: {{ reversedMsg }}</div>
    <div>第三次呼叫computed屬性: {{ reversedMsg }}</div>
    <!-- 下面是methods呼叫 -->
    <div>第一次呼叫methods方法: {{ reversedMsg1() }}</div>
    <div>第二次呼叫methods方法: {{ reversedMsg1() }}</div>
    <div>第三次呼叫methods方法: {{ reversedMsg1() }}</div>
  </div>
  <script type="text/javascript">
    var vm = new Vue({
      el: '#app',
      data: {
        msg: 'hello'
      },
      computed: {
        reversedMsg() {
          console.log(1111);
          // this 指向 vm 例項
          return this.msg.split('').reverse().join('')
        }
      },
      methods: {
        reversedMsg1() {
          console.log(2222);
          // this 指向 vm 例項
          return this.msg.split('').reverse().join('')
        }
      }
    });
    console.log(vm);
  </script>
</body>
</html>

執行後的結果如下所示:

如上程式碼我們可以看到, 在computed中有屬性reversedMsg, 然後在該方法中會列印 1111; 資訊出來, 在methods中的方法reversedMsg1也會列印 2222 資訊出來, 但是在computed中, 我們除了第一次之後,再次獲取reversedMsg值後拿得是快取裡面的資料, 因此就不會再執行該reversedMsg函數了。但是在methods中, 並沒有快取, 每次執行reversedMsg1()方法後,都會列印資訊。
從上面截圖資訊我們就可以驗證的。

那麼我們現在再來理解下快取的作用是什麼呢? computed為什麼需要快取呢? 我們都知道我們的http也有快取, 對於一些靜態資源, 我們nginx伺服器會快取我們的靜態資源,如果靜態資源沒有發生任何改變的話, 會直接從快取裡面去讀取,這樣就不會重新去請求伺服器資料, 也就是避免了一些無畏的請求, 提高了訪問速度, 優化了使用者體驗。

對於我們computed的也是一樣的。如上面程式碼, 我們呼叫了computed中的reversedMsg方法一共有三次,如果我們也有上百次呼叫或上千次呼叫的話, 如果依賴的資料沒有改變, 那麼每次呼叫都要去計算一遍, 那麼肯定會造成很大的浪費。因此computed就是來優化這件事的。

回到頂部

三:Vue中的watch的用法

watch它是一個對data的資料監聽回撥, 當依賴的data的資料變化時, 會執行回撥。在回撥中會傳入newVal和oldVal兩個引數。
Vue實列將會在例項化時呼叫$watch(), 他會遍歷watch物件的每一個屬性。

watch的使用場景是:當在data中的某個資料發生變化時, 我們需要做一些操作, 或者當需要在資料變化時執行非同步或開銷較大的操作時. 我們就可以使用watch來進行監聽。
watch普通監聽和深度監聽

如下普通監聽資料的基本測試程式碼如下:

<!DOCTYPE html>
<html>
<head>
  <title>vue</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <p>空智個人資訊情況: {{ basicMsg }}</p>
    <p>空智今年的年齡: <input type="text" v-model="age" /></p>
  </div>
  <script type="text/javascript">
    var vm = new Vue({
      el: '#app',
      data: {
        basicMsg: '',
        age: 31,
        single: '單身'
      },
      watch: {
        age(newVal, oldVal) {
          this.basicMsg = '今年' + newVal + '歲' + ' ' + this.single;
        }
      }
    });
  </script>
</body>
</html>

顯示效果如下:

如上程式碼, 當我們在input輸入框中輸入年齡後, 比如32, 那麼watch就能對 'age' 這個屬性進行監聽,當值發生改變的時候, 就會把最新的計算結果賦值給 'basicMsg' 屬性值, 因此最後在頁面上就會顯示 'basicMsg' 屬性值了。

理解handler方法及immediate屬性

如上watch有一個特點是: 第一次初始化頁面的時候, 是不會去執行age這個屬性監聽的, 只有當age值發生改變的時候才會執行監聽計算. 因此我們上面第一次初始化頁面的時候, 'basicMsg' 屬性值預設為空字串。那麼我們現在想要第一次初始化頁面的時候也希望它能夠執行 'age' 進行監聽, 最後能把結果返回給 'basicMsg' 值來。因此我們需要修改下我們的 watch的方法,需要引入handler方法和immediate屬性, 程式碼如下所示:

 

<!DOCTYPE html>
<html>
<head>
  <title>vue</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <p>空智個人資訊情況: {{ basicMsg }}</p>
    <p>空智今年的年齡: <input type="text" v-model="age" /></p>
  </div>
  <script type="text/javascript">
    var vm = new Vue({
      el: '#app',
      data: {
        basicMsg: '',
        age: 31,
        single: '單身'
      },
      watch: {
        age: {
          handler(newVal, oldVal) {
            this.basicMsg = '今年' + newVal + '歲' + ' ' + this.single;
          },
          immediate: true
        }
      }
    });
  </script>
</body>
</html>

如上程式碼, 我們給我們的age屬性綁定了一個handler方法。其實我們之前的watch當中的方法預設就是這個handler方法。但是在這裡我們使用了immediate: true; 屬性,含義是: 如果在watch裡面聲明瞭age的話, 就會立即執行裡面的handler方法。如果 immediate 值為false的話,那麼效果就和之前的一樣, 就不會立即執行handler這個方法的。因此設定了 immediate:true的話,第一次頁面載入的時候也會執行該handler函式的。即第一次 basicMsg 有值。

因此第一次頁面初始化效果如下:

理解deep屬性

watch裡面有一個屬性為deep,含義是:是否深度監聽某個物件的值, 該值預設為false。

如下測試程式碼:

<!DOCTYPE html>
<html>
<head>
  <title>vue</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <p>空智個人資訊情況: {{ basicMsg }}</p>
    <p>空智今年的年齡: <input type="text" v-model="obj.age" /></p>
  </div>
  <script type="text/javascript">
    var vm = new Vue({
      el: '#app',
      data: {
        obj: {
          basicMsg: '',
          age: 31,
          single: '單身'
        }
      },
      watch: {
        'obj': {
          handler(newVal, oldVal) {
            this.basicMsg = '今年' + newVal.age + '歲' + ' ' + this.obj.single;
          },
          immediate: true,
          deep: true // 需要新增deep為true即可對obj進行深度監聽
        }
      }
    });
  </script>
</body>
</html>

如上測試程式碼, 如果我們不把 deep: true新增的話,當我們在輸入框中輸入值的時候,改變obj.age值後,obj物件中的handler函式是不會被執行到的。受JS的限制, Vue不能檢測到物件屬性的新增或刪除的。它只能監聽到obj這個物件的變化,比如說對obj賦值操作會被監聽到。比如在mounted事件鉤子函式中對我們的obj進行重新賦值操作, 如下程式碼:

mounted() {
  this.obj = {
    age: 22,
    basicMsg: '',
    single: '單身'
  };
}

最後我們的頁面會被渲染到 age 為 22; 因此這樣我們的handler函式才會被執行到。如果我們需要監聽物件中的某個屬性值的話, 我們可以使用 deep設定為true即可生效。deep實現機制是: 監聽器會一層層的往下遍歷, 給物件的所有屬性都加上這個監聽器。當然效能開銷會非常大的。

當然我們可以直接對物件中的某個屬性進行監聽的,比如就對 'obj.age' 來進行監聽, 如下程式碼也是可以生效的。

<!DOCTYPE html>
<html>
<head>
  <title>vue</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <p>空智個人資訊情況: {{ basicMsg }}</p>
    <p>空智今年的年齡: <input type="text" v-model="obj.age" /></p>
  </div>
  <script type="text/javascript">
    var vm = new Vue({
      el: '#app',
      data: {
        obj: {
          basicMsg: '',
          age: 31,
          single: '單身'
        }
      },
      watch: {
        'obj.age': {
          handler(newVal, oldVal) {
            this.basicMsg = '今年' + newVal + '歲' + ' ' + this.obj.single;
          },
          immediate: true,
          // deep: true // 需要新增deep為true即可對obj進行深度監聽
        }
      }
    });
  </script>
</body>
</html>

watch 和 computed的區別是:

相同點:他們兩者都是觀察頁面資料變化的。

不同點:computed只有當依賴的資料變化時才會計算, 當資料沒有變化時, 它會讀取快取資料。
watch每次都需要執行函式。watch更適用於資料變化時的非同步操作。

回到頂部

四:computed的基本原理及原始碼實現

computed上面我們也已經說過, 它設計的初衷是: 為了使模板中的邏輯運算更簡單。它有兩大優勢:

1. 使模板中的邏輯更清晰, 方便程式碼管理。
2. 計算之後的值會被快取起來, 依賴的data值改變後會重新計算。

因此我們要理解computed的話, 我們只需要理解如下幾個問題:

1. computed是如何初始化的, 初始化之後做了那些事情?
2. 為什麼我們改變了data中的屬性值後, computed會重新計算, 它是如何實現的?
3. computed它是如何快取值的, 當我們下次訪問該屬性的時候, 是怎樣讀取快取資料的?

理解Vue原始碼中computed實現流程 

computed初始化

在理解如何初始化之前, 我們來看如下簡單的demo, 然後一步步看看他們的原始碼是如何做的。

<!DOCTYPE html>
<html>
<head>
  <title>vue</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <p>原來的資料: {{ msg }}</p>
    <p>反轉後的資料為: {{ reversedMsg }}</p>
  </div>
  <script type="text/javascript">
    var vm = new Vue({
      el: '#app',
      data: {
        msg: 'hello'
      },
      computed: {
        reversedMsg() {
          // this 指向 vm 例項
          return this.msg.split('').reverse().join('')
        }
      }
    });
  </script>
</body>
</html>

如上程式碼, 我們看到程式碼入口就是vue的例項化, new Vue({}) 作為入口, 因此會呼叫 vue/src/core/instance/index.js 中的init函式程式碼, 如下所示:

......... 更多程式碼省略
/*
 @param {options} Object
 options = {
   el: '#app',
   data: {
     msg: 'hello'
   },
   computed: {
     reversedMsg() {
       // this 指向 vm 例項
       return this.msg.split('').reverse().join('')
     }
   }
 };
*/
import { initMixin } from './init'
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)
}
initMixin(Vue);

..... 更多程式碼省略

export default Vue;

如上程式碼, 會執行 this._init(options); 方法內部,因此會呼叫 vue/src/core/instance/init.js 檔案中的_init方法, 基本程式碼如下所示:

import { initState } from './state';
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    .... 更多程式碼省略
    initState(vm);
    .... 更多程式碼省略
  }
}

因此繼續執行 initState(vm); 中的程式碼了, 因此會呼叫 vue/src/core/instance/state.js 中的檔案程式碼, 基本程式碼如下:

import config from '../config'
import Watcher from '../observer/watcher'
import Dep, { pushTarget, popTarget } from '../observer/dep'

..... 更多程式碼省略
/*
 @param {vm}
 vm = {
   $attrs: {},
   $children: [],
   $listeners: {},
   $options: {
     components: {},
     computed: {
       reversedMsg() {
         // this 指向 vm 例項
         return this.msg.split('').reverse().join('')
       }
     },
     el: '#app',
     ..... 更多屬性值
   },
   .... 更多屬性
 };
*/
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

..... 更多程式碼省略

如上程式碼, 形參上的vm引數值基本值如上註釋。程式碼內部先判斷 vm.$options.props 是否有該屬性, 有的話, 就呼叫 initProps()方法進行初始化, 接著會判斷 vm.$options.methods; 是否有該方法, 有的話,呼叫 initMethods() 方法進行初始化。這些所有的我們先不看, 我們這邊最主要的是看 if (opts.computed) initComputed(vm, opts.computed) 這句程式碼; 判斷 vm.$options.computed 是否有, 如果有的話, 就執行 initComputed(vm, opts.computed); 函式。因此我們找到 initComputed函式程式碼如下:

/*
 @param {vm} 值如下:
 vm = {
   $attrs: {},
   $children: [],
   $listeners: {},
   $options: {
     components: {},
     computed: {
       reversedMsg() {
         // this 指向 vm 例項
         return this.msg.split('').reverse().join('')
       }
     },
     el: '#app',
     ..... 更多屬性值
   },
   .... 更多屬性
 };
 @param {computed} Object
 computed = {
   reversedMsg() {
     // this 指向 vm 例項
     return this.msg.split('').reverse().join('')
   }
 };
*/

const computedWatcherOptions = { lazy: true };
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null);
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }
    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}

如上程式碼, 首先使用 Object.create(null); 建立一個空物件, 分別賦值給 watchers; 和 vm._computedWatchers; 接著執行程式碼:

const isSSR = isServerRendering(); 判斷是否是伺服器端渲染, 我們這邊肯定不是伺服器端渲染,因此 const isSSR = false;

接著使用 for in 迴圈遍歷 computed; 程式碼:for (const key in computed) { const userDef = computed[key] };

接著判斷 userDef 該值是否是一個函式, 或者也可以是一個物件, 因此我們可以推斷我們的 computed 可以如下編寫程式碼:

computed: {
  reversedMsg() {
    // this 指向 vm 例項
    return this.msg.split('').reverse().join('')
  }
}

或如下初始化程式碼也是可以的:

computed: {
  reversedMsg: {
    get() {
      // this 指向 vm 例項
      return this.msg.split('').reverse().join('')
    }
  }
}

當我們拿不到我們的getter的時候, vue會報出一個警告資訊。

接著程式碼, 如下所示:

if (!isSSR) {
  // create internal watcher for the computed property.
  watchers[key] = new Watcher(
    vm,
    getter || noop,
    noop,
    computedWatcherOptions
  )
}

如上程式碼, 我們會根據computed中的key來例項化watcher,因此我們可以理解為其實computed就是watcher的實現, 通過一個釋出訂閱模式來監聽的。給Watch方法傳遞了四個引數, 分別為VM實列, 上面我們獲取到的getter方法, noop 是一個回撥函式。computedWatcherOptions引數我們在原始碼初始化該值為:const computedWatcherOptions = { lazy: true }; 我們再來看下 Watcher函式程式碼, 該函式程式碼在:

vue/src/core/observer/watcher.js 中; 基本原始碼如下:

/*
 vm = {
   $attrs: {},
   $children: [],
   $listeners: {},
   $options: {
     components: {},
     computed: {
       reversedMsg() {
         // this 指向 vm 例項
         return this.msg.split('').reverse().join('')
       }
     },
     el: '#app',
     ..... 更多屬性值
   },
   .... 更多屬性
 };
 expOrFn = function reversedMsg() {}; expOrFn 是我們上面獲取到的getter函式.
 cb的值是一個回撥函式。
 options = {lazy: true};
 isRenderWatcher = undefined;
*/
export default class Watcher {
  ....
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    /*
     當前的watcher新增到vue的實列上, 因此:
     vm._watchers = [
      Watcher 
     ];
     即 vm._watchers[0].vm = {
       $attrs: {},
       $children: [],
       $listeners: {},
       $options: {
         components: {},
         computed: {
           reversedMsg() {}
         }
       }
     }
     ....
    */
    vm._watchers.push(this);
    // options
    /*
      options = {lazy: true};
      因此:
      // 如果deep為true的話,會對getter返回的物件再做一次深度的遍歷
      this.deep = !!options.deep; 即 this.deep = false; 
      // user 是用於標記這個監聽是否由使用者通過$watch呼叫的
      this.user = !!options.user; 即: this.user = false;
      
      // lazy用於標記watcher是否為懶執行,該屬性是給 computed data 用的,當 data 中的值更改的時候,不會立即計算 getter 
      // 獲取新的數值,而是給該 watcher 標記為dirty,當該 computed data 被引用的時候才會執行從而返回新的 computed 
      // data,從而減少計算量。
      
      this.lazy = !!options.lazy; 即: this.lazy = true;
      
      // 表示當 data 中的值更改的時候,watcher 是否同步更新資料,如果是 true,就會立即更新數值,否則在 nextTick 中更新。
      
      this.sync = !!options.sync; 即: this.sync = false;
      this.before = options.before; 即: this.before = undefined;
    */
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    // cb 為回撥函式
    this.cb = cb
    this.id = ++uid // uid for batching 
    this.active = true
    // this.dirty = true;
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set();
    /*
     把函式轉換成字串的形式(不是正式環境下)
     this.expression = "reversedMsg() { return this.msg.split('').reverse().join('') }"
    */
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    /*
     判斷expOrFn是否是一個函式, 如果是一個函式, 直接賦值給 this.getter;
     否則的話, 它是一個表示式的話, 比如 'a.b.c' 這樣的,因此呼叫 this.getter = parsePath(expOrFn); 
     parsePath函式的程式碼在:vue/src/core/util/lang.js 中。
    */
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    // 不是懶載入型別呼叫get
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}

因此如上程式碼執行完成後, 我們的 vue/src/core/instance/state.js 中的 initComputed() 函式中,如下這句程式碼執行後:

watchers[key] = new Watcher(
    vm,
    getter || noop,
    noop,
    computedWatcherOptions
  );

watchers["reversedMsg"] 的值變為如下:

watchers["reversedMsg"] = {
  active: true,
  before: false,
  cb: f noop(a, b, c) {},
  deep: false,
  depIds: Set,
  deps: [],
  dirty: true,
  expression: 'reversedMsg() { return this.msg.split('').reverse().join('') }',
  getter: f reversedMsg() { return this.msg.split('').reverse().join('') },
  id: 1,
  lazy: true,
  newDepIds: Set,
  newDeps: [],
  sync: false,
  user: false,
  value: undefined,
  vm: {
    // Vue的實列物件
  }
};

如果computed中有更多的方法的話, 就會返回更多的 watchers['xxxx'] 這樣的物件了。

現在我們再回到 vue/src/core/instance/state.js 中的 initComputed() 函式中,繼續執行如下程式碼:

// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
// 如果 computed中的key沒有在vm中, 則通過defineComputed掛載上去。第一次執行的時候, vm中沒有該屬性的
if (!(key in vm)) {
  defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
  // 如果我們的 computed中的key在data中或在props有同名的屬性的話,則直接發出警告。
  if (key in vm.$data) {
    warn(`The computed property "${key}" is already defined in data.`, vm)
  } else if (vm.$options.props && key in vm.$options.props) {
    warn(`The computed property "${key}" is already defined as a prop.`, vm)
  }
}

現在我們繼續檢視defineComputed函式程式碼如下:

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

如上程式碼, 首先執行 const shouldCache = !isServerRendering(); 判斷是不是伺服器端渲染, 我們這邊肯定不是的, 因此 shouldCache 為 true, 該引數的作用是否需要被快取資料, 為true是需要被快取的。也就是說我們的這裡的computed只要不是伺服器端渲染的話, 預設會快取資料的。
接著會判斷 userDef 是否是一個函式, 如果是函式的話,說明是我們的computed的用法。因此 sharedPropertyDefinition.get = createComputedGetter(key); 的返回值。如果不是函式, 有可能就是表示式, 比如 watch 中的監聽 'a.b.c' 這樣的話, 就執行else語句程式碼了。

現在我們來看下 createComputedGetter 函式程式碼如下:

/*
 @param key = "reversedMsg"
*/
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

因此 sharedPropertyDefinition.get,其實返回的是 computedGetter()函式的,即: function computedGetter() {};
最後我們再回到 export function defineComputed() 函式程式碼中:執行程式碼:Object.defineProperty(target, key, sharedPropertyDefinition); 使用Object.defineProperty來監聽物件屬性值的變化;

/*
 @param {target} vm實列物件
 @param {key} "reversedMsg"
 @param {sharedPropertyDefinition}
 sharedPropertyDefinition = {
   configurable: true,
   enumerable: true,
   get: function computedGetter () {
      var watcher = this._computedWatchers && this._computedWatchers[key];
      if (watcher) {
        if (watcher.dirty) {
          watcher.evaluate();
        }
        if (Dep.target) {
          watcher.depend();
        }
        return watcher.value
      }
    },
    set: function noop(a, b, c) {}
 }
*/
Object.defineProperty(target, key, sharedPropertyDefinition); 

如上程式碼我們可以看到, 我們會使用 Object.defineProperty來監聽Vue實列上的 reversedMsg 屬性. 然後會執行sharedPropertyDefinition中的get或set函式的。因此只要我們的data物件中的某個屬性發生改變的話, 我們的reversedMsg方法中依賴了該屬性的話, 也會呼叫sharedPropertyDefinition方法中的get/set方法的。
但是在我們的頁面第一次初始化的時候, 我們要如何初始化執行 computed中的對應方法呢?
因此我們現在需要再回到 vue/src/core/instance/init.js 中的_init()方法中,接著需要看下面的程式碼; 如下程式碼:

Vue.prototype._init = function (options?: Object) {
  ...... 更多的程式碼已省略
  /*
   vm = {
     $attrs: {},
     $children: [],
     $listeners: {},
     $options: {
       components: {},
       computed: {
         reversedMsg: f reversedMsg(){}
       },
       data: function mergedInstanceDataFn () {
          .....
       },
       el: '#app',
       ..... 更多引數
     }
   };
  */
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
  ...... 更多的程式碼已省略
}

因此執行 vm.$mount(vm.$options.el); 這句程式碼了; 該程式碼的作用是對我們的頁面中的模板進行編譯操作。
該程式碼在 vue/src/platforms/web/entry-runtime-with-compiler.js 中。具體的內部程式碼我們先不看, 在下一個章節中我們會有講解該內部程式碼的。我們只需要看該js中的最後一句程式碼即可, 如下程式碼:

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
  ): Component{
  ..... 省略很多很多程式碼
  return mount.call(this, el, hydrating);
}

最後一句程式碼, 會呼叫 mount.call(this, el, hydrating); 這句程式碼; 因此會找到 vue/src/platforms/web/runtime/index.js 中的程式碼:

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

接著執行程式碼 mountComponent(this, el, hydrating); 會找到 vue/src/core/instance/lifecycle.js 中程式碼

export function mountComponent() {
  ..... 省略很多程式碼

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

  .... 省略很多程式碼
}

在這裡我們就可以看到, 我們對Watcher進行實列化了, new Watcher(); 因此我們又回到了vue/src/core/observer/watcher.js 中對程式碼進行初始化;

export default class Watcher {
  ..... 省略很多程式碼
  constructor() {
    .... 省略很多程式碼
  this.value = this.lazy ? undefined : this.get();
  }
}

此時this.lazy = false; 因此會執行 this.get()函式, 該函式程式碼如下:

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

也就是說執行了 this.getter.call(vm, vm)方法; 最後就執行到 vue/src/core/instance/state.js中如下程式碼:

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

因此最後就返回 watcher.value 值了, 就是我們的computed的reversedMsg返回的值了。如上就是整個computed執行的過程,它最主要也是通過事件的釋出-訂閱模式來監聽物件資料的變化實現的。如上只是簡單的理解下原始碼如何做到的, 等稍後會有章節 講解 new Vue({}) 實列話,到底做了那些事情, 我們會深入講解到的。
對於methods及watcher也是一樣的,後續會更深入的講解到。