Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue2 reactive原理 #4

Open
Hazlank opened this issue Aug 22, 2021 · 2 comments
Open

vue2 reactive原理 #4

Hazlank opened this issue Aug 22, 2021 · 2 comments
Labels

Comments

@Hazlank
Copy link
Owner

Hazlank commented Aug 22, 2021

vue2响应式原理

vue2的依赖追踪的代码基于Object.defineProperty实现,收集触发相关依赖的代码还会依靠Watcher,Dep代码

observer

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
}

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])
    }
  }
}

vue在初始化组件的时候会initData,调用observe(data, true),监听data每层数据,observe函数接受value: any的参数,但是当它不为对象的时候会跳出函数,为什么呢,这个后面会讲。 经过一系列的判断,来到了new Observer(value)

Observer类有三个属性,value,vmCount,dep,两个方法,walk,observeArray,它会给对象绑定一层__ob__为当前的Observer,然后监听每个对象值。在这里,第一次看到了dep字眼,它的主要功能就是收集依赖用的,后面会揭开它的面纱。有人会好奇vmCount是啥,我们知道vue.$set是不能给根对象设置新的值vm.$set(vm.$data, key, value)将会抛出错误,所以他是用来判断是不是data的根。

在监听值的时候,如果值不为数组就会defineReactive(value),否则将会重新observe一遍每个值,为啥呢,下面讲到defineReactive的时候会解释

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : 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 = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

上面这段就是最重要的监听对象值变化的函数了。先来到最重要的Object.defineProperty,看到上面会有一段let childOb = !shallow && observe(val)这是干嘛的?为什么又跳回去Observe了。

Object.defineProperty虽然可以对值进行监听,但是无法监听对象的key的增加,包括对象里对象值的改变

var obj = {
	a:1, 
	b: { c: 2 }
};
Object.defineProperty(obj, 'b',{
    enumerable: true,
    configurable: true,
  	get: function(){
    	return {c: 2}
	}
	set: function(){
    	console.log('it can't printf')
	}
})

obj.b.c = 3//不会触发set
obj.b.d = 3//不会触发set

所以我们需要每层都进行监听

{
	a: 1
	b: {
		c: 2
		__ob__:{}
	}
	__ob__: {}
}

遇到数组也需要对每个值进行监听,如果监听数组本身也并不会触发set,所以上面遇到数组才会重新observe一遍。我们可以把数组看成一个对象

array = ['a','b','c']
它可以相当于下面这个对象
array = {
	0: 'a',
	1: 'b',
	2: 'c',
	length: 3,
	__proto__: Array.prototype
}

对于数组还需要进行其他的hack,使用数组方法的时候也会触发值改变,比如splice,reserve,push等,我们可以监听到Array.prototype某些方法有无被使用到

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, 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)
    // notify change
    ob.dep.notify()
    return result
  })
})

ok,回到let childOb = !shallow && observe(val),到这里我们知道observe在如果不是对象的时候会跳出,也就是说如果对象还存在子对象会重新在添上一层observe。

先看看getter函数

get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },

这里面都是关于dep的代码,虽然还没讲dep是什么,但是可以先告诉大家,get主要是做依赖收集,

    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }

到set函数,会判断如果值没变,将会直接返回,后面的(newVal !== newVal && value !== value))是啥呢?自己不等于自己?那就只有一种数据类型了NaN != NaN。set函数可以接受customSetter函数,在vue里有些值是不能被改变的,比如父辈传过来的值,inject,props,$attrs,$listens监听这些值的时候都可以传自定义setter让用户不能更改值。set最后会改变值并且触发相关依赖更新

在defineReactive里会看到一些奇怪难懂的判断

  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  .......
  if (getter && !setter) return

这是为了处理一些用户的边界情况,如果用户事先监听了对象值,会导致触发用户调用的getter,这种行为会很奇怪,所以如果有用户自定义的getter,就不取值

在setter里如果有用户定义的setter就返回也是因为如果不跳出的话,会到下面重新观察值收集依赖,这样会导致和最初的用户行为不一致

具体可以到这个issue进行了解

Dep

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 aren't 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的代码比较简单,能看到大部分函数会接受一个参数,Watcher,它的实例会维护一个装了watcher的数组,以便于更新值时触发所有的watcher来更新相关联的值

	//getter

    if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
	}

	//dep
  	depend () {
      if (Dep.target) {
      	Dep.target.addDep(this)
      }
    }
    
    .........
    
    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]
}

上面的代码能看到监听值的getter会触发dep的depend方法,但是这个Dep.target又是啥呢?看到最下面,在pushTarget里接收watcher再将它设置到模块化全局的变量里,那啥时候会触发pushTarget函数?

Watcher

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // 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()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    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)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

Watcher的代码比较长,它可以用于computed,监听值,还有在初始化每个组件都会有updateComponent的wather在值更新的时候重新渲染

  constructor() {
   ........
   this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

能看到一开始实例化对象的时候,会调用get()计算值,在get函数的第一行,pushTarget(this),这会将Dep.target设置当前实例化的对象,然后读取值value = this.getter.call(vm, vm),还记得之前对值设置的getter吗?

    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    }

这样就会将依赖收集起来了,所以每次在获取值的时候就会进行依赖收集,不过当然也会防止依赖的重复收集,这基本上就是依赖收集追踪的整个过程了

那数据变更后是怎么更新dom的呢?也没看到有data的watcher啊?

//lifecycle.js

let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }

在注入钩子的时候会在vm上定义一个updateComponent的watcher,当模板里需要数据的时候会把依赖添加进来,最后触发vm._update(vm._render(), hydrating),最后patch(newVode,oldVode)

当然watcher还有很多其他方法属性,比如setter触发dep.notify时会执行每个watcher.update ,取得最新的值

  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

queueWatcher能够保证在一定的时间内不重复更新dom,

lazy是用于computed,它只有在被需要的时候才会计算,并且会缓存值,re-render的时候不会重新计算,当相关值更新时才会计算最新的结果,具体怎么初始化可以到state.js里参考

//state.js
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

//watcher.js
construst(){
  this.value = this.lazy
  ? undefined
  : this.get()
}

.........
	
evaluate () {
  this.value = this.get()
  this.dirty = false
}

还有watcher里的user属性,是用于Options api里面的watch

//app.vue
{
  watch: a() {}
}

//watcher.run
 if (this.user) {
     const info = `callback for watcher "${this.expression}"`
     invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
 } else {
     this.cb.call(this.vm, value, oldValue)
 }

他会在try{}catch{}里执行用户定义的callback,防止函数错误导致js停止执行

over

@yuxino
Copy link

yuxino commented Aug 23, 2021

前端高手

@Hazlank
Copy link
Owner Author

Hazlank commented Aug 23, 2021

@yuxino 俺能找个电子厂上班了不

@Hazlank Hazlank added the vue2 label Aug 23, 2021
@Hazlank Hazlank changed the title vue2 reactive vue2 reactive原理 Sep 29, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants