Vue源码阅读前必须知道javascript的基础内容

前言

vue目前是前端使用频率较高的一套前端mvvm框架之一,提供了数据的响应式、watchcomputed等极为方便的功能及api,那么,vue究竟是如何实现这些功能的呢?在探究vue源码以前,必须了解如下几点javascript的基本内容,经过了解这些内容,你能够更加轻松的阅读vue源码。javascript

flow 类型检测

Flow就是JavaScript的静态类型检查工具,由Facebook团队于2014年的Scale Conference上首次提出。该库的目标在于检查JavaScript中的类型错误,开发者一般不须要修改代码便可使用,故使用成本很低。同时,它也提供额外语法支持,使得开发者能更大程度地发挥Flow的做用。总结一句话:将javascript从弱类型语言变成了强类型语言。前端

基础检测类型

Flow支持原始数据类型,其中void对应js中的undefined,基本有以下几种:vue

boolean
  number
  string
  null
  void
复制代码

在定义变量的同时,只须要在关键的地方声明想要的类型,基本使用以下:java

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

// 从新赋值
str = 'd' // error
str1 = 3  // error
复制代码

复杂类型检测

Flow支持复杂类型检测,基本有以下几种:git

Object
  Array
  Function
  自定义Class
复制代码

基本使用以下示例代码:github

// 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源码中出现频率最多的:web

/** * 声明带类型的函数 * 这里是声明一个函数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,声明一个自定义类,而后用法如同基本类型,基本代码以下:json

/** * 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进行配置,片断代码以下:api

// 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'
复制代码

图解基本以下:

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方法。

图解基本以下:

若是在建立当前对象上定义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
复制代码

图解基本以下:

Object.defineProperty中的configurableenumerablewritablevaluegetset几个参数相互之间的关系到底如何呢?能够用一张图来清晰说明:

对象标签

其实建立对象的同时都会附带一个__proto__的原型标签,除了使用Object.create(null)创建对象之外,代码以下:

let obj = {x: 1, y: 2}
  obj.__proto__.z = 3
  console.log(obj.z) // 3
复制代码

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表示左上角定点的坐标位置。

经过这个方法计算得出的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中监测到,效果图以下:

浏览器中performance处理模型基本以下(更多具体参数说明):

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

相关文章
相关标签/搜索