Vue响应式原理浅析
最近在学习前端框架Vue ,对其响应式原理做一些简单的分析
大致原理:
当把一个普通的 JavaScript 对象传入 Vue 实例作为 data
选项,Vue 将遍历此对象所有的属性,将这些属性转为getter/setter
,并针对每个key
创建一个对应的Dep
对象(用来管理watcher
)。 然后会解析el
的子元素,创建对应的watcher
,这样每个节点元素都有其对应的watcher
,随后的视图更新就是通过watcher
来做的。
当我们使用数据时,触发的是vm
的getter
方法,这时会将创建的watcher
添加到dep
中;在我们更改数据时,就会触发Vue中的setter
,会调用dep
的notify
方法,通知所有的watcher
进行update
大致效果:
效果图.gif1. Vue的初始化
这里做的主要事情是
- 将
data
中的数据代理到自身 - 创建一个
Observer
实例来将data
属性转为getter/setter
- 然后解析el的子元素
constructor(options) {
this.$options = options
this.$data = options.data
this.$el = options.el
// 监听setter/getter
new Observer(this.$data)
// 代理
Object.keys(this.$data).forEach(key => {
this._proxy(key)
})
// 解析el
new Complier(this.$el, this)
}
// 代理data的属性到Vue上
_proxy(key) {
Object.defineProperty(this, key, {
configurable:true,
enumerable:true,
get(){
return this.$data[key]
},
set(newValue) {
this.$data[key] = newValue
},
})
}
2. Observer
Observer
将Vue中data
的属性转为getter/setter
,依此来监测数据的变化。 data
中每一个 key
都有一个对应的Dep
实例,Dep
是依赖项,用来保存一个个watcher
。
class Observer {
constructor(data){
this.data = data
Object.keys(this.data).forEach(key => {
this.defineReactive(this.data, key, this.data[key])
})
}
// 设置响应式逻辑
defineReactive(data, key, val){
// 每个key都对应一个dep
const dep = new Dep()
// 设置setter/getter
Object.defineProperty(data, key, {
enumerable:true,
configurable:true,
get(){
// 添加watcher到对应的dep
if (Dep.target) {
dep.addSub(Dep.target)
}
return val
},
set(newValue){
if (newValue === val) {
return
}
val = newValue
// value发生变化 通知所有的watcher进行更新
dep.notify()
}
})
}
}
3. Dep
Dep
是和data中的key一一对应的,也就是每一个key都有对应的dep,dep对外暴露了一些方法,比如添加wacther的方法,以及很重要的notify
class Dep {
constructor(){
this.subs = []
}
// 添加watcher
addSub(watcher){
this.subs.push(watcher)
}
// 通知所有的watcher进行update
notify(){
this.subs.forEach(item => item.update())
}
}
之所以每个
key
对应一个Dep
实例,是因为data中可能有多个键值对,比如name,age
等,当我们修改name
的时候,肯定是只希望使用了name
数据的元素进行改变,而使用age
属性的不发生改变,使用dep
就是为了管理每个属性对应的多个watcher
4. Compiler
这里是将对el的处理都放到了Compiler
中,对el的子元素进行遍历,然后对子元素中使用的指令进行解析,创建对应的watcher
// 匹配{{}}语法的正则
const reg = /\{\{(.+)\}\}/
class Compiler {
constructor(el, vm){
this.el = document.querySelector(el)
this.vm = vm
this.frag = this._createFragment()
this.el.appendChild(this.frag)
}
_createFragment(){
const frag = document.createDocumentFragment()
let child
while (child = this.el.firstChild) {
this._compile(child)
frag.appendChild(child)
}
return frag
}
_compile(node) {
if (node.nodeType === Node.ELEMENT_NODE) {// 元素节点
const attrs = node.attributes
if (attrs.hasOwnProperty('v-model')) {
const attr = attrs['v-model']
// 取出v-model对应的变量名
const name = attr.nodeValue
// 监听input事件
node.addEventListener('input', e => {
// 将输入的内容保存
// 同时会触发vm的setter 以此来通知所有的watcher进行update
this.vm[name] = e.target.value
})
// 创建该节点对应的watcher
new Watcher(node, name, this.vm)
}
} else if (node.nodeType === Node.TEXT_NODE) {// 文本节点
const nodeValue = node.nodeValue // {{message}}
if (reg.test(nodeValue)) {
// 取出{{}}中的名称
const name = RegExp.$1.trim()
// 创建该节点对应的watcher
new Watcher(node, name, this.vm)
}
}
}
}
5. Watcher
watcher
的作用是用来通知元素进行更新,跟页面中的元素是一一对应的,这样当data
中的数据发生改变时,会触发setter
方法,这时dep就会通知其所有的watcher
进行update
class Watcher {
constructor(node, name, vm){
this.node = node
this.name = name
this.vm = vm
// 将自身保存 以便在调用data的getter时能添加到dep中
Dep.target = this
this.update()
// 清空target 防止重复添加
Dep.target = null
}
// 更新视图
update(){
if (this.node.nodeType === Node.ELEMENT_NODE) {// 元素节点
this.node.value = this.vm[this.name]
} else if (this.node.nodeType === Node.TEXT_NODE) {// 文本节点
this.node.nodeValue = this.vm[this.name]
}
}
}
6. 使用的方法
-
Object.defineProperty
该方法就是将data的属性转为getter/setter
的,也是整个响应式的关键所在。Object.defineProperty
是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。查看具体用法 -
document.createDocumentFragment()
在遍历el的时候我们使用了文档片段,使用文档片段的好处:-
文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段中不会引起页面回流,因此使用文档片段通常会有更好的性能。
-
将文档片段插入到一个父节点时,被插入的是片段的所有子节点,而不是片段本身,因为所有的节点会被一次插入到文档中,而这个操作仅发生一个重渲染的操作,而不是每个节点分别被插入到文档中,因为后者会发生多次重渲染的操作
-
-
appendChild
将子节点添加到指定父节点的子节点列表的末尾。其实没什么好说的,不过其有一个特点,就是当插入的子节点存在于当前文档的文档树中的话,会从原来位置移除,然后再插入到新的位置。所以当在我们遍历el的子元素之后,要再将文档片段添加到el中,不然el中将不会有子元素。查看具体用法
P.S. 当然在Vue中有着更复杂的操作,比如异步处理,安全校验等等,这里只是对其响应式做了一些简单的分析,难免有些错漏,还请各位大佬们指教。
项目地址:https://github.com/lwy121810/VueReactiveImpl
写在最后:
2020年了,希望在这一年有所成长。元旦快乐!
相关阅读: