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

閱讀目錄javascript

一. 理解Vue中的computed用法html

computed是計算屬性的; 它會根據所依賴的數據動態顯示新的計算結果, 該計算結果會被緩存起來。computed的值在getter執行後是會被緩存的。若是所依賴的數據發生改變時候, 就會從新調用getter來計算最新的結果。vue

下面咱們根據官網中的demo來理解下computed的使用及什麼時候使用computed。java

computed設計的初衷是爲了使模板中的邏輯運算更簡單, 好比在Vue模板中有不少複雜的數據計算的話, 咱們能夠把該計算邏輯放入到computed中去計算。nginx

下面咱們看下官網中的一個demo以下:web

<!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的目的就是能使模板中的運算邏輯更簡單。所以咱們如今須要把上面的代碼改寫成下面以下代碼:express

<!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 的值也會發生改變,以下控制檯打印的信息可知:app

如上打印的信息咱們能夠看獲得, 咱們的 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也是同樣的,後續會更深刻的講解到。

相關文章
相關標籤/搜索