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
第一步:这里呢,我们定义第一个单元测试
kotlin
// 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的值不等于original
expect(observed).not.toBe(original);
// 期待observed.foo 为 1
expect(observed.foo).toBe(1);
});
});
根据上面测试的内容,我们可以实现这样一个reactive
javascript
// reactive.ts
export function reactive(raw) {
// reactive 实际上返回的就是一个proxy对象
return new Proxy(raw, {
// 拦截get
get(target, key) {
const res = Reflect.get(target, key);
return res;
}
}
此时我们已经实现了一个简易的reactive,只不过还不支持依赖收集和触发依赖的逻辑。通过上文我们知道,vue3中依赖收集和触发依赖是在getter和setter中触发的,所以我们的代码可以写成下面这样:
javascript
// reactive.ts
export 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即可**。**
下面我们看我们的第二个单元测试:
javascript
// effect.spec.ts
import { 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);
// update
user.age++;
expect(nextAge).toBe(12);
});
});
可以看到上面的单元测试中定义了一个函数effect,effect 是一个函数,用于创建副作用。它是 Vue 3 中响应式 API 的一部分,用于处理响应式数据的变化。effect 函数接受一个回调函数作为参数,并在这个回调函数中定义副作用。当回调函数中依赖的响应式数据发生变化时,副作用将被重新执行**。**
这里先简单说一下这个依赖收集和触发依赖是个怎么回事,可以假设这么一个场景:
-
在火车站都有寄存包裹的地方,每个旅游团就是一个对象,旅游团的每个人就是对象的键。
-
当人员去存储包裹的时候,寄存处会看当前人员属于哪个旅游团,相同的旅游团集中放到一个包裹柜,后续方便查找。
-
然后在这个包裹柜上面找一个箱子给人员,并且给他一把箱子钥匙(依赖收集)
-
当相同的人员第二次存储包裹的时候,他会继续在原有的箱子里放新的东西(依赖收集 )
-
以此类推
-
当人员回来拿包裹时,会把钥匙给寄存处,寄存处会将钥匙对应的箱子里的所有东西拿出来(触发依赖 )
下面我们来实现effect:
javascript
// effect.ts
class ReactiveEffect {
private _fn: any;
constructor(fn) {
this._fn=fn;
}
run(){
activeEffect = this
this._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) {
// fn
const _effect = new ReactiveEffect(fn)
// 立即执行传入的函数
_effect.run();
}
此时我们的reactive就实现完成了,这里做个总结:
-
就是每个键在getter的时候,也就是effect函数传入的时候(这里会触发getter),将整个effect函数作为依赖,放入键值对应的箱子里
-
当数据更新的时候,也就是触发setter时,将箱子里的内容(fn函数)拿出来执行一遍。此时,相关的响应式数据也就更新了
实现ref
有了上面reactive的基础,ref会相当简单的学会。我们还是通过一个单元测试开始:
cs
// ref.spec.ts
describe("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,这里给出最终代码:
cs
// ref.ts
class 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 前端攻城狮
往期精彩: