Vue.js源码剖析-响应式原理
寻找入口文件
执行构建
npm run dev
# "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
# --environment TARGET:web-full-dev 设置环境变量 TARGET
# webweb平添 full完整版,包含编译器和运行时 dev开发版,不对代码进行压缩
-
script/config.js 的执行过程
// 判断环境变量是否有 TARGET // 如果有的话 使用 genConfig() 生成 rollup 配置文件 if (process.env.TARGET) { module.exports = genConfig(process.env.TARGET) } else { // 否则获取全部配置 exports.getBuild = genConfig exports.getAllBuilds = () => Object.keys(builds).map(genConfig) }
-
genConfig()执行过程
// 根据环境变量 TARGET 获取配置信息 // builds[name] 获取生成配置的信息 const opts = builds[name] // Runtime+compiler development build (Browser) 'web-full-dev': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.js'), format: 'umd', env: 'development', alias: { he: './entity-decoder' }, banner }
-
resolve()
// 获取入口和出口文件的绝对路径 const aliases = require('./alias') const resolve = p => { // 根据路径中的前半部分去alias中找别名 const base = p.split('/')[0] if (aliases[base]) { return path.resolve(aliases[base], p.slice(base.length + 1)) } else { return path.resolve(__dirname, '../', p) } }
-
结果
把 src/platforms/web/entry-runtime-with-compiler.js 构建成 dist/vue.js,如果设置 -- sourcemap 会生成 vue.js.map
src/platform 文件夹下是 Vue 可以构建成不同平台下使用的库,目前有 weex 和 web,还有服务 器端渲染的库
Vue的构造函数
-
src/platform/web/entry-runtime-with-compiler.js引用了./runtime/index.js
-
src/platform/web/runtime/index.js中设置Vue.config、注册平台相关的指令v-model、v-show,创建组件transition、transitio-group,设置平台相关的
__patch__
方法,设置$mount方法,挂载DOM// install platform runtime directives & components // 注册跟平台相关的全局的指令和组件 extend(Vue.options.directives, platformDirectives) extend(Vue.options.components, platformComponents) // install platform patch function // 虚拟DOM转换成真实DOM Vue.prototype.__patch__ = inBrowser ? patch : noop // public mount method Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
-
接着在src/core/index中定义Vue的静态方法initGlobalAPI(vue)
-
最后在src/core/instance/index.js中定义了Vue的构造函数
// 此处不用 class 的原因是因为方便后续给 Vue 实例混入实例成员 function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } // 调用 _init() 方法 相当于程序入口 this._init(options) } // 这几个方法 都是给Vue的原型上混入了成员 // 注册 vm 的 _init() 方法,初始化 vm initMixin(Vue) // 初始化vm的$data/$props 注册 vm 的/$set/$delete/$watch stateMixin(Vue) // 初始化事件相关方法 // $on/$once/$off/$emit eventsMixin(Vue) // 初始化生命周期相关的混入方法 // _update/$forceUpdate/$destroy lifecycleMixin(Vue) // 混入 render // $nextTick/_render renderMixin(Vue)
四个导出Vue的模块
- src/platforms/web/entry-runtime-with-compiler.js
- web平台相关的入口
- 重写了平台相关的$mount方法,增加功能
- 注册Vue.compile方法,将HTML字符串编译成Dom
- src/platforms/web/runtime/index.js
- web平台相关
- 注册和平台相关的全局指令:v-model、v-show
- 注册和平台相关的全局组件:v-transition、v-transitino-group
- 注册全局方法
__patch__
、$mount
- src/core/index.js
- 与平台无关
- 设置Vue的静态方法initGlobalAPI(vue)
- src/core/instance/index.js
- 与平台无关
- 定义vue的构造函数,调用this._init(options)方法,相当于程序入口
- 给Vue原型上换入实例成员
Vue初始化
-
src/core/global-api/index.js中初始化Vue的静态方法
// 注册 Vue 的静态属性/方法 initGlobalAPI(Vue) // src/core/global-api/index.js // 初始化 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 } // 静态方法 set/delete/nextTick 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/_base 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 plainobject // components with in Weex's multi-instance scenarios. 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)
-
src/core/instance/index.js中定义Vue的构造函数、初始化Vue的实例成员
// 此处不用 class 的原因是因为方便后续给 Vue 实例混入实例成员 function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } // 调用 _init() 方法 相当于程序入口 this._init(options) } // 这几个方法 都是给Vue的原型上混入了成员 // 注册 vm 的 _init() 方法,初始化 vm initMixin(Vue) // 初始化vm的$data/$props 注册 vm 的/$set/$delete/$watch stateMixin(Vue) // 初始化事件相关方法 // $on/$once/$off/$emit eventsMixin(Vue) // 初始化生命周期相关的混入方法 // _update/$forceUpdate/$destroy lifecycleMixin(Vue) // 混入 render // $nextTick/_render renderMixin(Vue)
-
src/core/instance/init.js中初始化_init方法
// 入口 export function initMixin (Vue: Class<Component>) { // 给 Vue 实例增加 _init() 方法 // 合并 options / 初始化操作 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 // 如果是 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 */ // 开发环境调用initProxy 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初始化 // $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 并注入到vue实例 initState(vm) // 初始化 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) } // 如果没有提供 el,调调用 $mount() 挂载页面 if (vm.$options.el) { vm.$mount(vm.$options.el) } } }
首次渲染过程
1.png浏览器断点位置
-
进入core/instance/index.js中,定义Vue构造函数,构造函数中调用_init方法(首次渲染),initMixin等等都是给Vue原型上挂载实例方法
_init首次渲染过程
-
设置_isVue变量,如果是Vue实例不被observe做响应式处理
-
判断Vue是否是组件,根据不同情况合并optinos选项
-
-
设置渲染代理对象,开发环境执行initProx(当前环境支持代理对象,则通过Proxy代理vm对象;不支持则将vm实例设置给renderProxy),生成环境设置_renderProxy = vm,直接将vue实例设置给renderProxy
-
执行initLifecycle等方法,给vue挂载成员
-
执行vm.optinos.el)方法,判断options中是否有render,有直接调用mount方法,没有判断是否有template,分情况处理,最后执行mount方法
-
mount中重新获取el(运行时版本需要获取),执行mountComponent方法
-
mountComponent判断是否有render,如果没有会提示下面报错
3.png
-
使用callHook触发beforeMount钩子,、定义
updateComponent(vm._update(vm._render(), hydrating))
,_renderd方法调用用户传入的render或编译器生成的render,最终生成虚拟DOM,_update将虚拟DOM转换为真实DOM -
创建Watcher对象,调用updateComponent方法
-
最后触发mounted钩子,页面加载完毕,首次渲染完成
-
进入core/index.js中,调用共initClobalAPI初始化Vue的静态成员
-
进入platforms/web/runtime/index.js中,初始化跟平台相关的内容(如指令和组件),并给Vue原型上挂载了
__path__
方法和$mount -
进入platforms/web/entry-runtime-with-compiler.js入口文件,重写上一文件的$mount方法,增加了将模板转换为render函数的编译函数
4.png
响应式实现原理
-
入口src/core/instance/init.js
-
调用initState(vm)初始化props、methods、data、computed、watch等
-
initData中判断是否和methods或props重名,将data成员注入到vue实例,并做响应式处理observe(data, true),true代表根数据
-
observe在ibserver下的index中定义,判断value不是对象,或者是VNode实例是直接返回,不需要做响应式处理
-
定义ob,如果value有
__ob__
属性并且是Observer对象实例,直接赋值ob变量 -
如果没有,则创建一个Observer对象,赋值给ob
-
Observer构造函数中,通过def定义
__ob__
属性,将Observer对象记录下来 -
Observer中对value进行响应式处理(数组、对象defineReactive)
数组的响应式处理在src/core/observer/index.js中
// 数组的响应式处理 if (Array.isArray(value)) { // 重新设置数组中会改变元素的方法 if (hasProto) { // target.__proto__ = src protoAugment(value, arrayMethods) } else { // 和上面方法的作用相同 copyAugment(value, arrayMethods, arrayKeys) } // 为数组中的每一个对象创建一个 observer 实例 this.observeArray(value) } else { // 遍历对象中的每一个属性,转换成 setter/getter this.walk(value) } // arrayMethods修改和数组相关的一些方法 // src/core/observer/array.js中对数组方法进行处理 /* * 1.遍历数组方法,找到可能会给数组新增元素的方法 * 2.如果新增了元素,调用ob.observerArray(inserted)遍历数组新增的元素设置为响应式数据 * 3.调用dep.notify()发送通知,并返回当前结果 */
处理数组响应式时,没有遍历数组中的所有属性,而是遍历数组中所有元素,将数组中对象转换为响应式对象;所以通过vm.arr[0]或vm.arr.length修改数组不会更新视图,可以通过vm.arr.splice替代
-
defineReactive中为对象定义响应式属性
-
创建dep依赖对象实例,获取当前对象的属性描述符,不可操作直接返回
-
调用Object.defineProperty转换为getter/setter
-
在get中进行依赖收集,如果Dep上存在target对象,为watcher对象,则建立依赖,调用dep.depend()将当前dep对象添加到watcher对象中的集合中,并且会将watcher对象添加到dep的subs数组中
-
在set中派发更新
Watcher类
三种:Computed Watcher、用户Watcher(侦听器)、渲染Watcher
渲染Watcher首次渲染:
-
core/instance/lifecycle.js中的mountComponent创建渲染Watcher
new Watcher(vm, updateComponent, noop, { before() { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)
-
进入Watcher构造函数,使用vm._watchers.push(this)存储所有watcher,判断第二个参数的类型
if (typeof expOrFn === 'function') { this.getter = expOrFn } else { // expOrFn 是字符串的时候,例如 watch: { 'person.name': function... } // parsePath('person.name') 返回一个函数获取 person.name 的值 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 ) } }
之后调用get方法,get会执行updateComponent方法更新视图
updateComponent = () => { vm._update(vm._render(), hydrating) }
数据更新
当数据更新时,调用dep的notify方法通知watcher,先把watcher放入一个队列中,遍历队列,调用所有watcher的run方法,在run方法中,最终调用了渲染watcher的updateComponent函数
数据响应式--总结
- vue实例的_init方法开始,调用initState()初始化Vue实例的状态,在initState中调用initData()将data属性注入到vue实例、observer()将data数据转换为响应式对象
- src/core/observer/index.js响应式入口observer(value),判断value是否时对象,如果不是直接返回,判断vaule是否有
__ob__
属性,如果有说明对象之前已经做过了响应式i处理,直接返回;如果没有为这个对象创建observer对象,返回observer - src/core/observer/index.js创建observer过程,给当前value对象定义不可枚举的
__ob__
属性,用来记录当前的observer对象,然后分别进行数组的响应式处理(设置数组的可改变原数组的方法)和对象的响应式处理(walk方法,遍历对象所有属性,调用defineReactive) - defineReactive中,为每个属性创建dep对象用于收集依赖,如果当前属性的值是对象,调用observe将此对象转换为响应式对象
- defineReactive中核心是定义getter和setter,在getter中为每个属性收集依赖,并返回属性的值;在setter中首先保存新值,如果是对象,要调用observe转换为响应式对象,数据发生变化派发更新,调用dep.notify方法
- 收集依赖过程,在watcher对象的get方法中调用pushTarget,pushTarget方法会将当前watcher对象记录到Dep.target属性中
- 接着当访问data中的成员时去收集依赖,触发defineReactive的getter,在getter中去收集依赖——将属性对应的watcher对象添加到dep的subs数组中,为属性收集依赖,如果属性的值也是对象,要创建childOb对象为子对象收集依赖(子对象发生变化时发送通知)
- 当数据发生变化时,在watcher中调用dep.notify发送通知,调用watcher的update方法,在update方法中调用queueWatcher()函数,判断watcher对象是否被处理,如果这个watcher对象没有被处理的话添加到队列queue中,并调用flushSchedulerQueue()刷新任务队列
- 在flushSchedulerQueue中触发beforeUpdate钩子,并调用watcher.run方法,在run方法中调用watcher的get方法,从而调用getter(updateComponent)
- watcher.run运行完成之后,数据更新视图完毕,此时渲染已完成
- 清空上一次依赖,重置watcher中的状态
- 触发actived、updated钩子函数
vm.$set和Vue.set
功能:向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性 (比如 this.myObject.newProperty = 'hi')
vm.$set(obj, 'foo', 'test')
-
源码位置
Vue.set()定义在global-api/index.js中
// 静态方法 set/delete/nextTick Vue.set = set Vue.delete = del Vue.nextTick = nextTick
vm.$set()定义在instance/index.js
// 注册 vm 的 $data/$props/$set/$delete/$watch // instance/state.js stateMixin(Vue) // instance/state.js Vue.prototype.$set = set Vue.prototype.$delete = del
set()方法定义在observer/index.js中
export function set (target: Array<any> | Object, key: any, val: any): any { ... // 判断 target 是否是对象,key 是否是合法的索引 if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) // 通过 splice 对key位置的元素进行替换 // splice 在 array.js进行了响应化的处理 target.splice(key, 1, val) return val } // 如果 key 在对象中已经存在直接赋值 if (key in target && !(key in Object.prototype)) { target[key] = val return val } // 获取 target 中的 observer 对象 const ob = (target: any).__ob__ // 如果 target 是 vue 实例或者$data 直接返回 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 不是响应式对象直接赋值 if (!ob) { target[key] = val return val } // 把 key 设置为响应式属性 defineReactive(ob.value, key, val) // 发送通知 ob.dep.notify() return val }
vm.$delete和Vue.delete()
删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制,但是你应该很少会使用它。
vm.$delete(vm.obj, 'msg')
// 源码逻辑基本和set逻辑相同
export function del (target: Array<any> | Object, key: any) {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive
value: ${(target: any)}`)
}
// 判断是否是数组,以及 key 是否合法
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 如果是数组通过 splice 删除
// splice 做过响应式处理
target.splice(key, 1)
return
}
// 获取 target 的 ob 对象
const ob = (target: any).__ob__
// target 如果是 Vue 实例或者 $data 对象,直接返回
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
// 如果 target 对象没有 key 属性直接返回
if (!hasOwn(target, key)) {
return
}
// 删除属性
delete target[key]
if (!ob) {
return
}
// 通过 ob 发送通知
ob.dep.notify()
}
vm.$watch()
观察 Vue 实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。表达式只接受监督的键路径。对于更复杂的表达式,用一个函数取代。
vm.$watch( expOrFn, callback, [options] )
-
参数
- expOrFn:要监视的 $data 中的属性,可以是表达式或函数
- callback:数据变化后执行的函数:回调函数;对象:具有 handler 属性(字符串或者函数),如果该属性为字符串则 methods 中相应 的定义
- options:可选的选项 deep:布尔类型,深度监听 immediate:布尔类型,是否立即执行一次回调函数
-
示例
const vm = new Vue({ el: '#app', data: { a: '1', b: '2', msg: 'Hello Vue', user: { firstName: '诸葛', lastName: '亮' } } }) // expOrFn 是表达式 vm.$watch('msg', function (newVal, oldVal) { console.log(newVal, oldVal) }) vm.$watch('user.firstName', function (newVal, oldVal) { console.log(newVal) }) // expOrFn 是函数 vm.$watch(function () { return this.a + this.b }, function (newVal, oldVal) { console.log(newVal) }) // deep 是 true,消耗性能 vm.$watch('user', function (newVal, oldVal) { // 此时的 newVal 是 user 对象 console.log(newVal === vm.user) }, { deep: true }) // immediate 是 true vm.$watch('msg', function (newVal, oldVal) { console.log(newVal) }, { immediate: true })
三种类型Watcher对象
-
watch方法中要使用Vue的实例
-
三种类型Watcher:计算属性Watcher、用户Watcher(侦听器)、渲染Watcher
-
创建顺序计算属性Watcher、用户Watcher、渲染Watcher
首先创建计算属性Watcher,并设置id为1
接着创建用户Watcher,设置id为2
8.png最后创建渲染Watcher,设置id为3
9.png-
执行顺序计算属性Watcher、用户Watcher、渲染Watcher
调用flush函数,在flush函数中对watcher的id从小到大进行排序,依次执行watcher
-
vm.$watch()源码目录src/core/instance/state.js
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { // 获取 Vue 实例 this const vm: Component = this if (isPlainObject(cb)) { // 判断如果 cb 是对象执行 createWatcher return createWatcher(vm, expOrFn, cb, options) } options = options || {} // 标记为用户 watcher 用户watcher在调用回调函数时需要加try...catch... options.user = true // 创建用户 watcher 对象 const watcher = new Watcher(vm, expOrFn, cb, options) // 判断 immediate 如果为 true if (options.immediate) { // 立即执行一次 cb 回调,并且把当前值传入 try { cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } // 返回取消监听的方法 return function unwatchFn () { watcher.teardown() } }
异步更新队列nextTick
-
Vue 更新 DOM 是异步执行的,批量的
-
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
-
vm.$nextTick(function () { /* 操作 DOM */ }) / Vue.nextTick(function () {})
-
调用方法
- 手动调用vm.$nextTick()
- 在Watcher的queueWatcher中执行nextTick()
-
源码
// 静态方法Vue.nextTick() src/core/global-api/index.js Vue.nextTick = nextTick // 实例方法vm.$nextTick() src\core\instance\render.js Vue.prototype.$nextTick = function (fn: Function) { return nextTick(fn, this) } // nextTick函数src/cote/util/next-tick.js export function nextTick (cb?: Function, ctx?: Object) { let _resolve // 把 cb 加上异常处理存入 callbacks 数组中 callbacks.push(() => { if (cb) { try { // 调用 cb() 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') { // 返回 promise 对象 return new Promise(resolve => { _resolve = resolve }) } }
找到所有回调函数,通过timerFunc()进行调用,timerFunc中调用flushCallbacks,flushCallbacks设置pedding为false,表示已经处理结束,备份callbacks并清空,调用备份中的每一个函数(函数并不是直接调用,而是通过Promise进行调用)
timerFunc多种处理方式- 优先使用Promise(微任务)处理flushCallbacks,微任务是在本次循环结束,所有同步任务执行完成之后才会执行;nextTick方法作用是获取DOM上最新的数据,而微任务执行的时候,DOM还没有渲染到浏览器。当nextTick函数执行之前,数据已经被改变了,当数据变化时会立即发送通知Wathcer渲染视图,而在Watcher中首先会将DOM上的数据进行更新(更改DOM树),DOM更新浏览器是在当前事件循环结束之后,才会执行DOM的更新操作,所以nextTick中如果使用Promise(微任务)的话,是从DOM树上直接获取数据,此时DOM还没有渲染
- 如果浏览器不兼容Promise,则使用setTimeout,并标志当前使用的是微任务isUsingMicroTask = true,接着判断当前浏览器不是IE(IE10、IE11开始支持,并且会有小问题),并且支持MutationObserver(监听DOM对象改变,如果改变会执行回调函数,也是以微任务的形式执行)
- 如果不支持Promise,并且不支持MutationObserver,则会执行setImmediate(只有IE浏览器和node环境支持),setImmediate性能比setTimeout好,因为setTimeoue设置为0时,也需要等待4ms的时间,而setImmediate会立即执行
首先以微任务方式执行回调函数,如果浏览器不支持会降低为宏任务