核心点:1.实现响应式、2,模版编译
cvue.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<div id="app">
<p style='color: red'>{{count}}</p>
<span c-text="title"></span>
<span c-html="desc"></span>
</div>
<script src="./cvue.js"></script>
<script>
const app = new CVue({
el: '#app',
data: {
count: 33,
title: '编译c-text',
desc: '<div>c-model编译</div>'
}
})
setInterval(() => {
app.count++
// console.log(app.$data.count)
}, 1000)
</script>
</body>
</html>
cvue.js
function defineReactive(obj, key, val) {
// 回调过滤val为object得情况
observe(val)
Object.defineProperty(obj, key, {
get() {
console.log('get')
return val;
},
set(newVal) {
if (newVal !== val) {
// 过滤新值为对象得情况
observe(newVal)
val = newVal;
console.log('set')
}
}
})
}
function observe(obj) {
if(typeof obj !== 'object' || obj === null) {
return obj;
}
console.log('observe')
new Observer(obj)
// Object.keys(obj).forEach(key => {
// defineReactive(obj, key, obj[key])
// })
}
function walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
// 数据响应式拦截 // 编译模版
// observer监听所有属性,区分对象类型做出响应式处理,在属性set的时候就监听到改变然后通知更新
class Observer {
constructor(obj) {
this.value = obj
if (Array.isArray(obj)) {
// 为数组时候的处理方法
} else {
walk(obj)
}
}
}
// 代理以便直接访问data中的数据
// vm 是vue实例
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
// 把data里面的属性代理到vue实例上,方便用户直接访问打他属性
Object.defineProperty(vm, key, {
get() {
return vm.$data[key]
},
set(newVal) {
vm.$data[key] = newVal
}
})
})
}
// 编译
class Compile {
constructor(el, vm) {
this.$el = document.querySelector(el) // 获取真实dom元素
this.$vm = vm
this.compile(this.$el)
}
compile(el) {
// 遍历el 的dom树
el.childNodes.forEach(node => {
// 判断node是元素还是文本
if(this.isElement(node)){
// 为元素的情况需要处理属性和子节点
this.compileElement(node)
console.log(node.nodeName, '编译节点')
// 递归遍历子节点
this.compile(node)
} else if (this.isInter(node)) { // 判断是否为文本模版{{}}
// RegExp.$1拿出最近一次正则表达式里面分组1的值
// 获取值并赋值给node
console.log(node.textContent, '编译文本')
this.compileText(node)
}
})
}
// 判断node是元素还是文本
isElement(node) {
// nodeType为1表示元素 为3表示文本
return node.nodeType === 1
}
// 判断是否为文本模版{{}}
isInter(node) {
// /\{\{(.*)\}\}/.test(node.textContent) 正则匹配节点文本是否为模板文本{{}}
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
// 编译文本
compileText(node) {
const key = RegExp.$1
node.textContent = this.$vm[key]
}
isDir(attrName) {
// 判断属性名是否为c-或者:开头
return attrName.startsWith('c-')
}
// 处理node中的属性
compileElement(node) {
// 获取节点属性 node.attributes 得到一个类数组,使用es6 Array.from()转为真实数组
Array.from(node.attributes).forEach(attr => {
// attr有两个属性分别为name和value
console.log(node, attr, attr.name, '属性')
const attrName = attr.name;
const exp = attr.value;
// 判断attr是否为一个指令
if(this.isDir(attrName)) {
// 如果为指令执行指令的处理函数
const dir = attrName.substring(2); // 获取指令名称
this[dir] && this[dir](node, exp)
}
})
}
text(node, exp) {
node.textContent = this.$vm[exp]
}
html(node, exp) {
node.innerHTML = this.$vm[exp]
}
}
// 做dom更新
let watchers = []
class Watcher {
// vm+key获取最新值,updateFn得到最新值更新视图
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn
watchers.push(this)
}
update() {
this.updateFn.call(this.vm, this.vm[this.key])
}
}
class CVue {
constructor(options) {
// 保存选项
this.$options = options
this.$data = options.data;
// 响应式处理 data props watch computed 和视图有依赖的都是响应式
observe(this.$data)
// 把data代理到vue实例上
proxy(this)
/*编译
1.根据跟元素options.el遍历子元素
2.判断子元素是文本还是节点如果是文本判断是一般文本还是模板文本{{}},如果是节点递归遍历子节点,
3.遍历属性比如v开头的v-html v-model, @开头的事件*/
new Compile(options.el, this)
}
}