基本流程
- 将el的所有子节点取出,添加到一个新建文档fragment对象中
- 对fragment 中所有层次子节点递归进行编译解析处理
- 对表达式文本节点解析
- 插值指令解析
- {{msg}}
- 插值指令解析
- 对元素节点的指令属性进行解析
- 事件指令解析
- v-on:click ="handle"
- 一般指令解析
- v-text = "msg"
- v-html = "msg"
- v-class = "myClass"
- v-style = "myStyle"
- 事件指令解析
- 对表达式文本节点解析
- fragment解析完后,把插入根节点中
Complie实现核心流程
- 将原始的dom结构,转移到fragment对象中进行模板解析,这样可以在内存进行dom操作优化。
node2Fragment: function(el) {
var child,
fragment = document.createDocumentFragment();
while(child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},
- 整体流程: 先转移到fragment,再编译初始化(第一次渲染的dom结构,没有双向绑定), 编译后在放回根节点中。
// 我们要借助fragment对象(文档碎片)对dom解析,这样可以优化解析模板性能
if(this.$el) {
// 将el 转移到 fragment容器中
this.$fragment = this.node2Fragment(this.$el);
// 编译模板初始化
this.init(this.$fragment);
//编译后的fragment 放入到el节点里
this.$el.appendChild(this.$fragment);
}
- 节点类型区分 => 解析不同节点的指令
node.nodeType == 1; //元素节点
node.nodeType == 3; //文本节点
node.nodeType == 2; //属性节点
// 节点种类: 元素节点(1) 属性节点(2) 文本节点(3)
// 遍历子节点并且判断不同的节点有对应的节点编译
compileNode: function(el) {
var childNodes = el.childNodes,
me = this;
Array.prototype.forEach.call(childNodes, node => {
var text = node.textContent;
var reg = /\{\{(.*)\}\}/; //匹配 {{}}
//判断是否元素节点
if(me.isElementNode(node)) {
me.compileElementNode(node);
}else if(me.isTextNode(node) && reg.test(text)) {
//匹配到的 msg 去掉 空格
me.complieTextNode(node, RegExp.$1.trim());
}
if(node.childNodes && node.childNodes.length) {
me.compileNode(node);
}
})
},
- 不同的指令进行对应的更新器
var complieUtil = {
// v-text
//node:当前节点 目的用于视图更新
//vm : 当前vue的实例 目的用于 匹配 data中的数据
//exp: 一个模板匹配的依据
text: function(node, vm, exp) {
this.bind(node, vm, exp, "text");
},
//....
}
- 根据不同指令选择更新节点信息
// 根据不同的指令 更新节点信息
var updater = {
textUpdater: function(node, value) {
node.textContent = typeof value == "undefined"? "" : value;
}
// ...
}
手写vue模板编译的Complie
注意让大家深入理解编译的思路
// 就是把数据正确渲染到页面中
function Compile(el, vm) {
//参数配置到vm上
this.$vm = vm;
this.$el = this.isElementNode(el)? el : document.querySelector(el);
// 我们要借助fragment对象(文档碎片)对dom解析,这样可以优化解析模板性能
if(this.$el) {
// 将el 转移到 fragment容器中
this.$fragment = this.node2Fragment(this.$el);
// 编译模板初始化
this.init(this.$fragment);
//编译后的fragment 放入到el节点里
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
contructor: Compile,
node2Fragment: function(el) {
var child,
fragment = document.createDocumentFragment();
while(child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},
init: function(fragment) {
this.compileNode(fragment);
},
// 节点种类: 元素节点(1) 属性节点(2) 文本节点(3)
// 遍历子节点并且判断不同的节点有对应的节点编译
compileNode: function(el) {
var childNodes = el.childNodes,
me = this;
Array.prototype.forEach.call(childNodes, node => {
var text = node.textContent;
var reg = /\{\{(.*)\}\}/; //匹配 {{}}
//判断是否元素节点
if(me.isElementNode(node)) {
me.compileElementNode(node);
}else if(me.isTextNode(node) && reg.test(text)) {
//匹配到的 msg 去掉 空格
me.complieTextNode(node, RegExp.$1.trim());
}
if(node.childNodes && node.childNodes.length) {
me.compileNode(node);
}
})
},
//解析元素节点 例如 <p>{{msg}}</p> <p v-text="msg"></p>
compileElementNode: function(node) {
//获取节点的属性
var nodeAttrs = node.attributes, //NamedNodeMap 结构非数组
me = this;
Array.prototype.forEach.call(nodeAttrs, attr => {
let attrName = attr.name;
if(me.isDirective(attrName)) {
// 区分事件指令
var exp = attr.value; // msg
var dir = attrName.substring(2); // text
//事件指令
if(me.isEventDirective(dir)) {
complieUtil.eventHandler(node, me.$vm, exp, dir)
}else {
complieUtil[dir] && complieUtil[dir](node,me.$vm, exp)
}
}
})
},
//解析文本节点
complieTextNode: function(node,exp) {
complieUtil.text(node, this.$vm, exp);
},
isElementNode:function(node) {
return node.nodeType == 1;
},
isTextNode:function(node) {
return node.nodeType == 3;
},
isDirective(attr) {
return attr.indexOf("v-") == 0;
},
isEventDirective(attr) {
return attr.indexOf("on") == 0;
}
};
// 根据不同指令进行对应的解析
var complieUtil = {
// v-text
//node:当前节点 目的用于视图更新
//vm : 当前vue的实例 目的用于 匹配 data中的数据
//exp: 一个模板匹配的依据
text: function(node, vm, exp) {
this.bind(node, vm, exp, "text");
},
html: function(node, vm, exp) {
this.bind(node, vm, exp, "html");
},
model: function(node, vm, exp) {
console.log(node, vm, exp);
//数据初始化渲染
this.bind(node, vm, exp, "model");
//获取初始的值
var oldVal = vm._data[exp];
//根据用户输入框改变 从而改vm的数据
node.addEventListener("input", function(e) {
var newVal = e.target.value;
//判断是否变化
if(newVal === oldVal) return;
vm._data[exp] = newVal;
oldVal = newVal;
//注意:
/**
* 由于我们还有没有实现 View层和Model层双向绑定,现在还有效果
*
*/
})
},
class: function(node, vm, exp) {
// 阅读这么久 请阅读者自我实现 (●'◡'●)
},
style: function(node, vm, exp) {
// 阅读这么久 请阅读者自我实现 (●'◡'●)
},
// 将节点(视图)中的数据 和 vue实例data中的数据 实现双向绑定
//node:当前节点 目的用于视图更新
//vm : 当前vue的实例 目的用于 匹配 data中的数据
//exp: 一个模板匹配的依据
//dir: 区分不同指令对应不同的更新器
bind: function(node, vm, exp, dir) {
//获取对应的更新节点的函数
var updaterFn = updater[dir + "Updater"];
// 获取对应的vue实例_data的值
var value = vm._data[exp];
updaterFn && updaterFn(node, value);
//注意
/**
*由于我们没有实现数据和视图双向绑定
* 等我们实现 依赖收集后,这里代码进行改写
* new Watcher(vm, exp, function(value, oldValue) {
//当表达式对应的一个属性值变化,更新界面中的节点
updaterFn && updaterFn(node, value, oldValue);
});
*/
},
//v-on
eventHandler: function(node, vm, exp, dir) {
//获取事件类型
var eventType = dir.split(":")[1];
// 获取事件在vue实例中的回调函数
var fn = vm.$options.methods && vm.$options.methods[exp];
if(eventType && fn) {
//给对应的节点添加 监听事件
node.addEventListener('click', fn.bind(vm), false);
}
}
}
// 根据不同的指令 更新节点信息
var updater = {
textUpdater: function(node, value) {
node.textContent = typeof value == "undefined"? "" : value;
},
htmlUpdater: function(node, value) {
node.innerHTML = typeof value == "undefined"? "" : value;
},
modelUpdater: function(node, value) {
node.value = typeof value == "undefined"? "" : value;
},
classUpdater: function(node, val, oldVal) {
// 阅读这么久 请阅读者自我实现 (●'◡'●)
},
styleUpdater: function(node, val, oldVal) {
// 阅读这么久 请阅读者自我实现 (●'◡'●)
}
}
MVVM 结构
(function() {
function Vue(options) {
// 配置对象保存到vm
this.$options = options || {};
var data = this._data = this.$options.data;
var me = this;
//数据代理 vm.xxx => vm._data.xxx 不需要递归
this.convertData(data);
// 发布订阅模式--数据劫持--依赖收集
// observe(data);
//模板编译
this.$compile = new Compile(options.el || document.body, this);
}
Vue.prototype = {
constructor: Vue,
convertData: function(data) {
var me = this;
Object.keys(data).forEach(key => {
me._proxyData(key);
})
},
_proxyData: function(key) {
var me = this;
Object.defineProperty(me, key, {
configurable: false,
enumerable: true,
// writable: true,
get: function proxyGetter() {
return me._data[key];
},
set: function proxySetter(newVal) {
me._data[key] = newVal;
}
})
}
}
return window.Vue = Vue;
})()
- 模板解析的html
<div id="app">
<h5>模板解析---插值指令/普通指令/事件指令</h5>
<p>{{msg}}</p>
<p v-html="msg">解析v-html</p>
<p v-text="msg">解析v-text</p>
<div v-on:click="handle">事件</div>
<h5>模板解析---v-modle指令</h5>
<input v-model="msg" placeholder="请输入指令对应的值">
</div>
- 模板解析的script
<script src="./js/compile.js"></script>
<script src="./js/min_vue.js"></script>
<script>
const app = new Vue({
el:"#app",
data: {
msg: "hello world",
person: {
name: "lisi",
age: 12
}
},
methods: {
handle() {
alert(123)
}
},
})
</script>
-
最终实现的效果
1611113463(1).png
实现整体MVVM流程哪一部分呢?
是图中所标记的,接下来我会依次总结其他部分实现原理。
图有点丑,万水千山总是情,点播关注行不行,阅读者!!!
网友评论