zl程序教程

您现在的位置是:首页 >  Javascript

当前栏目

莽村李青都看得懂的Vue响应式原理

2023-03-20 15:00:30 时间

八股文序

开篇来一段大家都会背诵的八股文。

某面试官: 请你简要介绍一下Vue的响应式原理。

答:Vue利用发布订阅模式结合Object.defineProperty劫持对象的get和set方法来实现响应式。

某面试官追问:你知道什么是依赖吗?

答:。。。

某面试官再次追问:请你简述一下依赖收集的过程。

答:。。。

违背老祖宗的决定将Vue响应式原理公众于世

我相信大部分人对于vue的响应式的了解程度也和上面八股文篇描写的差不多。所以我做了一个违背老祖宗的决定那就是

将Vue的响应式原理公众于世!!!!





因为是介绍响应式原理,所以我们以Vue2的一个最最简单的实例来入门。
在这里插入图片描述
从图可知,即使我们什么原理都不知道,也应该新建一个Vue类。

	 class Vue {
            constructor({ el, data }) {
                this.el = document.querySelector(el)
                this.data = data
            }
        }

响应式数据(Observe篇)

下一步,把普通对象变成响应式对象。如何实现呢? 八股文已经给出了答案。
在这里插入图片描述
在Vue的构造器中增加一行 observe(data)

	  class Vue {
            constructor({ el, data }) {
                this.el = document.querySelector(el)
                this.data = data
                // 执行observe函数将普通对象转换成响应式对象
                observe(data)
            }
        }

那么很显然,observe函数就是将普通对象变成响应式对象的函数。这一块并不难,所以这里直接给出代码,注意看注释

	 // observe 函数,observe中文是观察,观察一个普通对象 使它变成响应式的
        const observe = (target) => {
            // 如果不是对象数据类型,只是普通类型那么变成响应式
            if (typeof target !== 'object') return
            // 新建一个Observe类 实现响应式的具体逻辑
            new Observe(target)
        }

        class Observe {
            constructor(data) {
                Object.entries(data).forEach(item => {
                    Object.defineProperty(data, item[0], {
                        get() {
                            // 劫持对象的get方法
                            console.log(`看来你想获取${item[0]}的值`)
                            return item[1]
                        },
                        set(newValue) {
                            // 劫持对象的set方法  
                            // 这里要注意的是如果新值是对象类型 不要忘记响应化
                            if (typeof newValue === 'object')
                                observe(newValue)
                            item[1] = newValue
                        }
                    })
                })
            }
        }

没错 将一个普通对象变成响应式对象就是这么简单,这一小节已经完成了。

dom更新(Wacther篇)

由于本文主要介绍的是响应化原理,那么一些虚拟dom渲染的过程就以图带过。我们看到Vue的构造函数中的el属性
在这里插入图片描述
我们可以从el属性的值获取到实际dom结构。
我以图的形式来描述dom和watcher的关系
在这里插入图片描述
由图中的对应关系可得。每一个响应式变量都会对应一个watcher。
那么以我们本文开始的🌰做进一步解释

 <div id="app">
        {{name}}
  </div>

新建一个watcher实例需要三个属性,
1、监听的响应式对象(很好,在上一小节中我们已经完成了)
2、监听的key (从图文关系可知,每一个key对应一个watcher,所以需要传入key)
3、回调函数,也就是监听的响应式对象的key值发生变化的时候,需要执行的回调,这个回调函数就是dom更新函数。以保证数据改变—>回调执行—>dom更新

根据目前的分析,一个初步的Wather类已经写好了

	class Watcher {
            constructor(target, key, callBack) {
                this.target = target
                this.key = key
                this.callBack = callBack
            }
            // 传入的callback就是dom的更新函数,updateDom就是执行这个函数
            upDateDom() {
                this.callBack()
            }
        }

那么现在问题来了?如何让watcher观察的key和响应式数据中的key一一对应上呢?
八股文早已给出了答案
在这里插入图片描述
新建一个Dep类(很关键哦!),Dep是dependence的缩写,依赖的意思。那么他们的对应关系依然用图说话
在这里插入图片描述
由图可知,dom中的插值表达式和watcher是一对一的关系,但是响应式数据和watcher是一对多的关系
我们可以这样理解,不止一个dom节点使用到了name属性,所以name更新要通知全部的watcher更新它们的dom所以是一对多的关系。

新建一个发布订阅模式。

	class Dep {
            constructor() {
                // 保存响应式数据的所有依赖,这里的依赖就是watcher实例
                this.depList = []
            }

            // 由于是一对多的关系,所以要有一个add方法
            addDep(watcher) {
                this.depList.push(watcher)
            }

            // 通知全部的watcher更新视图
            noticeWatcher() {
                // 上面已经说过了,每一个依赖都是一个watcher实例
                // noticeWatcher就说明watcher(订阅者)已经收到了通知
                // 需要更新dom
                this.depList.forEach(item => {
                    item.upDateDom()
                })
            }
        }

由上图可知,响应式变量的每一个key都需要保存使用到它的dom节点(我们称为依赖)
下一小节介绍依赖收集。

依赖收集

如果你坚持到了这一小节,那么你已经完成了 发布者(Dep类),订阅者(Watcher类),响应式数据(Observe)类。已经是梅西进禁区----只差临门一脚了。依赖收集的过程就是将响应式数据关联的依赖(也就是使用到该响应式数据的dom节点)给收集起来。
注意看,这个男人叫小帅,他开始写最关键的代码了。改写watcher类。

 class Watcher {
            constructor(target, key, callBack) {
                this.target = target
                this.key = key
                this.callBack = callBack
                this.getValue()
            }
            // 传入的callback就是dom的更新函数,updateDom就是执行这个函数
            upDateDom() {
                this.callBack()
            }

            getValue() {
            	// 执行getValue函数完成依赖收集
                this.target[this.key]
            }
        }

什么?? 执行getValue的函数就可以完成依赖收集????
以本文的html为例,新建一个watcher实例

	var app = new Vue({
            el: '#app',
            data: {
                name: 'wxs'
            }
        })

        new Watcher(app.data, 'name', () => {
            console.log('假设这是一个dom更新函数!!!')
        })

当我们新建一个vue实例时,说明app.data已经变成了响应式对象,所以监听者watcher观察的就是这个对象,key为‘name’。由于在watcher的构造函数中执行了函数getValuethis.target[this.key]那么响应式对象的get方法就被触发了。
不好理解的话看图
在这里插入图片描述
在watcher的getValue函数中插一行代码,构造器中加一行代码

 getValue() {
				 // 保存当前watcher实例
                Dep.target = this
                // 执行getValue函数完成依赖收集
                this.target[this.key]
            }

在这里插入图片描述

它保存的是当前正在新建的wather实例。不一定要保留在Dep.target中,Dep.aaa,window.aaa都可以。也许现在你还不知道为什么要保存它。但是我保证,你在10行之内就知道它是干什么的了。
由于刚刚触发了响应式对象的get方法,我们回到响应式类Observe中。
在这里插入图片描述
上一小节最后一句
在这里插入图片描述
那说明响应式对象的每一个key都要有一个数组来存它全部的依赖。改写Observe类。

	 class Observe {
            constructor(data) {
                Object.entries(data).forEach(item => {
                    // 新建依赖收集器
                    const dep = new Dep()
                    Object.defineProperty(data, item[0], {
                        get() {
                            // 劫持对象的get方法
                            // 新建watcher实例会执行到key的get方法 所以Dep.target就是key需要收集的依赖
                             if (Dep.target !== null) {
                                // 避免重复收集
                                dep.addDep(Dep.target)
                            }
                            return item[1]
                        },
                        set(newValue) {
                            // 劫持对象的set方法  
                            // 这里要注意的是如果新值是对象类型 不要忘记响应化
                            if (typeof newValue === 'object')
                                observe(newValue)
                            item[1] = newValue
                            // 通知依赖更新
                            dep.noticeWatcher()
                        }
                    })
                })
            }
        }

写到这里也许你开始不耐烦了。但是


已经结束啦!!!!


是的你没有看错,但你完成这一步。Vue的响应式已经全部完成啦!。当然,我们实现的是简易版本的。但是请你相信。当你认真看完并理解。你已经掌握了Vue响应式的核心了。

出其不意的挂上全部代码

<!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">
        {{name}}
    </div>


    <script>
        class Dep {
            constructor() {
                // 保存响应式数据的所有依赖,这里的依赖就是watcher实例
                this.depList = []
            }

            // 由于是一对多的关系,所以要有一个add方法
            addDep(watcher) {
                this.depList.push(watcher)
            }

            // 通知全部的watcher更新视图
            noticeWatcher() {
                // 上面已经说过了,每一个依赖都是一个watcher实例
                // noticeWatcher就说明watcher(订阅者)已经收到了通知
                // 需要更新dom
                this.depList.forEach(item => {
                    item.upDateDom()
                })
            }
        }

        // observe 函数,observe中文是观察,观察一个普通对象 使它变成响应式的
        const observe = (target) => {
            // 如果不是对象数据类型,只是普通类型那么变成响应式
            if (typeof target !== 'object') return

            // 新建一个Observe类 实现响应式的具体逻辑
            new Observe(target)
        }

        class Observe {
            constructor(data) {
                Object.entries(data).forEach(item => {
                    // 新建依赖收集器
                    const dep = new Dep()
                    Object.defineProperty(data, item[0], {
                        get() {
                            // 劫持对象的get方法
                            // 新建watcher实例会执行到key的get方法 所以Dep.target就是key需要收集的依赖
                            if (Dep.target !== null) {
                                // 避免重复收集
                                dep.addDep(Dep.target)
                            }

                            return item[1]
                        },
                        set(newValue) {
                            // 劫持对象的set方法  
                            // 这里要注意的是如果新值是对象类型 不要忘记响应化
                            if (typeof newValue === 'object')
                                observe(newValue)
                            item[1] = newValue
                            // 通知依赖更新
                            dep.noticeWatcher()
                        }
                    })
                })
            }
        }

        class Vue {
            constructor({ el, data }) {
                this.el = document.querySelector(el)
                this.data = data
                // 执行observe函数将普通对象转换成响应式对象
                observe(data)
            }
        }

        class Watcher {
            constructor(target, key, callBack) {
                this.target = target
                this.key = key
                this.callBack = callBack
                this.getValue()
                // 依赖收集完成之后重置,避免重复收集
                Dep.target = null
            }
            // 传入的callback就是dom的更新函数,updateDom就是执行这个函数
            upDateDom() {
                this.callBack()
            }

            getValue() {
                // 保存当前watcher实例
                Dep.target = this
                // 执行getValue函数完成依赖收集
                this.target[this.key]
            }
        }

        var app = new Vue({
            el: '#app',
            data: {
                name: 'wxs'
            }
        })

        new Watcher(app.data, 'name', () => {
            console.log('假设这是一个dom更新函数!!!')
        })

    </script>
</body>

</html>

算上html结构以及换行才125行代码。学会它,超过隔壁莽村李青!