vue源码解析之事件机制原理


Posted in Javascript onApril 21, 2018

上一章没什么经验。直接写了组件机制。感觉涉及到的东西非常的多,不是很方便讲。今天看了下vue的关于事件的机制。有一些些体会。写出来。大家一起纠正,分享。源码都是基于最新的Vue.js v2.3.0。下面我们来看看vue中的事件机制:
老样子还是先上一段贯穿全局的代码,常见的事件机制demo都会包含在这段代码中:

<div id="app">
 <div id="test1" @click="click1">click1</div>
 <div id="test2" @click.stop="click2">click2</div>
 <my-component v-on:click.native="nativeclick" v-on:componenton="parentOn">
 </my-component>
</div>
</body>
<script src="vue.js"></script>
<script type="text/javascript">
var Child = {
 template: '<div>A custom component!</div>'
} 
Vue.component('my-component', {
 name: 'my-component',
 template: '<div>A custom component!<div @click.stop="toParent">test click</div></div>',
 components: {
 Child:Child
 },
 created(){
 console.log(this);
 },
 methods: {
 toParent(){
  this.$emit('componenton','toParent')
 }
 },
 mounted(){
 console.log(this);
 }
})
 new Vue({
 el: '#app',
 data: function () {
 return {
  heihei:{name:3333},
  a:1
 }
 },
 components: {
 Child:Child
 },
 methods: {
 click1(){
  alert('click1')
 },
 click2(){
  alert('click2')
 },
 nativeclick(){
  alert('nativeclick')
 },
 parentOn(value){
  alert(value)
 }
 }
})
</script>

上面的demo中一共有四个事件。基本涵盖了vue中最经典的事件的四种情况

普通html元素上的事件

好吧。想想我们还是一个个来看。如果懂vue组件相关的机制会更容易懂。那么首先我们看看最简单的第一、二个(两个事件只差了个修饰符):

<div id="test1" @click="click1">click1</div>

这是简单到不能在简单的一个点击事件。

我们来看看建立这么一个简单的点击事件,vue中发生了什么。

1:new Vue()中调用了initState(vue):看代码

function initState (vm) {
 vm._watchers = [];
 var opts = vm.$options;
 if (opts.props) { initProps(vm, opts.props); }
 if (opts.methods) { initMethods(vm, opts.methods); }//初始化事件
 if (opts.data) {
 initData(vm);
 } else {
 observe(vm._data = {}, true /* asRootData */);
 }
 if (opts.computed) { initComputed(vm, opts.computed); }
 if (opts.watch) { initWatch(vm, opts.watch); }
}

//接着看看initMethods
function initMethods (vm, methods) {
 var props = vm.$options.props;
 for (var key in methods) {
 vm[key] = methods[key] == null ? noop : bind(methods[key], vm);//调用了bind方法,我们再看看bind
 {
  if (methods[key] == null) {
  warn(
   "method \"" + key + "\" has an undefined value in the component definition. " +
   "Did you reference the function correctly?",
   vm
  );
  }
  if (props && hasOwn(props, key)) {
  warn(
   ("method \"" + key + "\" has already been defined as a prop."),
   vm
  );
  }
 }
 }
}

//我们接着看看bind

function bind (fn, ctx) {
 function boundFn (a) {
 var l = arguments.length;
 return l
  ? l > 1
  ? fn.apply(ctx, arguments)//通过返回函数修饰了事件的回调函数。绑定了事件回调函数的this。并且让参数自定义。更加的灵活
  : fn.call(ctx, a)
  : fn.call(ctx)
 }
 // record original fn length
 boundFn._length = fn.length;
 return boundFn
}

总的来说。vue初始化的时候,将method中的方法代理到vue[key]的同时修饰了事件的回调函数。绑定了作用域。

2:vue进入compile环节需要将该div变成ast(抽象语法树)。当编译到该div时经过核心函数genHandler:

function genHandler (
 name,
 handler
) {
 if (!handler) {
 return 'function(){}'
 }

 if (Array.isArray(handler)) {
 return ("[" + (handler.map(function (handler) { return genHandler(name, handler); }).join(',')) + "]")
 }

 var isMethodPath = simplePathRE.test(handler.value);
 var isFunctionExpression = fnExpRE.test(handler.value);

 if (!handler.modifiers) {
 return isMethodPath || isFunctionExpression//假如没有修饰符。直接返回回调函数
  ? handler.value
  : ("function($event){" + (handler.value) + "}") // inline statement
 } else {
 var code = '';
 var genModifierCode = '';
 var keys = [];
 for (var key in handler.modifiers) {
  if (modifierCode[key]) {
  genModifierCode += modifierCode[key];//处理修饰符数组,例如.stop就在回调函数里加入event.stopPropagation()再返回。实现修饰的目的
  // left/right
  if (keyCodes[key]) {
   keys.push(key);
  }
  } else {
  keys.push(key);
  }
 }
 if (keys.length) {
  code += genKeyFilter(keys);
 }
 // Make sure modifiers like prevent and stop get executed after key filtering
 if (genModifierCode) {
  code += genModifierCode;
 }
 var handlerCode = isMethodPath
  ? handler.value + '($event)'
  : isFunctionExpression
  ? ("(" + (handler.value) + ")($event)")
  : handler.value;
 return ("function($event){" + code + handlerCode + "}")
 }
}

genHandler函数简单明了,如果事件函数有修饰符。就处理完修饰符,添加修饰符对应的函数语句。再返回。这个过程还会单独对native修饰符做特殊处理。这个等会说。compile完后自然就render。我们看看render函数中这块区域长什么样子:

_c('div',{attrs:{"id":"test1"},on:{"click":click1}},[_v("click1")]),_v(" "),_c('div',{attrs:{"id":"test2"},on:{"click":function($event){$event.stopPropagation();click2($event)}}}

一目了然。最后在虚拟dom-》真实dom的时候。会调用核心函数:

function add$1 (
 event,
 handler,
 once$$1,
 capture,
 passive
) {
 if (once$$1) {
 var oldHandler = handler;
 var _target = target$1; // save current target element in closure
 handler = function (ev) {
  var res = arguments.length === 1
  ? oldHandler(ev)
  : oldHandler.apply(null, arguments);
  if (res !== null) {
  remove$2(event, handler, capture, _target);
  }
 };
 }
 target$1.addEventListener(
 event,
 handler,
 supportsPassive
  ? { capture: capture, passive: passive }//此处绑定点击事件
  : capture
 );
}

组件上的事件

好了下面就是接下来的组件上的点击事件了。可以预感到他走的和普通的html元素应该是不同的道路。事实也是如此:

<my-component v-on:click.native="nativeclick" v-on:componenton="parentOn">
 </my-component>

最简单的一个例子。两个事件的区别就是一个有.native的修饰符。我们来看看官方.native的作用:在原生dom上绑定事件。好吧。很简单。我们跟随源码看看有何不同。这里可以往回看看我少的可怜的上一章组件机制。vue中的组件都是扩展的vue的一个新实例。在compile结束的时候你还是可以发现他也是类似的一个样子。如下图:

_c('my-component',{on:{"componenton":parentOn},nativeOn:{"click":function($event){nativeclick($event)}}

可以看到加了.native修饰符的会被放入nativeOn的数组中。等待后续特殊处理。等不及了。我们直接来看看特殊处理。render函数在执行时。如果遇到组件。看过上一章的可以知道。会执行

function createComponent (
 Ctor,
 data,
 context,
 children,
 tag
) {
 if (isUndef(Ctor)) {
 return
 }

 var baseCtor = context.$options._base;

 // plain options object: turn it into a constructor
 if (isObject(Ctor)) {
 Ctor = baseCtor.extend(Ctor);
 }

 // if at this stage it's not a constructor or an async component factory,
 // reject.
 if (typeof Ctor !== 'function') {
 {
  warn(("Invalid Component definition: " + (String(Ctor))), context);
 }
 return
 }

 // async component
 if (isUndef(Ctor.cid)) {
 Ctor = resolveAsyncComponent(Ctor, baseCtor, context);
 if (Ctor === undefined) {
  // return nothing if this is indeed an async component
  // wait for the callback to trigger parent update.
  return
 }
 }

 // resolve constructor options in case global mixins are applied after
 // component constructor creation
 resolveConstructorOptions(Ctor);

 data = data || {};

 // transform component v-model data into props & events
 if (isDef(data.model)) {
 transformModel(Ctor.options, data);
 }

 // extract props
 var propsData = extractPropsFromVNodeData(data, Ctor, tag);

 // functional component
 if (isTrue(Ctor.options.functional)) {
 return createFunctionalComponent(Ctor, propsData, data, context, children)
 }

 // extract listeners, since these needs to be treated as
 // child component listeners instead of DOM listeners
 var listeners = data.on;//listeners缓存data.on的函数。这里就是componenton事件
 // replace with listeners with .native modifier
 data.on = data.nativeOn;//正常的data.on会被native修饰符的事件所替换

 if (isTrue(Ctor.options.abstract)) {
 // abstract components do not keep anything
 // other than props & listeners
 data = {};
 }

 // merge component management hooks onto the placeholder node
 mergeHooks(data);

 // return a placeholder vnode
 var name = Ctor.options.name || tag;
 var vnode = new VNode(
 ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
 data, undefined, undefined, undefined, context,
 { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }
 );
 return vnode
}

整段代码关于事件核心操作:

var listeners = data.on;//listeners缓存data.on的函数。这里就是componenton事件
// replace with listeners with .native modifier
data.on = data.nativeOn;//正常的data.on会被native修饰符的事件所替换

经过这两句话。.native修饰符的事件会被放在data.on上面。接下来data.on上的事件(这里就是nativeclick)会按普通的html事件往下走。最后执行target.add('',''')挂上原生的事件。而先前的data.on上的被缓存在listeneners的事件就没着么愉快了。接下来他会在组件init的时候。它会进入一下分支:

function initEvents (vm) {
 vm._events = Object.create(null);
 vm._hasHookEvent = false;
 // init parent attached events
 var listeners = vm.$options._parentListeners;
 if (listeners) {
 updateComponentListeners(vm, listeners);
 }
}

function updateComponentListeners (
 vm,
 listeners,
 oldListeners
) {
 target = vm;
 updateListeners(listeners, oldListeners || {}, add, remove$1, vm);
}

function add (event, fn, once$$1) {
 if (once$$1) {
 target.$once(event, fn);
 } else {
 target.$on(event, fn);
 }
}

发现组件上的没有.native的修饰符调用的是$on方法。这个好熟悉。进入到$on,$emit大致想到是一个典型的观察者模式的事件。看看相关$on,$emit代码。我加点注解:

Vue.prototype.$on = function (event, fn) {
 var this$1 = this;

 var vm = this;
 if (Array.isArray(event)) {
  for (var i = 0, l = event.length; i < l; i++) {
  this$1.$on(event[i], fn);
  }
 } else {
  (vm._events[event] || (vm._events[event] = [])).push(fn);//存入事件
  // optimize hook:event cost by using a boolean flag marked at registration
  // instead of a hash lookup
  if (hookRE.test(event)) {
  vm._hasHookEvent = true;
  }
 }
 return vm
 };

Vue.prototype.$emit = function (event) {
 var vm = this;
 console.log(vm);
 {
  var lowerCaseEvent = event.toLowerCase();
  if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
  tip(
   "Event \"" + lowerCaseEvent + "\" is emitted in component " +
   (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
   "Note that HTML attributes are case-insensitive and you cannot use " +
   "v-on to listen to camelCase events when using in-DOM templates. " +
   "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
  );
  }
 }
 var cbs = vm._events[event];
 console.log(cbs);
 if (cbs) {
  cbs = cbs.length > 1 ? toArray(cbs) : cbs;
  var args = toArray(arguments, 1);
  for (var i = 0, l = cbs.length; i < l; i++) {
  cbs[i].apply(vm, args);//当emit的时候调用该事件。注意上面说的vue在初始化的守候。用bind修饰了事件函数。所以组件上挂载的事件都是在父作用域中的
  }
 }
 return vm
 };

看了上面的on,emit用法下面这个demo也就瞬间秒解了(一个经常用的非父子组件通信):

var bus = new Vue()
// 触发组件 A 中的事件
bus.$emit('id-selected', 1)
// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
 // ...
})

是不是豁然开朗。

又到了愉快的总结时间了。segementfault的编辑器真难用。内容多就卡。哎。烦。卡的时间够看好多肥皂剧了。

总的来说。vue对于事件有两个底层的处理逻辑。

1:普通html元素和在组件上挂了.native修饰符的事件。最终EventTarget.addEventListener() 挂载事件

2:组件上的,vue实例上的事件会调用原型上的$on,$emit(包括一些其他api $off,$once等等)

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
DOM下的节点属性和操作小结
May 14 Javascript
JavaScript 类型的包装对象(Typed Wrappers)
Oct 27 Javascript
JS.getTextContent(element,preformatted)使用介绍
Sep 21 Javascript
jQuery遍历Table应用示例
Apr 09 Javascript
Javascript实现的常用算法(如冒泡、快速、鸽巢、奇偶等)
Apr 29 Javascript
IE浏览器中图片onload事件无效的解决方法
Apr 29 Javascript
jQuery实现form表单元素序列化为json对象的方法
Dec 09 Javascript
深入理解jquery中extend的实现
Dec 22 Javascript
angularjs2中父子组件的数据传递的实例代码
Jul 05 Javascript
详解Angular5路由传值方式及其相关问题
Apr 28 Javascript
微信小程序实现滴滴导航tab切换效果
Jul 24 Javascript
在小程序中集成redux/immutable/thunk第三方库的方法
Aug 12 Javascript
JavaScript获取用户所在城市及地理位置
Apr 21 #Javascript
在vue-cli项目中使用bootstrap的方法示例
Apr 21 #Javascript
详解webpack模块化管理和打包工具
Apr 21 #Javascript
Node.Js生成比特币地址代码解析
Apr 21 #Javascript
vue.js与element-ui实现菜单树形结构的解决方法
Apr 21 #Javascript
JS装饰器函数用法总结
Apr 21 #Javascript
vue 之 .sync 修饰符示例详解
Apr 21 #Javascript
You might like
谈谈PHP语法(2)
2006/10/09 PHP
PHP与SQL注入攻击[一]
2007/04/17 PHP
php根据数据id自动生成编号的实现方法
2016/10/16 PHP
PHP attributes()函数讲解
2019/02/03 PHP
PHP实现微信退款的方法示例
2019/03/26 PHP
PHP的静态方法与普通方法用法实例分析
2019/09/26 PHP
分享27款非常棒的jQuery 表单插件
2011/03/28 Javascript
获得所有表单值的JQuery实现代码[IE暂不支持]
2012/05/24 Javascript
Javascript操作cookie的函数代码
2012/10/03 Javascript
微信浏览器内置JavaScript对象WeixinJSBridge使用实例
2015/05/25 Javascript
JavaScript实现网页头部进度条刷新
2017/04/16 Javascript
nodejs前端自动化构建环境的搭建
2017/07/26 NodeJs
Vue自定义事件(详解)
2017/08/19 Javascript
vue使用pdfjs显示PDF可复制的实现方法
2018/12/14 Javascript
node.js中express模块创建服务器和http模块客户端发请求
2019/03/06 Javascript
微信小程序云开发之模拟后台增删改查
2019/05/16 Javascript
JS Math对象与Math方法实例小结
2019/07/05 Javascript
VUE路由动态加载实例代码讲解
2019/08/26 Javascript
JavaScript Array对象基本方法详解
2019/09/03 Javascript
js获取 gif 的帧数的代码实例
2019/09/10 Javascript
Vue自定义render统一项目组弹框功能
2020/06/07 Javascript
在Django框架中伪造捕捉到的URLconf值的方法
2015/07/18 Python
举例讲解Python中的list列表数据结构用法
2016/03/12 Python
python 快速把超大txt文件转存为csv的实例
2018/10/26 Python
Python 实现子类获取父类的类成员方法
2019/01/11 Python
python爬虫项目设置一个中断重连的程序的实现
2019/07/26 Python
解决Pycharm 包已经下载,但是运行代码提示找不到模块的问题
2019/08/31 Python
Python字符串中添加、插入特定字符的方法
2019/09/10 Python
css3media响应式布局实例
2016/07/08 HTML / CSS
英国儿童图书网站:Scholastic
2017/03/26 全球购物
大专计算机个人求职的自我评价
2013/10/21 职场文书
三好学生自我鉴定
2013/12/17 职场文书
房产委托公证书样本
2014/04/04 职场文书
2014组织生活会方案
2014/05/19 职场文书
公司市场专员岗位职责
2014/06/29 职场文书
IT工程师岗位职责
2014/07/04 职场文书