响应式系统(一)

前言

Vue的响应式系统还是有点大的,我们可以通过一个小栗子,由浅入深的走下设计思路
最终版demo

正文

const data = {
    key: 'value'
}

function $watch(exp, fn) {
    // ...
}

$watch('key', () => {
    console.log('data.key被修改了')
})

如上所示,我们需要完成这么个功能,其实倒也不难,难的是一些情况的处理。比如重复依赖、深度观测、数组观测等。我们先不考虑这些问题,按最简单的一步一步的来
首先我们需要监测到这个数据什么时候被修改了,所幸这个ES有现成的Object.defineProperty

Object.defineProperty(data, 'key', {
    get() {
        console.log('读取了data.key')
    },
    set() {
        console.log('设置了data.key')
    }
})

可见我们实现了对data.key的拦截。那么如何使得传入的函数和属性关联(收集依赖)、修改属性的时候触发传入的函数(触发依赖),很显然get收集依赖,set触发依赖。所以我们先整个容器,在get时把依赖收集到这个容器即可,set时就可以遍历这个容器触发依赖

// 容器
const dep = []
Object.defineProperty(data, 'key', {
    get() {
        dep.push(fn)
    },
    set() {
        dep.forEach(fn => fn())
    }
})

代码来看很简单,只是有个问题,这个fn也就是观察者如何来
其实我们$watch第二参数就是fn那么现在就需要俩者关联起来
这个也简单,因为我们调用$watch时是知道观测哪个属性的,那么我们可以在$watch里访问这个属性,这样子就可以触发这个属性的get函数以达到依赖收集的目的

const data = {
    key: 'value'
}

const dep = []
Object.defineProperty(data, 'key', {
    get() {
        dep.push(Target)
    },
    set() {
        dep.forEach(fn => fn())
    }
})
let Target = null
function $watch(exp, fn) {
    // 将fn赋值给Target使得get函数可以取到
    Target = fn
    // 访问属性触发get函数
    data[exp]
}

$watch('key', () => {
    console.log('data.key被修改了')
})

迄今为止,我们可得到这个简单版本,修改data.key可见fn被触发
不过既然是简单版本自然有很多问题:

  1. 这里只是针对data.key,那么data.xx呢,所以得做个遍历,而且还得考虑到深层
function walk(data) {
    for (const key in data) {
        if (data.hasOwnProperty(key)) {
            const val = data[key]
            const dep = []
            if(Reflect.toString.call(val) === '[object Object]') {
                walk(val)
            }
            Object.defineProperty(data, key, {
                get() {
                    dep.push(Target)
                },
                set() {
                    dep.forEach(fn => fn())
                }
            })
        }
    }
}
walk(data)

这里简单遍历一下对象,判断下若子项是对象那么递归处理即可

  1. 这里get函数没有return,导致data.xx === undefined
function walk(data) {
    for (const key in data) {
        if (data.hasOwnProperty(key)) {
            let val = data[key]
            const dep = []
            if(Reflect.toString.call(val) === '[object Object]') {
                walk(val)
            }
            Object.defineProperty(data, key, {
                get() {
                    dep.push(Target)
                    return val
                },
                set(nVal) {
                    if(nVal === val) {
                        return
                    }
                    const oVal = val
                    val = nVal
                    dep.forEach(fn => fn(nVal, oVal))
                }
            })
        }
    }
}
walk(data)

这里简单的处理了val的问题,且将新旧值传入fn,使得fn内可以取值

  1. 这里我们揭示下$watch函数的作用
let Target = null
function $watch(exp, fn) {
    // 将fn赋值给Target使得get函数可以取到
    Target = fn
    // 访问属性触发get函数
    data[exp]
}

其实我们可以可见它就干了两个事:

  • 传入观察者fn,而且这个观察者被收集还通过了Target
  • 触发get
    首先我们联想下Vue.$watch,这个exp既可以是xx.xx.xx也可以是函数,也就是这个其实只要是能触发你要观察的对象的get函数就行,不限制你用什么手段,所以可以进行简单改造
let Target = null
function $watch(exp, fn) {
    // 将fn赋值给Target使得get函数可以取到
    Target = fn
    // 访问属性触发get函数
    if(typeof exp === 'function') {
        exp()
        return
    }
    let obj = data
    const pathArr = exp.split('.')
    pathArr.forEach((path) => {
        obj = obj[path]
    })
}

$watch('key', (nVal, oVal) => {
    console.log('data.key被修改了', nVal, oVal)
})
$watch('a.b', (nVal, oVal) => {
    console.log('data.a.b被修改了', nVal, oVal)
})
const render = () => {
    document.write(`data.a.b: ${data.a.b} ---- data.key: ${data.key}
`) } $watch(render, render)

我们看$watch(render, render),这个render函数里面访问了data数据,所以自然也能触发get函数,那么自然也就能收集依赖,触发依赖的话重新执行render函数,这也就是Vue页面渲染思路

  1. 我们可见每次访问数据时都会触发get函数,也就会有重复收集依赖问题
    我们这里就简单处理下
function walk(data) {
    // ...
    Object.defineProperty(data, key, {
        get() {
            Target && dep.push(Target)
            return val
        },
        set(nVal) {
            // ...
        }
    })
    // ...
}
let Target = null
function $watch(exp, fn) {
    // 将fn赋值给Target使得get函数可以取到
    Target = fn
    // 访问属性触发get函数
    if (typeof exp === 'function') {
        exp()
        Target = null
        return
    }
    let obj = data
    const pathArr = exp.split('.')
    pathArr.forEach((path) => {
        obj = obj[path]
    })
    Target = null
}

我们在$watch里触发完了get函数之后把Target置空,get函数里就可以通过判断Target来收集依赖
当然这里也仅仅是简单处理,若是对同个属性多次调用$watch观测也是会重复的,当然还有很多其他的诸如数组观测问题也就懒得处理了
最终版demo

后言

由上文可见实现响应式系统大致也就这几步:

  1. 观测数据,将其转化为响应式对象
  2. 暴露$watch方法,接收exp、fn参数,确定要监听的属性以及接收数据变化的回调函数
  3. get收集依赖,set触发依赖,也就是get里收集fnset里触发fn

你可能感兴趣的:(响应式系统(一))