前言
Vue
的响应式系统还是有点大的,我们可以通过一个小栗子,由浅入深的走下设计思路
最终版demo
正文
const data = {
key: 'value'
}
function $watch(exp, fn) {
// ...
}
$watch('key', () => {
console.log('data.key被修改了')
})
如上所示,我们需要完成这么个功能,其实倒也不难,难的是一些情况的处理。比如重复依赖、深度观测、数组观测等。我们先不考虑这些问题,按最简单的一步一步的来
首先我们需要监测到这个数据什么时候被修改了,所幸这个ES
有现成的Object.defineProperty
Object.defineProperty(data, 'key', {
get() {
console.log('读取了data.key')
},
set() {
console.log('设置了data.key')
}
})
可见我们实现了对data.key
的拦截。那么如何使得传入的函数和属性关联(收集依赖)、修改属性的时候触发传入的函数(触发依赖),很显然get
收集依赖,set
触发依赖。所以我们先整个容器,在get
时把依赖收集到这个容器即可,set
时就可以遍历这个容器触发依赖
// 容器
const dep = []
Object.defineProperty(data, 'key', {
get() {
dep.push(fn)
},
set() {
dep.forEach(fn => fn())
}
})
代码来看很简单,只是有个问题,这个fn
也就是观察者如何来
其实我们$watch
第二参数就是fn
那么现在就需要俩者关联起来
这个也简单,因为我们调用$watch
时是知道观测哪个属性的,那么我们可以在$watch
里访问这个属性,这样子就可以触发这个属性的get
函数以达到依赖收集的目的
const data = {
key: 'value'
}
const dep = []
Object.defineProperty(data, 'key', {
get() {
dep.push(Target)
},
set() {
dep.forEach(fn => fn())
}
})
let Target = null
function $watch(exp, fn) {
// 将fn赋值给Target使得get函数可以取到
Target = fn
// 访问属性触发get函数
data[exp]
}
$watch('key', () => {
console.log('data.key被修改了')
})
迄今为止,我们可得到这个简单版本,修改data.key
可见fn
被触发
不过既然是简单版本自然有很多问题:
- 这里只是针对
data.key
,那么data.xx
呢,所以得做个遍历,而且还得考虑到深层
function walk(data) {
for (const key in data) {
if (data.hasOwnProperty(key)) {
const val = data[key]
const dep = []
if(Reflect.toString.call(val) === '[object Object]') {
walk(val)
}
Object.defineProperty(data, key, {
get() {
dep.push(Target)
},
set() {
dep.forEach(fn => fn())
}
})
}
}
}
walk(data)
这里简单遍历一下对象,判断下若子项是对象那么递归处理即可
- 这里
get
函数没有return
,导致data.xx === undefined
function walk(data) {
for (const key in data) {
if (data.hasOwnProperty(key)) {
let val = data[key]
const dep = []
if(Reflect.toString.call(val) === '[object Object]') {
walk(val)
}
Object.defineProperty(data, key, {
get() {
dep.push(Target)
return val
},
set(nVal) {
if(nVal === val) {
return
}
const oVal = val
val = nVal
dep.forEach(fn => fn(nVal, oVal))
}
})
}
}
}
walk(data)
这里简单的处理了val
的问题,且将新旧值传入fn
,使得fn
内可以取值
- 这里我们揭示下
$watch
函数的作用
let Target = null
function $watch(exp, fn) {
// 将fn赋值给Target使得get函数可以取到
Target = fn
// 访问属性触发get函数
data[exp]
}
其实我们可以可见它就干了两个事:
- 传入观察者
fn
,而且这个观察者被收集还通过了Target
- 触发
get
首先我们联想下Vue.$watch
,这个exp
既可以是xx.xx.xx
也可以是函数,也就是这个其实只要是能触发你要观察的对象的get
函数就行,不限制你用什么手段,所以可以进行简单改造
let Target = null
function $watch(exp, fn) {
// 将fn赋值给Target使得get函数可以取到
Target = fn
// 访问属性触发get函数
if(typeof exp === 'function') {
exp()
return
}
let obj = data
const pathArr = exp.split('.')
pathArr.forEach((path) => {
obj = obj[path]
})
}
$watch('key', (nVal, oVal) => {
console.log('data.key被修改了', nVal, oVal)
})
$watch('a.b', (nVal, oVal) => {
console.log('data.a.b被修改了', nVal, oVal)
})
const render = () => {
document.write(`data.a.b: ${data.a.b} ---- data.key: ${data.key}
`)
}
$watch(render, render)
我们看$watch(render, render)
,这个render
函数里面访问了data
数据,所以自然也能触发get
函数,那么自然也就能收集依赖,触发依赖的话重新执行render
函数,这也就是Vue
页面渲染思路
- 我们可见每次访问数据时都会触发
get
函数,也就会有重复收集依赖问题
我们这里就简单处理下
function walk(data) {
// ...
Object.defineProperty(data, key, {
get() {
Target && dep.push(Target)
return val
},
set(nVal) {
// ...
}
})
// ...
}
let Target = null
function $watch(exp, fn) {
// 将fn赋值给Target使得get函数可以取到
Target = fn
// 访问属性触发get函数
if (typeof exp === 'function') {
exp()
Target = null
return
}
let obj = data
const pathArr = exp.split('.')
pathArr.forEach((path) => {
obj = obj[path]
})
Target = null
}
我们在$watch
里触发完了get
函数之后把Target
置空,get
函数里就可以通过判断Target
来收集依赖
当然这里也仅仅是简单处理,若是对同个属性多次调用$watch
观测也是会重复的,当然还有很多其他的诸如数组观测问题也就懒得处理了
最终版demo
后言
由上文可见实现响应式系统大致也就这几步:
- 观测数据,将其转化为响应式对象
- 暴露
$watch
方法,接收exp、fn
参数,确定要监听的属性以及接收数据变化的回调函数 -
get
收集依赖,set
触发依赖,也就是get
里收集fn
,set
里触发fn