双向绑定原理

核心是通过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 })
}
Last Updated:
Contributors: af