文件结构
|- api ---------------------- 对外暴露的API
| data.js ---------------- 数据相关方法
| dom.js ----------------- DOM相关方法
| events.js -------------- 事件相关方法
| lifecycle.js ----------- 生命周期
|- internal ----------------- 内部使用的方法
| events.js -------------- 事件
| init.js ---------------- 初始化
| lifecycle.js ----------- 生命周期
| state.js --------------- 状态
| misc.js ---------------- 未知
|- vue.js ------------------- 入口
instance中分为两部分,对外暴露接口的api
和基于mixin提供内部方法的internal
。由于这个内容比较多,就分成两章来说明。我们就先来看api。
数据操作API - data.js
$get( ) , $set( ) - 设置,获取属性值
Vue.prototype.$get = function (exp, asStatement) {
// @parsers
// 转换属性表达式
// 返回一个这样的对象
// {
// 传进来的表达式
// exp: 'a.b.c',
// 读取这个属性的值的方法
// get: function() ...,
// 设置这个属性的值的方法
// set: function() ...
// }
var res = parseExpression(exp)
if (res) {
if (asStatement) {
var self = this
return function statementHandler () {
...
var result = res.get.call(self, self)
...
return result
}
} else {
...
return res.get.call(this, this)
...
}
}
}
Vue.prototype.$set = function (exp, val) {
var res = parseExpression(exp, true)
if (res && res.set) {
res.set.call(this, this, val)
}
}
$delete( ) - 删除属性值
Vue.prototype.$delete = function (key) {
// @util/lang
// 用于删除vm上的属性。
del(this._data, key)
}
$watch( ) - 监控属性值
Vue.prototype.$watch = function (expOrFn, cb, options) {
var vm = this
var parsed
// 解析表达式
if (typeof expOrFn === 'string') {
// @parsers/directive
// 比如解析返回以下格式
// {
// // 表达式
// expression: 'a + 1',
// // 以及跟表达式的过滤器
// filters: [
// { name: 'uppercase', args: null }
// ]
// }
parsed = parseDirective(expOrFn)
expOrFn = parsed.expression
}
// 解析完成后,给这个表达式开启一个观察者Watcher。
var watcher = new Watcher(..., expOrFn, ...)
...
return function unwatchFn () {
watcher.teardown()
}
}
$eval( ) - 解析表达式指令获取指
Vue.prototype.$eval = function (text, asStatement) {
// 这里的话,个人觉得没有必要对filterRE的判断。
// 因为如果filter没有的话,dir.filters是为空的。
// 统一逻辑能让代码结构更加清晰。
// 可能是性能考虑才在这里加上这个预判断的。
if (filterRE.test(text)) {
var dir = parseDirective(text)
var val = this.$get(dir.expression, asStatement)
return dir.filters
? this._applyFilters(val, null, dir.filters)
: val
} else {
return this.$get(text, asStatement)
}
}
$interpolate( ) - 解析模板
Vue.prototype.$interpolate = function (text) {
// @parsers/text
// 负责解析模板,比如会将'{{msg}} world'解析成下面这个数组
// [{
// // 内容是否是html
// html: false,
// // 开头是否为 * ,表示只绑定一次
// oneTime: false,
// // 是否是标签
// tag: true,
// // 标签里的变量值
// value: "msg"
// },
// {
// value: "world"
// }]
var tokens = parseText(text)
var vm = this
if (tokens) {
if (tokens.length === 1) {
return vm.$eval(tokens[0].value) + ''
} else {
return tokens.map(function (token) {
// 这里直接调的$eval
// 所以支持了嵌套属性
return token.tag
? vm.$eval(token.value)
: token.value
}).join('')
}
} else {
return text
}
}
$log( ) - 打印实例属性信息
Vue.prototype.$log = function (path) {
var data = path
? getPath(this._data, path)
: this._data
if (data) {
// clean是本模块内的,内置方法
// 功能是清除data对象上所有的getter/setter
// 实现的比较巧妙,序列化后反序列化
data = clean(data)
}
// 如果属性为空,则还将computed和props打印出来
if (!path) {
...
}
console.log(data)
}
DOM操作API - dom.js
$nextTick( ) - DOM更新后的回调
Vue.prototype.$nextTick = function (fn) {
// @util
// dom更新后,将调用fn
nextTick(fn, this)
}
$appendTo( ) - 将元素插到另一个元素内部结尾处
Vue.prototype.$appendTo = function (target, cb, withTransition) {
// insert是本模块内部抽象出的一个核心方法
// 抽象程度比较高
// 后续的其他方法也依赖于它,这里着重说明下,每个参数的意思
// vm:Vue实例
// target:目标组件
// cb:回调
// withTransition:是否有过渡动画
// append:
// withTransition为true时的操作
// append的实现较简单(本模块内部实现),不赘述
// appendWithTransition:
// withTransition为false的操作
// 带过渡的dom操作的方法的实现基本都依赖于transition模块
return insert(
this, target, cb, withTransition,
append, appendWithTransition
)
}
$before( ) - 将元素插到另一个元素的前面
Vue.prototype.$before = function (target, cb, withTransition) {
// 基本同上
// beforeWithCb:
// 除了特别简单的dom操作在本模块内部直接实现
// 比如上面的append。
// 其他真正对于dom操作的方法都是依赖于util/dom模块
// 比如before,remove等
return insert(
..., beforeWithCb, beforeWithTransition
)
}
$after( ) - 将元素插到另一个元素的后面
Vue.prototype.$after = function (target, cb, withTransition) {
target = query(target)
if (target.nextSibling) {
// 目标不是最后一个Node的case
// 则插入到下一个Node的前面
this.$before(target.nextSibling,...)
} else {
// 目标是最后一个Node的case
// 则通过父节点插入
this.$appendTo(target.parentNode,...)
}
return this
}
$remove( ) - 移除元素
Vue.prototype.$remove = function (cb, withTransition) {
...
if (this._isFragment) {
// @util/dom
// 为fragment时,则删除系列节点
removeNodeRange(...)
} else {
// 为dom时,直接删除节点
var op = withTransition === false
? removeWithCb
: removeWithTransition
op(...)
}
return this
}
事件操作API - events.js
$on( ) - 监听事件
Vue.prototype.$on = function (event, fn) {
// 将注册的事件回调推入回调队列
(this._events[event] || (this._events[event] = []))
.push(fn)
// 修改注册事件的回调数量
// 不仅自身的回调数量+1
// 递归其祖先vm,其回调数量都+1
// 这个数量的同步,在后续的broadcast中价值就体现出来了
modifyListenerCount(this, event, 1)
return this
}
$on( ) - 监听事件,仅一次
Vue.prototype.$once = function (event, fn) {
var self = this
function on () {
// 删除回调
self.$off(event, on)
// 执行方法
fn.apply(this, arguments)
}
// 在$off中,有一条策略是根据cb的fn属性匹配,来删除回调的
on.fn = fn
this.$on(event, on)
return this
}
$off( ) - 删除事件回调
Vue.prototype.$off = function (event, fn) {
// 删除事件回调有几种策略
var cbs
// case 1:参数为空时,删除所有事件及其回调
if (!arguments.length) {
...
return this
}
// case 2:事件压根都没有注册过,直接返回
cbs = this._events[event]
if (!cbs) {
return this
}
// case 3:参数只传了事件名,则删除该事件的所有回调
if (arguments.length === 1) {
...
return this
}
// case 4:删除指定事件的某个回调
var cb
var i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
...
break
}
}
return this
}
$emit( ) - 触发事件
Vue.prototype.$emit = function (event) {
var isSource = typeof event === 'string'
// ~_~ 对象形式的传值,没有暴露给外面,内部使用。
event = isSource
? event
: event.name
var cbs = this._events[event]
...
if (cbs) {
...
var args = toArray(arguments, 1)
// 将回调队列里的方法依次执行
for (var i = 0, l = cbs.length; i < l; i++) {
var cb = cbs[i]
var res = cb.apply(this, args)
...
}
}
return shouldPropagate
}
$broadcast( ) - 广播事件
Vue.prototype.$broadcast = function (event) {
var isSource = typeof event === 'string'
event = isSource
? event
: event.name
// 性能优化点,_eventsCount价值体现出来了
// 组件以及其子孙组件,都没有注册事件
// 则直接略过
if (!this._eventsCount[event]) return
var children = this.$children
var args = toArray(arguments)
if (isSource) {
// 这里对参数进行包装的目的应该是
// 区分信号来源,如果是对象形式的,都是用于内部传递的
args[0] = { name: event, source: this }
}
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i]
var shouldPropagate = child.$emit.apply(child, args)
// 判断是否允许传播
if (shouldPropagate) {
// 向下递归广播
child.$broadcast.apply(child, args)
}
}
return this
}
$dispatch( ) - 向上冒泡事件
Vue.prototype.$dispatch = function (event) {
...
var parent = this.$parent
var args = toArray(arguments)
args[0] = { name: event, source: this }
// 向上递归触发
while (parent) {
// 判断是否允许传播
shouldPropagate = parent.$emit.apply(parent, args)
parent = shouldPropagate
? parent.$parent
: null
}
return this
}
生命周期API - lifecycle.js
$mount( ) - 安装组件
Vue.prototype.$mount = function (el) {
...
el = query(el)
if (!el) {
el = document.createElement('div')
}
// @internal/lifecycle
// 用来编译节点
// 这里面比较复杂,到后面在详细说明。
this._compile(el)
// 省略安装hooks
...
return this
}
$destroy( ) - 销毁组件
Vue.prototype.$destroy = function (remove, deferCleanup) {
// @internal/lifecycle
// 用来删除节点
this._destroy(remove, deferCleanup)
}
$compile( ) - 编译片段
Vue.prototype.$compile = function (el, host, scope, frag) {
// @internal/lifecycle
// 用来编译片段
return compile(el, this.$options, true)(
this, el, host, scope, frag
)
}