Vue2源码共读-虚拟节点转真实节点

104 阅读2分钟

上篇文章结尾代码:

function genProps(attrs){
    let str=''
    for(let i=0;i<attrs.length;i++){
        let attr=attrs[i]
        if(attr.name==='style'){
            let styleObj={}
            attr.value.replace(/([^;:]+):([^;:]+)/g,function(){
                styleObj[arguments[1]]=argument[2]
            })
            attr.value=styleObj
        }
        str+=`${attr.name}:${JSON.stringify(attr.value)}`
    }
    return `{${str.slice(0,-1)}}`
}
function gen(el){
    if(el.type==1){
        return generate(el)
    }else{
        let text=el.text
        if(!defaultTagRE.test(text)){
            return '_v('${text}')'
        }else{
            // 进行拆分 'hello'+arr+'world'
            let tokens=[]
            let match
            let lastIndex=0
            while(match==defaultTagRE.exec(text)){
                let index=match.index
                if(index>lastIndex){
                    tokens.push(JSON.stringify(text.slice(lastIndex)))
                }
                lastIndex=index+match[0].length
            }
            if(lastIndex<text.length){
                tokens.push(JSON.stringify(text.slice(lastIndex)))
            }
            return `_v(${tokens.join('+')})`
        }
    }
}
// 生成儿子
function genChildren(){
    let children=el.children
    if(children){
        return children.map(c=>gen(c)).join(",")
    }
    return false
}
// html字符串转化为字符串
export function generate(root){ //_c('div',{id:'app',a:1},_c('span',{},'world'),_v())
    console.log('------------',root)
    let code=`_c('${root.tag}',${
        el.attrs.length?:'undefined'
    }${
        children?`,${children}`:
    })` // 表明我们创建的标签名
    
    return code
}

这一段转换最关键的部分是语法之间的转化,很大意义上并不是代码上的转换。这会我们就需要看用户是否传入了el属性,如果没传入的话可能传入了template,如果说template也没有传递的话我们将做如下的检查判断

书接上文,我们将一段文本的标签放入了_c(),文本放入了_v(),马斯塔奇放入了_s().我们得到了如下代码:

export function compileToFunction(template){
    // 生成代码
    let code=codegen(root)
    
    let render=new Function(`with(this){return ${code}}`)
    console.log(render.toString())
    render.call(vm)
}

在compile库下新建一个文件,命名为generator.js

template中的马斯塔器this问题

很多刚开始写Vue的小白老是会犯一个非常严重的错误,那就是因不应该在template的马斯塔器里面调用data里面的属性上用this。这里可以非常负责的说,在Vue2中是完全可以的,这里可以牵扯出一个JavaScript函数with的问题。以下面这个函数举例:

fn({arr:[1,2,3]})
function fn(vm){
    console.log(arr)
}

上面这个function是内部是没有定义arr的。但是我们可以直接使用vm这个函数内部的arr。这会我们这个this指向的是vm。我们直接使用arr可以等价为this.arr。在Vue template的马斯塔器语法中这个类似

那么引入到我们上面的new Function(with(this){return ${code}}) 我们这里的this其实指向的就是template了,然后code就是template对象里面的code。然后这样写完之后,我们就解决了一个问题,一般用户是不会传render方法的,除非它有渲染自定义h函数的需求。但是用户一定会传el或template。因为不传这两个我们就会给他报错。我们现在在做的,一直就是我们上篇文章说的。我们要将Vue的template和el都转成render。然后生成虚拟dom,我们再去处理。好的,现在我们来编写下这部分的关键代码;

Vue.prototype.$mount=function(el){
    const vm=this
    const options=vm.$options
    el=document.querySelector(el)
    if(!options.render){
        let template=options.template
        if(!template&&el){
            template=el.outerHTML
            let render=compileToFunction(template)
            options.render=render
        }
    }
    // options.render就是渲染函数
    console.log(options.render) // 调用render方法,渲染成真实dom,替换掉页面内容
    mountComponent(vm,el) // 组件的挂载流程
}

实现挂载流程

function mountComponent(vm,el){
    // 数据变化后会再次调用此方法
    let updateComponent=()=>{
        // 调用render函数生成虚拟dom
        vm._update(vm._render()) // 后续更新可以调用updateComponent
        // 用虚拟dom 生成真实dom
    }
    updateComponent()
}

在创建实例的时候扩展原型的update和render方法

// Vue的入口文件 src/index.js
import {initMixin} from './init'
import {lifecycleMixin} from './lifecycle'
function Vue(options){
    this._init(options)
}
initMixin(Vue)
renderMixin(Vue)
lifecycleMixin(Vue)
export default Vue;
//src/render.js
export function renderMixin(Vue){
    Vue.prototype._render=function(){
        const vm=this
        let render=vm.$options.render // 就是我们转义,解析,上篇文章得到的function,也可能是用户写的
        let vnode=render.call(vm)
    }
}
//src/lifecycle.js
export function lifecycleMixin(Vue){
    Vue.prototype._update=function(vnode){
        
    }
}
export function mountComponent(vm,el){
    let updateComponent=()=>{
        vm._update(vm._render())
    }
    updateComponent()
}

好的,现在我们重新回顾下,_C为解析标签,_v为解析文本,_s为解析马斯塔奇,我们经历了3000多个字,终于到达这个关键的地方了。

// 标签解析传入三个参数,标签名,属性,孩子
Vue.prototype._c=function(tagName,attr,...children){
    return createElement(this,...arguments)
}
Vue.prototype._v=function(text){
    return createTextElement(this,text)
}
Vue.prototype._s=function(val){
    if(typeof val=='object'){
       return JSON.stringify(val)
    }
    return val
    
}
// src/vdom/index.js
export function createElement(vm,tag,data={},...children){
    return vnode(vm,tag,data,data.key,children,undefined)
}
export function createTextElement(vm,text){
    return vnode(vm,undefined,undefined,undefined,undefined,text)
}
function vnode(vm,tag,data,key,children,text){
    return {
        vm,
        tag,
        data,
        key,
        children,
        text,
        
    }
}

将虚拟dom创建为真实dom

// src/lifecycle.js
import {patch} from './patch.js'
export function lifecycleMixin(Vue){
    Vue.prototype._update=function(vnode){
        const vm=this
        // 核心的diff流程,第一次更新可能是真实节点,但是第二次进来一定是虚拟节点
        patch(vm.$el,vnode)
    }
}
export function mountComponent(vm,el){
    let updateComponent=()=>{
        vm._update(vm._render())
    }
    updateComponent()
}
// src/init.js
Vue.prototype.$mount=function(el){
    const vm=this
    const options=vm.$options
    vm.$el=el 
    if(!options.render){
        let template=options.template
        if(!template&&el){
            template=el.outerHTML
            let render=compileToFunction(template)
            options.render=render
        }
    }
    mountComponent(vm,el)
}
//src/vnode/patch.js
export function patch(oldVnode,vnode){
    if(oldVnode.type==1){
        // 用vnode来生成真实dom,替换掉原本的dom元素
        const parentElm=oldVnode.parentNode // 找到他的父亲
        let elm=createElm(vnode) // 根据虚拟节点创建元素
        parentElm.insertBefore(oldVnode)
        parentElm.removeChild(oldVnode) 将这个元素给他删掉
        
    }
}
function createElm(vnode){
    let {tag,data,children,text,vm}=vnode
    if(typeof vnode.tag=='string'){ // 它是一个元素
        vnode.el=document.createElement(tag) // 虚拟节点会有一个el属性,对应真实节点
        children.forEach(child=>{
            vnode.el.appendChild(createElm(child))
        })
    }else{
        vnode.el=document.createTextNode(text)
    }
    return vnode.el
}

虚拟节点通过上述逻辑转化为了真实节点

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