双向绑定原理
核心是通过Object.defineProperty()来劫持各个属性的getter/setter,在数据变动时发布消息给订阅者,触发相应的监听回调
简单实现代码如下
代码按引入顺序贴出
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<div id="app2"></div>
<script src="index.js" type="module"></script>
</body>
</html>
import reactive from './reactive.js'
import Watcher from './watcher.js'
import computed from './computed.js'
import watch from './watch.js'
const data = reactive({
msg: 'Hello World',
number: 1,
})
new watch(
() => data.msg,
(nv, ov) => {
console.log('nv', nv)
console.log('ov', ov)
}
)
new Watcher(() => {
document.getElementById('app').innerHTML = `
<p>当前data的状态是:${JSON.stringify(data)}</p>
<p>请在控制台输入data,分别改变data.msg尝试效果</p>
<p>msg is ${data.msg}</p>`
})
const numberPlusOne = computed(() => data.number + 1)
new Watcher(() => {
document.getElementById('app2').innerHTML = `computed: 1 + number 是 ${numberPlusOne.value}`
})
window.data = data
import Dep from './dep.js'
function isObject(val) {
return val !== null && typeof val === 'object'
}
// 将对象定义为响应式
export default function reactive(data) {
if (isObject(data)) {
Object.keys(data).forEach(key => {
defineReactive(data, key)
})
}
return data
}
function defineReactive(data, key) {
let val = data[key]
// 收集依赖
const dep = new Dep()
Object.defineProperty(data, key, {
get() {
dep.depend()
return val
},
set(newVal) {
val = newVal
dep.notify()
},
})
if (isObject(val)) {
reactive(val)
}
}
export default class Dep {
constructor() {
this.deps = new Set()
}
depend() {
if (Dep.target) {
this.deps.add(Dep.target)
}
}
notify() {
this.deps.forEach(watch => watch.update())
}
}
// 正在运行的 watcher,一定是一个 Watcher 的实例
Dep.target = null
// watcher 栈
const targetStack = []
// 将上一个 watcher 推到栈里,更新 Dep.target 为传入的 _target 变量
export function pushTarget(_target) {
if (Dep.target) {
targetStack.push(Dep.target)
}
Dep.target = _target
}
// 取回上一个 watcher 作为Dep.target,并且栈里要弹出上一个 watcher
export function popTarget() {
Dep.target = targetStack.pop()
}
import Dep, { pushTarget, popTarget } from './dep.js'
export default class Watcher {
constructor(getter, options = {}) {
const { computed, watch, callback } = options
this.getter = getter
this.computed = computed
this.watch = watch
this.callback = callback
this.value = undefined
if (computed) {
this.dep = new Dep()
} else {
this.get()
}
}
get() {
pushTarget(this)
this.value = this.getter()
popTarget()
return this.value
}
// 仅为 computed 使用
depend() {
this.dep.depend()
}
update() {
if (this.computed) {
this.get()
this.dep.notify()
} else if (this.watch) {
const oldValue = this.value
this.get()
this.callback(this.value, oldValue)
} else {
this.get()
}
}
}
import Watcher from './watcher.js'
export default function computed(getter) {
let def = {}
const computedWatcher = new Watcher(getter, { computed: true })
Object.defineProperty(def, 'value', {
get() {
// 先让 computedWatcher 收集渲染 watcher 作为自己的依赖
computedWatcher.depend()
// 在这次执行用户传入的函数中,又会让响应式的值收集到 computedWatcher
return computedWatcher.get()
},
})
return def
}
import Watcher from './watcher.js'
export default function watch(getter, callback) {
new Watcher(getter, { watch: true, callback })
}