面對 this 指向丟失,尤雨溪在 Vuex 源碼中是怎麼處理的

1. 前言

你們好,我是若川。歡迎加我微信ruochuan12,關注個人公衆號若川視野,長期交流學習。很久之前我有寫過《面試官問系列》,旨在幫助讀者提高JS基礎知識,包含new、call、apply、this、繼承相關知識。其中寫了 面試官問:this 指向 文章。在掘金等平臺收穫了還算不錯的反饋。html

最近有小夥伴看個人 Vuex源碼 文章,提到有一處this指向有點看不懂(好不容易終於有人看個人源碼文章了,感動的要流淚了^_^)。因而我寫篇文章答疑解惑,簡單再說說 this 指向和尤大在 Vuex 源碼中是怎麼處理 this 指向丟失的。前端

2. 對象中的this指向

var person = {
  name: '若川',
  say: function(text){
    console.log(this.name + ', ' + text);
  }
}
console.log(person.name);
console.log(person.say('在寫文章')); // 若川, 在寫文章
var say = person.say;
say('在寫文章'); // 這裏的this指向就丟失了,指向window了。(非嚴格模式)
複製代碼

3. 類中的this指向

3.1 ES5

// ES5
var Person = function(){
  this.name = '若川';
}
Person.prototype.say = function(text){
  console.log(this.name + ', ' + text);
}
var person = new Person();
console.log(person.name); // 若川
console.log(person.say('在寫文章'));
var say = person.say;
say('在寫文章'); // 這裏的this指向就丟失了,指向 window 了。
複製代碼

3.2 ES6

// ES6
class Person{
  construcor(name = '若川'){
     this.name = name;
  }
  say(text){
    console.log(`${this.name}, ${text}`);
  }
}
const person = new Person();
person.say('在寫文章')
// 解構
const { say } = person;
say('在寫文章'); // 報錯 this ,由於ES6 默認啓用嚴格模式,嚴格模式下指向 undefined
複製代碼

4. 尤大在Vuex源碼中是怎麼處理的

先看代碼vue

class Store{
  constructor(options = {}){
     this._actions = Object.create(null);
  // bind commit and dispatch to self
      // 給本身 綁定 commit 和 dispatch
      const store = this
      const { dispatch, commit } = this
      // 爲什麼要這樣綁定 ?
      // 說明調用commit和dispach 的 this 不必定是 store 實例
      // 這是確保這兩個函數裏的this是store實例
      this.dispatch = function boundDispatch (type, payload) {
        return dispatch.call(store, type, payload)
      }
      this.commit = function boundCommit (type, payload, options) {
        return commit.call(store, type, payload, options)
      }
  }
  dispatch(){
     console.log('dispatch', this);
  }
  commit(){
     console.log('commit', this);
  }
}
const store = new Store();
store.dispatch(); // 輸出結果 this 是什麼呢?

const { dispatch, commit } = store;
dispatch(); // 輸出結果 this 是什麼呢?
commit();  // 輸出結果 this 是什麼呢?
複製代碼

輸出結果截圖

結論:很是巧妙的用了calldispatchcommit函數的this指向強制綁定到store實例對象上。若是不這麼綁定就報錯了。git

4.1 actions 解構 store

其實Vuex源碼裏就有上面解構const { dispatch, commit } = store;的寫法。想一想咱們平時是如何寫actions的。actions中自定義函數的第一個參數其實就是 store 實例。github

這時咱們翻看下actions文檔https://vuex.vuejs.org/zh/guide/actions.html面試

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})
複製代碼

也能夠用解構賦值的寫法。vuex

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}
複製代碼

有了Vuex源碼構造函數裏的call綁定,這樣this指向就被修正啦~不得不說祖師爺就是厲害。這一招,你們能夠免費學走~segmentfault

接着咱們帶着問題,爲啥上文中的context就是store實例,有dispatchcommit這些方法呢。繼續往下看。瀏覽器

4.2 爲何 actions 對象裏的自定義函數 第一個參數就是 store 實例。

如下是簡單源碼,有縮減,感興趣的能夠看個人文章 Vuex 源碼文章微信

class Store{
 construcor(){
    // 初始化 根模塊
    // 而且也遞歸的註冊全部子模塊
    // 而且收集全部模塊的 getters 放在 this._wrappedGetters 裏面
    installModule(this, state, [], this._modules.root)
 }
}
複製代碼

接着咱們看installModule函數中的遍歷註冊 actions 實現

function installModule (store, rootState, path, module, hot) {
    // 省略若干代碼
    // 循環遍歷註冊 action
    module.forEachAction((action, key) => {
      const type = action.root ? key : namespace + key
      const handler = action.handler || action
      registerAction(store, type, handler, local)
    })
}
複製代碼

接着看註冊 actions 函數實現 registerAction

/** * 註冊 mutation * @param {Object} store 對象 * @param {String} type 類型 * @param {Function} handler 用戶自定義的函數 * @param {Object} local local 對象 */
function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  // payload 是actions函數的第二個參數
  entry.push(function wrappedActionHandler (payload) {
    /** * 也就是爲何用戶定義的actions中的函數第一個參數有 * { dispatch, commit, getters, state, rootGetters, rootState } 的緣由 * actions: { * checkout ({ commit, state }, products) { * console.log(commit, state); * } * } */
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    // 源碼有刪減
}
複製代碼

比較容易發現調用順序是 new Store() => installModule(this) => registerAction(store) => let res = handler.call(store)

其中handler 就是 用戶自定義的函數,也就是對應上文的例子increment函數。store實例對象一路往下傳遞,到handler執行時,也是用了call函數,強制綁定了第一個參數是store實例對象。

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}
複製代碼

這也就是爲何 actions 對象中的自定義函數的第一個參數是 store 對象實例了。

好啦,文章到這裏就基本寫完啦~相對簡短一些。應該也比較好理解。

5. 最後再總結下 this 指向

摘抄下面試官問:this 指向文章結尾。

若是要判斷一個運行中函數的 this 綁定, 就須要找到這個函數的直接調用位置。 找到以後 就能夠順序應用下面這四條規則來判斷 this 的綁定對象。

  1. new 調用:綁定到新建立的對象,注意:顯示return函數或對象,返回值不是新建立的對象,而是顯式返回的函數或對象。
  2. call 或者 apply( 或者 bind) 調用:嚴格模式下,綁定到指定的第一個參數。非嚴格模式下,nullundefined,指向全局對象(瀏覽器中是window),其他值指向被new Object()包裝的對象。
  3. 對象上的函數調用:綁定到那個對象。
  4. 普通函數調用: 在嚴格模式下綁定到 undefined,不然綁定到全局對象。

ES6 中的箭頭函數:不會使用上文的四條標準的綁定規則, 而是根據當前的詞法做用域來決定this, 具體來講, 箭頭函數會繼承外層函數,調用的 this 綁定( 不管 this 綁定到什麼),沒有外層函數,則是綁定到全局對象(瀏覽器中是window)。 這其實和 ES6 以前代碼中的 self = this 機制同樣。


關於

做者:常以若川爲名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,惟善學。
公衆號若川視野
若川的博客
segmentfault前端視野專欄,開通了前端視野專欄,歡迎關注~
掘金專欄,歡迎關注~
知乎前端視野專欄,開通了前端視野專欄,歡迎關注~
github blog,求個star^_^~

相關文章
相關標籤/搜索