简易 Dep/Watcher
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="root">
<div class="c1">
<div title="tt1" id="id">233</div>
<div title="tt2">{{ age }}</div>
<div title="tt3">{{ gender }}</div>
<ul>
<li>111{{name.firstName}}-{{age}}</li>
<li>{{name.lastName}}</li>
<li>{{numArr}}</li>
<li>{{someThing.aaa}}</li>
<li>3</li>
</ul>
</div>
</div>
<script>
let depid = 0
class Dep {
constructor() {
this.id = depid++
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
for (let i = this.subs.length - 1; i >= 0; i--) {
if (sub === this.subs[i]) {
this.subs.splice(i, 1)
}
}
}
depend() {
if (Dep.target) {
this.addSub(Dep.target)
Dep.target.addDep(this)
}
}
notify() {
let deps = this.subs.slice()
deps.forEach((watcher) => {
watcher.update()
})
}
}
Dep.target = null
let targetStack = []
function pushTarget(target) {
targetStack.unshift(target)
Dep.target = target
}
function popTarget() {
targetStack.shift()
Dep.target = targetStack[targetStack.length - 1]
}
let watcherid = 0
class Watcher {
constructor(vm, expOrfn) {
this.vm = vm
this.getter = expOrfn
this.id = watcherid++
this.deps = []
this.depIds = {}
this.get()
}
get() {
pushTarget(this)
this.getter.call(this.vm, this.vm)
popTarget()
}
run() {
this.get()
}
update() {
this.run()
}
cleanupDep() {}
addDep(dep) {
this.deps.push(dep)
}
}
class VNode {
constructor(tag, data, value, type) {
this.tag = tag && tag.toLowerCase()
this.data = data
this.value = value
this.type = type
this.children = []
}
appendChild(vnode) {
this.children.push(vnode)
}
}
function getVNode(node) {
const type = node.nodeType
let _vnode = null
if (type === 1) {
const tag = node.nodeName
const attrs = node.attributes
const data = {}
for (let i = 0; i < attrs.length; i++) {
data[attrs[i].nodeName] = attrs[i].nodeValue
}
const value = undefined
_vnode = new VNode(tag, data, value, type)
const children = node.childNodes
for (let i = 0; i < children.length; i++) {
_vnode.appendChild(getVNode(children[i]))
}
} else if (type === 3) {
_vnode = new VNode(undefined, undefined, node.nodeValue, type)
}
return _vnode
}
function parseVNode(vnode) {
let oDom = null
if (vnode.type === 1) {
oDom = document.createElement(vnode.tag)
const data = vnode.data
Object.keys(data).forEach((key) => {
let attrName = key
let attrValue = data[key]
oDom.setAttribute(attrName, attrValue)
})
for (let i = 0; i < vnode.children.length; i++) {
oDom.appendChild(parseVNode(vnode.children[i]))
}
} else if (vnode.type === 3) {
oDom = document.createTextNode(vnode.value)
}
return oDom
}
function getValueByKey(obj, str) {
const arr = str.split('.')
let prop = null
while ((prop = arr.shift())) {
obj = obj[prop]
}
return obj
}
const reg = /\{\{(.+?)\}\}/g
function combine(vnode, data) {
let _type = vnode.type
let _data = vnode.data
let _value = vnode.value
let _tag = vnode.tag
let _children = vnode.children
let _vnode = null
if (_type === 3) {
_value = _value.replace(reg, function (_, g) {
return getValueByKey(data, g.trim())
})
_vnode = new VNode(_tag, _data, _value, _type)
} else if (_type === 1) {
_vnode = new VNode(_tag, _data, _value, _type)
_children.forEach((_subvnode) => _vnode.appendChild(combine(_subvnode, data)))
}
return _vnode
}
const ARRAY_METHOD = ['push', 'pop', 'shift', 'unshift', 'reverse', 'splice', 'sort']
const array_method = Object.create(Array.prototype)
ARRAY_METHOD.forEach((key) => {
array_method[key] = function () {
for (let i = 0; i < arguments.length; i++) {
if (typeof arguments[i] === 'object' && arguments[i] != null) {
def(arguments[i], '__dep__', new Dep())
}
observe(arguments[i])
}
setTimeout(() => {
this.__dep__.notify()
}, 0)
return Array.prototype[key].apply(this, arguments)
}
})
function def(target, key, val) {
Object.defineProperty(target, key, {
value: val
})
}
function defineReactiveProperty(target, key, value, enumerable) {
let dep = new Dep()
dep.__propName__ = key
if (typeof value === 'object' && value != null) {
def(value, '__dep__', dep)
observe(value)
}
Object.defineProperty(target, key, {
configurable: true,
enumerable: !!enumerable,
get() {
dep.depend()
return value
},
set(newVal) {
if (value === newVal) return
if (typeof newVal === 'object' && newVal != null) {
observe(newVal)
}
console.log('赋值成功了')
value = newVal
dep.notify()
}
})
}
function observe(data, isRoot) {
if (typeof data === 'object' && data != null && isRoot) {
def(data, '__dep__', new Dep())
}
if (Array.isArray(data)) {
data.__proto__ = array_method
data.forEach((item) => {
observe(item)
})
} else {
Object.keys(data).forEach((key) => {
let value = data[key]
defineReactiveProperty(data, key, value, true)
})
}
}
function proxy(target, middle, key) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get() {
return target[middle][key]
},
set(newVal) {
target[middle][key] = newVal
}
})
}
function MyVue(obj) {
this._data = obj.data
let el = document.querySelector(obj.el)
this._template = el
this._parent = this._template.parentNode
this.initData()
this.mount()
}
MyVue.prototype.initData = function () {
observe(this._data, true)
Object.keys(this._data).forEach((key) => {
proxy(this, '_data', key)
})
}
MyVue.prototype.mount = function () {
this.render = this.createRenderFunction()
this.mountComponent()
}
MyVue.prototype.mountComponent = function () {
let mount = () => {
this.update(this.render())
}
new Watcher(this, mount)
}
MyVue.prototype.createRenderFunction = function () {
const ast = getVNode(this._template)
return function () {
return combine(ast, this._data)
}
}
MyVue.prototype.update = function (vnode) {
let realDOM = parseVNode(vnode)
this._parent.replaceChild(realDOM, document.querySelector('#root'))
}
const mv = new MyVue({
el: '#root',
data: {
name: { firstName: 'wang', lastName: 'wu' },
age: 18,
gender: '男',
someThing: { aaa: 'a' },
numArr: [1, 2]
}
})
</script>
</body>
</html>