hello, 这里是link, 前半年基本都把时间花在了看 Vue
的源码上, 一直觉得自己对响应式的代码还是比较了解的. 但是前段时间看到有道面试题, 居然要手写一个响应式系统, 一时间无从下手. 查阅了一番资料, 决定把它梳理一下, 根据尤大自己发布的教程, 我们手把手写一个迷你的响应式系统. 你面试的时候按这么写, 如果别人说你写的不专业, 你就说是尤雨溪教的.😋, 看是面试官专业还是尤雨溪专业.
如果你特别了解响应式, 并且希望看尤大的课, 你可以直接看这个迷你响应式. 但是我会在除了原有代码的基础上, 阐述一些我个人觉得尤大没有细讲的地方, 如果你不是很了解响应式, 那我推荐你看完我的文章
何为响应式?
简单的来说就是我们某个数据发生了改变, 可以使得用到这个数据的其他数据也发生改变. 它意图就是: 帮开发者省去手动改变其他数据的这一过程
let a = 1 // 1
let b = a * 10 // 10
改变 a
- 正常情况下
a = 2 // 2
b = a * 10 // 20 😿
- 响应式
a = 2 // 2
b = 20 // 20 b 会根据 a 的值变成新的值, 而不需要我们自己动手去做
贴近Vue
上面这个例子好像不够形象, 我们来看看, 比较贴近vue的用法. 如果你是
vue2
的用户可能还是很匪夷所思, 这贴近vue
吗? 从直观感受来讲, 它更贴近vue3
的使用方式. 但是这段代码同样可以解释vue2
的响应式系统. 至于为何vue2/3
如此相像, 那是因为它们的原理及思想是一样的.
const states = {
count: 0
}
autorun(() => {
console.log("count:" + states.count)
})
// log 0
states.count++
// log 1
这段代码做的事情就是, 只要count
改变了, 就会自动执行一次 autorun 函数. 这其实就是vue的响应式做的事情. autorun传进去的函数, 它可能是计算属性
, 监听属性
, 也可以是一个组件
. 因为这些实例, 本质上都是一个函数.你可以理解为:
-
值改变了, 就更新页面
-
或者改变计算属性
-
亦或者触发监听属性
好了, 我们现在将上面代码中隐藏的功能拿出来逐一分析一下, 具体需要做哪些的事情, 才能让上面的代码实现
-
代码首次执行的时候, 就应该将目标对象的值变成响应式的(
数据劫持
) -
我们要实现
autorun
让目标函数能够在恰当的时候被执行(依赖收集
) -
我们改变
count
值的时候, 要能够触发目标函数(派发更新
)
我们分模块来实现上述的功能
数据劫持
通过Object.defineProperty()完成, 这个网上都讲烂了, 咱就不提了. 具体看实现吧
一步一步来, 在这个case中我们只考虑object类型, 并且是一个扁平的对象, 如果你了解一些边缘情况的处理(数组, 多层对象等), 推荐你看看vue的源码vue-core-analyse
function isObject(obj) {
return (
typeof obj === 'object'
&& !Array.isArray(obj)
&& obj !== null
&& obj !== undefined
)
}
function observe(obj) {
if (!isObject(obj)) return
defineReactivity(obj)
}
好了. 参数的校验就做到这里, 我们现在要批量地将对象内的值通过defineReactivity
变成响应式的, 这里定义activeValue
是为了赋值后能够通过get方法获取到
function defineReactivity(obj) {
Object.keys(obj).forEach((key) => {
let activeValue = obj[key]
Object.defineProperty(obj, key, {
get() {
return activeValue
},
set(newValue) {
activeValue = newValue
}
})
})
}
发布者
数据劫持就完成了, 现在数据被访问以及被设置成新值的过程, 我们都可以操控了. 现在我们需要考虑的时候, 数据变化等过程, 怎样才能找到对应的函数去执行? 简单, 找个地方存起来就好了. 而一般这个实例我们称之为 发布者. 为什么需要使一个实例? 因为它不仅要存函数, 还要用来通知函数执行
class Dep {
constructor() {
this.subscriber = new Set()
}
// 收集
depend() {
this.subscriber.add(xxxx)
}
// 通知更新
notify() {
this.subscriber.forEach((sub) => sub())
}
}
我们先不考虑具体怎么收集, 而是在什么时候收集, 在什么时候更新?, 简单, 数据被访问的时候收集, 数据被改变的时候通知更新, 我们来改造一下defineReactivity
函数
function defineReactivity(obj) {
Object.keys(obj).forEach((key) => {
let activeValue = obj[key]
+ const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
// 数据被访问的时候收集
+ dep.depend()
return activeValue
},
set(newValue) {
// 数据被改变的时候通知更新
activeValue = newValue
+ dep.notify()
}
})
})
}
订阅者
在
Dep
中,depend
的方法我们还没有完成, 我们现在把注意力集中到autorun
函数, 它的参数, 就是我们需要派发更新的对象. 在vue真正的代码中, 实际上 watcher 是一个类的实例, 但是这里为了方便我们理解, 我们简化代码, 用一个函数代替, 因为实际上, watcher实例也是会默认执行一个getter
函数, 可以认为就是我们传进去的目标函数.
let activeWatcher = null
function autorun(update) {
function watcher() {
activeWatcher = watcher
update()
activeWatcher = null
}
watcher()
}
- 这里保存一份全局的
activeWatcher
是用来给Dep实例
保存用的, 我们修改一下的depend
方法
// 收集
depend() {
+ if (activeWatcher) {
+ this.subscriber.add(activeWatcher)
+ }
}
到这里我相信你能理解, 保存一份全局watcher
方便给dep收集, 但是为什么需要在 watcher
执行的时候, 额外存一份activeWatcher
而不是直接存watcher函数
就好了呢? 好似有点脱裤子放屁🤔.
实际上作为一个迷你的响应式, 这么做确实没什么必要. 但是我们把它当做Vue的真正响应式系统来看的话, 你就明白为什么了. 因为watcher实例肯定不只有一个, 我们数据收集依赖的时候, 要确保收集的是当前被激活的watcher
.
全部代码
// 发布者模块
class Dep {
constructor() {
this.subscriber = new Set()
}
// 收集
depend() {
if (activeWatcher) {
this.subscriber.add(activeWatcher)
}
}
// 通知更新
notify() {
this.subscriber.forEach((sub) => sub())
}
}
// 数据劫持模块
function isObject(obj) {
return (
typeof obj === 'object'
&& !Array.isArray(obj)
&& obj !== null
&& obj !== undefined
)
}
function observe(obj) {
if (!isObject(obj)) return
defineReactivity(obj)
}
function defineReactivity(obj) {
Object.keys(obj).forEach((key) => {
let activeValue = obj[key]
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
// 数据被访问的时候收集
dep.depend()
return activeValue
},
set(newValue) {
// 数据被改变的时候通知更新
activeValue = newValue
dep.notify()
}
})
})
}
// 订阅者模块
let activeWatcher = null
function autorun(update) {
function watcher() {
activeWatcher = watcher
update()
activeWatcher = null
}
watcher()
}
const states = {
count: 0
}
observe(states)
autorun(() => {
console.log("count:" + states.count)
})
// log 0
states.count++
// log 1
感谢😘
如果觉得文章内容对你有帮助:
-
❤️欢迎关注点赞哦! 我会尽最大努力产出高质量的文章
个人公众号: 前端Link
联系作者: linkcyd 😁
往期: