手把手带你了解VUE响应式原理

广告:宝塔服务器面板,一键全能部署及管理,送你10850元礼包,点我领取~~~

手把手带你了解VUE响应式原理

本篇文章我们来了解 Vue2.X 响应式原理,然后我们来实现一个 Vue 响应式原理(写的内容简单)实现步骤和注释写的很清晰,大家有兴趣可以耐心观看,希望对大家有所帮助!

Vue2.X响应式原理

1.defineProperty 的应用

Vue2.X 响应式中使用到了 defineProperty 进行数据劫持,所以我们对它必须有一定的了解,那么我们先来了解它的使用方法把, 这里我们来使用 defineProperty来模拟 Vue 中的 data。(学习视频分享:vue视频教程)

<body>    <div id="app"></div>    <script>        // 模拟 Vue的data        let data = {            msg: '',        }        // 模拟 Vue 实例        let vm = {}        // 对 vm 的 msg 进行数据劫持        Object.defineProperty(vm, 'msg', {            // 获取数据            get() {                return data.msg            },            // 设置 msg            set(newValue) {                // 如果传入的值相等就不用修改                if (newValue === data.msg) return                // 修改数据                data.msg = newValue                document.querySelector('#app').textContent = data.msg            },        })        // 这样子就调用了 defineProperty vm.msg 的 set        vm.msg = '1234'    </script></body>
登录后复制

可以看见 上面 vm.msg 数据是响应式

2.defineProperty修改多个参数为响应式

修改多个参数

看了上面的方法只能修改一个属性,实际上我们 data 中数据不可能只有一个,我们何不定义一个方法把data中的数据进行遍历都修改成响应式呢

<body>    <div id="app"></div><script>        // 模拟 Vue的data        let data = {            msg: '哈哈',            age: '18',        }        // 模拟 Vue 实例        let vm = {}        // 把多个属性转化 响应式        function proxyData() {            // 把data 中每一项都[msg,age] 拿出来操作            Object.keys(data).forEach((key) => {                // 对 vm 的 属性 进行数据劫持                Object.defineProperty(vm, key, {                    // 可枚举                    enumerable: true,                    // 可配置                    configurable: true,                    // 获取数据                    get() {                        return data[key]                    },                    // 设置 属性值                    set(newValue) {                        // 如果传入的值相等就不用修改                        if (newValue === data[key]) return                        // 修改数据                        data[key] = newValue                        document.querySelector('#app').textContent = data[key]                    },                })            })        }        // 调用方法        proxyData(data)</script></body>
登录后复制

3.Proxy

Vue3 中使用 Proxy 来设置响应式的属性

先来了解下 Proxy 的两个参数

new Proxy(target,handler)

target :要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

其实 和 Vue2.X实现的逻辑差不多,不过实现的方法不一样

那么就放上代码了

<body>    <div id="app"></div>    <script>            // 模拟 Vue data            let data = {                msg: '',                age: '',            }            // 模拟 Vue 的一个实例            // Proxy 第一个            let vm = new Proxy(data, {                // get() 获取值                // target 表示需要代理的对象这里指的就是 data                // key 就是对象的 键                get(target, key) {                    return target[key]                },                // 设置值                // newValue 是设置的值                set(target, key, newValue) {                    // 也先判断下是否和之前的值一样 节省性能                    if (target[key] === newValue) return                    // 进行设置值                    target[key] = newValue                    document.querySelector('#app').textContent = target[key]                },            })    </script></body>
登录后复制

触发setget 的方法

// 触发了set方法vm.msg = 'haha'// 触发了get方法console.log(vm.msg)
登录后复制

4.发布订阅模式

在Vue 响应式中应用到了 发布订阅模式 我们先来了解下

首先来说简单介绍下 一共有三个角色

发布者订阅者信号中心 举个现实中例子 作者(发布者)写一篇文章 发到了掘金(信号中心) ,掘金可以处理文章然后推送到了首页,然后各自大佬(订阅者)就可以订阅文章

在Vue 中的例子 就是EventBus $on $emit

那么我们就简单模仿一下 Vue 的事件总线吧

之前代码缩进4个单位有点宽,这里改成2个

<body>  <div id="app"></div>  <script>    class Vue {      constructor() {        // 用来存储事件        // 存储的 例子 this.subs = { 'myclick': [fn1, fn2, fn3] ,'inputchange': [fn1, fn2] }        this.subs = {}      }      // 实现 $on 方法 type是任务队列的类型 ,fn是方法      $on(type, fn) {        // 判断在 subs是否有当前类型的 方法队列存在        if (!this.subs[type]) {          // 没有就新增一个 默认为空数组          this.subs[type] = []        }        // 把方法加到该类型中        this.subs[type].push(fn)      }      // 实现 $emit 方法      $emit(type) {        // 首先得判断该方法是否存在        if (this.subs[type]) {          // 获取到参数          const args = Array.prototype.slice.call(arguments, 1)          // 循环队列调用 fn          this.subs[type].forEach((fn) => fn(...args))        }      }    }    // 使用    const eventHub = new Vue()    // 使用 $on 添加一个 sum 类型的 方法到 subs['sum']中    eventHub.$on('sum', function () {      let count = [...arguments].reduce((x, y) => x + y)      console.log(count)    })    // 触发 sum 方法    eventHub.$emit('sum', 1, 2, 4, 5, 6, 7, 8, 9, 10)  </script></body>
登录后复制

5.观察者模式

与 发布订阅 的差异

与发布订阅者不同 观察者中 发布者和订阅者(观察者)是相互依赖的 必须要求观察者订阅内容改变事件 ,而发布订阅者是由调度中心进行调度,那么看看观察者模式 是如何相互依赖,下面就举个简单例子

<body>  <div id="app"></div>  <script>    // 目标    class Subject {      constructor() {        this.observerLists = []      }      // 添加观察者      addObs(obs) {        // 判断观察者是否有 和 存在更新订阅的方法        if (obs && obs.update) {          // 添加到观察者列表中          this.observerLists.push(obs)        }      }      // 通知观察者      notify() {        this.observerLists.forEach((obs) => {          // 每个观察者收到通知后 会更新事件          obs.update()        })      }      // 清空观察者      empty() {        this.subs = []      }    }    class Observer {      // 定义观察者内容更新事件      update() {        // 在更新事件要处理的逻辑        console.log('目标更新了')      }    }    // 使用    // 创建目标    let sub = new Subject()    // 创建观察者    let obs1 = new Observer()    let obs2 = new Observer()    // 把观察者添加到列表中    sub.addObs(obs1)    sub.addObs(obs2)    // 目标开启了通知 每个观察者者都会自己触发 update 更新事件    sub.notify()  </script></body>
登录后复制

6.模拟Vue的响应式原理

这里来实现一个小型简单的 Vue 主要实现以下的功能

接收初始化的参数,这里只举几个简单的例子 el data options通过私有方法 _proxyDatadata 注册到 Vue 中 转成getter setter使用 observerdata 中的属性转为 响应式 添加到 自身身上使用 observer 方法监听 data 的所有属性变化来 通过观察者模式 更新视图使用 compiler 编译元素节点上面指令 和 文本节点差值表达式1.vue.js

在这里获取到 el data

通过 _proxyDatadata的属性 注册到Vue 并转成 getter setter

/* vue.js */class Vue {  constructor(options) {    // 获取到传入的对象 没有默认为空对象    this.$options = options || {}    // 获取 el    this.$el =      typeof options.el === 'string'        ? document.querySelector(options.el)        : options.el    // 获取 data    this.$data = options.data || {}    // 调用 _proxyData 处理 data中的属性    this._proxyData(this.$data)  }  // 把data 中的属性注册到 Vue  _proxyData(data) {    Object.keys(data).forEach((key) => {      // 进行数据劫持      // 把每个data的属性 到添加到 Vue 转化为 getter setter方法      Object.defineProperty(this, key, {        // 设置可以枚举        enumerable: true,        // 设置可以配置        configurable: true,        // 获取数据        get() {          return data[key]        },        // 设置数据        set(newValue) {          // 判断新值和旧值是否相等          if (newValue === data[key]) return          // 设置新值          data[key] = newValue        },      })    })  }}
登录后复制2.observer.js

在这里把 data 中的 属性变为响应式加在自身的身上,还有一个主要功能就是 观察者模式在 第 4.dep.js 会有详细的使用

/* observer.js */class Observer {  constructor(data) {    // 用来遍历 data    this.walk(data)  }  // 遍历 data 转为响应式  walk(data) {    // 判断 data是否为空 和 对象    if (!data || typeof data !== 'object') return    // 遍历 data    Object.keys(data).forEach((key) => {      // 转为响应式      this.defineReactive(data, key, data[key])    })  }  // 转为响应式  // 要注意的 和vue.js 写的不同的是  // vue.js中是将 属性给了 Vue 转为 getter setter  // 这里是 将data中的属性转为getter setter  defineReactive(obj, key, value) {    // 如果是对象类型的 也调用walk 变成响应式,不是对象类型的直接在walk会被return    this.walk(value)    // 保存一下 this    const self = this    Object.defineProperty(obj, key, {      // 设置可枚举      enumerable: true,      // 设置可配置      configurable: true,      // 获取值      get() {        return value      },      // 设置值      set(newValue) {        // 判断旧值和新值是否相等        if (newValue === value) return        // 设置新值        value = newValue        // 赋值的话如果是newValue是对象,对象里面的属性也应该设置为响应式的        self.walk(newValue)      },    })  }}
登录后复制

在html中引入的话注意顺序

<script src="./js/observer.js"></script><script src="./js/vue.js"></script>
登录后复制

然后在vue.js 中使用 Observer

/* vue.js */class Vue {  constructor(options) {    ...    // 使用 Obsever 把data中的数据转为响应式    new Observer(this.$data)  }  // 把data 中的属性注册到 Vue  _proxyData(data) {   ...  }}
登录后复制

看到这里为什么做了两个重复性的操作呢?重复性两次把 data的属性转为响应式

obsever.js 中是把 data 的所有属性 加到 data 自身 变为响应式 转成 getter setter方式

vue.js 中 也把 data的 的所有属性 加到 Vue 上,是为了以后方面操作可以用 Vue 的实例直接访问到 或者在 Vue 中使用 this 访问

使用例子:

<body>    <div id="app"></div><script>        // 模拟 Vue的data        let data = {            msg: '哈哈',            age: '18',        }        // 模拟 Vue 实例        let vm = {}        // 把多个属性转化 响应式        function proxyData() {            // 把data 中每一项都[msg,age] 拿出来操作            Object.keys(data).forEach((key) => {                // 对 vm 的 属性 进行数据劫持                Object.defineProperty(vm, key, {                    // 可枚举                    enumerable: true,                    // 可配置                    configurable: true,                    // 获取数据                    get() {                        return data[key]                    },                    // 设置 属性值                    set(newValue) {                        // 如果传入的值相等就不用修改                        if (newValue === data[key]) return                        // 修改数据                        data[key] = newValue                        document.querySelector('#app').textContent = data[key]                    },                })            })        }        // 调用方法        proxyData(data)</script></body>0
登录后复制

这样在Vue$data 中都存在了 所有的data 属性了 并且是响应式的

3.compiler.js

comilper.js在这个文件里实现对文本节点 和 元素节点指令编译 主要是为了举例子 当然这个写的很简单 指令主要实现 v-text v-model

<body>    <div id="app"></div><script>        // 模拟 Vue的data        let data = {            msg: '哈哈',            age: '18',        }        // 模拟 Vue 实例        let vm = {}        // 把多个属性转化 响应式        function proxyData() {            // 把data 中每一项都[msg,age] 拿出来操作            Object.keys(data).forEach((key) => {                // 对 vm 的 属性 进行数据劫持                Object.defineProperty(vm, key, {                    // 可枚举                    enumerable: true,                    // 可配置                    configurable: true,                    // 获取数据                    get() {                        return data[key]                    },                    // 设置 属性值                    set(newValue) {                        // 如果传入的值相等就不用修改                        if (newValue === data[key]) return                        // 修改数据                        data[key] = newValue                        document.querySelector('#app').textContent = data[key]                    },                })            })        }        // 调用方法        proxyData(data)</script></body>1
登录后复制4.dep.js

写一个Dep类 它相当于 观察者中的发布者 每个响应式属性都会创建这么一个 Dep 对象 ,负责收集该依赖属性的Watcher对象 (是在使用响应式数据的时候做的操作)

当我们对响应式属性在 setter 中进行更新的时候,会调用 Depnotify 方法发送更新通知

然后去调用 Watcher 中的 update 实现视图的更新操作(是当数据发生变化的时候去通知观察者调用观察者的update更新视图)

总的来说 在Dep(这里指发布者) 中负责收集依赖 添加观察者(这里指Wathcer),然后在 setter 数据更新的时候通知观察者

说的这么多重复的话,大家应该知道是在哪个阶段 收集依赖 哪个阶段 通知观察者了吧,下面就来实现一下吧

先写Dep

<body>    <div id="app"></div><script>        // 模拟 Vue的data        let data = {            msg: '哈哈',            age: '18',        }        // 模拟 Vue 实例        let vm = {}        // 把多个属性转化 响应式        function proxyData() {            // 把data 中每一项都[msg,age] 拿出来操作            Object.keys(data).forEach((key) => {                // 对 vm 的 属性 进行数据劫持                Object.defineProperty(vm, key, {                    // 可枚举                    enumerable: true,                    // 可配置                    configurable: true,                    // 获取数据                    get() {                        return data[key]                    },                    // 设置 属性值                    set(newValue) {                        // 如果传入的值相等就不用修改                        if (newValue === data[key]) return                        // 修改数据                        data[key] = newValue                        document.querySelector('#app').textContent = data[key]                    },                })            })        }        // 调用方法        proxyData(data)</script></body>2
登录后复制登录后复制

obsever.js 中使用Dep

get 中添加 Dep.target (观察者)

set 中 触发 notify (通知)

<body>    <div id="app"></div><script>        // 模拟 Vue的data        let data = {            msg: '哈哈',            age: '18',        }        // 模拟 Vue 实例        let vm = {}        // 把多个属性转化 响应式        function proxyData() {            // 把data 中每一项都[msg,age] 拿出来操作            Object.keys(data).forEach((key) => {                // 对 vm 的 属性 进行数据劫持                Object.defineProperty(vm, key, {                    // 可枚举                    enumerable: true,                    // 可配置                    configurable: true,                    // 获取数据                    get() {                        return data[key]                    },                    // 设置 属性值                    set(newValue) {                        // 如果传入的值相等就不用修改                        if (newValue === data[key]) return                        // 修改数据                        data[key] = newValue                        document.querySelector('#app').textContent = data[key]                    },                })            })        }        // 调用方法        proxyData(data)</script></body>3
登录后复制5.watcher.js

**watcher **的作用 数据更新后 收到通知之后 调用 update 进行更新

<body>    <div id="app"></div><script>        // 模拟 Vue的data        let data = {            msg: '哈哈',            age: '18',        }        // 模拟 Vue 实例        let vm = {}        // 把多个属性转化 响应式        function proxyData() {            // 把data 中每一项都[msg,age] 拿出来操作            Object.keys(data).forEach((key) => {                // 对 vm 的 属性 进行数据劫持                Object.defineProperty(vm, key, {                    // 可枚举                    enumerable: true,                    // 可配置                    configurable: true,                    // 获取数据                    get() {                        return data[key]                    },                    // 设置 属性值                    set(newValue) {                        // 如果传入的值相等就不用修改                        if (newValue === data[key]) return                        // 修改数据                        data[key] = newValue                        document.querySelector('#app').textContent = data[key]                    },                })            })        }        // 调用方法        proxyData(data)</script></body>4
登录后复制登录后复制

那么去哪里创建 Watcher 呢?还记得在 compiler.js中 对文本节点的编译操作吗

在编译完文本节点后 在这里添加一个 Watcher

还有 v-text v-model 指令 当编译的是元素节点 就添加一个 Watcher

<body>    <div id="app"></div><script>        // 模拟 Vue的data        let data = {            msg: '哈哈',            age: '18',        }        // 模拟 Vue 实例        let vm = {}        // 把多个属性转化 响应式        function proxyData() {            // 把data 中每一项都[msg,age] 拿出来操作            Object.keys(data).forEach((key) => {                // 对 vm 的 属性 进行数据劫持                Object.defineProperty(vm, key, {                    // 可枚举                    enumerable: true,                    // 可配置                    configurable: true,                    // 获取数据                    get() {                        return data[key]                    },                    // 设置 属性值                    set(newValue) {                        // 如果传入的值相等就不用修改                        if (newValue === data[key]) return                        // 修改数据                        data[key] = newValue                        document.querySelector('#app').textContent = data[key]                    },                })            })        }        // 调用方法        proxyData(data)</script></body>5
登录后复制

当 我们改变 响应式属性的时候 触发了 set() 方法 ,然后里面 发布者 dep.notify 方法启动了,拿到了 所有的 观察者 watcher 实例去执行 update 方法调用了回调函数 cb(newValue) 方法并把 新值传递到了 cb() 当中 cb方法是的具体更新视图的方法 去更新视图

比如上面的例子里的第三个参数 cb方法

<body>    <div id="app"></div><script>        // 模拟 Vue的data        let data = {            msg: '哈哈',            age: '18',        }        // 模拟 Vue 实例        let vm = {}        // 把多个属性转化 响应式        function proxyData() {            // 把data 中每一项都[msg,age] 拿出来操作            Object.keys(data).forEach((key) => {                // 对 vm 的 属性 进行数据劫持                Object.defineProperty(vm, key, {                    // 可枚举                    enumerable: true,                    // 可配置                    configurable: true,                    // 获取数据                    get() {                        return data[key]                    },                    // 设置 属性值                    set(newValue) {                        // 如果传入的值相等就不用修改                        if (newValue === data[key]) return                        // 修改数据                        data[key] = newValue                        document.querySelector('#app').textContent = data[key]                    },                })            })        }        // 调用方法        proxyData(data)</script></body>6
登录后复制

还有一点要实现v-model的双向绑定

不仅要通过修改数据来触发更新视图,还得为node添加 input 事件 改变 data数据中的属性

来达到双向绑定的效果

7.测试下自己写的

到了目前为止 响应式 和 双向绑定 都基本实现了 那么来写个例子测试下

<body>    <div id="app"></div><script>        // 模拟 Vue的data        let data = {            msg: '哈哈',            age: '18',        }        // 模拟 Vue 实例        let vm = {}        // 把多个属性转化 响应式        function proxyData() {            // 把data 中每一项都[msg,age] 拿出来操作            Object.keys(data).forEach((key) => {                // 对 vm 的 属性 进行数据劫持                Object.defineProperty(vm, key, {                    // 可枚举                    enumerable: true,                    // 可配置                    configurable: true,                    // 获取数据                    get() {                        return data[key]                    },                    // 设置 属性值                    set(newValue) {                        // 如果传入的值相等就不用修改                        if (newValue === data[key]) return                        // 修改数据                        data[key] = newValue                        document.querySelector('#app').textContent = data[key]                    },                })            })        }        // 调用方法        proxyData(data)</script></body>7
登录后复制

OK 基本实现了 通过 观察者模式 来 实现 响应式原理

8.五个文件代码

这里直接把5个文件个代码贴出来 上面有的地方省略了,下面是完整的方便大家阅读

vue.js

<body>    <div id="app"></div><script>        // 模拟 Vue的data        let data = {            msg: '哈哈',            age: '18',        }        // 模拟 Vue 实例        let vm = {}        // 把多个属性转化 响应式        function proxyData() {            // 把data 中每一项都[msg,age] 拿出来操作            Object.keys(data).forEach((key) => {                // 对 vm 的 属性 进行数据劫持                Object.defineProperty(vm, key, {                    // 可枚举                    enumerable: true,                    // 可配置                    configurable: true,                    // 获取数据                    get() {                        return data[key]                    },                    // 设置 属性值                    set(newValue) {                        // 如果传入的值相等就不用修改                        if (newValue === data[key]) return                        // 修改数据                        data[key] = newValue                        document.querySelector('#app').textContent = data[key]                    },                })            })        }        // 调用方法        proxyData(data)</script></body>8
登录后复制

obsever.js

<body>    <div id="app"></div><script>        // 模拟 Vue的data        let data = {            msg: '哈哈',            age: '18',        }        // 模拟 Vue 实例        let vm = {}        // 把多个属性转化 响应式        function proxyData() {            // 把data 中每一项都[msg,age] 拿出来操作            Object.keys(data).forEach((key) => {                // 对 vm 的 属性 进行数据劫持                Object.defineProperty(vm, key, {                    // 可枚举                    enumerable: true,                    // 可配置                    configurable: true,                    // 获取数据                    get() {                        return data[key]                    },                    // 设置 属性值                    set(newValue) {                        // 如果传入的值相等就不用修改                        if (newValue === data[key]) return                        // 修改数据                        data[key] = newValue                        document.querySelector('#app').textContent = data[key]                    },                })            })        }        // 调用方法        proxyData(data)</script></body>9
登录后复制

compiler.js

<body>    <div id="app"></div>    <script>            // 模拟 Vue data            let data = {                msg: '',                age: '',            }            // 模拟 Vue 的一个实例            // Proxy 第一个            let vm = new Proxy(data, {                // get() 获取值                // target 表示需要代理的对象这里指的就是 data                // key 就是对象的 键                get(target, key) {                    return target[key]                },                // 设置值                // newValue 是设置的值                set(target, key, newValue) {                    // 也先判断下是否和之前的值一样 节省性能                    if (target[key] === newValue) return                    // 进行设置值                    target[key] = newValue                    document.querySelector('#app').textContent = target[key]                },            })    </script></body>0
登录后复制

dep.js

<body>    <div id="app"></div><script>        // 模拟 Vue的data        let data = {            msg: '哈哈',            age: '18',        }        // 模拟 Vue 实例        let vm = {}        // 把多个属性转化 响应式        function proxyData() {            // 把data 中每一项都[msg,age] 拿出来操作            Object.keys(data).forEach((key) => {                // 对 vm 的 属性 进行数据劫持                Object.defineProperty(vm, key, {                    // 可枚举                    enumerable: true,                    // 可配置                    configurable: true,                    // 获取数据                    get() {                        return data[key]                    },                    // 设置 属性值                    set(newValue) {                        // 如果传入的值相等就不用修改                        if (newValue === data[key]) return                        // 修改数据                        data[key] = newValue                        document.querySelector('#app').textContent = data[key]                    },                })            })        }        // 调用方法        proxyData(data)</script></body>2
登录后复制登录后复制

watcher.js

<body>    <div id="app"></div><script>        // 模拟 Vue的data        let data = {            msg: '哈哈',            age: '18',        }        // 模拟 Vue 实例        let vm = {}        // 把多个属性转化 响应式        function proxyData() {            // 把data 中每一项都[msg,age] 拿出来操作            Object.keys(data).forEach((key) => {                // 对 vm 的 属性 进行数据劫持                Object.defineProperty(vm, key, {                    // 可枚举                    enumerable: true,                    // 可配置                    configurable: true,                    // 获取数据                    get() {                        return data[key]                    },                    // 设置 属性值                    set(newValue) {                        // 如果传入的值相等就不用修改                        if (newValue === data[key]) return                        // 修改数据                        data[key] = newValue                        document.querySelector('#app').textContent = data[key]                    },                })            })        }        // 调用方法        proxyData(data)</script></body>4
登录后复制登录后复制

(学习视频分享:web前端开发、编程基础视频)

以上就是手把手带你了解VUE响应式原理的详细内容,更多请关注9543建站博客其它相关文章!

9543建站博客
一个专注于网站开发、微信开发的技术类纯净博客。
作者头像
admin创始人

肥猫,知名SEO博客站长,14年SEO经验。

上一篇:聊聊Uniapp中去掉导航栏返回按钮的方法
下一篇:vue改变div颜色

发表评论

关闭广告
关闭广告