浅曦Vue源码-33-挂载阶段-$mount-(22)render函数全貌

300 阅读4分钟

「这是我参与2022首次更文挑战的第37天,活动详情查看:2022首次更文挑战」。

一、前情回顾 & 背景

本篇小作文讨论了另外两个常用的功能对应的渲染函数:自定义组件、动态组件;二者的处理都是处理成 _c(componentName, data, children) 的调用形式;

此时此刻尚在根实例编译阶段,只有当根实例的编译阶段结束,得到根实例的渲染函数,接下来创建根实例渲染 watcher 就会调用根实例的渲染函数。

本篇小作文准备先来看看我们整个模板编译后得到的 render 函数主体全貌,接着我们看看渲染函数的生成;

二、根实例的渲染函数

我们这里一直讨论的都是根实例的渲染函数,有别于子实例的渲染函数;

2.1 test.html 模板

<div id="app">
   <div class="staticR">
      <article>hahahah</article>
   </div>
   <input :type="inputType" v-model="inputValue" />
   <span v-for="item in someArr" :key="index">{{item}}</span>
   <div v-if="inputType === 'checkbox'">inputType=checkBox</div>
   <div v-else-if="inputType === 'radio'">inputType=radio</div>
   <div v-else>inputType=something-else</div>
    {{ msg }}
   <some-com :some-key="forProp"></some-com>
   <div>someComputed = {{someComputed}}</div>
   <component is="someCom">
     <div>component 的插槽</div>
   </component>
 <div class="static-div">静态节点</div>
</div>

2.2 根实例的 render 函数主体

上面的 2.1 的模板最终都会被编译成下面的 render 函数主体,注意这个表述,严格意义上来说它还不是 render 函数,只是主体,真的 render 函数,需要被 with(this) 语句包裹;

"_c('div',{attrs:{\"id\":\"app\"}},[_m(0),_v(\" \"),((inputType)==='checkbox')?_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(inputValue),expression:\"inputValue\"}],attrs:{\"type\":\"checkbox\"},domProps:{\"checked\":Array.isArray(inputValue)?_i(inputValue,null)>-1:(inputValue)},on:{\"change\":function($event){var $$a=inputValue,$$el=$event.target,$$c=$$el.checked?(true):(false);if(Array.isArray($$a)){var $$v=null,$$i=_i($$a,$$v);if($$el.checked){$$i<0&&(inputValue=$$a.concat([$$v]))}else{$$i>-1&&(inputValue=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}}else{inputValue=$$c}}}}):((inputType)==='radio')?_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(inputValue),expression:\"inputValue\"}],attrs:{\"type\":\"radio\"},domProps:{\"checked\":_q(inputValue,null)},on:{\"change\":function($event){inputValue=null}}}):_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(inputValue),expression:\"inputValue\"}],attrs:{\"type\":inputType},domProps:{\"value\":(inputValue)},on:{\"input\":function($event){if($event.target.composing)return;inputValue=$event.target.value}}}),_v(\" \"),_l((someArr),function(item){return _c('span',{key:index},[_v(_s(item))])}),_v(\" \"),(inputType === 'checkbox')?_c('div',[_v(\"inputType=checkBox\")]):(inputType === 'radio')?_c('div',[_v(\"inputType=radio\")]):_c('div',[_v(\"inputType=something-else\")]),_v(\"\\n\\t\"+_s(msg)+\"\\n\\t\"),_c('some-com',{attrs:{\"some-key\":forProp}}),_v(\" \"),_c('div',[_v(\"someComputed = \"+_s(someComputed))]),_v(\" \"),_c(\"someCom\",{tag:\"component\"},[_c('div',[_v(\"component 的插槽\")])]),_v(\" \"),_c('div',{staticClass:\"static-div\"},[_v(\"静态节点\")])],2)"

上面这一大坨着实让人有点蒙圈,所以我手动格式化了一下,这个可是个体力活😂😂,这个比码字还痛苦;

先提示一下 _c 方法的作用:它用于创建元素,关注一下他的参数能更好的理解这个过程:

  1. 第一个参数是标签名,可以是自定义的组件或者动态组件名,也可以是 HTML 原生标签;
  2. 第一个参数的行内属性,attrs
  3. 第三个参数是一个数组,标识的是当前元素的子元素;
let renderFunctionsString = `_c( // 渲染 div#app 
   'div', 
   { attrs: { "id": "app" } },// div#app 的行内属性
   [ // 这个数组是 div#app 的子元素
     _m(0),
     _v(" "),
     ((inputType)==='checkbox') // 处理 <input :type="inputType"... /> 不同 type 的 v-model
       ? _c( // 如果 type 是 checkbox
         'input', 
         { 
           directives: [{
             name: "model",
             rawName: "v-model",
             value: (inputValue),
             expression: "inputValue"
           }],
           attrs: { "type": "checkbox" },
           domProps: { "checked": Array.isArray(inputValue) 
                           ? _i(inputValue,null) > -1 
                           : (inputValue)
           },
           on: { "change": function ($event) {
                              // 如果 type 是 checkbox,绑定 change 事件
                              // 事件 handler 就是这个函数
                              var $$a = inputValue,
                                   $$el = $event.target,
                                   $$c = $$el.checked ? (true) : (false);
                              if (Array.isArray($$a)){
                                // 处理 checbox 绑定数组项
                                // 将绑定的值更新到数组中
                                var $$v = null,
                                $$i = _i($$a,$$v); // _i 是 indexOf 
                                if ($$el.checked) {
                                  $$i < 0 && (inputValue = $$a.concat([$$v]))
                                } else {
                                  $$i > -1 && (inputValue = $$a.slice(0, $$i).concat($$a.slice($$i+1)))
                                }
                              } else {
                                // 非数组
                                inputValue = $$c
                              }
                          }
                      }
           }
         ) 
         : ((inputType) === 'radio') // input type 是 radio 时
           ? _c(
               'input',
               { directives: [{ 
                 name: "model",
                 rawName: "v-model",
                 value: (inputValue),
                 expression: "inputValue"
               }],
               attrs: { "type": "radio" },
               domProps: { "checked": _q(inputValue, null) },
               on: { "change": function($event) { inputValue = null } }
              }
            )
           : _c(
             'input',
             { 
               directives:[{
                 name: "model",
                 rawName: "v-model",
                 value: (inputValue),
                 expression: "inputValue"
               }],
               attrs: { "type": inputType },
               domProps:{ 
                 "value": (inputValue)},
                 on:{ "input": function ($event) {
                                 if ($event.target.composing) return;
                                 inputValue = $event.target.value
                               }
                 }
              }
         ),
        _v(" "),
        _l( // 处理 <span v-for="item in someArr" :key="index">{{item}}</span>
          (someArr),
          function (item) {
            return _c(
              'span',
              { key:index },
              [
                _v(_s(item))
              ]
             )
          }
        ),
        _v(" "),
        (inputType === 'checkbox') // <div v-if="inputType === 'checkbox'">
          ? _c(
           'div',
           [
             _v("inputType=checkBox")
           ]
          ) 
         : (inputType === 'radio') // <div v-else-if="inputType === 'radio'">
           ? _c(
            'div',
            [
              _v("inputType=radio")
            ]
           ) 
          : _c( // <div v-else>
            'div',
            [
              _v("inputType=something-else")
            ]
          ),
      _v(\n\t"+_s(msg)+"\n\t"), // 渲染文本 {{ msg }}
      _c( // 渲染 <some-com></some-com> 组件
        'some-com',
         { attrs: { "some-key": forProp } }
      ),
     _v(" "), // 空行
     _c( // 渲染 <div>someComputed = {{someComputed}}</div>
       'div',
       [
         _v("someComputed = "+_s(someComputed))
       ]
     ), //
     _v(" "),
     _c( // 渲染动态组件 <component is="someCom"> ...
       "someCom",
       { tag: "component" },
       [
         _c( // 动态组件 component 标签的子元素的子元素 
           'div',
           [
             _v("component 的插槽")
           ]
         )
       ]
     ),
     _v(" "),
     _c(
       'div', // 渲染 <div class="static-div">静态节点</div>
       { staticClass: "static-div" },
       [_v("静态节点")]
     )
   ],
   2
)`

这里缺少一个对照视图,建议把 test.html 中的模板代码和这个 render 函数体进行对照,效果会十分明显;

我敢保证,这是全网你距离 Vue 渲染函数最近的一次,写这个专栏的初衷就是让每个人都能看得懂 Vue 的源码,加油吧老铁们;

三、渲染函数

前面我们也称为上面第二个主题中说的渲染函数主体为渲染函数,这是个不严谨的过程,刚刚我们也提及了。本主题就是要说真正的渲染函数的产生过程;

3.1 包裹 with(this)

关于 generate 我们说了很懂,调用栈十分的深,到这里将 parse 阶段获得到的 ast 变成渲染函数体的过程已经结束。这个过程是 generate 的一个中间过程,后面的过程来自 generate 方法的后半部分,代码如下

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)

  // 生成字符串格式的代码,比如 '_c(tag, data, children, normalizationType)'
  const code = ast 
     ? (ast.tag === 'script' 
         ? 'null' 
         : genElement(ast, state))
     : '_c("div")'
  
  // 以下为生成 render 函数体的后续工作
  // 这里 return 的对象,对前面的生成的 render 函数主体包裹 with 语句
  // 把静态根节点的渲染函数提升
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

从上面的代码可以看到出来,生成 render 函数主体后,generate 返回一个对象,对象的 render 属性值就是上面那一大坨 _c(div, { id: "app" }, ...) 被包裹 with(this) 语句之后的字符串;

这里先提一下,为啥要包裹 with(this) 语句呢?其实前面也提到过,with(this) 语句的作用就是延长 js 的作用域链,使用 with(obj) 块中的变量可以查找到 obj 中的属性作值为变量的值;

这里使用 with(this)this 就是当前的 Vue 实例,所以后面 _c 执行时用到的变量先从 Vue 实例上取值,这也就是我们在模板中绑定数据时,例如<span v-for="someArr"/>someArr 此时绑定到 Vue 实例上的 someArr,即 this.someArr

那么 this.someArr 又是哪里来的呢?

还记得前面讲响应式数据的时候,data、computed、props 最终都会代理到 vm 吗?如此一来,data,computed,props 都已经代理到 Vue 实例,这就大大简化了 Vue 的渲染函数处理工作;

试想一下,如果没有代理到 Vue 实例,那么我们写模板的时候就要写 data.someArr, computed.someArr, props.someArr

3.2 生成渲染函数

上面 3.1 的返回结果 { render, staticRenderFns } 返回,使得 comile 方法调用栈出栈,回到下面的 compileToFunctions 作用域中:

export function createCompileToFunctionFn (compile: Function): Function {
  const cache = Object.create(null)

  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    // compileToFunctions 作用域

    // compile
    // 执行编译函数,得到编译结果:{ render, renderStaticFns }
    const compiled = compile(template, options) 
    
    const res = {}

    // 转换编译得到的字符串代码为函数,通过 new Function(code) 实现
    res.render = createFunction(compiled.render, fnGenErrors)
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })

    // 缓存编译结果
    return (cache[key] = res)
  }
}

3.3 createFunction 方法

方法位置:src/compiler/to-function.js -> function createFunction

方法参数:

  1. code: 代码字符串
  2. errors: 收集

方法作用:通过 new Function 传入字符串,创建函数

function createFunction (code, errors) {
  try {
    return new Function(code)
  } catch (err) {
    errors.push({ err, code })
    return noop
  }
}

3.4 render 函数成形调用栈依次出栈

所以经历过这个步骤之后,compileToFunctions 也会获取到最终的结果形如:

const res = {
  render: function () {
     return with (this) {
       return _c('div', { id: 'app' }, [.....])
     }
  },
  staticRenderFns: [....]
}

此时,compileToFunctions 的作用域也会在添加缓存之后出栈,回到 createCompiler 作用,将 compileToFunctions 返回结果作为 compileToFunctions 属性的值;

export function createCompilerCreator (baseCompile) {
  return function createCompiler () {
 
    function compile () {
      
      const compiled = baseCompile(template.trim(), finalOptions)
   
      return compiled
    }

    return {
      compile,
      // createCompileToFunctionFn(compile)->
      // -> { render, staticRenderFns }
      compileToFunctions: createCompileToFunctionFn(compile) 
    }
  }
}

createCompiler 作用域(调用栈)返回 { compile, compileToFunctions } 以后也就退出了,这回退出到了哪里呢?

这回退出到 Vue.proptotype.$mount 方法上,终于回来了啊~~~

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
      // ...
      // 编译模板,得到动态渲染函数和静态渲染函数
      // 这个对象就是上面一系列加工后得到的
      const { 
          render, 
          staticRenderFns 
      } = compileToFunctions(template,...)

      // 将两个渲染函数放到 vm.$options 上,
      // 即 this.$options,给 Vue.prototype._render 方法用
      options.render = render // funciton () { return with(this) {...} }
      options.staticRenderFns = staticRenderFns
    }
  }
  // 执行挂载
  return mount.call(this, el, hydrating)
}

四、总结

本篇小作文我们给大家展示了根实例的最终得到的 render 函数主体:是一个 _c() 的一个调用,他的第一个参数是标签名,第二个是行内属性,第三个是代表子元素的数组;

从这个 render 函数的主体可以看出,他是一个递归调用的过程,最终完成顶层 div#app 的渲染工作;

获得 render 函数主体之后,又经过 createFunction 得到最终的 render 函数,接着就是一些列的返回结果并出栈,回到了 Vue.prototype.$mount 方法,将获得的 render、renderStaticFn 赋值到 this.$options 上,以备 Vue.prototype._render 调用进行挂载;