你明知道输定了还跟我赌,所以说每个赌徒都有他们的借口
前言
首先,这篇文章名称为什么叫All in
?
因为这是收尾篇,兔兔要梭哈。收尾很匆匆?
倘若真的按长篇系列写下去,后面还应该有<实例方法篇>、<全局API篇>、<过滤器篇>、<指令篇>、<插件篇>、<内置组件篇>
等,毋庸置疑,内容铺天盖地。兔兔正处于快速成长期,怎么可能花费过多精力详详细细去写这些,有太多东西需要去学,且按照大家的脾性,大多都没有耐心去读文章,所以兔兔决定开启暴走模式,将这些内容一锅烩,嗯,乱炖。
分析源码的最终目的就是为了解读作者的思想,这个过程需要花费大量的时间、精力,针对应用场景进行多角度且深入的思考,不过痛苦的过程会让人印象深刻哦。你熬出来,你就成功了。兔兔可能也就粗略浅读,不过是受用的了。对于大多数人,最真实的情况莫过于,学完了,忘完了,很痛苦。兔兔也不例外,赶紧总结一波,因为我们最终记住的也只是一种思想,所以我尽可能用精炼的语言阐述思想。
正文
合并策略optionMergeStrategies
optionMergeStrategies
主要用于 mixin
以及 Vue.extend()
方法时对于子组件和父组件如果有相同的属性(option
)时的合并策略。
defaultStrat
默认的合并策略为:子组件优先级较高。子组件的选项存在就使用子组件自身的,如果子组件的选项不存在才使用父组件的选项。
options.el、options.propsData
采用默认的合并策略;options.hook、options.watch
生命周期的钩子函数选项、自定义的watch选项,会合并为一个数组,父组件的在前,子组件的在后,也就是父组件的函数优先执行;options.components、options.directives、options.filters
合并的策略是返回一个合并后的新对象,新对象的自有属性全部来自子组件对象, 但是通过原型链委托在了父组件对象上。沿原型链向父组件查找属性,所以还是子组件优先级较高;options.props、options.methods、options.computed
与第3条略微不同的一点是,有同名属性时,这里是直接用子组件对象覆盖父组件对象,没有通过原型链委托;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
总的大概分为两部分:
- 能力检测
在内部对异步队列尝试使用原生的 Promise.then
、MutationObserver
和 setImmediate
,如果执行环境不支持,则会采用 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)
}
}
复制代码
- 执行回调函数队列
两个注意点:
① 使用了一个异步锁的概念,即接收第一个回调函数时,先关上锁,执行异步方法。此时,浏览器处于等待执行完同步代码就执行异步代码的情况。
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.set
、Vue.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
进行了几种情况的处理:
- 如果没有提供参数,则移除所有的事件监听器;
- 如果传入的需要移除的事件名是一个数组,就表示需要一次性移除多个事件。通过遍历该数组,然后将数组中的每一个事件都递归调用
$off
方法进行移除; - 如果需要移除的事件名在事件中心中找不到,那表明在事件中心从来没有订阅过该事件,那就谈不上移除该事件,直接返回,退出程序;
- 如果只提供了事件,则移除该事件所有的监听器;
- 如果既传入了事件名,又传入了回调函数,则只移除这个回调的监听器。遍历所有回调函数数组
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()
}
}
复制代码
原理很简单,就是手动的去执行一下实例watcher
的update
方法,执行更新。
vm.$nextTick
同上Vue.nextTick
vm.$mount、vm.$destroy
补充
兔兔最近很浮躁,去看vue-router
、vuex
的源码,耐不住性子,看了个大概,简述下原理:
vue-router
以插件形式安装,VueRouter
类提供了静态install
方法,在install
中使用Vue.mixin
混入了beforeCreate
、destroyed
钩子函数。beforeCreate
中进行路由的初始化,会根据mode
配置项来决定使用hash
、history
、abstract
模式之一。hash
模式监听hashChange
事件,history
模式监听popstate
事件,监听到改变时,用改变后的值,去匹配路由表,切换组件。大致是这么个原理。
vuex
vuex
与vue-router
执行相同的安装流程,嵌套的模块通过递归去解析,store
实例挂载在根组件上,子组件通过this.$store = options.parent.$store
去父组件获取,保证共用一份store
实例,做到全局唯一状态。我们都可以写个简单的状态管理,见官方简单状态管理起步使用,vuex
也就是有着严格的约定,更细致化,考虑的更全面。
最后
保持学习,保持快乐!!!