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)
}
相关推荐
Cachel wood19 分钟前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
学代码的小前端20 分钟前
0基础学前端-----CSS DAY9
前端·css
joan_8524 分钟前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
还是大剑师兰特1 小时前
什么是尾调用,使用尾调用有什么好处?
javascript·大剑师·尾调用
m0_748236111 小时前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
Watermelo6171 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_748248941 小时前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
m0_748235611 小时前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O3 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink6 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss