一、vue的基本执行流程
- 获得模板: 模板中有 “坑”(比如
{{}}
) - 利用 Vue 构造函数中所提供的数据来 “填坑”, 得到可以在页面中显示的 “标签了”
- 将标签替换页面中原来有坑的标签
总的来说:Vue利用我们提供的数据和 页面中模板生成了一个新的 HTML 标签 ( node 元素 ),替换到了页面中放置模板的位置.
二、简单渲染模型
Vue 本质上是使用 HTML 的字符串作为模板的, 将字符串的 模板 转换为 AST(抽象语法树), 再转换为 VNode(虚拟dom).
- 模板 -> AST
- AST -> VNode
- VNode -> DOM
那一个阶段最消耗性能?
最消耗性能是字符串解析 ( 模板 -> AST )
三、模仿vue的渲染流程
- 将真正的 DOM 转换为 虚拟 DOM
- 将虚拟 DOM 转换为 真正的 DOM
<!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="root">
<div class="c1">
<div title="tt1" id="id">{{ name }}</div>
<div title="tt2">{{ age }}</div>
<div title="tt3">{{ gender }}</div>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
</div>
</div>
<script>
/** 虚拟 DOM 构造函数 */
class VNode {
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 -> VNode: 将这个函数当做 compiler 函数 */
function getVNode( node ) {
let nodeType = node.nodeType;
let _vnode = null;
if ( nodeType === 1 ) {
// 元素
let nodeName = node.nodeName;
let attrs = node.attributes;
let _attrObj = {};
for ( let i = 0; i < attrs.length; i++ ) { // attrs[ i ] 属性节点 ( nodeType == 2 )
_attrObj[ attrs[ i ].nodeName ] = attrs[ i ].nodeValue;
}
_vnode = new VNode( nodeName, _attrObj, undefined, 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, undefined, node.nodeValue, nodeType );
}
return _vnode;
}
/** 将虚拟 DOM 转换成真正的 DOM */
function parseVNode( vnode ) {
// 创建 真实的 DOM
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 data = vnode.data; // 现在这个 data 是键值对
Object.keys( data ).forEach( ( key ) => {
let attrName = key;
let attrValue = data[ key ];
_node.setAttribute( attrName, attrValue );
} );
// 子元素
let children = vnode.children;
children.forEach( subvnode => {
_node.appendChild( parseVNode( subvnode ) ); // 递归转换子元素 ( 虚拟 DOM )
} );
return _node;
}
}
let rkuohao = /\{\{(.+?)\}\}/g;
/** 根据路径 访问对象成员 */
function getValueByPath( obj, path ) {
let paths = path.split( '.' ); // [ xxx, yyy, zzz ]
let res = obj;
let prop;
while( prop = paths.shift() ) {
res = res[ prop ];
}
return res;
}
/** 将 带有 坑的 Vnode 与数据 data 结合, 得到 填充数据的 VNode: 模拟 AST -> VNode */
function combine( vnode, data ) {
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( rkuohao, function ( _, g ) {
return getValueByPath( data, g.trim() );
} );
_vnode = 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;
}
function JGVue( options ) {
this._data = options.data;
let elm = document.querySelector( options.el ); // vue 是字符串, 这里是 DOM
this._template = elm;
this._parent = elm.parentNode;
this.mount(); // 挂载
}
JGVue.prototype.mount = function () {
// 需要提供一个 render 方法: 生成 虚拟 DOM
this.render = this.createRenderFn() // 带有缓存 ( Vue 本身是可以带有 render 成员 )
this.mountComponent();
}
JGVue.prototype.mountComponent = function () {
// 执行 mountComponent() 函数
let mount = () => { // 这里是一个函数, 函数的 this 默认是全局对象 "函数调用模式"
this.update( this.render() )
}
mount.call( this ); // 本质应该交给 watcher 来调用, 但是还没有讲到这里
// 为什么
// this.update( this.render() ); // 使用发布订阅模式. 渲染和计算的行为应该交给 watcher 来完成
}
/**
* 在真正的 Vue 中使用了 二次提交的 设计结构
* 1. 在 页面中 的 DOM 和 虚拟 DOM 是一一对应的关系
* 2. 先 有 AST 和 数据 生成 VNode ( 新, render )
* 3. 将 就的 VNode 和 新的 VNode 比较 ( diff ), 更新 ( update )
*/
// 这里是生成 render 函数, 目的是缓存 抽象语法树 ( 我们使用 虚拟 DOM 来模拟 )
JGVue.prototype.createRenderFn = function () {
let ast = getVNode( this._template );
// Vue: 将 AST + data => VNode
// 我们: 带有坑的 VNode + data => 含有数据的 VNode
return function render () {
// 将 带有 坑的 VNode 转换为 待数据的 VNode
let _tmp = combine( ast, this._data );
return _tmp;
}
}
// 将虚拟 DOM 渲染到页面中: diff 算法就在里
JGVue.prototype.update = function ( vnode ) {
// 简化, 直接生成 HTML DOM replaceChild 到页面中
// 父元素.replaceChild( 新元素, 旧元素 )
let realDOM = parseVNode( vnode );
// debugger;
// let _ = 0;
this._parent.replaceChild( realDOM, document.querySelector( '#root' ) );
// 这个算法是不负责任的:
// 每次会将页面中的 DOM 全部替换
}
let app = new JGVue( {
el: '#root',
data: {
name: '张三'
, age: 19
, gender: '难'
}
} );
</script>
</body>
</html>