Vue模板编译原理

214 阅读3分钟

1.元素挂载顺序

vue的元素挂载有顺序之分,如果存在render函数,就用render函数渲染组件;如果没有render函数,但是存在template,就将template中的内容编译成render函数,最后做渲染;如果既没有render函数也没有template函数,就获取el里的内容作为template,同样编译成render函数。因此从模板编译成render函数就是vue的关键点。

  Vue.prototype.$mount = function (el) {
    // 挂载
    const vm = this;
    const options = vm.$options;
    el = document.querySelector(el);
    vm.$el = el;
    if(!options.render) { // 没有render方法
      let template = options.template;
      if(!template && el){ // 没有template 但是有el 获取el中的内容
        template = el.outerHTML
      }
      // 将模板编译成render函数
      const render = compileToFunctions(template)
      options.render = render;
    }
    // 渲染时用的都是render函数
    // 挂载组件
    mountComponent(vm,el);
  }

2.模板编译函数compileToFunctions

从模板到render函数主要分为三个部分:

  • 1.将html代码转换成ast语法树
  • 2.优化静态节点
  • 3.通过ast树 重新生成代码
export function compileToFunctions(template) {
  // html模板 => render函数
  // 1.需要将html代码转换成ast语法树
  // 虚拟DOM  是用来描述节点的  结构
  // AST 是用来描述语言本身的  语法
  // 前端必须要掌握的数据结构---树
  let ast = parseHTML(template);
  console.log(ast);
  // 2.优化静态节点
  // 3.通过这棵树 重新生成代码
  let code = generate(ast);
  console.log(code);
  // 4.将字符串变成函数
  // 通过with来进行取值  稍后调用render函数的时候改变this
  let render = new Function (`with(this){return ${code}}`);
  console.log(render);
  return render;
}

我们看一个简单的例子: 模板:

  <div id="app" style="color:red">
    <div>{{name}} hello<span>world</span></div>
  </div>

AST树:

code代码:

_c('div',{id:"app",style{"color":"red"}},
_c('div',undefined,_v(_s(name)+"hello"),_c('span',undefined,_v("world"))))

render函数:

ƒ anonymous() {
    with(this){
        return _c('div',{id:"app",style:{"color":"red"}},
        _c('div',undefined,_v(_s(name)+"hello"),_c('span',undefined,_v("world"))))
    }
}

虚拟DOM:

3.将模板代码转换成ast语法树---parseHTML

通过正则循环解析模板生成语法树


const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`// 标签名
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`);
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
const startTagClose = /^\s*(\/?)>/;
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
export function parseHTML(html) {
  function createASTElement(tagName,attrs) {
    return {
      tag:tagName, // 标签名
      type:1// 元素类型
      children:[], // 子元素列表
      attrs, // 属性集合
      parent:null // 父元素
    }
  }
  let root;
  let currentParent;
  let stack = [];  // 栈结构
  // 标签是否符号预期 <div><span></span></div>
  function start(tagName,attrs) {
    let element = createASTElement(tagName,attrs)
    if (!root) {
      root = element
    }
    currentParent = element; // 当前解析的标签
    stack.push(element);
  }
  
  function end(tagName) { // 在结尾标签处创建父子关系
    let element = stack.pop(); // 取最后一个
    currentParent = stack[stack.length-1];
    if(currentParent) { // 在闭合时可以知道父亲是谁
      element.parent = currentParent;
      currentParent.children.push(element)
    }
  }
  
  function chars(text) {
    text= text.replace(/\s/g,'');
    if(text) {
      currentParent.children.push({
        type:3,
        text
      })
    }
  }
  while (html) { // 一直解析  直到字符串为空
    let textEnd = html.indexOf('<')
    if (textEnd == 0) {
      const startTagMatch = parseStartTag(); // 开始标签匹配的结果
      if(startTagMatch) {
        start(startTagMatch.tagName,startTagMatch.attrs)
        continue;
      }
      const endTagMatch = html.match(endTag);
      if(endTagMatch) { // 处理结束标签
        advance(endTagMatch[0].length)
        end(endTagMatch[1]); // 传入结束标签
        continue;
      }
    }
    let text;
    if (textEnd > 0) { // 是文本
      text = html.substring(0,textEnd)
    }
    if(text) { // 处理文本
      advance(text.length)
      chars(text);
    }
  }
  function advance(n) { // 将字符串进行截取操作  更新html内容
    html = html.substring(n)
  }
  function parseStartTag() {
    const start = html.match(startTagOpen)
    if(start) {
      const match = {
        tagName: start[1],
        attrs:[]
      }
      advance(start[0].length); //删除开始标签
      let end;
      let attr;
      // 不是结尾标签 并且可以匹配到属性
      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;
      }
    }
  }
  return root;
}

4.通过ast语法树重新生成代码---generate

把ast语法树转化成_c(创建节点)_v(创建文本)_s(获取对象文本)的代码字符串


// 编写: <div id="app" style="color:red">hello {{name}}<span>hello</span></div>
// 结果: render() {
//   return _c('div',{id:'app',style:{color:'red'}},_v('hello'+_s(name)),_c('span',null,_v('hello')))
// }
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
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 => {
        let [key,value] = item.split(':');
        obj[key] = value;
      });
      attr.value = obj
    }
    str += `${attr.name}:${JSON.stringify(attr.value)},`;
  }
  return `{${str.slice(0,-1)}}`;
}
function gen(node) {
  if (node.type == 1) {
    return generate(node);
  }else  {  
    let text = node.text;
    if (!defaultTagRE.test(text)) {
      // 如果是普通文本
      return `_v(${JSON.stringify(text)})` 
    }
    // _v('hello {{name}}') =》 _v('hello'+_s(name))
    let tokens = []; // 存放每一段的代码 最后join
    let lastIndex = defaultTagRE.lastIndex = 0;// 正则是全局模式,每次使用前都置为0
    let match,index; // 每次匹配结果
    while(match = defaultTagRE.exec(text)) {
      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('+')})`;
     
  }
}
function genChildren(el) {
  const children = el.children;
  if (children) { // 将多个转化后的儿子用逗号拼接起来
    return children.map(child=>gen(child)).join(',')
  }
}
export function generate(el) {
  let children = genChildren(el);
  let code = `_c('${el.tag}',${
    el.attrs.length?`${genProps(el.attrs)}`:'undefined'
  }${
    children?`,${children}`:''
  })`;
  return code;
}

5.将字符串变成函数

  // 通过with来进行取值(锁定变量作用域)  稍后调用render函数的时候改变this  
  let render = new Function (`with(this){return ${code}}`);

6.通过render函数产生虚拟节点

export function renderMixin(Vue) {
  Vue.prototype._c = function () { // 创建虚拟dom元素
    return createElement(...arguments);
  }
  Vue.prototype._s = function (val) { // stringify
    return val == null ? '' : (typeof val == 'object') ? JSON.stringify(val):val;
  }
  Vue.prototype._v = function (text) { // 创建虚拟文本元素
    return createTextVnode(text);
  }
  Vue.prototype._render = function () {
    const vm = this;
    const render = vm.$options.render;
    let vnode = render.call(vm);
    console.log(vnode);
    return vnode;
  }
}
function createElement(tag,data={},...children) {
  return vnode(tag,data,data.key,children)
}
function createTextVnode(text) {
  return vnode(undefined,undefined,undefined,undefined,text)
}
// 用来产生虚拟dom
function vnode(tag,data,key,children,text) {
  return {
    tag,
    data,
    key,
    children,
    text
  }
}

7.将虚拟节点转化成真实节点


export function patch(oldVnode,vnode) {
  // 将虚拟节点转化成真实节点
  let el = createElm(vnode); // 产生真实的dom
  let parentElm = oldVnode.parentNode// 获取老的app的父亲-》 body
  parentElm.insertBefore(el,oldVnode.nextSibling); // 当前真实元素的后面
  parentElm.removeChild(oldVnode); // 删除老的节点
}
function createElm(vnode) {
  let {tag,children,key,data,text} = vnode;
  if(typeof tag == 'string'){ // 创建元素 放到vnode.el上
    vnode.el = document.createElement(tag);
    children.forEach(child => { // 遍历儿子 将子节点渲染后的结果添加到父节点中
      vnode.el.appendChild(createElm(child));
    });
  }else { // 创建文件,放到vnode.el上
    vnode.el = document.createTextNode(text);
  }
  return vnode.el;
}

8.总结

vue的渲染流程:

  • 1.初始化数据
  • 2.将模板进行编译
  • 3.生成render函数
  • 4.生成虚拟节点
  • 5.生成真实的dom
  • 6.挂载到页面上