本文详细介绍响应式原理
内容包括
- 什么是响应式原理
- 响应式原理的实现
- 什么是依赖跟踪
- 依赖跟踪的实现
- 小型响应式系统的实现
响应式
什么是响应式?
响应式:当某一状态更新,系统会自动更新与其关联的状态
Vue是如何跟踪变化的?
场景:实现一个程序,变量b总数a的10倍
方案一:
1 2 3 4 5 6 7 8
| let a = 1; let b = a * 10;
a = 2; console.log(b);
b = a * 10;
|
方案二:方案一显然不是很好,我们希望他们的关系是声明式的,系统可以自动关系
1 2 3 4
| function onAChange(){ b = a *10; }
|
defineProperty
使用ES5新增的对象方法defineProperty进行响应式设计
MDN 关于 Object.defineProperty的说明
预期结果
reactive.js
1 2 3 4 5 6 7 8 9
| const convert = require('./convert.js') let state = { count:0, }
convert(state);
state.count; state.count = 1;
|
代码实现
convert.js
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
| module.exports = function convert(obj) { if (!isObject(obj)) { throw new TypeError() } Object.keys(obj).forEach(key => { let internalValue = obj[key] Object.defineProperty(obj, key, { get() { console.log(`getting key "${key}": ${internalValue}`) return internalValue }, set(newValue) { console.log(`setting key "${key}" to: ${newValue}`) internalValue = newValue } }) })
}
function isObject(obj) { return typeof obj === 'object' && !Array.isArray(obj) && obj !== null && obj !== undefined }
|
运行代码
控制台输出
getting key “count”: 0
setting key “count” to: 1
依赖跟踪
什么是依赖跟踪?
dependency tracking
依赖:找到一种方式去建立关联(订阅者模式)
- depend:正在执行的代码,收集依赖
- notify:依赖发生改变,任何之前被定义为依赖都会被通知重新执行
- aoturun:接收一个更新函数,进入该函数表示进入响应空间,可以注册依赖
- 将依赖项和更新函数建立依赖
- 调用notify后会自动执行这段逻辑
代码实现
Dep类
autorun
- 接收一个update函数,在收集依赖时就是收集这个update函数
预期结果
depenTest.js
1 2 3 4 5 6 7 8 9 10
| const {Dep,autorun} = require('./depen.js')
const dep = new Dep()
autorun(()=>{ dep.depen(); console.log("update"); })
dep.notify()
|
代码实现
Dep使用了发布订阅模式(学过发布订阅者模式的很容易就能看懂Dep类)
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
| exports.Dep = class Dep { constructor() { this.subscribers = new Set() } depen() { if (activeUpdate) { this.subscribers.add(activeUpdate) } } notify() { this.subscribers.forEach(sub => sub()) } }
let activeUpdate
exports.autorun = function autorun(update) { function wrappedUpdate() { activeUpdate = wrappedUpdate update() activeUpdate = null } wrappedUpdate() }
|
运行代码
以下写法好像也可以,但不知道为什么尤雨溪要那样写
1 2 3 4 5
| exports.autorun = function autorun(update) { activeUpdate = update update() activeUpdate = null }
|
图示说明
执行autorun后,从图中的1
开始执行,将wrappedUpdate存入全局变量中
- 因为JavaScript是单线程,所以可以用一个全局变量监测目前正在执行的函数

在执行dep.notify时,会将所有订阅
函数执行一遍,即每次add进去的activeUpdate函数
迷你观察者
实现目标
1 2 3 4 5 6 7 8 9 10 11 12 13
| const state = { count: 0 }
observe(state)
autorun(() => { console.log(state.count) })
state.count++
|
代码
需要用到上面的convert(函数名改为observe)、Dep和autorun的代码
observe.js
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
| const { Dep } = require('./depen.js') module.exports = function observe(obj) { if (!isObject(obj)) { throw new TypeError() } let dep = new Dep() Object.keys(obj).forEach(key => { let internalValue = obj[key] Object.defineProperty(obj, key, { get() { dep.depen() return internalValue }, set(newValue) { if (newValue !== internalValue) { internalValue = newValue dep.notify() } } }) }) }
function isObject(obj) { return typeof obj === 'object' && !Array.isArray(obj) && obj !== null && obj !== undefined }
|
mini-observer.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const observe = require('./observe.js') const { autorun } = require('./depen.js') const state = { count: 0 } observe(state)
autorun(() => { console.log(`count is: ${state.count}`); })
state.count = 1
|
运行测试
控制台输出
count is: 0
count is: 1
源码地址:Github
参考