Vue 原理之数据响应
定义 Reactive 属性
通过 Object.defineProperty
可以为一个对象设置具有 getter
和 setter
的属性,于是,我们可以为一个对象添加 reactive
的属性。
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
get: function reactiveGetter () {
return val
},
set: function reactiveSetter (newVal) {
console.log(`${key} is set to ${newVal}`)
val = newVal
}
})
}
const apple = {}
defineReactive(apple, 'color', 'red')
console.log(apple.color) // -> 'red'
apple.color = 'yellow' // color is set to yellow
定义 computed 属性
function defineComputed (target, key, computeFunc, updateCb) {
Object.defineProperty(target, key, {
get: function computedGetter() {
// evaluate computeFunc and return value
return computeFunc()
},
set: function computedSetter() {
// noop
}
})
}
为计算属性添加依赖追踪(简单实现)
定义一个全局的变量用于追踪依赖
const Dep = {
target: null
}
修改 defineComputed
:
function defineComputed (target, key, computeFunc, updateCb) {
const onDepUpdated = () => {
// recompute val when dep update
const val = computeFunc()
updateCb(val)
}
Object.defineProperty(target, key, {
get: function computedGetter() {
Dep.target = onDepUpdated
const val = computeFunc()
Dep.target = null
return val
},
set: function computedSetter() {
// noop
}
})
}
修改 defineReactive
:
function defineReactive (obj, key, val) {
const deps = []
Object.defineProperty(obj, key, {
get: function reactiveGetter () {
if (Dep.target) {
deps.push(Dep.target)
}
return val
},
set: function reactiveSetter (newVal) {
val = newVal
deps.forEach(func => {
func()
})
}
})
}
测试:
defineComputed(apple, 'description', function () {
return `this is a ${ apple.color } apple`
}, function (newVal) {
console.log(`description has changed to '${newVal}'`)
})
console.log(apple.color)
// red
console.log(apple.description)
// this is a red apple
apple.color = 'yellow'
// description has changed to 'this is a yellow apple'
console.log(apple.description)
// this is a yellow apple
上面是通过在执行计算属性的 getter
时设置一个标志 Dep.target
,调用到计算属性的依赖的 getter
时,会去读取 Dep.target
,即可在 deps
中保存下依赖关系。
为计算属性添加依赖追踪(嵌套)
依赖追踪的简单实现中,我们将 onDepUpdated
保存在 Dep.target
中,但是存在多层依赖关系时,简单修改 Dep.target
并不能满足需求。
const apple = {}
defineReactive(apple, 'color', 'red')
defineComputed(apple, 'appleName', function () {
return `${ apple.color } apple`
}, function (newVal) {
console.log(`appleName has changed to '${newVal}'`)
})
defineComputed(apple, 'description', function () {
return `this is a ${ apple.appleName }`
}, function (newVal) {
console.log(`description has changed to '${newVal}'`)
})
console.log(apple.color)
// red
console.log(apple.appleName)
// red apple
console.log(apple.description)
// this is a red apple
apple.color = 'yellow'
// appleName has changed to 'yellow apple'
// appleName has changed to 'yellow apple'
appleName
的 onDepUpdated
在 deps
里被存了两份。且修改 apple.color
后,期望的输出应该是 appleName has changed to 'yellow apple'
与 description has changed to 'this is a yellow apple'
。
原因在于向 deps
中添加 Dep.target
时并没有判重,且嵌套计算属性时,Dep.target
被最近读取的计算属性所覆盖,考虑使用栈模型解决这个问题。
const Dep = {}
Dep.target = null
const targetStack = []
function pushTarget (_target) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
function popTarget () {
Dep.target = targetStack.pop()
}
function defineReactive (obj, key, val) {
const deps = []
Object.defineProperty(obj, key, {
get: function reactiveGetter () {
if (Dep.target && deps.indexOf(Dep.target) === -1) {
deps.push(Dep.target)
}
return val
},
set: function reactiveSetter (newVal) {
val = newVal
deps.forEach(func => {
func()
})
}
})
}
function defineComputed (target, key, computeFunc, updateCb) {
const deps = []
const onDepUpdated = () => {
const val = computeFunc()
updateCb(val)
deps.forEach(func => {
func()
})
}
Object.defineProperty(target, key, {
get: function computedGetter() {
if (Dep.target && deps.indexOf(Dep.target) === -1) {
deps.push(Dep.target)
}
pushTarget(onDepUpdated)
const val = computeFunc()
popTarget()
return val
},
set: function computedSetter() {
// noop
}
})
}
将 target
存在 targetStack
中,每次执行计算属性的 getter
时,调用 pushTarget
和 popTarget
来修改 targetStack
。
模块化 Dep
将 defineReactive
与 defineComputed
中 Dep
相关的函数抽出。
// dep.js
export default class Dep {
}
Dep.target = null
const targetStack = []
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}
import Dep, { pushTarget, popTarget } from './dep'
最终代码
// dep.js
export default class Dep {
}
Dep.target = null
const targetStack = []
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}
import Dep, { pushTarget, popTarget } from './dep'
function defineReactive (obj, key, val) {
const deps = []
Object.defineProperty(obj, key, {
get: function reactiveGetter () {
if (Dep.target && deps.indexOf(Dep.target) === -1) {
deps.push(Dep.target)
}
return val
},
set: function reactiveSetter (newVal) {
val = newVal
deps.forEach(func => {
func()
})
}
})
}
function defineComputed (target, key, computeFunc, updateCb) {
const deps = []
const onDepUpdated = () => {
const val = computeFunc()
updateCb(val)
deps.forEach(func => {
func()
})
}
Object.defineProperty(target, key, {
get: function computedGetter() {
if (Dep.target && deps.indexOf(Dep.target) === -1) {
deps.push(Dep.target)
}
pushTarget(onDepUpdated)
const val = computeFunc()
popTarget()
return val
},
set: function computedSetter() {
// noop
}
})
}