先看用法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 插值绑定 -->
<p>{{counter}}</p>
<p>{{counter}}</p>
<p>{{counter}}</p>
<!-- 指令 -->
<p k-text="counter"></p>
<p k-html="desc"></p>
</div>
<script src="https://juejin.cn/post/compile.js"></script>
<script src="https://juejin.cn/post/kvue.js"></script>
<script>
const app = new KVue({
el:'#app',
data: {
counter: 1,
desc:'<span style="color:red">kvue可还行?</span>'
},
})
setInterval(() => {
app.counter++
// app.$data.counter++
}, 1000);
</script>
</body>
</html>
复制代码
设计思路图解
劫持监听所有属性实现响应化功能
- 将data对象返回的每一个key做响应化处理 新建一个dep方便对Watcher进行管理 kvue.js
- 在key首次渲染的时候 新建一个Watcher实例保存更新函数在读取key时把Watcher绑定到相应的dep上 在key发生变化的时候批量对相应的dep下的Watcher保存的函数进行更新 kvue.js
function defineReactive(obj, key, val) {
// 递归
observe(val)
// 创建一个Dep和当前key一一对应
const dep = new Dep()
// 对传入obj进行访问拦截
Object.defineProperty(obj, key, {
get() {
console.log('get ' + key);
// 依赖收集在这里
Dep.target && dep.addDep(Dep.target)
return val
},
set(newVal) {
if (newVal !== val) {
console.log('set ' + key + ':' + newVal);
// 如果传入的newVal依然是obj,需要做响应化处理
observe(newVal)
val = newVal
// 通知更新
// watchers.forEach(w => w.update())
dep.notify()
}
}
})
}
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
// 希望传入的是obj
return
}
// 创建Observer实例
new Observer(obj)
}
// 代理函数,方便用户直接访问$data中的数据
function proxy(vm, sourceKey) {
// vm[sourceKey]就是vm[$data]
Object.keys(vm[sourceKey]).forEach(key => {
// 将$data中的key代理到vm属性中
Object.defineProperty(vm, key, {
get() {
return vm[sourceKey][key]
},
set(newVal) {
vm[sourceKey][key] = newVal
}
})
})
}
// 创建KVue构造函数
class KVue {
constructor(options) {
// 保存选项
this.$options = options;
this.$data = options.data;
// 响应化处理
observe(this.$data)
// 代理
proxy(this, '$data')
// 创建编译器
new Compiler(options.el, this)
}
}
// 根据对象类型决定如何做响应化
class Observer {
constructor(value) {
this.value = value
// 判断其类型
if (typeof value === 'object') {
this.walk(value)
}
}
// 对象数据响应化
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
// 数组数据响应化,待补充
}
// 观察者:保存更新函数,值发生变化调用更新函数
// const watchers = []
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm
this.key = key
this.updateFn = updateFn
// watchers.push(this)
// Dep.target静态属性上设置为当前watcher实例
Dep.target = this
this.vm[this.key] // 读取触发了getter
Dep.target = null // 收集完就置空
}
update() {
this.updateFn.call(this.vm, this.vm[this.key])
}
}
// Dep:依赖,管理某个key相关所有Watcher实例
class Dep {
constructor(){
this.deps = []
}
addDep(dep) {
this.deps.push(dep)
}
notify() {
this.deps.forEach(dep => dep.update())
}
}
复制代码
解析指令 实现编译器的功能
- 将vue中的插值绑定和属性和每一个key进行绑定 compile.js
// 编译器
// 递归遍历dom树
// 判断节点类型,如果是文本,则判断是否是插值绑定
// 如果是元素,则遍历其属性判断是否是指令或事件,然后递归子元素
class Compiler {
// el是宿主元素
// vm是KVue实例
constructor(el, vm) {
// 保存kVue实例
this.$vm = vm
this.$el = document.querySelector(el)
if (this.$el) {
// 执行编译
this.compile(this.$el)
}
}
compile(el) {
// 遍历el树
const childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
// 判断是否是元素
if (this.isElement(node)) {
// console.log('编译元素'+node.nodeName);
this.compileElement(node)
} else if (this.isInter(node)) {
// console.log('编译插值绑定'+node.textContent);
this.compileText(node)
}
// 递归子节点
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
eventHandler(node,exp,dir){
const fn=this.$vm.$options && this.$vm.$options.methods[exp]
node.addEventListener(dir,fn.bind(this.$vm))
}
isEvent(name){
return name.indexOf('@')
}
isElement(node) {
return node.nodeType === 1
}
isInter(node) {
// 首先是文本标签,其次内容是{{xxx}}
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
compileText(node) {
this.update(node, RegExp.$1, 'text')
}
// 元素编译
compileElement(node) {
// 节点是元素
// 遍历其属性列表
const nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach(attr => {
// 规定:指令以k-xx="oo"定义 k-text="counter"
const attrName = attr.name // k-xx k-text
const exp = attr.value // oo counter
if (this.isDirective(attrName)) {
const dir = attrName.substring(2) // xx text
// 执行指令
this[dir] && this[dir](node, exp)
}
})
}
isDirective(attr) {
return attr.indexOf('k-') === 0
}
// 更新函数作用:
// 1.初始化
// 2.创建Watcher实例
update(node, exp, dir) {
// 初始化
// 指令对应更新函数xxUpdater
const fn = this[dir + 'Updater']
fn && fn(node, this.$vm[exp])
// 更新处理,封装一个更新函数,可以更新对应dom元素
new Watcher(this.$vm, exp, function (val) {
fn && fn(node, val)
})
}
// k-model
model(node,exp){
// 双向绑定 data的改变要能够作用于表单 表单值的改变也要能作用于data
this.update(node,exp,'model')
node.addEventListener('input',e=>{
this.$vm[exp]=e.target.value
})
}
// k-text
text(node, exp) {
this.update(node, exp, 'text')
}
// k-html
html(node, exp) {
this.update(node, exp, 'html')
}
modelUpdater(node,val){
node.value=val
}
textUpdater(node, value) {
node.textContent = value
}
htmlUpdater(node, value) {
node.innerHTML = value
}
}
复制代码
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END