美文网首页
从源码的角度分析Vue视图更新和nexttick机制

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

作者: hello_小丁同学 | 来源:发表于2020-11-22 19:11 被阅读0次
    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/

    相关文章

      网友评论

          本文标题:从源码的角度分析Vue视图更新和nexttick机制

          本文链接:https://www.haomeiwen.com/subject/sbbuiktx.html