前言
这篇文章渲染模板到页面不包含diff算法
改动src文件目录
- compile
- index.js
- generate.js // 处理成render字符串
- parseAst.js // 将html解析成ast树
- vnode
- index.js // 创建虚拟dom
- patch.js // 解析虚拟dom生成真实dom
- index.js
- init.js
- lifeCycle.js
compile文件夹改动
提取index.js文件中生成的ast语法树代码到parseAst.js文件中
parseAst.js文件:
let root // 表示根元素
let createParent // 当前元素的父元素
let stack = [] // 数据结构 栈
const attribute =
/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 获取属性
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*` // 获取标签名称
// const qnameCapture = `((?:${ncname}\\:)?${ncname})` //
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`) // 标签开头的正则 捕获的内容是标签名
const startTagClose = /^\s*(\/?)>/ // 匹配结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
export function parseHTML(html) {
// 遍历 html 为空就结束
while (html) {
// 判断标签
let textEnd = html.indexOf('<');
let text
if (textEnd === 0) { // 标签
// 有两种可能开始标签 和 结束标签
let 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) {
// 获取到文本内容
text = html.substring(0, textEnd)
}
if (text) {
// 删除文本
advance(text.length)
charts(text)
}
}
function parseStartTag() {
// 匹配开始标签
let start = html.match(startTagOpen);
if (!start) {
return
}
let match = {
tagName: start[1],
attrs: [],
}
// 删除开始标签
advance(start[0].length)
// 遍历属性
let attr
let end
while (!(end= html.match(startTagClose)) && (attr = html.match(attribute))){
match.attrs.push({ name:attr[1],value:attr[3]||attr[4]||attr[5] })
advance(attr[0].length)
}
if (end) {
advance(end[0].length)
return match
}
}
function advance(n) {
html = html.substring(n)
}
return root
}
// 遍历开始的标签
function start(tagName,attrs) {
let element = createASTElement(tagName, attrs)
if (!root) {
root=element
}
createParent = element
stack.push(element)
}
// 遍历文本标签
function charts(text) {
// 空格
text = text.replace(/ /g, '')
if (text) {
createParent.children.push({type:3,text:text})
}
}
// 遍历结束标签
function end(tagName) {
let element = stack.pop() // 获取最后的元素进行出栈
createParent=stack[stack.length-1]
if (createParent) { // 元素闭合
element.parent = createParent.tag
createParent.children.push(element)
}
}
function createASTElement(tag,attrs) {
return {
tag, // 表示元素
attrs, // 表示属性
children: [], // 是否有子节点
type: 1,
parent: null,
}
}
generate.js处理ast语法树成render字符串:
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
/**
*
* @param {Object} el - 传入ast对象
* @returns {String}
*/
export function generate(el) {
let children = genChildren(el);
let code = `_c('${el.tag}',${el.attrs.length ? `${genPorps(el.attrs)}` : undefined}${children?`,${children}`:undefined})`
return code
}
// 处理属性
function genPorps(attrs) {
let str = "";
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
// 将 style 行内样式转换成对象形式
if (attr.name === "style") {
let obj = {}
attr.value.split(";").forEach(item => {
let [key, val] = item.split(":");
obj[key] = val;
});
attr.value = obj;
// str += `style:${genStyle(attr.value)},`;
}
str += `${attr.name}:${JSON.stringify(attr.value)},`
}
return `{${str.slice(0,-1)}}`
}
// 处理子节点
function genChildren(el) {
let children = el.children;
if (children && children.length) {
return children.map(child => gen(child)).join(",")
}
return null
}
function gen(node) {
// 1 元素
if (node.type === 1) {
return generate(node)
} else { // 3文本 (两种情况):一种是文本 一种有插值语法
let text = node.text;
if (!defaultTagRE.test(text)) {
return `_v(${JSON.stringify(text)})`
}
// 解析 {{}} 表达式
let tokens = []
let lastindex = defaultTagRE.lastIndex = 0;
let match
while (match=defaultTagRE.exec(text) ){
const index = match.index
if (index > lastindex) {
tokens.push(JSON.stringify(text.slice(lastindex, index)))
}
tokens.push(`_s(${match[1].trim()})`) // _s(msg)
lastindex = index + match[0].length
}
if (lastindex < text.length) {
// 将文本与模板后面的内容也一起push上去
tokens.push(JSON.stringify(text.slice(lastindex)))
}
return `_v(${tokens.join('+')})`
}
}
index.js文件将render字符串转为render函数并返回:
import { parseHTML } from "./parseAst.js"
import { generate } from "./generate.js"
/***
* @description 生成ast语法树
* @param {string} template
*
*/
export function compileToFunction(template) {
// 解析HTML 变成ast语法树
let ast = parseHTML(template)
// ast语法树 -> 字符串 -> 变成render字符串
let code = generate(ast) // _c 元素 _v 文本 _s 变量
// 将字符串变成函数
let render = new Function(`with(this){return ${code}}`)
return render
}
init.js文件改动
import { initState } from "./initState";
import { compileToFunction } from "./compile/index";
import { mounetComponent } from "./lifeCycle";
/**
* @description 初始化vue
* @param {Object} Vue
* @returns {void}
*/
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
let vm = this
vm.$options = options
// 初始化状态
initState(vm)
// 渲染模板
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
// 创建$mount 进行模板编译
Vue.prototype.$mount = function (el) {
let vm = this
// 获取dom元素
el = document.querySelector(el)
vm.$el= el
let options = vm.$options
// 没有render函数
if (!options.render) {
let template = options.template
if (!template && el) {
// 获取html
el = el.outerHTML
// 转换成ast语法树 vnode(虚拟dom) [ast语法树是能操作js和css 虚拟dom只能操作一个节点]
let render = compileToFunction(el)
// render函数 将函数变成vnode
options.render = render
}
}
// 执行生成模板
mounetComponent(vm, el)
}
}
根目录index.js文件改动
import { initMixin } from "./init"
import { lifeCycleMixin } from "./lifeCycle";
import { renderMixin } from "./vnode/index";
/**
* @author xwya
* @since 2023-12-11
* @description Vue 构造函数
* @param {Object} options - Vue 的初始化选项。
* @returns {void} - 没有返回值。
*/
function Vue(options) {
// 初始化
this._init(options);
}
initMixin(Vue)
lifeCycleMixin(Vue) // 添加生命周期
renderMixin(Vue) // 添加_render
export default Vue;
lifeCycle.js文件生成html模板
import { patch } from "./vnode/patch";
export function mounetComponent(vm, el) {
// (1) vm._render 将render转换成虚拟dom (vnode)
// (2) vm._update 将vnode变成真实dom在放到页面上
vm._updata(vm._render())
}
export function lifeCycleMixin(Vue) {
Vue.prototype._updata = function (vnode) {
let vm = this
// 传两个参数 旧的dom 和 vnode
vm.$el= patch(vm.$el, vnode)
}
}
vnode文件夹
index.js处理render函数变成vnode:
export function renderMixin(Vue) {
// 解析文本
Vue.prototype._c = function () {
return createElement(...arguments)
}
// 解析元素
Vue.prototype._v = function (text) {
return createText(text)
}
// 解析变量
Vue.prototype._s = function (val) {
return val == null ? '' : typeof val === 'object' ? JSON.stringify(val) : val
}
Vue.prototype._render = function () {
let vm = this
let render = vm.$options.render
let vnode = render.call(this)
return vnode
}
}
// 创建元素
function createElement(tag, data = {}, ...children) {
return vnode(tag,data,data.key,children)
}
// 创建虚拟dom
function vnode(tag, data,key,children,text) {
return {
tag,
data,
key,
children,
text
}
}
// 创建文本
function createText(text) {
return vnode(undefined,undefined,undefined,undefined,text)
}
patch.js文件将vnode替换成真实dom:
export function patch(oldnode,vnode) {
console.log(oldnode, vnode, "准备将虚拟dom变成真实dom");
// 创建新的dom
let el = createEl(vnode)
// 替换
let parentEl = oldnode.parentNode // 父节点
parentEl.replaceChild(el, oldnode) // 替换
return el
// console.log(el,parentEl);
}
// 创建dom
function createEl(vnode) {
let { tag, children, key, data, text } = vnode
if (typeof tag === "string") {
vnode.el = document.createElement(tag)
if (children.length > 0) {
children.forEach(item => {
vnode.el.appendChild(createEl(item))
})
}
} else {
vnode.el = document.createTextNode(text)
}
return vnode.el
}
总结vue渲染流程
初始化数据
=>对模板进行编译
=>变成render函数
(ast语法树 => render字符串 => render函数)
=>通过render函数变成vnode
=>将vnode虚拟节点转为真实dom
=> 放到页面上