学习Vue源码前的几项必要储备(一)
从接下来的一段时间里,Mg要进行阅读源码的工作。再阅读源码前,梳理一下准备工作。
7项重要储备
- Flow 基本语法
- 发布/订阅模式
- ES6+ 语法
- 原型链、闭包
- 函数柯里化
- event loop
1.flow
1.1 什么是flow
没有类型的静态检查是 JavaScript 语言的先天缺失,所有很多变量类型相关的问题只能在运行阶段暴露出来。为了使 JavaScript 语言的类型更加安全,业界的实践有 TypeScript;这些都需要你重新学习一套语言语法,然后由编译器把相应的代码编译成原生的 JavaScript 代码;在一个现有的系统中把当前代码改成 TypeScript 成本比较高,需要重写所有逻辑。
Facebook 推出的 Flow 是另一个思路。Flow 是一个静态类型检测工具;在现有项目中加上类型标注后,可以在代码阶段就检测出对变量的不恰当使用。Flow 弥补了 JavaScript 天生的类型系统缺陷。利用 Flow 进行类型检查,可以使你的项目代码更加健壮,确保项目的其他参与者也可以写出规范的代码;而 Flow 的使用更是方便渐进式的给项目加上严格的类型检测。那么这么好的 Flow,要怎么开始使用呢?
1.2 基础类型检查
Flow 支持原始数据类型,有如下几种:
boolean number string null void( 对应 undefined )
如下使用:
let str:string = 'str'; // 重新赋值 str = 3 // 报错
1.3 更多详情有如下资料
官方文档:flow.org/en/
Flow 的使用入门:zhuanlan.zhihu.com/p/26204569
欲知详情,可进一步阅读
2.发布/订阅模式
Vue 的双向绑定机制采用数据劫持结合发布/订阅模式实现的: 通过 Object.defineProperty()
来劫持各个属性的 setter,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调。
而在Vue3.0中,使用proxy代替原来的Object.defineProperty()以实现发布订阅模式。
从Vue的源码来看,Vue的双向绑定主要做了2件事
- 数据劫持
- 添加观察者
// 老版本通过 Object.defineProperty 递归可以实现 // src/core/observer/index.js Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() } if (Array.isArray(value)) { dependArray(value) } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val if (newVal === value || (newVal !== newVal && value !== value)) { return } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } })
这里就是劫持了对象的get和set方法。在所代理的属性的get方法中,当dep.Target
存在的时候会调用 dep.depend().
划重点:2行代码
- Object.defineProperty
- dep.depend()
// 最新版可以通过 Proxy 实现 Proxy(data, { get(target, key) { return target[key]; }, set(target, key, value) { let val = Reflect.set(target, key, value); _that.$dep[key].forEach(item => item.update()); return val; } })
从上面的代码看出,无非就劫持了对象的get和set方法,在数据劫持之外最重要的部分就是 Dep
和 Watcher
,这其实是一个观察者模式。用最简单的代码实现以下 Vue 的观察者模式。
观察者模式实现:(源码精简)
// 观察者 class Dep { constructor() { this.subs = [] } addSub(sub) { this.subs.push(sub) } depend() { if (Dep.target) { Dep.target.addDep(this); } } notify() { this.subs.forEach(sub => sub.update()) } } // 被观察者 class Watcher { constructor(vm, expOrFn) { this.vm = vm; this.getter = expOrFn; this.value; } get() { Dep.target = this; var vm = this.vm; var value = this.getter.call(vm, vm); return value; } evaluate() { this.value = this.get(); } addDep(dep) { dep.addSub(this); } update() { console.log('更新, value:', this.value) } } // 观察者实例 var dep = new Dep(); // 被观察者实例 var watcher = new Watcher({x: 1}, (val) => val); watcher.evaluate(); // 观察者监听被观察对象 dep.depend() dep.notify()
划重点:3件事
- 通过
watcher.evaluate()
将自身实例赋值给Dep.target
- 调用
dep.depend()
将dep实例将 watcher 实例 push 到 dep.subs中 - 通过数据劫持,在调用被劫持的对象的 set 方法时,调用 dep.subs 中所有的
watcher.update()
从此。双向绑定完成。
3 ES6+ 语法
3.1 export default
和 export
的区别
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'; // 导入的时候无需花括号
3.2
箭头函数
主要提两点
- 箭头函数中的 this 指向是固定不变的,即是在定义函数时的指向
- 而普通函数中的 this 指向时变化的,即是在使用函数时的指向
3.3
箭头函数
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
方法才能返回父类实例。
ES5 和 ES6 实现继承的区别
ES5 的继承,实质是先创造子类的实例对象 this
,然后再将父类的方法添加到 this
上面(Parent.apply(this)
)。
ES6 的继承机制完全不同,实质是先创造父类的实例对象 this
(所以必须先调用 super()
方法),然后再用子类的构造函数修改 this
。
3.4
proxy
在Vue3.0版本的 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 完全不兼容。
......篇幅过长~大家都看累了吧,请转下一集!