从源码的角度分析Vue视图更新和nexttick机制

Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '未更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '已更新'
      console.log(this.$el.textContent) // => '未更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '已更新'
      })
    }
  }
})

这篇文章从vue源码的角度分析,为什么在this.$nextTick回调里面才能看到视图更新了。

TL;DR

this.message = '已更新' 这个被执行的时候会把创建虚拟dom和创建真实dom的函数作为回调传到nexttick里。
updateMessage 是一个点击事件回调,他是个宏任务,当执行到updateMessage 的时候,会将执行栈里的所有任务都处理完成,才会检查微任务队列中是否有任务,

  1. 若是当前浏览器采用的nexttick是微任务:则在本次事件循环就可以完成dom更新,同时在函数手动调用的this.nexttick中的回调也会dom更新之后在本次事件循环中被执行。
  2. 若是当前浏览器采用的nexttick是宏任务:要等未来的事件循环发生,从宏任务队列中的取出任务才可以完成dom更新,在函数中手动调用的this.nexttick中的回调也会dom更新之后的下一次事件循环中被执行。这也可以看到nexttick采用宏任务的缺点是视图更新会不及时。

下面是很长的源码分析,采用的vue源码版本是2.6.11

Vue数据变化的注册和派发

将data对象处理成响应式

Vue的响应式原理核心就是利用Object.defineProperty。进入源码看一下vue是怎么把data变成响应式的,
从src/core/instance/state.js 开始,initState函数会执行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 */)
}

主要做了3件事情:

  1. 调用getData,把
  data: function () {
    return {
      message: '未更新'
    }
  },

生成一个新对象赋值给data和_data

  1. proxy(vm, _data, key) 用this.message代理对data.message的修改
  2. observe(data, true /* asRootData */) 给data创建一个观察者对象,使得data的属性修改可以被检测到,其实就是拦截message的getter 和 setter函数,并在其中加入一些逻辑。
    主要看下observe的实现,位于src/core/observer/index.js
/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
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
}

会执行ob = new Observer(value) 进入这个类的构造器

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

两个关键点:

  1. def(value, 'ob', this) 给data添加ob属性
  2. 执行 this.walk(value)
    进入walk函数
  /**
   * 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])
    }
  }

给data里的每一个属性调用defineReactive函数,这里key[i]就是message参数,进入defineReactive

/**
 * Define a reactive property on an Object.
 */
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()
    }
  })
}

核心逻辑是对set和get函数进行的处理,先看get函数,会判断Dep.target是否为空,进入Dep看一下Dep.target是一个持有Watcher实例的静态属性

export default class Dep {
  static target: ?Watcher;

它是通过pushTarget来赋值的

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

pushTarget被调用是watcher被创建的时候在get函数里调用的,

  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
  }

第一次生成虚拟dom和生成真实dom是在这个函数里value = this.getter.call(vm, vm),而在生成虚拟dom的时候才会第一次触发data里面的数据中的get函数,所以,Dep.target不会为空。会进入dep.depend()

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

进入addDep

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

最终会执行dep.addSub(this),这里的this是Watcher实例,实际上就是让dep持有用到message的所有watcher。

数据更新

当数据发生改变,上面的set函数会被调用,最终会调用dep.notify()

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

subs[i].update()是调用watcher的update,这里使用的是注册订阅模式,继续看Watcher的update函数

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

就是将变动的watcher放到一个queen里面去管理,queueWatcher是src/core/observer/scheduler.js中的函数,

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    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 (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

将watcher放在管理队列,将flushSchedulerQueue函数作为引用传给nextTick,注意flushSchedulerQueue会被异步执行。先看下flushSchedulerQueue

/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

核心逻辑是遍历queen,执行watcher.run(),进入watcher里的run函数

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

核心是执行get函数,get函数里的value = this.getter.call(vm, vm)中的getter是mountComponent中的updateComponent,这个函数中会调用 vm._render()和vm._update(vnode, hydrating) 实现生成虚拟dom和创建真实dom。

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

nexttick

    updateMessage: function () {
      this.message = '已更新'
      console.log(this.$el.textContent) // => '未更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '已更新'
      })

最上面的例子,在函数执行栈执行this.message = '已更新'的时候,实际上只执行到了 nextTick(flushSchedulerQueue) 这一步,将回调丢给nicktick去执行。
下面就解释一下事件循环机制:
js主线程的执行栈代码执行完, 检查宏任务队列中是否有任务,

  1. 若无,则本次事件循环结束,插入ui渲染,进入下一次事件循环。
  2. 若有,则取出队首的一个宏任务放到执行栈中去执行,若是在执行过程中遇到新的宏任务,则添加到宏任务队列,放到下次事件循环去执行,若是在执行过程中遇到新的微任务,则放到微任务队列,会在本次事件循环中执行。宏任务执行完成,会检查微任务队列中是否有任务?
    a. 若有,则把所有的微任务从头部一个一个取出来放到执行栈中去执行,直到微任务队列被清空,若是在执行过程中遇到新的宏任务,则添加到宏任务队列,放到下次事件循环去执行,若是在执行过程中遇到新的微任务,则放到微任务队列,会在本次事件循环中执行。本次事件循环结束,插入ui渲染,进入下一次事件循环。
    b. 若无,本次事件循环结束,插入ui渲染,进入下一次事件循环。

常见的宏任务有:
setTimeout、setInterval、setImmediate的回调函数
UI事件的回调函数
ajax执行完成后的回调函数

常见的微任务有:
Promise的then回调
MutationObserver的回调

进入nexttick源码,位于src/core/util/next-tick.js

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  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
    })
  }
}

是将回调函数放入callbacks队列,执行timerFunc,timerFunc会根据浏览器的兼容性会采用微任务或者宏任务的方式处理异步任务。

了解了vue数据触发视图更新的完整流程,以及nexttic的事件循环机制,再看最上面的例子为何需要在nexttick中才能获取到dom内容

总结

updateMessage 是一个点击事件回调,他是个宏任务,当执行到updateMessage 的时候,会将执行栈里的所有任务都处理完成,才会检查微任务队列中是否有任务,

  1. 若是当前浏览器采用的nexttick是微任务:则在本次事件循环就可以完成dom更新,同时在函数手动调用的this.nexttick中的回调也会dom更新之后在本次事件循环中被执行。
  2. 若是当前浏览器采用的nexttick是宏任务:要等未来的事件循环发生,从宏任务队列中的取出任务才可以完成dom更新,在函数中手动调用的this.nexttick中的回调也会dom更新之后的下一次事件循环中被执行。这也可以看到nexttick采用宏任务的缺点是视图更新会不及时。

参考:
https://cn.vuejs.org/v2/guide/reactivity.html
https://github.com/logan70/Blog/issues/34
https://cloud.tencent.com/developer/article/1701427
https://github.com/answershuto/learnVue/blob/master/docs/Vue.js%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0DOM%E7%AD%96%E7%95%A5%E5%8F%8AnextTick.MarkDown
https://juejin.cn/post/6844904084319764488
https://juejin.cn/post/6844904000542736398#heading-0
https://www.cnblogs.com/zjjDaily/p/10478634.html
https://blog.csdn.net/weixin_42752574/article/details/108612569
https://harttle.land/2019/01/16/how-eventloop-affects-rendering.html
https://ustbhuangyi.github.io/vue-analysis/v2/reactive/

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

推荐阅读更多精彩内容