本文是我新开的坑的第一篇文章,这个坑就是vue3,接下来我会围绕着vue3进行一系列的动作,包括但不限于:
- 完整的源码解析
- jsx工程最佳实践
- 个性化的jsx方案
- vue3生态库开发(目前有一个正在进行)
- 以及可能的自定义一个vue3的runtime
关于源码解析,网站已经上线,vue3源码解析,最佳实践,网站是逐行代码形式的解析,更多关注于源码,而在掘金上分享的文章则类似于总结,会用更复合一篇文章的结构来写。如果你想持续跟进vue3源码,可以打开前面的网站关注我。
那么,开始!
vue3最大的变化莫过于其对于响应式原理的重构,以及其新发布的composition api,本文聚焦于前者,来深度剖析一下vue3中响应式到底是怎么实现的。
我们以reactiveAPI 为例,
const Comp = {
setup() {
const state = reactive({
a: 'jokcy'
})
return () => {
return <input value={state.a} onChange={(e) => state.a = e.targent.value} />
}
}
}
我们看上面的例子,这个例子很简单,创建了一个组件,他有一个响应式的数据对象,然后render里面的input的value绑定的是state.a以及他的onChange会修改state.a。这是非常简单且直观的一个数据绑定的例子,而这个逻辑能实现的根本原因,是我们在调用state.a = xxx的时候,vue会重新渲染我们return的render函数,来更新节点
而篇文章就是要来看一下,我们通过reactive创建的对象,到底有什么魔力,能够帮我们完成这个任务。
其实本身 API 是很简单的,传入一个对象,返回一个 reactive 对象,创建的方法是createReactiveObject
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target;
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
);
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 前面都是一些对象是否已经proxy等的判断逻辑,这里就不展示了
const observed = new Proxy(
target,
collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
);
def(
target,
isReadonly ? ReactiveFlags.READONLY : ReactiveFlags.REACTIVE,
observed
);
return observed;
}
那么这里最重要的就是new Proxy了,可以说理解 vue3 的响应式原理过程就是理解这个proxy创建的过程,而了解这个过程,主要就是看第二个参数,在这里就是collectionHandlers或者baseHandlers,大部分是后者,前者主要针对,Set、Map 等。
那么我们就来看baseHandlers:
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys,
};
可见 vue 代理了这几个操作,那么我们一个个看这几个操作做了啥:
function get(target: object, key: string | symbol, receiver: object) {
// ...内部key的货足
// 关于数组的一些特殊处理
const targetIsArray = isArray(target);
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
// 获取请求值
const res = Reflect.get(target, key, receiver);
// ...如果是内部值的获取则直接返回res
if (!isReadonly) {
track(target, TrackOpTypes.GET, key);
}
// 返回的一些处理
return res;
}
这里的重点就在于track(target, TrackOpTypes.GET, key);,这个是我们正常获取值的时候都会执行到的代码:
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!shouldTrack || activeEffect === undefined) {
return;
}
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key,
});
}
}
}
好了,重点来了,我们逐行分析。
第一个if,就是根据环境变量的shouldTrack来判断是否要进行跟踪,如果你已经看过我的源码解析中的processComponent的章节,那么你现在应该就是豁然开朗的感觉。因为在执行processComponent里面的setup的时候,我们特地关闭了track,而那时候就是把shouldTrack改为了false。
let depsMap = targetMap.get(target)这行,targetMap是一个 map,用来记录所有的响应对象。之后如果目前没有记录该对象,那么就重新记录。
这个 target 对应的也是一个 map,他会对每个 key 建立一个 set。
最后要记录这次的 effect 了,这里所谓的effect是什么呢?就是当前正在调用这个对象的函数。在我们的例子里面,就是return回去的render函数,这个函数在执行的时候会调用state.a所以会进入proxy对于get的代理,这个时候 proxy 就调用了track,那么这时候的activeEffect就是这个 render 方法。这里的effect就是,当state.a改动的时候,我们需要重新执行该 render 方法来进行渲染。
那么他是什么时候被设置的呢?在mount章节的时候我们提到了,在执行render方法的时候,我们执行这样一句代码instance.update = effect(function componentEffect()...),就是在这里调用的effect方法里面,把activeEffect记录为componentEffect,而这个componentEffect里面则运行了render方法。
另外这个getHandler里面有句代码也挺有意思的:
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res);
}
在获取属性的时候,如果返回的值是对象的时候,才会对其执行reactive,这也就是延迟处理,而且readonly的话是不执行reactive的。
OK,到这里我们已经知道了在执行render函数的时候,因为我们调用了state.a所以这个函数就相当于依赖state.a,这在vue3里面被称为effect。
那么下一篇文章,我们就来讲一讲,这些effect在state.a变动的时候是如何被调用的,敬请期待。