源码分析: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源码分析——从模板解析到运行时事件绑定

我们先通过源码来分析一下整个流程([email protected]的dist/vue.common.js)。

v-on的解析

分析事件绑定,先去找v-on的实现代码:

dist/vue.common.js

通过搜索,我定位了这样一段代码。

processAttrs这个函数是处理模板中的属性的,其中有个分支是处理 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了。

模板编译流程

说起来,通过搜索定位到某段代码并不能吧流程看全,我们从模板编译的入口开始看。

你可以在[email protected]的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指向只有在运行时才能确定。

猜你喜欢

转载自juejin.im/post/5bcf934b6fb9a05cf715d4cb