Vue-事件的注册和触发

By
写代码

Vue最常用的就是各种交互事件,各种v-on:xxx比如DOM事件click、hover,还有自定义的那些更加乱七八糟。

不同于ReactVue里的事件是否为自定义事件,是按照是否写在组件标签上来区分的。如果是组件,除加额外的:native等属性,否则一众事件都使用$on/$emit的模式来调用,哪怕是on:click这些DOM事件。如果是普通tag,哪怕是自定义事件,也会以addEventListener添加。

比如分别在组件和普通tag上分别定义了DOM事件和自定义事件

相应的处理方法定义(在根Vue实例上)

之后,经过模板解析,上面的内容变为

一眼看上去,除了tagName,其他地方没什么区别,下面createElement的地方才是区别所在,这个在上一篇(Vue-组件的初始化和渲染过程)讲过。除了渲染的区别,事件的注册也有差别。

这里把事件的流程单独拿出来看一下,基本的渲染步骤是一样的。

事件注册

首先,模板解析之后,事件会被放入data,所有的节点都会这样处理,没差别

开始渲染

如果是普通tag,会使用无componentOptions参数的方式初始化VNode

之后,就会创建普通DOM元素document.createElement(tag),然后调用addEventListeners注册事件。

如果是组件,会使用有componentOptions参数的方式初始化VNode

接着,实例化组件

之后,不同的地方来了。Vue把这部分时间通过vm.$on的方式,放进了实例的_events对象中。这个对象干啥用呢?_events存放的这些functions,会在调用$emit的时候,作为回调事件触发。

之后按照之前的流程,组件会在初始化所有子内容之后,把第一个子DOM元素放入vnode.elm。然后在这个元素上执行addEventListeners,这里对事件会有一些过滤。

简单画了个流程图
vue event init

vue事件注册流程

问题:为什么Vue的事件可以不用bind绑定作用域(像react一样)?

看模板解析后的结果:

其中this就是vm实例,在调用_h(即createElement)时会使用这个作为作用域。

问题:如何让组件可以监听DOM事件?

添加.native关键字,如v-on:click.native='xxx'

这样,在处理的时候,事件会被分为on(如果有的话)和nativeOn存放

new VNode的时候,会分别处理:on里的放vm._eventsnativeOn通过addEventListener注册。

由于后面vnode.elm会直接取第一个子元素,所以这个操作实际是把事件透传给渲染出的第一个子元素。但是如果这个子元素不支持这个事件(比如给div添加了focus事件),那么就会无效(addEventListener不支持这个事件,静默失败)。

解决方法是另外一个“补丁”-$listeners。这里不讲(当前代码版本还未支持)。

问题:为什么普通tag不能用v-on:counter绑定自定义事件?

普通tag也是调用了addEventListener了的,但是对于DOM元素来说,DOM事件是固定类别的,所以会静默失败。

补充:initEvents的处理过程

initEvents使用了闭包处理,这里直接贴代码吧,因为代码太清楚了。

这段代码的入口是vm._updateListeners。

这样,回调函数都使用闭包绑定了对应的环境变量。

补充:invokeCreateHooks做了哪些工作

invokeXXXHook(s)里定义了一系列的节点操作,这些函数会依次在节点上调用。类似的还有invokeDestroyHookinvokeInsertHook

这一步会依次执行预制的create处理函数(包含以下7个)

事件触发

事件注册之后就要触发。触发主要做的在于更新操作,所以在 Vue-初始化和渲染过程 里收集的依赖,这里也要挨个处理啦!

首先,比如一个点击事件,就会在DOM上触发处理函数fnInvoker

比如这里进行data的更改(set),那么就会触发使用defineProperty定义好的reactiveSetter

前面说过,一个data属性对应一个Observer,一个Observer里包含一个dep实例。dep.subs中存储了所有观察者(Watcher),属性变更,就会触发(notify)这些watcher。这里就是要走一遍这个流程。

触发一次渲染,渲染流程就和初始化时一样了。

注意,flushSchedulerQueue时,会对watcher进行一个排序,前面说过,watcher是按id自增的,所以id为0的是根Vue实例的,按照层级加深,id递增。所以这个顺序会保证:

1、组件是从父节点到子节点更新的;
2、用户的watcher永远在renderwatcher之前执行(用户watcher以后再看);
3、如果在父组件watcher执行过程中,某个子组件销毁了,这个子组件不会影响进度(直接被跳过)。

如果没有排序,那么如果先更新子组件,向上到某一级,这个子组被销毁了,那么就导致状态不一致,必须返回去再更新这个子组件。

补充:nextTick的设计

nextTick用于使某个函数在下次渲染的时候,才被调用。

nextTick基本结构

后话:观感

Vue的代码初看……很简单嘛。但是细看却会一头雾水,流程也变得模模糊糊的,反思了一下,可能大部分的初始化(比如nextTickwatcher)是在初始化时内置好的,时间一长就会忘记。而且和环境本身绑定紧密(大量使用defineproperty、闭包作用域等概念),需要时不时的想一想“这里闭包里都有啥呢”这样的问题。

这么一想,React可能一开始就是奔着到处运行(比如React-native)去的吗?

0

Comments: 2

  1. 技术学习了 有用

  2. 有没有人直接跳到评论的。

    09月07日

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

:razz: