• Vue.js 源码分析(三) 基础篇 模板渲染 el、emplate、render属性详解


    Vue有三个属性和模板有关,官网上是这样解释的:

    el     ;提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标

    template  ;一个字符串模板作为 Vue 实例的标识使用。模板将会 替换 挂载的元素。挂载元素的内容都将被忽略,除非模板的内容有分发插槽。

    render    ;字符串模板的代替方案,允许你发挥 JavaScript 最大的编程能力。该渲染函数接收一个 createElement 方法作为第一个参数用来创建 VNode

    简单说一下,就是:

      Vue内部会判断如果没有render属性则把template属性的值作为模板,如果template不存在则把el对应的DOM节点的outerHTML属性作为模板,经过一系列正则解析和流程生成一个render函数,最后通过with(this){}来执行。

      也就是说template的优先级大于el。

      render的参数是Vue内部的$createElement函数(位于4486行),它的可扩展性更强一些,在一些项目的需求中,可以用很简单的代码得到一个模板。例如Vue实战9.3里介绍的例子,有兴趣可以看看

    render可以带3个参数,分别如下:

        tag          ;元素的标签名,也可以是组件名
        data        ;该VNode的属性,是个对象
        children        ;子节点,是个数组
    其中参数2可以省略的,在4335行做了修正,最后执行_createElement()函数,如下:

    function createElement (  //第4335行
      context,
      tag,
      data,
      children,
      normalizationType,
      alwaysNormalize
    ) {
      if (Array.isArray(data) || isPrimitive(data)) {     //如果data是个数组或者是基本类型
        normalizationType = children;
        children = data;                                      //修正data为children
        data = undefined;                                     //修正data为undefined
      }
      if (isTrue(alwaysNormalize)) {
        normalizationType = ALWAYS_NORMALIZE;
      }
      return _createElement(context, tag, data, children, normalizationType)     //最后执行_createElement创建一个虚拟VNode
    }

    例如下面三个Vue实例,分别用el、template和rentder指定模板,它们的输出是一样的

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <script src="https://cdn.bootcss.com/vue/2.5.16/vue.js"></script>
        <title>Document</title>
    </head>
    <body>
        <div id="app1">{{message}}</div>
        <div id="app2"></div>
        <div id="app3"></div>
        <script>
            var data={message:'you are so annoying'}
            new Vue({el:'#app1',data})                                                          //用el做模板
            new Vue({el:'#app2',data,template:"<div>{{message}}</div>"})                        //用template做模板
            new Vue({el:'#app3',data,render:function(h){return h('div',this.message)}})         //直接用render函数指定模板
        </script>        
    </body>
    </html>

     、浏览器显示结果:

    可以看到输出是一摸一样的

     源码分析

    Vue实例后会先执行_init()进行初始化,快结束时会判断是否有el属性,如果存在则调用$mount进行挂载,$mount函数如下:

     writer by:大沙漠 QQ:22969969

    Vue.prototype.$mount = function (    //定义在10861行
      el,
      hydrating
    ) {
      el = el && query(el);
    
      /* istanbul ignore if */
      if (el === document.body || el === document.documentElement) {
        "development" !== 'production' && warn(
          "Do not mount Vue to <html> or <body> - mount to normal elements instead."
        );
        return this
      }
    
      var options = this.$options;
      // resolve template/el and convert to render function
      if (!options.render) {                                            //如果render属性不存在
        var template = options.template;                                //则尝试获取template属性并将其编译成render   
        if (template) {
          if (typeof template === 'string') {                              
            if (template.charAt(0) === '#') {
              template = idToTemplate(template);
              /* istanbul ignore if */
              if ("development" !== 'production' && !template) {
                warn(
                  ("Template element not found or is empty: " + (options.template)),
                  this
                );
              }
            }
          } else if (template.nodeType) {
            template = template.innerHTML;
          } else {
            {
              warn('invalid template option:' + template, this);
            }
            return this
          }
        } else if (el) {                                                //如果templtate不存在但是el存在,则获取调用getOuterHTML()函数获取el的outerHTML属性,getOuterHTML()定义在10933行,也就是末尾,用户获取DOM的outerHTML
          template = getOuterHTML(el);
        }
        if (template) {
          /* istanbul ignore if */
          if ("development" !== 'production' && config.performance && mark) {
            mark('compile');
          }
    
          var ref = compileToFunctions(template, {
            shouldDecodeNewlines: shouldDecodeNewlines,
            shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
            delimiters: options.delimiters,
            comments: options.comments
          }, this);                                                     //这里调用compileToFunctions()将template解析成一个render函数,并返回
          var render = ref.render;
          var staticRenderFns = ref.staticRenderFns;
          options.render = render;
          options.staticRenderFns = staticRenderFns;
    
          /* istanbul ignore if */
          if ("development" !== 'production' && config.performance && mark) {
            mark('compile end');
            measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
          }
        }
      }
      return mount.call(this, el, hydrating)
    };
    compileToFunctions函数是由createCompiler()返回的(这里有点绕,研究代码的时候在里面绕了好几天),我把大致主体贴出来,如下:
    var baseOptions ={}                                                     //编译的配置项 第9802行
    function createCompileToFunctionFn(compile){
      var cache = Object.create(null);  
      return return function compileToFunctions(template, options, vm) {    //编译时先执行这里
        /**/
        compile(template,options)       
        /**/
      }
    }
    function createCompilerCreator(baseCompile){
      return function(baseOptions){
        function compile(template, options) {/**/}
                  return {
                      compile: compile,
                      compileToFunctions: createCompileToFunctionFn(compile)    //难点:匿名函数返回的值中又调用了createCompileToFunctionFn函数
                  }
      }
    }
    var createCompiler = createCompilerCreator(function(){                //传入一个匿名函数
            var ast = parse(template.trim(), options);                          //编译时,第二步:再执行这里
            if (options.optimize !== false) {          
                  optimize(ast, options);
            }
            var code = generate(ast, options);         
            return {ast: ast,render: code.render,staticRenderFns: code.staticRenderFns}   //最后返回一个对象
    })
    var ref$1 = createCompiler(baseOptions);
    var compileToFunctions = ref$1.compileToFunctions;                  //编译的入口文件

    是不是有点晕呢,我举一个例子就能看明白了,如下:

        function show(show){      //shou函数也直接返回一个匿名函数,带一个参数
          return function(info){
            show(info)              //show通过作用域链就可以访问到参数的show函数了
          }
        }
    
        var info=show(function(info){
          console.log(info)
        })                          //这里执行show函数,传入一个匿名函数
        info({name:'gsz'})     //控制台输出:{name: "gsz"}

    Vue内部看得晦涩是因为传参的时候都注明了一个函数名,其实这个函数名是可以忽略的,这样看起来会更清晰一点    注:这样设计是为了跨平台一些代码的复用和存放吧,代码结构在node下更好理解一点

    compileToFunctions函数内部会调用parse()将模板经过一系列的正则解析,用一个AST对象保存,然后调用generate()做静态节点标记,最后调用generate生成一个render函数

    以上面的第一个Vue实例来说,parse()解析后的AST对象如下:

    、再通过generate()后生成如下一个对象,其中render就是最终要执行的render函数了

    compileToFunctions函数返回值是一个对象,以上面的第一个vue实例为例,返回后的信息如下:
    {
      render:"(function anonymous() {with(this){return _c('div',{attrs:{"id":"app1"}},[_v(_s(message))])}})",     //最终渲染出来的render函数
      staticRenderFns:Function[]                                                    //如果是静态节点,则保存到这里
    }
    以后分析到每个API时这里会单独分析的
    最后在mountcomponent()函数内会以当前Vue实例为上下文,执行该render函数(在2739行),此时就会完成渲染watch的收集,并生成虚拟VNode,最后调用_update()方法生成真实DOM节点。
  • 相关阅读:
    WPF TreeView IsExpanded 绑定不上的问题
    WPF TreeView BringIntoViewBehavior
    WPF ListBox的进阶使用(二)
    WPF ListBox的进阶使用(一)
    双缓冲队列解决WPF界面卡死
    C# 对接Https接口
    软件架构的六大设计原则
    FeignClient接口封装
    CentOS修改root密码
    并发编程的挑战(Java并发编程的艺术)
  • 原文地址:https://www.cnblogs.com/greatdesert/p/11011485.html
Copyright © 2020-2023  润新知