数据驱动视图
通过 ViewModel 在 Model(模型) 修改的时候触发 View(视图) 渲染,
View 中事件(点击等)修改 model 层数据
1、发布订阅者
2、脏值检查
3、数据劫持
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm入口函数,整合以上三者
<div id="app">
<input type="text" v-model="message.a"/>
{{message.a}}
div>
let vm = new MVVM({
el: '#app',
data: {
message: {
a: 'hello'
}
}
})
使用Vue的时候会new Vue,需要创建一个类,因此先初始化一个MVVM类
class MVVM {
constructor (options) {
// 一上来先将可用的东西挂载在实例上
this.$el = options.el
this.$data = options.data
// 如果有要编译的模版就开始编译
if (this.$el) {
// 数据劫持 就是把对象方法里面的所有属性改成get和set
new Observer(this.$data)
this.proxyData(this.$data)
// 用数据和元素进行编译
new Compile(this.$el, this)
}
}
proxyData (data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get () {
return data[key]
},
set (newValue) {
data[key] = newValue
}
})
})
}
}
实现Oberve,监听属性的变化
class Observer {
constructor (data) {
this.observer(data)
}
observer (data) {
// 要对这个data数据原有属性改写成get和set
if (!data || typeof data !== 'object') {
return
}
// 要将数据--劫持,先获取取到data的key和value
Object.keys(data).forEach(key => {
// 劫持
this.defineReactive(data, key, data[key])
this.observer(data[key])
})
}
// 定义响应式
defineReactive(obj, key, value) {
let that = this
let dep = new Dep() // 每个变化的数据都会对应一个数组,这个数组是存放所有更新的操作
// 在获取某个值的时候做点事
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () { // 当取值是调用的方法
Dep.target && dep.addSub(Dep.target)
return value
},
set (newValue) { // 当给data属性中设置值的时候,更改获取的属性的值
if (newValue != value) { // 这里的this不是vm
that.observer(newValue)
value = newValue
dep.notify() // 通知所有人数据更新了
}
}
})
}
}
class Dep {
constructor () {
// 订阅的数组
this.subs = []
}
// 添加订阅
addSub (watcher) {
this.subs.push(watcher)
}
// 通知
notify () {
this.subs.forEach(watcher => watcher.update())
}
}
compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
class Compile {
constructor (el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
if (this.el) {
// 如果能获取到该元素,我们才开始编译
// 1、先把这些真实的DOM移入到内存中 fragment
let fragment = this.node2fragment(this.el)
// 2、编译 => 提取想要的元素节点 v-model 和文本节点 {{}}
this.compile(fragment)
// 3、把编译好的fragment再塞回到页面中
this.el.appendChild(fragment)
}
}
/* 专门写一些辅助方法 */
isElementNode (node) {
return node.nodeType === 1
}
// 是不是指令
isDirective (name) {
return name.includes('v-')
}
/* 核心方法 */
compileElement (node) {
// 带v-model
let attrs = node.attributes
Array.from(attrs).forEach(attr => {
// 判断属性名字是不是包含v-model [v, model]
let attrName = attr.name // v-model
if (this.isDirective(attrName)) {
// 取到对应的值放到节点中
let expr = attr.value
let [, type] = attrName.split('-')
// node this.vm.$data expr
CompileUtil[type](node, this.vm, expr)
}
})
}
compileText (node) {
let expr = node && node.textContent // 取文本的内容
let reg = /\{\{([^}]+)\}\}/g // {{a}} {{b}} {{c}}
if (reg.test(expr)) {
// node this.vm.$data expr
CompileUtil['text'](node, this.vm, expr)
}
}
compile (fragment) {
// 获取全部节点
let childNodes = fragment.childNodes
Array.from(childNodes).forEach(node => {
if (this.isElementNode(node)) {
// 是元素节点,还需要继续深入的检查
// 这里需要编译元素
this.compileElement(node)
this.compile(node)
} else {
// 文本节点
// 这里需要编译文本
this.compileText(node)
}
})
}
node2fragment (el) { // 需要将el中的内容全部放到内存中
// 文档碎片,不是真实的DOM,是内存中的
let fragment = document.createDocumentFragment()
let firstChild
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild)
}
return fragment // 内存中的节点
}
}
CompileUtil = {
getVal (vm, expr) { // 获取实例上对应的数据
expr = expr.split('.')
return expr.reduce((prev, next) => { // vm.$data.a
return prev[next]
}, vm.$data)
},
getTextVal (vm, expr) { // 获取编译文本后的结果
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
return this.getVal(vm, arguments[1])
})
},
text (node, vm, expr) { // 文本处理
let updateFn = this.updater['textUpdater']
// message.a => [meaasge, a] vm.$data.message.a
let value = this.getTextVal(vm, expr)
// {{a}} {{b}} 这样的文本两个都要建立观察者
expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
new Watcher(vm, arguments[1], (newValue) => {
// 如果数据变化了,文本节点需要重新获取依赖的数据更新文本中的内容
updateFn && updateFn(node, this.getTextVal(vm, expr))
})
updateFn && updateFn(node, value)
})
},
setVal (vm, expr, value) { // [message, a]
expr = expr.split('.')
return expr.reduce((prev, next, currentIndex) => {
if (currentIndex === expr.length - 1) {
return prev[next] = value
}
return prev[next]
}, vm.$data)
},
model (node, vm, expr) { // 输入框处理
let updateFn = this.updater['modelUpdater']
// 这里应该加一个监控,数据变化了应该调用watch的callback
new Watcher(vm, expr, (newValue) => {
// 当值变化后会调用cb将新值传递过来()
updateFn && updateFn(node, this.getVal(vm, expr))
})
node.addEventListener('input', e => {
let newValue = e.target.value
this.setVal(vm, expr, newValue)
})
updateFn && updateFn(node, this.getVal(vm, expr))
},
updater: {
// 文本更新
textUpdater (node, value) {
node.textContent = value
},
// 输入框更新
modelUpdater (node, value) {
node.value = value
}
}
}
创建观察者累,目的就是给需要变化的那个元素添加一个观察者,当数据变化后执行对应的方法
class Watcher {
constructor (vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
// 先获取以下老的值
this.value = this.get()
}
getVal (vm, expr) { // 获取实例上对应的数据
expr = expr.split('.')
return expr.reduce((prev, next) => { // vm.$data.a
return prev[next]
}, vm.$data)
}
get () {
Dep.target = this
let value = this.getVal(this.vm, this.expr)
Dep.target = null
return value
}
// 对外暴露方法
update () {
let newValue = this.getVal(this.vm, this.expr)
let oldValue = this.value
if (newValue != oldValue) {
this.cb(newValue) // 调用watch的callback
}
}
}