4. 将ast树变成render函数并渲染真实dom

36 阅读3分钟

前言

这篇文章渲染模板到页面不包含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 => 放到页面上