【手撕源码】vue3响应式原理解析
一、认识Proxy
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
new Proxy(target,handler)表示生成一个Proxy实例,target参数表示所要拦截的目标对象,它可以是任意类型的对象,包括内置的数组,函数等,handler也是一个对象,用来定制拦截行为,当发生某些操作时触发该对象。
二、原理分析
1.reactive
声明副作用变量,如果该变量没有值就不进行追踪。在 Vue2 的时候,有一个“全局变量”,叫做 Dep.target – watcher,vue3中还要有这么一个全局变量,就是activeEffect。
//副作用变量
let activeEffect;
targetMap用来存放依赖
let targetMap = new WeakMap();
判断传入的数据data是否为对象,需要除去data为null的情况。因为typeof null === ‘object’,null的机器码都是0,object机器码为000
function isObject(data) {
return data && typeof data === 'object'
}
声明reactive函数,返回proxy实例。proxy支持get、set、deleteProperty、has、ownKeys等方法。
- get
- 通过Reflect.get(target, key, receiver)获取到属性为key的值
- track收集依赖
- ret为对象则递归为对象创建proxy代理
- set
- Reflect.set(target, key, value, receiver)设置属性为key的值
- trigger 执行更新
receiver代表当前proxy对象或者继承proxy的对象,保证传递正确的this 给 getter、setter。
export function reactive(data) {
//判断是否为对象
if (!isObject(data)) return
// 返回proxy实例
return new Proxy(data, {
get(target, key, receiver) {
const ret = Reflect.get(target, key, receiver);
// 收集依赖
track(target, key)
//如果获取的数据还是对象的话就递归,继续为此对象创建 Proxy 代理
return isObject(ret) ? reactive(ret) : ret
},
//set修改数据,需要返回一个布尔值
set(target, key, value,receiver) {
// 首先获取旧值
const oldValue = Reflect.get(target, key, receiver)
// 判断新值和旧值是否一样来决定是否更新setter
let result = true;
// 当新值不等于旧值的时候执行更新草错
if (oldValue !== value) {
result = Reflect.set(target, key, value, receiver)
// 更新操作
trigger(target, key)
}
//返回布尔类型
return result
},
deleteProperty(target, key) {
//首先需要判断是否有要删除的key
const hasK = hasKey(target, key)
const ret = Reflect.deleteProperty(target, key)
//存在key且有值则更新
if(hasK&&ret){
//更新
trigger(target, key)
}
return ret
},
has(target, key) {
track(target, key)
const ret = Reflect.has(target, key)
},
ownKeys(target, key) {
track(target)
return Reflect.ownKeys(targety)
},
})
}
// 判断对象中key是否存在
const hasKey = (target, key) => Object.prototype.hasOwnProperty.call(target, key)
2.track
track里面会收集各种依赖,把依赖关系做成各种映射的关系,映射关系就叫 targetMap, 内部拿到这个key,就可以通过映射关系找到对应的value,就可以影响这个执行函数,
function track(target, key) {
// 如果当前没有effect就不执行追踪
if (!activeEffect) return
//找target有么有被追踪
let depsMap = targetMap.get(target);
//判断target是否为空,如果target为空则没被追踪,则set设置一个值
if (!depsMap) targetMap.set(target, (depsMap = new Map()));
//判断depsMap中有没有key,没有key就set一个(判断target.key有没有被追踪)
let dep = depsMap.get(key)
// 如果key没有被追踪,那么添加一个
if (!dep) depsMap.set(key, (dep = new Set()))
//触发副作用函数
trackEffect(dep)
}
//副作用函数
function trackEffect(dep) {
//相当于 Dep.target && dep.add(Dep.target)
//如果key没有添加activeEffect,则添加一个
if (!dep.has(activeEffect)) dep.add(activeEffect);
}
3.trigger
修改数据时通过 trigger目标对象找到key,根据映射关系找到cb函数执行更新视图。
function trigger(target, key) {
// 获取依赖数据,对依赖数据循环,
const depsMap = targetMap.get(target)
console.log(depsMap,'depsMap')
if (!depsMap) return
//如果effect存在则执行run方法,run方法就是执行的视图更新回调
depsMap.get(key).forEach(effect =>
effect && effect.run()
);
}
4.ref
ref中声明了一个RefImpl类,初始化时传入参数init。
get
- 追踪变量,收集依赖
- 返回初始化变量的值
set
- 修改旧值,赋新值
- trigger 更新
export function ref(init) {
class RefImpl {
constructor(init) {
// 接收传过来的参数
this.__value = init;
}
//获取数据,直接返回传过来的数据
get value() {
track(this, 'value')
return this.__value
}
//更新数据
set value(newVal) {
this.__value = newVal;
trigger(this, 'value')
}
}
return new RefImpl(init);
}
5.effect
effect第一个参数是函数,如果这个函数中有使用 ref/reactive 对象,当该对象的值改变的时候effect就会执行。
function effect(fn,option={}){
//effect 数据类型为ReactiveEffect,一上来就会执行run方法,之后可以自定义执行run方法,即设置option的内容
let __effect = new ReactiveEffect(fn);
if(!option.lazy){
__effect.run();
}
return __effect
}
6.ReactiveEffect
声明ReactiveEffect类,间接定义effect的数据类型。
class ReactiveEffect{
constructor(fn){
this.fn = fn;
}
// 依赖收集之前触发
run(){
activeEffect = this;
return this.fn()
}
}
7.computed
计算属性
export function computed(fn){
//只考虑函数情况
let __computed;
const e = effect(fn,{lazy:true })
__computed = {
get value(){
return e.run();
}
}
return __computed
}
8.mount
mount的参数instance相当于整个app,el相当于挂在的节点
export function mount(instance,el){
// 执行effect
effect(function(){
instance.$data && update(instance,el)
})
instance.$data = instance.setup();
//更新节点
update(instance,el)
function update(instance,el){
//挂载节点的render函数返回值内容复制给节点的innerHTML,进行更新
el.innerHTML = instance.render();
}
}
- 点赞
- 收藏
- 关注作者
评论(0)