console
// 各种类型指令的编译策略
const compileUtil = {
getVal(expr, vm) {
// person.name -> [person, name] -> vm.$data['person'] -> currentData['name']
return expr.split('.').reduce((data, key) => data[key], vm.$data)
},
setVal(expr, vm, inputVal) {
const propsArr = expr.split('.')
const targetProp = propsArr.pop()
const target = propsArr.reduce((data, key) => data[key], vm.$data)
target[targetProp] = inputVal
},
getContentValue(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(args[1], vm)
})
},
text(node, expr, vm) {
// v-text="text" {{person.name}}
let value
if (expr.indexOf('{{') !== -1) {
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], () => {
this.updater.textUpdater(node, this.getContentValue(expr, vm))
})
return this.getVal(args[1], vm)
})
} else {
value = this.getVal(expr, vm)
new Watcher(vm, expr, (newValue) => {
this.updater.textUpdater(node, newValue)
})
}
this.updater.textUpdater(node, value)
},
html(node, expr, vm) {
const value = this.getVal(expr, vm)
new Watcher(vm, expr, (newValue) => {
this.updater.htmlUpdater(node, newValue)
})
this.updater.htmlUpdater(node, value)
},
model(node, expr, vm) {
const value = this.getVal(expr, vm)
new Watcher(vm, expr, (newValue) => {
this.updater.modelUpdater(node, newValue)
})
node.addEventListener('input', (event) => {
this.setVal(expr, vm, event.target.value)
})
this.updater.modelUpdater(node, value)
},
on(node, expr, vm, eventName) {
const fn = vm.$options.methods && vm.$options.methods[expr]
node.addEventListener(eventName, fn.bind(vm))
},
updater: {
textUpdater(node, value) {
node.textContent = value
},
htmlUpdater(node, value) {
node.innerHTML = value
},
modelUpdater(node, value) {
node.value = value
}
}
}
// 数据观察者
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
this.oldValue = this.getOldValue()
}
getOldValue() {
Dep.target = this
const value = compileUtil.getVal(this.expr, this.vm)
Dep.target = null
return value
}
update() {
const newValue = compileUtil.getVal(this.expr, this.vm)
if (newValue !== this.oldValue) {
this.cb(newValue)
}
}
}
// 数据与视图的依赖收集器
class Dep {
constructor() {
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(w => {
w.update()
})
}
}
// Observer 用于劫持数据,在 getter 中收集依赖,setter 触发依赖更新
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(obj, key, value) {
this.observer(obj[key])
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
get() {
Dep.target && dep.addSub(Dep.target)
return value
},
set: (newValue) => {
// 如果设置的是一个对象,需要重新观察
this.observer(newValue)
if (newValue !== value) {
value = newValue
dep.notify()
}
}
})
}
}
// 编译器,用于编译模板,解析指令
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
// 将模板节点添加到文档碎片,减少性能消耗
const fragment = this.node2Fragment(this.el)
// 开始编译
this.compile(fragment)
this.el.appendChild(fragment)
}
compile(fragment) {
const childNodes = fragment.childNodes
// 遍历子节点,判断节点的类型执行不同的编译方式
;[...childNodes].forEach(child => {
if (this.isElementNode(child)) {
this.compileElement(child)
} else {
this.compileText(child)
}
// 递归遍历所有子节点
if (child.childNodes && child.childNodes.length) {
this.compile(child)
}
})
}
// 编译 element 节点类型
compileElement(node) {
const attrs = node.attributes
;[...attrs].forEach(attr => {
const { name, value } = attr
if (this.isDirective(name)) {
//v-text v-html v-model v-on:click -> text html model on:click
const [, directive] = name.split('-')
// on:click -> on click
const [dirName, eventName] = directive.split(':')
compileUtil[dirName] && compileUtil[dirName](node, value, this.vm, eventName)
node.removeAttribute('v-' + directive)
}
})
}
// 编译文本类型
compileText(node) {
const content = node.textContent
// {{person.name}}
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil.text(node, content, this.vm)
}
}
isDirective(name) {
return name.startsWith('v-')
}
node2Fragment(node) {
const fragment = document.createDocumentFragment()
let firstChild
while (firstChild = node.firstChild) {
fragment.appendChild(firstChild)
}
return fragment
}
isElementNode(node) {
return node.nodeType === 1
}
}
class MVue {
constructor(options) {
this.$el = options.el
this.$data = options.data
this.$options = options
if (this.$el) {
new Observer(this.$data)
new Compile(this.$el, this)
this.proxyData(this.$data)
}
}
// 代理 data 数据,实现 this.xx 访问和修改数据
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newVal) {
data[key] = newVal
}
})
})
}
}
window.vueInstance = new MVue({
el: '#app',
data: {
person: {
name: 'Zhang San',
age: 20,
fav: 'Game'
},
htmlContent: `<em>User data:</em>`
},
methods: {
handleClick() {
this.person.name = 'Li Si'
}
}
})
<div id="app">
<h1 v-text="person.name"></h1>
<h4 v-html="htmlContent"></h4>
<ul>
<li>Age: {{person.age}}</li>
<li>Fav: {{person.fav}}</li>
</ul>
<input type="text" v-model="person.name" />
<button v-on:click="handleClick">changeName</button>
</div>