当Vue响应式数据被赋值后,都发生了什么

137 阅读3分钟

这篇文章讲述了从响应式数据被赋值到操作dom去更新视图的整个过程。vue提供了声明式的方式,我们只需关注状态的变化,而不必手动操作dom。vue通过数据绑定和虚拟dom封装了操作dom的过程,但最终更新视图还是要操作dom,我在操作dom更新文本处打个断点,看下整个调用的过程。

<div id="app">
      <h1>{{num}}</h1>
      <button @click="add">add</button>
    </div>
    <script>
      new Vue({
        el: '#app',
        data: {
          num: 0
        },
        methods: {
          add() {
            this.num++
          }
        }
      })
    </script>

点击add按钮后,打断点看调用堆栈,分析响应式数据是如何更新视图的。

d29e5e4fbad3d8e16211f43d3f85725.png

  1. 点击add按钮执行了this.num++,修改该属性值时触发了object.defineProperty的set proxySetter函数。
add() {
            this.num++
         }
  1. proxySetter函数,这个函数用到了代理模式,同学们有没有想过,当我们在方法中通过this.num来拿到_data的数据是如何做到的。其实就是遍历_data的数据,使用Object.defineProperty的get自定义属性的读取行为,返回this._data.num来做的。当我们使用vm.a时,通过get代理,返回的是this.data.a。除此之外,vue初始化方法时,使用bind函数改变this的指向为vm实例。所以我们定义在methods中的函数可以通过this.a使用this.data.a,函数体中的this就是vm实例。
// 代理模式设置属性读取和被赋值时的自动行为
export function proxy(target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key]
  }
  // 当修改this.num时,通过代理修改this._data.num
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
 // 下列可以直接在控制台上输出,看下通过代理实现了访问vm.a得到了vm 
 const vm = {
        data: {
          a: 0
        }
      }
      function proxy(target, sourceKey, key) {
        Object.defineProperty(target, key, {
          // 自定义属性的读取行为,当读取this.num时返回this._data.num
          get: function proxyGetter() {
            return this[sourceKey][key]
          },
          // 自定义属性的写入操作,当设置this.num时,就是设置this._data.num
          set: function proxySetter(val) {
            this[sourceKey][key] = val
          },
          enumerable: true,
          configurable: true
        })
      }
      const keys = Object.keys(vm.data)
      let i = keys.length
      while (i--) {
        const key = keys[i]
        proxy(vm, 'data', key)
      }
      console.log(vm.a)
  1. reactiveSetter 是定义响应数据的defineProperty的set,当属性被赋值时就会触发ractiveSetter, 这个函数的作用是当响应式数据被赋值时执行dep.notify。通知依赖该响应式数据的所有组件执行视图更新
 set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      // 如果值没有发生改变,直接return
      if (!hasChanged(value, newVal)) {
        return
      }
      if (__DEV__ && customSetter) {
        customSetter()
      }
      // 处理这个对象原来的自定义读取行为
      if (setter) {
        setter.call(obj, newVal)
      } else if (getter) {
        // #7981: for accessor properties without setter
        return
      } else if (!shallow && isRef(value) && !isRef(newVal)) {
        value.value = newVal
        return
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal, false, mock)
      if (__DEV__) {
        dep.notify({
          type: TriggerOpTypes.SET,
          target: obj,
          key,
          newValue: newVal,
          oldValue: value
        })
      } else {
        // 通知依赖这个属性的所有watcher
        dep.notify()
      }
    }
  1. dep.notify 遍历dep实例上收集的所有watcher,调用update函数
notify(info?: DebuggerEventExtraInfo) {
    // stabilize the subscriber list first
    const subs = this.subs.filter(s => s) as DepTarget[]
    if (__DEV__ && !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++) {
      const sub = subs[i]
      if (__DEV__ && info) {
        sub.onTrigger &&
          sub.onTrigger({
            effect: subs[i],
            ...info
          })
      }
      sub.update()
    }
  }
  1. Wathcer.update 就是将该组件的watcher推入到视图更新的queue队列中
 update() {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  1. queueWatcher 当我们触发了多次依赖,在queueWatcher时就会将已经放入到queue队列的过滤掉, 保证queue队列中每个都是id不重复的watcher。
function queueWatcher(watcher: Watcher) {
  const id = watcher.id
  if (has[id] != null) {
    return
  }

  if (watcher === Dep.target && watcher.noRecurse) {
    return
  }

  has[id] = true
  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 (__DEV__ && !config.async) {
      flushSchedulerQueue()
      return
    }
    nextTick(flushSchedulerQueue)
  }
}
  1. nextTick,将该flushSchedulerQueue更新视图的队列函数推入到微任务队列中
function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e: any) {
        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
    })
  }
}
  1. timerFunc 使用Promise.resolve.then,将flushCallbacks函数推入到微任务队列中,
timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  1. flushCallbacks 当这个函数被执行时就会遍历执行cb(也就是flushSchedulerQueue) 更新响应式数据发生变化所推入的watcher组件视图
function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
  1. flushSchedulerQueue 触发组件beforeUpdate钩子,并执行watcher.run
function flushSchedulerQueue() {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  queue.sort(sortCompareFn)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
  }

  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
  cleanupDeps()

  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}
  1. watcher.run 主要就是执行this.get。就是watcher实例化参数的getters就是执行 虚拟dom diff更新视图 即updateComponent
 run() {
    if (this.active) {
      const value = this.get()
    }
  }
  1. updateComponet vm._render() 返回虚拟dom,执行虚拟dom diff,
updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  1. vm._update 执行diff 并将实例化完成的dom绑定到vm.$el上
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    if (!prevVnode) {
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    let wrapper: Component | undefined = vm
    while (
      wrapper &&
      wrapper.$vnode &&
      wrapper.$parent &&
      wrapper.$vnode === wrapper.$parent._vnode
    ) {
      wrapper.$parent.$el = wrapper.$el
      wrapper = wrapper.$parent
    }
  }
  1. patch diff算法,最后 通过diff算法找出需要更新的地方,操作dom。 作者会不断的完善这篇文章。如果觉得有用,请动动发财的小手点点赞吧。