3 / 23 看完这篇你一定会懂Vue响应式原理

前面的话

前端日问,巩固基础,不打烊!!!

源码系列一直是小柒想总结的部分,之后也会逐步更新。

解答

  • Observer :扮演的角色是发布者,主要作用调用defineReactive函数,在defineReactive函数中使用Object。defineProperty方法对对象的每一个属性添加get与set方法
  • Dep:是调度中心/ 订阅器,作用是收集观察者watcher,以及通知watcher进行更新。每个对象的属性都有自己的订阅器dep实例,用(dep.subs)存放所有订阅了该属性的watcher对象,当数据变化时,会遍历(dep.subs),通知所有的watcher,执行update方法。
  • Watcher: 观察者/订阅者。分为三种类型:渲染watcher、计算属性watcher、侦听器watcher。三种watcher的顺序: `计算属性watcher --> 侦听器watcher --> 渲染watcher 。原因:尽可能保证,在更新组件视图时,computed属性已经是最新的值,如果 渲染watcher 排在 计算属性watcher 前面,就会导致页面更新的时候 computed 值为旧数据。
响应式入口

在beforeCreate钩子函数后,我们就可以拿到props、data等数据,看一下代码如何实现:

// src/core/instance/state.js

export function initState(vm: Component) {
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)              // 初始化props
  if (opts.methods) initMethods(vm, opts.methods)        // 初始化methods
  if (opts.data) initData(vm)                            // 初始化data
  if (opts.computed) initComputed(vm, opts.computed)     // 初始化computed
  if (opts.watch) initWatch(vm, opts.watch)              // 初始化watch
  }
}

可以看到:propsmethodsdatacomputedwatcher都依次初始化。

我们看一下initData方法的具体实现:

// src/core/instance/state.js

function initData(vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
                    ? getData(data, vm)
                    : data || {}
  observe(data, true /* asRootData */)             // 给data做响应式处理
}

看到observe字眼有没有很激动。在这个方法里执行了observe(data, true),看看这个方法的具体实现:

// src/core/observer/index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {
  let ob: Observer | void
  ob = new Observer(value)
  return ob
}

直接返回了Observer构造函数的实例ob,并且将data作为参数传递。了解new原理的你一定知道,new一个实例,将会执行一遍其构造函数。

Observer

那我们看看这个Observer构造函数里做了什么:

// src/core/observer/index.js

export class Observer {
  value: any;
  dep: Dep;

  constructor (value: any) {
    value: any;
    this.dep = new Dep()
    def(value, '__ob__', this)    // def方法保证不可枚举
    this.walk(value)
  }

  // 遍历对象的每一个属性并将它们转换为getter/setter
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) { // 把所有可遍历的对象响应式化
      defineReactive(obj, keys[i])
    }
  }
}

export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean) {
  const dep = new Dep()         // 在每个响应式键值的闭包中定义一个dep对象

  // 如果之前该对象已经预设了getter/setter则将其缓存,新定义的getter/setter中会将其执行
  const getter = property && property.get
  const setter = property && property.set

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val         // 如果原本对象拥有getter方法则执行
      if (Dep.target) {                    // 如果当前有watcher在读取当前值
        dep.depend()                       // 那么进行依赖收集,dep.addSub
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val    // 先getter
      if (newVal === value || (newVal !== newVal && value !== value)) {   // 如果跟原来值一样则不管
        return
      }
      if (setter) { setter.call(obj, newVal) }         // 如果原本对象拥有setter方法则执行
      else { val = newVal }
      dep.notify()                                     // 如果发生变更,则通知更新,调用watcher.update()
    }
  })
}
  • constructor函数中,先给每个实例添加了一个dep实例,然后遍历对象(这里就是上面的参数data)的每一个属性,并且执行defineReactive(obj, keys[i])
  • defineReactive函数,给每个属性定义了一个dep对象,并且给每个属性都添加getset方法,分别用于收集依赖(dep.depend())触发依赖(dep.notify())。当Dep.target存在时,才会收集依赖。(Dep.target就是一个watcher)
Dep

上面的代码中多次用到Dep,对于依赖收集这个词也不陌生, 简单来说,收集依赖就是收集watcher。如何收集,我们接着看:

//  src/core/observer/dep.js
let uid = 0            // Dep实例的id,为了方便去重
export  class Dep {
  static target: ?Watcher; // 全局属性Dep.target
  id: number;
  subs: Array<Watcher>; // 存储watcher的数组

  constructor () {
    this.id = uid++
    this.subs = []  // 用来收集watcher
  }

// 添加一个watcher到subs数组中
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
// 移除一个watcher
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
//  调用watcher实例的addDep方法
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
// 通知数组中的每一个watcher实例进行更新
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Dep.target = null

//待处理的观察者队列
const targetStack = [];

export function pushTarget(_target) {
  //如果当前有正在处理的观察者,将他压入待处理队列
  if(Dep.target) {
    targetStack.push(Dep.target);
  }
  //将Dep.target指向需要处理的观察者
  Dep.target = _target;
}
 
export function popTarget() {
  //将Dep.target指向栈顶的观察者,并将他移除队列
  Dep.target = targetStack.pop();
}

Dep中首先定义了一个全局属性target,只能Dep类自己调用,创建这个Dep类时,Dep.taget == null。Dep.target 只是一个标记,存储是watcher实例。

Dep中提供了下面几个方法:

  • addSub: 接受一个Watcher实例对象,并将其添加到subs数组中
  • removeSub: 接受一个Watcher实例对象,只是从subs数组中移除这个对象
  • depend: 前面出现过,当Dep.target存在时(也就是当前Watcher实例存在时),调用该实例的addDep方法。addDep功能下面在说。
  • notify: 通知数组subs中的所有watcher实例,执行其update方法,完成更新操作。

代码最后还暴露了两个方法pushTargetpopTarget。 Dep.target 的值会在调用 pushTarget 和 popTarget 时被赋值,值为当前 watcher 实例对象。

Watcher

大家现在应该还有疑惑?Watcher是什么?它与Dep是什么关系? watcher实例的addDep方法功能是什么?看看Watcher类的实现:Watcher的代码比较多,小柒省略部分代码:

export default class Watcher {
    constructor(
        vm: Component,
        expOrFn: string | Function,   // expOrFn可以是字符串或者函数 , watch 的属性名称
        cb: Function,    // 回调函数
        options?: ?Object,  
        isRenderWatcher?: boolean // 是否是渲染watcher标志位,Vue 初始化时,为true
       ){
          this.vm = vm
          if(isRenderWatcher) {
		 	vm._watcher = this
		 }
		 vm._watcher.push(this)
		 if(options) {
		 	this.deep = !!options.deep; //是否启用深度监听
   		    this.user = !!options.user; //主要用于错误处理,侦听器 watcher的 user为true,其他基本为false
   			this.lazy = !!options.lazy; //惰性求值,当属于计算属性watcher时为true
   			this.sync = !!options.sync; //标记为同步计算,三大类型暂无
		 }else {
   			 this.deep = this.user = this.lazy = this.sync = false;
  		}
 		// 初始化各种属性
  		this.cb = cb; //观察者的回调 ,除了侦听器 watcher外,其他大多为空函数
  		this.id = ++uid$1; // uid for batching
  		this.active = true;
  		this.dirty = this.lazy; // for lazy watchers
  		this.deps = [];
  		this.newDeps = [];
  		this.depIds = new _Set();
 		this.newDepIds = new _Set();
 		// 解析expOrfn,赋值给this.getter
 		// 当是渲染watcher时,expOrfn是updateComonent,即重新渲染执行render(_update)
 		// 当是计算watcher时,expOrFn是计算属性的计算方法
 		// 当是侦听器watcher时,expOrFn是watch属性的名字,this.cb就是watch的handler属性
 		this.expression = expOrFn.toString(); 
		 
		// 上面说了expOrFn可以是字符串,可以是函数:
        // 什么时候会是字符串,例如我们正常使用的时候,watch: { x: fn }, Vue内部会将 `x` 这个key 转化为字符串
        // 什么时候会是函数,其实 Vue 初始化时,就是传入的渲染函数 new Watcher(vm, updateComponent, ...);

	//对于渲染watcher和计算watcher来说,expOrFn的值是一个函数,可以直接设置getter
  	//对于侦听器watcher来说,expOrFn是watch属性的名字,会使用parsePath函数,该函数返回watch属性的值(即返回一个函数)
        if (typeof expOrFn === 'function') {
            this.getter = expOrFn
        } else {
            this.getter = parsePath(expOrFn)  
             // 省略一丢丢代码...  
        }
  
        // 这里调用了this.get(),也就意味着 new Watcher 时会调用 this.get()
        // this.lazy 是修饰符,除非用户自己传入,不然都是 false。可以先不管它
        this.value = this.lazy
            ? undefined
            : this.get()
    }
    
    // 取值操作
    get (){
		// 将Dep.target设置为当前的watcher实例,并将 Dep.target入栈 ,存入targetStack数组中
        pushTarget(this)
        // 省略部分代码...
        try {
            // 这里执行了 this.getter,获取到 属性的初始值
            // 如果是初始化时 传入的 updateComponent 函数,这个时候会返回 udnefined
            value = this.getter.call(vm, vm)
        } catch (e) {
            // 省略部分代码...
        } finally {
            // 省略部分代码...
            // 出栈 targetStack数组移除当前的watcher
            popTarget()
            // 省略部分代码...
        }        
        // 返回属性的值
        return value
    }
    

    // 这里再回顾一下
    // dep.depend 方法,会执行 Dep.target.addDep(dep) 其实也就是 watcher.addDep(dep)
    // watcher.addDep(dep) 会执行 dep.addSub(watcher)
    // 将当前 watcher 实例 添加到 dep 的 subs 数组 中,也就是收集依赖
    // dep.depend 和 这个 addDep 方法,有好几个 this, 可能有点绕。
    addDep (dep: Dep) {
        const id = dep.id
        // 下面两个 if 条件都是去重的作用,我们可以暂时不考虑它们
        // 只需要知道,这个方法 执行 了 dep.addSub(this)
        if (!this.newDepIds.has(id)) {
            this.newDepIds.add(id)
            this.newDeps.push(dep)
            if (!this.depIds.has(id)) {
                // 将当前 watcher 实例添加到 dep 的 subs 数组中
                dep.addSub(this)
            }
        }
    }
    
    // 派发更新
    update () {
        //  三种watcher,只有计算属性watcher的lazy设置了true,表示启动惰性求值
        if (this.lazy) {
            this.dirty = true
        } else if (this.sync) {
        // this.sync 为true表示立即执行 this.run; 不是三大类型,会走下面的queueWatcher 
            this.run()
        } else {
           // queueWatcher 内部也是执行的 watcher实例的 run 方法,只不过内部调用了 nextTick 做性能优化。
        // 它会将当前 watcher 实例放入一个队列,在下一次事件循环时,遍历队列并执行每个 watcher实例的run() 方法
            queueWatcher(this)
        }
    }
    
    // 在update执行后,会执行run
    run () {
        if (this.active) {
            // 获取新的属性值
            const value = this.get()
            if (
                // 如果新值不等于旧值
                value !== this.value ||
                // 如果新值是一个 引用 类型,那么一定要触发回调
                // 举个例子,如果旧值本来就是一个对象,
                // 在新值内,我们只改变对象内的某个属性值,那新值和旧值本身还是相等的
                // 也就是说,如果 this.get 返回的是一个引用类型,那么一定要触发回调
                isObject(value) ||
                // 是否深度 watch 
                this.deep
            ) {
                // 设置新值
                const oldValue = this.value
                this.value = value
                // this.user 是一个标志符,如果开发者添加的 watch 选项,这个值默认为 true
                // 如果是用户自己添加的 watch ,就加一个 try catch。方便用户调试。否则直接执行回调。
                if (this.user) {
                    try {
                        // 触发回调,并将 新值和旧值 作为参数
                        // 这也就是为什么,我们写 watch 时,可以这样写: function (newVal, oldVal) { // do }
                        this.cb.call(this.vm, value, oldValue)
                    } catch (e) {
                        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
                    }
                } else {
                    this.cb.call(this.vm, value, oldValue)
                }
            }
        }
    }
    
    // 省略部分代码...
    
    // 以下是 Watcher 类的其他方法
    cleanUpDeps() { }
    evaluate() { }
    depend() { }
    teardown() { }
}

上面的主要代码注释部分解释的很清楚了。

  • new Watcher 时,就会调用其get方法,将Dep.target设置为当前的watcher实例,并获取初始值
  • watcher实例的appdep方法实质就是执行dep.addSub ,将当前的watcher实例添加到subs数组中
  • Dep实际上就是watcher的管理者
总结:
  • Observer对数据进行监听,对每一个数据设置dep实例、get/set属性
  • 被监听的数据进行getter操作时,如果存在Dep.target(某一个观察者),说明这个观察者是依赖这个数据的,就会把这个观察者添加到数据的dep.subs中,同时该观察者会把数据的dep添加到自己的deps中,方便其他地方使用。
  • 被监听的数据进行setter操作时,就会触发dep.notify,通知每一个dep.subs中每一个watcher执行update方法,进而执行相应的run方法
  • run方法里面触发cb回调函数。在Vue中cb是更新视图的核心,调用diff并更新视图的过程

你可能感兴趣的:(#,Vue)