Vue2.0源码阅读计划(七)——All in

你明知道输定了还跟我赌,所以说每个赌徒都有他们的借口

前言

首先,这篇文章名称为什么叫All in

因为这是收尾篇,兔兔要梭哈。收尾很匆匆?

倘若真的按长篇系列写下去,后面还应该有<实例方法篇>、<全局API篇>、<过滤器篇>、<指令篇>、<插件篇>、<内置组件篇>等,毋庸置疑,内容铺天盖地。兔兔正处于快速成长期,怎么可能花费过多精力详详细细去写这些,有太多东西需要去学,且按照大家的脾性,大多都没有耐心去读文章,所以兔兔决定开启暴走模式,将这些内容一锅烩,嗯,乱炖。

分析源码的最终目的就是为了解读作者的思想,这个过程需要花费大量的时间、精力,针对应用场景进行多角度且深入的思考,不过痛苦的过程会让人印象深刻哦。你熬出来,你就成功了。兔兔可能也就粗略浅读,不过是受用的了。对于大多数人,最真实的情况莫过于,学完了,忘完了,很痛苦。兔兔也不例外,赶紧总结一波,因为我们最终记住的也只是一种思想,所以我尽可能用精炼的语言阐述思想。

正文

合并策略optionMergeStrategies

optionMergeStrategies 主要用于 mixin 以及 Vue.extend() 方法时对于子组件和父组件如果有相同的属性(option)时的合并策略。

defaultStrat默认的合并策略为:子组件优先级较高。子组件的选项存在就使用子组件自身的,如果子组件的选项不存在才使用父组件的选项。

  1. options.el、options.propsData采用默认的合并策略;
  2. options.hook、options.watch生命周期的钩子函数选项、自定义的watch选项,会合并为一个数组,父组件的在前,子组件的在后,也就是父组件的函数优先执行;
  3. options.components、options.directives、options.filters合并的策略是返回一个合并后的新对象,新对象的自有属性全部来自子组件对象, 但是通过原型链委托在了父组件对象上。沿原型链向父组件查找属性,所以还是子组件优先级较高;
  4. options.props、options.methods、options.computed与第3条略微不同的一点是,有同名属性时,这里是直接用子组件对象覆盖父组件对象,没有通过原型链委托;
  5. options.data若组件中data是以函数的形式存在,则先执行函数,拿到最终返回的对象。合并依旧保持子组件优先级高的原则,将父组件对象的属性合并到子组件对象,若父子组件同时具有某一属性且为Object类型,则递归合并,最终返回合并后的子组件对象。

自定义合并策略:当我们开发插件时,可能用的到。

官网示例:

Vue.config.optionMergeStrategies._my_option = function (parent, child, vm) {
  return child + 1
}

const Profile = Vue.extend({
  _my_option: 1
})

// Profile.options._my_option = 2
复制代码

vue-router 示例:

const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
复制代码

全局API

Vue.extend

整个过程就是先创建一个类Sub,接着通过原型继承的方式将该类继承基础Vue类,然后给Sub类添加一些属性以及将父类的某些属性合并到Sub类上,最后将Sub类返回。

Vue.extend = function (extendOptions) {
    const Super = this
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    // ......
    return Sub
}
复制代码

Vue.nextTick

鉴于可能存在多次连续触发setter的情况,所以vue内部使用了队列来存储多个回调函数(这里指存储的依赖及手动调用nextTick时的回调),利用异步方式让这些函数能够在下一个tick一次性同步执行完毕,避免dom多次重新渲染,从而做到性能的优化。

nextTick总的大概分为两部分:

  1. 能力检测

在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。宏任务耗费的时间是大于微任务的,所以在浏览器支持的情况下,优先使用微任务。

let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  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)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
复制代码
  1. 执行回调函数队列

两个注意点:

① 使用了一个异步锁的概念,即接收第一个回调函数时,先关上锁,执行异步方法。此时,浏览器处于等待执行完同步代码就执行异步代码的情况。

const callbacks = []
let pending = false

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') { // 未提供回调时返回一个promise
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
复制代码

② 当执行flushCallbacks函数时,有备份回调函数队列的操作,执行的也是备份的回调函数队列。目的是应对nextTick套用nextTick的情况,防止嵌套的内层nextTick中的回调函数进入当前回调队列,它应该出现在下一层回调队列中。

function flushCallbacks () {
  pending = false // 打开异步锁
  const copies = callbacks.slice(0) // 备份回调函数队列
  callbacks.length = 0 // 清空当前存储的回调队列,为存储下一层回调队列做准备
  for (let i = 0; i < copies.length; i++) { // 使用备份去执行
    copies[i]()
  }
}
复制代码

Vue.set

在之前响应式一篇的最后link列了这样一个问题:

很多人难以理解在defineReactive里面已经有一个Dep实例了,为什么在Observer里面最开始还要创建一个Dep实例?

其实在那里已经回答过了。这里再拉出来讲一下,在Observer的构造函数里面会初始化一个Dep实例:

export class Observer {
  value: any;
  dep: Dep;

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    // ......
  }
复制代码

这个dep收集依赖的时机是在getter中的childOb.dep.depend():

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
}
复制代码

这个Observer实例的dep是一个对象有一个,目的就是为Vue.setVue.delete服务的:

// Vue.set 实现的关键代码
if (Array.isArray(target) && isValidArrayIndex(key)) {
  target.length = Math.max(target.length, key)
  target.splice(key, 1, val)
  return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
复制代码

对于对象,在我们使用了Vue.set后会将设置的属性转换为响应式,紧跟着触发依赖派发更新。
对于数组,直接使用splice方法来添加,注意此时的splice是改变了数组原型指向后的,也就是加了拦截器的,拦截器内部还是调用了ob.dep.notify()

def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify() // 在这里派发更新
    return result
  })
复制代码

Vue.delete

// Vue.delete 实现的关键代码
if (Array.isArray(target) && isValidArrayIndex(key)) {
  target.splice(key, 1)
  return
}
delete target[key]
if (!ob) {
  return
}
ob.dep.notify()
复制代码

理解了Vue.set后,Vue.delete就没啥好讲的,略。

Vue.directive

自定义指令的本质其实就是在合适的时机执行定义指令时所设置的钩子函数。也就是在虚拟DOMpatch的过程中,执行各种时机的钩子函数。

Vue.filter

过滤器的工作原理就是将用户写在模板中的过滤器通过模板编译,编译成_f函数的调用字符串,之后在执行渲染函数的时候会执行_f函数,从而使过滤器生效。

所谓_f函数其实就是resolveFilter函数的别名,在resolveFilter函数内部是根据过滤器id从当前实例的$options中的filters属性中获取到对应的过滤器函数,在之后执行渲染函数的时候就会执行获取到的过滤器函数。

Vue.component

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
复制代码

通过源码,我们可以理出如下伪代码:

Vue.component = function (id, definition) {
    this.options.components[id] = this.options._base.extend(definition)
}
复制代码

this.options._base.extend就是Vue.extend,所以组件注册的原理就是创建一个Vue的子类,然后挂载到this.options.components上。当我们用到该组件时,会从上面去获取,然后走new Vue()的一整套流程,最终insert入父节点中。

Vue.use

原理:内部会调用插件提供的install 方法,同时将Vue 作为参数传入。另外,由于插件只会被安装一次,所以该API内部还应该防止 install 方法被同一个插件多次调用。

Vue.use = function (plugin) {
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) { // 避免插件重复安装
        return this
    }

    const args = toArray(arguments, 1) // 拿到第二个选项对象参数
    args.unshift(this) // 添加 Vue 为第一参数 
    if (typeof plugin.install === 'function') {
        plugin.install.apply(plugin, args) // 调用install方法,并传入两个参数[Vue, options]
    } else if (typeof plugin === 'function') {
        plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
}
复制代码

Vue.mixin

请查看上面的合并策略一节

实例方法

vm.$watch

查看三类Watcher-$watch一节

vm.$set

同上Vue.set

vm.$delete

同上Vue.delete

vm.$on

$on$emit这两个方法的内部原理是设计模式中最典型的发布订阅模式,首先定义一个事件中心,通过$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
}
复制代码

关于事件注册需要注意的是:父组件给子组件的注册事件中,把自定义事件传给子组件,在子组件实例化的时候进行初始化;而浏览器原生事件是在父组件中处理。

vm.$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
  }
}
复制代码

就是根据传入的事件名从当前实例的_events属性(即事件中心)中获取到该事件名所对应的回调函数cbs,因为往事件中心添加的时候是push,数组类型,所以这里也要带着入参循环执行。

vm.$off

Vue.prototype.$off = function (event, fn) {
    var vm = this;
    // all
    if (!arguments.length) {
      vm._events = Object.create(null);
      return vm
    }
    // array of events
    if (Array.isArray(event)) {
      for (var i$1 = 0, l = event.length; i$1 < l; i$1++) {
        vm.$off(event[i$1], fn);
      }
      return vm
    }
    // specific event
    var cbs = vm._events[event];
    if (!cbs) {
      return vm
    }
    if (!fn) {
      vm._events[event] = null;
      return vm
    }
    // specific handler
    var cb;
    var i = cbs.length;
    while (i--) {
      cb = cbs[i];
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1);
        break
      }
    }
    return vm
  };
复制代码

$off进行了几种情况的处理:

  1. 如果没有提供参数,则移除所有的事件监听器;
  2. 如果传入的需要移除的事件名是一个数组,就表示需要一次性移除多个事件。通过遍历该数组,然后将数组中的每一个事件都递归调用$off方法进行移除;
  3. 如果需要移除的事件名在事件中心中找不到,那表明在事件中心从来没有订阅过该事件,那就谈不上移除该事件,直接返回,退出程序;
  4. 如果只提供了事件,则移除该事件所有的监听器;
  5. 如果既传入了事件名,又传入了回调函数,则只移除这个回调的监听器。遍历所有回调函数数组cbs,如果cbs中某一项与fn相同,或者某一项的fn属性与fn相同,那么就将其从数组中删除(比较的是内存地址)。

vm.$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
}
复制代码

原理很简单,通过$on注册事件,事件被执行一次后,通过$off进行卸载。

但是细品,还是有东西的。on.fn = fn这一行,为什么要多此一举?

我们通过自定义的on函数进行$on注册事件,此时事件中心的存储会如下所示:

vm._events = {
  'xxx':[on]
}
复制代码

在用户触发该事件之前想手动调用$off方法移除该事件时,执行vm.$off('xxx',fn)会找不到对应的事件监听器,所以通过on.fn = fn的方式在on上添加fn属性,在$off内部移除的时候,有这么一行判断if (cb === fn || cb.fn === fn),就做到了避免错误的发生。

vm.$forceUpdate

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

原理很简单,就是手动的去执行一下实例watcherupdate方法,执行更新。

vm.$nextTick

同上Vue.nextTick

vm.$mount、vm.$destroy

见上篇Vue2.0源码阅读计划(六)——生命周期

补充

兔兔最近很浮躁,去看vue-routervuex的源码,耐不住性子,看了个大概,简述下原理:

vue-router

以插件形式安装,VueRouter 类提供了静态install方法,在install中使用Vue.mixin混入了beforeCreatedestroyed钩子函数。beforeCreate中进行路由的初始化,会根据mode配置项来决定使用hashhistoryabstract模式之一。hash模式监听hashChange事件,history模式监听popstate事件,监听到改变时,用改变后的值,去匹配路由表,切换组件。大致是这么个原理。

vuex

vuexvue-router执行相同的安装流程,嵌套的模块通过递归去解析,store实例挂载在根组件上,子组件通过this.$store = options.parent.$store去父组件获取,保证共用一份store实例,做到全局唯一状态。我们都可以写个简单的状态管理,见官方简单状态管理起步使用vuex也就是有着严格的约定,更细致化,考虑的更全面。

最后

保持学习,保持快乐!!!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享