Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!
2013年7月28日,尤雨溪第一次在 GItHub 上为 Vue.js 提交代码;2015年10月26日,Vue.js 1.0.0版本发布;2016年10月1日,Vue.js 2.0发布。
最早的 Vue.js 只做视图层,没有路由, 没有状态管理,也没有官方的构建工具,只有一个库,放到网页里就可以直接用了。
后来,Vue.js 慢慢开始加入了一些官方的辅助工具,比如路由(Router)、状态管理方案(Vuex)和构建工具(Vue-cli)等。此时,Vue.js 的定位是:The Progressive Framework。翻译成中文,就是渐进式框架。
Vue.js2.0 引入了很多特性,比如虚拟 DOM,支持 JSX 和 TypeScript,支持流式服务端渲染,提供了跨平台的能力等。Vue.js 在国内的用户有阿里巴巴、百度、腾讯、新浪、网易、滴滴出行、360、美团等等。
Vue 已是一名前端工程师必备的技能,现在就让我们开始深入学习 Vue.js 内部的核心技术原理吧!
响应式原理
在前端开发中,"响应式"通常指的是用户界面对数据的变化做出相应的能力。换句话说,当数据发生变化时,界面能够自动更新以反映这些变化。这种机制可以让开发者专注于数据和业务逻辑,而不必手动管理界面的更新。
在Vue.js中,响应式是框架的核心特性之一。在Vue 3中,响应式的原理主要依赖于ES6中的Proxy对象。
具体来说,Vue 3的响应式原理包括以下几个步骤:
初始化阶段:当你创建一个Vue实例或者定义一个响应式对象时,Vue会对数据进行初始化。在初始化阶段,Vue会使用Proxy对象来监听数据的变化。
Getter和Setter:对象被Proxy包裹后,每个属性都会有对应的Getter和Setter函数。当你访问响应式对象的属性时,会触发Getter函数,Vue会将这个属性与当前的组件实例关联起来,这样Vue就知道哪些组件依赖于这个属性。当属性被修改时,会触发Setter函数,Vue会通知所有依赖于该属性的组件进行更新。
依赖追踪:Vue使用依赖追踪来跟踪数据属性与组件之间的关联关系。每个组件都有一个依赖收集器,用于存储与该组件相关的所有数据属性。当属性被访问时,Vue会将当前组件与这个属性建立关联,并将属性的变化依赖于这个组件。
触发更新:当响应式对象的属性被修改时,会触发Setter函数。Setter函数会通知所有依赖于这个属性的组件进行更新,从而使界面能够反映数据的变化。
总的来说,Vue 3的响应式原理利用了ES6中的Proxy对象来实现数据的监听和依赖追踪,从而实现了高效的数据响应式更新。这种机制让Vue能够在数据发生变化时自动更新相关的界面组件,使开发者能够更加专注于业务逻辑的实现。
实现reactive
开发思想,从单元测试出发,先定义自己想要的最终结果,然后逐步实现相关的API
第一步:这里呢,我们定义第一个单元测试
// reactive.spec.ts (这里用的单元测试为 jest)// 这里引入的是我们即将实现的自己的reactive
import { reactive } from "../reactive";
// 定义单元测试的标题为reactive,此处定义为hello world都可以
describe("reactive",()→{it("first case",()→{// 定义一个原生对象const original = {foo:1};// 此处用reactive包裹后返回一个对象const observed = reactive(original);// 期待observed的值不等于originalexpect(observed).not.toBe(original);// 期待observed.foo 为 1expect(observed.foo).toBe(1);});
});
根据上面测试的内容,我们可以实现这样一个reactive
// reactive.tsexport function reactive(raw) {// reactive 实际上返回的就是一个proxy对象return new Proxy(raw, {// 拦截getget(target, key) {const res = Reflect.get(target, key);return res;}
}
此时我们已经实现了一个简易的reactive,只不过还不支持依赖收集和触发依赖的逻辑。通过上文我们知道,vue3中依赖收集和触发依赖是在getter和setter中触发的,所以我们的代码可以写成下面这样:
// reactive.tsexport function reactive(raw) {return new Proxy(raw, {get(target, key) {const res = Reflect.get(target, key);// TODO 依赖收集track(target, key);return res;},set(target,key,value) {const res = Reflect.set(target, key, value);// TODO 触发依赖trigger(target, key)return res;}
}
此时我们只需要实现 track和trigger即可。
下面我们看我们的第二个单元测试:
// effect.spec.tsimport { reactive } from '../reactive'
// 这里的effect也是我们后面将要实现的
import { effect } from '../effect'// effect 就是我们的依赖,也叫做副作用
describe("effect",()→{it("second case",()→{const user = reactive({age: 10,});let nextAge;effect(()→{nextAge=user.age + 1;});expect(nextAge).toBe(11);// updateuser.age++;expect(nextAge).toBe(12);});
});
可以看到上面的单元测试中定义了一个函数effect,effect 是一个函数,用于创建副作用。它是 Vue 3 中响应式 API 的一部分,用于处理响应式数据的变化。effect 函数接受一个回调函数作为参数,并在这个回调函数中定义副作用。当回调函数中依赖的响应式数据发生变化时,副作用将被重新执行。
这里先简单说一下这个依赖收集和触发依赖是个怎么回事,可以假设这么一个场景:
1. 在火车站都有寄存包裹的地方,每个旅游团就是一个对象,旅游团的每个人就是对象的键。
2. 当人员去存储包裹的时候,寄存处会看当前人员属于哪个旅游团,相同的旅游团集中放到一个包裹柜,后续方便查找。
3. 然后在这个包裹柜上面找一个箱子给人员,并且给他一把箱子钥匙(依赖收集)
4. 当相同的人员第二次存储包裹的时候,他会继续在原有的箱子里放新的东西(依赖收集)
5. 以此类推
6. 当人员回来拿包裹时,会把钥匙给寄存处,寄存处会将钥匙对应的箱子里的所有东西拿出来(触发依赖)
下面我们来实现effect:
// effect.tsclass ReactiveEffect {private _fn: any;constructor(fn) {this._fn=fn;}run(){activeEffect = thisthis._fn(); }
}// 所有依赖收集到的地方,可以理解成一个寄存处
const targetMap = new Map();
// 收集依赖
export function track(target, key) {let depsMap = targetMap.get(target);// 先看寄存处里面是否已经由当前对象对应的包裹柜if(!depsMap){depsMap = new Map();targetMap.set(target,depsMap);}let dep = depsMap.get(key)// 再看当前对象对应的键值,是否有对应的箱子if(!dep){dep = new Set();depsMap.set(key, dep)}// 最后将用户传入的fn作为依赖,添加进入箱子中trackEffects(dep)
}export function trackEffects(dep){dep.add(activeEffect);
}// 实现trigger
export function trigger(target, key) {// 先根据旅游团找到对应的包裹柜let depsMap = targetMap.get(target);// 根据人员找到对应的箱子let dep = depsMap.get(key);// 把箱子里所有的内容拿出来执行triggerEffects(dep)
}export function triggerEffects(dep){for(const effect of dep){effect.run();}
}let activeEffect;
export function effect(fn) {// fnconst _effect = new ReactiveEffect(fn)// 立即执行传入的函数_effect.run();
}
此时我们的reactive就实现完成了,这里做个总结:
就是每个键在getter的时候,也就是effect函数传入的时候(这里会触发getter),将整个effect函数作为依赖,放入键值对应的箱子里
当数据更新的时候,也就是触发setter时,将箱子里的内容(fn函数)拿出来执行一遍。此时,相关的响应式数据也就更新了
实现ref
有了上面reactive的基础,ref会相当简单的学会。我们还是通过一个单元测试开始:
// ref.spec.tsdescribe("ref",()→{it("first case",()={const a = ref(1);expect(a.value).toBe(1);});it("second case",()=>{ const a = ref(1);let dummy;let calls = 0;effect(()=>{calls++;dummy = a.value;}};expect(calls).toBe(1);expect(dummy).toBe(1);a.value = 2;expect(calls).toBe(2);expect(dummy).toBe(2);})
})
ref都是通过.value来触发,我们可以使用一个类,然后拦截他的get和set,这里给出最终代码:
// ref.tsclass RefImpl {private _value: any;// 存放依赖的箱子public dep;constructor(value) {this._value = value;this.dep = new Set();}get value(){// 收集依赖trackEffects(this.dep)return this._value;}set value(newValue){this.value = newValue// 触发依赖triggerEffects(this.dep)}
}
export function ref(value) {return new RefImpl(value);
}
Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!
叶阳辉
HFun 前端攻城狮
往期精彩:
Vue 进阶系列丨Object 的变化侦测
Vue 进阶系列丨Array 的变化侦测
Vue 进阶系列丨虚拟DOM和VNode
Vue 进阶系列丨Patch 和模板编译
Vue 进阶系列丨事件相关的实例方法
Vue 进阶系列丨生命周期相关的实例方法
Vue 进阶系列丨生命周期
Vue 进阶系列丨自定义指令
Vue 进阶系列丨最佳实践
Vue 进阶系列丨Mixin 混入
Vue 进阶系列丨npm发布vue组件
Vue 进阶系列丨Vuex+TS 代码提示
Vue 进阶系列丨自定义指令实现按钮权限功能
Vue 进阶系列丨Pinia 的基本使用
Vue 进阶系列丨vue2和vue3定义插件的区别
Vue 进阶系列丨vuex持久化
Vue 进阶系列丨webWorker 多线程
Vue 进阶系列丨大文件切片上传
Vue 进阶系列丨实现简易VueRouter