这不是Vue的源码
这不是Vue的源码
这不是Vue的源码
只是为了帮助咱们去理解Vue的思想
顺序
- 把el挂载到Vue实例上
- render函数生成虚拟DOM
- 虚拟DOM和数据结合渲染到页面上
- 数据一旦改变,立即生成新的虚拟DOM,新的虚拟DOM去和旧的虚拟DOM比较
- 更新页面
创建一个属于自己的Vue,在这个函数里去执行挂载方法
function myVue( options ) {
this._data = options.data;
// vue 是字符串, 这里是 DOM
this._template = document.querySelector( options.el );
this.mount(); // 挂载
}
带有缓存功能的就是这个render
- 这个render函数内部会生成虚拟DOM
- 因为需要带有缓存,所以用creater的写法
- 这里的mount其实发布订阅者模式,传递了一个watcher
- 为了便于理解,这里这么写
myVue.prototype.mount = function () {
// 需要提供一个 render 方法: 生成 虚拟 DOM
this.render = this.createRenderFn()
this.mountComponent();
}
updata就是把虚拟DOM渲染到页面上
myVue.prototype.mountComponent = function () {
// 执行 mountComponent() 函数
let mount = () => {
this.update( this.render() ) // 渲染虚拟DOM
}
mount.call( this ); // 本质应该交给 watcher 来调用
}
这里是生成 render 函数
- 目的是缓存 抽象语法树 ( 我们使用 虚拟 DOM 来模拟 )
- 在Vue源码里这里是缓存的抽象语法树
myVue.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 算法就在里
myVue.prototype.update = function () {
// 简化, 直接生成 HTML DOM replaceChild 到页面中
}
在真正的 Vue 中使用了 二次提交的 设计结构
- 在 页面中 的 DOM 和 虚拟 DOM 是一一对应的关系
- 先 有 AST 和 数据 生成 VNode ( 新, render )
- 将 就的 VNode 和 新的 VNode 比较 ( diff ), 更新 ( update )
之所以不直接拿新的VNode去直接替换
- 数据只要发生变化就会生成一个新的VNode,是带有新数据的VNode
- 把带有新数据的VNode去和旧的VNode进行比较,这个比较就是diff算法
- 旧的VNode和页面标签是相互对应的
- 如果去直接替换的话,他们之间的绑定关系就得重新绑定 重新绑定就涉及到树的递归遍历访问,比较消耗性能
- 东西相同就忽略,不同就更新。也就相当于更新页面了
- 上面写的三个函数对应的功能也显示出来了
- createRenderFn(缓存抽象语法树)
- render(生成虚拟DOM)
- updata(更新)
虚拟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;
}
将 带有 插值语法的 Vnode 与数据 data 结合, 得到 填充数据的 VNode: 模拟 AST -> VNode ,上一篇文章中,我们写过的
let rkuohao = /\{\{(.+?)\}\}/g; // 用来匹配插值语法的正则表达式
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;
}
完整的源码
<script>
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 );
}
}
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++ ) {
_attrObj[ attrs[ i ].nodeName ] = attrs[ i ].nodeValue;
}
_vnode = new VNode( nodeName, _attrObj, undefined, nodeType );
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;
}
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;
}
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 myVue( options ) {
this._data = options.data;
this._template = document.querySelector( options.el );
this.mount();
}
myVue.prototype.mount = function () {
this.render = this.createRenderFn()
this.mountComponent();
}
myVue.prototype.mountComponent = function () {
let mount = () => {
this.update( this.render() )
}
mount.call( this );
}
myVue.prototype.createRenderFn = function () {
let ast = getVNode( this._template );
return function render () {
let _tmp = combine( ast, this._data );
return _tmp;
}
}
myVue.prototype.update = function () {
// 简化, 直接生成 HTML DOM replaceChild 到页面中
}
let app = new myVue( {
el: '#root',
data: {
name: '张三', age: 19
}
} );
app.name = '李四'; // 这个赋值已完成, 页面数据就更新
</script>