一、vue 渲染模型
- 对以上进行分析:
-
页面初始化 的时候,AST(抽象语法树) 结合 数据data 生成 一份 虚拟DOM(VNode) ,内存把这个 虚拟DOM(VNode) 保存下来, 抽象语法树(AST) 也被缓存下来了。
-
当 数据变化 的时候,AST 结合 数据 生成一个 新的虚拟DOM(VNode)。旧的虚拟DOM 和 新的虚拟DOM 进行比较(这里面比较涉及到 diff 算法),没有变化的模板及数据不进行替换,有变化的模板及数据用 新的虚拟DOM 里面的内容进行替换。然后 旧的虚拟DOM 这时候是更新后的状态,再转化成 真实DOM, 渲染到页面上。
- 在真正的 Vue 中使用了二次提交的 设计结构
-
在页面中 的 DOM 和 虚拟DOM 是一一对应的关系。 -
先用 AST 和 数据 生成 虚拟DOM -
然后将 旧的 VNode 和 新的 VNode 比较(通过 diff 算法),最后更新页面(update)
二、仿 vue 生成虚拟DOM
- Vue 中的是 通过 AST 结合 数据 生成一份虚拟DOM。 我们现在是通过 带坑的虚拟DOM(有{{}}) 结合 数据 来生成一份 有数据的虚拟DOM(不带坑的{{}})。
<div id="root">
<div class="box">{{name}}-{{age}}</div>
<div>{{message}}</div>
</div>
<script>
// 把 【虚拟 DOM】 转化成【真实 DOM】
function parseVnode(vnode) {
let type = vnode.type;
let _node = null;
if (type == 3) {
return document.createTextNode(vnode.value);
} else if (type == 1) {
// 创建一个元素节点
_node = document.createElement(vnode.tag);
// 给元素节点添加属性
let attrs = vnode.data;
Object.keys(attrs).forEach(key => {
let attrName = key;
let attrValue = attrs[key];
_node.setAttribute(attrName, attrValue);
})
// 给元素节点添加子元素节点
let childrenNodes = vnode.children;
for (var i = 0; i < childrenNodes.length; i++) {
_node.appendChild(parseVnode(childrenNodes[i]))
}
}
return _node;
}
// 转虚拟DOM 的类
class Vnode {
/**
* tag 表示元素节点元素名称nodeName,例如 div; 文本节点 nodeName 值为 undefined
* data 表示元素节点上的 属性对象,例如{title: '123', id: 'box'}; 文本节点 没有属性
* value 表示文本节点的 nodeValue; 元素节点的 nodeValue 为 undefined
* type 表示节点的类型,元素节点的 nodeType 值为 1, 文本节点的 nodeType 值为 3
*/
constructor(tag, data, value, type) {
this.tag = tag && tag.toLowerCase();
this.data = data;
this.value = value;
this.type = type;
this.children = [];
}
appendChild(vNode) {
this.children.push(vNode);
}
}
// 把 HTML DOM 转化成 虚拟DOM(VNode)
function getVnode(node) {
let nodeType = node.nodeType;
let _vnode = null;
if (nodeType === 1) {
// 元素节点
let NodeName = node.nodeName;
let attrs = node.attributes;
let attrsObj = {};
for (var i = 0; i < attrs.length; i++) {
attrsObj[attrs[i].nodeName] = attrs[i].nodeValue;
}
_vnode = new Vnode(NodeName, attrsObj, undefined, nodeType);
let childNodes = node.childNodes;
for (var i = 0; i < childNodes.length; i++) {
_vnode.appendChild(getVnode(childNodes[i]))
}
} else if (nodeType == 3) {
_vnode = new Vnode(undefined, undefined, node.nodeValue, nodeType);
}
return _vnode;
}
let rKuohao = /{\{(.+?)\}\}/g;
// 多层次数据返回属性值
function getValueByPath(obj, path) {
let paths = path.split('.');
let res = obj;
let prop;
while (prop = paths.shift()) {
res = res[prop];
};
return res;
}
// 将带有坑的 VNode 与数据 data 结合,得到填充数据的 VNode, 模拟 Vue 源码中的 AST 转化成 VNode
function combine(vnode, data) {
let _type = vnode.type; // 获取节点类型
let _value = vnode.value; // 获取文本节点 value 值
let _tag = vnode.tag; // 获取元素节点 标签名
let _data = vnode.data; // 获取元素节点 属性数据
let _children = vnode.children; // 获取元素节点下的子元素节点
let _vnode = null;
if(_type === 3) {
// 文本节点
// 对文本进行处理
_value = _value.replace(rKuohao, (_, g) => {
return getValueByPath(data, g.trim())
})
return new Vnode(_tag, _data, _value, _type);
} else if(_type == 1) {
// 表示元素节点
_vnode = new Vnode(_tag, _data, _value, _type);
_children.forEach(_subVnode => _vnode.appendChild(combine(_subVnode, data)))
}
return _vnode;
}
// 创建 JGVue 构造函数
function JGVue(options) {
this._data = options.data;
this._template = document.querySelector(options.el);
this.mount(); // 进行实例挂载
}
JGVue.prototype.mount = function () {
// 需要提供一个 render 方法, 生成 【虚拟 DOM】
this.render = this.createRenderFn();
this.mountComponent();
}
JGVue.prototype.mountComponent = function () {
// 这里执行 mountComponent 函数
let mount = () => {
this.updata(this.render());
}
mount.call(this)
}
// 这里是生成 【虚拟 DOM】 的函数,目的是缓存抽象语法树,我们这里用的是 【虚拟 Dom】 来模拟 vue 源码中的 AST
JGVue.prototype.createRenderFn = function () {
let ast = getVnode(this._template);
console.log(ast, '带坑的虚拟DOM')
// Vue 源码: 是将 AST(抽象语法树) 结合 数据data 生成一个 虚拟DOM(VNode)
// 我们这里:带有坑的 虚拟Dom(VNode) 结合 数据data 生成含有数据的 VNode
return function render() {
// 将带有坑的 VNode 转换为 带数据的 VNode
let _tmp = combine(ast, this._data);
console.log(_tmp, '带数据的虚拟DOM')
return _tmp;
}
}
// 将 【虚拟 DOM】 渲染到页面当中,这里涉及到 diff 算法
JGVue.prototype.updata = function () {
}
// 创建 vue 实例
let app = new JGVue({
el: '#root',
data: {
name: '姓名',
age: '年龄',
message: '消息',
}
})