这是我参与更文挑战的第4天,活动详情查看: 更文挑战
- 数据响应式 Object.defineProperty()
- 模板引擎简写 {{}} v-text v-html @ v-model
测试用例
<!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>FVue 测试用例</title>
</head>
<body>
<div id="app">
<div>{{counter}}</div>
<div f-text="counter"></div>
<div f-html="desc" @click="changeContent"></div>
<input type="text" f-model="desc">
</div>
<script src="./fvue.js"></script>
<script>
const app = new FVue({
el: '#app',
data: {
counter: 1,
desc: '<span style="color: green">村长好帅,谢谢村长!</span>'
},
methods: {
changeContent () {
this.desc = '<span style="color: olive">谢谢村长,村长好帅!</span>'
}
},
})
setInterval(() => {
app.counter++
}, 1000)
</script>
</body>
</html>
实现
// fvue.js
// 响应式数据函数
function defineReactive(obj, key, val) {
// 对值进行递归
observe(val);
const dep = new Dep()
// 对属性进行拦截
Object.defineProperty(obj, key, {
get () {
// 依赖收集
Dep.target && dep.addDep(Dep.target)
return val;
},
set (newVal) {
if (val !== newVal) {
// obj[key] = newVal; 这样设置会一直触发set方法造成死循环
// 此处对新设置的属性在值为对象的时候进行响应式处理
observe(newVal);
val = newVal;
// 全量更新测试
// watchers.forEach(w => w.update())
// 通知更新
dep.notify()
}
}
})
}
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get () {
return vm.$data[key];
},
set (val) {
vm.$data[key] = val;
}
})
})
}
// 遍历对象的所有属性,执行响应式处理
function observe (obj) {
if (typeof obj !== 'object' || obj === null) {
// 判断obj是否是对象,注意: typeof null === 'object'
return obj;
}
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
}
class FVue {
constructor (options) {
this.$options = options;
this.$data = options.data;
// 首先递归遍历data中的所有对象,做响应式处理
observe(this.$data);
// 此时,数据还需要通过this.$data.xxx访问,在这做一个代理处理,方便可以直接通过this.xxx访问
// 此处this指的是vue实例
proxy(this);
// 编译模板
new Compile(options.el, this);
}
}
// 遍历dom tree,解析其中动态部分,初始化,并获得更新函数
class Compile {
constructor (el, vm) {
// 保存实例
this.$vm = vm;
// 获取宿主元素
const dom = document.querySelector(el);
// 对宿主元素进行编译
this.compile(dom);
}
compile (dom) {
// 遍历dom
const childNodes = dom.childNodes;
childNodes.forEach(node => {
// console.log(this.isInsertExpress(node))
if (this.isElement(node)) {
// 元素-解析动态的指令、属性绑定、事件等
// attrs为一个类数组
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
// 判断是否是动态属性
// 置顶f-xxx="counter"
// 属性名
const attrName = attr.name;
// 表达式
const exp = attr.value;
if (this.isDir(attrName)) {
// 截取指令名
const dir = attrName.substring(2);
// 判断是否是合法指令,如果是则这行处理函数
this[dir] && this[dir](node, exp);
}
if (this.isEvent(attrName)) {
// 截取事件名 @click="clickName"
const dir = attrName.substring(1); // click
// exp clickName
this.eventHandler(node, exp, dir);
}
})
// 递归
if (node.childNodes.length > 0) {
this.compile(node);
}
} else if (this.isInsertExpress(node)) {
// 插值表达式{{title}} 此处仅考虑表达式为简单的data中的属性这种情况
this.compileText(node);
}
})
}
// 公用update方法
update (node, exp, dir) {
// 初始化
const fn = this[dir + 'Updater']
fn && fn(node, this.$vm[exp])
// 更新
new Watcher(this.$vm, exp, function (val) {
fn && fn(node, val)
})
}
// f-text
text(node, exp) {
this.update(node, exp, 'text');
// node.textContent = this.$vm[exp];
}
textUpdater (node, val) {
node.textContent = val
}
compileText (node) {
this.update(node, RegExp.$1, 'text');
// node.textContent = this.$vm[RegExp.$1];
}
// f-html
html (node, exp) {
this.update(node, exp, 'html');
// node.innerHTML = this.$vm[exp];
}
htmlUpdater (node, val) {
node.innerHTML = val;
}
isElement (node) {
return node.nodeType === 1;
}
isInsertExpress (node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
isDir (attrName) {
// 判断是否以f-开头
return attrName.startsWith('f-');
}
isEvent (attrName) {
return attrName.indexOf('@') === 0
}
// @click="clickName"
eventHandler (node, exp, dir) {
const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp]
// 要给clickName方法绑定上下文环境,方便在clickName中使用this指向vue实例
node.addEventListener(dir, fn.bind(this.$vm))
}
// f-model="test"
model (node, exp) {
// 给input赋值及更新
this.update(node, exp, 'model');
// 事件监听
node.addEventListener('input', event => {
// 将新的值赋值给data中的数据
this.$vm[exp] = event.target.value;
})
}
modelUpdater (node, val) {
// 给表单元素赋值
node.value = val;
}
}
// 全量更新测试
// const watchers = []
class Watcher {
constructor (vm, key, updater) {
// vue 实例
this.vm = vm;
// data中的key
this.key = key;
// 真正做具体dom操作的更新函数
this.updater = updater;
// 全量更新测试
// watchers.push(this)
// 读当前值,触发依赖收集
Dep.target = this
this.vm[this.key]
Dep.target = null
}
// 更新函数,暴露给dep调用
update () {
this.updater.call(this.vm, this.vm[this.key])
}
}
class Dep {
constructor () {
this.deps = [];
}
addDep (watcher) {
this.deps.push(watcher)
}
notify () {
this.deps.forEach(dep => dep.update())
}
}