大家好,我是王大傻。最近在公司做分享时候,发现了大家对Vue的响应式原理理解并不透彻,索性就给公司同事讲了响应式原理,并略有心得,所以这就来分享一波(第一次写文章,有什么不到位的地方可以留言告诉我)
分析
首先呢,我们可以看下响应式原理这个图。当我们创建Vue实例时候,我们做了两件事
- 数据劫持
- 解析指令 在数据劫持这条流程中,当我们数据劫持后,我们会将数据发生的变化及时反馈给我们的Dep实例,此时Dep(目标发布者)会给我们的Watcher(观察者)发生通知,而我们的Watcher在接到通知后调用相关的函数去更新视图。到此,数据劫持算是也进行完毕。等等,我们还有另外一条线路。 在外面解析这条线路中,我们首先将指令发送给我们的Compiler进行解析,完毕后我们直接渲染到视图层去替代我们之前的插值表达式,去订阅数据的具体变化,并将绑定我们的更新函数,此时Watcher将会去我们的Dep上添加订阅者。
观察者模式
在讲原理前,首先我们需要先了解一下观察者模式是什么。
如图所示,被观察者也就是我们的发布者可以同时支持多个观察者,而被观察者主要是用来注册我们的观察实例,一有结果就马上通知我们的观察者。
// # 发布者
class Dep {
constructor() {
this.subs = []// 存储我们的观察者
}
addSub(sub) {
if (sub && sub.update) {// 判断是否有观察者函数 update为观察者中的函数
this.subs.push(sub)
}
}
notify() {
this.subs.forEach(sub => {
sub.update()// 执行我们观察者函数
})
}
}
// # 观察者
class Watcher {
constructor() {
update()
{
console.log('update')
}
}
}
let dep = new Dep()
let watch = new Watcher()
dep.addSub(watch)// 注册我们观察实例
dep.notify() // 通知到我们的观察者
清晰了我们观察者模式后,我们再来思考下在Vue中我们具体的使用吧
第一步 创建Vue类
首先最最最重要的肯定是我们需要初始化一个Vue类
let vm=new Vue({
el:'#app',
data:{
msg:'hello',
title:'你好'
}
})
console.log(vm)
这里的Vue是我们自己创建的实例,首先分析
- Vue实例里面接受了一个对象
- 对象里面包含el和data两个参数
- 那么我们Vue具有什么功能呢
- 负责接收初始化参数
- 负责把data中的属性注入到Vue实例 转换成getter/setter
- 负责调用observer监听data中所有属性的变化
- 负责调用compiler解析指令/表达式 那么我们依据我们所了解的功能先去初始化实例
class Vue {
constructor(options) {
//1. 通过属性保存选项的数据
this.$options = options
this.$data = options.data || {}
this.$el = typeof options.el === "string"
? document.querySelector(options.el)
: options.el
//2. 把data中的成员转换为getter setter 注入到vue实例中
this._proxyData(this.$data)
//3. 调用observer对象 监听数据的变化
new Observer(this.$data)
//4. 调用compiler对象 解析指令和插值表达式
new Compiler(this)
}
_proxyData(data) {
// 遍历data中所有的属性
Object.keys(data).forEach(key => {
// 把data的属性注入到vue实例中
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(newValue) {
if (newValue === data[key]) {// 判断导入值是否和当前值相等 相等了我们就不做替换
return
}
data[key] = newValue
}
})
})
}
}
第二步 创建Observer
接下来,我们需要创建一个Observer类,用来为我们Vue中的data属性设置setter、getter 那么我们分析一下它的功能
- 负责把data选项中的属性转换为响应式数据
- data中的某个属性也是对象 把该属性转换为响应式数据
- 数据变化发送通知 我们依据它的功能来实现一下
class Observer {
constructor(data) {// 接收我们在Vue实例中传过来的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])
})
}
defineReactive(data, key, value) {
const self = this// 保存我们的指针 使当前this指向我们的实例
let dep = new Dep()// 在创建时候先去生成一个发布者实例
this.walk(value)// 如果是属性的话 不会处理 如果是对象的话 会对对象也做监听
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 判断我们的Dep实例中是否有target属性 并将target加入到我们的发布者存储中
Dep.target && dep.addSub(Dep.target)
// 当前我们预留了这段代码 后续在Watcher中为Dep添加target属性
return value
},
set(newVal) {
if (newVal === value) {
return
}
value = newVal
self.walk(newVal)
dep.notify()// 当我们去设置新的值的时候 发送通知
}
})
}
}
至此我们可以在控制台上打印下我们的vm实例
- 需要注释当前未声明的类及对类的相关使用,如Vue中的Compiler Observer中的Dep
第三步 创建我们的Compiler类
到此为止呢,我们已经初始化了Vue实例并通过Observer去给我们实例上的数据挂载到我们Vue上并添加了getter、setter方法,那么接下来到了我们的重点,Compiler类,那么它又有什么功能呢。
- 负责编译模板,解析指令和插值表达式
- 负责页面的首次渲染
- 当数据变化后重新渲染视图 说到编译模板,那我们就需要对标签进行一些操作了,在此我们实现了插值表达式以及v-text v-model的填充运算
如图所示 我们需要做的内容就是
- 把插值表达式部分替换成我们data中的具体数据
- v-text绑定的变量替换为我们data中的具体数据
- v-model绑定的表单数据我们也要替换为具体数据,并且在更改数据同时也要及时响应到页面中。
那么根据上述我们创建Compiler类
class Compiler {
constructor(vm) {
this.el = vm.$el
this.vm = vm
}
// 编译模板 处理文本和元素节点
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)
}
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 编译元素节点 处理指令
compileELement(node) {
Array.from(node.attributes).forEach(attr => {
let attrName = attr.name
if (this.isDerictive(attrName)) {
attrName = attrName.substr(2)
let key = attr.value
this.update(node, key, attrName)
}
})
// 遍历所有的节点
// 判断是否是指令
}
// 判断需要执行的方法
update(node, key, attrName) {
let fn = this[attrName + 'Update']
fn && fn.call(this, node, this.vm[key], key)
// 此处需要用call来改变我们的this指向 因为我们将函数存储为变量时 this指针此时指向了window对象
}
// 处理v-text指令
textUpdate(node, val, key) {
node.textContent = val
new Watcher(this.vm, key, (newVal) => {
node.textContent = newVal
})
}
// 处理v-model指令
modelUpdate(node, val, key) {
node.value = val
new Watcher(this.vm, key, (newVal) => {
node.value = newVal
})
// 双向绑定
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
// 编译文本节点 处理插值表达式
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])
// 创建watcher对象 属性改变时候更新视图
new Watcher(this.vm, key, (newVal) => {
// 写入我们的watcher 方法 在处理节点时添加上我们的观察者
node.textContent = newVal
})
}
}
// 判断元素是否是指令
isDerictive(attrName) {
return attrName.startsWith('v-')
}
// 判断节点是否是文本节点
isTextNode(node) {
return node.nodeType === 3
}
// 判断是否为元素节点
isElementNode(node) {
return node.nodeType === 1
}
}
对于代码中的某些判定,我们将单独解释下
- 我们需要判断两种节点(文本节点和元素节点)
- 在文本节点中 通过判断nodetype===3 并可以通过textContent进行赋值
- 在元素节点中 通过判断nodetype===1 如果是表单元素那么可以通过value进行赋值 如果不是 我们可以取得attributes 并通过textContent赋值 文本节点
元素节点
至此我们页面上的插值表达式以及 v-text v-model已经转换为我们对应的数据了
第四步 创建我们的观察者模式完成最后的操作
在之前我们对观察者模式已经做了相应的介绍,所以话不多说,我们直接走起 首先是我们的发布者
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
其次是我们的观察者 那么在我们观察者中根据此时的处理 我们需要的功能如下
- 当数据变化时候触发依赖 dep通知Watcher实例更新视图
- 自身变化时候在dep中添加自己
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
// 当前的watcher记录到dep的target中
// 触发get方法 在get中addSub
Dep.target = this
this.oldValue = vm[key]
Dep.target = null// 在最后对target进行回收操作 避免多次添加
}
update() {
let newValue = this.vm[this.key]
if (this.oldValue === newValue) {
return
}
this.cb(newValue)
}
}
至此,我们的Vue响应式原理就大功告成了,让我们在页面中跑一下看看吧
首先我们按照类之间互相依赖的顺序将文件引入到html页面中
我们运行到页面中的结果
当我们在输入框中输入数据时候触发了双向绑定
over,至此我们明白了响应式原理的简单版运作方法。如果大家有什么疑问,可以在评论区留言。我会一一解答,如果有哪块做的不好的,欢迎大家指正