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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容