由繁到简: Vue component是如何生成的

204 阅读4分钟

前言

Vue的解析过程: 读取template => 解析生成AST => 拼接成render字符串 => new Function执行render字符串生成vnode => patch生成真实DOM

今天我们就从“render字符串 ”阶段开始,化繁为简,从Vue源码中抽取核心代码,然后进行手写,实现从render字符串到真实DOM的渲染

注: 

当前都不涉及动态属性,因为动态属性就是使用了with函数改变作用域从而实现数据访问

下面全文所说的vnode在Vue里面其实也就是一个存数据的class类,这里使用一个对象表示vnode

本人由于表达能力不好,因为会尽量多到少说话,多做事的...

手写实现

1.解析DOM

解析成vnode,render函数实现

模板:

  <div id="app">hello world</div>

解析后的render字符串

 _c('div',{attrs:{"id":"app"}},[_v(" hello world ")])

 实现 _v 方法

function _v(val){
    return {  text: val  }
}

实现 _c 方法

function _c(tag, data, children){
   //规范数据格式,如果当前标签不存在attributes时
   //生成的函数data里面的value就是其children     
   if (Array.isArray(data)) {       
     children = data       
     data = undefined   
   }  
   return { tag: tag, data: data, children}
}

没看错,就这样简单,这样子下来将render字符串执行后得到的结果为

let vnode =  _c('div',{attrs:{"id":"app"}},[_v(" hello world ")])

//vnode:  
{  
    tag: "div", 
    data: { "id":"app" }, 
    children: [ { text: 'hello world' } ]
}

vnode渲染,patch函数实现

我们知道,

hello world
被解析完毕后需要被替换掉,那么就需要

获取当前DOM元素的父级,以及当前DOM元素是父级的第几个元素

function insert(parent, elm, ref) {    
    if (ref !== undefeind) {        
        parent.insertBefore(elm, ref)    
    } else {        
        parent.appendChild(elm)    
    }
}
 function createElm(vnode, parentElm, refElm) {       
    if (vnode.tag !== undefined) {           
        //DOM节点           
        vnode.elm = document.createElement(elm.tag)           
        if (Array.isArray(vnode.children)) {              
            for (child of vnode.children) {                   
                createElm(child, vnode.elm, null)               
            }           
        }
        insert(parentElm, vnode.elm, refElm)       
    } else {            
        //文本节点           
        vnode.elm = document.createTextNode(vnode.text)           
        insert(parentElm, vnode.elm, refElm)      
    }
}


function patch(oldvnode,vnode){
    if(oldvnode.nodeType !== undefined){
        oldvnode = { tag: oldvnode.tagName.toLowerCase(),  elm: oldvnode }
    }else{
        //diff比较差异阶段
    }    
    let parentElm = document.getElementById('app').parentNode
    //挂载真实DOM
    createElm(
        vnode, 
        parentElm, 
        document.getElementById('app').nextSibling
    )
    //删除模板
    if(parentElm !== null){      
        parentElm.removeChild(oldvnode.elm)
    }
}

测试

 let vnode = _c('div', { attrs: { "id": "app" } }, [_c('p', [_v("hello world")])])
 let realDom = patch(document.getElementById('app'), vnode) console.log(realDom); 
//<div><p>hello world</p></div>

小节

_c函数的作用其实就是识别标签类型然后创建对应的vnode对象,作用仅此而已,不用被render字符串的复杂性搞乱头像,

patch函数是对vnode进行递归遍历然后生成真是DOM保存刀vnode.elm属性里面并且根据关系挂载到页面上

2.解析组件

解析成vnode,render函数实现

 模板

 <div id="app"><children></children> </div>
//children组件
children: {   template: '<p>children hello world</p>'}

解析后的render字符串

_c('div',{attrs:{"id":"app"}},[_c('children')]) 

组件是作为特殊的一个载体,内容都在内部,就好像一个大球套着一个小球,当前解析只是解析到了div和children标签,组件children内的内容并没有被解析到,那children里面的内容何时解析呢?这里我们知道“children”它不是内置的html标签,因此我们需要定义一个数组来区分内置html标签和自定义标签,修改_c函数

function _c(tag, data, children){
   //规范数据格式,如果当前标签不存在attributes时
   //生成的函数data里面的value就是其children     
   if (Array.isArray(data)) {       
     children = data       
     data = undefined   
   }  
+   let vnode = {}                            
+   let isHTML = ['div','p','span','ul','li'] 
+   if(isHTML.some(tag => tag)){    
+      //内置html标签          
+      vnode = { tag: tag, data: data, children}  
+   }else if(/** 这里需要判断配置项components里面是否存在有与tag对应的组件 */){
+      //自定义组件
+      vnode = { 
+        tag: 'vue-component-'+ 全局递增id +tag, 
+        data, 
+        children, 
+        componentOptions: { 
+             tag,
+             //Ctor就是从component里面得到的组件配置项children,然后通过Vue.extend生成的一个构造器
+             Ctor,
+             componentInstance: null //Ctor组件被new后返回的实例化对象
+         } 
+      }
+   }else{
+      //既不是html标签也不是组件
+      vnode = { tag, data, children }   
+   }                         
+   return vnode
-   return { tag: tag, data: data, children }
}

Vue的每个组件其实都是通过Vue.extend方法继承得到的,在生成vnode阶段,如果解析到了某个组件的标签,那么就去配置项component里面找到对应的组件,然后获取组件的对应的配置项data, method, template这些属性,然后以这个配置项继承得到一个组件构造器保存到vnode里面

let vnode = _c('div',{attrs:{"id":"app"}},[_c('children')]) 
//vnode:
{
    tag: "div",
    data: { attr: { "id": "app" } }
    [        {            tag: "vue-component-1-children",            data: null,            children: null            componentOptions: {                tag: "children",                Ctor,                componentInstance: null            }        }    ]
}

vnode渲染,patch函数实现

人狠话不多,直接对createElm函数进行改造

function createElm(vnode, parentElm, refElm) {
+    if(vnode.componentOptions !== null){
+        let child = new vnode.componentOptions.Ctor(vnode)
+        //开始进入child的template解析阶段
+        child.$mount(vnode.elm)
+    }
       
    if (vnode.tag !== undefined) {           
        //DOM节点           
        vnode.elm = document.createElement(elm.tag)           
        if (Array.isArray(vnode.children)) {              
            for (child of vnode.children) {                   
                createElm(child, vnode.elm, null)               
            }           
        }
        insert(parentElm, vnode.elm, refElm)       
    } else {            
        //文本节点           
        vnode.elm = document.createTextNode(vnode.text)           
        insert(parentElm, vnode.elm, refElm)      
    }
}

就这样简单,因为createElm是递归解析vnode的,如果遇到组件之间new它的构造器,然后调用其mount方法,mount方法,mount方法是Vue.extend继承的时候得到的,这时候就开始进入了组件的解析环境,一直到解析到最深度的一个组件,因此这就是Vue的生活钩子函数为什么是:

created(父) created(子1) created(子2) mounted(子2) mounted(子1) mounted(父)的原因

用一个脑图来表示解析过程就是:

总结:

当前是省略了大量的细节,简单的介绍了一下Vue里面的组件是如何渲染成DOM元素,这里只是简单的介绍了一下组件如何渲染,并没有包括组件的传值,插槽等,这些只是附加内容,因为创建vnode时有父子关系,实现传值,插槽这些只需要通过父子关系就可以得到对应的数据了,而DOM的动态属性,事件,以及生命钩子这些因为vnode里面保存了最完整的属性,因此只需要在对应阶段解析其属性即可