Vue源码---虚拟Dom
真实dom
浏览器引擎渲染工作流程大致分为5步,创建dom树 -> 创建style Rules -> 创建render树 -> 布局layout -> 绘制painting
虚拟dom
虚拟dom节点,通过js的object 对象模拟dom中的节点,然后通过特定的render方法渲染成真实的dom节点
- 真实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)
}