从源码上看v-for

289 阅读3分钟

从源码上看v-for

参考文章:1.5.Vue模板编译原理-AST生成Render字符串

在前面一篇文章《Vue相关指令(一)》中讲到代码生成器,当时没有详细讲源码,现在讲讲。其实代码生成器的入口就是generate函数:

export function generate (
  //处理好的抽象语法树
  ast: ASTElement | void,
  //一些选项,data啥的
  options: CompilerOptions
): CodegenResult {
  // 初始化一些options
  const state = new CodegenState(options)
  // 重点在这里!
  // 有没有生成抽象语法树ast?有就传入ast和options生成代码:没有就直接生成一个空div
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    // 生成代码之后最外层包一个 with(this) 之后返回
    render: `with(this){return ${code}}`,
    // staticRenderFns这个数组中的函数与VDOM中的diff算法优化相关
    // 那些被标记为staticRoot的虚拟节点 会单独生成静态节点渲染函数。
    staticRenderFns: state.staticRenderFns
  }
}

这个diff算法啥的后面再说,没个几千字说不完啦。这里我们看看重点,这个入口函数的重点就是得到code这一块代码。所以我们看一看genElement这个函数:

export function genElement (el: ASTElement, state: CodegenState): string {
  //对一些标签属性,如v-once,v-if等等的处理
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  //先判断有没有v-for,有的话就处理v-for
  //forProcessed表示for属性处理完了
  } else if (el.for && !el.forProcessed) {
    //执行genFor
    return genFor(el, state)
  //再判断有没有v-if,有的话就处理
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  //这个else后面讲
  } else {...}

看看上面的代码,我们能得出一个结论:"哇哦!VUE真的没有骗我们!for的优先级真的比if要高!",其他也看不出啥了。

好,重点又转移到genFor上了,接下来再来看看genFor,这里有些key警告什么的被我删掉了,如果好奇的也可以点击这里直接看源码:src/compiler/codegen/index.js

//渲染v-for指令
function genFor (         
  el,
  state,
  altGen,
  altHelper
) {
  //获取for的值
  var exp = el.for;
  //获取别名item
  var alias = el.alias;                       
  //key
  var iterator1 = el.iterator1 ? ("," + (el.iterator1)) : '';    
  //index
  var iterator2 = el.iterator2 ? ("," + (el.iterator2)) : '';       

  //...一些关于key的警告   
      
  // 表示for属性处理完了 避免递归 因为后面还要继续调用genElement生成子节点的代码
  el.forProcessed = true; 
  //拼凑_l函数 目前并没有传递altHelper altGen这两个东西 所以我们直接忽略他
  //(其实我也不知道这是啥,没办法跟你们讲..如果你们知道的话可以在评论告诉我呀)
  return (altHelper || '_l') + "((" + exp + ")," +                
    "function(" + alias + iterator1 + iterator2 + "){" +
      //递归他的子节点,生成子节点的代码(p也有子节点,是文本节点)
      "return " + ((altGen || genElement)(el, state)) +
    '})'
}

"_l"其实就是用来生成列表VNode的,它只是一个缩写,对应的函数其实是renderList,后面会讲一下renderList的代码。现在的话,我们先来看看上面那一串return回去的丑丑的字符串。我们可以举个例子来看看里面都是什么好东西。

先拿个例子,来源:Vue.js 源码分析(十八) 指令篇 v-for 指令详解

<div id="app">
    <p v-for="(item,key,index) in infos">{{index}}:{{key}}:{{item}}</p>    
</div>
<script>
    var app = new Vue({
        data(){return {infos:{name:'gege',age:12}}},
        el:'#app'
    })
</script> 

目前只看里面的<p>标签,如果我们的代码是这样的格式:

<p v-for="(item,key,index) in infos">{{index}}:{{key}}:{{item}}</p>  

那么对于<p>标签会生成这样子的字符串:

_l( (infos), function( item, key, index){
    //_c函数能够生成一个VNode节点返回
    return _c(
        //节点类型是p节点
        'p',
        //节点内容是文本节点,_v创建一个文本节点
        [ _v ( 
        //文本内容是:index的字符串形式 + ":" + key的字符串形式 + ":" + item的字符串形式
            _s(index) + ":" + _s(key) + ":" + _s(item) 
        ) ]
}),0  //这个零是用来区分节点的,不用管它

好啦,知道这个字符串是啥之后,v-for的代码就差不多讲完了,其实还挺少的。(实际上我写这篇文章的过程真的很漫长很漫长,回头一看,居然只写了这么一点...)

最后看点简单的,我们来看看"_l"是怎么渲染的,也就是renderList函数的内容。这一部分很长但是挺简单的,源代码在src/core/instance/render-helpers/render-list.js,坚持看完哦:

//渲染v-for指令
function renderList (     
  //我们在data中定义的渲染数据
  val,  
  //渲染函数
  render
) {
  var ret, i, l, keys, key;
  //如果val是个数组或字符串
  if (Array.isArray(val) || typeof val === 'string') {    
    //将ret定义成val一样大小的数组
    ret = new Array(val.length);      
    //遍历val数组|字符串
    for (i = 0, l = val.length; i < l; i++) {           
      //依次调用render函数,参数1为值,参数2为索引,返回VNode,并把结果VNode保存到ret里面。
      ret[i] = render(val[i], i);                                 
    }
  //如果是一个数字
  } else if (typeof val === 'number') {
    ret = new Array(val);
    for (i = 0; i < val; i++) {
      //遍历 参数1值为1,2,3...val,参数2是索引。
      ret[i] = render(i + 1, i);
    }
  //如果是一个对象
  } else if (isObject(val)) {
    //得到对象的所有键值
    keys = Object.keys(val);
    ret = new Array(keys.length);
    for (i = 0, l = keys .length; i < l; i++) {
      key = keys[i];
      //参数1值为key对应的值,参数2是对象的key,参数3是index。
      ret[i] = render(val[key], key, i);
    }
  }
  //如果ret存在(成功调用了)
  if (isDef(ret)) {      
    //则给该数组添加一个_isVList标记,值为true
    (ret)._isVList = true;                                  
  }
  //最后返回ret
  return ret                                                

每个节点的内容都交给render,然后render帮我们生成虚拟节点了之后,放到ret这个收集节点的数组里面。最后这个数组会作为参数,交给父节点的"_c"函数去处理。

从源码来看render函数字符串的生成

那现在我们再回到最开始的代码中,从<div>开始捋一遍整个代码生成render函数字符串的过程。(晕,好不容易看完怎么又讲回去了...)这里为了大家方便看,直接粘贴过来了:

对于这一段代码:

<div id="app">
    <p v-for="(item,key,index) in infos">{{index}}:{{key}}:{{item}}</p>    
</div>
<script>
    var app = new Vue({
        data: {
            infos:{
                name:'gege',
                age:12
            }
        },
        el:'#app'
    })
</script> 

我们执行generate函数:

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  //之前我们看到这里了
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    // 生成代码之后最外层包一个 with(this) 之后返回
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

执行之后,去除那两个反单引号,最终render函数的内容其实是这样的:

with(this) {
    return _c('div', {
        attrs: {
            "id": "app"
        }
    //这下面就是我们上面分析的p标签对应的代码字符串
    }, _l((infos), function (item, key, index) {
        return _c('p', [_v(_s(index) + ":" + _s(key) + ":" + _s(item))])
    }),0)
}

div啥的是怎么来的嘞?

不知道你记不记得前面的那个被我省略的else?genElement函数里面的那个?现在来看看它的真面目哈~这里依然删掉了一些代码,好奇的朋友可以去src/compiler/codegen/index.js上看,我们理清主要逻辑就好。

else {
    // component or element
    let code
    if (el.component) {
      // ...component组件处理生成函数
    } else {
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        // attributes节点属性处理生成函数
        data = genData(el, state)  
      }
      // 处理子节点并调用genChildren()
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      // 主要看这一句 el.tag是标签名
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // 这里放了在标签中写上的属性
      }${
        children ? `,${children}` : '' // 这里放子节点
      })`
    }
    // 静态class style 处理
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

我们在el上挂载的那个div对应的render代码就是从这里出来滴,有没有很眼熟!看看我们最终生成的代码,对应关系就明确了:

with(this) {
    //生成div节点 el.tag对应了div
    return _c('div', {
        //这里是我们在div标签中写上的属性
        attrs: {
            "id": "app"
        }
    //这下面就是我们之前分析的p标签对应的代码字符串
    }, _l((infos), function (item, key, index) {
        return _c('p', [_v(_s(index) + ":" + _s(key) + ":" + _s(item))])
    }),0)
}

那我们再来看看genChildren函数吧~

export function genChildren (
  el: ASTElement,
  //一些初始化之后的选项
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  //得到所有的子节点
  const children = el.children
  //如果存在子节点
  if (children.length) {
    const el: any = children[0]
    //如果子节点的长度等于1,而且存在v-for,而且不是模板或插槽
    if (children.length === 1 &&
      el.for &&
      el.tag !== 'template' &&
      el.tag !== 'slot'
    ) {
       //normalizationType表示子节点规范的类型
      const normalizationType = checkSkip
         //看看是不是组件或某些节点类型,这里我们子节点是p类型,所以会返回0
        ? state.maybeComponent(el) ? `,1` : `,0`
        : ``
      //执行genElement,得到子节点的render函数字符串
      return `${(altGenElement || genElement)(el, state)}${normalizationType}`
    }
    //其他情况就执行这里的代码 
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    const gen = altGenNode || genNode
    //把子节点以数组的形式返回出去
    return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}

所以说,实际上是在生成父节点div的过程中,处理子节点的时候,调用了genChildren,然后才开始处理p标签的。具体流程如下:

  1. 对于根节点<div>生成的抽象语法树,调用generate,开始生成render函数的代码字符串。

  2. generate函数进入到genElement函数,根据不同的指令来执行不同的代码生成函数。

  3. 执行genElement的时候,进入其中的else块,由于她不是组件,所以又进入else块。在这个时候处理了我们写在<div>中的特性,然后开始执行genChildren处理<div>的子节点<p>

  4. 在执行genChildren的时候,又调用了genElement函数,然后在genElement进入判断。由于<p>身上有v-for指令,进入genFor函数。

  5. genFor函数中生成<p>对应的render代码字符串。

  6. 由于<p>中存在文本节点,作为<p>的子节点,所以又进行一次genElement生成文本节点代码,这里就不啰嗦了,直接给代码:

    [_v(_s(index)+":"+_s(key)+":"+_s(item))]
    
  7. <p>的子节点递归完成,拼接<p>的render函数字符串。

    _l( (infos), function( item, key, index){
        return _c(
            'p',
            [ _v ( 
                _s(index) + ":" + _s(key) + ":" + _s(item) 
            ) ]
    }),0
    
  8. <div>的子节点处理结束,返回到父节点<div>genElement上来,根据之前处理的属性,子节点等,形成下面的字符串。

    _c('div', {
        attrs: {
            "id": "app"
        }
    }, _l((infos), function (item, key, index) {
        return _c('p', [_v(_s(index) + ":" + _s(key) + ":" + _s(item))])
    }),0)
    
  9. 最后,返回到generate函数中,用with(this){return ${code}}包括起来,形成最终的render代码。

    with(this) {
        return _c('div', {
            attrs: {
                "id": "app"
            }
        //这下面就是我们上面分析的p标签对应的代码字符串
        }, _l((infos), function (item, key, index) {
            return _c('p', [_v(_s(index) + ":" + _s(key) + ":" + _s(item))])
        }),0)
    }