/**
实现一个数据劫持 - Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
实现一个模板编译 - Compiler,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
实现一个 - Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
MVVM 作为入口函数,整合以上三者
**/
/**
// 数据劫持 - Observer
Observer 类主要目的就是给 data 数据内的所有层级的数据都进行数据劫持,让其具备监听对象属性变化的能力
【重点】:
1.当对象的属性值也是对象时,也要对其值进行劫持 --- 递归
2.当对象赋值与旧值一样,则不需要后续操作 --- 防止重复渲染
3.当模板渲染获取对象属性会调用get添加target,对象属性改动通知订阅者更新 --- 数据变化,视图更新
*/
class Observer {
constructor(data) {
this.observer(data)
}
observer(data) {
// 判断是否是对象 是才观察
if (typeof data === 'object' && data != null) {
for (let key in data) {
this.defineReactive(data, key, data[key])
}
}
}
defineReactive(data, key, value) {
let dep = new Dep()
this.observer(value) // 如果值也是对象,继续观察
Object.defineProperty(data, key, {
// 获取值
get() {
Dep.target && Dep.addSub(Dep.target)
return value
},
// 设置值
set(newValue) {
// 新值与旧值不一样才替换,一样就不用替换
if (newValue != value) {
this.observer(newValue) // 如果值是对象继续观察
value = newValue
dep.notify() // 通知所有订阅者更新了
}
}
})
}
}
/**
* 模板编译 - Compiler
* Compiler 是解析模板指令,将模板中的变量替换成数据,
* 然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,
* 添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
* Compiler 主要做了三件事:
* 1.将当前根节点所有子节点遍历放到内存中
2.编译文档碎片,替换模板(元素、文本)节点中属性的数据
3.将编译的内容回写到真实DOM上
【重点】:
1.先把真实的 dom 移入到内存中操作 --- 文档碎片
2.编译 元素节点 和 文本节点
3.给模板中的表达式和属性添加观察者
*/
class Compiler {
// el 为元素节点,el选项中有可能是‘#app’字符串也有可能是document.getElementById('#app')
// vm 实列
constructor(el, vm) {
// 判断el是否是元素,不是就获取
this.el = this.isElmentNode(el) ? el : document.querySelector(el)
this.vm = vm
// 把当前节点的元素获取放到内存中
let fragment = this.nodeToFragment(this.el)
// 1. 编译模板 用data中的数据编译, 2. 把内存中的内容进行替换
this.compile(fragment)
// 3. 再把替换后的内容回写到页面中
this.el.appendChild(fragment)
}
// 判断是不是元素
isElmentNode(node) {
return node.nodeType === 1
}
// 判断是否是指令
isDirective(attrName) {
return attrName.startsWith('v-') // 是否含有v-
}
// 将节点添加到内存中
nodeToFragment(node) {
// 创建一个文档片段接口 目的是为了将这个节点中的每个孩子都写到这个文档碎片中
let fragment = document.createDocumentFragment()
let firstChild // 这个节点中的第一个孩子
while (firstChild = node.firstChild) {
// appendChild具有移动性,每移动一个节点到内存中,页面上就会少一个节点
fragment.appendChild(firstChild)
}
return fragment
}
// 编译内存中的dom节点
compile(fragmentNode) {
let childNodes = fragmentNode.childNodes // 获取的是类数组NodeLis
let childArr = [...childNodes] // 转化为真实数据, Array.from [...xxx] [].slice.call
childArr.forEach(child => {
// 是否是元素节点
if (this.isElmentNode(child)) {
this.compileElement(child)
this.compile(child) // 如果是元素的话 需要把自己传进去 再去遍历子节点 递归
} else {
// 文本节点
this.compileText(child)
}
})
}
// 文件节点的编译
compileText(node) {
let content = node.textContent // 获取内容
// 通过正则去匹配只需要含有{{}}大括号的,空的不需要 获取大括号中间的内容
if (/\{\{(.+?)\}\}/.test(content)) {
// 需要文本来处理
CompilerUnit['text'](node, content, this.vm)
}
}
// 元素节点的编译
compileElement(node) {
let attrbutes = node.attrbutes // 获取元素节点的属性
let attrbutesArr = [...attrbutes]
attrbutesArr.forEach(attr => {
// attr格式:type="text" v-model="obj.name"
let [name, expr] = attr.split('=')
// 判断是否是指令 v-model v-bind v-html等
if (this.isDirective(name)) {
let [, directive] = name.split('-')
// 需要调用不同的指令来处理
CompilerUnit[directive](node, expr, this.vm)
}
})
}
}
const CompilerUnit = {
// 根据表达式获取对应数据
getVal(vm, expr) {
return expr.split('.').reduce((data, current) => {
return data[current]
}, vm.$data)
},
// 设置值
setVal(vm, expr, value) {
expr.split('.').reduce((data, current, index, arr) => {
if (index === arr.length - 1) {
return data[current] = value
}
return data[current]
}, vm.$data)
},
// 文本节点内容
getContentValue(vm, expr) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(vm, args[1].trim())
})
},
// 处理v-model node 节点 expr表达式 vm当前实列
model(node, expr, vm) {
// 给输入框赋予value属性 node.value = xxx
let fn = this.updater['modelUpdater']
//给输入框加一个观察者 数据更新会触发此方法 会拿新值给 输入框赋值
new Watcher(vm, expr, newValue => {
fn(node, newValue)
})
node.addEventListener('input', e => {
let value = e.target.value
this.setVal(vm, expr, value)
})
let value = this.getVal(vm, expr)
fn(node, value)
},
// 处理文本 node 节点, expr表达式 vm 实列
text(node, expr, vm) {
let fn = this.updater['textUpdater']
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 给表达式 {{}} 加上观察者
new Watcher(vm, args[1], () => {
fn(node, this.getContentValue(vm, expr)) // 返回了一个新的字符串
})
return this.getVal(vm, args[1].trim())
})
fn(node, content)
},
updater: {
// 把数据插入到节点中
modelUpdater(node, value) {
node.value = value
},
// 处理文本
textUpdater(node, value) {
node.textContent = value
}
}
}
/**
* 发布订阅 - Watcher
* Watcher 订阅者作为 Observer 和 Compile 之间通信的桥梁,主要做的事情是:
* 1.在自身实例化时往属性订阅器(dep)里面添加自己
2.自身必须有一个update()方法
3.待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
*
* Dep 和 Watcher 是简单的观察者模式的实现,Dep 即订阅者,它会管理所有的观察者,并且有给观察者发送消息的能力。
* Watcher 即观察者,当接收到订阅者的消息后,观察者会做出自己的更新操作。
*/
// Dep 即订阅者,它会管理所有的观察者,并且有给观察者发送消息的能力。
class Dep {
constructor() {
this.subs = []
}
// 订阅
addSub(watcher) {
this.subs.push(watcher)
}
// 发布
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
// 观察者 当接收到订阅者的消息后,观察者会做出自己的更新操作
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr // 添加到订约中
this.cb = cb
this.oldValue = this.get() // 存放一个旧值
}
get() {
Dep.target = this // 将当前的watcher放入Dep.target
let value = CompilerUnit.getVal(this.vm, this.expr)
Dep.target = null
return value
}
update() {
let newValue = CompilerUnit.getVal(this.vm, this.expr)
if (newValue != this.oldValue) {
this.cb(newValue)
}
}
}
/**
* 整合 - MVVM
* MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,
* 通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,
* 达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
*
*/
class MVVM {
constructor(options) {
// 当new该类时,参数就会传到构造函数中 options就是el data computed ...
this.$el = options.el
this.$data = options.data
// 判断根元素是否存在 <div id='app'></div> => 编译模板
if (this.$el) {
// 把data里的数据 全部转化成用Object.defineProperty来定义
new Observer(this.$data)
new Compiler(this.$el, this)
}
}
}
// 扩展 - 实现computed
// computed 具有缓存功能,当依赖的属性发送变化,才会更新视图变化
function initComputed() {
let vm = this // 将当前this挂载到vm
let computed = this.$options.computed // 从options上拿到computed属性
// 得到的都是对象的key可以通过Object.keys转化为数组
Object.keys(computed).forEach(key => {
Object.defineProperty(vm, key, {
// 判断是computed里的key是对象还是函数
// 若是函数,则直接就调get方法
// 若是对象,则需要手动调一下get方法
// 因为computed只根据依赖的属性进行触发,当获取依赖属性时,系统会自动的去调用get方法,所以就不要用Watcher去监听变化了
get() {
return typeof computed[key] === 'function' ? computed[key] : computed[key].get
},
set() {
}
})
})
}
console