Vue作为一款现代前端三驾马车之一,快速高效深受大家的喜爱。我们在学习使用前端框架的同时,也多少需要掌握它的底层实现原理,不仅仅是因为它是面试常考题目,也对我们后续的开发之路提供了很好的引导和开阔视野的作用。今天这篇文章将从Vue2版本开始简单讲解Vue的响应式原理,和即将发布正式版的Vue3的实现差别。
在Vue中,我们开发一个页面的时候,需要分别开发html模板和js数据部分。而数据将直接渲染到模板中,而模板中的input输入值也可以直接反应到js数据中,这就是对响应式的简单解释。有了响应式,我们就只需要关心和处理数据,而无需关心如何去渲染他们。
Vue2的响应式原理
由于历史原因,Vue中使用的是ES5中对象方法 Object.defineProperty 来进行数据响应,他的作用可以为一个对象的某个字段添加拦截get/set方法,对该字段添加一个设置和读取的拦截操作:
let age = 18const obj = { age }Object.defineProperty(obj, 'age', { get() { return age + '岁' }, set(value) { age = value }})console.log(obj.age) // "18岁"obj.age = 30console.log(obj.age) // "30岁"
使用Object.defineProperty方法监听obj中的age字段变化,当在外部读取obj.age时,将触发其中的get方法;当外部设置obj.age时,将触发set方法,并将设置的目标值传入。
注意:不能在Object.defineProperty中的get/set拦截方法中读取或设置当前监听的字段,这样会造成死循环。
到这里Vue2的响应式原理应该已经清晰了:将data对象中的每个字段都使用Object.defineProperty绑定监听事件,当监听到数据变化时,修改模板对应内容。
...html
// jslet text = ''const obj = { text }Object.defineProperty(obj, "text", { get() { return text; }, set(value) { text = value; document.getElementById('txt').innerHTML = value; }});document.getElementById('input').addEventListener('keyup', function (e) { obj.text = e.target.value})
很容易就实现了简单的响应式渲染,利用这个方法,我们也能大概的想象出computed和watch的简单实现方式。但是这个方式有它的缺陷:
缺点1:不支持监听数组变化(元素、长度)
尤大重写了数组的几个函数,让每个原生函数都重新绑定响应事件,才让我们在Vue2中使用数组函数时,能够实现数组的响应式变化。
缺点2:不能监听字段的新增和删除
Object.defineProperty只能监听对象中已存在的字段,不能监听字段的新增和删除,所以在Vue中新增和删除都要使用$set方法重新绑定响应事。
缺点3:只能浅监听,无法监听更深层级的字段
只能监听最外层字段,如果想监听更深层,则需要递归绑定。
Vue3响应式原理
2020年5月,Vue3进入beta版本,意味着正式版距离我们越来越近,Vue3彻底重写,全面拥抱ES6,因为Object.defineProperty的种种缺陷,Vue3中使用ES6中新增的Proxy类进行数据响应式处理。
与Object.defineProperty相同的是,Proxy 同样拥有get/set方法,可以监听获取和设置;但不同的是,Proxy不仅仅监听对象中的指定字段,而是监听某个对象的变化,这样就可以监听到字段的新增和删除了,可以说的质的变化;而且它还可以直接监听一个数组,数据拦截方式更加灵活多样。
还是上面第一段代码,我们使用Proxy重写:
const obj = { age: 18 }new Proxy(obj, { get(target, key) { return key in target ? target[key] + '岁' : `不存在${key}字段` }, set(target, key, value) { target[key] = value }})console.log(obj.age) // "18岁"obj.age = 30console.log(obj.age) // "30岁"console.log(obj.name) // "不存在name字段"
还记得Object.defineProperty的死循环提示吗,在这样中就不需要关心这个问题,因为get/set方法中的target并不会循环触发监听对象的响应方法。
如果硬要为Proxy提一个缺点的话,那只能是ES6的兼容性比ES5差了(早已经不是问题了)。
更多
面试题 1:除了const,如何实现只读效果?
绑定响应拦截后不设置set方法
面试题 2:响应式原理有哪些应用?
- 数据拦截
- 计算属性
- 记录日志
- ...
如果你有更好的题目和思路,欢迎在评论区留言分享!