首先,响应系统是Vue.js的重要组成部分,在此,从底层逻辑以及语言规范层面出发,思考如何尝试实现一个相对完善的响应系统,在这之前,需要知道什么是响应式数据和副作用函数,为什么需要副作用函数,两个副作用函数之间会产生什么影响,如何避免无限递归,等等很多再构建响应式系统时需要考虑的细节。
1.副作用函数指会产生副作用的函数:如图示
当effect函数执行时,它会设置body的文本内容,但除了effect函数之外的任何函数都可以读取或设置body的文本内容。也就是说,effect函数的执行会直接或间接的影响其他函数的执行,这时我们说effect函数产生了副作用。副作用函数很容易产生,例如一个函数修改了全局变量,如图示
2.了解了什么是副作用函数,再来说说什么是响应式数据。假设在一个副作用函数中读取了某个对象的属性:
如上面的代码所示,副作用函数effect会设置body元素的innerText属性,其值为obj.text。当obj.text的值发生变化,理想状态应该是effect函数会重新执行,如果能实现这个目标,那么对象obj就是响应式数据。但很显然,从上面的代码来看,目前还做不到这一点。因为obj是一个普通对象,当修改它的值,除了值本身发生变化外,不会有其他任何反应。
3.那究竟如何才能让obj实现响应式呢?
不难发现两点线索:
第一: 当副作用函数effect执行时,会触发字段onj.text的读取操作;
第二:当修改obj.text的值时,会触发字段obj.text的设置操作
那么事情就变得简单了,只要能拦截一个对象的读取和设置操作,当读取字段obj.text时,把effect副作用函数存储到一个“桶”里,接着,当设置obj.text时,再把副作用函数effect从“桶”里取出来即可
现在的问题俨然成为了如何才能拦截一个对象属性的读取和设置操作,在ES2015之前,只能通过Object.defineProperty函数实现,这也是Vue.js2所采用的方式。在ES2015+中,可以使用代理对象Proxy来实现,这也是Vue.js3所采用的方式
那么接下来直接上代码,如图示:
首先,创建了一个用于存储副作用函数的桶bucket,它是Set类型。接着定义原始数据data,obj时原始数据的代理对象,然后分别设置了get和set拦截函数,用于拦截读取和设置操作。当读取属性时将副作用函数effect添加到“桶”里,即bucket.add(effect),然后返回属性值。当设置属性值时先更新原始数据,再将副作用函数从“桶”里取出并重新执行,这样就初步实现了响应式数据。
总结:目前的实现还存在很多缺陷,直接通过名字(effect)来获取副作用函数,这种硬编码的方式很不灵活,副作用函数的名字可以任取,甚至可以是一个匿名函数,因此,还需要改进这种响应式数据的方式