本项目将在GitHub上维护更新。
https://github.com/dangjingtao/FeRemarks
编译器的工作流程:
来看基本的需求:
现在Due
需要支持下列语句:
<body>
<div id="app">
<p>{{name}}</p>
<!--插值语句-->
<p d-text="name"></p>
<p>{{doubleAge}}</p>
<input type="text" d-model="name">
<!--双向绑定-->
<button @click="change">hehe</button>
<!--事件-->
<div d-html="html"></div>
</div>
<script src="Complie.js"></script>
<script src="Due.js"></script>
<script>
const app = new Due({
el: '#app',
data: {
name: 'i am djtao',
age: 29,
html: `<button>button</button>`
},
created() {
setTimout(() => {
this.name = 'dangjingtao'
}, 2000)
},
methods: {
change(){
this.name = 'taotao';
this.age = 30;
}
}
})
</script>
新建一个Complie.js
:写构造函数吧!
class Compile{
constructor(el,vm){
this.$vm=vm; //拿到Due实例
this.$el=document.querySelector(el)
// 开始编译
if(this.$el){
// 提取宿主中模板内容,放到一个dom标签:
this.$fragment=this.node2Fragement(this.$el)
// 对碎片进行编译,收集依赖
this.compile(this.$fragment);
// 替换完成后,追加到目标宿主中
this.$el.appendChild(this.$fragment)
}
}
一开始是要拿到宿主el
(#app)和Due。
node2Fragement
: 提取宿主里面的内容complie
,执行编译,收集依赖那现在来实现这几个功能:
class Compile{
//...
node2Fragement(el){
console.log(el)
// 原生dom方法,把宿主的子元素全扔进去。
const fragement=document.createDocumentFragment();
let child;
while (child=el.firstChild){
fragement.appendChild(child)
}
return fragement
}
compile(el){
const childNodes=el.childNodes;
Array.from(childNodes).forEach(node=>{
// nodetype 为1,是element节点
if(node.nodeType===1){
console.log('编译元素节点'+node.nodeName)
}else if(this.isInterpolation(node)){
// 是否双大括号
console.log('编译插值文本'+node.textContent)
}
// 递归子节点
if(node.childNodes&&node.childNodes.length>0){
this.compile(node)
}
})
}
//判断插值文本
isInterpolation(node){
//是文本且符合双大括弧正则
return node.nodeType===3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
在due.js中的Due构造函数中·new Complie(this.$option.el,this). 在浏览器环境秀泡一下:
发现都已经执行了。
新建两个函数,分别处理元素节点和文本节点:
compile(el){
const childNodes=el.childNodes;
Array.from(childNodes).forEach(node=>{
// nodetype 为1,是element节点
if(node.nodeType===1){
this.compileElement()
}else if(this.isInterpolation(node)){
// 是否双大括号
this.compileText(node)
}
// 递归子节点
if(node.childNodes&&node.childNodes.length>0){
this.compile(node)
}
})
}
编译文本相当简单,把vm
对应的key
值更新到视图即可。
一再强调的,元素节点一定要对属性和标签进行区分:
在Due的构造函数最后,执行Created:
// 编译完成
if(options.created){
options.created.call(this)
}
现在可以写完watcher的逻辑了。 watcher接收vm,key,和回调。
class Watcher{
constructor(vm,key,cb){
this.vm=vm;
this.key=key;
this.cb=cb;
//hack:让dep拿到watcher的this,以方便调用方法;
Dep.target=this;
// 读一下触发get
this.vm[this.key]
Dep.target=null;
}
// 更新
update(){
console.log('属性已更新')
this.cb.call(this.vm,this.vm[this.key])
}
}
那么回调函数写什么呢?
/**
* 更新函数接收4个参数
* @节点 {*} node
* @Due实例 {*} vm
* @表达式 {*} exp
* @指令 {*} dir 比如渲染文本为`text`
*/
update(node,vm,exp,dir){
//根据不同的指令调用方法
let updaterFn=this[dir+'Updater'];
updaterFn&&updaterFn(node,vm[exp]);
// 收集
new Watcher(vm,exp,function(value){
updaterFn && updaterFn(node,value);
})
}
textUpdater(node,val){
node.textContent=val
}
过多的具体业务逻辑就不下强调了。
// 编译标签节点
compileElement(node){
// <div d-text="txt"></div>
let nodeAttrs=node.attributes;
Array.from(nodeAttrs).forEach(attr=>{
const attrName=attr.name;
const exp=attr.value;
// 首先判断是事件还是指令
if (this.isEvent(attrName)) {
const dir=attrName.substring(1);//@后面的事件类型
//在这里,exp是函数
this.eventHandler(node,this.$vm,exp,dir);
}
// 指令
if(this.isDir(attrName)){
const dir=attrName.substring(2);//比如d-text中的text
//在这里,exp是指令值
this[dir]&&this[dir](node,this.$vm,exp);
}
})
}
isDir(attr){
return attr.indexOf('d-')==0
}
isEvent(attr){
return attr.indexOf('@')==0;
}
// v-text处理
text(node,vm,exp){
this.update(node,vm,exp,'text')
}
// 处理事件
eventHandler(node,vm,exp,dir){
const fn=vm.$options.methods&&vm.$options.methods[exp]
console.log(123,node)
node.addEventListener(dir,fn.bind(vm))
}
现在进行其它方法绑定:
html(node,vm,exp){
// console.log(ddd)
this.update(node,vm,exp,'html')
}
model(node,vm,exp){
this.update(node,vm,exp,'model');
node.addEventListener('input',e=>{
vm[exp]=e.target.value
})
}
htmlUpdater(node,value){
node.innerHTML=value;
}
textUpdater(node,val){
node.textContent=val
}
那么这里的核心就讲完了。虚拟Dom等到react再说