Vue源码—虚拟Dom
真实dom
浏览器引擎渲染工作流程大致分为5步,创建dom树 -> 创建style Rules -> 创建render树 -> 布局layout -> 绘制painting
虚拟dom
虚拟dom节点,通过js的object 对象模拟dom中的节点,然后通过特定的render方法渲染成真实的dom节点
- 真实dom性能开销大
let div = document.createElement('div')
let str = ''
for(var key in div) {str += key +''
}
真实的dom元素非常庞大。
使用正则表达式,解析出ast树
代码如下
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 = 1const TEXT_TYPE = 3const 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 = currentParentcurrentParent.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, endwhile (!(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 说明就是文本的结束位置debuggerlet 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树转换为模板字符串,将编译出来的模板,形成渲染函数
import { parseHTML } from "./parse";const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g // {{xxx}}
function gen(node) {if (node.type === 1) { // 元素return codegen(node)} else {// 文本let text = node.textif (!defaultTagRE.test(text)) {return `_v(${JSON.stringify(text)})`} else {let tokens = []let matchdefaultTagRE.lastIndex = 0let lastIndex = 0while (match = defaultTagRE.exec(text)) {let index = match.indexif (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 Functioncode= `with(this){return ${code}}`console.log(code)let render = new Function(code)console.log(render)// 根据代码生成render函数return render
}
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 = 1const TEXT_TYPE = 3const 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 = currentParentcurrentParent.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, endwhile (!(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
import {createElementVNode, createTextVNode} from "./vdom/index";export function initLiftCycle(Vue) {Vue.prototype._update = function (vnode) {const vm = thisconst el = vm.$elvm.$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 valuereturn JSON.stringify(value)}
}
function patch(oldVNode, vnode) {const isRealElement = oldVNode.nodeTypeif (isRealElement) {const elm = oldVNodeconst parentElm = elm.parentNodelet newElm = createElm(vnode)parentElm.insertBefore(newElm,elm.nextSibling)parentElm.removeChild(elm)return newElm}else {// diff算法}
}
function createElm(vnode) {let { tag, data, children, text } = vnodeif (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 = elvm._update(vm._render())console.log(vm._update)
}
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.keyif (key) {delete data.key}return vnode(vm, tag, key ,data, children)
}
export function createTextVNode(vm, text) {return vnode(vm, undefined, undefined, undefined, undefined, text)
}