研究的原动力
自学Vue已经有一段时间了,基本上都停留在使用的层面上,但是出于好奇心,想知道Vue内部作者是如何实现数据的双向绑定以及如何做到指令和模板的编译,于是我抽时间查了一下相关前辈的博客以及部分公开课视频的研习,大概了解了其原理,Vue的内部大概实现了 Observe(劫持所有属性的类)、Compile(解析指令和模板的类)、Dep(存储Watcher以及通知变化)、Watcher(绑定更新函数以及更新视图),但是当时只是知道有这几种类,他们之间是如何工作的以及如何关联起来工作,这些都比较模糊,但是经过本人不懈的研读以及调试,逐渐清晰,所以此处做一下记录,希望能够帮助有需求的伙伴们。
理论总结及原理展望
Vue采用数据劫持结合发布者-订阅者模式的方法,通过Object.defineProperty()来劫持各个属性的setter,getter属性,通过 Complie 来解析编译模板指令,在模板编译处注入Watcher的回调方法,当数据变化时,通过Dep的noty方法去出发Watcher的updata()更新方法,然后callBack之前模板中注入的回调方法,之后渲染新值。
流程图
重要组成类
- Observer
作用:主要是对Vue中的data下的所有属性进行监听以及Dep的关联逻辑
class Observer {
constructor(data) {
this.observer(data)
}
observer(data) {
if (data && typeof data === 'object') {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
}
defineReactive(data, key, value) {
// 递归遍历
this.observer(value)
const dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
// 订阅数据变化时,向Dep中添加观察者
Dep.target && dep.addSub(Dep.target)
return value
},
set: newValue => {
this.observer(newValue)
if (value !== newValue) {
value = newValue
}
// 告诉Dep通知变化
dep.noty()
}
})
}
}
- Compile
作用:主要是进行模板和指令的编译和解析以及创建Watcher和实现更新的回调逻辑处理
class Complie {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
if (this.el) {
// 1.获取文档碎片对象,放入内存中,会减少页面的回流和重绘
let fragment = this.nodeFragment(this.el)
// 2.编译模板
this.complie(fragment)
// 3.追加子元素到根元素上
this.el.appendChild(fragment)
}
}
isElementNode(node) {
return node.nodeType === 1
}
nodeFragment(el) {
el.firstChild
// 创建一个内存碎片对象
const fragment = document.createDocumentFragment()
let firstChild
while ((firstChild = el.firstChild)) {
fragment.appendChild(firstChild)
}
return fragment
}
// 遍历获取并区分元素节点还是文本节点,然后进行相应的处理
complie(fragment) {
// 1.获取到每个子节点
const childNodes = fragment.childNodes
childNodes.forEach(child => {
if (this.isElementNode(child)) {
// 是元素节点
// 编译元素节点
this.complieElement(child)
} else {
// 文本节点
// 编译文本节点
this.complieText(child)
}
if (child.childNodes && child.childNodes.length) {
this.complie(child)
}
})
}
// 编译元素
complieElement(node) {
//
const attributes = node.attributes
Array.from(attributes).forEach(attr => {
const { name, value } = attr
if (this.isDirective(name)) {
// 表明是一个指令
const [, dirctive] = name.split('-')
const [dirName, eventName] = dirctive.split(':') // text html model on
/*
node(整个节点 )
value(msg)
this.vm(相当于整个 MVue实例对象)
eventName(v-on:click='btnClick') 中的事件名btnClick
*/
// 更新数据 数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName)
// 删除有指令的标签上的属性
node.removeAttribute('v-' + dirctive)
} else if (this.isEventName(name)) {
// @click='handleClick'
let [, eventName] = name.split('@')
compileUtil['on'](node, value, this.vm, eventName)
}
})
}
isEventName(eventName) {
return eventName.startsWith('@')
}
// 检测字符串是否以 v- 开头
isDirective(attrName) {
return attrName.startsWith('v-')
}
// 编译文本
complieText(node) {
const content = node.textContent
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil['text'](node, content, this.vm)
}
}
}
- compileUtil
作用:主要是实现Complie中的处理逻辑抽离,以便于复用处理
const compileUtil = {
getVal(expr, vm) {
// console.log(expr.split('.')); ["person", "name"]
return expr.split('.').reduce((data, currentVal) => {
// console.log('aaaa' + JSON.stringify(data[currentVal]));
return data[currentVal]
}, vm.$data);
},
setVal(expr, vm, inputVal) {
return expr.split('.').reduce((data, currentVal) => {
data[currentVal] = inputVal
}, vm.$data)
},
getContentVal(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(args[1].trim(), vm)
})
},
text(node, expr, vm) {
//
let value
if (expr.indexOf('{{') !== -1) {
// {{msg}} ---{{person.name}}
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], () => {
this.updater.textUpdater(node, this.getContentVal(expr, vm))
})
return this.getVal(args[1].trim(), vm)
})
} else {
value = this.getVal(expr, vm)
}
this.updater.textUpdater(node, value)
},
html(node, expr, vm) {
const val = this.getVal(expr, vm)
new Watcher(vm, expr, newVal => {
this.updater.htmlUpdater(node, newVal)
})
this.updater.htmlUpdater(node, val)
},
model(node, expr, vm) {
const val = this.getVal(expr, vm)
// 绑定更新函数 数据 =>视图
new Watcher(vm, expr, newVal => {
this.updater.modelUpdater(node, newVal)
})
// 视图 =>数据=>视图
node.addEventListener('input', e => {
this.setVal(expr, vm, e.target.value)
})
this.updater.modelUpdater(node, val)
},
on(node, expr, vm, eventName) {
let fn = vm.$options.methods && vm.$options.methods[expr]
node.addEventListener(eventName, fn.bind(vm), false)
},
// 更新的函数
updater: {
textUpdater(node, value) {
node.textContent = value
},
htmlUpdater(node, value) {
node.innerHTML = value
},
modelUpdater(node, value) {
node.value = value
}
}
}
- Dep
作用:主要是收集 Watcher以及实现数据更新时的通知回调方法
class Dep {
constructor() {
this.subs = []
}
// 收集观察者
addSub(watcher) {
this.subs.push(watcher)
}
// 通知观察者去更新
noty() {
this.subs.forEach(w => w.update())
console.log(this.subs)
}
}
- Watcher
作用: 连接Dep和Compile,接受每个属性变动的通知,绑定更新函数,通过Dep的数据变化通知,然后遍历所有Dep中的Watcher的updater的更新方法,从而回调给Compile中的cb函数,从而实现视图的更新
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
this.oldVal = this.getOldVal()
}
getOldVal() {
Dep.target = this
const oldVal = compileUtil.getVal(this.expr, this.vm)
Dep.target = null
return oldVal
}
update() {
const newVal = compileUtil.getVal(this.expr, this.vm)
if (newVal != this.oldVal) {
this.cb(newVal)
}
}
}
- MVue
作用:我们封装的js的调用
class MVue {
constructor(options) {
this.$el = options.el
this.$data = options.data
this.$options = options
if (this.$el) {
// 1.实现数据的观察者
new Observer(this.$data)
// 2.实现指令的解析器
new Complie(this.$el, this)
}
}
}
// 如何使用自己封装的js
let vm = new MVue({
el: '#app',
data: {
person: {
name: '小明',
age: 18,
fav: '撸代码'
},
msg: '学习MVVM实现原理',
htmlStr: '大家好,你们学的怎么样?
'
},
methods: {
btnClick() {
this.$data.person.name = '学习前端'
}
}
})
总结
以上大概是我整个的代码实现,如果有不完善的地方,望留言指正,如果对您理解Vue的响应式原理有帮助的话,希望点个赞,让我们共同成长。。。
GitHub地址:https://github.com/wzXJF/Vue-Org.git(此处有完整的代码)