Vue响应式原理

Codepen - 尤雨溪原版代码

Bilibili - 尤雨溪Vue源码分析

掘金 - 尤雨溪国外教程:亲手带你写个简易版的Vue!

掘金 - [Vue官方教程笔记]- 尤雨溪手写mini-vue

响应性概念

响应性:当状态更新,系统会自动更新关联状态;在Web场景下,指的是不断变化的状态反映到DOM上的变化。

例如实现一个功能,使得变量b的值总是变量a的值的10倍。如果我们拥有一个magic function onAChanged(),即当a的值改变之后,自动调用该函数,则可以实现类似的功能。

// 当a的值改变, 调用该回调函数
onAChanged(() => {
    document.querySelector('.cell.b1').textContent = a * 10;
    // 上面的代码可以抽象为:
    // view = render(state);  // 当状态改变, 渲染对应的DOM元素
});

在React中,实现方式类似:

let update;
const onStateChanged = _update => {
    update = _update;
};

// 必须使用setState更新状态
const setState = newState => {
    state = newState;
    update();
};

在Angular中,使用脏值检测实现,拦截例如点击等时间,检查数据是否被更新。

在Vue中,使用ES5的Object.defineProperty()方法,重写对象所有属性的gettersetter方法。

getter和setter

通过ES5的Object.defineProperty()方法,监听属性值的变更,注意:

  • 下面的做法相当于,redefine对象objkey属性,所以要求configurable不能为false
  • 所以,当再次调用convert(stu)时,会报错
function convert(obj) {
    // 通过forEach监听obj对象的每个属性
    Object.keys(obj).forEach(key => {
        let internalValue = obj[key];  // 通过闭包保存原来的值
        Object.defineProperty(obj, key, {
            configurable: false,  // 该属性不能被redefine
            get() {
                console.log(`getting key "${key}": ${internalValue}`);
                return internalValue;
            },
            set(newValue) {
                console.log(`setting key "${key}" to: ${newValue}`);
                internalValue = newValue;
            }
        });
    });
}

let stu = { name: 'Lee', age: 20 };
convert(stu);
console.log(stu);  // { name: [Getter/Setter], age: [Getter/Setter] }

let age = stu.age;  // getting key "age": 20
stu.age = 50;  // setting key "age" to: 50

// 由于上面已经将新的属性值设置为configurable: false, 所以不能进行redefine
// convert(stu);  // TypeError: Cannot redefine property: name

可以看到,当使用convert()方法转换stu对象之后,每当读取/修改对象的属性时,都会收到提醒。

依赖追踪

我们期望实现一个Dep类,它可以使用depend()方法收集依赖项,当所依赖项发生改变时,使用notify()方法触发依赖项的执行。

const dep = new Dep();

// 自动执行, 收集依赖项
autorun(() => {
  dep.depend();  // 收集依赖项
  console.log('updated'); 
})

dep.notify();  // 通知以上收集的依赖项: 所依赖的变量updated

实现一个真正的Dep类:

let activeEffect;  // 当前受依赖项影响的函数
class Dep {
    subscribers = new Set();  // 所有受依赖项影响的函数
    
    depend() {  // 收集当前受依赖项影响的函数, 加入subscribers
        if (activeEffect) {
            this.subscribers.add(activeEffect);
        }
    }

    notify() {  // 通知所有受依赖项影响的函数: 依赖项已经被改变, 需要执行函数
        this.subscribers.forEach(effect => effect());
    }
}

// 建立起effect函数与其依赖项的订阅关系: 当依赖项被改变, 执行effect()
function watchEffect(effect) {
    activeEffect = effect;
    effect();
}

如果我们要使用Dep类,很明显,依赖项不能是普通的对象,而是需要设置过getter和setter的对象,该对象属性的getter和setter需要完成的额外功能是:

  • get():当值被读取时,使该值对应的dep收集依赖
  • set():当值被修改时,通知受该值依赖的函数执行

例如,我们实现上面提到的功能,变量b的值总是变量a的值的10倍:

/* use Dep */
const dep = new Dep();

let a = 0, b = 0;

const state = {};  // 以后需要使用state操作a, 从而实现对a的数据劫持
Object.defineProperty(state, 'a', {
    get() {
        dep.depend();
        return a;
    },
    set(val) {
        if (a !== val) {
            a = val;
            dep.notify();
        }
    }
});

// effect: () => {b = state.a * 10;}
// 首先在watchEffect函数中, 由于执行了effect(), 所以对state.a进行了读取, dep.depend()添加订阅
// 于是, 每当state.a的值改变, dep.notify()执行受state.a依赖的effect()
watchEffect(() => {
    b = state.a * 10;
});

console.log(state.a);  // 0
console.log(b);  // 0

state.a = 10;
console.log(b);  // 100

迷你观察者

我们将上面的convert()函数和Dep类进行结合,就得到了一个迷你观察者:

function observe(raw) {
    // 1. 遍历对象的所有key
    Object.keys(raw).forEach(key => {
        // 2. 为每个key建立一个dep对象
        const dep = new Dep();

        // 3. 重写对象的key属性
        let realVal = raw[key];
        Object.defineProperty(raw, key, {
            get() {
                dep.depend();  // 4. 读取key属性时, 建立依赖
                return realVal;
            },
            set(newVal) {
                realVal = newVal;
                dep.notify();  // 4. key属性改变时, 通知被依赖的effect
            }
        });
    });
    return raw;
}

我们再实现上面的功能,就会更加简洁,不用手动重写getter和setter了:

let obj = {a: 1};  // 依赖项a
let b = 0;
observe(obj);

watchEffect(() => {
    b = obj.a * 10;
});

console.log(b);  // 10 (这是由于在watchEffect()中以及执行过effect()一次了)
obj.a = 20;
console.log(b);  // 200
posted @   lv6laserlotus  阅读(35)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示