vue2源码----响应式

第一章:响应式原理

一、观察者模式

  • 观察者(订阅者) -- Watcher
    update():当事件发生时,具体要做的事情
  • 目标(发布者) -- Dep
    subs 数组:存储所有的观察者
    addSub():添加观察者
    notify():当事件发生,调用所有观察者的 update() 方法
  • 没有事件中心
// 目标(发布者)
// Dependency
class Dep {
  constructor() {
    // 存储所有的观察者
    this.subs = []
  }
  // 添加观察者
  addSub(sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 通知所有观察者
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
// 观察者(订阅者)
class Watcher {
  update() {
    console.log('update')
  }
}
// 测试
let dep = new Dep()
let watcher = new Watcher()
dep.addSub(watcher)
dep.notify()
  • 总结
    观察者模式是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。
    发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

二、响应式原理

  1. watcher在第一次视图绑定变量的时候(插值表达式、指令处理时)创建,每个key有自己的一个watcher。
  2. 创建watcher的时候应该加到对应的发布者里去,所以在watcher构造函数处调用dep.addSub(watcher)
  3. 发布者当数据变更时需要通知观察者dep.notify(),所以发布者的实例创建即new Dep()应该是在数据变更处,即Observer的walk里。
  4. 而watcher构造函数里为了能调用到Observer的walk里defineProperty的dep实例,可以借用this.oldValue = vm[key]去操作。所以可以把dep.addSub(watcher)写在getter里,并通过Dep.target控制只有第一次才addSub。
// 处理插值表达式
  compileText(node) {
    const regExp = /\{\{(.+?)\}\}/
    const value = node.textContent
    if (regExp.test(value)) {
      const key = RegExp.$1.trim()
      node.textContent = value.replace(regExp, this.vm[key])

      // 为当前的key创建watcher,当key变化时更新视图
      new Watcher(this.vm, key, (newValue) => {
        node.textContent = newValue
      })
    }
  }
class Watcher {
  // 每个数据变化都要触发更新,所以每个key对应一个watcher
  constructor(vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb
    
    // 创建watcher的时候应该加到对应的发布者里去,发布者当数据变更时需要通知观察者,所以发布者应该是在数据变更处,那么添加watcher也可以放在对应地方即Observer的walk里
    // 此处跟Observer里walk相关的也就获取旧数据,为了在walk里面触发添加watcher,可以把添加写在getter里,并通过Dep.target控制只有第一次才addSub

    // 把watcher对象记录到Dep类的静态属性target
    Dep.target = this
    // 触发get方法,在get方法中会调用addSub
    this.oldValue = vm[key]
    Dep.target = null
  }

  // 当数据变化的时候更新视图
  update() {
    let newValue = this.vm[this.key]
    if (this.oldValue === newValue) {
      return
    }

    this.cb(newValue)
  }
}
class Observer {
  constructor(data) {
    this.walk(data)
  }

  walk(data) {
    if (!data || typeof data !== 'object') {
      return
    }
    
    Object.keys(data).forEach(key => {
      const that = this
      const val = data[key] // 避免循环先存下来
      const dep = new Dep() // 负责收集依赖,当数据变化的时候通知观察者
      this.walk(val)
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get() {
          // 只有创建watcher时Dep.target才存在,所以只有创建watcher的时候获取数据才会触发添加watcher
          Dep.target && dep.addSub(Dep.target)

          return val
        },
        set(newVal) {
          if (newVal === val) {
            return
          }
          that.walk(newVal)
          val = newVal // val被闭包缓存了,此处修改的是当前作用域下的val,get返回的也是同一个val
          dep.notify() // 通知观察者调用update
        }
      })
    })
  }
}

一、对象监测

object.defineProperty只能观测到取值和改值,如果是增加或删除key是监测不到的,还得通过Vue.setVue.delete解决。

二、数组监测

数组的读取和整个替换是可以通过object.defineProperty监测到的,但数据内部元素的增删改查无法监测到。可以自行实现数组的增删改查方法,在里面实现监测和调用watcher。但数组下标的修改无法监测。
Array原型中可以改变数组自身内容的方法有7个,分别是:push,pop,shift,unshift,splice,sort,reverse

// 源码位置:/src/core/observer/array.js

const arrayProto = Array.prototype
// 创建一个对象作为拦截器,确保不是直接修改原型上的方法
export const arrayMethods = Object.create(arrayProto)

// 改变数组自身内容的7个方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]      // 缓存原生方法
  Object.defineProperty(arrayMethods, method, {
    enumerable: false,
    configurable: true,
    writable: true,
    value:function mutator(...args){
      const result = original.apply(this, args)
      return result
    }
  })
})

做好数组方法拦截器之后,还要让用户的数组实例默认都用这些修改后的方法。

  1. 因为数组实例本身没有push之类的方法,都是用原型上的,所以我们可以直接把实例的原型proto改成修改后的原型。
  2. 若不支持proto,则直接往实例上加修改后的方法。
// 源码位置:/src/core/observer/index.js
export class Observer {
  constructor (value) {
    this.value = value
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
    } else {
      this.walk(value)
    }
  }
}
// 能力检测:判断__proto__是否可用,因为有的浏览器不支持该属性
export const hasProto = '__proto__' in {}

const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

function protoAugment (target, src: Object, keys: any) {
  target.__proto__ = src
}

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])
  }
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容