Vue的响应式

前言

这周阅读了《深入浅出Vue.js》的第一篇,梳理了一下Vue的响应式。

响应式总览

我们知道响应式是发布订阅模式的实现,基于ES5的Object.defineProperty追踪对象变化。每当从data的key中读取数据时,get函数被触发,进行依赖收集;每当往data的key中设置数据时,set函数被触发,通知依赖更新。 响应式通过Observer、Watcher、Dep三兄弟来实现的。我们来分析清理三者是什么,他们之间的关系。

对象响应式

如何收集依赖

如果只是把Object.defineProperty进行封装,那其实并没什么实际用处,真正有用的是收集依赖,依赖其实它是一个Watcher实例,保存在Dep.target上。

对象的每个key都有一个dep,dep就是管理依赖的对象,可以进行依赖收集和通知依赖数据变化了;dep.depend用来存储当前key的依赖,dep.notify来通知所有依赖,数据变化了。

function defineReactive(
  obj: Object | any,
  key: string,
  val: any
) {
  const dep = new Dep()
  let childOb: any = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = 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 = val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      val = newVal
      childOb = observe(newVal)
      // 通知所有依赖,执行对应的方法
      dep.notify()
    }
  })
}


复制代码

什么是Dep

封装的Dep类,它专门帮助我们管理依赖,进行依赖的收集、删除或者发送通知

/* @flow */

import type Watcher from './watcher'
import { remove } from './utils';

let uid = 0
export default class Dep {
  static target?: Watcher | null;
  id: number;
  subs: Array<Watcher>;

  constructor() {
    this.id = uid++
    this.subs = []
  }

  addSub(sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub(sub: Watcher) {
    remove(this.subs, sub)
  }

  depend() {
    // 收集dep
    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
const targetStack: Array<any> = []

export function pushTarget(target?: Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

复制代码

什么是Watcher

收集谁,也就是属性变化了,通知谁;通知到对象可能一个用户写的一个watch,也可能是模板,这时抽象出一个Watcher类

Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。watcher的经典使用vm.$watch("a.b.c", function(newVal, oldVal) {})

这段代码表示data.a.b.c属性发生变化时,触发第二个参数中的函数。

这个功能的实现只要把watcher实例添加到data.a.b.c属性的Dep中,当data.a.b.c属性变化时,通知Watcher执行参数中的回调函数。

class Watcher {
  constructor(
    vm: any,
    expOrFn: string | Function,
    cb: Function,
    options: any
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    if (options) {
      this.deep = !!options.deep
    } else {
      this.deep  = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.deps = [] // 订阅了哪些dep
    this.newDeps = []
    this.depIds = new Set() // 记录当前watcher已经订阅的dep,防止重复订阅
    this.newDepIds = new Set()
    this.expression = ''
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = Function.prototype
      }
    }
    this.value = this.get()
  }


  get() {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      throw e
    } finally {
      if (this.deep) {
        traverse(value)
      }
      popTarget()
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   */
  addDep(dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }


  update() {
    const oldValue = this.value
    const value = this.get()
    this.cb.call(this.vm, value, oldValue)
  }
  
  depend() {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}
复制代码

Observer

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
    // 为什么创建dep
    // object里面有新增删除,或者数组有变更方法,通过dep通知变更
    this.dep = new Dep()
    this.vmCount = 0
    // 设置一个__ob__属性引用当前Observer实例
    def(value, '__ob__', this)
    // 判断类型
    if (Array.isArray(value)) {
      // 替换数组对象原型
      value.__proto__ = arrayMethods
      // 如果数组里面的元素还是对象
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk(obj: any) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }

  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
复制代码

数组响应式

拦截器覆盖Array.prototype,拦截数组的7种常用方法,push、pop、shift、unshift、splice、sort和reverse

const arrayProto = Array.prototype
export const arrayMethod = Object.create(arrayProto)
;['push','pop','shift','unshift','splice','sort','reverse'].forEach(method=>{
  const original = arrayProto[method]
  Object.defineProperty(arrayMethod, method, {
    value: 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)
      ob.dep.notify(); // 向依赖发送消息
      return result
    },
    enumerable: false,
    writable: true,
    configurable: true
  })
})
复制代码

Vue

首先,初始化的时候,将数据进行设置get和set方法。new Watcher的时候,传入回调函数和对应的用到的data的key,首先触发一个get方法,dep将Watcher实例收集到dep.subs数组中,等待set触发,执行Watcher实例的回调函数。

class Vue {
  private $options: any
  constructor(options: any) {
    this.$options = options;
    this._init()
    // 测试代码
    this.$options.mounted && this.$options.mounted.call(this)
  }
  // 初始化
  _init() {
    this.initState(this)
  }

  initState(vm: any) {
    let data = vm.$options.data;
    vm._data = data
    // data代理
    const keys = Object.keys(data);
    let i = keys.length;
    while (i--) {
      const key = keys[i];
      proxy(vm, `_data`, key);
    }
    observe(data)

  }

  // 用户watcher
  $watch(expOrFn: string | Function, cb: any, options?: any) {
    const vm = this
    options = options || {}
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    return function unwatchFn() {
      watcher.teardown()
    }
  }

}
// 测试代码
const options = {
  data: {
    a: {
      b: 3,
      c: [1, 2, { a: 3 }]
    }
  },
  mounted() {
    this.$watch('a.c', (val: any) => {
      document.getElementById('root').textContent = val
    }, {
      immediate: false,
      deep: true
    })
    this.$watch(() => {
      return this.a.c[2].a
    }, (val: any) => {
      console.log(val, '监听到了---zzzz');
    })
  }
}
const vm: any = new Vue(options)
vm.a.b++
复制代码

总结

1. 对象响应式 Object可以通过Object.definProperty将属性转换成getter/setter的形式来追踪变化。读取属性触发getter,修改数据时触发setter。

我们在getter中收集哪些依赖使用了数据,在setter被触发时吗,通知收集的依赖数据发生变化了。

所谓的依赖就是Watcher,只有Watcher触发了getter才会收集依赖,哪个watcher触发了,就把哪个收集到dep中。全局设置一个唯一的watcher,当前watcher正在读取数据时,把这个watcher收集到dep中。

数据发生了变化是,触发setter,dep通知所有的watcher,触发更新。

2.数组响应式

Array追踪变化的方式不一样。因为它通过方法来改变内容,所以通过创建拦截器去覆盖数组原型方式来追踪变化。

由于数组要在拦截器中向依赖发消息,所以依赖不能保存在defineProperty中,我们将依赖保存在Observer实例上。

每个侦测了变化的数据都标上__ob__,并把this(Observer实例)保存在__ob__上。起到2个作用:1.标记被侦测了变化的数据。2.在拦截器中可以通知更新。

数组新增的项,我们需要对新增的数据做侦测变化。

最后

代码地址:github.com/zhuye1993/m…

猜你喜欢

转载自juejin.im/post/7042872778327867400