【手写 Vue2.x 源码】第十七篇 - 生成 render 函数 - 函数生成

430 阅读3分钟

一,前言

上篇,主要介绍了生成 render 函数 - 代码拼接,主要涉及以下几个点:

  • render 函数的分析和实现方案;
  • 拼接 render 函数结构:generate(ast);
  • 处理属性及属性值中的样式:genProps(ast.attrs);
  • 递归处理儿子:genChildren(el);
  • 对文本和变量进行包装处理,详细分析文本处理流程;

本篇,生成 render 函数 - 函数生成


二,包装 code 生成 render 函数

1,render 函数对比

对比 vue2 模板生成的render函数:

image.png

上篇,完成了return内部的代码拼接工作,即render函数中with内部的代码拼接已经完成;

目前,距离得到真正的render函数,还差以下两步:

  1. 向外一层,包装 with 代码块;
  2. 再向外一层,包装成为 Function

2,包装 with 并生成 render 函数

// src/complier/index.js

export function compileToFunction(template) {
  console.log("***** 进入 compileToFunction:将 template 编译为 render 函数 *****")
  
  // 1,将模板变成 AST 语法树
  let ast = parserHTML(template);
  
  // 2,使用 AST 生成 render 函数
  let code = generate(ast);
  // 包装 Function 和 with 代码块,生成完整的 render 函数
  let render = new Function(`with(this){return ${code}}`);
  console.log("包装 with 生成 render 函数:"+ render.toString())
  
  return render
}

备注:new Functionevel 的区别:

  • new Function(str) 会将内部的字符串包装成为一个函数;
  • evel(str) 则会将内部的字符串作为 js 执行;

将生成的render函数保存到options选项:

// src/init.js

Vue.prototype.$mount = function (el) {
    const vm = this;
    const opts = vm.$options;
    el = document.querySelector(el);
    vm.$el = el;
    if (!opts.render) {
      let template = opts.template;
      if (!template) {
        template = el.outerHTML;
      }

      let render = compileToFunction(template);
      // 保存生成的 render 函数
      opts.render = render;	
      console.log("打印 compileToFunction 返回的 render = " + JSON.stringify(render))
    }
  }

测试 render 函数生成:

image.png

借助上图中输出的render函数,再说明一下为什么需要使用with代码块进行一次包装:

  • 首先,在 with 代码块中含是有变量的,比如:nameage 等;
  • 其次,在html模板中的变量名和数量都是不能确定的,这些变量存在于data中,比如:{name:'brave', age:'34'}
  • 结论,使用with进行包装之后,在执行render.call(vm)渲染时,vm中包含全部数据,此时在render函数中的this上下文就是vm,那么就可以通过vm.xxx获取到变量对应的值,从而完成赋值操作;

备注:前端模板引擎的实现原理,大多都是依靠 new Function + with 来实现的,比如:ejsjadehandlerbar

3,生成 render 函数的流程回顾

html模板到生成render函数的大致流程:

  • 解析html模板并编译成为ast语法树;
  • 根据ast语法树拼接render函数的返回内容code;
  • 使用with代码块对生成的code字符串进行一次包装;
  • 将包装后的完整code字符串,通过new Function输出为render函数;

至此,整个模板编译的流程就搞定了,后续将进行以下工作:

  • 1)根据render函数,生成虚拟dom...
  • 2)虚拟dom + 真实数据, 生成真实 dom...

备注:模板编译中涉及到的v-forv-if虽然并没有进行实现,但内部的实现原理也都是一系列的字符串拼接操作;

todo:将整个模板编译部分作为一个主题拿出来;

todo:代码重构:可以将src/complier/index.js中的一系列代码拼接和生成的方法,抽取到src/complier/generate.js中;


三,结尾

本篇,生成 render 函数 - 函数生成,主要做了以下两件事:

  • 使用 with 代码块对生成的 code 字符串进行一次包装;
  • 将包装后的完整 code 字符串,通过 new Function 输出为 render 函数;

下一篇,根据 render 函数,生成虚拟节点

todo:本篇的内容较少,后续考虑与相关内容进行合并;


维护日志

  • 20230126:添加文字内容的代码高亮,优化少量内容描述,更新文章摘要,添加 todo;
  • 20230127:调整部分内容和代码,添加 todo;
  • 20230128:添加了 2 个备注,说明了 render 函数中使用with包装的原理;