本文从一个简单的双向绑定开始,逐步升级到由defineProperty和Proxy分别实现的响应式系统,注重入手思路,抓住关键细节,希望能对你有所帮助。
一、极简双向绑定
首先从最简单的双向绑定入手:
// html <input type="text"> <span></span>
// js let input = document.getElementById('input') let span = document.getElementById('span') input.addEventListener('keyup', function(e) { span.innerHTML = e.target.value })
以上似乎运行起来也没毛病,但我们要的是数据驱动,而不是直接操作dom:
// 操作obj数据来驱动更新 let obj = {} let input = document.getElementById('input') let span = document.getElementById('span') Object.defineProperty(obj, 'text', { configurable: true, enumerable: true, get() { console.log('获取数据了') return obj.text }, set(newVal) { console.log('数据更新了') input.value = newVal span.innerHTML = newVal } }) input.addEventListener('keyup', function(e) { obj.text = e.target.value })
以上就是一个简单的双向数据绑定,但显然是不足的,下面继续升级。
二、以defineProperty实现响应系统
在Vue3版本来临前以defineProperty实现的数据响应,基于发布订阅模式,其主要包含三部分:Observer、Dep、Watcher。
1. 一个思路例子
// 需要劫持的数据 let data = { a: 1, b: { c: 3 } } // 劫持数据data observer(data) // 监听订阅数据data的属性 new Watch('a', () => { alert(1) }) new Watch('a', () => { alert(2) }) new Watch('b.c', () => { alert(3) })
以上就是一个简单的劫持和监听流程,那对应的observer和Watch该如何实现?
2. Observer
observer的作用就是劫持数据,将数据属性转换为访问器属性,理一下实现思路:
①Observer需要将数据转化为响应式的,那它就应该是一个函数(类),能接收参数。
②为了将数据变成响应式,那需要使用Object.defineProperty。
③数据不止一种类型,这就需要递归遍历来判断。
// 定义一个类供传入监听数据 class Observer { constructor(data) { let keys = Object.keys(data) for (let i = 0; i < keys.length; i++) { defineReactive(data, keys[i], data[keys[i]]) } } } // 使用Object.defineProperty function defineReactive (data, key, val) { // 每次设置访问器前都先验证值是否为对象,实现递归每个属性 observer(val) // 劫持数据属性 Object.defineProperty(data, key, { configurable: true, enumerable: true, get () { return val }, set (newVal) { if (newVal === val) { return } else { data[key] = newVal // 新值也要劫持 observer(newVal) } } }) } // 递归判断 function observer (data) { if (Object.prototype.toString.call(data) === '[object, Object]') { new Observer(data) } else { return } } // 监听obj observer(data)
3. Watcher
根据new Watch('a', () => {alert(1)})我们猜测Watch应该是这样的:
class Watch { // 第一个参数为表达式,第二个参数为回调函数 constructor (exp, cb) { this.exp = exp this.cb = cb } }
那Watch和observer该如何关联?想想它们之间有没有关联的点?似乎可以从exp下手,这是它们共有的点:
class Watch { // 第一个参数为表达式,第二个参数为回调函数 constructor (exp, cb) { this.exp = exp this.cb = cb data[exp] // 想想多了这句有什么作用 } }
data[exp]这句话是不是表示在取某个值,如果exp为a的话,那就表示data.a,在这之前data下的属性已经被我们劫持为访问器属性了,那这就表明我们能触发对应属性的get函数,那这就与observer产生了关联,那既然如此,那在触发get函数的时候能不能把触发者Watch给收集起来呢?此时就得需要一个桥梁Dep来协助了。
4. Dep
思路应该是data下的每一个属性都有一个唯一的Dep对象,在get中收集仅针对该属性的依赖,然后在set方法中触发所有收集的依赖,这样就搞定了,看如下代码:
class Dep { constructor () { // 定义一个收集对应属性依赖的容器 this.subs = [] } // 收集依赖的方法 addSub () { // Dep.target是个全局变量,用于存储当前的一个watcher this.subs.push(Dep.target) } // set方法被触发时会通知依赖 notify () { for (let i = 1; i < this.subs.length; i++) { this.subs[i].cb() } } } Dep.target = null class Watch { constructor (exp, cb) { this.exp = exp this.cb = cb // 将Watch实例赋给全局变量Dep.target,这样get中就能拿到它了 Dep.target = this data[exp] } }
此时对应的defineReactive我们也要增加一些代码: