vue渲染流程

  • 把模板转化成render函数
  • 调用render函数产生虚拟节点,将虚拟节点插入到真实节点上
  let oldTemplate = `<div>{{message}}</div>`;
  let vm1 = new Vue({data:{message:'hello world'}});
  const render1 = compileToFunction(oldTemplate);
  const oldVnode = render1.call(vm1);//虚拟dom
  document.body.appendChild(createElm(oldVnode));
生成render函数方法:compileToFunction
//parse.js
const attribute =
  /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; //属性正则
const unicodeRegExp =
  /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;
const dynamicArgAttribute =
  /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`; //标签名
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //用来获取标签名的,match后索引为1的
const startTagOpen = new RegExp(`^<${qnameCapture}`); //开始标签
const startTagClose = /^\s*(\/?)>/; //<div/>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); //闭合标签
const doctype = /^<!DOCTYPE [^>]+>/i;
// #7298: escape - to avoid being passed as HTML comment when inlined in page
const comment = /^<!\--/;
const conditionalComment = /^<!\[/;

export function parseHtml(html) {
  let root = null;
  let stack = [];
  function advance(len) {
    html = html.substring(len);
  }
  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;
    } else {
      //说明不是开始标签
      return false;
    }
  }
  while (html) {
    //看要解析的内容是否存在,如果存在就不停的解析
    let textEnd = html.indexOf("<"); //可能是开始标签,也可能是结束标签
    if (textEnd == 0) {
      //说明存在,
      const startTagMatch = parseStartTag(html); //解析开始标签;
      if (startTagMatch) {
        start(startTagMatch.tagName, startTagMatch.attrs);
        continue;
      }
      const endTagMatch = html.match(endTag);
      if (endTagMatch) {
        end(endTagMatch[1]);
        advance(endTagMatch[0].length);
        continue;
      }
    }
    let text;
    if (textEnd > 0) {
      text = html.substring(0, textEnd);
    }
    if (text) {
      charts(text);
      advance(text.length);
    }
  } //html字符串解析成对应的脚本来触发 tokens <div id="app"></div>
  function createAstElement(tagName, attrs) {
    return {
      tag: tagName,
      children: [],
      attrs,
      type: 1,
      parent: null,
    };
  }

  function start(tagName, attributes) {
    let parent = stack[stack.length - 1];
    let element = createAstElement(tagName, attributes);
    if (!root) {
      root = element;
    }
    if (parent) {
      element.parent = parent; //当放入栈时,记录父亲是谁
      parent.children.push(element);
    }
    stack.push(element);
  }
  function end(tagName) {
    let last = stack.pop();
    if (last.tag != tagName) {
      throw new Error("标签闭合有误");
    }
  }
  function charts(text) {
    text = text.replace(/\s/g, "");
    let parent = stack[stack.length - 1];
    if (text) {
      parent.children.push({
        type: 3,
        text,
      });
    }
  }
  return root;
}

//generate.js
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
function getProps(el){//{[name:'id',value:app]}
    let str = ''
    el.forEach(attr=>{
      //修改style前_c('div'),{d:"app",class:"app",style:"color:red;background:blue"}
      if (attr.name == "style") {
        let styleObj = {};
        attr.value.replace(/([^:;]+):([^:;]+)/g, function () {
          styleObj[arguments[1]] = arguments[2];
        });
        attr.value = styleObj;
      }
      //修改style之后_c('div'),{d:"app",class:"app",style:{"color":"red","background":"blue"}}
      str += `${attr.name}:${JSON.stringify(attr.value)},`;
    })
    return `{${str.slice(0,-1)}}`
}
function gen(el){
  if(el.type == 1){
    return generate(el);
  }else{
    let text = el.text;
    if(!defaultTagRE.test(text)){
       return `_v("${text}")`
    }else{
      // 'hello' + arr + 'world' hello{{arr}}world
      let tokens = [];
      let match;
      let lastIndex = defaultTagRE.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('+')})`
    }
  }
}
function generateChildren(el){
  let children = el.children
  if(children){
    return children.map(c=>gen(c)).join(',')
  }
  return false
}            
export function generate(el){
  //遍历树,将树拼接成字符串
  let children  = generateChildren(el);
  let str = el.attrs.length>0?getProps(el.attrs):'undefined';
  let code = `_c('${el.tag}',${str}${children?`,${children}`:''})`;
  return code
}


//index.js
import { generate } from "./generate";
import { parseHtml } from "./parse";

export function compileToFunction(el) {
  const root = parseHtml(el);
    //生成代码 _c('div',{id:'app',a:1},[text])
  let code = generate(root);
  let render = new Function (`with(this){return ${code}}`)
 
  return render
  //html->ast(只能描述语法,语法不存在的属性无法描述)->render函数->虚拟dom(增减额外的属性)->生成真实的dom
}
创建虚拟节点方法:createElm
export function patch(oldVnode,vnode){//新的虚拟节点替换掉老的节点,先插入,后删除
    if(!oldVnode){
        return createElm(vnode)
    }
    if(oldVnode.nodeType == 1){
        const parent = oldVnode.parentNode;
        const newElm = createElm(vnode);
        parent.insertBefore(newElm, oldVnode.nextSibling);
        parent.removeChild(oldVnode);
        return newElm;//首次渲染时将老节点删除掉,会导致再次更新数据,获取不到老节点,所以没办法插入,所以每次更新老节点
    }
}
function createComponent(vnode){
    let i = vnode.data; 
    if((i = i.hook) && (i = i.init)){
        i(vnode);//调用init方法
    }
    if (vnode.componentInstance) {
      //有属性说明子组件new完毕了,并且组件的真实dom挂载到了vnode。componentInstance
      return true;
    }
} 
export function createElm(vnode){
    let {vm,tag,data,children,text} = vnode;
    if(typeof tag === 'string'){
        //判断是否是组件
        if( createComponent(vnode)){
            //返回组件对应的真实节点
            console.log(vnode.componentInstance.$el);
            return vnode.componentInstance.$el
        }
        vnode.el = document.createElement(tag);
        if(children.length){
            children.forEach(child=>{
                vnode.el.appendChild(createElm(child));
            })
        }
    }else{
        vnode.el = document.createTextNode(text);
    }
    return vnode.el;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容