一个简单的响应式实现方式

520 阅读2分钟

首先它会去执行refreshView一遍初始化视图显示data的初始化值, 然后observer对data里的数据进行劫持, 说得简单点就是对data的数据都添加了取值、赋值的监听方法. 当get方法执行时候添加订阅操作, 当取值时候去通知视图更新方法refreshView更新视图. 最后还有个优化, 将更新视图的方法变成一个微任务, 待同步的代码执行完了, 再去更新视图, 并且多次数据变更只更新一次视图.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>响应式源码分析</title>
</head>
<body>
<div id="app"></div>
<button id="btn">change data</button>
<script>
    // 用于响应式的值仿vue的data
    let data = {
        value: '',
        name: '',
        age: ''
    }

    // 更新视图的回调
    let active = null
    const refreshView  = function (cb) {
        active = cb
        cb()
        active = null
    }

    // 异步执行队列, 将通知更新视图的方法变成一个微任务, 待同步的数据更改代码执行完了, 才去执行一次页面的更新
    let queue = []
    let nextTick = cb => Promise.resolve().then(cb)
    let queueJob = job => {
        // 较少视图更新的方法执行
        if (!queue.includes(job)) {
            queue.push(job)
            nextTick(flushJobs)
        }
    }
    let flushJobs = () => {
        let job
        while ((job = queue.shift()) !== undefined) {
            job()
        }
    }

    // 发布订阅
    class Dep {
        constructor () {
            this.deps = new Set()
        }
        depend (cb) {
            active && this.deps.add(cb)
        }
        notify () {
            this.deps.forEach(dep => queueJob(dep))
        }
    }

    // 劫持data的数据, 给data属性的取值赋值成get,set方法
    const observer = obj => {
        const dep = new Dep()
        Object.keys(obj).forEach(key => {
            let value = obj[key]
            Object.defineProperty(obj, key, {
                // 取值执行的方法
                get () {
                    dep.depend(active) // 将更新视图回调函数添加到订阅
                    return value
                },
                // 赋值执行的方法, 当数据发生变化通知视图更新
                set (newVal) {
                    if (value === newVal) return
                    value = newVal
                    dep.notify() // 通知更新视图
                }
            })
        })
        
    }
    observer(data)

    // 更新视图
    refreshView (() => {
        document.querySelector('#app').innerHTML = `时间:${data.value} - 姓名:${data.name} - 年龄: ${data.age}岁`
    })

    // 改变data.value就可以更新视图了
    const btn = document.querySelector('#btn')
    btn.onclick = function () {
        let num = Math.ceil(Math.random() * 10)
        data.value = new Date()
        data.name = 'biu' + num
        data.age = num
    }

</script>
</body>
</html>