响应式编程与Vue(一)

1,416 阅读2分钟

最近一后端朋友想了解下vue的响应式,新公司的工作也不是很忙,正好抽时间把响应式编程研究一下

首先来看什么是响应式编程:
  • 响应式编程是一种面向数据串流和变化传播的编程范式,wiki定义
  • 这可以在编程语言中很方便的表示静态和动态的数据流,而相关的数据模型会自动将变化的值通过数据流进行传播
  • example: 表达式a = b + c 中, 将结果赋值给a后, 改变b或者c的值, 在命令式编程中, a 的值不会进行变化,而在响应式编程中,a会随c或者b的更新而更新, 如excel的计算公式
根据定义,简单实现一个demo
  • 首先来看我们的需求,有一表达式,y = x + 2, 当x 变化时,y 的值会自动更新
  • 我们需要检测x的变化,目前可以有proxy,Object.defineProperty两种API可以支持,下面我们来简单实现下

let collectFlag // 收集依赖,两个函数中共享
let makeReact = function (val) {
    return new Proxy({}, {
        get (target, prop) {
            return Reflect.get(target, prop)
        },
        set (target, prop, value) { // 使用Relect 避免赋值的时候栈溢出,另一种Object.defineProperty是通过闭包实现的
            if (Reflect.get(target, prop) !== value) {
                let result = Reflect.set(target, prop, value);
                collectFlag && collectFlag();
                return result
            }
        }
    })
}
let watchX = (fn) => {
	collectFlag = fn
}
// 将值 转为可观测对象,为了方便,我们暂且用简单值来测试
let x = makeReact(1)
let fy = () => {
	console.log('invoked')
	return x + 2
}
watchX(fy)
x.value = 2
x.value = 3
  • 现在我们增加一下难度,让y = x + z * 2, 这样的话我们就要增加一个来收集依赖的机制了,代码如下
class Dep { // 依赖收集类
    constructor () {
        this.deps = new Set()
    }
    add (dep) {
        if (typeof dep === 'function') {
            this.deps.add(dep)
        }
    }
    notify () {
        this.deps.forEach(dep => dep())
    }
}
let collectFlag
let ref = (value) => {
    let dep = new Dep() // 在闭包中保存依赖
    return new Proxy({
        value: value
    }, {
        get (target, prop) {
            dep.add(collectFlag)
            return Reflect.get(target, prop)
        },
        set (target, prop, value) {
            if (Reflect.get(target, prop) !== value) {
                let result = Reflect.set(target, prop, value);
                dep.notify()
                return result
            }
        }
    })
}

let watchReref = (fn) => {
    collectFlag = fn
    collectFlag()
    collectFlag = null 
}

let x = ref(1)
let z= ref(2)

watchReref(() => {
    console.log('y', x.value + z.value * 2)
})
x = 2
x = 3
z = 6
  • 好的,我们进一步增加需求,想想我们在vue中写代码的时候,一个交互可能会引起多个数据的更新,在上例中,每次x,z变化的时候,都会触发依赖,在例子中可能并没有什么大问题,但是在vue中,一个渲染函数里可能有多个依赖,如果每个依赖变化都要进行一遍diff和DOM 挂载,肯定性能不理想,所以vue里用了异步队列$nextTick来收集响应,当所有同步代码执行完再统一更新,异步队列代码如下:
let nextTick = (function () {
    let queue = []
    function dequeue () {
        while (queue.length) {
            queue.shift()()
        }
    }
    return function (fn) {
        if (!queue.includes(fn)) {
            queue.push(fn)
        }
        Promise.resolve().then(dequeue)
    }
})()
// 将上例中Dep 类的 notify函数更改一下,将其中的依赖执行委托给异步队列处理
notify () {
	this.deps.forEach(nextTick)
}

// 这是一段测试代码
x.value = 3;
x.value = 6;
z.value = 10;
setTimout(() => {z.value = 12, x.value = 9})

  • 好,现在我们就实现了 一个输入变化 输出自动跟随的响应式模型,这个模型对应到vue中就是数据(输入)到模板渲染(输出),把本例中的y = x.value + z.value * 2 改为 document.write(<div>x:${x.value},z:${z.value}</div>)就是一个简化版数据到html渲染的响应式模型了(这个省略了模板编译,和虚拟DOM)
本篇主要描述了响应式模型和异步队列的构建,下一篇再论述vue 中watch 和computed 的实现