一、Vue的执行步骤
1.首先会获得页面的模板,模板中会有 {{}} 这样的变量
2.利用Vue的构造函数中所提供的数据来替换 {{}} 中的变量,得到可以在页面中显示的 ”标签“
3.将 ”标签”替换之前页面中有 {{}} 的标签
Vue利用提供的数据和模板生成一个新的HTML标签(node节点),替换到页面放模板的地方
二、实现
1.步骤拆解:
拿到模板->拿到准备的数据->将数据和模板结合,得到HTML元素->将真正需要渲染的HTML元素挂载到页面中
2.具体实现 1.0版本
未解决的问题* 1.代码没有整合(vue中使用了一个构造函数)* 2.只考虑了单属性{{name}},而Vue中大量的使用层级,比如{{child.name.firstname}}* 3.vue使用的虚拟DOM
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root">
<p>{{name}}</p>
<p>{{message}}</p>
</div>
<script>
/**
* 考虑的问题:
* 标签的嵌套:算法(递归、队列、深度优先....等等)+正则的方式来解决
*
*/
let parren = /\{\{(.+?)\}\}/g;
/** 1.拿到模板*/
let tempNode = document.querySelector('#root');
/** 2.拿到准备数据*/
let data = {
name: '名字',
message: '消息'
}
/** 3.将数据放到模板当中,得到HTML元素*/
function compiler(template, data) {
/**
* 在这个案例中template是DOM元素
* 在vue的源码中是字符串,DOM->字符串模板->VNode->正则的DOM
*/
let childNodes = template.childNodes;
for (let i = 0; i < childNodes.length; i++) {
let type = childNodes[i].nodeType;
if (type === 3) {
/**
* 文本节点:判断其内容有没有{{}}
*/
let text = childNodes[i].nodeValue;
text = text.replace(/\{\{(.+?)\}\}/g, function (word, g) {
/**
* 每次匹配到文本都会调用后面的function
* _中是匹配到的内容(有{{}})
* _后面的参数是分组的内容,也就是每组{{}}里面的内容
*/
let key = g.trim();//{{}}中的内容
let value = data[key];
/**
* 将{{}}及其内容替换为data中的数据
*/
return value;
}
);
childNodes[i].nodeValue = text;
} else if (type === 1) {
/**
* 元素:考虑是否有子元素,有的话考虑子元素进行是否要插值
*/
compiler(childNodes[i], data)
}
}
}
let generateNode = tempNode.cloneNode(true)//保留原始的模板,将复制的模板去做操作(避免之后对数据的修改没有原始模板,就不知道哪里是需要修改数据的地方)
console.log(tempNode)
compiler(generateNode, data)
console.log(generateNode)
/** 4.将需要渲染的HTML元素放到页面中*/
root.parentNode.replaceChild(generateNode, root)
</script>
</body>
</html> 3.具体实现 2.0版本
1)代码整合
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--模板-->
<div id="root">
<p>{{name}}</p>
<p>{{message}}</p>
</div>
<script>
/** 习惯:在Vue中内部的数据一般使用下划线开头,只读数据使用$开头*/
let parren = /\{\{(.+?)\}\}/g
function compiler(template, data) {
let childNodes = template.childNodes;
for (let i = 0; i < childNodes.length; i++) {
let type = childNodes[i].nodeType;
if (type === 3) {
let text = childNodes[i].nodeValue;
text = text.replace(/\{\{(.+?)\}\}/g, function (word, g) {
let key = g.trim();
let value = data[key];
return value;
});
childNodes[i].nodeValue = text;
} else if (type === 1) {
compiler(childNodes[i], data)
}
}
}
function JGVue(options) {
/**
* Vue构造函数
*/
this._data = options.data;
this._el = options.el;
/** 准备工作(准备模板)*/
this.$el = this._templateDOM = document.querySelector(this._el);
this._parent = this.$el.parentNode;
/** 渲染工作*/
this.render()
}
JGVue.prototype.render = function () {
/**渲染函数 将数据放到模板中,得到DOM元素,并加载到页面中*/
this.complier()
}
JGVue.prototype.complier = function () {
/** 将数据放到模板中,得到真正DOM元素*/
let realHTMLDOM = this._templateDOM.cloneNode(true);
compiler(realHTMLDOM, this._data);
this.update(realHTMLDOM)
}
JGVue.prototype.update = function (realHTMLDOM) {
/** 将DOM元素加载到页面中*/
this._parent.replaceChild(realHTMLDOM, document.querySelector('#root'))
}
/**
*构造函数的使用
*/
let app = new JGVue({
el: '#root',
data: {
name: 'luyiyi',
message: 'info'
}
})
</script>
</body>
</html> 2)解决属性嵌套问题
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--模板-->
<div id="root">
<p>{{name.firstName}}</p>
<p>{{message}}</p>
</div>
<script>
/**解决使用'xxx.yyy.zzz'访问对象的问题
* Vue使用到函数科里化的技巧:
* 因为这个函数是在Vue编译模板的时候就生成了,所以可以在任何地方使用
* 在使用的时候可以减少函数的调用,提高一点性能
*/
function getValueByPath(obj, path) {
/**
* 先取得obj.xxx
* 再去结果中的yyy
* 再取结果中的zzz
*/
let paths = path.split('.');
let res = obj;
for (let i = 0; i < paths.length; i++) {
res = res[paths[i]];
}
return res;
}
let parren = /\{\{(.+?)\}\}/g
function compiler(template, data) {
let childNodes = template.childNodes;
for (let i = 0; i < childNodes.length; i++) {
let type = childNodes[i].nodeType;
if (type === 3) {
let text = childNodes[i].nodeValue;
text = text.replace(/\{\{(.+?)\}\}/g, function (word, g) {
let path = g.trim();
let value = getValueByPath(data, path);
return value;
});
childNodes[i].nodeValue = text;
} else if (type === 1) {
compiler(childNodes[i], data)
}
}
}
function JGVue(options) {
/**
* Vue构造函数
*/
this._data = options.data;
this._el = options.el;
/** 准备工作(准备模板)*/
this.$el = this._templateDOM = document.querySelector(this._el);
this._parent = this.$el.parentNode;
/** 渲染工作*/
this.render()
}
JGVue.prototype.render = function () {
/**渲染函数 将数据放到模板中,得到DOM元素,并加载到页面中*/
this.complier()
}
JGVue.prototype.complier = function () {
/** 将数据放到模板中,得到真正DOM元素*/
let realHTMLDOM = this._templateDOM.cloneNode(true);
compiler(realHTMLDOM, this._data);
this.update(realHTMLDOM)
}
JGVue.prototype.update = function (realHTMLDOM) {
/** 将DOM元素加载到页面中*/
console.log(this._parent)
this._parent.replaceChild(realHTMLDOM, document.querySelector('#root'))
}
/**
*构造函数的使用
*/
let app = new JGVue({
el: '#root',
data: {
name: {
firstName: 'lu',
lastName: 'yiyi'
},
message: 'info'
}
})
</script>
</body>
</html> 3)生成虚拟DOM
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root">
<ul class="ul">
<li>li1</li>
<li>li2</li>
<li>li3</li>
</ul>
<div class="div1">div1</div>
<div>div2</div>
<div>div3</div>
</div>
<script>
/**
* 1.解决的问题:虚拟DOM
* 2.为什么要使用虚拟DOM:提高性能,所有操作都在内存中,只需要最后操作一次
* 3.考虑:
* 1)将真正的DOM转为虚拟DOM
* 2)将虚拟DOM转为真正的DOM
* 4.思想:与深拷贝类似
* 5.具体实现:使用js对象表示HTML标签=》
* tag:标签名
* value:文本节点的值
* data:属性名
* children:子元素
* type:这个元素的类型
*/
class VNode {
constructor(tag, value, data, type) {
this.tag = tag && tag.toLowerCase();
this.value = value;
this.data = data;
this.children = [];
this.type = type
}
appendChild(vnode) {
this.children.push(vnode)
}
}
/**
* 使用递归来遍历DOM元素生成虚拟DOM,vue源码中使用栈存储父元素实现递归生成(因为需要解析字符串,这种方法更加便利)
*/
function getVNode(node) {
let nodeType = node.nodeType;
let _vnode = null;
if (nodeType == 1) {
let nodeName = node.nodeName;
/** attrs返回所有属性组成的伪数组
* 需要将attrs变为一个对象
*/
let attrs = node.attributes;
let _attrObj = {};
for (let i = 0; i < attrs.length; i++) {
_attrObj[attrs[i].nodeName] = attrs[i].nodeValue;
}
_vnode = new VNode(nodeName, undefined, _attrObj, nodeType);
/** 考虑传进来node的子元素,将子元素加入到生成的虚拟节点下面*/
let childNodes = node.childNodes;
for (let i = 0; i < childNodes.length; i++) {
_vnode.appendChild(getVNode(childNodes[i]))
}
} else if (nodeType == 3) {
_vnode = new VNode(undefined, node.nodeValue, undefined, nodeType);
}
return _vnode;
}
let root = document.querySelector('#root');
let vroot = getVNode(root);
console.log(vroot)
/**
* 将虚拟DOM转为真实DOM
*/
function parseNode(vnode) {
let _node = null
if (vnode.type == 1) {
/** 节点是元素节点的时候,创建元素节点*/
_node = document.createElement(vnode.tag);
if (vnode.data) {
/** 考虑是否有属性*/
for (let item in vnode.data) {
_node.setAttribute(item, vnode.data[item])
}
}
/** 考虑是否有子元素*/
if (vnode.children) {
for (let i = 0; i < vnode.children.length; i++) {
_node.appendChild(parseNode(vnode.children[i]));
}
}
} else if (vnode.type == 3) {
/** 节点是文本节点的时候,创建文本节点*/
_node = document.createTextNode(vnode.value);
}
return _node;
}
</script>
</body>
</html> 4.完整版(接近源码,但是有很多地方被简化)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--模板-->
<div id="root">
<p>{{name.firstName}}</p>
<p>{{message}}</p>
</div>
<script>
/**
* 在Vue中使用了二次提交的设计结构
* 1.在页面中的DOM和虚拟DOM是一一对应的关系
* 2.先由AST和数据生成新的VNode(render函数)
* 3.将旧的VNode和新的VNode进行比较(diff),更新(update函数)
*/
/** Vue构造函数*/
function JGVue(options) {
this._data = options.data;
let elm = document.querySelector('#root');
this._template = elm;
this._parent = elm.parentNode;
this.mount();//挂载函数
}
/** 虚拟DOM构造函数*/
class VNode {
constructor(tag, value, data, type) {
this.tag = tag && tag.toLowerCase();
this.value = value;
this.data = data;
this.children = [];
this.type = type
}
appendChild(vnode) {
this.children.push(vnode)
}
}
/** 由HTML DOM生成虚拟DOM,将这个函数当做complier函数使用,模拟抽象语法树
* 而在Vue中将真正的node作为一个字符串解析,得到一个抽象语法树
*/
function getVNode(node) {
let nodeType = node.nodeType;
let _vnode = null;
if (nodeType == 1) {
let nodeName = node.nodeName;
/** attrs返回所有属性组成的维数组
* 需要将attrs变为一个对象
*/
let attrs = node.attributes;
let _attrObj = {};
for (let i = 0; i < attrs.length; i++) {
_attrObj[attrs[i].nodeName] = attrs[i].nodeValue;
}
_vnode = new VNode(nodeName, undefined, _attrObj, nodeType);
/** 考虑传进来node的子元素,将子元素加入到生成的虚拟节点下面*/
let childNodes = node.childNodes;
for (let i = 0; i < childNodes.length; i++) {
_vnode.appendChild(getVNode(childNodes[i]))
}
} else if (nodeType == 3) {
_vnode = new VNode(undefined, node.nodeValue, undefined, nodeType);
}
return _vnode;
}
function parseNode(vnode) {
/** 将虚拟DOM转为真实DOM*/
let _node = null
if (vnode.type == 1) {
_node = document.createElement(vnode.tag);
if (vnode.data) {
for (let item in vnode.data) {
_node.setAttribute(item, vnode.data[item])
}
}
if (vnode.children) {
for (let i = 0; i < vnode.children.length; i++) {
_node.appendChild(parseNode(vnode.children[i]));
}
}
} else if (vnode.type == 3) {
_node = document.createTextNode(vnode.value);
}
return _node;
}
function getValueByPath(obj, path) {
/** 根据路径访问对象任意层级的属性*/
let paths = path.split('.');
let res = obj;
for (let i = 0; i < paths.length; i++) {
res = res[paths[i]];
}
return res;
}
let parren = /\{\{(.+?)\}\}/g
function combine(vnode, data) {
/** 将带有{{}}的vnode与数据结合得到填充数据的vnode,模拟AST到vnode的行为*/
let _type = vnode.type;
let _data = vnode.data;
let _value = vnode.value;
let _tag = vnode.tag;
let _children = vnode.children;
let _vnode = null;
if (_type == 3) {
_value = _value.replace(parren, function (str, path) {
return getValueByPath(data, path.trim());
});
_vnode = new VNode(_tag, _value, _data, _type);
} else if (_type == 1) {
_vnode = new VNode(_tag, _value, _data, _type);
_children.forEach(item => {
_vnode.appendChild(combine(item, data))
})
}
return _vnode;
}
/** 挂载函数*/
JGVue.prototype.mount = function () {
this.render = this.createRenderFn()/** 1.根据Vue构造函数中的内容生成虚拟DOM*/
this.mountComponent();/** 2.将虚拟DOM渲染到页面*/
}
JGVue.prototype.mountComponent = function () {
/* let mount = () => {
this.update(this.render())//渲染到页面上
mount.call(this)//本质上应该交给watcher来调用
*/
this.update(this.render())//渲染到页面上
}
JGVue.prototype.createRenderFn = function () {
/** 生成render函数,目的是缓存AST(使用虚拟DOM模拟)*/
let ast = getVNode(this._template)
return function render() {
/**
* Vue中:将AST+data=>VNode
* 这里使用带{{}}的模板和数据来模拟=》含有数据的VNode
*/
return combine(ast, this._data)
}
}
JGVue.prototype.update = function (vnode) {
/** 将虚拟DOM渲染到页面中,diff算法(减少比较、减少操作)就在这里
* 简化:直接生成HTML DOM,再replaceChild到页面中
*/
let realDOM = parseNode(vnode)
this._parent.replaceChild(realDOM, document.querySelector('#root'))
/** 每次会将页面中的DOM全部替换,会使每次ID都是新的,需要重新获取*/
}
let app = new JGVue({
el: '#root',
data: {
name: {
firstName: 'luyiyi'
},
message: 'joaihia'
}
})
/**
* 讨论问题
* 科里化:主要是用在判断是否为内置标签的地方,缓存了一部分行为
* 将标签生成一个Set对象,是内置标签值为true,否则为false
* 只需要在第一次渲染的时候调用一次,后面就可以直接通过键名来获取值,如果是函数的话则需要每次都调用
* 因此能够提高一些性能
*/
</script>
</body>
</html> 
京公网安备 11010502036488号