vue双向数据绑定原理及简易Vue2的实现
1. 一句话解释
vue双向数据绑定原理是通过 数据劫持
结合 发布订阅模式
的方式来实现的。
1.1 什么是数据劫持?
数据劫持是一种技术,它通过监听数据的变化来自动更新视图,同时也可以反过来,通过监听视图的变化来更新数据。
在不同版本的Vue中,数据劫持的使用方式略有不同。
Vue 2.x使用的是 ES5 中的 Object.defineProperty
方法, vue3.x使用的是Es6中的 Proxy
方法。
1.2 什么是发布订阅模式?
发布订阅模式是一种消息范式,包含一个主题/事件中心,通常会有多个订阅者,当主题对象发布事件时,订阅者对象会收到事件通知,然后进行相应的处理。
类似于一家报社发布报纸,读者可以订阅,当新的报纸发布时,读者就会收到通知。这种模式可以实现松散耦合,订阅者不需要知道谁发布了事件,发布者也不需要知道谁订阅了事件,从而使代码更灵活、可复用和易于维护。
1.3 发布订阅模式和观察者模式之间的区别:
发布订阅模式中,发布者和订阅者并没有直接的联系。发布者将消息发布到一个通道(channel)中,而订阅者通过指定并订阅这个通道来接收消息。发布者和订阅者之间的关系是通过这个通道建立起来的。
相比之下,观察者模式中,观察者和被观察者是直接耦合的。当被观察者状态发生改变时,它会直接通知观察者,而观察者也会相应地做出反应。
因此,发布订阅模式更加松散耦合,而观察者模式更加紧密耦合。
2. 双向数据绑定在vue框架中的作用
由三块部分构成:
数据层 (Model)
:应用的数据及业务逻辑,为开发者编写的业务代码;视图层 (View)
:应用的展示效果,各类UI组件,由 template 和 css 组成的代码;业务逻辑层 (ViewModel)
:框架封装的核心,它负责将数据与视图关联起来;
上面这个分层的架构方案,有一个专业术语:MVVM
,核心功能便是“双向数据绑定”
。
3. 理解双向数据绑定
3.1 主要职责:
它允许数据模型的变化自动更新视图,同时允许视图的变化自动更新数据模型。
3.2 主要构成:
- 监听器 (Observer) : 观察数据,时刻关注数据的任何变化,通知视图更新;
- 解析器 (Compiler) :观察视图层,时刻关注视图发生的一切交互,更新数据;
3.3 双向数据绑定的过程
双向数据绑定的过程可以归纳为以下几个步骤:
- 绑定数据模型和视图元素;
- 监听数据模型的变化,并更新视图元素;
- 监听视图元素的变化,并更新数据模型;
3.4 双向数据绑定实现步骤
-
首先在 VUE 初始化的时候,对数据(data)进行劫持监听(响应化处理),需要设置一个监听器(Observer), 用来监听所有属性。
监听器中使用到了ES5中的
Object.defineProperty()
方法,该方法能够劫持对象的 setter 和 getter方法,已达到监听数据的效果。 -
对模版执行编译操作,需要一个 指令解析器(Compile),对每个节点元素进行扫描和解析(元素节点/文本节点)。
找到其中动态绑定的数据,从 data 中获取并初始化视图。
同时,初始化一个 订阅者(Watcher)并添加更新函数,将来数据变化时 Watcher 会调用更新函数。
-
由于数据data中的key在一个视图中可能出现多次,订阅者会有多个,所以需要一个 消息订阅器(Dep)来专门收集这些订阅者,进行统一管理。
-
如果data中的属性发生了变化,会先找到对应的消息订阅器(Dep), 通知所有的 订阅者(Watcher)执行更新函数。
实现双向数据绑定可以归纳为以下3个步骤:
-
实现一个监听器 Observer, 用来劫持并监听所有属性,如果有变动,就通知订阅者。
-
实现一个订阅者 Watcher, 可以收到属性的变化通知并执行相应的函数,从而更新视图。
a. 订阅者可能有多个,需要一个订阅收集器来统一管理,在observer-get中,给每个属性赋值时,会初始化一个 Dep类,用来收集和发布订阅者
-
实现一个解析器 Compile, 可以扫描和解析每个节点的相关指令,并根据初始化模版数据以及初始化相应的订阅器。
4. 模拟响应式原理,实现一个简易的Vue
4.1 效果展示及项目结构:

Vue2-mini
├─ Compiler.js
├─ Dep.js
├─ index.html
├─ Observer.js
├─ vue.js
└─ Watcher.js
git地址: github.com/wuyanfeiyin…
4.1 初始化Vue
index.html
<div id="app">
<h1>学习响应式原理,Vue2 简易版</h1>
<hr/>
<h4>双向数据绑定:</h4>
<input type="text" v-model="msg">
<h6>{{msg}}</h6>
<hr/>
<h4>v-html:</h4>
<div v-html="htmlStr"></div>
<hr/>
<h4>v-text</h4>
<div v-text="msg"></div>
<hr/>
<h4>v-on:click</h4>
<span>{{number}}</span>
<button v-on:click="changeCount">点击+1</button>
</div>
<script>
new Vue({
el: '#app',
data: {
msg: 'hi~ wuyanfeiying',
htmlStr: `<span style='color:green;'>你好哇~</span>`,
number: 0
},
methods: {
changeCount() {
this.number ++
}
}
})
</script>
4.2 vue.js
-
将传入的配置项赋值到当前vue实例上;
-
代理data到vm上(
_proxyData
方法 );- 作用:方便取值,例如:vm.msg 相当于 vm.$data.msg
-
初始化 监听器 (
Observer
方法 );- 作用:对data的属性进行响应化处理
-
初始化 模板编译器 (
Compiler
方法);- 作用:解析指令和差值表达表达式
class Vue {
constructor (options){
this.$options = options || {}
this.$data = options.data
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
debugger
// 代理data到vm上,例如:vm.msg = vm.$data.msg
this._proxyData(this.$data)
// 初始化 监听器:对data的属性进行响应化处理
new Observer(this.$data)
// 初始化 模板编译器:解析指令和差值表达式
new Compiler(this)
}
// 代理data到vm上
_proxyData (data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this,key,{
enumberable: true, // 是否可枚举,比如能不能遍历这个属性
configurable: true, // 是否可配置,比如能不能删除这个属性
// 访问属性时
get(){
return data[key]
},
set(newValue) {
if (newValue === data[key]) {
return
}
data[key] = newValue
}
})
})
}
}
4.3 Observer.js (数据劫持)
-
校验传入的data的格式
-
遍历data的所有属性,进行响应化处理
-
每个属性在响应化处理前,都会先注册一个Dep(订阅收集器)
-
get方法中(访问属性时)
- 作用:收集依赖(订阅者 Watcher 的实例),存储到dep.subs数组中
- 注意:
- 此时 Dep.target 是订阅者Watcher的实例
- 怎么触发的呢?
- 在 Watcher 实例化的时候(这个发生在模版解析器里),将 Dep.target = new Watcher(),之后,访问了属性,触发了属性对应的getter,就到这里来了
- 在这里(指属性对应的getter方法中)进行依赖收集
- 最后(在Wathcer中),Dep.target = null (进行销毁处理)
-
set方法中(属性被赋值时)
- 发送通知
- 作用:告诉当前 Dep(订阅收集器)的所有订阅者(Watcher实例),更新数据
-
class Observer {
constructor (data) {
this.walk(data)
}
walk (data) {
// 判断data是否是对象
if (!data || typeof data !== 'object') {
return
}
// 遍历 data 对象的所有属性
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
// 对每个属性进行响应化处理
defineReactive(obj, key, val) {
let that = this
// 初始化 订阅收集器:负责收集订阅者(Watcher), 并发送通知
let dep = new Dep()
// val 如果也是对象,需要继续将内部属性响应化
that.walk(val)
Object.defineProperty(obj, key, {
enumerable: true, // 是否可枚举,比如 是否可以循环
configurable: true, // 是否可配置,比如 是否可以删除属性
// 访问属性时
get(){
// 收集依赖(收集订阅器 Watcher 的实例)
// 注意:这里 Dep.target 是 订阅器Watcher的实例,
// 在初始化Watcher的时候,被缓存到Dep的target上面
// 怎么触发的呢?
// 在Watcher初始化的时候(这个发生在模版解析器中),访问了属性,触发这个属性的getter,就到这里来了
// 触发完属性之后,又会被销毁掉(Dep.target = null)
Dep.target && dep.addSub(Dep.target)
return val
},
// 属性被赋值时
set(newValue) {
if (newValue === val) {
return
}
val = newValue
that.walk(newValue)
// 发送通知
// 告诉当前Dep(订阅收集器)关联的所有 订阅者(Watcher 实例),更新数据
dep.notify()
}
})
}
}
4.4 Compiler.js (模板解析)
遍历节点,根据节点类型进行模版解析
-
元素节点,
- 根据不同指令类型进行处理
- 如果存在子节点,递归编译
-
文本节点
- 正则匹配差值表达式
-
元素节点/文本节点 取值后,会创建 订阅者(Watcher)以及更新函数,当数据改变时,通过更新函数更新视图
class Compiler {
constructor (vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
// 编译模板:处理 元素节点 和 文本节点
compile(el) {
let childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
// 元素节点
if (this.isElementNode(node)) {
this.compileElement(node)
} else if (this.isTextNode(node)) {
// 文本节点
this.compileText(node)
}
// 判断是否有子节点
if (node.childNodes && node.childNodes.length > 0) {
// 对子节点进行递归调用
this.compile(node)
}
})
}
// 元素节点 解析
compileElement(node) {
// 遍历所有的属性节点
Array.from(node.attributes).forEach(attr => {
let attrName = attr.name
// 判断是否为指令 v-
if (this.isDirective(attrName)) {
// v-text ---> text
attrName = attrName.substr(2)
let key = attr.value
this.update(node, key, attrName)
}
// 判断是否为事件指令 v-on:事件名
if (this.isEvent(attrName)) {
let key = attr.value // 解析后是:changeCount
// 注意区分 substr 和 substring 用法
// attrName 此时已经在上面经过处理了(attrName.substr(2))
// 'v-on:click="changeName"'.substr(2) --> 'on:click'
// 'on:click'.substring(3) --> 'click'
const dir = attrName.substring(3) // 解析后是:click
this.eventHandler(node, this.vm, key, dir)
}
})
}
// 更新获取到的值
update(node, key, attrName) {
let updateFn = this[attrName + 'Updater']
updateFn && updateFn.call(this, node, this.vm[key], key)
}
// v-text 指令
textUpdater(node, value, key) {
node.textContent = value
// 初始化 订阅者, 传入更新函数
new Watcher(this.vm, key, newValue => {
node.textContent = newValue
})
}
// v-model 指令
modelUpdater(node, value, key) {
node.value = value
new Watcher(this.vm, key, newValue => {
node.value = newValue
})
// 双向数据绑定
node.addEventListener('input', ()=> {
this.vm[key] = node.value
})
}
// v-html 指令
htmlUpdater(node, value, key) {
node.innerHTML = value
new Watcher(this.vm, key, newValue => {
node.innerHTML = newValue
})
}
// 添加事件
eventHandler(node, vm, exp, dir) {
const fn = vm.$options.methods && vm.$options.methods[exp]
if (dir && fn) {
node.addEventListener(dir, fn.bind(vm))
}
}
// 文本节点 解析
compileText(node) {
// 正则匹配 差值表达式
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {
let key = reg.exec(value)[1]
node.textContent = value.replace(reg, this.vm[key])
// 创建 订阅者(Watcher), 当数据改变时 更新视图
new Watcher(this.vm, key, newValue => {
node.textContent = newValue
})
}
}
// 判断元素属性是否是指令
isDirective(attrName) {
return attrName.startsWith('v-')
}
// 判断是否为 事件指令 v-on:事件名
isEvent(attrName) {
return attrName.indexOf("on:") === 0
}
// 判断节点是否为 元素节点
isElementNode(node) {
return node.nodeType === 1
}
// 判断节点是否为 文本节点
isTextNode(node) {
return node.nodeType === 3
}
}
4.5 Dep.js (订阅收集器)
-
subs 数组:
- 存储所有订阅者(Watcher实例)
-
addSub 方法
- 添加订阅者
- 此方法会在Observer(数据劫持)中属性对应的get方法中调用
-
notify 方法
- 发布通知
- Dep(订阅收集器)下的所有订阅者都会触发更新函数
- 此方法会在Observer(数据劫持)中属性对应的set方法中调用
class Dep {
constructor() {
// 存储所有订阅者(Watcher实例)
this.subs = []
}
// 添加订阅者
// 此方法,会在 Observer(数据劫持) 中属性对应的 get方法中进行调用
addSub(watcher) {
if (watcher && watcher.update) {
this.subs.push(watcher)
}
}
// 发送通知
// Dep下的所有订阅者都会触发更新函数
// 此方法,会在 Observer(数据劫持) 中属性对应的 set方法中 进行调用
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
4.6 Watcher.js (订阅者)
-
形参(vm,key,cb):
- key:data的属性
- cb: 回调函数,作用是更新视图。此回调函数,是在Compiler模版解析过程中,取值时,创建订阅者(Watcher实例)的时候创建的
-
Dep.target:
- 初始化Vue时,先执行Observer进行数据的响应化处理,每个属性在响应化时,会初始化 订阅收集器(Dep)
-
- 将当前 Watcher的实例 存储到 Dep.target 上面
-
- 通过数据的取值操作,会触发一次属性的get方法,在Observer中,会进行依赖的收集操作,将Dep.target存储到dep.subs中
-
- 数据销毁(Dep.target = null)
-
update 方法:
- 数据发生变化时,更新视图
- 当数据发生变动时,会触发Observer中监听的属性的set方法
- set方法中,会调用 Dep中的notify, 对 订阅收集器(Dep)下面的所有订阅者(Watcher 实例)进行统一调用watcher.update方法
- 从而触发回调函数(cb),更新视图数据
class Watcher {
constructor (vm, key, cb) {
this.vm = vm;
// data的key
this.key = key;
// 回调函数:更新视图
// 此回调函数,是在 Compiler 中,模板解析的过程中,取值时,创建Watcher实例时,创建的
this.cb = cb;
// 初始化Vue时,先执行Observer进行数据的响应化处理,每个属性在响应化时,会初始化 订阅收集器(Dep)
// 此处将当前 Watcher的实例 存储到 Dep.target 上面
Dep.target = this;
// 此处,进行了属性的取值操作,会触发一次属性的get方法,在observer中,会进行依赖的收集操作,将Dep.target存储到dep.subs中
this.oldValue = vm[key]
// 数据销毁
Dep.target = null
}
// 数据发生变化时,更新视图
// 当数据发生变动时,会触发Observer中监听的属性的set方法,
// set方法中,会调用 Dep中的notify, 对 订阅收集器(Dep)下面的所有订阅者(Watcher 实例)进行统一调用watcher.update方法
// 从而触发回调函数,更新视图数据
update() {
let newValue = this.vm[this.key]
if (this.oldValue === newValue) {
return
}
this.cb(newValue)
}
}
5. Vue2和Vue3的响应式原理有什么区别?
版本 | 实现方式 | 优点 | 缺点 |
---|---|---|---|
Vue2.X | Object.defineProperty | 1. 基于 数据劫持/依赖收集 的双向绑定的优点;2. 兼容性好,兼容IE9等 | 1. 不能监听数组新增和删除属性,因为数组长度不确定,太长性能负担大;2. 只能监听属性,而不是整个对象,需要遍历属性;3. get方法没有传入参数,如果需要返回原值,需要在外部缓存之前的值;4. 只能监听属性的变化,不能监听属性的删减 |
Vue3.X | Proxy | 1. 可以监听数组;2. 可以监听整个对象,不需要递归;3. get方法可以传入对象和属性,可以直接在函数内部操作,不需要外部变量;4. new Proxy()会返回一个新对象,不会污染原对象; | 兼容性差,不支持IE等 |
6.总结
本文介绍了Vue框架中双向数据绑定的实现原理。主要分为两个部分:数据劫持和发布订阅模式。Vue 2.x 中使用的是 Object.defineProperty
方法,Vue 3.x 中使用的是 Proxy
方法。
在Vue的MVVM架构中,数据层、视图层和业务逻辑层共同构成了框架的核心。
双向数据绑定的过程涉及到监听器、解析器和订阅器、订阅收集器等模块,其中监听器用于劫持数据并监听数据变化,解析器用于扫描和解析每个节点的指令或表达式,订阅收集器用于统一管理订阅者,当数据发生变化时通知所有订阅者执行更新函数。
最后,本文还简单介绍了如何模拟响应式原理,实现一个简易的Vue。
参考:
vue的双向绑定原理及实现 - canfoo#! - 博客园
Daily-earning/Vue-mini版实现 at master · endless-z/Daily-earning
转载自:https://juejin.cn/post/7242519176853356601