Vue如何让数据具有响应式和nextTick的用法

如何让数据具有响应式的效果?

我们知道vue的data和props以及computed等都会让视图随着数据的改变而改变,可是往往止步于用还是有点朦胧的感觉,基于黄老师的教程,和自己调试以及工作中的问题,总结一片博文

下面就从创建vue开始

在我们使用 new Vue 的时候,调用了 this._init()函数

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

在定义此方法的地方 vue-dev\src\core\instance\init.js  中赋值给init的对应方法是以下:

 // expose real self
    vm._self = vm
    initLifecycle(vm) 
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate') // 生命周期beforeCreate
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created') // 生命周期created

由上面的顺序可知为什么 beforeCreate 获取不到对应的vm实例和数据,因为此时他们还没有创建

我们看  initState(vm) 这是init props data的地方

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

我们看到了 initProps , initMethods , initData , initComputed 和 initWatch 这几个判断和init
着重从initProps和initData进行分析
先看initData

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

里面有一些是对于data的类型进行判断以及对于和props、methods的key重复的判断我们抽离核心的代码

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  ...
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      // 判断methods重复
    }
    if (props && hasOwn(props, key)) {
      // 判断props
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key) // 通过Object.defineProperty 将vm._props.xxx简写为vm.xxx
    }
  }
  // observe data
  observe(data, true /* asRootData */) // 对data添加响应式效果
}

上面代码简化后看出来initData做了一些判断后就将data传入了observe函数,那么我们猜想响应式的核心应该就在这个函数中

observe的作用

我们打开文件 observer/index.js 查看对应的代码

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 判断value是否具有__ob__的不可枚举的属性,如果有直接返回
  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
}

进入第16行的new 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
    this.dep = new Dep() // 为data new 一个Dep的实例
    this.vmCount = 0
    def(value, '__ob__', this) // 为data添加__ob__属性,上面用来判断用的————ob__属性,此处没有传枚举的参数默认取值false
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value) //如果是否数组进入递归重复调用observe方法
    } else {
      this.walk(value) // 如果是一个对象的话则调用walk函数
    }
  }
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

遍历去调用defineReactive方法~

我们主要看这个方法的定义get和set的地方

  // ### 代码3
	const dep = new Dep()
  ...
	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()
    }
  })

我们看上面的代码第八行的 Dep.target 可能会很疑惑,因为Dep看起来是个类,如果这样被使用,那么target肯定是个静态属性了

那也很奇怪这里为啥要判断这个静态属性呢?

我们知道其实Watcher是一个"观察器",代表了谁希望对某个属性的变化做出响应。

而我们从创建vue的过程中

 new Vue({
   render: h => h(App),
 }).$mount('#app')

的$mount函数中进行了第一次的依赖收集,我们来看这个 $mount 是什么

Vue.prototype.$mount = function (
  el?: any,
  hydrating?: boolean
): Component {
  return mountComponent(
    this,
    el && query(el, this.$document),
    hydrating
  )
}

由上面代码可知他是一个高阶函数,通过mountComponent包装后返回的,下面列出对应的函数

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  ...
  callHook(vm, 'beforeMount') // 生命周期
  let updateComponent
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {...}
  else {
    // 定义了Watcher中的
    updateComponent = () => {
      vm._update(vm._render(), hydrating) // 此处很重要,稍后会详细介绍
    }
  }
   // 此处new Watcher
  new Watcher(vm, updateComponent, noop, {
    before () {...}
  }, true /* isRenderWatcher */)
  hydrating = false

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

上面代码的第17行开始 new watcher 传入了 expOrFn (后面会知道它的作用)

我们继续往下走,new的过程中会执行 Watcher类中的constructor,constructor中最后一段代码

if (typeof expOrFn === 'function') {
      this.getter = expOrFn
 } else { this.getter = parsePath(expOrFn)}
      ...

	this.value = this.lazy? undefined: this.get()
  // 代码6
	get () {
    pushTarget(this) // 先执行了pushTarget操作
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
      // 此处this.getter就是上面传进来的 vm._update(vm._render(), hydrating)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps() // 每次收集依赖钱清空之前的依赖
    }
    return value
  }

pushTarget(this) 是Dep类文件里导出的一个方法

export function pushTarget (target: ?Watcher) {
  targetStack.push(target) // 将当前的watcherpush进去
  Dep.target = target
}
// 对应的popTarget
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

走完这段后继续执行代码6 处的第七行 value = this.getter.call(vm, vm) ,对应的方法是在mount的时候传进来的: vm._update(vm._render(), hydrating) ,执行的 vm._render() 时生成虚拟DOM并且使用了data或者props里面数据,即访问了VM上面数据(触发了数据的get) 即是最开始initState的时候为数据添加的响应式逻辑(对应代码3处的defineProperty的get)

 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
 }

get方法返回对应的key的值,之前做判断当前Watcher是否存在,如果存在调用 dep.depend 将当前watcher添加到当前dep的subs里面

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

对应的Watcher里面的addDep

  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)
      }
    }
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

添加进入Dep的属性subs里面,这个数组存储每个属性的Watcher

更新

当触发了set(比如更改data里面的属性值~)后会触发dep的方法notify然后触发Watcher里面的update方法

Watcher.prototype.update = function update () {
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};

非缓存非同步更新的状态(this.sync的值,这个值在watch中用于同步执行而不是加入微任务中)就加入queueWatcher队列中等待微任务(vue2.6+)去执行

queueWatcher函数中调用nextTick将函数flushSchedulerQueue传入作为回调

 nextTick(flushSchedulerQueue);

// ## 代码10
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 将传入的cb 传入callbacks里面提供给下面的flushCallbacks函数使用
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc() // 调用timerFunc
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

nextTick与更新的结合~

在nextTick函数中调用 timerFunc函数

timerFunc();

// timerFunc优先使用微任务进行处理,最后才会采用setTimeout等宏任务处理~
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

上面代码太长,摘取部分

  timerFunc = () => {
    p.then(flushCallbacks)
    // 兼容部分机型的promise.then的延迟问题~
    if (isIOS) setTimeout(noop)
  }

timerFunc又调用了flushCallbacks

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)// 复制当前
  callbacks.length = 0 // 释放callBacks数组
  for (let i = 0; i < copies.length; i++) {
    copies[i]() // 遍历执行cb
  }
}

flushCallbacks 函数很短,但是很重要。它遍历执行了nextTick传入的cb函数,而我们传入的cb就是上面代码10处上面的flushSchedulerQueue,在这个函数中调用了 watcher.run()

以上就是当你触发某个dep 的notify的时候过程

posted @ 2021-03-05 11:43  O噗寺的小和尚  阅读(222)  评论(0)    收藏  举报