Vue源码---虚拟Dom

Vue源码---虚拟Dom

真实dom

浏览器引擎渲染工作流程大致分为5步,创建dom树 -> 创建style Rules -> 创建render树 -> 布局layout -> 绘制painting

虚拟dom

虚拟dom节点,通过js的object 对象模拟dom中的节点,然后通过特定的render方法渲染成真实的dom节点

  1. 真实dom性能开销大
javascript 复制代码
let div = document.createElement('div')
let str = ''
for(var key in div) {
    str += key +''
}

真实的dom元素非常庞大。

使用正则表达式,解析出ast树

代码如下

javascript 复制代码
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`);   // 开始标签<xxx>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)  // 结束标签</xxx>
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/  // 匹配属性
const startTagClose = /^\s*(\/?)>/
export function parseHTML(html) {    // 解析一个删除一个,直到全部解析完成

    const ELEMENT_TYPE = 1
    const TEXT_TYPE = 3
    const stack = []  // 用于存放元素
    let currentParent    // 指向栈中的最后一个
    let root
    // 生成AST节点
    function createASTElememt(tag, attrs) {
        return {
            tag,
            type: ELEMENT_TYPE,
            children: [],
            attrs,
            parent: null
        }
    }

    function start(tag, attrs) {
        let node = createASTElememt(tag, attrs)    // 创建一个ast节点
        if (!root) {  // 判断是否为空树
            root = node     // 如果为空,则当前的树为根节点
        }
        if (currentParent) {
            node.parent = currentParent
            currentParent.children.push(node)
        }
        stack.push(node)
        currentParent = node     // currentParent为栈中的最后一个
    }

    // 匹配文本
    function chars(text) {      // 文本直接放到当前指向的节点中
        text = text.replace(/\s/g, '')
        text && currentParent.children.push({
            type: TEXT_TYPE,
            text,
            parent: currentParent
        })
    }
    // 结束
    function end() {
        let node = stack.pop()
        currentParent = stack[stack.length - 1]
    }
    // 对这个文件中html字符串进行减少,作为判断后续while循环结束的标记
    function advance(n) {
        html = html.substring(n)
    }
    //匹配开始标签
    function parseStartTag() {    // 获取开始标签
        const start = html.match(startTagOpen)
        if (start) {
            const match = {
                tagName: start[1],   //标签名
                attrs: []                // 属性
            }
            advance(start[0].length)
            let attr, end
            while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {   // 匹配属性
                advance(attr[0].length)
                match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5]})
            }
            if (end) {
                advance(end[0].length)
            }
            console.log("match",match);
            return match
        }
        return false  // 不是开始标签
    }
    // 循环实现ast树的构建
    while (html) {
        // 如果textEnd == 0 说明是开始标签或者结束标签
        // 如果textEnd > 0 说明就是文本的结束位置
        debugger
        let textEnd = html.indexOf('<')

        if (textEnd == 0) {
            const startTagMatch = parseStartTag()   // 开始标签的匹配结果
            if (startTagMatch) {  // 解析到的开始标签
                start(startTagMatch.tagName, startTagMatch.attrs)
                continue
            }
            let endTagMatch = html.match(endTag)
            if (endTagMatch) {
                advance(endTagMatch[0].length)
                end(endTagMatch[1])
                continue
            }
        }
        if (textEnd > 0) {
            let text = html.substring(0, textEnd)
            if (text) {
                chars(text)
                advance(text.length)
            }

        }
    }
    // console.log(root)
    return root
}

这个函数返回的是一个ast树的格式。attrs 表示属性,children表示嵌套的子盒子, parent表示嵌套中的父盒子,tag表示标签,即该节点表示的盒子的类型。type表示节点的类型,如果type = 1 表示的是html标签,如果是type = 3,则表示的是文本类型

将ast树转换为模板字符串,将编译出来的模板,形成渲染函数

javascript 复制代码
import { parseHTML } from "./parse";

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g    // {{xxx}}
function gen(node) {
    if (node.type === 1) {    // 元素
        return codegen(node)
    } else {
        // 文本
        let text = node.text
        if (!defaultTagRE.test(text)) {
            return `_v(${JSON.stringify(text)})`
        } else {
            let tokens = []
            let match
            defaultTagRE.lastIndex = 0
            let lastIndex = 0
            while (match = defaultTagRE.exec(text)) {
                let index = match.index
                if (index > lastIndex) {
                    tokens.push(JSON.stringify(text.slice(lastIndex, index)))
                }
                tokens.push(`_s(${match[1].trim()})`)
                lastIndex = index + match[0].length
            }
            if(lastIndex<text.length){
                tokens.push(JSON.stringify(text.slice(lastIndex)))
            }
            return `_v(${tokens.join('+')})`
        }
    }
}
// children
function genChildren(children) {
    return children.map(child => gen(child)).join(',')
}

// 属性
function genProps(attrs) {
    let str = ''
    for (let i = 0; i < attrs.length; i++) {
        let attr = attrs[i]
        if (attr.name == 'style') {
            let obj = {}
            attr.value.split(';').forEach(item => {     // qs库
                let [key, value] = item.split(':')
                obj[key] = value
            })
            attr.value = obj
        }
        str += `${attr.name}:${JSON.stringify(attr.value)},`
    }
    return `{${str.slice(0, -1)}}`
}

function codegen(ast) {
    let children = genChildren(ast.children)
    let code = `_c('${ast.tag}',${ast.attrs.length > 0 ? genProps(ast.attrs) : null
    }${ast.children.length ? `,${children}` : ''
    })`
    return code
}
export function compileToFunction(template) {
     const ast = parseHTML(template)
     let code = codegen(ast)
    console.log(code)
    // 模板引擎的实现原理  with + new Function

    code= `with(this){return ${code}}`
    console.log(code)
    let render = new Function(code)
    console.log(render)
    // 根据代码生成render函数
    return render
}
javascript 复制代码
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`);   // 开始标签<xxx>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)  // 结束标签</xxx>
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/  // 匹配属性
const startTagClose = /^\s*(\/?)>/
export function parseHTML(html) {    // 解析一个删除一个,直到全部解析完成
    const ELEMENT_TYPE = 1
    const TEXT_TYPE = 3
    const stack = []  // 用于存放元素
    let currentParent    // 指向栈中的最后一个
    let root
    // 生成AST节点
    function createASTElememt(tag, attrs) {
        return {
            tag,
            type: ELEMENT_TYPE,
            children: [],
            attrs,
            parent: null
        }
    }

    function start(tag, attrs) {
        let node = createASTElememt(tag, attrs)    // 创建一个ast节点
        if (!root) {  // 判断是否为空树
            root = node     // 如果为空,则当前的树为根节点
        }
        if (currentParent) {
            node.parent = currentParent
            currentParent.children.push(node)
        }
        stack.push(node)
        currentParent = node     // currentParent为栈中的最后一个
    }

    // 匹配文本
    function chars(text) {      // 文本直接放到当前指向的节点中
        text = text.replace(/\s/g, '')
        text && currentParent.children.push({
            type: TEXT_TYPE,
            text,
            parent: currentParent
        })
    }
    // 结束
    function end() {
        let node = stack.pop()
        currentParent = stack[stack.length - 1]
    }
    // 对这个文件中html字符串进行减少,作为判断后续while循环结束的标记
    function advance(n) {
        html = html.substring(n)
    }
    //匹配开始标签
    function parseStartTag() {    // 获取开始标签
        const start = html.match(startTagOpen)
        if (start) {
            const match = {
                tagName: start[1],   //标签名
                attrs: []                // 属性
            }
            advance(start[0].length)
            let attr, end
            while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {   // 匹配属性
                advance(attr[0].length)
                match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5]})
            }
            if (end) {
                advance(end[0].length)
            }
            // console.log("match",match);
            return match
        }
        return false  // 不是开始标签
    }
    // 循环实现ast树的构建
    while (html) {
        // 如果textEnd == 0 说明是开始标签或者结束标签
        // 如果textEnd > 0 说明就是文本的结束位置
        let textEnd = html.indexOf('<')

        if (textEnd == 0) {
            const startTagMatch = parseStartTag()   // 开始标签的匹配结果
            if (startTagMatch) {  // 解析到的开始标签
                start(startTagMatch.tagName, startTagMatch.attrs)
                continue
            }
            let endTagMatch = html.match(endTag)
            if (endTagMatch) {
                advance(endTagMatch[0].length)
                end(endTagMatch[1])
                continue
            }
        }
        if (textEnd > 0) {
            let text = html.substring(0, textEnd)
            if (text) {
                chars(text)
                advance(text.length)
            }

        }
    }
    console.log(root)
    return root
}

将生成的render函数,转换为虚拟dom

javascript 复制代码
import {createElementVNode, createTextVNode} from "./vdom/index";

export function initLiftCycle(Vue) {
    Vue.prototype._update = function (vnode) {
        const vm = this
        const el = vm.$el
        vm.$el = patch(el, vnode)
    }
    Vue.prototype._render = function () {
        return this.$options.render.call(this)
    }
    Vue.prototype._c = function () {
        return createElementVNode(this, ...arguments)
    }
    Vue.prototype._v = function () {
        return createTextVNode(this, ...arguments)
    }
    Vue.prototype._s = function (value) {
        if (typeof value != 'object') return value
        return JSON.stringify(value)
    }
}
function patch(oldVNode, vnode) {
    const isRealElement = oldVNode.nodeType
    if (isRealElement) {
        const elm = oldVNode
        const parentElm = elm.parentNode
        let newElm = createElm(vnode)
        parentElm.insertBefore(newElm,elm.nextSibling)
        parentElm.removeChild(elm)
        return newElm
    }else {
        // diff算法
    }
}
function createElm(vnode) {
    let { tag, data, children, text } = vnode
    if (typeof tag == 'string') {
        vnode.el = document.createElement(tag)   // 将真实节点和虚拟节点对应起来
        patchProps(vnode.el, data)
        children.forEach(child => {
            vnode.el.appendChild(createElm(child))
        });
    } else {
        vnode.el = document.createTextNode(text)
    }
    return vnode.el
}
function patchProps(el, props) {   // 处理属性的方法
    for (let key in props) {
          for (let key in props) {
        if (key === 'style') {
            for (let styleName in props.style) {
                el.style[styleName] = props.style[styleName]
            }
        } else {
            console.log(key, props[key])
            el.setAttribute(key, JSON.stringify(props[key]))
        }
        }
    }
}
export function mountComponent(vm, el) {
    vm.$el = el
    vm._update(vm._render())
    console.log(vm._update)
}
javascript 复制代码
function vnode(vm, tag, key, data,children, text) {
    return {
        vm,
        tag,
        key,
        data,
        children,
        text
    }
}
export function createElementVNode(vm, tag, data = {}, ...children) {
    if (data == null) {
        data = {}
    }
    let key = data.key
    if (key) {
        delete data.key
    }
    return vnode(vm, tag, key ,data, children)
}
export function createTextVNode(vm, text) {
    return vnode(vm, undefined, undefined, undefined, undefined, text)
}
相关推荐
永乐春秋2 分钟前
WEB攻防-通用漏洞&文件上传&js验证&mime&user.ini&语言特性
前端
鸽鸽程序猿3 分钟前
【前端】CSS
前端·css
ggdpzhk5 分钟前
VUE:基于MVVN的前端js框架
前端·javascript·vue.js
小曲曲1 小时前
接口上传视频和oss直传视频到阿里云组件
javascript·阿里云·音视频
学不会•2 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
EasyNTS3 小时前
H.264/H.265播放器EasyPlayer.js视频流媒体播放器关于websocket1006的异常断连
javascript·h.265·h.264
活宝小娜5 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点5 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow5 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o5 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app