所谓渐进式框架,就是把框架分层。
最核心的部分是视图层渲染,然后往外是组件机制,在这个基础上再加入路由机制,再加入状态管理,最外层是构建工具。
image.png

第一篇 变化侦测

使用 Object.defineProperty对数据的 key 进行数据劫持,每当从 data 的 key 中读取数据时,get 函数被触发;每当往 data 的 key 中设置数据时,set 函数被触发。

  1. function defineReactive (data, key, val) {
  2. Object.defineProperty(data, key, {
  3. enumerable: true,
  4. configurable: true,
  5. get: function () {
  6. return val
  7. },
  8. set: function (newVal) {
  9. if(val === newVal){
  10. return
  11. }
  12. val = newVal
  13. }
  14. })
  15. }

在 getter 中收集依赖,在 setter 中触发依赖。

如何收集依赖

  1. export default class Dep {
  2. constructor () {
  3. this.subs = []
  4. }
  5. addSub (sub) {
  6. this.subs.push(sub)
  7. }
  8. removeSub (sub) {
  9. remove(this.subs, sub)
  10. }
  11. depend () {
  12. if (Dep.target) {
  13. Dep.target.addDep(this)
  14. }
  15. }
  16. notify () {
  17. const subs = this.subs.slice()
  18. for (let i = 0, l = subs.length; i < l; i++) {
  19. subs[i].update()
  20. }
  21. }
  22. }
  23. Dep.target = null

Watcher

Watcher 是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。

  1. export default class Watcher {
  2. constructor (vm,expOrFn,cb) {
  3. this.vm = vm
  4. // 执行this.getter(),就可以读取data.a.b.c的内容
  5. this.getter = parsePath(expOrFn)
  6. this.cb = cb
  7. this.value = this.get()
  8. }
  9. get() {
  10. window.target = this
  11. let value = this.getter.call(this.vm, this.vm)
  12. window.target = undefined
  13. return value
  14. }
  15. update () {
  16. const oldValue = this.value
  17. this.value = this.get()
  18. this.cb.call(this.vm, this.value, oldValue)
  19. }
  20. }

依赖注入到 Dep 中后,每当 data.a.b.c 的值发生变化时,就会让依赖列表中所有的依赖循环触发 update 方法,也就是 Watcher 中的 update 方法。而 update 方法会执行参数中的回调函数,将 value 和 oldValue 传到参数中。

  1. /**
  2. * 解析简单路径
  3. */
  4. const bailRE = /[^\w.$]/
  5. export function parsePath (path: string): any {
  6. if (bailRE.test(path)) {
  7. return
  8. }
  9. const segments = path.split('.')
  10. return function (obj) {
  11. for (let i = 0; i < segments.length; i++) {
  12. if (!obj) return
  13. obj = obj[segments[i]]
  14. }
  15. return obj
  16. }
  17. }

总结

  • 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 并没有提供元编程的能力,所以在对象上新属性和删除属性都无法被追踪到。

image.png

  • Data 通过 Observer 转换成了getter/setter的形式来追踪变化。
  • 当外界通过 Watcher 读取数据时,会触发 getter 从而将 Watcher 添加到依赖中。
  • 当数据发生了变化时,会触发 setter,从而向 Dep 中的依赖( Watcher )发送通知。
  • Watcher 接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。

数组

  1. const methodsToPatch = [
  2. 'push',
  3. 'pop',
  4. 'shift',
  5. 'unshift',
  6. 'splice',
  7. 'sort',
  8. 'reverse'
  9. ]
  10. /**
  11. * Intercept mutating methods and emit events
  12. */
  13. methodsToPatch.forEach(function (method) {
  14. // cache original method
  15. const original = arrayProto[method]
  16. def(arrayMethods, method, function mutator (...args) {
  17. const result = original.apply(this, args)
  18. const ob = this.__ob__
  19. let inserted
  20. switch (method) {
  21. case 'push':
  22. case 'unshift':
  23. inserted = args
  24. break
  25. case 'splice':
  26. inserted = args.slice(2)
  27. break
  28. }
  29. if (inserted) ob.observeArray(inserted)
  30. // notify change
  31. ob.dep.notify()
  32. return result
  33. })
  34. })

使用拦截器覆盖Array 原型

  1. export class Observer {
  2. value: any;
  3. dep: Dep;
  4. vmCount: number; // number of vms that have this object as root $data
  5. constructor (value: any) {
  6. this.value = value
  7. this.dep = new Dep()
  8. this.vmCount = 0
  9. def(value, '__ob__', this)
  10. if (Array.isArray(value)) {
  11. if (hasProto) {
  12. protoAugment(value, arrayMethods)
  13. } else {
  14. copyAugment(value, arrayMethods, arrayKeys)
  15. }
  16. this.observeArray(value)
  17. } else {
  18. this.walk(value)
  19. }
  20. }
  21. }
  22. /**
  23. * Augment a target Object or Array by intercepting
  24. * the prototype chain using __proto__
  25. */
  26. function protoAugment (target, src: Object) {
  27. /* eslint-disable no-proto */
  28. target.__proto__ = src
  29. /* eslint-enable no-proto */
  30. }
  31. /**
  32. * Augment a target Object or Array by defining
  33. * hidden properties.
  34. */
  35. /* istanbul ignore next */
  36. function copyAugment (target: Object, src: Object, keys: Array<string>) {
  37. for (let i = 0, l = keys.length; i < l; i++) {
  38. const key = keys[i]
  39. def(target, key, src[key])
  40. }
  41. }

用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 清空数组的操作就无法拦截。