Vue笨蛋学原理:渲染模型

这不是Vue的源码
这不是Vue的源码
这不是Vue的源码
只是为了帮助咱们去理解Vue的思想

顺序

  1. 把el挂载到Vue实例上
  2. render函数生成虚拟DOM
  3. 虚拟DOM和数据结合渲染到页面上
  4. 数据一旦改变,立即生成新的虚拟DOM,新的虚拟DOM去和旧的虚拟DOM比较
  5. 更新页面

创建一个属于自己的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 中使用了 二次提交的 设计结构

  1. 在 页面中 的 DOM 和 虚拟 DOM 是一一对应的关系
  2. 先 有 AST 和 数据 生成 VNode ( 新, render )
  3. 将 就的 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>

猜你喜欢

转载自blog.csdn.net/m0_47883103/article/details/108650582