Vue框架源码:源码剖析-响应式原理

232 阅读15分钟

概览

课程目标

  • Vue.js的静态成员和实例成员的初始化过程
  • 首次渲染过程
  • 数据响应式原理

Vue源码文件结构

Vue按照功能将代码拆分到了不同的文件夹,再拆分成小的模块,提高了可读性和可维护性。Vue的虚拟DOM重写了Snabbdom,增加了组件相关机制。

  • compiler:编译相关代码,将Vue中的模板转换成render函数,创建虚拟DOM。
  • core:Vue核心库,定义了一些组件(例如keep-alive)、静态方法、Vue构造、初始化、声明周期函数、响应式机制函数、虚拟DOM函数。
  • platforms:平台相关代码,web、weex,里面的entry开头文件都是打包时的入口文件。
  • server:ssr、服务端渲染相关代码
  • sfc:single file component单文件组件,.vue文件编译为js对象
  • shared:公共的代码

Vue2.0的类型检查是使用的Flow,3.0版本已经使用TS,我们不必太去研究Flow,它们都是JS超集,最终都会编译成JS。

准备工作

调试设置

打包

Vue的打包工具是Rollup:

  • Vue.js 源码的打包工具使用的是 Rollup,比 Webpack 轻量。
  • Webpack 把所有文件当做模块,Rollup 只处理 js 文件,更适合在 Vue.js 这样的库中使用。
  • Rollup 打包不会生成冗余的代码,Webpack打包时会生成浏览器端支持的模块化代码。

这两打包器有各自的使用场景。

调试

在rollup的打包命令中添加 sourcemap 参数,方便断点调试:

"scripts": {
  "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
}

Vue不同的构建版本

可以使用npm run build命令重新打包所有文件。可通过文档查看具体的版本差异。

完整版

同时包含编译器和运行时的版本。

编译器

用来将模板字符串编译成为 JavaScript 渲染函数的代码。

运行时

用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码。基本上就是除去编译器的其它一切。runtime版本是不包含编译器的,所以不能通过Template方式去写html,而要自己以render函数形式去写html结构。

正式开始

寻找入口文件

顺着脚本命令能找到对应执行的脚本文件,从脚本文件分析就能看到 Vue 的 Rollup 配置文件是如何动态化生成的,并且依此能找到入口文件对应位置。

从入口开始

先通过查看源码来解决一个问题,当同时设置template和render时,会执行谁?渲染谁?

mount源码:

// $mount是把生成的DOM挂载到页面上来
Vue.prototype.$mount = function (
	el?: string | Element,
 	hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  // 判断el是否是body或html
  if (el === document.body || el === document.documentElement) {
    // 非生产环境则会进行警告,Vue只能挂载到普通元素上
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    // 并直接返回当前实例
    return this
  }
  // 获取创建Vue实例时,传入的所有属性
  const options = this.$options
  // resolve template/el and convert to render function

  // 判断是否有render选项
  if (!options.render) {
    // 如果未传递render,则会执行相关逻辑,后面详细看。整个过程是将template转换成render函数
    let template = options.template
    // 如果模板存在
    if (template) {
      // 如果模板是字符串
      if (typeof template === 'string') {
        // 如果模板是id选择器
        if (template.charAt(0) === '#') {
          // 获取对应DOM对象的 innerHTML
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }

    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
      // 生成render函数
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  // 如果传了render函数,则直接执行mount
  return mount.call(this, el, hydrating)
}

query函数:

/**
 * Query an element selector if it's not an element already.
 */
export function query (el: string | Element): Element {
  if (typeof el === 'string') {
    const selected = document.querySelector(el)
    if (!selected) {
      // 如果找不到根元素,在非生产环境下会报出警告
      process.env.NODE_ENV !== 'production' && warn(
        'Cannot find element: ' + el
      )
      // 默认创建一个div标签返回
      return document.createElement('div')
    }
    return selected
  } else {
    return el
  }
}

通过查看源码,我们发现:

  • el 不能是 body 或 html 标签。
  • 如果没有 render,则会把 template 转换成 render 函数。
  • 如果有 render 方法,直接调用 mount 挂载 DOM。

在调用栈中能看到 $mount 方法是在哪里被调用的,Vue的构造函数是在哪被调用的。

Vue初始化的过程

## entry-runtime-with-compiler.js

该入口文件对mount方法进行了增强,判断是否传入render方法

该入口文件增加了实例方法,Vue.compile = compileToFunctions。
它用于把html字符串编译成render函数。

该入口文件的核心是增加了将 template 转换成 render 函数的代码。

静态成员的初始化

直接上源码解析:

/* @flow */

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    // 如果是非生产环境,则会添加一个set方法,警告开发人员,不要去给config对象重新赋值
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }

  // 初始化Vue config对象
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  // 这些工具不视作全局Api的一部分,除非你已经意识到某些风险,否则不要去依赖它们
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  // 定义静态方法
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  // 让一个对象可响应
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

  // 初始化 Vue.options 对象,并给其扩展
  // 也就是'components','directives','filters',存储全局的组件、指令、过滤器
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  // 记录当前的Vue构造函数
  Vue.options._base = Vue

  // 注册了内置的keep-alive组件到全局,这里把一个对象所有属性拷贝到另一个对象中来
  // 
  extend(Vue.options.components, builtInComponents)

  // 注册Vue.use()用来注册组件
  initUse(Vue)
  // 注册Vue.mixin()实现混入
  initMixin(Vue)
  // 注册Vue.extend()基于传入的options返回一个组件的构造函数
  initExtend(Vue)
  // 注册Vue.directive()、Vue.component()、Vue.filter()
  initAssetRegisters(Vue)
}

同时在看设置Vue构造函数上的config时,在思考为啥没设置set方法,却依然能给它的属性做修改,并自己写了个demo:

testConfigDef.get = () => {
  return config
}
testConfigDef.get = () => {
  return {a: 1}
}

最后发现了原因,直接设置对象就不能修改,得用内存地址引用的方式,才能进行修改。

实例成员的初始化

在instance/index.js中能看到初始化实例成员的过程:

// 以下函数的作用都是给Vue的原型上,混入一些成员方法

// 注册 vm 的 _init() 方法,初始化 vm,相当于整个Vue的入口
initMixin(Vue)

// 注册 vm 的 $data/$props/$set/$delete/$watch
stateMixin(Vue)

// 初始化事件相关方法
// $on/$once/$off/$emit
eventsMixin(Vue)

// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)

// 混入 render
// $nextTick/_render
renderMixin(Vue)

接下来就分析initMixin函数的执行过程:

initMixin

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    // 定义唯一标识
    vm._uid = uid++

    // 性能检测相关
    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    // 标识vm对象为Vue实例,避免被observe,响应处理
    vm._isVue = true

    // merge options
    // 合并options,将用户传入的options和Vue构造函数中的options合并
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    /* istanbul ignore else */
    // 初始化renderProxy
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm

    // vm 的生命周期相关变量初始化
    // $children/$parent/$root/$refs
    initLifecycle(vm)

    // vm 的事件监听初始化, 父组件绑定在当前组件上的事件
    initEvents(vm)

    // vm 的编译render初始化,主要初始化h函数
    // $slots/$scopedSlots/_c/$createElement/$attrs/$listeners
    initRender(vm)

    // 触发第一个生命周期函数, beforeCreate 生命钩子的回调
    callHook(vm, 'beforeCreate')

    // 把 inject 的成员注入到 vm 上,依赖注入
    initInjections(vm) // resolve injections before data/props

    // 初始化 vm 的 _props/methods/_data/computed/watch
    initState(vm)

    // 初始化 Vue实例的provide属性,用于保存父组件的依赖
    initProvide(vm) // resolve provide after data/props

    // 触发第一个生命周期函数, created 生命钩子的回调
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    // 调用 $mount() 挂载整个页面
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

调试Vue初始化过程

下面以一张图来描述Vue首次渲染的过程:

对于有编译器的Vue版本编译,这里会发现有两个mount,这里是因为会给最原始的mount,这里是因为会给最原始的mount包装一下,加入编译器,用于对模板生成render函数。

然后再调用保存的$mount方法,在这个方法里触发了一些钩子函数,并且定义updateComponent,它内部是用于将虚拟DOM转换成真实DOM。

并且这里会创建一个渲染Watcher,在这里将其lazy属性设置为了false,所以它会立刻执行get方法(),这个get方法中会运行updateComponent函数,此时就完成了页面的刷新,将虚拟DOM挂载到了页面上。然后会执行mounted钩子函数。

经过调试发现,这个get函数在渲染过程中执行后,返回的value是个undefined。那是不是说这个watcher不会再执行了?

然后首次的渲染过程到这就结束了,this._init执行完毕。

数据响应式原理

数据响应式和双向绑定机制,是数据驱动开发的基石。这里要通过看源码来解答以下问题:

  • vm.msg = { count: 0 } ,重新给属性赋值,是否是响应式的?
  • vm.arr[0] = 4 ,给数组元素赋值,视图是否会更新
  • vm.arr.length = 0 ,修改数组的 length,视图是否会更新
  • vm.arr.push(4) ,视图是否会更新

响应式处理的入口

首先看src\core\instance\init.js,在Vue构造函数的init方法中,调用了initState方法,它的作用是初始化Vue实例的状态。 还初始化了 _data、_props、methods 等。

initState中又调用了initData方法,它的作用是将data中的属性注入到Vue实例,并将其转换为响应式对象。

// src\core\instance\state.js

if (opts.data) {
  // 将data里的成员注入到Vue实例上,并把data对象转换成一个响应式对象
  initData(vm)
} else {
  // 没有data属性,则初始化一个空对象,并转换成响应式对象
  observe(vm._data = {}, true /* asRootData */)
}

其中observe方法就是响应式的入口,它的地址为src/core/observer/index.js。observe方法会递归的为每一个对象转换为响应式对象,每一个响应式对象都会创建一个对应的 Observer 对象,记录到该对象的 __ob__ 属性中,并且每一个 Observer 对象都会有一个 dep 对象,负责为该对象收集依赖,当该对象发生变化时发送通知

数据响应式原理-依赖收集

这个dep和watcher和我们之前实现的有些区别,在Vue的watcher中也存储了dep,它是为了处理一个细节,视频中没深入。

数据响应式原理-数组

数组的响应式处理过程的核心代码在Observer类的构造函数中。判断value是否是数组,是数组的话就给它添加提前定义好的arrayMethods原型。

核心代码如下:

// 是否支持__proto__属性,处理兼容性
if (hasProto) {
  protoAugment(value, arrayMethods)
} else {
  // 直接将方法定义在数组对象上
  copyAugment(value, arrayMethods, arrayKeys)
}

// 为数组中的每一个对象创建一个 observer 实例,方法内部会判断成员是否是对象
this.observeArray(value)

如果是数组,则会改写它的原型对象上的方法,这里还会进行__proto__兼容性处理。它将会改变数组的方法进行了重写:

    • 先调用原方法获取结果。
    • 针对push、unshift、splice方法,获取新增的元素,调用observe方法进行观察。
    • 重写的方法中,加入了调用notify方法的逻辑,每次修改数组,发起通知。
    • 最后调用observeArray方法,为数组中的每一个对象创建一个 observer 实例。

被改写后的数组,它的属性如下,最下面的__proto__才是真正的原生数组原型:

从源码可看出,Vue未处理改变索引的值、改变数组长度时的响应式处理。Vue只处理了每一个元素,未处理每一个属性。可以用splice方法替代上述操作。

数据响应式原理-Watcher

Watcher分为三种:Computed Watcher、用户Watcher(侦听器)、渲染Watcher。

渲染Watcher初始化

Watcher的初始化在src/core/observer/watcher.js中进行了注释

Watcher的执行

和之前自己实现的观察者模式不同,Watcher实例中还有一个deps属性,它是用来干嘛的呢?首先明确,1个Dep可以被多个Watcher订阅,但是1个Watcher也可以订阅多个Dep,watcher使用它来记录所有订阅的Dep。

watcher执行过程,有一个queue的概念,它会根据watcher的id进行排序,它的目的如下:

  1. 组件更新顺序保证为父组件到子组件,因为先创建父组件再创建子组件
  2. 组件的用户watcher要在渲染watcher之前进行,因为用户watcher是在渲染watcher之前被创建的。用户watcher、计算属性watcher是在initState中创建的,在mountComponent中创建的渲染watcher。
  3. 如果一个组件在父组件运行期间被销毁,则跳过该watcher

渲染watcher的执行过程是:数据发生变化后,调用Dep的notify方法,根据watcher的id进行从小到大排序,再调用每个watcherUpdate方法。在queueWatcher方法里,watcher会被放入队列中,然后依次调用它们的run方法,在run方法中调用了get方法,最终调用了updateComponent函数。

而对于渲染watcher,它不需要被notify,它会直接执行get方法。

调试-响应式数据执行过程

数组响应式处理的核心过程

constructor (value: any) {
  this.value = value
  this.dep = new Dep()
  // 初始化实例的vmCount为0
  this.vmCount = 0
  // 将实例挂载到对象的__ob__属性
  def(value, '__ob__', this)

  // !!!!对数组做额外的响应式处理!!!!
  if (Array.isArray(value)) {
    // 判断当前浏览器是否支持proto,处理兼容性问题
    if (hasProto) {
      protoAugment(value, arrayMethods)
    } else {
      copyAugment(value, arrayMethods, arrayKeys)
    }
    // 为数组中的每一个对象创建一个observer实例
    this.observeArray(value)
    
  } else {
    // 遍历对象中的每一个属性,转换成setter/getter
    this.walk(value)
  }
}

数组收集依赖的过程

// 下面部分为收集依赖
// 如果存在当前依赖目标,即 watcher 对象,则建立依赖
if (Dep.target) {
  // 为该属性收集依赖
  dep.depend()
  if (childOb) {
    // 这里的dep是为当前的这个子对象收集依赖,当子对象发生添加或删除操作时,就可以发送通知
    childOb.dep.depend()
    if (Array.isArray(value)) {
      // 如果该属性为数组,则会寻找数组中为“对象”的元素,判断其有没有__ob__(observer对象)
      // 如果有的话,则将当前的watcher添加到其dep中去,当数据变化时,通知它改变
      dependArray(value)
    }
  }
}

数组的数据改变时,watcher执行的过程

根据源码可以发现Vue只对数组的key和数组对象本身收集了依赖,所以当数组的属性变化时,并不会触发notify更新。

经过调试发现,在Vue中对于渲染watcher的更新函数就是updateComponent,也就是patch函数,用于寻找新旧VNode之间的差异,并更新至真实DOM。用户watcher的更新函数就是用户所传入的了。

疑问:为什么要清空上一次的依赖呢?也就是watcher为什么要定义一个deps属性,来收集这个watcher所依赖的dep呢?

回答:

 /**
   * Depend on all deps collected by this watcher.
   */
// 这里存在多对多的关系,watcher也可以订阅多个dep,在这里会遍历deps中所有dep,
// 将本watcher实例通过Dep.target记录到,对应响应式数据的 dep 的 subs 数组中
depend () {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

所以当deps中,某个数据变化时,就会通知这个watcher。根据源码显示,貌似只有定义计算属性的时候,才会执行这个watcher.depend()其实可以理解,只有计算属性的监听,才会同时监听多个dep吧

根据实际测试,发现渲染watcher,也会监听多个watcher,表现为,页面直接依赖多少个数据,deps就会存多少个dep。

新的认知: 首次渲染时,传给 watcher 的 cb 就是 updateComponent,而Vue初始化时,给每个state(model)数据所绑定的watcher,回调函数都是 updateComponent,也就是在之后每次数据变化时,都会执行这个更新虚拟DOM->更新真实视图的方法。

响应式处理过程总结

此处数组响应式处理,是对会更改原数组的方法,做了处理,添加了notify方法

$set 源码

// global-api/index.js

// 构造函数中的放法,静态方法
Vue.set()

// instance/index.js

// 实例方法
vm.$set()
/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  // 判断target是否未定义,是否为原始值
  if (process.env.NODE_ENV !== 'production' &&
      (isUndef(target) || isPrimitive(target))
     ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 如果target是数组,则判断索引是否为有效的索引
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    // 通过splice方法对key位置的元素进行替换
    // splice 在 array.js 进行了响应化处理,有更新能通知视图变化
    // 这里最终会调用target上ob对象的dep对象的notify方法
    target.splice(key, 1, val)
    return val
  }

  // 如果key在对象上已经存在,且不在原型上(防止用户赋值原型上的属性),则直接进行赋值
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }

  // 如果key在target上不存在,则继续执行,获取ob对象
  const ob = (target: any).__ob__
  // 如果 target 是Vue实例,或者为$data,则直接返回。这里$data的vmCount为1,其它的都为0,可在源码中观察到这点
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }

  // 如果不存在ob对象,则target不是响应式对象,则直接赋值。有什么情况会不存在ob对象呀
  if (!ob) {
    target[key] = val
    return val
  }

  // 如果有ob对象,则将key设置为响应式属性
  defineReactive(ob.value, key, val)
  // 并且发送通知
  ob.dep.notify()
  return val
}

$delete源码

  • 功能

删除对象的属性,如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开Vue不能检测到属性被删除的限制,但是你应该很少会使用到它。

目标对象不能是一个 Vue 实例或 Vue 实例的根数据对象。

Vue.delete( target, propertyName/index )

如果删除对象是响应式的,则会调用dep.notify方法更新视图。

$ watch源码

  • 功能

观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受简单的键路径。对于更复杂的表达式,用一个函数取代。

vm.$watch( expOrFn, callback, [options] )
  • 参数
    • {string | Function} expOrFn
    • {Function | Object} callback
    • {Object} [options]
      • {boolean} deep
      • {boolean} immediate
  • 返回值:{Function} unwatch

三种类型的Watcher对象

  • 没有静态方法,因为$watch方法中要使用Vue的实例
  • Watcher分三种:计算属性Watcher、用户Watcher(侦听器)、渲染Watcher
    • 创建顺序:计算属性Watcher、用户Watcher(侦听器)、渲染Watcher
  • vm.$wtach()
    • src\core\instance\state.js

疑问:计算属性computed的源码

答:computed也是watcher的一种,传入了computedWatcherOptions配置,标记lazy为true。计算属性为lazy,是因为计算属性是在模板中被调用的,它是在render过程中,调用对应计算属性的方法的

watcher如上顺序所调用,在flushScheduleQueue方法中,会对watcher进行排序。

源码部分

疑问:watcher.teardown()源码学习

异步更新队列-nextTick方法

  • Vue更新DOM是异步执行的,批量的
    • 在下次更新DOM更新循环结束之后,执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。

nextTick源码

该方法既有静态方法也有实例方法。它的核心在于timerFunc方法的处理。所有watcher的更新,是放在nextTick中去执行的。

它的调用可分为:

  • 手动调用vm.$nextTick()
  • 在Watcher的queueWatcher中执行nextTick()

Vue 会根据当前浏览器环境优先使用原生的 Promise.then 和 MutationObserver,如果都不支持,就会采用 setTimeout 代替,目的是延迟函数到 DOM 更新后再使用。

如果浏览器支持Promise,那么就用Promise.then的方式来延迟函数调用,Promise.then方法可以将函数延迟到当前函数调用栈最末端,也就是函数调用栈最后调用该函数。从而做到延迟。

我认为这个方法不仅仅是等DOM渲染完后,获取最新的视图数据,还有一个点是做缓冲,在数据更新完毕后,拿到最终的数据,只进行一次DOM更新。不然一个数据被循环改变100次,就要执行100次重绘,性能开销很大的。

同步任务执行完之后会执行异步任务,在异步任务中先执行一个宏任务,然后清空微任务队列,然后进行GUI渲染视图

也就是我们通过将timerFunc包装成异步任务,然后我们的nexttick中传入的回调会在flushSchedulerQueue执行之后执行,所以我们在回调中是可以获取到最新的DOM的,不同在于如果timerFunc如果是微任务的话,浏览器把DOM更新的操作放在Tick执行microTask的阶段来完成,相比使用宏任务生成的一个macroTask会少一次UI的渲染。

相关解析: