所谓渐进式框架,就是把框架分层。
最核心的部分是视图层渲染,然后往外是组件机制,在这个基础上再加入路由机制,再加入状态管理,最外层是构建工具。
第一篇 变化侦测
使用 Object.defineProperty
对数据的 key 进行数据劫持,每当从 data 的 key 中读取数据时,get 函数被触发;每当往 data 的 key 中设置数据时,set 函数被触发。
function defineReactive (data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
return val
},
set: function (newVal) {
if(val === newVal){
return
}
val = newVal
}
})
}
在 getter 中收集依赖,在 setter 中触发依赖。
如何收集依赖
export default class Dep {
constructor () {
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
Watcher
Watcher 是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。
export default class Watcher {
constructor (vm,expOrFn,cb) {
this.vm = vm
// 执行this.getter(),就可以读取data.a.b.c的内容
this.getter = parsePath(expOrFn)
this.cb = cb
this.value = this.get()
}
get() {
window.target = this
let value = this.getter.call(this.vm, this.vm)
window.target = undefined
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
依赖注入到 Dep 中后,每当 data.a.b.c 的值发生变化时,就会让依赖列表中所有的依赖循环触发 update 方法,也就是 Watcher 中的 update 方法。而 update 方法会执行参数中的回调函数,将 value 和 oldValue 传到参数中。
/**
* 解析简单路径
*/
const bailRE = /[^\w.$]/
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
总结
- Object 可以通过 Object.defineProperty 将属性转换成 getter/setter 的形式来追踪变化。读取数据时会触发getter,修改数据时会触发 setter。
- 在 getter 中收集有哪些依赖使用了数据。当 setter 被触发时,去通知 getter 中收集的依赖数据发生了变化
- 收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep ,它用来收集依赖、删除依赖和向依赖发送消息等。
- 所谓的依赖,其实就是 Watcher 。只有 Watcher 触发的 getter才会收集依赖,哪个 Watcher 触发了getter,就把哪个 Watcher 收集到 Dep 中。当数据发生变化时,会循环依赖列表,把所有的 Watcher 都通知一遍。
- Watcher 的原理是先把自己设置到全局唯一的指定位置(例如window.target ),然后读取数据。因为读取了数据,所以会触发这个数据的 getter。接着,在 getter 中就会从全局唯一的那个位置读取当前正在读取数据的 Watcher ,并把这个 Watcher 收集到 Dep中去。通过这样的方式,Watcher 可以主动去订阅任意一个数据的变化。
- 此外,我们创建了 Observer 类,它的作用是把一个 object 中的所有数据(包括子数据)都转换成响应式的,也就是它会侦测 object 中所有数据(包括子数据)的变化。
- 由于在 ES6 之前 JavaScript 并没有提供元编程的能力,所以在对象上新属性和删除属性都无法被追踪到。
- Data 通过 Observer 转换成了getter/setter的形式来追踪变化。
- 当外界通过 Watcher 读取数据时,会触发 getter 从而将 Watcher 添加到依赖中。
- 当数据发生了变化时,会触发 setter,从而向 Dep 中的依赖( Watcher )发送通知。
- Watcher 接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。
数组
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 原型
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
}
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
*/
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
/**
* Augment a target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
用hasProto 判断浏览器是否支持 proto :如果支持,则使用protoAugment 函数来覆盖原型;如果不支持,则调用copyAugment 函数将拦截器中的方法挂载到value 上。
总结
Array 追踪变化的方式和Object 不一样。因为它是通过方法来改变内容的,所以我们通过创建拦截器去覆盖数组原型的方式来追踪变化。
Array 收集依赖的方式和 Object 一样,都是在 getter 中收集。但是由于使用依赖的位置不同,数组要在拦截器中向依赖发消息,所以依赖不能像 Object 那样保存在 defineReactive 中,而是把依赖保存在了 Observer 实例上。
在 Observer 中,我们对每个侦测了变化的数据都标上印记 ob,并把 this (Observer 实例)保存在 ob 上。这主要有两个作用,一方面是为了标记数据是否被侦测了变化(保证同一个数据只被侦测一次),另一方面可以很方便地通过数据取到 ob ,
从而拿到Observer 实例上保存的依赖。当拦截到数组发生变化时,向依赖发送通知。
除了侦测数组自身的变化外,数组中元素发生的变化也要侦测。我们在Observer 中判断如果当前被侦测的数据是数组,则调用 observeArray 方法将数组中的每一个元素都转换成响应式的并侦测变化。
除了侦测已有数据外,当用户使用 push 等方法向数组中新增数据时,新增的数据也要进行变化侦测。我们使用当前操作数组的方法来进行判断,如果是 push 、unshift 和 splice 方法,则从参数中将新增数据提取出来,然后使用observeArray 对新增数据进行变化侦测。
由于在ES6之前,JavaScript并没有提供元编程的能力,所以对于数组类型的数据,一些语法无法追踪到变化,只能拦截原型上的方法,而无法拦截数组特有的语法,例如使用 length 清空数组的操作就无法拦截。