写
Vue
最常用的就是各种交互事件,各种v-on:xxx
比如DOM
事件click、hover
,还有自定义的那些更加乱七八糟。不同于
React
,Vue
里的事件是否为自定义事件,是按照是否写在组件标签上来区分的。如果是组件,除加额外的:native
等属性,否则一众事件都使用$on/$emit
的模式来调用,哪怕是on:click
这些DOM
事件。如果是普通tag
,哪怕是自定义事件,也会以addEventListener
添加。比如分别在组件和普通
tag
上分别定义了DOM
事件和自定义事件相应的处理方法定义(在根
Vue
实例上)btnClickHandler () {
console.log('btnClickHandler');
},
counterHandler () {
console.log('counterHandler');
}
之后,经过模板解析,上面的内容变为
_h(tagName, { // 'button-counter'或'div'
on: {
'click': btnClickHandler,
'counter': counterHandler
}
})
这里把事件的流程单独拿出来看一下,基本的渲染步骤是一样的。
事件注册
首先,模板解析之后,事件会被放入
data
,所有的节点都会这样处理,没差别data: {
on: {
click: boundFn
counter: boundFn
}
}
开始渲染
vm._render
-createElm
如果是普通tag,会使用无
componentOptions
参数的方式初始化VNode
new VNode(...) // 无componentOptions参数
之后,就会创建普通
DOM
元素document.createElement(tag)
,然后调用addEventListeners
注册事件。如果是组件,会使用有
componentOptions
参数的方式初始化VNode
-new VNode(..., componentOptions)
// componentOptions的结构
componentOptions = {
Ctor: Ctor,
propsData: propsData,
listeners: listeners, // data.on
tag: tag,
children: children
}
接着,实例化组件
new VueComponent(options)
// options结构中包含了组件标签上的事件(listeners)
options = {
...
_parentListeners: vnode.componentOptions.listeners
...
}
之后,不同的地方来了。
Vue
把这部分时间通过vm.$on
的方式,放进了实例的_events
对象中。这个对象干啥用呢?_events
存放的这些functions
,会在调用$emit
的时候,作为回调事件触发。之后按照之前的流程,组件会在初始化所有子内容之后,把第一个子
DOM
元素放入vnode.elm
。然后在这个元素上执行addEventListeners
,这里对事件会有一些过滤。-Vue._init
-initEvents
-updateListeners
-vm.$on
// 组件事件被存入实例的_events中,供$emit触发时,回调使用
-vm._events = {
click: [array of function],
counter: [array of function]
}
// 初始化组件的后续(hooks)工作
-initComponent
-invokeCreateHooks // 这里会做一系列工作,后面说
-updateDOMListeners
-vnode.elm.addEventListeners
简单画了个流程图

vue event init
vue事件注册流程
问题:为什么
Vue
的事件可以不用bind
绑定作用域(像react
一样)?看模板解析后的结果:
(function anonymous(
) {
with(this){return _h('div',{attrs:{"id":"demo"},on:{"click":function($event){return 0}}},[_h('div',{on:{"click":clickHandler}},[_s(foo)+", "+_s(bar)])])}
})
其中
this
就是vm
实例,在调用_h
(即createElement
)时会使用这个作为作用域。问题:如何让组件可以监听
DOM
事件?添加.native关键字,如
v-on:click.native='xxx'
。这样,在处理的时候,事件会被分为
on
(如果有的话)和nativeOn
存放data = {
nativeOn: function,
on: function
}
在
new VNode
的时候,会分别处理:on
里的放vm._events
,nativeOn
通过addEventListener
注册。var listeners = data.on;
data.on = data.nativeOn;
由于后面
vnode.elm
会直接取第一个子元素,所以这个操作实际是把事件透传给渲染出的第一个子元素。但是如果这个子元素不支持这个事件(比如给div
添加了focus
事件),那么就会无效(addEventListener
不支持这个事件,静默失败)。解决方法是另外一个“补丁”-
$listeners
。这里不讲(当前代码版本还未支持)。问题:为什么普通
tag
不能用v-on:counter
绑定自定义事件?普通
tag
也是调用了addEventListener
了的,但是对于DOM元素来说,DOM事件
是固定类别的,所以会静默失败。补充:initEvents的处理过程
initEvents使用了闭包处理,这里直接贴代码吧,因为代码太清楚了。
var on = bind(vm.$on, vm);
var off = bind(vm.$off, vm);
vm._updateListeners = function (listeners, oldListeners) {
updateListeners(listeners, oldListeners || {}, on, off);
};
if (listeners) {
vm._updateListeners(listeners)
}
// 其中bind函数,接收两个参数(fn,ctx),返回一个函数
function boundFn (a){
fn.apply(ctx, arguments) / fn.call(ctx, a) / fn.call (ctx)
}
function updateListeners (on, oldOn, add, remove) {
// ...
fn = cur = on[name]; // 这里简略,实际是遍历name in on
cur = on[name] = {};
cur.fn = fn;
cur.invoker = fnInvoker(cur); // fnInvoker是个统一处理函数,调用cur.fn
add(event, cur.invoker, capture);
// ...
}
这段代码的入口是vm._updateListeners。
-updateListeners //传入了两个函数on/off,都用闭包添加了vm实例作为作用域
-on(event, cur.invoker, capture) // cur.invoker又是一个闭包,cur处在其作用域里
-vm._events[event].push(cur.invoker)
这样,回调函数都使用闭包绑定了对应的环境变量。
补充:invokeCreateHooks做了哪些工作
invokeXXXHook(s)
里定义了一系列的节点操作,这些函数会依次在节点上调用。类似的还有invokeDestroyHook
、invokeInsertHook
。这一步会依次执行预制的
create
处理函数(包含以下7个)function updateAttrs (oldVnode, vnode) { }
function updateClass (oldVnode, vnode) { }
function updateDOMListeners (oldVnode, vnode) { }
function updateDOMProps (oldVnode, vnode) {}
function updateStyle (oldVnode, vnode) {}
function create (_, vnode) {}
function bindDirectives (oldVnode, vnode) {}
事件触发
事件注册之后就要触发。触发主要做的在于更新操作,所以在 Vue-初始化和渲染过程 里收集的依赖,这里也要挨个处理啦!
首先,比如一个点击事件,就会在
DOM
上触发处理函数fnInvoker
-boundFn(event)
-clickHandler.call(ctx, event)
比如这里进行
data
的更改(set),那么就会触发使用defineProperty
定义好的reactiveSetter
。前面说过,一个
data
属性对应一个Observer
,一个Observer
里包含一个dep
实例。dep.subs
中存储了所有观察者(Watcher
),属性变更,就会触发(notify
)这些watcher
。这里就是要走一遍这个流程。clickHandler
-proxySetter
-reactiveSetter
// 触发watcher.update
dep.notify()
-subs[i].update
=watcher.update
-queueWatcher(this) // this == 当前Watcher
// 放入队列
-queue.push(watcher)
if (!waiting) {
waiting = true;
nextTick(flushSchedulerQueue);
}
// 执行watcher
flushSchedulerQueue // Flush both queues and run the watchers
-Watcher.run
-Watcher.get
触发一次渲染,渲染流程就和初始化时一样了。
注意,
flushSchedulerQueue
时,会对watcher
进行一个排序,前面说过,watcher
是按id自增的,所以id为0的是根Vue
实例的,按照层级加深,id递增。所以这个顺序会保证:1、组件是从父节点到子节点更新的;
2、用户的
3、如果在父组件watcher执行过程中,某个子组件销毁了,这个子组件不会影响进度(直接被跳过)。
2、用户的
watcher
永远在render
的watcher
之前执行(用户watcher
以后再看);3、如果在父组件watcher执行过程中,某个子组件销毁了,这个子组件不会影响进度(直接被跳过)。
如果没有排序,那么如果先更新子组件,向上到某一级,这个子组被销毁了,那么就导致状态不一致,必须返回去再更新这个子组件。
补充:
nextTick
的设计nextTick
用于使某个函数在下次渲染的时候,才被调用。nextTick
基本结构nextTick = queueNextTick (cb, ctx) {
// 队列处理
callbacks.push(func);
if (!pending) {
pending = true;
timerFunc(nextTickHandler, 0); // nextTickHandler负责依次执行callbacks队列的函数
}
}
// 其中timerFunc函数的结构(如果浏览器不支持promise,会使用MutationObserver,或者setTimeout)
p = Promise.resolve();
timerFunc = function () {
p.then(nextTickHandler);
}
后话:观感
Vue
的代码初看……很简单嘛。但是细看却会一头雾水,流程也变得模模糊糊的,反思了一下,可能大部分的初始化(比如nextTick
、watcher
)是在初始化时内置好的,时间一长就会忘记。而且和环境本身绑定紧密(大量使用defineproperty
、闭包作用域等概念),需要时不时的想一想“这里闭包里都有啥呢”这样的问题。这么一想,
React
可能一开始就是奔着到处运行(比如React-native
)去的吗?