Vue响应式系统的基本原理

关于Vue.js

Vue.js是一款MVVM框架,通过响应式在修改数据的时候更新视图。Vue.js的响应式原理依赖于Object.defineProperty,尤大大在Vue.js文档中就已经提到过,这也是Vue.js不支持IE8 以及更低版本浏览器的原因。Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图 ,下面来一探究竟。

4.1、Object.defineProperty

Object.defineProperty方法会直接在一个对象上定义一个新的属性,或者修改对象的现有属性并返回这个对象,它的语法如下:

Object.defineProperty(obj, prop, descriptor)

  • obj是当前要操作的对象
  • props是要定义或修改的属性名称
  • descriptor是要被定义或修改的属性的描述符

⽐较核⼼的是 descriptor ,它有很多可选键值,具体的可以去参阅它的⽂档。这⾥我们最关⼼的是
get 和 set , get 是⼀个给属性提供的 getter ⽅法,当我们访问了该属性的时候会触发 getter ⽅
法; set 是⼀个给属性提供的 setter ⽅法,当我们对该属性做修改的时候会触发 setter ⽅法。
⼀旦对象拥有了 getter 和 setter,我们可以简单地把这个对象称为响应式对象。那么 Vue.js 把哪些对象
变成了响应式对象了呢,接下来我们从源码层⾯分析。

4.2、initState初始化数据

我们上面讲过,在 Vue 的初始化阶段, _init ⽅法执⾏的时候,会执⾏ initState(vm) ⽅法,这个⽅法主要是对 props 、 methods 、 data 、 computed 和 wathcer 等属性做了初始化操作。这⾥我们重点分析 props 和 data 。

4.2.1、 initProps

function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  //初始化vm_props对象,最终可以通过vn._props访问props数据
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }
  //遍历props中所有数据
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      //调用defineReactive,把每个prop对应的值变成响应式
      defineReactive(props, key, value)
    }
    if (!(key in vm)) {
      //通过proxy将vm._props.xxx代理到vm.xxx上
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

总结:props 的初始化主要过程,就是遍历定义的 props 配置。遍历的过程主要做两件事情:⼀个是调⽤ defineReactive ⽅法把每个 prop 对应的值变成响应式,可以通过 vm._props.xxx 访问到定
义 props 中对应的属性。对于 defineReactive ⽅法,我们稍后会介绍;另⼀个是通过 proxy
把 vm._props.xxx 的访问代理到 vm.xxx 上。

4.2.2、initData

function initData (vm: Component) {
  //获取实例上的数据
  let data = vm.$options.data 
  //先判断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
  //将data的数据和props,methods上的数据进行比较,不能出现重复的定义
  //因为他们最终都会挂载到vm上
  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 (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    //如果props中存在这个键值
    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初始化做了三件事

  • 遍历data,检查data中的数据是否和methods,props中定义的数据重复
  • 遍历data,将每一个值vm._data.xxx都代理到vm.xxx上
  • 调用observer方法观测整个data的变化,使data变成响应式数据

我们看到,无论是props还是data初始化,都是把他们变成响应式对象,在这个过程中我们使用了几个重要的函数函数,下面就是介绍这些函数。

4.2.3、proxy

首先介绍下代理,代理的作用就是吧props和data上的属性代理到vm实例上,这也是为什么我们定义了props可以直接通过this.xxx调用。

const sharedPropertyDefinition = {
  enumerable: true,//是否是可枚举的 
  configurable: true,//是否是可配置的
  get: noop,//空函数
  set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
//初始化变量的get方法,把target[sourceKey][key]的读取变成了对target[key]的读取
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
//初始化变量的set方法,把target[sourceKey][key]的设置变成了对target[key]的设置
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

4.1.4、observer

observe ⽅法的作⽤就是给⾮ VNode 的对象类型数据添加⼀个 Observer ,如果已经添加过则直接返回,否则在满⾜⼀定条件下去实例化⼀个 Observer 对象实例。接下来我们来看⼀下 Observer
类。

export class Observer {
  value: any;
    //observer和watcher的纽带,当数据发生变化时会被observer观察到,然后由dep通知watcher去更新视图
  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)//增加一个标志,表示已经被observer观察
    // 如果value是数组,就辨遍历数组,对数组中每一项进行observer观察
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)//遍历数组的函数
    } else {
    //如果是对象,就遍历对象的每一个key,对每个key调用defineReactive获取对key的set和get的控制权
      this.walk(value)
    }
  }

Observer 是⼀个类,它的作⽤是给对象的属性添加 getter 和 setter,⽤于依赖收集和派发更新。在 Observer 的构造函数中,会对 value 做判断,对于数组会调⽤ observeArray ⽅法,否则对纯对象调⽤ walk ⽅法。可以看到 observeArray 是遍历数组再次调⽤ observe ⽅法,⽽walk ⽅法是遍历对象的 key 调⽤ defineReactive ⽅法。

  • observeArray:遍历数组,对数组的每个元素调用observer
  • walk:遍历对象的每个Key,对对象上的每个key调用defineReactive
  • defineReactive:通过Object.defineProperty 设置对象的key属性,使得我们能够获得该属性的该属性的get/set使用权,一般是由Watcher的实例进行get操作,此时Watcher实例对象会被添加到Dep数组中,在外部操作触发set时,通过Dep通知Watcher进行更新。

4.1.5、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,
     //Dep.target全局变量指向当前正在解析指令的Compile生成的Watcher
      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()//被更新了,通知所有watcher去更新
    }
  })
}

defineReactive 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对⼦对
象递归调⽤ observe ⽅法,这样就保证了⽆论 obj 的结构多复杂,它的所有⼦属性也能变成响应
式的对象,这样我们访问或修改 obj 中⼀个嵌套较深的属性,也能触发 getter 和 setter。最后利⽤
Object.defineProperty 去给 obj 的属性 key 添加 getter 和 setter

4.3、依赖收集

经过上面的分析我们知道,Vue会把普通对象变成响应式对象,响应式对象的getter就是用来收集依赖的,再看看getter的实现。

get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val,
      //Dep.target全局变量指向当前正在解析指令的Compile生成的Watcher
      if (Dep.target) {
        dep.depend()//被读取了,将这个以来收集起来
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },

getter函数最重要的一步就是通过调用depend函数进行依赖的收集,depend函数是dep定义的一个函数,稍后会详细介绍。

4.3.1、Dep

Dep是observer和watcher的纽带,当数据发生变化时会被observer观察到,然后由dep通知watcher。

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++//每个dep都有唯一的id
    this.subs = []//用于存放依赖
  }

  //向subs数组添加依赖
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  //移除依赖
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  //设置Watcher的依赖,这里添加Deo.target目的是判断是不是Watcher的构造函数的调用
  //也就是说判断他是Watcher的this.get调用的的而不是普通调用
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  //通知所有绑定的Watcher调用update()进行更新
  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.
//这是全局唯一的,在任何时候只有一个watcher正在评估
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  // 将当前的watcher推入堆栈中,关于为什么要推入堆栈,主要是要处理模板或render函数中嵌套了多层组件,需要递归处理
  targetStack.push(target)
  // 设置当前watcher到全局的Dep.target,通过在此处设置,key使得在进行get的时候对当前的订阅者进行依赖收集
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
  • add:接受参数为Watcher实例,并把Watcher实例记录依赖的数组中
  • depend:Dep.target存放的是当前需要操作的Watcher实例,调用depend会调用该实例的addDep方法
  • notify:通知数组中所有Watcher进行更新操作

4.3.2、Watcher

Watcher作为观察者,用来订阅数据变化并执行相应的操作,比如视图的更新。

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean//是否为渲染watcher
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    //当前Watcher添加到vue实例上
    vm._watchers.push(this)
    // options
    //参数配置,默认是false
    if (options) {
      ....
    } 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') {
    //将watcher对象的getter设置成uptateComponent
      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()
  }
  //在get函数中,主要是收集一些依赖,然后在初始化或者有更新时,调用this.getter(对应着updateComponent函数)
  get () {
    //将Dep的target添加到targetStack,同时Dep的target赋值为当前watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      //调用updateComponent方法,之后在updateComponent中接着会调用_update方法更新dom
      //这时挂载到vue原型上的方法,而_render方法重新渲染了VNode
      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)
      }
      //update执行完成之后,又将dep.target从targetStack弹出
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
  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)
      }
    }
  }
  cleanupDeps () {
    let i = this.deps.length
    ......
  }
  //通知watcher更新
  update () {
   .......
  }
  run () {
   .......
  }
.............
}

  • addDep:接收参数dep,让当前Watcher订阅dep
  • cleanupDeps:将新的newDepIds(这里保存的是dep的id)和旧的deps去对比,找存在于出旧的deps但不存在于新的newDeplds中的dep,就从这些dep中移除当前的依赖,这样可以有效地避免没必要的updata,稍后在new Watcher()渲染过程分析中我会可以举个简单的例子说明。
  • updata:立即执行watcher或者将watcher加入队列等待统一flush
  • run:运行watcher,调用this.get()求值,然后触发回调

4.3.3、new Watcher发生了什么

之前我们讲过,在$mount函数中我们主要是通过调用mountComponent()函数去实现我们的数据挂载,而在mountComponent函数中就使用了new Watcher(),在这个过程中我们调用了每个数据的getter函数,这样就实现了每个数据首次依赖的收集。

当我们去实例化⼀个渲染 watcher 的时候,⾸先进⼊ watcher 的构造函数逻辑,然后会执⾏它的
this.get() ⽅法,进⼊ get 函数,⾸先会执⾏

pushTarget(this)


//函数的实现
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}

执行这个函数就是把 Dep.target 赋值为当前的渲染 watcher 并压栈(为了恢复⽤)。接着⼜执⾏了

value = this.getter.call(vm, vm)
//this.getter  对应就是  updateComponent  函数,这实际上就是在执⾏:
vm._update(vm._render(), hydrating)

它会先执⾏ vm._render() ⽅法,因为之前分析过这个⽅法会⽣成 渲染 VNode,并且在这个过程中
会对 vm 上的数据访问,这个时候就触发了数据对象的 getter。那么每个对象值的 getter 都持有⼀个 dep ,在触发 getter 的时候会调⽤ dep.depend() ⽅法,也就会执⾏

Dep.target.addDep(this)

刚才我们提到这个时候 Dep.target 已经被赋值为当前在操作的 watcher ,那么就执⾏到 addDep ⽅法:

addDep (dep: Dep) {
    const id = dep.id//获取dep的id
    //如果新的DepIds数组中没有当前dep,将入组
    if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id)
        this.newDeps.push(dep)
        //如果dep没有当前正在操作的Watcher
        if (!this.depIds.has(id)) {
            dep.addSub(this)
        }
    }
}

这时候会做⼀些逻辑判断(保证同⼀数据不会被添加多次)后执⾏ dep.addSub(this) ,那么就会执
⾏ this.subs.push(sub) ,也就是说把当前的 watcher 订阅到这个数据持有的 dep 的 subs中。

所以在 vm._render() 过程中,会触发所有数据的 getter,这样实际上已经完成了⼀个依赖收集的过程。那么到这⾥就结束了么,其实并没有,再完成依赖收集后,还有⼏个逻辑要执⾏,⾸先是:

if (this.deep) {
    traverse(value)
}

这个是要递归去访问 value ,触发它所有⼦项的 getter ,这个之后会详细讲。接下来执⾏:

popTarget()
//函数实现
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

实际上就是把 Dep.target 恢复成上⼀个状态,因为当前 vm 的数据依赖收集已经完成,那么对应的渲染 Dep.target 也需要改变。最后执⾏this.cleanupDeps()这里着重讲下this.cleanupDeps()这个函数。

考虑到 Vue 是数据驱动的,所以每次数据变化都会重新render,那么 vm._render() ⽅法⼜会再次执⾏,并再次触发数据的 getters,我们知道每次执行一次render都是一次的性能消耗,那有什么办法能帮助我们去阻止没必要render,所以 Wathcer 在构造函数中会初始化 2 个 Dep 实例数组, newDeps 表⽰新添加的 Dep 实例数组,⽽ deps 表⽰上⼀次添加的 Dep 实例数组。在执⾏ cleanupDeps 函数的时候,会⾸先遍历 deps ,移除对 dep 的订阅,然后把 newDepIds和 depIds 交换, newDeps 和 deps 交换,并把 newDepIds 和 newDeps 清空。那么为什么需要做 deps 订阅的移除呢,在添加 deps 的订阅过程,已经能通过 id 去重避免重复订阅了。

考虑到⼀种场景,我们的模板会根据 v-if 去渲染不同⼦模板 a 和 b,当我们满⾜某种条件的时候渲染 a 的时候,会访问到 a 中的数据,这时候我们对 a 使⽤的数据添加了 getter,做了依赖收集,那么当我们去修改 a 的数据的时候,理应通知到这些订阅者。那么如果我们⼀旦改变了条件渲染了 b 模板,⼜会对 b 使⽤的数据添加了 getter,如果我们没有依赖移除的过程,那么这时候我去修改 a 模板的数据,会通知 a 数据的订阅的回调,这显然是有浪费的。因此 Vue 设计了在每次添加完新的订阅,会移除掉旧的订阅,这样就保证了在我们刚才的场景中,如果渲染 b 模板的时候去修改 a 模板的数据,a 数据订阅回调已经被移除了,所以不会有任何浪费,真的是⾮常赞叹 Vue 对⼀些细节上的处理。

4.4、异步更新DOM策略及nextTick

Vue实现响应式并不是数据变化后DOM立即变化,而是按照一定策略进行DOM更新,在Vue文档中指出Vue是异步执行DOM更新。那Vue为什么要使用异步更新又是怎么实现的?

我们先看看Watcher队列 ,当触发某个数据的setter时,Dep就会通过调用notify去通知所有的依赖watcher.

  //通知watcher更新
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      /*同步则执行run直接渲染视图*/
      this.run()
    } else {
        /*异步推送到观察者队列中,下一个tick时调用。*/
      queueWatcher(this)
    }
  }

从源码中我们可以知道Vue.js默认是使用异步更新,当执行updata时,会调用queueWatcher函数。

//将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非是在队列被刷新时推送。
export function queueWatcher (watcher: Watcher) {
    //获取watcher的id
  const id = watcher.id
    //检测id是否存在,已经存在直接跳过,不存在直接标志哈希表has
  if (has[id] == null) {
    has[id] = true
        /*如果没有flush掉,直接push到队列中即可*/
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

这⾥引⼊了⼀个队列的概念,这也是 Vue 在做派发更新的时候的⼀个优化的点,它并不会每次数据改
变都触发 watcher 的回调,⽽是把这些 watcher 先添加到⼀个队列⾥,然后在 nextTick 后执
⾏ flushSchedulerQueue 。这⾥有⼏个细节要注意⼀下,⾸先⽤ has 对象保证同⼀个 Watcher 只添加⼀次;接着对flushing 的判断,else 部分的逻辑稍后我会讲;最后通过 wating 保证对
nextTick(flushSchedulerQueue) 的调⽤逻辑只有⼀次。那什么是nextTick?

/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false

 /*下一个tick时的回调*/
function flushCallbacks () {
  pending = false
    //将每次保存的cb函数取出并执行
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
let timerFunc

/* 
 一共有Promise、MutationObserver以及setTimeout三种尝试得到timerFunc的方法
 优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法都会在microtask中执行,会比setTimeout更早执行,所以优先使用。如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。
 */
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)
  /*新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),即textNode.data = String(counter)时便会触发回调*/
  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.
  /*使用setTimeout将回调推入任务队列尾部*/
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick (cb?: Function, ctx?: Object) {
  //cb是一个函数遍历队列中的watcher,执行watcher.run();
  let _resolve
  /*cb存到callbacks中*/
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

这里解释一下,一共有Promise、MutationObserver以及setTimeout三种尝试得到timerFunc的方法。 优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法的回调函数都会在microtask中执行,它们会比setTimeout更早执行,所以优先使用。 如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。 为什么在miscrotask执行会更早呢,JS 的 event loop 执行时会区分 task 和 microtask,引擎在每个 task 执行完毕,从队列中取下一个 task 来执行之前,会先执行完所有 microtask 队列中的 microtask。setTimeout 回调会被分配到一个新的 task 中执行,而 Promise 的 resolver、MutationObserver 的回调都会被安排到一个新的 microtask 中执行,会比 setTimeout 产生的 task 先执行。

综上,nextTick的目的就是产生一个回调函数加入task或者microtask中,当前栈执行完以后(可能中间还有别的排在前面的函数)调用该回调函数,起到了异步触发(即下一个tick时触发)的目的。

为什么要异步更新视图呢?

来看一下下面这一段代码

<template>
  <div>
    <div>{{test}}</div>
  </div>
</template>
export default {
    data () {
        return {
            test: 0
        };
    },
    mounted () {
      for(let i = 0; i < 1000; i++) {
        this.test++;
      }
    }
}

现在有这样的一种情况,mounted的时候test的值会被++循环执行1000次。 每次++时,都会根据响应式触发setter->Dep->Watcher->update->patch。 如果这时候没有异步更新视图,那么每次++都会直接操作DOM更新视图,这是非常消耗性能的。 所以Vue.js实现了一个queue队列,在下一个tick的时候会统一执行queue中Watcher的run。同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行1000次Watcher的run。最终更新视图只会直接将test对应的DOM的0变成1000。 保证更新视图操作DOM的动作是在当前栈执行完以后下一个tick的时候调用,大大优化了性能 。

4.5、computed和watch

  • computed

computed实质上是一个computed watcher,当它所依赖的数据发生变化时会进行重新计算,然后对比新旧值,如果发生了变化就会触发渲染watcher重新渲染,所以对于计算属性Vue想确保不仅仅是计算属性依赖的值发生变化,而是想当计算属性最终计算的值发生变化才会触发渲染watcher重新渲染,这本质上是一种优化。

  • watch

  • ⼀旦我们 watch 的数据发送变化,它最终会执⾏ watcher 的run ⽅法,执⾏回调函数 cb ,并且如果我们设置了 immediate 为 true,则直接会执⾏回调函数cb ,最后返回了⼀个 unwatchFn ⽅法,它会调⽤ teardown ⽅法去移除这个 watcher 。

拓展:watcher 总共有 4 种类型,这里介绍deep watcher

  • deep watcher

    通常,如果我们想对⼀下对象做深度观测的时候,需要设置这个属性为 true,考虑到这种情况:

    var vm = new Vue({
        data() {
            a: {
                b: 1
            }
        },
        watch: {
            a: {
                handler(newVal) {
                    console.log(newVal)
                }
            }
        }
    })
    vm.a.b = 2
    
    

    这个时候是不会 log 任何数据的,因为我们是 watch 了 a 对象,只触发了 a 的 getter,并没有触发a.b 的 getter,所以并没有订阅它的变化,导致我们对 vm.a.b = 2 赋值的时候,虽然触发了setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了。⽽我们只需要对代码做稍稍修改,就可以观测到这个变化了

    watch: {
        a: {
            deep: true,
            handler(newVal) {
                console.log(newVal)
            }
        }
    }
    

    这样就创建了⼀个 deep watcher 了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342