人人都能懂的Vue原始碼系列(五)—initProxy

淼淼真人發表於2018-06-02

上篇文章中,主要講了mergeOptions方法的處理邏輯,不明白的同學們可以點選這裡檢視。今天回到init方法,接下來看原始碼

if (process.env.NODE_ENV !== 'production') {
  initProxy(vm)
} else {
  vm._renderProxy = vm
}
複製程式碼

上面的程式碼邏輯很簡單,主要就是為Vue例項的_renderProxy屬性賦值,不同的程式碼執行環境賦值的結果不同。

  1. 當前環境是開發環境,則呼叫initProxy方法
  2. 如果不是開發環境,則vue例項的_renderProxy屬性指向vue例項本身。

initProxy

initProxy方法究竟怎麼樣代理vm屬性呢?通過原始碼來一探究竟。

  initProxy = function initProxy (vm) {
    if (hasProxy) {
      // determine which proxy handler to use
      const options = vm.$options
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
  }
複製程式碼

通過判斷hasProxy,來執行不同的處理邏輯,我們先來看hasProxy的原始碼。

  const hasProxy =
    typeof Proxy !== 'undefined' &&
    Proxy.toString().match(/native code/)
複製程式碼

從變數的名字我們知道它的作用就是判斷當前環境中Proxy是否可用,同學們如果不熟悉Proxy的用法,可以點選這裡。這個判斷方法在我們平時寫程式碼中也可以進行借鑑。

回到程式碼中,如果當前環境存在Proxy,則執行塊內的語句。

  const options = vm.$options
  const handlers = options.render && options.render._withStripped
    ? getHandler
    : hasHandler
複製程式碼

上面兩行程式碼的邏輯是,如果options上存在render屬性,且render屬性上存在_withStripped屬性,則proxy的traps(traps其實也就是自定義方法)採用getHandler方法,否則採用hasHandler方法。更多關於traps相關的內容可點選這裡

接下來看看getHandler和hasHandler方法

getHandler

  const getHandler = {
    get (target, key) {
      if (typeof key === 'string' && !(key in target)) {
        warnNonPresent(target, key)
      }
      return target[key]
    }
  }
複製程式碼

getHandler方法主要是針對讀取代理物件的某個屬性時進行的操作。當訪問的屬性不是string型別或者屬性值在被代理的物件上不存在,則丟擲錯誤提示,否則就返回該屬性值。

該方法可以在開發者錯誤的呼叫vm屬性時,提供提示作用。

  const hasHandler = {
    has (target, key) {
      const has = key in target
      const isAllowed = allowedGlobals(key) || key.charAt(0) === '_'
      if (!has && !isAllowed) {
        warnNonPresent(target, key)
      }
      return has || !isAllowed
    }
  }
複製程式碼

hasHandler

hasHandler方法的應用場景在於檢視vm例項是否擁有某個屬性—比如呼叫for in迴圈遍歷vm例項屬性時,會觸發hasHandler方法。

首先使用in操作符判斷該屬性是否在vm例項上存在

const has = key in target
複製程式碼

接下來通過下列語句,確定屬性名稱是否可用

const isAllowed = allowedGlobals(key) || key.charAt(0) === '_'
複製程式碼

allowedGlobals的定義如下:

 const allowedGlobals = makeMap(
    'Infinity,undefined,NaN,isFinite,isNaN,' +
    'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
    'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
    'require' // for Webpack/Browserify
  )
複製程式碼

我們結合makeMap函式一起來看

export function makeMap (
  str: string,
  expectsLowerCase?: boolean
): (key: string) => true | void {
  const map = Object.create(null)
  const list: Array<string> = str.split(',')
  for (let i = 0; i < list.length; i++) {
    map[list[i]] = true
  }
  return expectsLowerCase
    ? val => map[val.toLowerCase()]
    : val => map[val]
}
複製程式碼

先來分析makeMap,其作用是通過傳入的string引數來生成對映表。如傳入下列引數

'Infinity,undefined,NaN,isFinite,isNaN,'
....
複製程式碼

通過makeMap方法可以生成下面這樣的一個對映表

{
  Infinity: true,
  undefined: true
  ......
}
複製程式碼

這個方法我們在日常寫程式碼的時候也可以進行借鑑。結合上面的分析,我們知道allowedGlobals最終儲存的是一個代表特殊屬性名稱的對映表。

回到原始碼,結合has和isAllowed屬性,我們知道當讀取物件屬性時,如果屬性名在vm上不存在,且不在特殊屬性名稱對映表中,或沒有以_符號開頭,則丟擲異常。最後回到initProxy程式碼中

  if (hasProxy) {
      ...
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
  }
複製程式碼

如果Proxy屬性存在,則把包裝後的vm屬性賦值給_renderProxy屬性值,否則把vm是例項本身賦值給_renderProxy屬性。

總結

The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).

代理物件是es6的新特性,它主要用來自定義物件一些基本操作(如查詢,賦值,列舉等)。

上述原始碼部分主要應用了lookup和enumeration部分的自定義能力。proxy是一個強大的特性,為我們提供了很多"超程式設計"能力。只不過目前規範還沒有很完善,使用的時候要稍加註意。最後按照老規矩,以一張圖完成今天的講解。

render proxy屬性

相關文章