【vue2.x原理剖析十】指令原理

79 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天,点击查看活动详情

前言

源码分析文章看了很多,也阅读了至少两遍源码。终归还是想自己写写,作为自己的一种记录和学习。重点看注释部分和总结,其余不用太关心,通过总结对照源码回看过程和注释收获更大

指令生效,其实就是在合适的时机执行定义指令时所设置的钩子函数

v-for

在对template进行解析时会将相关指令收集,在编译时会做统一的处理,会将v-for生成_l函数(类似于 forEach)。处理时先处理v-for,再处理v-if,所以**v-for的优先级比v-if高,如果同时写v-forv-if,假如v-if值为false,那么先v-for渲染,再v-if隐藏,多出了不必要的渲染,所以不推荐v-forv-if同时使用,而是利用计算属性代替**

// src/compiler/codegen/index.js
export function genElement (el: ASTElement, state: CodegenState): string {
  ...
  else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  }
  ...
}
export function genFor (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altHelper?: string
): string {
  // v-for='a in arr'
  const exp = el.for // arr
  const alias = el.alias // a
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : '' // 第一个参数
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : '' // 第二个参数


  el.forProcessed = true // avoid recursion生成循环函数,防止死循环
  // 字符串拼接 -l((arr),function(a){return _c('div'), {}})
  return `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
      `return ${(altGen || genElement)(el, state)}` +
    '})'
}

v-if

v-if在编译时会产生类似三元表达式的写法,如果不显示就会走之后的逻辑(比如 v-else),将当前编译成_e函数(空虚拟节点)

export function genIf(
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string,
): string {
  el.ifProcessed = true; // el.ifConditions.slice()可能会有多个条件 v-if v-else v-else-if
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty);
}
function genIfConditions(
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string,
): string {
  if (!conditions.length) {
    return altEmpty || '_e()';
  }
  // 取出第一个条件
  const condition = conditions.shift();
  // 三元表达式
  if (condition.exp) {
    // 如果有表达式
    return `(${condition.exp})?${
      // 将表达式拼接起来
      genTernaryExp(condition.block)
    }:${
      // v-else-if
      genIfConditions(conditions, state, altGen, altEmpty)
    }`;
  } else {
    return `${genTernaryExp(condition.block)}`; // 没有表达式就直接生成元素 像v-else
  }

  function genTernaryExp(el) {
    return altGen
      ? altGen(el, state)
      : el.once
      ? genOnce(el, state)
      : genElement(el, state);
  }
}

v-show

v-show指令类似于自定义指令,会在不同时机去调用设置的钩子函数,在创建虚拟节点的时候会设置styledisplay属性

// /src/platforms/directives/show.js
export default {
  bind (el: any, { value }: VNodeDirective, vnode: VNodeWithData) {
    vnode = locateNode(vnode)
    const transition = vnode.data && vnode.data.transition
    const originalDisplay = el.__vOriginalDisplay =
      el.style.display === 'none' ? '' : el.style.display
    if (value && transition) {
      vnode.data.show = true
      enter(vnode, () => {
        el.style.display = originalDisplay
      })
    } else {
      el.style.display = value ? originalDisplay : 'none'
    }
  },

  update (el: any, { value, oldValue }: VNodeDirective, vnode: VNodeWithData) {
    ...
  },

  unbind (el,binding,vnode,oldVnode,isDestroy) {
    if (!isDestroy) {
      el.style.display = el.__vOriginalDisplay
    }
  }
}

v-model

v-model使用场景有两种方式,一种是表单控件绑定,一种是组件上使用
解析属性

// src/compiler/parser/index.js
function processAttrs (el) {
  const list = el.attrsList
  let i, l, name, rawName, value, modifiers, syncGen, isDynamic
  for (i = 0, l = list.length; i < l; i++) {
    name = rawName = list[i].name
    value = list[i].value
    if (dirRE.test(name)) {
      if (bindRE.test(name)) { // v-bind
        ...
      } else if (onRE.test(name)) { // v-on
        ...
      } else { // normal directives
        // 替换v-
        name = name.replace(dirRE, '')
        const argMatch = name.match(argRE)
        let arg = argMatch && argMatch[1]
        isDynamic = false
        ...
        addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
      }
    } else {
      ...
    }
  }
}
// src/compiler/helpers.js
export function addDirective (
  el: ASTElement,
  name: string,
  rawName: string,
  value: string,
  arg: ?string,
  isDynamicArg: boolean,
  modifiers: ?ASTModifiers,
  range?: Range
) {
  // 添加到el.directives数组中
  (el.directives || (el.directives = [])).push(rangeSetItem({
    name,
    rawName,
    value,
    arg,
    isDynamicArg,
    modifiers
  }, range))
  el.plain = false
}

生成代码

// src/compiler/codegen/index.js
let data;
if (!el.plain || (el.pre && state.maybeComponent(el))) {
  data = genData(el, state);
}
export function genData (el: ASTElement, state: CodegenState): string {
  const dirs = genDirectives(el, state)
  ...
}
function genDirectives (el: ASTElement, state: CodegenState): string | void {
  // gen此时为model
  const gen: DirectiveFunction = state.directives[dir.name]
}
// src/platforms/web/compiler/directives/model.js
export default function model (el,dir,_warn){
  warn = _warn
  const value = dir.value
  const modifiers = dir.modifiers
  const tag = el.tag
  const type = el.attrsMap.type
  // 动态组件
  if (el.component) {
    genComponentModel(el, value, modifiers)
    return false
  // 判断tag类型
  } else if (tag === 'select') {
    genSelect(el, value, modifiers)
  } else if (tag === 'input' && type === 'checkbox') {
    genCheckboxModel(el, value, modifiers)
  } else if (tag === 'input' && type === 'radio') {
    genRadioModel(el, value, modifiers)
  } else if (tag === 'input' || tag === 'textarea') {
    genDefaultModel(el, value, modifiers)
  } else if (!config.isReservedTag(tag)) {
    genComponentModel(el, value, modifiers)
    return false
  } 
  return true
}
function genDefaultModel (el,value,modifiers) {
  const event = lazy? 'change': type === 'range'? RANGE_TOKEN: 'input'
  // 定义表达式
  let valueExpression = '$event.target.value'
  if (trim) {
    valueExpression = `$event.target.value.trim()`
  }
  if (number) {
    valueExpression = `_n(${valueExpression})`
  }
  // 跨平台逻辑
  let code = genAssignmentCode(value, valueExpression)
  if (needCompositionGuard) {
    code = `if($event.target.composing)return;${code}`
  }
  // 为el(input)添加value的prop
  addProp(el, 'value', `(${value})`)
  // 为el添加事件
  addHandler(el, event, code, null, true)
  if (trim || number) {
    addHandler(el, 'blur', '$forceUpdate()')
  }
}

v-model事实上是input + :value的语法糖(两者还是有些许差别),编译阶段会在v-model的元素上定义一个valueprop,并生成一个inputevent事件,所以在使用v-model时不能定义value的prop,会冲突

// 示例
// 此种写法不能和v-model等价,当输入中文时,输入一个字母时,此种写法会实时更新,而v-model不会,会监听compositionstart和compositionend事件,当监听到输入结束时会手动调用input方法
let vm = new Vue({
  el: '#app',
  template: '<div>' + 
  '<p>{{message}}</p>' +
  '<input' + ':value="message"' +
  '@input="message=$event.target.value"' +
  'placeholder="edit me"' + '</div>',
  data(){
    return {
      message: ''
    }
  }
})

在组件上使用

let chile = {
  template: '<div>' +
  '<input : value="value" @input="updateValue"' + '</div>',
  props: ['value'],
  methods: {
    updateValue(e) {
      this.$emit('input', e.target.value)
    }
  }
}
let vm = new Vue({
  el: '#app',
  template: '<div>'+
  '<child v-model="message"></child>' + 
  '<p>{{message}}</p>' +
  '</div>',
  data(){
    return {
      message: ''
    }
  },
  components: {child}
})

与表单控件绑定不一样的是,在生成代码阶段会走不同的处理函数,它会创建model属性,定义值和回调函数,为data扩展model对象,在创建组件时会转化为props和events,所以再子组件内需要定义model对象,存放event(事件)prop

// src/compiler/codegen/index.js
if (el.component) {
    genComponentModel(el, value, modifiers)
    return false
  // 判断tag类型
  } else if (tag === 'select') {
    genSelect(el, value, modifiers)
  } else if (tag === 'input' && type === 'checkbox') {
    genCheckboxModel(el, value, modifiers)
  } else if (tag === 'input' && type === 'radio') {
    genRadioModel(el, value, modifiers)
  } else if (tag === 'input' || tag === 'textarea') {
    genDefaultModel(el, value, modifiers)
  } else if (!config.isReservedTag(tag)) {
    // 组件
    genComponentModel(el, value, modifiers)
    return false
  }
// core/compiler/directives/model.js
export function genComponentModel (el,value,modifiers) {
  const { number, trim } = modifiers || {}
  const baseValueExpression = '$$v'
  ...
  // 定义表达式
  const assignment = genAssignmentCode(value, valueExpression)
  // 创建model属性,定义值和回调函数,为data扩展model对象,在创建组件时会转化为props和events
  el.model = {
    value: `(${value})`,
    expression: JSON.stringify(value),
    callback: `function (${baseValueExpression}) {${assignment}}`
  }
}
export function genAssignmentCode (value,assignment) {
  const res = parseModel(value)
  if (res.key === null) {
    return `${value}=${assignment}`
  } else {
    return `$set(${res.exp}, ${res.key}, ${assignment})`
  }
} 

v-slot

插槽有普通插槽和作用域插槽,两者区别是渲染位置不同

  • 普通插槽是父组件编译完毕后替换子组件的内容
<div id ="app">
  <home>
    <h1 v-slot:title>标题</h1>
    <div #content>内容</div>
  </home>
</div>
<script>
  Vue.component('home', {
    template: `<div>
      <slot name="title"></slot>
      <slot name="content"></slot>
    </div>`
  })
</script>

首先编译父组件如果遇到子组件有slot会给对应的ast元素节点的data上存放slot属性,值为插槽名字,父组件编译完成后,在codegen阶段开始编译子组件,在编译子组件时在parser阶段,遇到slot标签时候会给对应的ast元素节点添加slotName属性,在codegen阶段会判断如果当前是slot标签,则执行genSlot函数得到_t函数,_t函数会拿插槽名字去$slot属性上找对应的vnode,在编译子组件时候父组件已经编译完成,$slots属性的生成是在子组件init过程中会执行initRender函数,会执行resolveSlot方法,遍历父vnode的children,拿到每一个child的data,通过data.slot拿到插槽名称,接着以插槽名称为key把child添加到slots中,如果data.slot不存在,则是默认插槽的内容,则把对应的child添加到slots.defaults中,$slots就是slots,它是一个对象,key是插槽名称,value是一个vnode类型的数组,因为他可以有多个同名插槽

  • 作用域插槽是在子组件里边渲染插槽的内容
<div id ="app">
  <home>
    <template slot-scope="{article}">
      <h1>{{article.title}}</h1>
    </template>
  </home>
</div>
<script>
  Vue.component('home', {
    template: `<div>
      <slot :article="{title: '标题',content: '内容'}"></slot>
    </div>`
  })
</script>

解析插槽

// src/compiler/parser/index.js
function processSlotContent (el) {
  let slotScope
  if (el.tag === 'template') {
    slotScope = getAndRemoveAttr(el, 'scope')
    el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope')
  } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
    el.slotScope = slotScope
  }
  // 取当前属性绑定的slot
  const slotTarget = getBindingAttr(el, 'slot')
  if (slotTarget) {// 增加slotTarget属性
    //如果没有给名字会默认是default
    el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget
    el.slotTargetDynamic = !!(el.attrsMap[':slot'] || el.attrsMap['v-bind:slot'])
    if (el.tag !== 'template' && !el.slotScope) {
      // 给el添加slot属性 {slot: xxx}
      addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'))
    }
  }
  // 2.6.x处理v-slot
  if (process.env.NEW_SLOT_SYNTAX) {
    ...
  }
}

生成代码

// src/compiler/codegen/index.js
// slot 为data扩展一个slot属性
if (el.slotTarget && !el.slotScope) {
    data += `slot:${el.slotTarget},`
  }
// <slot></slot>
function genSlot (el, state) {
  const slotName = el.slotName || '"default"'
  const children = genChildren(el, state)
  // 生成_t函数(去$slots属性上找对应的name的vnode)
  let res = `_t(${slotName}${children ? `,function(){return ${children}}` : ''}`
  const attrs = el.attrs || el.dynamicAttrs
    ? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
        name: camelize(attr.name),
        value: attr.value,
        dynamic: attr.dynamic
      })))
    : null
  ...
}

首先是编译父组件,读取slot-scope属性并赋值给当前ast元素节点的slotScope属性,构造ast树的时候,对于拥有slotScope属性的元素而言,是不会作为children添加到ast中,而是存在了父元素节点的scopedSlots(对象,key为插槽名字)属性上,在codegen生成代码阶段会对scopedSlots对象遍历,执行genScopedSlot函数,genScopedSlot会生成一段函数代码,参数时scoped-slot对应的值,返回一个对象,key是插槽名称,fn是生成的函数代码,此时,scopedSlots为一个_u函数(遍历传入的数组,生成一个对象,对象的key是插槽名称,value是函数)。在编译子组件时,与普通插槽过程基本相同,唯一区别在于codegen时,会对attrsv-bind做处理,也会得到_t函数,会去$scopedSlots去找插槽名字的fn,然后把相关数据扩展到slot-scope属性上,作为函数的参数传入,执行函数返回生成的vnode,后续渲染。$scopedSlots是在子组件渲染前执行时得到

普通插槽和作用域插槽区别:

  • 普通插槽在父组件编译和渲染阶段生成vnode,所以数据的作用域是父组件实例,子组件渲染的时候直接拿到这些渲染好的vnode
  • 作用域插槽父组件在编译和渲染阶段并不会直接生成vnode,而是在父节点vnode的data中保留一个scopedSlots对象,储存着不同名称的插槽以及他们对应的渲染函数,只有在渲染子组件阶段才会执行这个渲染哈数生成vnode