源码分析:vue和react组件事件绑定中的this
vue组件定义methods使用箭头函数
直接从问题开始吧。
第一种情况代码:
<template>
<button @click="sayHello">say hellow</button>
</template>
<script>
export default {
methods: {
sayHello() {
console.log('hello:',this);
}
}
}
</script>
运行结果:
第二种情况:
<template>
<button @click="sayHello">say hellow</button>
</template>
<script>
export default {
methods: {
sayHello: () => {
console.log('hello:',this);
}
}
}
</script>
运行结果:
你能解释出为什么会这样么?
vue源码分析——从模板解析到运行时事件绑定
我们先通过源码来分析一下整个流程(vue@2.5.17的dist/vue.common.js)。
v-on的解析
分析事件绑定,先去找v-on的实现代码:
dist/vue.common.js通过搜索,我定位了这样一段代码。
这个函数是处理模板中的属性的,其中有个分支是处理 v-on指令的
v-on:click.native.stop="sayHello"
这里的name就是click,value就是sayHello,而native和stop就是modifiers,el为传进来的当前解析的元素。
addHandler顾名思义就是给当前的xx事件绑定一个handler,我们接着去看addHandler的实现。
dist/vue.common.js删掉了一些无关代码后的addHandler方法如图,开始是处理各种modifier,然后是创建一个newHandler,加到事件的handlers数组中去,因为我们这里只绑定了一个handler,所以走的else的分支。
到这,元素的click已经绑定了handler了。
模板编译流程
说起来,通过搜索定位到某段代码并不能吧流程看全,我们从模板编译的入口开始看。
你可以在vue@2.5.17的dist/vue.common.js文件的最后看到:
在Vue上挂了compile这个属性,而这个属性指向compileToFunctions,从名字可以看出,这个方法是把模板编译成函数的。
通过搜索,发现在这个方法属于ref$1这个对象,而这个对象是通过createCompiler方法创建的。
继续搜索,看到他是调用createCommpilerCreator来生成的,而createCommpilerCreator通过注释可以看到他是有针对ssr的特殊处理,这里我们不用管,看图中标出的3个地方,就是模板编译的3个阶段:parse、optimize、generate。parse是从模板编译成ast抽象语法树,ast抽象语法树优化(optimize)之后,通过generate来生成最终代码,可以看到返回的renderer就是我们生成的。这就是模板编译成render函数的过程。
handler代码生成
其实我们之前分析的processAttrs就是parse的部分,现在我们关注的是generate的部分,因为我们要去看handler生成的代码,
从根元素开始生成,继续去看genElement
可以看到处理了static、once、for、if等指令,处理了template,slot等特殊标签,然后判断了是不是组件,我们这里明显不是,所以走到了genData$2这个函数。
这个函数是处理vnode的各种属性,我们这里只关注events的handler,所以继续去看genHandlers
这里只是对native和非native的events分别做了处理,加上了前缀on或者nativeOn,继续去看genHandler
我们没有modifier所以,是这个分支。
我们知道v-bind的值可以是
sayHello
function() {alert('hello');} 或 () => {alert('hello');}
sayHello($event);
这3种方式吧,通过正则表达式判断出了方法路径(methodPath),函数表达式(functionExpression)这两种方式。
(其实看到正则表达式我就犯晕,感叹想要写模板解析必须正则表达式得很熟啊)
我们开始的sayHello属于方法路径的方式,所以直接返回sayHello。
至此,我们已经完成了模板到render函数的解析,判断出了最终生成的handler就是sayHello,没做任何处理。
vdom的运行时解析
接下来就是render函数渲染的vdom的解析生成真实dom了,我们只需要看事件绑定的部分,所以搜索addEventListener,然后你会发现这段代码。
这貌似是我们要找的代码,往上查找调用add$1的地方,
看到updateDOMListeners这个函数名,就可以确定找对了,这里调用了updateListeners函数,
这里的on就是handlers,而cur就是具体的handler,也就是说我们sayHello就是在这里绑定到了元素上。
vue组件初始化
但是我们还没有看到对this的处理啊,这是因为我们之分析了模板和render部分,没有分析组件对option中methods的处理。
这里的initMixin就是初始化的过程,会处理options
点进去以后,你会发现
这说明vue对state的定义就是包含data、props、computed、methods和watch的,这和react的state定义差别挺大。
我们看initMethods部分,这部分是我们所关心的。
看到这里已经找到我们想要的东西了:组件在init的时候会把所有methods都给绑定到vm上。
箭头函数的解析
还记得我们该开始的问题是什么吗?
刚开始的问题是为什么this打印的是undefined,这里已经绑定到this了啊。
这时候我们打开babel官网,输入这段代码:
你发现箭头函数的this是绑定到当前上下文,也就是父级函数运行时的this的,而我们的组件定义根本没父级函数。
<script>
export default {
methods : {
sayHello: () => {
console.log('hello:', this);
}
}
}
</script>
他的this指向全局对象,在严格模式下,全局对象就是undfined。
用babel repl验证一下也是这样。
分析过程总结
分析到这里,我们已经定位到问题是因为箭头函数的this绑定到了全局对象,而全局独享在严格模式下为undefined导致。
虽然对于模板编译的流程和组件初始化过程的分析没多大必要,但是通过分析,我们知道了3种handler定义方式(方法路径、函数表达式、函数体)最终生成的函数代码的区别,以及vue组件初始化的时候会自动把methods的this绑定到组件实例。
简化的运行流程如图所示,我们先是分析了模板编译的流程,主要是parse阶段(把模板解析成ast)和generate阶段(根据ast生成vdom),然后分析了vdom运行时绑定dom handler的过程,之后又分析了组件初始化时对methods的处理。分析的流程不代表运行的流程,运行时还是从组件初始化开始的。
react组件的使用箭头函数定义
class Hello extends React.Component {
sayHello = () => {
console.log('hello', this);
}
render() {
return <button onClick={this.sayHello}>say hello</button>;
}
}
ReactDOM.render(
<Hello/>,
document.getElementById('container')
);
你觉得上面的写法有问题么
是没有问题的,那为什么vue中有问题呢,就算vue使用render函数还是有问题,不信你可以试下下面的代码。
<script>
export default {
methods:{
sayHello: () => {
console.log('hello:', this);
}
},
render:function (createElement) {
return createElement('button', {
on: {
click: this.sayHello
}
},'say Hello')
}
}
</script>
打印的this依然是undefined。
为什么同样的逻辑在vue和react里表现不一样呢?
其实,是因为写法的不一样,react的组件定义只是类的声明,创建实例后才会运行,而创建组件实例时,会初始化this,这时候this自然指向组件对象。而vue的组件定义是对象式的写法,在定义的过程中箭头函数就已经绑定到了当前上下文,而这时候组件还没创建,这时候this就是undefined。
所以,react组件的定义时方法可以使用箭头函数,而vue的组件定义时methods不可以使用箭头函数。
java和js中this绑定的区别
java是纯面向对象的语言,通过new + 类的构造器的方式创建出对象以后,对象的方法里this永远指向该对象,也就是对象在创建好的那一刻,this就永远固定了。
js既有面向对象的成分,也支持面向过程的写法,在js里函数作为一种对象类型而存在。这就导致了函数时可以被多个对象引用的,并且也可以作为一种变量而存在。
java从机制上保证了方法是只属于一个类的对象的,没法被别的类或变量共享,this自然永远不变。而js因为把函数当作一种对象类型,自然也就可以被多个对象或变量共享,那么this就只能在运行时动态确定了。
java就像封建社会,方法是一辈子只能嫁给一个类,this永远不变,而js就像现代社会,函数是可以随时改变所属对象的,需要运行时才能确定。
也正因为这样的语言特性,使得this成为了js开发无处不在的一个问题。
总结
通过vue源码的模板编译和组件初始化时methods的处理,以及babel对箭头函数的转译等方面进行分析,确定了vue组件中methods使用箭头函数写法,this为undefind的原因:对象式的定义方式下methods绑定到了全局对象,所以就算使用render函数替代模板也不能解决问题。
而react中使用箭头函数定义方法是没问题的,因为类式的声明写法,之后在创建对象时才会去解析执行,render时this已经指向组件对象了。
之后通过java中方法和js中方法的区别,通过内存结构图说明了为什么this是js中很常见的一个问题。
总之,因为js中函数是一种对象类型,在堆中分配空间,所以函数的指向是可以修改的,this指向只有在运行时才能确定。