Vue原始碼閱讀前必須知道javascript的基礎內容

wqzwh發表於2019-03-03

前言

vue目前是前端使用頻率較高的一套前端mvvm框架之一,提供了資料的響應式、watchcomputed等極為方便的功能及api,那麼,vue到底是如何實現這些功能的呢?在探究vue原始碼之前,必須瞭解以下幾點javascript的基本內容,通過了解這些內容,你可以更加輕鬆的閱讀vue原始碼。

flow 型別檢測

Flow就是JavaScript的靜態型別檢查工具,由Facebook團隊於2014年的Scale Conference上首次提出。該庫的目標在於檢查JavaScript中的型別錯誤,開發者通常不需要修改程式碼即可使用,故使用成本很低。同時,它也提供額外語法支援,使得開發者能更大程度地發揮Flow的作用。總結一句話:將javascript從弱型別語言變成了強型別語言。

基礎檢測型別

Flow支援原始資料型別,其中void對應js中的undefined,基本有如下幾種:

  boolean
  number
  string
  null
  void
複製程式碼

在定義變數的同時,只需要在關鍵的地方宣告想要的型別,基本使用如下:

let str:number = 1;
let str1:string = 'a';

// 重新賦值
str = 'd' // error
str1 = 3  // error
複製程式碼

複雜型別檢測

Flow支援複雜型別檢測,基本有如下幾種:

  Object
  Array
  Function
  自定義Class
複製程式碼

基本使用如下示例程式碼:

// Object 定義
let o:Object = {
  key: 123
}
//宣告瞭Object的key
let o2:{key:string} = {
  key: '111'
}

// Array 定義
//基於基本類似的陣列,陣列內都是相同型別
let numberArr:number[] = [12,3,4,5,2];
//另一個寫法
let numberAr2r:Array<number> = [12,3,2,3];

let stringArr:string[] = ['12','a','cc'];
let booleanArr:boolean[] = [true,true,false];
let nullArr:null[] = [null,null,null];
let voidArr:void[] = [ , , undefined,void(0)];

//陣列內包含各個不同的型別資料
//第4個原素沒有宣告,則可以是任意型別
let arr:[number,string,boolean] = [1,'a',true,function(){},];
複製程式碼

Function定義寫法如下,vue原始碼中出現頻率最多的:

/**
 * 宣告帶型別的函式
 * 這裡是宣告一個函式fn,規定了自己需要的引數型別和返回值型別。
 */
function fn(arg:number,arg2:string):Object{
  return {
    arg,
    arg2
  }
}

/**
 * vue原始碼片段
 * src/core/instance/lifecycle.js
 */
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // 省略
}
複製程式碼

自定義的class,宣告一個自定義類,然後用法如同基本型別,基本程式碼如下:

/**
 * vue原始碼片段
 * src/core/observer/index.js 
*/
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number;
  constructor (value: any) {
    // 省略
  }
}  
複製程式碼

直接使用flow.js,javascript是無法在瀏覽器端執行的,必須藉助babel外掛,vue原始碼中使用的是babel-preset-flow-vue這個外掛,並且在babelrc進行配置,片段程式碼如下:

// package.json 檔案

// 省略
"devDependencies": {
 // 省略
  "babel-preset-flow-vue": "^1.0.0"
}  
// 省略

// babelrc 檔案
{
  "presets": ["es2015", "flow-vue"],
  "plugins": ["transform-vue-jsx", "syntax-dynamic-import"],
  "ignore": [
    "dist/*.js",
    "packages/**/*.js"
  ]
}
複製程式碼

物件

這裡只對物件的建立、物件上的屬性操作相關、getter/setter方法、物件標籤等進行再分析,對於原型鏈以及原型繼承原理不是本文的重要內容。

建立物件

一般建立物件有以下三種寫法,基本程式碼如下:

  // 第一種 最簡單的寫法
  let obj = { a: 1 }
  obj.a // 1
  typeof obj.toString // 'function'

  // 第二種
  let obj2 = Object.create({ a: 1 })
  obj2.a // 1
  typeof obj2.toString // 'function'

  // 第三種
  let obj3 = Object.create(null)
  typeof obj3.toString // 'undefined'
複製程式碼

圖解基本如下:

Vue原始碼閱讀前必須知道javascript的基礎內容

Object.create可以理解為繼承一個物件,它是ES5的一個新特性,對於舊版瀏覽器需要做相容,基本程式碼如下(vue使用ie9+瀏覽器,所以不需要做相容處理):

  if (!Object.create) {
    Object.create = function (o) {
      function F() {}  //定義了一個隱式的建構函式
      F.prototype = o;
      return new F();  //其實還是通過new來實現的
    };
  }
複製程式碼

其中,在vue原始碼中會看見使用Object.create(null)來建立一個空物件,其好處不用考慮會和原型鏈上的屬性重名問題,vue程式碼片段如下:

// src/core/global-api/index.js
// 再Vue上定義靜態屬性options並且賦值位空物件,ASSET_TYPES是在vue上定義的'component','directive','filter'等屬性
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })
複製程式碼

屬性操作相關

其實在建立物件的同時,物件上會預設設定當前物件的列舉型別值,如果不設定,預設所有列舉型別均為false,那麼如何定義物件並且設定列舉型別值呢?主要使用到的是ES5的新特性Object.defineProperty

Object.defineProperty(obj,prop,descriptor)中的descriptor有如下幾種引數:

  • configurable 當且僅當該屬性的 configurable 為 true 時,該屬性描述符才能夠被改變,同時該屬性也能從對應的物件上被刪除。預設為 false
  • enumerable 當且僅當該屬性的enumerable為true時,該屬性才能夠出現在物件的列舉屬性中。預設為 false
  • value 該屬性對應的值。可以是任何有效的 JavaScript 值(數值,物件,函式等)。預設為 undefined。
  • writable 當且僅當該屬性的writable為true時,value才能被賦值運算子改變。預設為 false。
  • get 一個給屬性提供 getter 的方法,如果沒有 getter 則為 undefined。當訪問該屬性時,該方法會被執行,方法執行時沒有引數傳入,但是會傳入this物件(由於繼承關係,這裡的this並不一定是定義該屬性的物件)。預設為 undefined。
  • set 一個給屬性提供 setter 的方法,如果沒有 setter 則為 undefined。當屬性值修改時,觸發執行該方法。該方法將接受唯一引數,即該屬性新的引數值。預設為 undefined

注意:在 descriptor 中不能同時設定訪問器 (get 和 set) 和 value。

完整示例程式碼如下:

  Object.defineProperty(obj,prop,
    configurable: true,
    enumerable: true,
    writable: true,
    value: '',
    get: function() {

    },
    set: function() {
      
    }
  )
複製程式碼

通過使用Object.getOwnPropertyDescriptor來檢視物件上屬性的列舉型別值,具體使用相關示例程式碼如下:

  // 如果不設定列舉型別,預設都是false
  let obj = {}
  Object.defineProperty(obj, 'name', {
    value : "wqzwh"
  })
  Object.getOwnPropertyDescriptor(obj, 'name')
  // {value: "wqzwh", writable: false, enumerable: false, configurable: false}

  let obj2 = {}
  Object.defineProperty(obj2, 'name', {
    enumerable: true,
    writable: true,
    value : "wqzwh"
  })
  Object.getOwnPropertyDescriptor(obj2, 'name')
  // {value: "wqzwh", writable: true, enumerable: true, configurable: false}
複製程式碼

通過Object.keys()來獲取物件的key,必須將enumerable設定為true才能獲取,否則返回是空陣列,程式碼如下:

  let obj = {}
  Object.defineProperty(obj, 'name', {
    enumerable: true,
    value : "wqzwh"
  })
  Object.keys(obj) // ['name']
複製程式碼

通過propertyIsEnumerable可以判斷定義的物件是否可列舉,程式碼如下:

  let obj = {}
  Object.defineProperty(obj, 'name', {
    value : "wqzwh"
  })
  obj.propertyIsEnumerable('name') // false

  let obj = {}
  Object.defineProperty(obj, 'name', {
    enumerable: true,
    value : "wqzwh"
  })
  obj.propertyIsEnumerable('name') // true
複製程式碼

通過hasOwnProperty來檢測一個物件是否含有特定的自身屬性;和 in 運算子不同,該方法會忽略掉那些從原型鏈上繼承到的屬性。程式碼如下:

  // 使用Object.defineProperty建立物件屬性
  let obj = {}
  Object.defineProperty(obj, 'name', {
    value : "wqzwh",
    enumerable: true
  })
  let obj2 = Object.create(obj)
  obj2.age = 20
  for (key in obj2) {
    console.log(key); // age, name
  }
  for (key in obj2) {
    if (obj2.hasOwnProperty(key)) {
      console.log(key); // age
    }
  }
  
  // 普通建立屬性
  let obj = {}
  obj.name = 'wqzwh'
  let obj2 = Object.create(obj)
  obj2.age = 20
  for (key in obj2) {
    console.log(key); // age, name
  }
  for (key in obj2) {
    if (obj2.hasOwnProperty(key)) {
      console.log(key); // age
    }
  }
複製程式碼

注意:如果繼承的物件屬性是通過Object.defineProperty建立的,並且enumerable未設定成true,那麼for in依然不能列舉出原型上的屬性。(感謝 @SunGuoQiang123 同學指出錯誤問題,已經做了更改)

getter/setter方法

通過get/set方法來檢測屬性變化,基本程式碼如下:

  function foo() {}
  Object.defineProperty(foo.prototype, 'z', 
    {
      get: function(){
        return 1
      }
    }
  )
  let obj = new foo();
  console.log(obj.z) // 1
  obj.z = 10
  console.log(obj.z) // 1
複製程式碼

這個是z屬性是foo.prototype上的屬性並且有get方法,對於第二次通過obj.z = 10並不會在obj本身建立z屬性,而是直接原型觸發上的get方法。

圖解基本如下: Vue原始碼閱讀前必須知道javascript的基礎內容

如果在建立當前物件上定義z屬性,並且設定writableconfigurabletrue,那麼就可以改變z屬性的值,並且刪除z屬性後再次訪問obj.z仍然是1,測試程式碼如下:

  function foo() {}
  Object.defineProperty(foo.prototype, 'z', 
    {
      get: function(){
        return 1
      }
    }
  )
  let obj = new foo();
  console.log(obj.z) // 1
  Object.defineProperty(obj, 'z', 
    {
      value: 100,
      writable: true,
      configurable: true
    }
  )
  console.log(obj.z) // 100
  obj.z = 300
  console.log(obj.z) // 300
  delete obj.z
  console.log(obj.z) // 1
複製程式碼

圖解基本如下: Vue原始碼閱讀前必須知道javascript的基礎內容

Object.defineProperty中的configurableenumerablewritablevaluegetset幾個引數相互之間的關係到底如何呢?可以用一張圖來清晰說明: Vue原始碼閱讀前必須知道javascript的基礎內容

物件標籤

其實建立物件的同時都會附帶一個__proto__的原型標籤,除了使用Object.create(null)建立物件以外,程式碼如下:

  let obj = {x: 1, y: 2}
  obj.__proto__.z = 3
  console.log(obj.z) // 3
複製程式碼
Vue原始碼閱讀前必須知道javascript的基礎內容

Object.preventExtensions方法用於鎖住物件屬性,使其不能夠擴充,也就是不能增加新的屬性,但是屬性的值仍然可以更改,也可以把屬性刪除,Object.isExtensible用於判斷物件是否可以被擴充,基本程式碼如下:

  let obj = {x : 1, y : 2};
  Object.isExtensible(obj); // true
  Object.preventExtensions(obj);
  Object.isExtensible(obj); // false
  obj.z = 1;
  obj.z; // undefined, add new property failed
  Object.getOwnPropertyDescriptor(obj, 'x');
  // Object {value: 1, writable: true, enumerable: true, configurable: true}
複製程式碼

Object.seal方法用於把物件密封,也就是讓物件既不可以擴充也不可以刪除屬性(把每個屬性的configurable設為false),單數屬性值仍然可以修改,Object.isSealed由於判斷物件是否被密封,基本程式碼如下:

  let obj = {x : 1, y : 2};
  Object.seal(obj);
  Object.getOwnPropertyDescriptor(obj, 'x');
  // Object {value: 1, writable: true, enumerable: true, configurable: false}
  Object.isSealed(obj); // true
複製程式碼

Object.freeze完全凍結物件,在seal的基礎上,屬性值也不可以修改(每個屬性的wirtable也被設為false),Object.isFrozen判斷物件是否被凍結,基本程式碼如下:

  let obj = {x : 1, y : 2};
  Object.freeze(obj);
  Object.getOwnPropertyDescriptor(obj, 'x');
  // Object {value: 1, writable: false, enumerable: true, configurable: false}
  Object.isFrozen(obj); // true
複製程式碼

DOM自定義事件

在介紹這個命題之前,先看一段vue原始碼中的model的指令,開啟platforms/web/runtime/directives/model.js,片段程式碼如下:

  /* istanbul ignore if */
  if (isIE9) {
    // http://www.matts411.com/post/internet-explorer-9-oninput/
    document.addEventListener('selectionchange', () => {
      const el = document.activeElement
      if (el && el.vmodel) {
        trigger(el, 'input')
      }
    })
  }

  // 省略
  function trigger (el, type) {
    const e = document.createEvent('HTMLEvents')
    e.initEvent(type, true, true)
    el.dispatchEvent(e)
  }
複製程式碼

其中document.activeElement是當前獲得焦點的元素,可以使用document.hasFocus()方法來檢視當前元素是否獲取焦點。

對於標準瀏覽器,其提供了可供元素觸發的方法:element.dispatchEvent(). 不過,在使用該方法之前,我們還需要做其他兩件事,及建立和初始化。因此,總結說來就是:

  document.createEvent()
  event.initEvent()
  element.dispatchEvent()
複製程式碼

createEvent()方法返回新建立的Event物件,支援一個引數,表示事件型別,具體見下表:

  引數	        事件介面	        初始化方法
  HTMLEvents	HTMLEvent	  initEvent()
  MouseEvents	MouseEvent	  initMouseEvent()
  UIEvents	  UIEvent	  initUIEvent()
複製程式碼

initEvent()方法用於初始化通過DocumentEvent介面建立的Event的值。支援三個引數:initEvent(eventName, canBubble, preventDefault). 分別表示事件名稱,是否可以冒泡,是否阻止事件的預設操作。

dispatchEvent()就是觸發執行了,上文vue原始碼中的el.dispatchEvent(e), 引數e表示事件物件,是createEvent()方法返回的建立的Event物件。

那麼這個東東具體該怎麼使用呢?例如自定一個click方法,程式碼如下:

  // 建立事件.
  let event = document.createEvent('HTMLEvents');
  // 初始化一個點選事件,可以冒泡,無法被取消
  event.initEvent('click', true, false);
  let elm = document.getElementById('wq')
  // 設定事件監聽.
  elm.addEventListener('click', (e) => {
    console.log(e)
  }, false);
  // 觸發事件監聽
  elm.dispatchEvent(event);
複製程式碼

陣列擴充套件方法

every方法/some方法

接受兩個引數,第一個是函式(接受三個引數:陣列當前項的值、當前項在陣列中的索引、陣列物件本身),第二個引數是執行第一個函式引數的作用域物件,也就是上面說的函式中this所指向的值,如果不設定預設是undefined。

這兩種方法都不會改變原陣列

  • every(): 該方法對陣列中的每一項執行給定函式,如果該函式對每一項都返回 true,則返回true。
  • some(): 該方法對陣列中的每一項執行給定函式,如果該函式對任何一項返回 true,則返回true。

示例程式碼如下:

let arr = [ 1, 2, 3, 4, 5, 6 ];  
console.log( arr.some( function( item, index, array ){  
  console.log( 'item=' + item + ',index='+index+',array='+array );  
  return item > 3;  
}));  
console.log( arr.every( function( item, index, array ){  
  console.log( 'item=' + item + ',index='+index+',array='+array );  
  return item > 3;  
}));  
複製程式碼

some方法是碰到一個返回true的值時候就返回了,並沒有繼續往下執行,而every也一樣,第一個值就是一個false,所以後面也沒有進行下去的必要了,就直接返回結果了。

getBoundingClientRect

該方法返回一個矩形物件,其中四個屬性:left、top、right、bottom,分別表示元素各邊與頁面上邊和左邊的距離,x、y表示左上角定點的座標位置。

Vue原始碼閱讀前必須知道javascript的基礎內容

通過這個方法計算得出的left、top、right、bottom、x、y會隨著視口區域內滾動操作而發生變化,如果你需要獲得相對於整個網頁左上角定位的屬性值,那麼只要給top、left屬性值加上當前的滾動位置。

為了跨瀏覽器相容,請使用 window.pageXOffset 和 window.pageYOffset 代替 window.scrollX 和 window.scrollY。不能訪問這些屬性的指令碼可以使用下面的程式碼:

// For scrollX
(((t = document.documentElement) || (t = document.body.parentNode))
  && typeof t.scrollLeft == 'number' ? t : document.body).scrollLeft
// For scrollY
(((t = document.documentElement) || (t = document.body.parentNode))
  && typeof t.scrollTop == 'number' ? t : document.body).scrollTop
複製程式碼

在IE中,預設座標從(2,2)開始計算,導致最終距離比其他瀏覽器多出兩個畫素,程式碼如下:

  document.documentElement.clientTop;  // 非IE為0,IE為2
  document.documentElement.clientLeft; // 非IE為0,IE為2

  // 所以為了保持所有瀏覽器一致,需要做如下操作
  functiongGetRect (element) {
    let rect = element.getBoundingClientRect();
    let top = document.documentElement.clientTop;
    let left= document.documentElement.clientLeft;
    return{
      top: rect.top - top,
      bottom: rect.bottom - top,
      left: rect.left - left,
      right: rect.right - left
    }
  }
複製程式碼

performance

vue中片段原始碼如下:

  if (process.env.NODE_ENV !== 'production') {
    const perf = inBrowser && window.performance
    /* istanbul ignore if */
    if (
      perf &&
      perf.mark &&
      perf.measure &&
      perf.clearMarks &&
      perf.clearMeasures
    ) {
      mark = tag => perf.mark(tag)
      measure = (name, startTag, endTag) => {
        perf.measure(name, startTag, endTag)
        perf.clearMarks(startTag)
        perf.clearMarks(endTag)
        perf.clearMeasures(name)
      }
    }
  }
複製程式碼

performance.mark方法在瀏覽器的效能條目緩衝區中建立一個具有給定名稱的緩衝區,performance.measure在瀏覽器的兩個指定標記(分別稱為起始標記和結束標記)之間的效能條目緩衝區中建立一個命名,測試程式碼如下:

  let _uid = 0
  const perf = window.performance
  function testPerf() {
    _uid++
    let startTag = `test-mark-start:${_uid}`
    let endTag = `test-mark-end:${_uid}`

    // 執行mark函式做標記
    perf.mark(startTag)

    for(let i = 0; i < 100000; i++) {
      
    }

    // 執行mark函式做標記
    perf.mark(endTag)
    perf.measure(`test mark init`, startTag, endTag)
  }
複製程式碼

測試結果可以在谷歌瀏覽器中的Performance中監測到,效果圖如下: Vue原始碼閱讀前必須知道javascript的基礎內容

瀏覽器中performance處理模型基本如下(更多具體引數說明): Vue原始碼閱讀前必須知道javascript的基礎內容

Proxy相關

get方法

get方法用於攔截某個屬性的讀取操作,可以接受三個引數,依次為目標物件、屬性名和 proxy 例項本身(嚴格地說,是操作行為所針對的物件),其中最後一個引數可選。

攔截物件屬性的讀取,比如proxy.foo和proxy['foo']

基本使用如下:

  let person = {
    name: "張三"
  };

  let proxy = new Proxy(person, {
    get: (target, property) => {
      if (property in target) {
        return target[property];
      } else {
        throw new ReferenceError("Property \"" + property + "\" does not exist.");
      }
    }
  });

  proxy.name // "張三"
  proxy.age // 丟擲一個錯誤
複製程式碼

如果一個屬性不可配置(configurable)且不可寫(writable),則 Proxy 不能修改該屬性,否則通過 Proxy 物件訪問該屬性會報錯。示例程式碼如下:

  const target = Object.defineProperties({}, {
    foo: {
      value: 123,
      writable: false,
      configurable: false
    },
  });
  const handler = {
    get(target, propKey) {
      return 'abc';
    }
  };
  const proxy = new Proxy(target, handler);
  proxy.foo // TypeError: Invariant check failed
複製程式碼

has方法

此方法可以接受兩個引數,分別是目標物件、需查詢的屬性名,主要攔截如下幾種操作:

  • 屬性查詢: foo in proxy
  • 繼承屬性查詢: foo in Object.create(proxy)
  • with 檢查: with(proxy) { (foo); }
  • Reflect.has()

如果原物件不可配置或者禁止擴充套件,這時has攔截會報錯。基本示例程式碼如下:

  let obj = { a: 10 };
  Object.preventExtensions(obj);
  let p = new Proxy(obj, {
    has: function(target, prop) {
      return false;
    }
  });
  'a' in p // TypeError is thrown
複製程式碼

has攔截只對in運算子生效,對for...in迴圈不生效。基本示例程式碼如下:

  let stu1 = {name: '張三', score: 59};
  let stu2 = {name: '李四', score: 99};
  let handler = {
    has(target, prop) {
      if (prop === 'score' && target[prop] < 60) {
        console.log(`${target.name} 不及格`);
        return false;
      }
      return prop in target;
    }
  }
  let oproxy1 = new Proxy(stu1, handler);
  let oproxy2 = new Proxy(stu2, handler);
  'score' in oproxy1
  // 張三 不及格
  // false
  'score' in oproxy2
  // true
  for (let a in oproxy1) {
    console.log(oproxy1[a]);
  }
  // 張三
  // 59
  for (let b in oproxy2) {
    console.log(oproxy2[b]);
  }
  // 李四
  // 99
複製程式碼

使用with關鍵字的目的是為了簡化多次編寫訪問同一物件的工作,基本寫法如下:

  let qs = location.search.substring(1);
  let hostName = location.hostname;
  let url = location.href;

  with (location){
    let qs = search.substring(1);
    let hostName = hostname;
    let url = href;
  }
複製程式碼

使用with關鍵字會導致程式碼效能降低,使用let定義變數相比使用var定義變數能提高一部分效能,示例程式碼如下:

  // 不使用with
  function func() {
    console.time("func");
    let obj = {
      a: [1, 2, 3]
    };
    for (let i = 0; i < 100000; i++) {
      let v = obj.a[0];
    }
    console.timeEnd("func");// 1.310302734375ms
  }
  func();

  // 使用with並且使用let定義變數
  function funcWith() {
    console.time("funcWith");
    const obj = {
      a: [1, 2, 3]
    };
    with (obj) {
      let a = obj.a
      for (let i = 0; i < 100000; i++) {
        let v = a[0];
      }
    }
    console.timeEnd("funcWith");// 14.533935546875ms
  }
  funcWith();

  // 使用with
  function funcWith() {
    console.time("funcWith");
    var obj = {
      a: [1, 2, 3]
    };
    with (obj) {
      for (var i = 0; i < 100000; i++) {
        var v = a[0];
      }
    }
    console.timeEnd("funcWith");// 52.078857421875ms
  }
  funcWith();
複製程式碼

js引擎在程式碼執行之前有一個編譯階段,在不使用with關鍵字的時候,js引擎知道a是obj上的一個屬性,它就可以靜態分析程式碼來增強識別符號的解析,從而優化了程式碼,因此程式碼執行的效率就提高了。使用了with關鍵字後,js引擎無法分辨出a變數是區域性變數還是obj的一個屬性,因此,js引擎在遇到with關鍵字後,它就會對這段程式碼放棄優化,所以執行效率就降低了。

使用has方法攔截with關鍵字,示例程式碼如下:

  let stu1 = {name: '張三', score: 59};
  let handler = {
    has(target, prop) {
      if (prop === 'score' && target[prop] < 60) {
        console.log(`${target.name} 不及格`);
        return false;
      }
      return prop in target;
    }
  }
  let oproxy1 = new Proxy(stu1, handler);

  function test() {
    let score
    with(oproxy1) {
      return score
    }
  }
  test() // 張三 不及格
複製程式碼

在使用with關鍵字時候,主要是因為js引擎在解析程式碼塊中變數的作用域造成的效能損失,那麼我們可以通過定義區域性變數來提高其效能。修改示例程式碼如下:

  // 修改後
  function funcWith() {
    console.time("funcWith");
    const obj = {
      a: [1, 2, 3]
    };
    with (obj) {
      let a = obj.a
      for (let i = 0; i < 100000; i++) {
        let v = a[0];
      }
    }
    console.timeEnd("funcWith");// 1.7109375ms
  }
  funcWith();
複製程式碼

但是在實際使用的時候在with程式碼塊中定義區域性變數不是很可行,那麼刪除頻繁查詢作用域的功能應該可以提高程式碼部分效能,經測試執行時間幾乎相同,修改程式碼如下:

  function func() {
    console.time("func");
    let obj = {
      a: [1, 2, 3]
    };
    let v = obj.a[0];
    console.timeEnd("func");// 0.01904296875ms
  }
  func();

  // 修改後
  function funcWith() {
    console.time("funcWith");
    const obj = {
      a: [1, 2, 3]
    };
    with (obj) {
      let v = a[0];
    }
    console.timeEnd("funcWith");// 0.028076171875ms
  }
  funcWith();
複製程式碼

配上has函式後執行效果如何呢,片段程式碼如下:

  // 第一段程式碼其實has方法沒用,只是為了對比使用
  console.time("測試");
  let stu1 = {name: '張三', score: 59};
  let handler = {
    has(target, prop) {
      if (prop === 'score' && target[prop] < 60) {
        console.log(`${target.name} 不及格`);
        return false;
      }
      return prop in target;
    }
  }
  let oproxy1 = new Proxy(stu1, handler);

  function test(oproxy1) {
    return {
      render: () => {
        return oproxy1.score
      }
    }
  }
  console.log(test(oproxy1).render()) // 張三 不及格
  console.timeEnd("測試"); // 0.719970703125ms


  console.time("測試");
  let stu1 = {name: '張三', score: 59};
  let handler = {
    has(target, prop) {
      if (prop === 'score' && target[prop] < 60) {
        console.log(`${target.name} 不及格`);
        return false;
      }
      return prop in target;
    }
  }
  let oproxy1 = new Proxy(stu1, handler);

  function test(oproxy1) {
    let score
    return {
      render: () => {
        with(oproxy1) {
          return score
        }
      }
    }
  }
  console.log(test(oproxy1).render()) // 張三 不及格
  console.timeEnd("測試"); // 0.760009765625ms
複製程式碼

vue中使用with關鍵字的片段程式碼如下,主要通過proxy來攔截AST語言樹中涉及到的變數以及方法,並且判斷是否AST語言樹中是否存在為定義的變數及方法,至於為什麼vue會使用with關鍵字,具體可以點選檢視

  export function generate (
    ast: ASTElement | void,
    options: CompilerOptions
  ): CodegenResult {
    const state = new CodegenState(options)
    const code = ast ? genElement(ast, state) : '_c("div")'
    return {
      render: `with(this){return ${code}}`,
      staticRenderFns: state.staticRenderFns
    }
  }
複製程式碼

outerHTML

開啟platforms/web/entry-runtime-width-compile.js,檢視getOuterHTML方法,片段程式碼如下:

  function getOuterHTML (el: Element): string {
    if (el.outerHTML) {
      return el.outerHTML
    } else {
      const container = document.createElement('div')
      container.appendChild(el.cloneNode(true))
      return container.innerHTML
    }
  }
複製程式碼

由於在IE9-11中SVG標籤元素是沒有innerHTMLouterHTML這兩個屬性,所以會有else之後的語句

2018-07-17補充

這裡針對proxyObject.definePropertyvue原始碼中使用做一次補充說明下。vue中的定義的data其實是通過Object.defineProperty來進行監聽變化的,如果定義的data單純是物件,按照Object.definePropertyapi介紹是合理的,但是如果是陣列呢?這個是如何實現的呢?

注意:Object.defineProperty有一定的缺陷:只能針對obj中的屬性進行資料劫持,如果物件層級過深,那麼需要深度遍歷整個物件;對於陣列不能監聽到資料的變化

這裡想說明的是Object.defineProperty無法監聽陣列的變化,帶著這個疑問檢視原始碼,先檢視src/core/instance/state.js中的initData方法,片段程式碼如下:


export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

// 省略

function initData (vm: Component) {
  // 省略
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}
複製程式碼

這裡重要的是proxyobserve,那麼問題來了,為什麼proxy已經監聽了,為什麼還需要observe再次監聽呢,繼續開啟src/core/observer/index.js,片段程式碼如下:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
複製程式碼

這裡就判斷了value的型別,如果value是物件那麼直接return,如果是陣列,那麼會繼續執行ob = new Observer(value),其實就是再次監聽。然後根據方法最終找到了,開啟src/core/observer/array.js核心程式碼如下:

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})
複製程式碼

這裡為什麼會將Array.prototype賦值給arrayProto,並且重新定義一個變數arrayMethods繼承arrayProto,個人覺得這是一個小技巧,這樣methodsToPatch方法中的def(src/core/util/lang.js檔案中的方法,其實就是Object.defineProperty)的第一個引數就是個物件了,並且將陣列的幾個方法全部使用Object.defineProperty再包裝一次,這樣就能尊崇Object.definePropertyapi規範了。

話題轉回來,其實如果是陣列,那麼vue中需要通過vm.$set才能及時更新試圖,經過測試發現呼叫vm.$set改變陣列,其實是觸發了陣列的splice方法,而splice方法又被監聽了,所以才能實現最開始的疑問陣列也能被監聽,測試程式碼如下:

<div>
{{arr}}
</div>
let vm = new Vue({
  el: '#app',
  data() {
    return {
      arr: [1, 2]
    }
  }
})
// 只能通過vm.$set來更新試圖
vm.$set(vm.arr, 0, 31)
複製程式碼

這種實現感覺存在效能問題,就是陣列需要遍歷並且呼叫Object.defineProperty方法。

再說回proxy,其實這個也有getset方法,proxy其實是優越Object.defineProperty,因為它可以攔截陣列型別的資料,測試程式碼如下:

// 因為proxy肯定能攔截物件,所以這裡只用陣列來做測試
const handler = {
  get (target, key) {
    console.log('----get-----')
    return target[key];
  },
  set (target, key, value) {
    console.log('----set-----')
    target[key] = value;
    return true;
  }
};
const target = [1,2];
const arr = new Proxy(target, handler);

arr[0] = 3 // '----set-----'
複製程式碼

因此我覺得,vue完全可以使用proxy來替代Object.defineProperty,效能也能得到一定的提升。

以上是我對proxyObject.defineProperty做的一個補充,如果有什麼不對的地方,希望能夠指出來。

總結

以上主要是在閱讀原始碼時,發現不是很明白的api以及一些方法,每個人可以根據自己的實際情況選擇性閱讀,以上就是全部內容,如果有什麼不對的地方,歡迎提issues

相關文章