浅谈Vue响应式原理(个人向)

782 阅读11分钟

浅谈响应式原理

  • 关于响应式原理,其实对于vue这样一个大项目来说肯定有很多细节和优化的地方,在下水平精力有限,不能一一尝试探索,本文仅以将响应式的大致流程个人向的梳理完毕为目的。
  • 对于响应式主要分为三大部分来分析,1.响应式对象;2.依赖收集;3.派发更新。 最后将是个人的分析。

1、响应式对象 (Object.defineProperty)

我们先从初始化数据开始,再介绍几个比较核心的方法。

1.1、initState

文件位置:src/core/instance/state.js

在Vue的初始化阶段,_init⽅法执⾏的时候,会执⾏initState(vm)⽅法,。是对props、methods 、data、computed和wathcer等属性做了初始化操作。

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

此处我们仅大致看一下对于props和data的初始化。

1.1.1 initProps

源码略

作用:遍历定义的props配置。遍历的过程主要做两件事情:⼀个是调用defineReactive ⽅法把每个prop对应的值变成响应式;另⼀个是通过proxy 把vm._props.xxx 的访问代理到 vm.xxx上.

1.1.2 initData

源码略

作用:两件事,⼀个是对定义data函数返回对象的遍历,通过proxy 把每⼀个值 vm._data.xxx都代理到vm.xxx上;另⼀个是调⽤observe⽅法观测整个data的变化,把data 也变成响应式,

  • 小结:这里可以看到再初始化props和data的时候都调用了proxy(代理)和将其变成响应试的过程,虽然props时用的defineReactive,但追究其中还是调用了observer这个方法将数据变成响应式。接下来我们看一下proxy和observer.

1.2、proxy

源码位置:src/core/instance/state.js

作⽤是把props和data上的属性代理到vm实例上,,通过Object.defineProperty把 target[sourceKey][key]的读写 变成了对target[key]的读写。

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

经过这种代理的操作。所以对于props⽽⾔,对vm._props.xxx的读写变成了vm.xxx 的读写,⽽对于vm._props.xxx我们可以访问到定义在 props中的属性,所以我们就可 以通过 vm.xxx访问到定义在props中的xxx属性了。同理,对于data⽽⾔,对vm._data.xxxx的读写变成了对vm.xxxx的读写,⽽对于vm._data.xxxx我们可以访问到定义在data函数返回对象中的属性,所以我们就可以通过vm.xxxx访问到定义在data函数返回对 象中的xxxx属性了。

1.3、oberver

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

observe的功能就是⽤来监测数据的变化

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

在这里vue源码的注释已经说的很明白。通过这个函数会给⾮VNode的对象类型数据添加⼀个Observer ,如果已经添加过则直接返回,否则在满⾜⼀定条件下去实例化⼀个Observer对象实例。下面看一下这个Observer类的具体内容

1.3.1 Observer

作⽤是给对象的属性添加 getter 和 setter,⽤于依赖收集和派发更新

 /**
   * Observer class that is attached to each observed  //这个类是附加再每个被observed重
   * object. Once attached, the observer converts the target//对象一旦被驱动,观察者就转换(观察到这个)目标
   * objects property keys into getter/setters that //将对象属性键放入getter/setter中
   * collect dependencies and dispatch updates. //收集依赖项并分发更新。!!!!!重要
 **/
  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)
      }
    }
  
    /**
     * Walk through all properties and convert them into
     * getter/setters. This method should only be called when
     * value type is Object.
     */
    walk (obj: Object) {
      const keys = Object.keys(obj)
      for (let i = 0; i < keys.length; i++) {
        defineReactive(obj, keys[i])
      }
    }
  
    /**
     * Observe a list of Array items.
     */
    observeArray (items: Array<any>) {
      for (let i = 0, l = items.length; i < l; i++) {
        observe(items[i])
      }
    }
  }

Observer的构造函数逻辑很简单,⾸先实例化Dep对象,接着通过执⾏def函数把⾃⾝实例添加到数据对象 value的 ob 属性上,

源码位置:src/core/util/lang.js

 /**
 * Define a property.
 */
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

def函数是⼀个⾮常简单的Object.defineProperty的封装,这就是为什么我在开发中输出data 上对象类型的数据,会发现该对象多了⼀个__ob__的属性。

继续看Observer 的构造函数,不得不说Vue的源码注释还是相当好的,大致翻译我写在上面了,请关注一下,然后看这个类的具体操作包括

它会对value做判断,对于数组会调⽤observeArray⽅法,否则对纯对象调⽤walk⽅法。可以看到observeArray是遍历数组再次调⽤observe⽅法,⽽walk⽅法是遍历对象的key调⽤defineReactive⽅法,

1.4、defineReactive

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

defineReactive的功能就是定义⼀个响应式对象,给对象动态添加 getter 和 setter. 源码略了,太长了,有兴趣的可自行查看,下面贴一个自己学着写的简易版,大致是这么个意思

//定义响应式
    definReactive(obj,key,value){
        let that = this
        let dep = new Dep() //每个变化的数据,都会对应一个数组,这个数组是存放所有更新的操作
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:true,
            get(){
                Dep.target && dep.addSub(Dep.target)
                return value;
            },
            set(newValue){ //当给data属性中设置值。更改获取属性的值
                if(newValue!=value){
                    //这里的this不是实例
                    that.observer(newValue) //如果是对象继续劫持
                    value=newValue
                    dep.notify() //通知所有人 数据更新了
                }
            },
        })
    }

函数最开始初始化Dep对象的实例,接着拿到obj的属性描述符,然后对⼦对 象递归调⽤observe ⽅法,这样就保证了⽆论obj的结构多复杂,它的所有⼦属性也能变成响应式的对象,

  • 小结:所谓响应式对象的总体就是利⽤Object.defineProperty给数据添加了getter和setter,⽬的就是为了在我们访问数据以及写数据的时候能⾃动执⾏⼀些逻辑,究其核心就是使用defineReactive这个方法进行处理,同时也将dep的添加subs方法存放在了get 中,将dep的notify方法放在set中。从而为后面的依赖收集和派发更新提供了入口?(不知道怎么形容好,,,,,)。

  • 前置:所谓依赖收集其实和后面的派发更新完全是以watcher和dep为核心的关系。 接下来我们根绝不同的目的来大致看一下这两个方法,

2、依赖收集(getter)

我们去回看 defineReactive方法,对于依赖收集我们关注两点,⼀个是const dep = newDep()实例化⼀个Dep的实例,另⼀个是在get函数中通过dep.depend做依赖收集。

2.1、Dep

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

Dep是整个 getter 依赖收集的核⼼ 对于依赖收集主要是(dep.depend()),派发更新主要是(dep.notify())

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  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 () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs arent sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

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

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

Dep是⼀个Class,它定义了⼀些属性和⽅法,这⾥需要特别注意的是它有⼀个静态属性target , 这是⼀个全局唯⼀Watcher ,这是⼀个⾮常巧妙的设计,因为在同⼀时间只能有⼀个全局的Watcher 被计算,另外它的⾃⾝属性subs也是Watcher的数组。Dep实际上就是对Watcher的⼀种管理,Dep脱离Watcher单独存在是没有意义的,为了完整地讲清楚依赖收集过程,我们有必要看⼀下Watcher的⼀些内容

2.2、watcher

源码位置: src/core/observer/watcher.js 源码太长略 下面贴一个自己学着写的简易版 注释已经和很详细了,

//观察者 的目的就是给需要观察的元素天机一个观察这,当数据变化后执行相对应的方法
class Watcher{
    constructor(vm,expr,cb){
        this.vm=vm
        this.expr=expr
        this.cb=cb;
        //先获取一下老的值
        this.value  = this.get();
    }

    getVal(vm,expr){ //获取实例上对应的数据
        expr= expr.split('.');
        return expr.reduce((prev,next)=>{ //vm.$data.a
            return prev[next]
        },vm.$data)
    }
    get (){

        //
        Dep.target=this //这个地方就是上面在1.3.1 Observer中注释中说到的大致意思,要将所有被观察者的数据都要放在dep的target中后面,dep会将这个target存起来,等到notify时一个个分发出去。然后走这里的update方法,去更新视图
        
        //
        let value= this.getVal(this.vm,this.expr);


        Dep.tatget=null

        return value
    }
    //对外暴露的方法
    update(){
        let newValue = this.getVal(this.vm,this.expr);
        let oldValue = this.value;
        if(newValue!=oldValue){
            this.cb(newValue); //调用watch的callback
        }
    }
}

//用新值和老值对比,如果变化就调用更新方法

  • 小结:收集依赖的⽬的是为了当这些响应式数据发送变化,触发它们的setter的时候,我们把这个过程叫派发更新,其实Watcher和Dep就是⼀个⾮常经典的观察者设计模式的实现。

3、派发更新(setter)

我们继续回去看 defineReactive方法,对于派发更新集我们关注两点,⼀个是childOb =!shallow &&observe(newVal),如果shallow为false的情况,会对新设置的值变成⼀个响应式对象;另⼀个是 dep.notify(),通知所有的订阅者,

  1. 这里其实对越整个响应式的大致流程已经走完,但是这里Vue对于派发更新引⼊了⼀个队列的概念,这也是Vue 在做派发更新的时候的⼀个优化的点,它并不会每次数据改 变都触发watcher的回调,⽽是把这些watcher先添加到⼀个队列⾥,然后在nextTick后执⾏flushSchedulerQueue(源码地址:src/core/observer/scheduler.js)。
  2. 对于这个队列优化部分,和flushSchedulerQueue这个很关键的函数,这篇文章不去细讲,因为关系到我们常用的$nextTick之类的,如果有精力的化,计划再写个文章细说吧。
  • 小结:我们对Vue 数据修改派发更新的过程也有了认识,实际上就是当数据发⽣变化的 时候,触发 setter逻辑,把在依赖过程中订阅的的所有观察者,也就是watcher,都触发它们的update过程,这个过程⼜利⽤了队列做了进⼀步优化(略)

好了,到这里我们总算是把整个响应式的大致流程走完了,下面我先放一个我自己做的关系图,根据图我再做个总结

总结

  • 首先,我们可以看出针对所谓响应式最基础的核心还是我们入门记得理解Object.defineProperty,Vue依靠它实现了数据变成响应式对象。依次为基础我们才能再通过数据劫持和发布订阅者去真正实现数据的响应式。
  • 其次,就是Vue通过observe与defineReactive的功能进行遍历循环去实现将数据都使用Object.defineProperty变成响应式。注意的是,在defineReactive过程中的我们就将Dep实例化,并且将depend和notify分别放在了setter和getter中。以实现数据的劫持(不敢断定,貌似是)?
  • 而后就是Dep和Watcher上演观察订阅模式的实践了。具体就是,
    1. 我们在getter中去获取dep中的target并通过depen方法存起来,而这个target其实就是来自与Watcher在出事数据中就赋予的,也可以说在这个过程中,把当前的watcher订阅到这个数据持有的dep的subs中,这个⽬的是为后续数据变化时候能通知到哪些subs准备。
    2. 我们在setter中会走Dep的notify,此时我们就会遍历所有的subs然后执行watcher中的update的过程,继而更新数据和视图,完成整个流程的过程,当然在notify过程中Vue做的各种优化和操作,又是一个大内容暂不讨论,
  • 最后,我们完成了整个流程。(MM呀 终于完事了。。。)

后记

  1. 关于源码的学习,说实话,很早之前就看过vue源码的资料,内容和关系脑图一直是手写在A4纸上,我阅读学习的资料是Vue源码和一个好久之前忘了来源的一个PDF讲Vue源码的。这里说明一下,文章引用部分来自那个PDF,人家写的很干练了就直接引用了。当时学习就是一边看PDF一遍看源码学习,讲道理看了好多遍,算是有些理解.其实看起来也真的头疼。
  2. 为什么写这篇博客(电子知识梳理),是年后网上看到了某培训机构两年前的MVVM源码公开课内容,用两个小时看完,继而又花了三个小时跟着撸了一遍,在撸完的那一刹那,我感觉最起码这响应式基础的流程在我心中真的是豁然开朗。我就想着赶紧记录下来,这也是总结了一句话。纸上得来终觉浅,绝知此事要躬行。。哈哈,,,,如果有人有兴趣看看简易版MMVM的代码,也就是文章中我贴的,可以直接下载看看,我就厚脸皮的贴上地址了"github.com/jia-03/self…"
  3. 讲道理,这文章我吧我之前的Xmind文档整理书写到现在花了四个小时,好累啊,,,但是算是又过了一遍流程,也算是收获吧,也希望看到的你有收获。
  4. 如果哪里写的不对不好的,需要改进的希望能友善交流。俺第一次发掘金。
  5. 最后我放上,PDF版的原理图梳理,大佬梳理的更好,但是很多东西,在下能力有限,不能一次获取,还有我真的时不知道,PDF的出处,如果有任何知识自主的侵犯,希望作者能告知,我加上引用或是删除内容。。。。