vue2源码解析每日产出

192 阅读13分钟

变化侦测篇

Object的变化侦测(20200812)

  1. 利用Object.definePrototype();中的get和set属性可以知道什么时候读取了数据,什么时候设置了数据
  2. 在Observer中,get属性中收集依赖,在set属性中通知依赖
  3. Dep就是一个依赖管理器,负责收集依赖,以及向依赖发送通知
  4. Dep收集以及通知的都是Watcher,由Watcher再去向外界通知。

Array的变化侦测(20200813)

  1. array还在data()方法返回的对象中,所以还是在getter中收集依赖。
  2. 重新拦截了改变数组的7个方法:push,pop,shift,unshift,splice,sort,reverse,在拦截中给依赖发送通知;所以只有对数组使用这7个方法,才会发送通知,使视图更新。(不讨论vue.set和vue.delete后续再讨论)
  3. 对数组进行深度侦测的方法是将数组中的每个值都变成Observer。
  4. 对push,unshift.splice三个方法特殊处理了添加值得操作,将添加得值也进行放到依赖收集中。

虚拟dom篇

vue中的虚拟dom(20200814)

  1. 虚拟dom,用一个js对象来描述一个真实的dom节点。
  2. 操作真实dom太耗费性能,所以使用虚拟dom,通过消耗js计算性能来减少操作真实dom。当数据发生变化时,对比前后虚拟节点的变化,通过DOM-Diff算法计算出需要更新的dom节点,再去更新需要更新的视图。
  3. vue中有一个VNode类来实例化出不同类型的虚拟dom节点。总共有6类虚拟节点,注释节点、文本节点、元素节点、组件节点、函数式组件节点、克隆节点。

vue中的DOM-Diff(20200814)

在vue中,DOM-Diff的过程叫作patch过程。以新的VNode为基准,去更新旧的VNode与新的VNode一致,主要要做三件事(patch.js文件中):

  1. 创建节点:新VNode中存在而旧VNode不存在,就在旧的VNode中创建。上面提到有6种虚拟节点,实际插入到真实dom中的只有3种,元素节点、注释节点和文本节点。会判断插入的这个节点的类型,从而调用不同的方法插入到真实dom中。
  2. 删除节点:新的VNode中不存在,而旧VNode中存在,就在旧的VNode中删除。删除节点比较简单,拿到需要被删除的节点的父节点,调用removeChild方法即可。
  3. 更新节点:新、旧VNode都存在时,以新的VNode为基准,更新旧的VNode。(patchVnode())
    1. 如果新、旧VNode都是静态节点,那么不需要做处理。
    2. 如果新VNode是文本节点,那么如果旧VNode也是文本节点,比较两者的文本内容是否相同,不同就更新文本内容就行,如果旧VNode不是文本节点,那么直接调用setTextNode,设置为文本节点,并且内容与新VNode一致。
    3. 如果新VNode是元素节点,分为2种情况:
      1. 该新节点包含子节点,此时要看旧节点是否包含子节点。如果包含子节点,需要递归对比更新子节点(updateChildren());如果不包含子节点,这个旧节点可能是文本节点或者是空节点,如果是文本节点那么清空文本节点,并将新节点的子节点创建一份插入到旧节点中,如果是空节点,就直接创建一份新节点的子节点插入到旧节点中。
      2. 该新节点不包含子节点,同时又不是文本节点,那么说明它就是一个空节点,只需将旧VNode清空即可。

更新子节点(20200815)

  1. 更新子节点会出现4种情况,oldCh:旧子节点组,newCh:新子节点组,
    1. 更新子节点:如果新旧子节点相同,并且位置也相同,只要更新oldCh里的该旧节点,使之与新节点一致。
    2. 移动子节点:如果newCh里的某个子节点在oldChd中找到了,但是位置不同,那么先更新oldCh中的旧节点,使之与新节点一致,并将oldCh中的旧节点移动到newCh中的新节点一致的位置。
    3. 创建子节点:如果在newCh中的节点在oldCh中没有找到,那么就在oldCh中创建一个与newCh中一致的节点。
    4. 删除子节点:如果在newCh中都已经循环完了,oldCh中还有未处理的子节点,那么直接将这些子节点都删除。
  2. 优化子节点更新,在循环oldCh和newCh的时候,先将4种特殊情况先处理,再进行循环。
    1. 新前(newStartVnode,newCh数组里所有未处理子节点的第一个子节点)和旧前(oldStartVnode,oldCh数组里的所有未处理子节点的第一个子节点)相同,进行更新子节点操作(patchVnode())。
    2. 新后(newEndVnode,newCh数组里所有未处理子节点的最后一个子节点)和旧后(oldEndVnode,oldCh数组里的所有未处理子节点的最后一个子节点)相同,进行更新子节点操作。
    3. 新后和旧前相同,进行移动子节点操作。(先patchVnode(),后移动)
    4. 新前和旧后相同,进行移动子节点操作。(先patchVnode(),后移动)
    5. 将上述4种特殊情况处理之后,如果未在oldCh中找到newCh中的子节点的话,会在剩余的子节点中循环查找。若在oldCh中找不到当前循环的newCh里的子节点,就新增节点(在oldCh中查找newCh中的子VNode的key值在oldCh中是否存在,存在的话拿到在oldCh中的下标);若在oldCh里找到了当前循环的newCh里的子节点,对比新旧子节点,如果相同,更新子节点并移动位置,如果不同,创建子节点(查找是根据VNode的key值进行查找)
  3. 循环的条件是oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx。当跳出循环时,若oldStartIdx > oldEndIdx,说明旧节点循环完了,还有新节点未在oldCh中找到,进行创建子节点操作;若newStartIdx > newEndIdx,说明新节点循环完了,在oldCh中删除未用到的旧子节点。

模板编译篇

  1. 我们将写在template标签中的类似原生HTML的内容称之为模板。
  2. Vue会把模板编译,生成渲染函数render函数。具体流程分为三个阶段:
    1. 模板解析阶段--解析器:parse
    2. 模板优化阶段--优化器:optimize
    3. 代码生成阶段--代码生成器:generate

模板解析阶段

模板解析阶段的主要工作是将中的模板使用正则等方式解析成AST抽象语法树。在源码中对应解析器parser模块。

主要有3种解析器,HTML解析器、文本解析器、过滤器解析器。

主要是通过HTML解析器解析整个模板,在解析过程种遇到文本的时候再使用文本解析器,在文本中如果包含过滤器就使用过滤器解析器。

  1. HTML解析器(parseHTML)

  2. 文本解析器(parseText)做了三件事

    // 当解析到标签的文本时,触发chars
    chars (text) {
      if(res = parseText(text)){
           let element = {
               type: 2,
               expression: res.expression,
               tokens: res.tokens,
               text
           }
        } else {
           let element = {
               type: 3,
               text
           }
        }
    }
    
    let text = "我叫{{name}},我今年{{age}}岁了";
    let res = parseText(text);
    // 通过文本解析器之后
    res = {
        expression:"我叫"+_s(name)+",我今年"+_s(age)+"岁了",
        tokens:[
            "我叫",
            {'@binding': name },
            ",我今年"
            {'@binding': age },
        	"岁了"
        ]
    }
    
    1. 判断传入的文本是否包含变量
    2. 构造expression
    3. 构造tokens
  3. 过滤器解析器(parseFilters)在使用文本解析器解析文本时,遇到{{}}就使用过滤器解析器。

模板优化阶段

patch过程中,会跳过静态节点,即VNode中有一个isStatic属性,它表示这个节点是否是静态节点,VNode是通过render函数得到的,模板搬移最终的目的是生成一个render函数。这个isStatic属性就是在模板优化节点打标的。模板优化主要做了这2件事:

  1. 在通过解析器生成的AST中,找出所有静态节点并打上标记。
  2. 在AST中,找到所有静态根节点并打上标记

代码生成阶段

  1. vue实例在挂载的时候会调用自身的render函数来生成实例上的template选项所对应的VNode。

  2. render函数可能是开发者手写的,也可能是vue自己生成的。

  3. 代码生成其实就是根据template解析出的AST抽象语法树通过generate函数生成render函数字符串,再通过调用createFunction生成render函数,调用render函数就可以得到模板所对应的虚拟DOM。

  4. generate函数内部会根据AST抽象语法树中不同的元素节点属性,调用不同的方法,生成render函数字符串。虽然元素节点属性的种类很多,但是实际真正创建出来的VNode就三种:

    1. 元素节点

      const data = el.plain ? undefined : genData(el, state)
      
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
      data ? `,${data}` : '' // data
      }${
      children ? `,${children}` : '' // children
      })`
      

      生成元素节点render函数就是生成一个_c()函数调用的字符串,包含3个参数,分别是标签名tag、节点属性data、节点的子节点列表children。

    2. 文本节点

      export function genText (text: ASTText | ASTExpression): string {
        return `_v(${text.type === 2
          ? text.expression // no need for () because already wrapped in _s()
          : transformSpecialNewlines(JSON.stringify(text.text))
        })`
      }
      

      文本型的VNode可以通过_v(text)函数来创建。text参数可以可能是静态文本ASTText,也可能是动态文本ASTExpression。

    3. 注释节点

      export function genComment (comment: ASTText): string {
        return `_e(${JSON.stringify(comment.text)})`
      }
      

      注释型的VNode通过_e(text)函数来创建。

生命周期篇

Vue实例的生命周期大致分为4个阶段:

  1. 初始化阶段:
  2. 模板编译阶段:
  3. 挂载阶段:
  4. 销毁节点:

初始化阶段--new Vue

初始化阶段做的第一件事就是new Vue创建一个Vue的实例.

// src/core/instance/index.js
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')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

new Vue就做了一件事调用原型上的_init(options)方法把用户所写的options选项传入.

这个_init(options)方法是从initMixin(Vue)内来的.

// src/core/instance/init.js
export function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    // 将vue实例赋值给变量vm
    const vm = this
    // 将用户传递的options选项和构造函数的options属性及其父级构造函数的option属性整合
    vm.$options = mergeOptions(
        // 其实是vm.constructor.options,相当于Vue.options
        // 在initGlobalAPI方法内部定义了Vue.options
        // 给Vue.options设置了components、directives、filters这三个属性的值为空对象.
        // 并把3个内部组件设置到components属性上(keep-alive、transition、transition-group)
        resolveConstructorOptions(vm.constructor), 
        options || {},
        vm
    )
    vm._self = vm
    initLifecycle(vm) // 初始化生命周期
    initEvents(vm) // 初始化事件
    initRender(vm) // 初始化渲染
    callHook(vm, 'beforeCreate') // 执行beforeCreate钩子函数
    initInjections(vm) // resolve injections before data/props 初始化injections
    initState(vm) // 初始化状态 props data methods computed watch
    initProvide(vm) // resolve provide after data/props // 初始化Provide
    callHook(vm, 'created') // 执行created钩子函数
      
	/**
	*用户传入了el,那么就会调用$mount进入模板编译与挂载阶段,如果没有,则需要手动执行vm.$mount进入下一个生命	 *	周期阶段
	*/
    if (vm.$options.el) { 
      vm.$mount(vm.$options.el)
    }
  }
}

在initMixin内部,给Vue原型绑定了_init方法.

实例方法篇

实例方法是全部挂载到Vue.prototype上的.

数据相关的方法

数据相关的三个实例方法,vm.setvm.set、vm.delete、vm.$watch,它们是在stateMixin函数中挂载到Vue原型上的

export function stateMixin (Vue: Class<Component>) {
  // 此处省略不相关的代码
  Vue.prototype.$set = set
  Vue.prototype.$delete = del

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}
  1. vm.$set

    export function set (target: Array<any> | Object, key: any, val: any): any {
      if (process.env.NODE_ENV !== 'production' && // 如果target是null、undefined或原始类型
        (isUndef(target) || isPrimitive(target))
      ) {
        warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
      }
        // 目标对象是数组,并且key是有效的下标值
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key)
        target.splice(key, 1, val)
        return val
      }
        // 如果这个key在这个对象中且不在Object原型上时
      if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
      }
      const ob = (target: any).__ob__ // target有__ob__属性,则说明target是个Observer实例
      // 如果目标是Vue实例或者是Vue实例的根数据对象(只有root节点vm.Count才大于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
      }
      if (!ob) { // 不是响应式对象,不需要将新数据转化成响应式
        target[key] = val
        return val
      }
      // 如果target是响应式的,那么将新的属性添加,并设置成响应式的
      defineReactive(ob.value, key, val)
      // 给依赖发通知
      ob.dep.notify()
      return val
    }
    
    1. set函数传入三个参数,依次分别是target、key、val.
    2. 判断目标数据是否是null,undefined或者原始数据类型,如果是,并且不是在生产环境就发成警告
    3. 判断目标数据是否是数组,并且key符合数组下标要求,如果返回true的话,就利用数组的splice方法去更新数组的数据(数组splice方法自动发送依赖通知),最后返回val
    4. 判断key是否已经在目标数据中,并且key不在Object.prototype中,如果是,直接更新目标数据的key值就可以了(不需要设置响应式、发依赖通知操作),最后返回val
    5. 判断是否是Vue的实例或者Vue根数据对象(ob属性的vmCount初始值为0,在Observer类中的observe方法中,只有根数据对象,vmCount++),如果是的话直接返回val值.
    6. 根据目标数据的__ob__属性来判断是否是响应式对象,如果不是,直接给目标数据key设置val值,并且返回val
    7. 如果是响应式对象,那么将新的属性添加,并设置响应式,给依赖发通知,最后返回val.
  2. vm.$delete

    export function del (target, key) {
      if (process.env.NODE_ENV !== 'production' && // 目标对象是undefined null 或者原始数据类型
        (isUndef(target) || isPrimitive(target))
      ) {
        warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
      }
      if (Array.isArray(target) && isValidArrayIndex(key)) { // 如果是数组.执行splice触发更新
        target.splice(key, 1)
        return
      }
      const ob = (target: any).__ob__
      if (target._isVue || (ob && ob.vmCount)) { // 如果是vue实例或者vue根数据对象,不执行删除操作
        process.env.NODE_ENV !== 'production' && warn(
          'Avoid deleting properties on a Vue instance or its root $data ' +
          '- just set it to null.'
        )
        return
      }
      if (!hasOwn(target, key)) { // 对象没有这个属性值
        return
      }
      delete target[key] // 删除这个属性值
      if (!ob) { // 如果不是响应式的数据那就直接返回,否则需要给依赖发通知
        return
      }
      ob.dep.notify()
    }
    
    1. del函数的原理与set函数原理类型,依次传入2个参数,target和key.
    2. 判断目标数据target是否是undefined、null或者原始数据类型,如果是的话,发出warn警告.
    3. 判断目标数据target是否是数组,并且key是否符合数组下标要求,如果是的话,直接使用数组的splice方法删除.
    4. 判断目标数据target是否是Vue实例或者Vue根数据对象,是的话发出警告warn并直接return.
    5. 判断目标对象target是否有这个属性key,如果没有的话直接return
    6. 删除目标对象target中的key属性
    7. 如果目标对象target不是响应式数据就直接return,如果是响应式数据,就发送依赖通知.
  3. vm.$watch

    Vue.prototype.$watch = function (expOrFn,cb,options) {
        const vm: Component = this
        if (isPlainObject(cb)) { // cb是否是个对象,如果是对象,说明第二和第三个参数合起来放在这个对象中
          return createWatcher(vm, expOrFn, cb, options)
        }
        options = options || {}
        options.user = true // 用来区别vue创建的watch实例
        const watcher = new Watcher(vm, expOrFn, cb, options)
        if (options.immediate) { // 先调用一次
          cb.call(vm, watcher.value)
        }
        return function unwatchFn () { // 取消观察函数
          watcher.teardown() // 遍历自己的依赖管理器,并从中一一删除
        }
      }
    
    1. vm.$watch方法传入三个参数,分别是expOrFn、cb、options

    2. 判断cb是否是个对象,如果是对象,说明第二个参数和第三个参数一起合并在了这个对象中,执行createWatcher函数,并返回这个函数的结果(createWatcher内部把参数重新分配到$watch上)

      vm.$watch(
          'a.b.c',
          function (val, oldVal) { /* ... */ },
          {
            deep: true
          }
      )
      // 写成这样的话,Vue内部会把第二个参数中值拆成到$watch的第二、第三个参数中
      vm.$watch(
          'a.b.c',
          {
              handler: function (val, oldVal) { /* ... */ },
              deep: true
          }
      )
      // createWatcher内部把参数重新分配到$watch上
      function createWatcher (vm,expOrFn,handler,options) {
          if (isPlainObject(handler)) {
              options = handler
              handler = handler.handler
          }
          if (typeof handler === 'string') {
              handler = vm[handler]
          }
          return vm.$watch(expOrFn, handler, options)
      }
      
    3. 设置options.user=true,来和Vue创建的watch实例区分开

    4. 创建一个Watcher实例,如果options.immediate为true,先执行一遍cb

    5. 最后返回取消观察函数,函数内部遍历自己的依赖管理器,并一一删除.

事件相关的方法

与事件相关的实例方法有4个,vm.onvm.on、vm.once、vm.offvm.off、vm.emit.

在eventsMixin函数中,将这4个方法挂载到了Vue的原型上.

export function eventsMixin (Vue) {
    Vue.prototype.$on = function (event, fn) {}
    Vue.prototype.$once = function (event, fn) {}
    Vue.prototype.$off = function (event, fn) {}
    Vue.prototype.$emit = function (event) {}
}
  1. $on--侦听当前实例上的自定义事件

    onon和emit是发布订阅模式.首先定义一个事件中心,通过on订阅事件,将事件存储在事件中心里面,然后通过on订阅事件,将事件存储在事件中心里面,然后通过emit触发事件中心里面存储的订阅事件.

    Vue.prototype.$on = function (event, fn) {
        const vm: Component = this
        if (Array.isArray(event)) {
            for (let i = 0, l = event.length; i < l; i++) {
                this.$on(event[i], fn)
            }
        } else {
            (vm._events[event] || (vm._events[event] = [])).push(fn)
        }
        return vm
    }
    
    1. $on接收2个参数,分别是event和fn.event可以是数组或者字符串,fn是个回调函数.
    2. 首先判断event是否是数组,如果是数组,再重新调用$on,将数组中的事件依次放入到事件中心_events数组中.(这个属性定义在initEvents函数中)
    3. 如果event不是一个数组就将它放入到Vue实例的_events事件中心中,最后返回vm.
  2. $emit--触发当前实例上的事件

    Vue.prototype.$emit = function (event: string): Component {
        const vm: Component = this
        let cbs = vm._events[event]
        if (cbs) {
          cbs = cbs.length > 1 ? toArray(cbs) : cbs
          const args = toArray(arguments, 1)
          for (let i = 0, l = cbs.length; i < l; i++) {
            try {
              cbs[i].apply(vm, args)
            } catch (e) {
              handleError(e, vm, `event handler for "${event}"`)
            }
          }
        }
        return vm
      }
    }
    
    1. $emit的第一个参数是要触发的事件名event,之后的参数都会传给被触发事件的调用函数.
    2. 从事件中心_events中拿到该event事件名所对应的变量函数cbs,
    3. 获取附加参数args
    4. 循环cbs并执行每个变量函数,同时将args传入.
  3. $off--移除自定义事件监听器

    Vue.prototype.$off = function (event, fn) {
        const vm: Component = this
        // all
        if (!arguments.length) {
            vm._events = Object.create(null)
            return vm
        }
        // array of events
        if (Array.isArray(event)) {
            for (let i = 0, l = event.length; i < l; i++) {
                this.$off(event[i], fn)
            }
            return vm
        }
        // specific event
        const cbs = vm._events[event] // 这是一个数组
        if (!cbs) {
            return vm
        }
        if (!fn) {
            vm._events[event] = null
            return vm
        }
        if (fn) {
            // specific handler
            let cb
            let i = cbs.length
            while (i--) {
                cb = cbs[i]
                if (cb === fn || cb.fn === fn) {
                    cbs.splice(i, 1)
                    break
                }
            }
        }
        return vm
    }
    
    1. 如果没有传入任何参数,就移除所有事件
    2. 如果传入的第一个参数是个数组,那么循环这个数组,再依次调用$off
    3. 如果传入的event不在_event中,则直接返回vm.
    4. 如果未传入了第二个参数fn,则将event在_events事件中心中置空
    5. 如果传入了第二个参数fn,并且cbs中的某一项与fn相同或者某一项的fn属性与fn相同,则从cbs中移除这个fn(变量函数列表cbs是个数组),即只移除event事件下的这个fn回调的监听器.
  4. $once--监听一个自定义事件,但是只触发依次.一旦触发之后,监听器就被移除.

    Vue.prototype.$once = function (event, fn) {
        const vm: Component = this
        function on () {
            vm.$off(event, on)
            fn.apply(vm, arguments)
        }
        on.fn = fn
        vm.$on(event, on)
        return vm
    }
    
    1. $once接收2个参数,第一个是事件名称event,第二个是回调函数fn
    2. once内部,定义了on内部函数,on函数的fn属性设置为传入的fn,以便在执行once内部,定义了on内部函数,将on函数的fn属性设置为传入的fn,以便在执行off的时候能移除这个事件
    3. 调用$on将event添加到_events事件中心
    4. on内部函数里先是调用$off移除event监听器,然后执行fn.

生命周期相关的方法

生命周期相关的方法有4个,分别是forceUpdateforceUpdate、destory、nextTicknextTick、mount.

export function lifecycleMixin (Vue) {
    Vue.prototype.$forceUpdate = function () {}
    Vue.prototype.$destroy = function (fn) {}
}

export function renderMixin (Vue) {
    Vue.prototype.$nextTick = function (fn) {}
}

forceUpdateforceUpdate和destory在lifecycleMixin函数中挂载到Vue.prototype上.

$nextTick在renderMixin函数中挂载到Vue.prototype上.

  1. $forceUpdate

    Vue.prototype.$forceUpdate = function () {
        const vm: Component = this
        if (vm._watcher) {
            vm._watcher.update()
        }
    }
    

    当前实例的_watcher属性就是实例的watcher,强制执行更新,就是去手动执行一下实例watcher的update方法即可.

  2. $destory

    完全销毁一个实例,清理它与其他实例的连接,解绑它的全部指令和事件监听器.

  3. $nextTick

    const callbacks = []   // 回调队列
    let pending = false    // 异步锁
    let timerFunc
    
    // 支持Promise就第一选择用Promise
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      const p = Promise.resolve()
      timerFunc = () => {
        p.then(flushCallbacks)
        // 解决适配性,为了让微任务强制执行,通过添加一个空计时器来强制刷新微任务队列
        if (isIOS) setTimeout(noop)
      }
      isUsingMicroTask = true
    } else if (!isIE && typeof MutationObserver !== 'undefined' && (
      isNative(MutationObserver) ||
      // PhantomJS and iOS 7.x
      MutationObserver.toString() === '[object MutationObserverConstructor]'
    )) { // 支持MutationObserver,微任务
      // e.g. PhantomJS, iOS7, Android 4.4
      // (#6466 MutationObserver is unreliable in IE11)
      let counter = 1
      const observer = new MutationObserver(flushCallbacks)
      const textNode = document.createTextNode(String(counter))
      observer.observe(textNode, {
        characterData: true
      })
      timerFunc = () => {
        counter = (counter + 1) % 2
        textNode.data = String(counter)
      }
      isUsingMicroTask = true
    } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
      // 宏任务setImmediate
      timerFunc = () => {
        setImmediate(flushCallbacks)
      }
    } else {
      // Fallback to setTimeout.
      timerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }
    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
        })
      }
    }
    
    

    微任务比宏任务优先执行.

    $nextTick中依次优先使用Promise(微任务)、MutationObserver(微任务)、setImmediate(宏任务)、setTimeout(宏任务).

  4. $mount

全局API篇

与实例方法挂载到Vue的原型上不同,全局API是挂载在Vue上的,一共有12个.

  1. Vue.extend--创建Vue的一个子类

    一边通过传入的属性方法,将子类的添加上独有的属性方法,一边复制了Vue父类的属性方法复制到子类.

  2. Vue.nextTick--原理同$nextTick

  3. Vue.set--原理同$set

  4. Vue.delete--原理同$delete

  5. Vue.directive--注册指令

  6. Vue.filter--过滤器

  7. Vue.component--全局注册组件

  8. Vue.use--使用插件

  9. Vue.mixin--全局的混入

  10. Vue.compile--编译模板字符串

  11. Vue.observable--让一个对象可响应

  12. Vue.version

    该API在日常业务开发中用不到,但在插件开发时,非常有用,可以根据不同的版本做不同的事.

    它是在构建的时候读取了package.json中的version.

内置组件篇

keep-alive

是Vue的一个内置组件,本身不会渲染一个DOM元素,也不会出现在父组件链上,当它封装动态组件时,会缓存不活动的组件实例,而不是销毁他们.有3个属性:

  1. include:字符串或者正则表达式.只有名称匹配的组件会被缓存.
  2. exclude:字符串或者正则表达式.任何名称匹配的组件都不会被缓存.
  3. max:最大缓存组件实例的数量.

keep-alive组件的主要原理在于它的render.它使用的是LRU的缓存策略

  1. render内部拿到默认插槽第一个节点vnode并且拿到它的options
  2. 判断vnode是否在include和exclude范围内
  3. 如果不在缓存范围内,则直接返回vnode;
  4. 如果在缓存范围内,那么拿到vnode的key,并且去缓存中查找这个key相对应的componentInstance
  5. 如果查找到就直接使用,并且更新组件在缓存队列中的位置
  6. 如果查找不到就将组件塞到缓存队列尾部.
  7. 最后设置vnode.data.keepAlive = true,并将vnode返回.

组件一旦被keep-alive标签缓存,那么再次渲染的时候就不会执行created,mounted等钩子函数,vue提供了activated和deactivated2个钩子函数来使组件在被激活和被终止时调用.