0%

Javascript 响应式原理——数据绑定与依赖追踪机制

简单介绍一下 Javascript 中响应式的实现原理,从 Vue2/3 的响应式机制到 React Hooks 的响应式实现…

介绍

  今天我们来深入探讨一下 Javascript 中响应式系统的实现原理。响应式编程是一种面向数据流和变更传播的编程范式,这意味着当底层数据发生变化时,相关的计算和视图会自动更新。在前端框架中,响应式系统是核心之一,它让我们可以更直观地编写声明式的代码。

  今天是国庆假期,一边看阅兵一边写代码,感觉挺有意思的。看着国家的强大,也激发我深入探索技术的热情。废话不多说,我们进入今天的主题!

Vue 响应式系统分析

Vue2.x 的响应式实现

  Vue2.x 使用 Object.defineProperty() 来劫持各个属性的 gettersetter,当数据发生变化时通知视图更新。这种方法有一定的局限性,比如无法检测到对象属性的添加或删除,也无法检测到数组索引的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// Vue2.x 核心响应式实现原理 function defineReactive(obj, key, val) {
// 递归处理嵌套对象 observe(val);

// 创建依赖收集器 const dep = new Dep();

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// 依赖收集 if (Dep.target) {
dep.depend();
}
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) {
return;
}
val = newVal;

// 递归观察新值 observe(newVal);

// 通知更新 dep.notify();
}
});
}

// 依赖收集器 class Dep {
constructor() {
this.subs = [];
}

static target = null;

addSub(sub) {
this.subs.push(sub);
}

depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}

notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}

// 观察者 class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm;
this.getter = typeof expOrFn === 'function' ? expOrFn : this.parsePath(expOrFn);
this.cb = cb;
this.value = this.get();
}

get() {
Dep.target = this;
const value = this.getter.call(this.vm, this.vm);
Dep.target = null;
return value;
}

update() {
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}

addDep(dep) {
dep.addSub(this);
}

parsePath(path) {
const segments = path.split('.');
return function(obj) {
let ret = obj;
for (let i = 0; i < segments.length; i++) {
if (!ret) return ret;
ret = ret[segments[i]];
}
return ret;
};
}
}

Vue2 响应式的局限性

  1. 无法检测属性添加或删除

    1
    vm.obj.newProp = '新属性'; // Vue 无法检测到这个新属性 Vue.set(vm.obj, 'newProp', '新属性'); // 需要使用 Vue.set 或 vm.$set
  2. 无法检测数组索引变化

    1
    vm.items[indexOfItem] = newValue; // 无法触发响应式更新 vm.items.splice(indexOfItem, 1, newValue); // 需要用 splice 等方法

Vue3.x 的响应式实现

  Vue3.x 使用了 ES6 的 Proxy 来替代 Object.defineProperty(),从根本上解决了 Vue2.x 的响应式限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// Vue3.x 响应式核心实现 function reactive(target) {
// 如果不是对象,直接返回 if (!isObject(target)) {
return target;
}

// 创建响应式代理 const proxy = new Proxy(target, mutableHandlers);
return proxy;
}

// 响应式处理器 const mutableHandlers = {
get(target, key, receiver) {
// 依赖收集 track(target, 'get', key);

const res = Reflect.get(target, key, receiver);

// 深层响应式处理 if (isObject(res)) {
return reactive(res);
}

return res;
},

set(target, key, value, receiver) {
const oldValue = target[key];

// 设置新值 const result = Reflect.set(target, key, value, receiver);

// 比较值是否有变化 if (hasChanged(value, oldValue)) {
// 触发更新 trigger(target, 'set', key, value, oldValue);
}

return result;
},

deleteProperty(target, key) {
const hadKey = hasOwn(target, key);
const result = Reflect.deleteProperty(target, key);

if (hadKey) {
trigger(target, 'delete', key, undefined);
}

return result;
}
};

// 依赖收集函数 const targetMap = new WeakMap();

function track(target, type, key) {
if (!activeEffect) {
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);
}
}

// 触发更新函数 function trigger(target, type, key, newValue, oldValue) {
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}

const effects = new Set();
const add = (effectsToAdd) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect) {
effects.add(effect);
}
});
}
};

// 添加所有相关的副作用 if (key !== void 0) {
add(depsMap.get(key));
}

// 遍历 effects 并执行 const run = (effect) => {
effect();
};

effects.forEach(run);
}

Vue3 响应式优势

  1. 可以拦截对象的所有属性操作

    1
    2
    const reactiveObj = reactive({});
    reactiveObj.newProp = '新属性'; // 完全支持 delete reactiveObj.newProp; // 完全支持
  2. 可以拦截数组索引和 length 的变化

    1
    2
    const arr = reactive([]);
    arr.push(1); // 响应式 arr[0] = 10; // 响应式 arr.length = 0; // 响应式
  3. 更好的性能

    • 不需要预先遍历对象所有属性
    • 懒观察,只有访问时才建立依赖关系

React Hooks 响应式机制

  React 通过 Hooks 提供了一种不同的响应式体验。虽然不像 Vue 那样自动追踪依赖,但通过 useState、useEffect 等 Hooks 提供了精确的响应式控制。

useState 与响应式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { useState, useEffect } from 'React';

function Counter() {
// 使用 useState 创建响应式状态 const [count, setCount] = useState(0);

// 使用 useEffect 响应状态变化 useEffect(() => {
document.title = `计数器: ${count}`;
}, [count]); // 依赖数组,类似 Vue 的响应式依赖 return (
<div>
<p>计数: {count}</p>
<button onClick={() => setCount(count + 1)}>
增加
</button>
</div>
);
}

自定义响应式 Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 创建一个类似 Vue 计算属性的 Hook
function useComputed(getter, deps) {
const [value, setValue] = useState(() => getter());

useEffect(() => {
setValue(getter());
}, deps);

return value;
}

// 使用示例 function App() {
const [a, setA] = useState(1);
const [b, setB] = useState(2);

// 计算属性:当 a 或 b 变化时,自动重新计算 const sum = useComputed(() => a + b, [a, b]);

return (
<div>
<p>a: {a}, b: {b}</p>
<p>sum: {sum}</p>
<button onClick={() => setA(prev => prev + 1)}>增加 a</button>
<button onClick={() => setB(prev => prev + 1)}>增加 b</button>
</div>
);
}

useReducer + Context 实现状态管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React, { createContext, useContext, useReducer } from 'React';

// 创建状态上下文 const StateContext = createContext();

// 状态 Provider 组件 export function StateProvider({ reducer, initialState, children }) {
return (
<StateContext.Provider value={useReducer(reducer, initialState)}>
{children}
</StateContext.Provider>
);
}

// 自定义 Hook 获取状态 export const useStateValue = () => useContext(StateContext);

// 使用示例 const initialState = {
user: null,
basket: []
};

const reducer = (state, action) => {
switch (action.type) {
case 'SET_USER':
return {
...state,
user: action.user
};
case 'ADD_TO_BASKET':
return {
...state,
basket: [...state.basket, action.item]
};
default:
return state;
}
};

// 在组件中使用 function App() {
const [{ user, basket }, dispatch] = useStateValue();

return (
<div>
<h1>欢迎, {user?.name || '访客'}</h1>
<p>购物车: {basket.length} 件商品</p>
</div>
);
}

Vue 与 React 响应式对比

设计理念差异

特性VueReact
响应式类型响应式数据绑定函数式响应
更新机制自动依赖追踪手动状态管理
模板语法模板 + 指令JSX
学习曲线较平缓中等

性能对比

  1. 初始渲染

    1
    2
    3
    4
    5
    6
    7
    8
    // Vue - 数据驱动,模板编译优化
    <template>
    <div>{{ message }}</div>
    </template>

    // React - 函数式组件 function Component({ message }) {
    return <div>{message}</div>;
    }
  2. 更新性能

    • Vue:细粒度依赖追踪,精准更新
    • React:虚拟 DOM 比较,批量更新

实现简单的响应式系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 简化的响应式系统实现 class SimpleReactive {
constructor(data) {
this.data = data;
this.effectStack = [];
this.depMap = new Map();
this.walk();
}

walk() {
Object.keys(this.data).forEach(key => {
this.convert(key, this.data[key]);
});
}

convert(key, val) {
if (typeof val === 'object' && val !== null) {
new SimpleReactive(val); // 递归处理嵌套对象
}

const dep = new Dep();
this.depMap.set(key, dep);

Object.defineProperty(this.data, key, {
get: () => {
if (this.effectStack.length) {
dep.add(this.effectStack[this.effectStack.length - 1]);
}
return val;
},
set: (newVal) => {
if (newVal !== val) {
if (typeof newVal === 'object' && newVal !== null) {
new SimpleReactive(newVal);
}
val = newVal;
dep.notify();
}
}
});
}

effect(fn) {
const effectFn = () => {
this.effectStack.push(effectFn);
fn();
this.effectStack.pop();
};
effectFn();
}
}

class Dep {
constructor() {
this.subs = new Set();
}

add(sub) {
this.subs.add(sub);
}

notify() {
this.subs.forEach(sub => sub());
}
}

// 使用示例 const reactiveData = new SimpleReactive({
count: 0,
name: 'Vue'
});

// 创建副作用函数 reactiveData.effect(() => {
console.log('count 变化:', reactiveData.data.count);
});

// 修改数据触发副作用 reactiveData.data.count++; // 输出: count 变化: 1

响应式最佳实践

Vue 响应式最佳实践

  1. 避免直接修改数组索引

    1
    2
    3
    4
    // 错误做法 vm.items[0] = newValue;

    // 正确做法 Vue.set(vm.items, 0, newValue);
    // 或使用 vm.items.splice(0, 1, newValue);
  2. 使用计算属性优化性能

    1
    2
    3
    4
    5
    computed: {
    // 基于其他响应式数据计算得出,具有缓存特性 filteredList() {
    return this.list.filter(item => item.active);
    }
    }

React 响应式最佳实践

  1. 正确使用依赖数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    useEffect(() => {
    // 副作用逻辑
    }, [dependency]); // 依赖必须完整

    // 错误示例 - 遗漏依赖 const [count, setCount] = useState(0);
    useEffect(() => {
    document.title = `Count: ${count}`;
    // 缺少 count 依赖
    }, []); // 这会导致闭包陷阱
  2. 使用 useCallback 优化函数

    1
    2
    3
    const handleClick = useCallback(() => {
    console.log(count);
    }, [count]); // 当 count 变化时才重新创建函数 return <button onClick={handleClick}>点击</button>;

总结

  • 响应式系统是现代前端框架的核心,它让我们可以专注于业务逻辑而非手动更新视图。
  • Vue 通过自动依赖追踪提供声明式的数据绑定,而 React 通过函数式编程提供精确的状态管理。
  • Vue3 的 Proxy 方案从根本上解决了 Vue2 的限制,提供了更强大的响应式能力。
  • React 的 Hooks 机制为函数式组件带来了状态管理和生命周期的响应式能力。
  • 无论使用哪种方案,理解响应式的工作原理都能帮助我们更好地利用它们,避免常见的陷阱。

晚上出去和家人一起看烟花,感觉今天学习的这些技术知识就像是为思维点亮的烟花,绚烂而有意义!

参考资料

  • Vue 响应式原理
  • React Hooks
  • ECMAScript Proxy
  • 深入理解 Vue 响应式系统
bulb