Vue2源码共读-Vue2的Render渲染机制

164 阅读4分钟

数据劫持的概念

当我去操作数据的时候,知道他改了这个数据。

获取数据模板

if(vm.$options.el){
    // 如果有数据的话,那么就把用户的数据挂载到模板上。
    vm.$mount(vm.$options.el)
}
// 这个方法存在是为了如果用户没有$mount那么我们就允许用户手动指定el上去
Vue.prototype.$mount=function(el){
    el=document.querySelector(el)
}
// 上述这个方法处理过之后,我们就能拿到这个el了。

那么我们就需要考虑一个问题。如果说数据改变之后,我们html上的数据变化了,我们怎么通知vue进行改变

解决方法是我们更新html上挂载到vue上的节点数据的时候,我们可以通知vue进行改变,我们也可以使用虚拟dom。将真实dom传入,通过渲染函数将其变成虚拟dom。然后diff算法来模拟数据改变

将所有的节点数据都放在虚拟节点上,最后我们再产生真实节点进行更新,这样我们的性能会更高一些。

但是这里要注意一个点,那就是用户可能传入options的时候,里面带了一个render函数,这个函数代表用户想要返回他自己定义的虚拟节点。那么我们就渲染这个自己定义的函数,这个就是render的内容了。我们后面再讲。

Vue.prototype.$mount=function(el){
    let options=vm.$options
    if(vm.$options.render){
       let template=options.template
       // 如果说用户也没有传入template,那么我们就找el上面的outHTML进行打印
       if(!template&&el){
           template=el.outerHTML
           let render=complieToFunction(template)
           options.render=render
       }
    }
}

本质上我们要做的事情就是将template变成render函数

上面我们引入了一个方法叫做,compilerToFunction。这里就引出了Vue2渲染的一个关键类库,叫做compile库。通过这个库我们可以将真实节点转化为虚拟节点。方便我们的Vue去做处理。

我们今天从零开始手写一个Vue2的模板渲染器。可能和Vue2官方的不同,不过这个更加体现了创意。可能符合了活动规范,也算是我眼中的Vue2把。毕竟吃饭的东西。还是要把每个细节都把握好的。

词法分析器

下方的变量存放了用来解析模板的正则,后面我们将慢慢将这些模板都给他用上去

const ncname =`[a-zA-Z_ ][\\-\1.0-9 a-zA-Z]*`; // 标签名
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+I'([^']*)'+|([^\s"'=<> ]+)))?/; 
const startTagClose = /^\s*(\/?)>/;
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
const qnameCapture =`((?:${ncname}\\:)?${ncname})`; //<aa:xxx></aa:xxx>
const startTagOpen = new RegExp(`^<${ qnameCapture}`); 
const endTag = new RegExp(`<\\/$ {qnameCapture}[^>]*>`);

Vue并不是逐词解析的,他是用正则表达式来解析的,用来匹配各种各样的标签属性,这个模板的标签名,闭合标签。如果正常来写的话应该用状态机来逐词解析。然后引入状态机的概念来一个一个的解析。

  • ncname: 标签名
  • qnameCapture: 匹配特殊的标签 aa:xxx
  • startTagOpen: 匹配开始标签
  • endTag: 匹配闭合标签
  • attribute:匹配标签上传入的属性
  • startTagClose:匹配标签关闭
  • defaultTagRE:匹配马斯塔奇语法 {{aaa}}

我们拿到了template之后,我们需要解析我们的HTML,然后HTML解析成ast语法树

export function compileToFunction(template){
    // 解析开始标签
    function start(){
        
    }
    // 解析结束标签是谁
    function end(){
    
    }
    // 解析文本内容
    function chars(){
    
    }
}

这里我们方法封装一下

专门解析ast的方法

// 解析开始标签
    function start(){
        
    }
    // 解析结束标签是谁
    function end(){
    
    }
    // 解析文本内容
    function chars(){
    
    }
function parseHTML(html){ //html: <div>123123123</div>
     while(html){ // 看要解析的内容是否存在,存在就不停的解析。
         let textEnd=html.indexOf('<') // 看看是不是尖角号
         if(textEnd==0){
             const startTagMatch=parseStartTag() // 解析开始标签,但是可能解析不出来
             if(startTagMatch){
             }
             const endTagMatch=parseEndTag() // 解析结束标签,如果说一个<符号所在的标签不是开始符号就是结束符号
             if(startTagMatch){
             }
         }
     }
}

export function compileToFunction(template){
    parseHTML(template)
}

上面将一个标签传进去之后,我们用来解析开始标签和结束标签。下面介绍具体匹配开始和结束标签的方法

function parseStartTag(html){
    const start=html.match(startTagOpen)
    if(start){
        const match={
            tagName: start[1],
            attrs: []
        }
    }
    return false // 不是开始标签就直接跳过
}

那么我们现在里面判断了两种情况,如果说我们拿到了标签,那么我们就判断它是不是一个开始标签。如果不是函数标签的话呢,我们就直接返回false,走匹配文本的逻辑。如果是的话呢,我们就想办法拿到开始标签里面的属性。并且我们每次匹配将开始标签都给他删除掉,那么我们就需要写一个步进器:

function advance(len){
    html=html.substring(len) // 此处的html是parseHtml中的html
}
function parseStartTag(){
    const start=html.match(startTagOpen)
    if(start){
        const match={
            tagName: start[1],
            attrs: []
        }
        advance(html.length)
    }
    return false // 不是开始标签就直接跳过
}

我们标签删除完了,我们就需要开始匹配结束标识。将一个标签里面存在的所有属性给他提取出来。

function advance(len){
    html=html.substring(len) // 此处的html是parseHtml中的html
}
function parseStartTag(){
    const start=html.match(startTagOpen)
    if(start){
        // 用来记录处理标签属性和名称的空间
        const match={
            tagName: start[1],
            attrs: []
        }
        advance(html.length)
        let end=html.match(startTagClose)
        let attr=html.match(attribute)
        while(!end&&!attr){
            // 下方的3,4,5标识匹配标签和哪一个reg分组匹配
            match.attrs.push({name:attr[1],value:attr[3]||attr[4]||attr[5]})
            advance(attr[0].length) // 匹配到属性之后我们要将匹配过后的属性给他删除
        }
        if(end){
            advance(end.length) // 如果说end还有值的话,我们将结束标签也删除完
        }
        return match
    }
    return false // 不是开始标签就直接跳过
}

image.png

这里我们就可以把标签的名称和属性给他解析出来了。

标签解析完了,我们回到最开始的地方,现在我们已经知道它是一个开始标签了,我们就往后面走回到我们最开始遍历每个字符串的地方

while(html){
    let textEnd=html.indexOf('<');
    if(textEnd==0){
        const startTagMatch=parseStartTag()
        if(startTagMatch){ // 第一次解析完了直接退出循环
            start(startTagMatch.tagName,startTagMatch.attrs) // 解析开始标签读取到的属性
            break
        }
    }
    // 遇到文本了
    let text // 123123</div>
    if(textEnd>0){
        text=html.substring(0,textEnd) //123123
    }
    if(text){
        // 处理文本
        chars(text)
        advance(text.length) // 处理完文本之后删除文本
        break; // 告诉状态机文本也处理好了,处理结束标签去
    }
    
}

image.png

匹配闭合标签


while(html){
    let textEnd=html.indexOf('<');
    if(textEnd==0){
        const startTagMatch=parseStartTag()
        if(startTagMatch){ // 第一次解析完了直接退出循环
            start(startTagMatch.tagName,startTagMatch.attrs) // 解析开始标签读取到的属性
            break
        }
        const endTagMatch=html.match(endTag)
        if(endTagMatch){
            end(endTagMatch[1])
            advance(endTagMatch[0].length)
        }
    }
    // 遇到文本了
    let text // 123123</div>
    if(textEnd>0){
        text=html.substring(0,textEnd) //123123
    }
    if(text){
        // 处理文本
        chars(text)
        advance(text.length) // 处理完文本之后删除文本
    // 告诉状态机文本也处理好了,处理结束标签去
    }
    
}

现在我们的开始,结束,内容都已经能够正常拿出来了,下篇文章我们研究vue拿到这些标签和属性之后它会干些什么,它是怎么通过这些东西生成虚拟节点的

我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!