我最近在写 Vue 进阶的内容。在这个过程当中,有些人问我看 Vue 源码须要有哪些准备吗?因此也就有了这篇计划以外的文章。前端
当你想学习 Vue 源码的时候,须要有扎实的 JavaScript 基础,下面罗列的只是其中的一部分比较具备表明性的知识点。若是你还不具有 JavaScript 基础的话,建议不要急着看 Vue 源码,这样你会很容易放弃的。vue
我会从如下 7 点来展开:node
Flow 基本语法ios
发布/订阅模式es6
Object.defineProperty面试
ES6+ 语法npm
原型链、闭包浏览器
函数柯里化babel
须要注意的是这篇文章每一个点不会讲的特别详细,我这里就是把一些知识点概括一下。每一个详细的点仍需本身花时间学习。闭包
相信看过 Vue、Vuex 等源码的人都知道它们使用了 Flow 静态类型检查工具。
咱们知道 JavaScript 是弱类型的语言,因此咱们在写代码的时候容易出现一些始料未及的问题。也正是由于这个问题,才出现了 Flow 这个静态类型检查工具。
这个工具能够改变 JavaScript 是弱类型的语言的状况,能够加入类型的限制,提升代码质量。
// 未使用 Flow 限制 function sum(a, b) { return a + b; } // 使用 Flow 限制 a b 都是 number 类型。 function sum(a: number, b:number) { return a + b; }
Flow 支持原始数据类型,有以下几种:
boolean number string null void( 对应 undefined )
在定义变量的同时在关键的地方声明类型,使用以下:
let str:string = 'str'; // 从新赋值 str = 3 // 报错
Flow 支持复杂类型检测,有以下几种:
Object Array Function 自定义的 Class
须要注意直接使用 flow.js,JavaScript 是没法在浏览器端运行的,必须借助 babel 插件,vue 源码中使用的是 babel-preset-flow-vue 这个插件,而且在 babelrc 进行配置。
详细的 Flow 语法能够看如下资料:
这里推荐两个资料
官方文档:https://flow.org/en/
咱们知道 Vue 是内部是实现了双向绑定机制,使得咱们不用再像从前那样还要本身操做 DOM 了。
其实 Vue 的双向绑定机制采用数据劫持结合发布/订阅模式实现的: 经过 Object.defineProperty() 来劫持各个属性的 setter,getter,在数据变更时发布消息给订阅者,触发相应的监听回调。
我发现有的人把观察者模式和发布/订阅模式混淆一谈,其实订阅模式有一个调度中心,对订阅事件进行统一管理。而观察者模式能够随意注册事件,调用事件。
我画了一个大概的流程图,用来讲明观察者模式和发布/订阅模式。以下:
这块我会在接下的文章中详细讲到,这里先给出一个概念,感兴趣的能够本身查找资料,也可等个人文章出炉。
其实咱们对这种模式再熟悉不过了,但可能你本身也没发现:
let div = document.getElementById('#div'); div.addEventListener('click', () => { console.log("div 被点击了一下") })
能够思考下上面的事件绑定执行的一个过程,你应该会有共鸣。
数据属性包含一个数据值的位置。这个位置能够读取和写入值。数据属性有 4 个描述他行为的特性:
若是你想要修改上述 4 个默认的数据属性,就须要使用 ECMAScript 的 Object.defineProperty() 方法。
该方法包含3个参数:属性所在的对象,属性名,描述符对象。描述符对象的属性必须在上述 4 个属性中。
var person = { name: '', }; // 不能修改属性的值 Object.defineProperty(person, "name",{ writable: false, value: "小生方勤" }); console.log(person.name); // "小生方勤" person.name = "方勤"; console.log(person.name); // "小生方勤"
访问器属性不包含数据值,他们包含一对 getter 和 setter 函数(非必须)。在读写访问器属性的值的时候,会调用相应的 getter 和 setter 函数,而咱们的 vue 就是在 getter 和 setter 函数中增长了咱们须要的操做。
须要注意的是【value 或 writable】必定不能和【get 或 set】共存。
访问器属性有如下 4 个特性:
接下来给个例子:
var person = { _name : "小生方勤" }; Object.defineProperty(person, "name", { //注意 person 多定义了一个 name 属性 set: function(value){ this._name = "来自 setter : " + value; }, get: function(){ return "来自 getter : " + this._name; } }); console.log( person.name ); // 来自 getter : 小生方勤 person.name = "XSFQ"; console.log( person._name ); // 来自 setter : XSFQ console.log( person.name ); // 来自 getter : 来自 setter : XSFQ
若是以前都不清楚有 Object.defineProperty() 方法,建议你看《JavaScript 高级程序设计》的 139 - 144 页。
咱们在源码随处能够 this.set=Object.create(null) 这样的赋值。为何这样作呢?这样写的好处就是不须要考虑原型链上的属性,能够真正的建立一个纯净的对象。
首先 Object.create 能够理解为继承一个对象,它是 ES5 的一个特性,对于旧版浏览器须要作兼容,基本代码以下:
if (!Object.create) { Object.create = function (o) { function F() {} // 定义了一个隐式的构造函数 F.prototype = o; return new F(); // 其实仍是经过new来实现的 }; }
其实这点应该是默认你须要知道的,不过鉴于以前有人问过我一些相关的问题,我稍微讲一下。
exportdefault 和 export 的区别
在一个文件或模块中 export 能够有多个,但 exportdefault 仅有一个
经过 export 方式导出,在导入时要加 { },而 exportdefault 则不须要
1.export
//a.js
export const str = "小生方勤";
//b.js
import { str } from 'a'; // 导入的时候须要花括号
2.export default
//a.js
const str = "小生方勤";
export default str;
//b.js
import str from 'a'; // 导入的时候无需花括号
exportdefaultconsta=1; 这样写是会报错的哟。
这个一笔带过:
箭头函数中的 this 指向是固定不变的,便是在定义函数时的指向
Class 能够经过 extends 关键字实现继承,这比 ES5 的经过修改原型链实现继承,要清晰和方便不少。
class staff { constructor(){ this.company = "ABC"; this.test = [1,2,3]; } companyName(){ return this.company; } } class employee extends staff { constructor(name,profession){ super(); this.employeeName = name; this.profession = profession; } } // 将父类原型指向子类 let instanceOne = new employee("Andy", "A"); let instanceTwo = new employee("Rose", "B"); instanceOne.test.push(4); // 测试 console.log(instanceTwo.test); // [1,2,3] console.log(instanceOne.companyName()); // ABC // 经过 Object.getPrototypeOf() 方法能够用来从子类上获取父类 console.log(Object.getPrototypeOf(employee) === staff) // 经过 hasOwnProperty() 方法来肯定自身属性与其原型属性 console.log(instanceOne.hasOwnProperty('test')) // true // 经过 isPrototypeOf() 方法来肯定原型和实例的关系 console.log(staff.prototype.isPrototypeOf(instanceOne)); // true
super 关键字,它在这里表示父类的构造函数,用来新建父类的 this 对象。
子类必须在 constructor 方法中调用 super 方法,不然新建实例时会报错。这是由于子类没有本身的 this 对象,而是继承父类的 this 对象,而后对其进行加工。
- 只有调用 super 以后,才可使用 this 关键字,不然会报错。这是由于子类实例的构建,是基于对父类实例加工,只有 super 方法才能返回父类实例。
`super` 虽然表明了父类 `A` 的构造函数,可是返回的是子类 `B` 的实例,即` super` 内部的 `this ` 指的是 `B`,所以 `super()` 在这里至关于 A.prototype.constructor.call(this)
ES5 的继承,实质是先创造子类的实例对象 this,而后再将父类的方法添加到 this 上面( Parent.apply(this))。
ES6 的继承机制彻底不一样,实质是先创造父类的实例对象 this (因此必须先调用 super() 方法),而后再用子类的构造函数修改 this。
对最新动态了解的人就会知道,在下一个版本的 Vue 中,会使用 proxy 代替 Object.defineProperty 完成数据劫持的工做。
尤大说,这个新的方案会使初始化速度加倍,于此同时内存占用减半。
proxy 对象的用法:
var proxy = new Proxy(target, handler);
new Proxy() 即生成一个 Proxy 实例。target 参数表示所要拦截的目标对象,handler 参数也是一个对象,用来定制拦截行为。
var proxy = new Proxy({}, { get: function(obj, prop) { console.log('get 操做') return obj[prop]; }, set: function(obj, prop, value) { console.log('set 操做') obj[prop] = value; } }); proxy.num = 2; // 设置 set 操做 console.log(proxy.num); // 设置 get 操做 // 2
除了 get 和 set 以外,proxy 能够拦截多达 13 种操做。
注意,proxy 的最大问题在于浏览器支持度不够,IE 彻底不兼容。
假若你基本不了解 ES6, 推荐下面这个教程:
阮一峰 ECMAScript 6 入门:http://es6.ruanyifeng.com/
由于以前我特地写了一篇文章来解释原型链,因此这里就不在讲述了:
原型链:http://www.javashuo.com/article/p-skwfkoft-kn.html
这里我先放一段 Vue 源码中的 once 函数。这就是闭包调用 —— 函数做为返回值:
/** * Ensure a function is called only once. */ export function once (fn: Function): Function { let called = false return function () { if (!called) { called = true fn.apply(this, arguments) } } }
这个函数的做用就是确保函数只调用一次。
为何只会调用一次呢? 由于函数调用完成以后,其执行上下文环境不会被销毁,因此 called 的值依然在那里。
闭包究竟是什么呢。《JavaScript 高级程序设计》的解释是:
闭包是指有权访问另外一个函数做用域中的变量的函数。建立闭包的常见方式,就是在一个函数内部建立另外一个函数。
简单讲,闭包就是指有权访问另外一个函数做用域中的变量的函数。
给两段代码,若是你知道他们的运行结果,那么说明你是了解闭包的:
// 第一段 var num = 20; function fun(){ var num = 10; return function con(){ console.log( this.num ) } } var funOne = fun(); funOne(); // 20 // 第二段 var num = 20; function fun(){ var num = 10; return function con(){ console.log( num ) } } var funOne = fun(); funOne(); // 10
所谓"柯里化",就是把一个多参数的函数,转化为单参数函数。
先说说我以前遇到过得一个面试题:
如何使 add(2)(3)(4)() 输出 9
在那次面试的时候,我仍是不知道柯里化这个概念的,因此当时我没答上。后来我才知道这能够用函数柯里化来解,即:
function add(num){ var sum=0; sum= sum+num; return function tempFun(numB){ if(arguments.length===0){ return sum; }else{ sum= sum+ numB; return tempFun; } } }
那这和 Vue 有什么关系呢?固然是有关系的:
咱们是否常常这样写判断呢?
if( A ){ // code }else if( B ){ // code }
这个写法没什么问题,但是在重复的出现这种相同的判断的时候。这个就显得有点不那么智能了。这个时候函数柯里化就能够排上用场了。
由于 Vue 能够在不一样平台运行,因此也会存在上面的那种判断。这里利用柯里化的特色,经过 createPatchFunction 方法把一些参数提早保存,以便复用。
// 这样不用每次调用 patch 的时候都传递 nodeOps 和 modules export function createPatchFunction (backend) { // 省略好多代码 return function patch (oldVnode, vnode, hydrating, removeOnly) { // 省略好多代码 } }
四个概念:
同步任务:即在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
异步任务:指的是不进入主线程,某个异步任务能够执行了,该任务才会进入主线程执行
macrotask:主要场景有 主代码块、setTimeout、setInterval 等
这一点网上教程已经不少了,再由于篇幅的问题,这里就不详细说了。
推荐一篇文章,说的很细致:
JavaScript 执行机制:http://www.javashuo.com/article/p-cmtsxzha-h.html
这篇文章讲到这里就结束了。不过有一点我须要在说一篇,这篇文章的定位并非面面俱到的将全部知识都讲一遍,这不现实我也没这个能力。
我只是但愿经过这篇文章告诉你们一个观点,要想看源码,一些必备的 JavaScript 基础知识必需要扎实,不然你会举步维艰。
愿你天天都有进步。
最近总有朋友问我 Vue 相关的问题,所以接下来我会输出 9 篇 Vue 相关的文章,但愿对你们有必定的帮助。我会保持在 7 到 10 天更新一篇。
【前端词典】Vuex 注入 Vue 生命周期的过程(完成)
【前端词典】浅析 Vue 响应式原理
【前端词典】新老 VNode 进行 patch 的过程
【前端词典】如何开发功能组件并上传 npm
【前端词典】从这几个方面优化你的 Vue 项目
【前端词典】从 Vue-Router 设计讲前端路由发展
【前端词典】在项目中如何正确的使用 Webpack
【前端词典】Vue 服务端渲染
建议你关注个人公众号,第一时间就能够接收最新的文章。
若是你想加群交流,也能够添加有点智能的机器人,自动拉你进群:
随意吧,开心就好