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

浏览器断点位置

  1. 进入core/instance/index.js中,定义Vue构造函数,构造函数中调用_init方法(首次渲染),initMixin等等都是给Vue原型上挂载实例方法

    _init首次渲染过程

    • 设置_isVue变量,如果是Vue实例不被observe做响应式处理

    • 判断Vue是否是组件,根据不同情况合并optinos选项

2.png
  • 设置渲染代理对象,开发环境执行initProx(当前环境支持代理对象,则通过Proxy代理vm对象;不支持则将vm实例设置给renderProxy),生成环境设置_renderProxy = vm,直接将vue实例设置给renderProxy

  • 执行initLifecycle等方法,给vue挂载成员

  • 执行vm.mount(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钩子,页面加载完毕,首次渲染完成

  1. 进入core/index.js中,调用共initClobalAPI初始化Vue的静态成员

  2. 进入platforms/web/runtime/index.js中,初始化跟平台相关的内容(如指令和组件),并给Vue原型上挂载了__path__方法和$mount

  3. 进入platforms/web/entry-runtime-with-compiler.js入口文件,重写上一文件的$mount方法,增加了将模板转换为render函数的编译函数


    4.png
响应式实现原理
  1. 入口src/core/instance/init.js

  2. 调用initState(vm)初始化props、methods、data、computed、watch等

  3. initData中判断是否和methods或props重名,将data成员注入到vue实例,并做响应式处理observe(data, true),true代表根数据

  4. observe在ibserver下的index中定义,判断value不是对象,或者是VNode实例是直接返回,不需要做响应式处理

  5. 定义ob,如果value有__ob__属性并且是Observer对象实例,直接赋值ob变量

  6. 如果没有,则创建一个Observer对象,赋值给ob

  7. Observer构造函数中,通过def定义__ob__属性,将Observer对象记录下来

  8. 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替代

5.png
6.png
  1. defineReactive中为对象定义响应式属性

  2. 创建dep依赖对象实例,获取当前对象的属性描述符,不可操作直接返回

  3. 调用Object.defineProperty转换为getter/setter

  4. 在get中进行依赖收集,如果Dep上存在target对象,为watcher对象,则建立依赖,调用dep.depend()将当前dep对象添加到watcher对象中的集合中,并且会将watcher对象添加到dep的subs数组中

  5. 在set中派发更新

Watcher类

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

渲染Watcher首次渲染:

  1. core/instance/lifecycle.js中的mountComponent创建渲染Watcher

    new Watcher(vm, updateComponent, noop, {
        before() {
            if (vm._isMounted && !vm._isDestroyed) {
             callHook(vm, 'beforeUpdate')
            }
        }
    }, true /* isRenderWatcher */)
    
  2. 进入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函数

数据响应式--总结
  1. vue实例的_init方法开始,调用initState()初始化Vue实例的状态,在initState中调用initData()将data属性注入到vue实例、observer()将data数据转换为响应式对象
  2. src/core/observer/index.js响应式入口observer(value),判断value是否时对象,如果不是直接返回,判断vaule是否有__ob__属性,如果有说明对象之前已经做过了响应式i处理,直接返回;如果没有为这个对象创建observer对象,返回observer
  3. src/core/observer/index.js创建observer过程,给当前value对象定义不可枚举的__ob__属性,用来记录当前的observer对象,然后分别进行数组的响应式处理(设置数组的可改变原数组的方法)和对象的响应式处理(walk方法,遍历对象所有属性,调用defineReactive)
  4. defineReactive中,为每个属性创建dep对象用于收集依赖,如果当前属性的值是对象,调用observe将此对象转换为响应式对象
  5. defineReactive中核心是定义getter和setter,在getter中为每个属性收集依赖,并返回属性的值;在setter中首先保存新值,如果是对象,要调用observe转换为响应式对象,数据发生变化派发更新,调用dep.notify方法
  6. 收集依赖过程,在watcher对象的get方法中调用pushTarget,pushTarget方法会将当前watcher对象记录到Dep.target属性中
  7. 接着当访问data中的成员时去收集依赖,触发defineReactive的getter,在getter中去收集依赖——将属性对应的watcher对象添加到dep的subs数组中,为属性收集依赖,如果属性的值也是对象,要创建childOb对象为子对象收集依赖(子对象发生变化时发送通知)
  8. 当数据发生变化时,在watcher中调用dep.notify发送通知,调用watcher的update方法,在update方法中调用queueWatcher()函数,判断watcher对象是否被处理,如果这个watcher对象没有被处理的话添加到队列queue中,并调用flushSchedulerQueue()刷新任务队列
  9. 在flushSchedulerQueue中触发beforeUpdate钩子,并调用watcher.run方法,在run方法中调用watcher的get方法,从而调用getter(updateComponent)
  10. watcher.run运行完成之后,数据更新视图完毕,此时渲染已完成
  11. 清空上一次依赖,重置watcher中的状态
  12. 触发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没有静态方法,因为watch方法中要使用Vue的实例

  • 三种类型Watcher:计算属性Watcher、用户Watcher(侦听器)、渲染Watcher

  • 创建顺序计算属性Watcher、用户Watcher、渲染Watcher

    首先创建计算属性Watcher,并设置id为1

7.png

接着创建用户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多种处理方式

    1. 优先使用Promise(微任务)处理flushCallbacks,微任务是在本次循环结束,所有同步任务执行完成之后才会执行;nextTick方法作用是获取DOM上最新的数据,而微任务执行的时候,DOM还没有渲染到浏览器。当nextTick函数执行之前,数据已经被改变了,当数据变化时会立即发送通知Wathcer渲染视图,而在Watcher中首先会将DOM上的数据进行更新(更改DOM树),DOM更新浏览器是在当前事件循环结束之后,才会执行DOM的更新操作,所以nextTick中如果使用Promise(微任务)的话,是从DOM树上直接获取数据,此时DOM还没有渲染
    2. 如果浏览器不兼容Promise,则使用setTimeout,并标志当前使用的是微任务isUsingMicroTask = true,接着判断当前浏览器不是IE(IE10、IE11开始支持,并且会有小问题),并且支持MutationObserver(监听DOM对象改变,如果改变会执行回调函数,也是以微任务的形式执行)
    3. 如果不支持Promise,并且不支持MutationObserver,则会执行setImmediate(只有IE浏览器和node环境支持),setImmediate性能比setTimeout好,因为setTimeoue设置为0时,也需要等待4ms的时间,而setImmediate会立即执行
      首先以微任务方式执行回调函数,如果浏览器不支持会降低为宏任务
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容