ホームページ > 記事 > ウェブフロントエンド > VUE の応答性の原理を段階的に理解します。
この記事では、Vue2 について学習します。ご興味があれば、辛抱強く見ていただけますので、皆様のお役に立てれば幸いです。
Vue2.X 応答原理
defineProperty
はデータ乗っ取りに対するVue2.X の応答性で使用されているため、ある程度の理解が必要です。まずは使い方を理解しましょう。 defineProperty を使用して、Vue の data をシミュレートします。 (学習ビデオ共有: vue ビデオ チュートリアル )
<div></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>
上記の vm.msg
データはであることがわかります。応答性
2.defineProperty 複数のパラメーターを応答性を持たせるように変更します。複数のパラメーターを変更します
#上記のメソッドでは 1 つの属性のみを変更できることがわかりました。実際、data のデータを走査するメソッドを定義してはどうでしょうかdata
にはデータが 1 つだけ存在することはできません。
すべてレスポンシブに変更されました
<div></div>
<script>
// 模拟 Vue的data
let data = {
msg: '哈哈',
age: '1VUE の応答性の原理を段階的に理解します。',
}
// 模拟 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>
3.Proxy
Vue3
で使用レスポンシブ属性を設定するための Proxyまず、Proxy
new Proxy(target,handler) の 2 つのパラメーターを理解しましょう。
target
handler
: 通常、属性として関数を持つオブジェクト。各属性の関数は、さまざまな操作を実行するときのエージェント
実際には、Vue2 によって実装されたロジック # および
get<div></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>
4. パブリッシュおよびサブスクライブ モード
は Vue の応答性に適用されます パブリッシュおよびサブスクライブ モデルに関しては、まず理解しましょう
# まず最初に、3 つの役割があることを簡単に紹介しましょうパブリッシャー、
サブスクライバー、シグナル センター
実際の例として、著者 (パブリッシャー) が記事を書いてナゲッツに送信します。 (Signal Center)。Nuggets は記事を処理してホームページにプッシュし、その後、それぞれの上司 (購読 (または) 記事を購読できます。
Vue の例は
EventBus# です)##$on $emit それでは、Vue のイベント バスを単純に真似してみましょう
前のコードの 4 単位のインデント少し幅が広かったので、ここでは 2 単位に変更しています
// 触发了set方法 vm.msg = 'haha' // 触发了get方法 console.log(vm.msg)
#5. オブザーバー パターン
パブリッシュ/サブスクライブとの違い
パブリッシュ/サブスクライバーとの違い オブザーバーのパブリッシャーとサブスクライバー (オブザーバー) は相互依存関係にあり、オブザーバーがコンテンツ変更イベントをサブスクライブする必要があり、パブリッシュ サブスクライバーはディスパッチ センターによってスケジュールされます。オブザーバー パターンは相互に依存します。これは簡単な例です
<div></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, VUE の応答性の原理を段階的に理解します。, VUE の応答性の原理を段階的に理解します。, 9, 10) </script>
6. Vue の応答原理をシミュレートします
主に次の関数を実装しますこちらは小さくて単純な
Vue
初期化パラメータを受け取ります。ここではいくつかの簡単な例を示しますel data
optionsプライベート メソッド経由
_proxyData
に登録され、getter
に変換されます。セッター
/* 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 }, }) }) } }
在这里把 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></script> <script></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 访问
使用例子:
<div></div> <script></script> <script></script> <script> let vm = new Vue({ el: '#app', data: { msg: '123', age: 21, }, }) </script>
这样在Vue
和 $data
中都存在了 所有的data 属性了 并且是响应式的
comilper.js在这个文件里实现对文本节点 和 元素节点指令编译 主要是为了举例子 当然这个写的很简单 指令主要实现 v-text v-model
/* compiler.js */ class Compiler { // vm 指 Vue 实例 constructor(vm) { // 拿到 vm this.vm = vm // 拿到 el this.el = vm.$el // 编译模板 this.compile(this.el) } // 编译模板 compile(el) { // 获取子节点 如果使用 forEach遍历就把伪数组转为真的数组 let childNodes = [...el.childNodes] childNodes.forEach((node) => { // 根据不同的节点类型进行编译 // 文本类型的节点 if (this.isTextNode(node)) { // 编译文本节点 this.compileText(node) } else if (this.isElementNode(node)) { //元素节点 this.compileElement(node) } // 判断是否还存在子节点考虑递归 if (node.childNodes && node.childNodes.length) { // 继续递归编译模板 this.compile(node) } }) } // 编译文本节点(简单的实现) compileText(node) { // 核心思想利用把正则表达式把{{}}去掉找到里面的变量 // 再去Vue找这个变量赋值给node.textContent let reg = /\{\{(.+?)\}\}/ // 获取节点的文本内容 let val = node.textContent // 判断是否有 {{}} if (reg.test(val)) { // 获取分组一 也就是 {{}} 里面的内容 去除前后空格 let key = RegExp.$1.trim() // 进行替换再赋值给node node.textContent = val.replace(reg, this.vm[key]) } } // 编译元素节点这里只处理指令 compileElement(node) { // 获取到元素节点上面的所有属性进行遍历 ![...node.attributes].forEach((attr) => { // 获取属性名 let attrName = attr.name // 判断是否是 v- 开头的指令 if (this.isDirective(attrName)) { // 除去 v- 方便操作 attrName = attrName.substr(2) // 获取 指令的值就是 v-text = "msg" 中msg // msg 作为 key 去Vue 找这个变量 let key = attr.value // 指令操作 执行指令方法 // vue指令很多为了避免大量个 if判断这里就写个 uapdate 方法 this.update(node, key, attrName) } }) } // 添加指令方法 并且执行 update(node, key, attrName) { // 比如添加 textUpdater 就是用来处理 v-text 方法 // 我们应该就内置一个 textUpdater 方法进行调用 // 加个后缀加什么无所谓但是要定义相应的方法 let updateFn = this[attrName + 'Updater'] // 如果存在这个内置方法 就可以调用了 updateFn && updateFn(node, key, this.vm[key]) } // 提前写好 相应的指定方法比如这个 v-text // 使用的时候 和 Vue 的一样 textUpdater(node, key, value) { node.textContent = value } // v-model modelUpdater(node, key, value) { node.value = value } // 判断元素的属性是否是 vue 指令 isDirective(attr) { return attr.startsWith('v-') } // 判断是否是元素节点 isElementNode(node) { return node.nodeType === 1 } // 判断是否是 文本 节点 isTextNode(node) { return node.nodeType === 3 } }
写一个Dep类 它相当于 观察者中的发布者 每个响应式属性都会创建这么一个 Dep 对象 ,负责收集该依赖属性的Watcher对象 (是在使用响应式数据的时候做的操作)
当我们对响应式属性在 setter 中进行更新的时候,会调用 Dep 中 notify 方法发送更新通知
然后去调用 Watcher 中的 update 实现视图的更新操作(是当数据发生变化的时候去通知观察者调用观察者的update更新视图)
总的来说 在Dep(这里指发布者) 中负责收集依赖 添加观察者(这里指Wathcer),然后在 setter 数据更新的时候通知观察者
说的这么多重复的话,大家应该知道是在哪个阶段 收集依赖 哪个阶段 通知观察者了吧,下面就来实现一下吧
先写Dep类
/* dep.js */ class Dep { constructor() { // 存储观察者 this.subs = [] } // 添加观察者 addSub(sub) { // 判断观察者是否存在 和 是否拥有update方法 if (sub && sub.update) { this.subs.push(sub) } } // 通知方法 notify() { // 触发每个观察者的更新方法 this.subs.forEach((sub) => { sub.update() }) } }
在 obsever.js 中使用Dep
在 get 中添加 Dep.target (观察者)
在 set 中 触发 notify (通知)
/* observer.js */ class Observer { ... } // 遍历 data 转为响应式 walk(data) { ... } // 这里是 将data中的属性转为getter setter defineReactive(obj, key, value) { ... // 创建 Dep 对象 let dep = new Dep() Object.defineProperty(obj, key, { ... // 获取值 get() { // 在这里添加观察者对象 Dep.target 表示观察者 Dep.target && dep.addSub(Dep.target) return value }, // 设置值 set(newValue) { if (newValue === value) return value = newValue self.walk(newValue) // 触发通知 更新视图 dep.notify() }, }) } }
**watcher **的作用 数据更新后 收到通知之后 调用 update 进行更新
/* watcher.js */ class Watcher { constructor(vm, key, cb) { // vm 是 Vue 实例 this.vm = vm // key 是 data 中的属性 this.key = key // cb 回调函数 更新视图的具体方法 this.cb = cb // 把观察者的存放在 Dep.target Dep.target = this // 旧数据 更新视图的时候要进行比较 // 还有一点就是 vm[key] 这个时候就触发了 get 方法 // 之前在 get 把 观察者 通过dep.addSub(Dep.target) 添加到了 dep.subs中 this.oldValue = vm[key] // Dep.target 就不用存在了 因为上面的操作已经存好了 Dep.target = null } // 观察者中的必备方法 用来更新视图 update() { // 获取新值 let newValue = this.vm[this.key] // 比较旧值和新值 if (newValue === this.oldValue) return // 调用具体的更新方法 this.cb(newValue) } }
那么去哪里创建 Watcher 呢?还记得在 compiler.js中 对文本节点的编译操作吗
在编译完文本节点后 在这里添加一个 Watcher
还有 v-text v-model 指令 当编译的是元素节点 就添加一个 Watcher
/* compiler.js */ class Compiler { // vm 指 Vue 实例 constructor(vm) { // 拿到 vm this.vm = vm // 拿到 el this.el = vm.$el // 编译模板 this.compile(this.el) } // 编译模板 compile(el) { let childNodes = [...el.childNodes] childNodes.forEach((node) => { if (this.isTextNode(node)) { // 编译文本节点 this.compileText(node) } ... } // 编译文本节点(简单的实现) compileText(node) { let reg = /\{\{(.+)\}\}/ let val = node.textContent if (reg.test(val)) { let key = RegExp.$1.trim() node.textContent = val.replace(reg, this.vm[key]) // 创建观察者 new Watcher(this.vm, key, newValue => { node.textContent = newValue }) } } ... // v-text textUpdater(node, key, value) { node.textContent = value // 创建观察者2 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } // v-model modelUpdater(node, key, value) { node.value = value // 创建观察者 new Watcher(this.vm, key, (newValue) => { node.value = newValue }) // 这里实现双向绑定 监听input 事件修改 data中的属性 node.addEventListener('input', () => { this.vm[key] = node.value }) } }
当 我们改变 响应式属性的时候 触发了 set() 方法 ,然后里面 发布者 dep.notify 方法启动了,拿到了 所有的 观察者 watcher 实例去执行 update 方法调用了回调函数 cb(newValue) 方法并把 新值传递到了 cb() 当中 cb方法是的具体更新视图的方法 去更新视图
比如上面的例子里的第三个参数 cb方法
new Watcher(this.vm, key, newValue => { node.textContent = newValue })
还有一点要实现v-model的双向绑定
不仅要通过修改数据来触发更新视图,还得为node添加 input 事件 改变 data数据中的属性
来达到双向绑定的效果
VUE の応答性の原理を段階的に理解します。.测试下自己写的
到了目前为止 响应式 和 双向绑定 都基本实现了 那么来写个例子测试下
<div> {{msg}} <br> {{age}} <br> <div></div> <input> </div> <script></script> <script></script> <script></script> <script></script> <script></script> <script> let vm = new Vue({ el: '#app', data: { msg: '123', age: 21, }, }) </script>
OK 基本实现了 通过 观察者模式 来 实现 响应式原理
VUE の応答性の原理を段階的に理解します。.五个文件代码
这里直接把5个文件个代码贴出来 上面有的地方省略了,下面是完整的方便大家阅读
vue.js
/* 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) // 使用 Obsever 把data中的数据转为响应式 new Observer(this.$data) // 编译模板 new Compiler(this) } // 把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 }, }) }) } }
obsever.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 // 创建 Dep 对象 let dep = new Dep() Object.defineProperty(obj, key, { // 设置可枚举 enumerable: true, // 设置可配置 configurable: true, // 获取值 get() { // 在这里添加观察者对象 Dep.target 表示观察者 Dep.target && dep.addSub(Dep.target) return value }, // 设置值 set(newValue) { // 判断旧值和新值是否相等 if (newValue === value) return // 设置新值 value = newValue // 赋值的话如果是newValue是对象,对象里面的属性也应该设置为响应式的 self.walk(newValue) // 触发通知 更新视图 dep.notify() }, }) } }
compiler.js
/* compiler.js */ class Compiler { // vm 指 Vue 实例 constructor(vm) { // 拿到 vm this.vm = vm // 拿到 el this.el = vm.$el // 编译模板 this.compile(this.el) } // 编译模板 compile(el) { // 获取子节点 如果使用 forEach遍历就把伪数组转为真的数组 let childNodes = [...el.childNodes] childNodes.forEach((node) => { // 根据不同的节点类型进行编译 // 文本类型的节点 if (this.isTextNode(node)) { // 编译文本节点 this.compileText(node) } else if (this.isElementNode(node)) { //元素节点 this.compileElement(node) } // 判断是否还存在子节点考虑递归 if (node.childNodes && node.childNodes.length) { // 继续递归编译模板 this.compile(node) } }) } // 编译文本节点(简单的实现) compileText(node) { // 核心思想利用把正则表达式把{{}}去掉找到里面的变量 // 再去Vue找这个变量赋值给node.textContent let reg = /\{\{(.+?)\}\}/ // 获取节点的文本内容 let val = node.textContent // 判断是否有 {{}} if (reg.test(val)) { // 获取分组一 也就是 {{}} 里面的内容 去除前后空格 let key = RegExp.$1.trim() // 进行替换再赋值给node node.textContent = val.replace(reg, this.vm[key]) // 创建观察者 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } } // 编译元素节点这里只处理指令 compileElement(node) { // 获取到元素节点上面的所有属性进行遍历 ![...node.attributes].forEach((attr) => { // 获取属性名 let attrName = attr.name // 判断是否是 v- 开头的指令 if (this.isDirective(attrName)) { // 除去 v- 方便操作 attrName = attrName.substr(2) // 获取 指令的值就是 v-text = "msg" 中msg // msg 作为 key 去Vue 找这个变量 let key = attr.value // 指令操作 执行指令方法 // vue指令很多为了避免大量个 if判断这里就写个 uapdate 方法 this.update(node, key, attrName) } }) } // 添加指令方法 并且执行 update(node, key, attrName) { // 比如添加 textUpdater 就是用来处理 v-text 方法 // 我们应该就内置一个 textUpdater 方法进行调用 // 加个后缀加什么无所谓但是要定义相应的方法 let updateFn = this[attrName + 'Updater'] // 如果存在这个内置方法 就可以调用了 updateFn && updateFn.call(this, node, key, this.vm[key]) } // 提前写好 相应的指定方法比如这个 v-text // 使用的时候 和 Vue 的一样 textUpdater(node, key, value) { node.textContent = value // 创建观察者 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } // v-model modelUpdater(node, key, value) { node.value = value // 创建观察者 new Watcher(this.vm, key, (newValue) => { node.value = newValue }) // 这里实现双向绑定 node.addEventListener('input', () => { this.vm[key] = node.value }) } // 判断元素的属性是否是 vue 指令 isDirective(attr) { return attr.startsWith('v-') } // 判断是否是元素节点 isElementNode(node) { return node.nodeType === 1 } // 判断是否是 文本 节点 isTextNode(node) { return node.nodeType === 3 } }
dep.js
/* dep.js */ class Dep { constructor() { // 存储观察者 this.subs = [] } // 添加观察者 addSub(sub) { // 判断观察者是否存在 和 是否拥有update方法 if (sub && sub.update) { this.subs.push(sub) } } // 通知方法 notify() { // 触发每个观察者的更新方法 this.subs.forEach((sub) => { sub.update() }) } }
watcher.js
/* watcher.js */ class Watcher { constructor(vm, key, cb) { // vm 是 Vue 实例 this.vm = vm // key 是 data 中的属性 this.key = key // cb 回调函数 更新视图的具体方法 this.cb = cb // 把观察者的存放在 Dep.target Dep.target = this // 旧数据 更新视图的时候要进行比较 // 还有一点就是 vm[key] 这个时候就触发了 get 方法 // 之前在 get 把 观察者 通过dep.addSub(Dep.target) 添加到了 dep.subs中 this.oldValue = vm[key] // Dep.target 就不用存在了 因为上面的操作已经存好了 Dep.target = null } // 观察者中的必备方法 用来更新视图 update() { // 获取新值 let newValue = this.vm[this.key] // 比较旧值和新值 if (newValue === this.oldValue) return // 调用具体的更新方法 this.cb(newValue) } }
以上がVUE の応答性の原理を段階的に理解します。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。