为什么做这个系列
谈到 Vue.js 这个框架,其本身最广为人知的特性,同时也是与日常开发关系最密切、求职面试考察最多的,一定是响应式系统。因此深入理解响应式系统的实现原理对每个使用 Vue 的开发者来说都是十分必要的。
学习原理最好的方式是什么?自己从 0 开始手写实现一遍,由此得来的理解的一定是最准确的并且记忆最深刻。
渐进式实现
Vue 官方文档首页就声明了,它是一个渐进式框架。意思是新手不必一上来就使用框架提供的所有特性,而是可以由浅到深逐步集成,因此学习曲线非常平滑。
学习原理也是一样的道理,一上来就理解并实现所有功能对绝大部分人来说都是不现实的。此时也可以借鉴"渐进式"的思路,从最最基础的实现开始,一步步迭代增加新功能,逐步完善,最终得到完整实现。
因此我决定开始《Vue.js 3 渐进式实现》 这一系列,手把手带大家从 0 开始一步步实现 Vue 的主要功能模块,从响应式系统开始。记录我自己学习过程的同时也由衷希望能帮助到更多的人。
适合阅读人群
不需要看过一行的源码,只要你有一定的 Vue 使用基础,都可以跟随本系列深入学习原理。学完之后,相信日常工作和面试中遇到的90%关于 Vue 的问题对你来说都不将是问题。
响应式数据的基本实现
响应式原理是数据与函数的关联:当数据变化时,依赖了数据的函数自动重新运行。
一个响应式系统的工作流程如下:
- 当读取操作发生时,将副作用函数收集到"桶"中;
- 当设置操作发生时,从"桶"中取出副作用函数并执行。
思路
为了实现这一点,我们需要拦截对数据的读和写。由于 JavaScript 中没有任何方式能拦截对原始值的读写,因此响应式数据只能是对象。
Vue 2 中,为了兼容 IE,使用 ES5 的 Object.defineProperty函数实现,这种方式有不能拦截对象属性的增删以及无法拦截通过数组索引访问等缺点。
Vue 3 使用了 ES6 新增的 Proxy 实现响应式系统。Proxy 是一个构造函数,可以生成源对象的代理对象,拦截对源对象的操作,并且可以完全拦截 13 种对象的基本操作。
代码
javascript
// 原始数据
const data = { text: 'hellow world' }
function effect() {
// effect 函数的执行会读取 data.text
console.log(obj.text)
}
// 存储副作用函数的桶
const bucket = new Set()
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 effect 添加到存储副作用的桶中
bucket.add(effect)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal
// 把副作用函数从桶里取出并执行
bucket.forEach(fn => fn())
// 返回 true 代表设置操作成功
return true
}
})
已实现
我们使用 Proxy 代理源对象,拦截对源对象的操作:
- 当读取属性时,将副作用函数 effect 添加到桶里,再返回属性值
- 当设置属性时,先更新原始数据,再将副作用函数从桶里取出并重新执行
缺陷/待实现
目前我们直接通过名字 "effect" 来获取副作用函数。这种硬编码的方式很不灵活,因为副作用函数的名字可以任意取,甚至是匿名函数。下一节我们将解决这个问题。
结语
本系列将一步步渐进式地实现 Vue 主要模块,从响应式系统开始。帮助大家深入理解原理的同时也是记录作者自己的学习过程。
响应式系统的第一节中,我们使用 Proxy 拦截对象属性的读和写,实现了当数据变化时,依赖了数据的函数自动重新运行。下一节中我们将解决硬编码副作用函数的问题。