Vue响应式原理与模拟
程序员文章站
2024-02-02 11:16:58
...
Vue
数据响应式原理:
数据劫持(拦截)、观察者模式
Vue2: getter和setter
Vue3: Proxy
通信模式
发布订阅模式
- 事件中心,提供发布事件(emit方法)和订阅事件(on)的接口,是发布订阅模式的核心,作为事件和事件处理函数的仓库。
- 订阅者:使用事件中心的订阅接口来订阅某种类型的事件,待事件发布时,会调用事件处理函数
- 发布者:使用事件中心的发布接口来发布某种类型的事件,触发事件处理函数的调用
观察者模式:Vue的响应式机制
-
观察者(订阅者):watcher
- 拥有一个update方法,当事件发生时调用update
-
目标(发布者):dependency,作为了观察者模式的核心,收集依赖,通知依赖
- subs数组:存储所有观察者
- addSub():添加观察者
- notify():当事件发生时,通知所有观察者(即调用所有观察者的update方法)
实现Vue
任务
- 负责接收初始化参数(选项对象)
- 负责把data中的属性注入到当前的实例,转换为getter/setter,这样方便直接操作实例属性。
- 负责调用boserver监听data中的所有属性的变化
- 负责调用compiler解析模板中的指令和插值表达式
类图
vue实例属性以$开头
-
$options
:存储选项对象 -
$el
:将选项对象的el属性转换为DOM节点并存储。通常options.el是一个CSS选择器或DOM节点,作为Vue实例的挂载点 -
$data
:存储选项对象的data属性 -
_proxyData()
:私有成员,将data中的属性转换为getter/setter注入到Vue实例中
class Vue {
constructor (options) {
// 1. 通过属性保存options和options.data、options.el
this.$options = options || {}
this.$el = typeof options.el === 'string'? document.querySelector(options.el): options.el
this.$data = options.data || {}
// 2. 把options.data的成员转为getter/setter,注入到实例中
this._proxyData(this.$data)
// 3. 调用observer来监听数据变化
// 4. 调用compiler对象来编译模板,解析指令和插值表达式
}
// 遍历options.data的所有属性注入到实例中,转换为getter/setter
_proxyData (data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get () {
// vue中不存在值,值存储在data中
return data[key]
},
set (newValue) {
if (newValue === data[key]) {
return
}
data[key] = newValue
}
})
})
}
}
Observer
- 功能
- 负责把data选项中的属性(深度遍历,即如果属性也是一个对象,则要递归地将该对象的属性也转为getter/setter)转换为getter/setter响应式数据
- 监听数据,数据变化时发送通知给watcher,由watcher的update方法来更新视图
- 类图
-
walk(data)
:遍历data对象,walk方法会在遍历的过程中,调用defineReactive
方法。 -
defineReactive(data, key, value)
:将data对象的属性转为getter/setter
class Observer {
constructor (data) {
this.walk(data)
}
walk (data) {
// 1. 判断data是否是空对象,或者是否是对象
if (!data || typeof data !== 'object') {
return
}
// 2. 遍历data的所有属性
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
// 把对象的属性转为getter/setter,value是属性值,
// 因为如果不传入value,使用obj[key]来获取属性值,会导致getter函数的无限递归
defineReactive (obj, key, value) {
// 如果value是对象,其属性也会转为响应式数据
this.walk(value)
let self = this
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
// 这里的value不能替换为obj[key]
return value
},
// 这里的value值形成了一个闭包
set (newValue) {
if (newValue === value) {
return
}
value = newValue
self.walk(newValue)
// 发送通知
}
})
}
}
Compiler
- 功能
- 负责编译模板,解析指令和插值表达式
- 负责页面的首次渲染
- 当数据变化后重新渲染视图
- 这里没有使用虚拟DOM,而是直接操作DOM
- 类图
-
el
:保存Vue实例的$el属性,即挂载点DOM -
vm
:保存当前Vue实例 -
compile(el)
:编译el内部的模板 -
compilerElement(node)
:编译元素节点 -
compileText(node)
:编译文本节点 -
isDirective(attrName)
:判断属性是否是指令 -
isTextNode(node)
:判断节点是否是文本节点 -
isElementNode(node)
:判断节点是否是元素节点
class Compiler {
constructor (vm) {
this.vm = vm
this.el = vm.$el
this.compile(this.el)
}
// 编译模板,处理文本节点和元素节点
compile (el) {
let childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
if (this.isTextNode(node)) {
// 处理文本节点
this.compileText(node)
} else if (this.isElementNode(node)) {
// 处理元素节点
this.compileElement(node)
}
// 判断node是否有子节点,有子节点,则递归调用compile
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 编译元素节点,处理指令
compileElement (node) {
// console.log(node.attributes)
// 遍历所有属性节点
Array.from(node.attributes).forEach(attr => {
let attrName = attr.nodeName
// 判断是否是指令
if (this.isDirective(attrName)) {
// v-text --> text
attrName = attrName.substr(2)
let key = attr.nodeValue
this.update(node, key, attrName)
}
})
}
update (node, key, attrName) {
let updateFn = this[attrName + 'Updater']
updateFn && updateFn.call(this, node, this.vm[key], key)
}
// 处理v-text指令,将节点的文本内容替换为v-text属性值
textUpdater (node, value, key) {
node.textContent = value
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
// 处理v-model指令,将表单元素的value值替换为v-model属性值
modelUpdater (node, value, key) {
node.value = value
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
}
// 编译文本节点,处理插值表达式
compileText (node) {
// 将node以对象的形式输出
// console.dir(node)
// 注意这里的正则表达式中.+?使用了非贪婪模式,意味着匹配{{ msg }}而不是{{ msg }} msg }}
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {
// 使用正则表达式构造函数的静态属性来获取捕获组
let key = RegExp.$1.trim()
// 将文本节点的插值表达式替换为对应的属性值
node.textContent = value.replace(reg, this.vm[key])
}
}
// 判断属性是否是指令
isDirective (attrName) {
return attrName.startsWith('v-')
}
// 判断是否是文本节点
isTextNode (node) {
return node.nodeType === 3
}
// 判断是否是元素节点
isElementNode (node) {
return node.nodeType === 1
}
}
Dependency:收集依赖,通知依赖
- 收集依赖:每一个响应式的数据属性都会创建一个Watcher,在getter中收集这些依赖于数据属性的watcher,实际就是将watcher添加到Dependency中。
- 依赖的收集开始于模板编译,因为只有在模板编译时才会去访问数据属性值,创建watcher,才能触发getter,在getter中收集依赖。
- 通知依赖:当数据属性发生变化时,在setter中调用notify方法通知并调用watcher的update方法
- 功能
- 收集依赖,添加观察者
- 通知所有观察者
- 类图
-
subs
:依赖(watcher)数组 -
addSub(sub)
:添加依赖(watcher) -
notify()
:通知依赖
class Dependency {
constructor () {
// 初始化所有观察者数组
this.subs = []
}
// 添加观察者
addSub (sub) {
// 判断是否存在并且是观察者(拥有update方法)
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 通知观察者
notify () {
this.subs.forEach(sub => {
sub.update()
})
}
}
Watcher
- 功能
- 数据变化时触发依赖,Dependency通知所有watcher更新视图
- watcher自身实例化的时候,添加自己到Dependency实例的subs数组中
- 类图
-
vm
:Vue实例 -
key
:属性名,可以通过vm[key]得到数据属性值,用来更新视图 -
cb
:每个watcher对象所对应的回调函数,用来更新对应的视图,因为每个watcher所对应的视图不同,更新方式不同。 -
oldValue
:视图之前的数据 -
update()
:更新视图
在做DOM操作时创建watcher对象,因为watcher对象就是用来更新视图的,watcher对象的cb回调函数接收新的数据属性值来更新DOM。
class Watcher {
constructor (vm, key, cb) {
this.vm = vm
// key即data对象的属性名
this.key = key
// 回调函数用来更新视图
this.cb = cb
// 当前watcher对象记录到Dependency的静态属性target
// 触发get方法,在get方法中调用addSub
Dependency.target = this
// 当访问该属性时,已经触发了get方法
this.oldValue = this.vm[this.key]
// 将target置空
Dependency.target = null
}
// 当数据发生变化时,更新视图
update () {
// 在调用update时,数据已经得到更新,所以可以通过属性名拿到新属性值
let newValue = this.vm[this.key]
if (this.oldValue === newValue) {
return
}
this.cb(newValue)
}
}
双向绑定
对于表单元素而言,双向绑定使得
- 视图更新触发数据更新
- 数据更新触发视图更新
// compiler.js
// 处理v-model指令,将表单元素的value值替换为v-model属性值
modelUpdater (node, value, key) {
node.value = value
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
// 双向绑定,就是给表单元素添加一个数据更新要触发的事件,如input,change等,然后将元素的value值更新到data的数据属性
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
上一篇: 【MVVM】WPF