基本概念介绍:
vue实例方法:vm.$set、vm.$del、vm.$nextTick等,挂在Vue.prototype上的方法。
全局API: Vue.directive、Vue.filter、Vue.component等,挂在Vue构造函数上的方法。
本章从源码角度分析,Vue.js是如何实现这些功能的
一、Vue实例方法的实现:
Vue构造函数源码:
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
// 限制Vue构造函数只能用 new Vue(options) 方式调用
// 此时 this 是 Vue构造函数的实例,(this instanceof Vue) 为true
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
// 进行Vue实例的初始化,由于内容过多,这块会另起一章详细描述。
this._init(options)
}
// 定义了_init方法,供Vue构造函数调用
initMixin(Vue)
// 以下四个方法,通过在Vue.prototype上添加方法,实现了Vue的实例方法
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
stateMixin:
stateMixin函数实现了与数据相关的实例方法:vm.$set、vm.$del、vm.$watch
具体代码如下:
export function stateMixin (Vue: Class<Component>) {
// 将set、del方法赋值给 Vue.prototype
Vue.prototype.$set = set
Vue.prototype.$delete = del
// 利用watcher构造函数实现$watch
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()
}
}
}
其中所用到的方法在之前的博客里已经详细写过,这里不再赘述
set、del方法不清楚的同学请看: 解析vue2.x源码之API $set、$del
watcher构造函数不清楚的请看: 解析vue2.x源码之Object与Array的变化侦测
eventsMixin:
eventsMixin函数实现了与事件相关的实例方法:vm.$on、vm.$once、vm.$off、vm.$emit
具体代码如下:
export function eventsMixin (Vue: Class<Component>) {
// 注册事件,event可以是字符串或数组,字符串可直接注册,数组需循环取注册
// 如 vm.$on('click', cb) 或 vm.$on(['click','mouseover'], cb)
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
// 将注册事件存入 vm._events
(vm._events[event] || (vm._events[event] = [])).push(fn)
}
return vm
}
// $once 注册的事件只会响应一次,响应后会取消监听该事件
Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this
// 这里的操作称为函数劫持
// 用回调函数On替代原本的回调函数fn,这样当事件触发时一样会调用fn,并且取消事件监听
function on () {
vm.$off(event, on)
fn.apply(vm, arguments)
}
//on.fn这个属性是为了方便在外部$off取消监听
on.fn = fn
// 实际监听的是封装函数on
vm.$on(event, on)
return vm
}
// 取消事件监听
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// 如果没传参数,默认将所有监听清空
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// 如果event是一个数组,循环调用$off取消监听
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// 如果event是字符串,取出event对应的事件数组。
const cbs = vm._events[event]
// 事件数组为空则函数结束
if (!cbs) {
return vm
}
// 事件数组不为空,fn为空,则默认情况该event对应的事件数组
if (!fn) {
vm._events[event] = null
return vm
}
// 事件数组不为空,fn也不为空,遍历事件数组找出与fn相同的一项,移出数组,实现取消监听。
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
// cb.fn === fn 这个条件是为了移除 $once注册的事件
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
// $emit负责从_events中找出事件对应的回调数组并执行
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
// 取出event对应的回调数组
let cbs = vm._events[event]
if (cbs) {
// toArray去除event,保留剩下的参数
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
// 循环遍历数组并执行
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
}
lifecycleMixin:
lifecycleMixin函数实现了与生命周期相关的实例方法:vm.$forceUpdate、vm.$destroy
具体代码如下:
export function lifecycleMixin (Vue: Class<Component>) {
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
// 调用组件watcher的update方法,强制组件重新渲染
if (vm._watcher) {
vm._watcher.update()
}
}
Vue.prototype.$destroy = function () {
const vm: Component = this
// 如果已经在销毁过程中,则return,以免多次调用
if (vm._isBeingDestroyed) {
return
}
// 调用组件beforeDestroy钩子中方法
callHook(vm, 'beforeDestroy')
// 标识组件正在销毁中
vm._isBeingDestroyed = true
// 讲组件从父组件中移除
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// vm._watcher 是当前组件渲染函数的watcher
// vm._watchers 是用户自定义的watcher
// 详情可见watcher构造函数代码
// 释放组件中的状态所收集的依赖
if (vm._watcher) {
vm._watcher.teardown()
}
// 释放用户所定义的watcher中状态收集的依赖
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// 标识已销毁
vm._isDestroyed = true
// 调用__patch__,发现新vnode为null
// 说明组件已销毁,这时触发vnode树的destory钩子函数
vm.__patch__(vm._vnode, null)
// 调用组件destroyed钩子中方法
callHook(vm, 'destroyed')
// 调用实例方法的$off方法,且不传任何参数,这时会将所有监听都取消,_event清空
vm.$off()
}
}
renderMixin :
renderMixin 函数实现了与渲染相关的实例方法:vm.$nextTick
具体代码如下:
export function renderMixin (Vue: Class<Component>) {
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
}
要了解nextTick首先需要了解事件循环,js中的任务分为同步任务与异步任务,异步任务又分为宏任务与微任务。
每次事件循环中,先优先执行同步任务,同步任务执行完后会到微任务队列中取出所有微任务执行,微任务全部执行完毕后再去宏任务队列取出一个宏任务执行,执行完再按这个顺序重复循环,这个循环就叫事件循环。
nextTick的作用是将调用nextTick的回调函数存入一个数组,在异步任务中遍历执行。
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
// 用来存在nextTick传入的回调函数
const callbacks = []
// 标识是否需要等待
let pending = false
// 执行所有回调函数,并清空回调函数数组
function flushCallbacks () {
// 解锁
pending = false
// 先拷贝,清空原来回调数组,再执行拷贝数组
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
// Promise 与 MutationObserver 属于微任务
// setImmediate 与 setTimeout 属于宏任务
// 优先使用微任务,如果运行环境不支持,则依次降级为宏任务
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) ||
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)
}
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 将回调函数加入callbacks数组
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 如果pending为false则调用timerFunc,执行回调数组里的所有方法
if (!pending) {
// 执行过程中上锁,以免重复调用
pending = true
timerFunc()
}
// 这里使为了支持 this.$nextTick().then(cb)这种调用方式。
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
二、全局API的实现:
Vue.extend:
Vue.cid = 0
let cid = 1
/**
* 传入options,返回一个Vue构造函数
* 如:
* let test = Vue.extend({
* template: '<p>{{word}}</p>',
* data: function() {
* return {
* word: 'Hello, world!'
* }
* }
* })
* new test().$mount('#app')
*/
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
// this 指向vue的构造函数
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
// 缓存中如果有这个构造函数直接返回
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
// Sub为要返回的构造函数,函数内调用 _init进行Vue实例的初始化
const Sub = function VueComponent (options) {
this._init(options)
}
// 继承Vue构造函数
Sub.prototype = Object.create(Super.prototype)
// 修复constructor指针
Sub.prototype.constructor = Sub
Sub.cid = cid++
// 合并父子options
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// 代理,使访问vm.key 时, 返回vm._props.key
if (Sub.options.props) {
initProps(Sub)
}
// 代理,使访问vm.key 时, 返回用户自定义的computed的getter,下一章会详细讲解
if (Sub.options.computed) {
initComputed(Sub)
}
// 将Vue构造函数上的API赋给子构造函数
// 分别有 extend、mixin、use、directive、filter、component
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// 将子构造函数存入组件数组中
if (name) {
Sub.options.components[name] = Sub
}
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// 缓存子构造函数
cachedCtors[SuperId] = Sub
return Sub
}
Vue.set、Vue.del、Vue.nextick:
这三个API都是复用之前定义过的函数,上文Vue的实例方法中已经讲过。
Vue.set = set;
Vue.delete = del;
Vue.nextTick = nextTick;
Vue.directive、Vue.filter、Vue.component:
这三个方法实现方式相似,所以源码中是放在一个数组中循环实现的。
以Vue.directive为例,
// Vue.directive(id, [definition])
// 传入id 与 definition,代表注册指令
Vue.directive('my-directive', {
bind: function() {
...
}
})
// 传入id,不传definition,代表取出指令
let myDirective = Vue.directive('my-directive', function() {
bind: function() {
...
}
})
实现源码:
/**
* ASSET_TYPES = ['component','directive','filter']
*/
ASSET_TYPES.forEach(function (type) {
Vue[type] = function (
id,
definition
) {
// 不传definition,代表取出
if (!definition) {
return this.options[type + 's'][id]
} else {
// 传入id 与definition,代表注册
if (type === 'component') {
// 注册component需要校验component名称
validateComponentName(id);
}
if (type === 'component' && isPlainObject(definition)) {
// 注册component,如果definition是object
// 取出组件名,用Vue.extend,返回构造函数
definition.name = definition.name || id;
definition = Vue.extend(definition);
}
// 注册指令时,如果传入的是方法,则转为一个对象
// 默认在bind与update钩子上调用这个函数
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition };
}
// 注册,存入options
this.options[type + 's'][id] = definition;
return definition
}
};
});
Vue.use:
Vue.use = function (plugin: Function | Object) {
// 取出已注册的插件数组
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
// 如果该插件已存在则return,避免重复注册
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// 去掉第一个参数,即plugin本身
const args = toArray(arguments, 1)
// 将Vue构造函数插到第一个参数,供插件方法使用
args.unshift(this)
// 如果plugin有install方法,则调用install方法
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
// 如果plugin没有install,且自身是一个函数,则将plugin视为install调用
plugin.apply(null, args)
}
// 讲插件加入插件数组
installedPlugins.push(plugin)
return this
}
Vue.mixin:
Vue.mixin用于全局混入,Vue.mixin方法注册后,会影响之后创建的每个Vue实例,因为该方法会更改Vue.options
Vue.mixin({
created: function () {
console.log(this.$options.word)
}
})
new Vue({
word: 'Hello, world!'
})
//Hello, world!
其实现原理就是将用户传入的options与Vue.options合并在一起
Vue.mixin = function (mixin) {
this.options = mergeOptions(this.options, mixin);
return this
};
总结:
本篇介绍了Vue的实例方法与全局API,区别在于:实例方法定义在Vue.proyotype,全局API定义在Vue构造函数上。