浅曦Vue源码-27-挂载阶段-$mount- genDirectives(16)

876 阅读6分钟

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

一、前情回顾 & 背景

上一篇小作文在说 parse 生成 ast 后的下一个阶段 generate,这个阶段会将 ast 变成渲染函数代码字符串。这个过程的核心方法是 generate() 方法,而 generate 方法的核心又是 genElment 方法,genElement 方法的第一个步骤就是处理静态根节点即 genStatic 方法,提升静态渲染函数到 staticRenderFns 数组中;

前文说道了 genData() 方法中会调用 genDirectives 方法优先处理指令,这个方法又是理解 Vue 双向数据绑定的一个小细节,本篇小作文的篇幅将会送给它。

二、genDirectives

方法位置:src/compiler/codegen/index.js -> function genDirectives

方法参数

  1. elast 节点对象;
  2. stateCodegenState 实例对象

方法作用:调用 genDirectives() 方法进行指令的编译,所谓指令编译就是调用前面提到过的指令处理方法处理平台上的指令,例如 web 平台下的 v-html、v-model 等,指令处理方法在初始化 CodegenState 实例的时候挂载到实例上this.directives = extend(extend({}, baseDirectives), options.directives),即 state.directives

function genDirectives (el: ASTElement, state: CodegenState): string | void {
  // 获取指令数组
  const dirs = el.directives

  // 如果没有指令则退出
  if (!dirs) return

  // 指令的处理结果,为啥长这样?这是因为 genDirectives 是被 genData 调用的,
  // 所以返回的是 data 对象的一个 key,
  // data = '{';
  // dirs = genDirectives(); 
  // data += dirs + ','; data 就变成这样了 { direcitves: [....],
  let res = 'directives:['

  // 标记标记指令是否需要在运行时代码的配合,比如 v-model 的 input 事件就是运行时配合的部分
  let hasRuntime = false

  // 遍历指令数组
  let i, l, dir, needRuntime
  for (i = 0, l = dirs.length; i < l; i++) {
    dir = dirs[i]
    needRuntime = true
    // 获取节点当前指令的处理方法,比如 web 平台的 v-html、v-text、v-model
    // state.directives 这部分哪里来的呢?前文讲初始化 CodegenState 的时候说过啊~
    const gen: DirectiveFunction = state.directives[dir.name]
    if (gen) {
      // 执行指令的处理方法,如果指令还需要运行时配合,返回 true,比如 v-model
      // compile-time directive that manipulates AST.
      // returns true if it also needs a runtime counterpart.
      needRuntime = !!gen(el, dir, state.warn)
    }
    if (needRuntime) {
      // 需要运行时处理的指令的,返回的 res 拼接一个对象: {name, rawName, arg, modifiers } 
      hasRuntime = true
      res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
        dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
      }${
        dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''
      }${
        dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
      }},`
    }
  }
  if (hasRuntime) {
    // 如需要运行时则返回结果 [{ name, rawName, arg, modifiers }]
    return res.slice(0, -1) + ']'
  }
}

2.1 state.directives

前面介绍 parseHTML 的时候会利用 pluckModuleFunctionoptions.modules 提取 preTransformNode、transformNode、postTransformNode 方法用于处理 ast 节点对象。而 options 就是创建编译器时传递的 baseOptions

接下来要说的这个 options.directives 也是来自 baseOptions.directives;我们在开发 Vue 项目的过程中使用一个指令可以实现一个复杂的功能,之所以能够用起来很轻松,是因为有框架在负重前行。而原生的指令比如 v-model/v-text/v-html 都需要编译时的支持,甚至还需要运行时+编译时的协作;

  • 模块位置:src/platforms/web/compiler/options.js
import directives from './directives/index'

// src/platform/web/compiler/options.js
export const baseOptions: CompilerOptions = {
  expectHTML: true,
  modules, // 处理 class、style、v-module
  directives, // 处理指令的方法
  // ....
  staticKeys: genStaticKeys(modules)
}
  • directives 来自这个模块:src/platforms/web/compiler/directives/index.js
import model from './model'
import text from './text'
import html from './html'

export default {
  model, // 处理 v-model
  text, // 处理 v-text
  html // 处理 v-html
}

2.2 v-model 和 model 模块

方法位置:src/platforms/web/compiler/directives/model.js -> function model

方法参数:

  1. elast` 节点对象
  2. dirast 节点上使用的指令及其详细信息;从上面 genDirectives 可以看出 direl.directives 的项,el.directives 中的项是前面 parseHTML 时调用 prcessAttrs 时调用 addDirective() 将节点上的指令添加的;
  3. _warn: 警告信息提示方法

方法作用:获取指令的详细信息,分情况处理 selectinput、以及 input 标签不同的 type,给 v-model 在运行时代码中为元素绑定不同的事件处理 handler 以实现不同类型的双向绑定;具体分为以下步骤:

  1. dir 获取指令详细数据:指令绑定的值 value,修饰符 modifier,标签名 tag,以及 type

  2. 处理 el 是自定义组件且使用了 v-model 情况,调用 genComponentModel

  3. 处理 el 的是 select 的双向绑定,调用 genSelect

  4. 处理 type = radioinput 的双向绑定

  5. 处理 input type 默认值 text 和 textarea 的情况,这个情况也是我们最常见的情况,调用 genDefaultModel 方法处理;

  6. 如果 el.tag 不是平台保留标签,就将按照自定义组件的方式处理,调用 genComponentModelreturn false

  7. 最后,这个方法的返回值是标识当前这个 v-model 是否需要需要运行时配合。这个结果会影响 上面 genDirectives() 方法的返回结果;

export default function model (
 el: ASTElement,
 dir: ASTDirective,
 _warn: Function
): ?boolean {
 warn = _warn
 const value = dir.value
 const modifiers = dir.modifiers
 const tag = el.tag
 const type = el.attrsMap.type

 if (process.env.NODE_ENV !== 'production') {
   // input type = file 是只读,不能写,警告
 }

 if (el.component) {
   // 处理自定义组件的 v-model,自定义组件的 v-model 由组件定义者自己实现
   // 所以它不需要框架额外提供运行时辅助
   genComponentModel(el, value, modifiers)
   // component v-model doesn't need extra runtime
   return false
 } else if (tag === 'select') {
   // 处理 select 标签的 v-model
   genSelect(el, value, modifiers)
 } else if (tag === 'input' && type === 'checkbox') {
   // 处理 checkbox 的 v-model
   genCheckboxModel(el, value, modifiers)
 } else if (tag === 'input' && type === 'radio') {
   // 处理 radio v-mdel
   genRadioModel(el, value, modifiers)
 } else if (tag === 'input' || tag === 'textarea') {
   // 默认情况下 <input type="text / textarea" /> 的 v-model
   genDefaultModel(el, value, modifiers)
 } else if (!config.isReservedTag(tag)) {
   genComponentModel(el, value, modifiers)
   // component v-model doesn't need extra runtime
   return false
 } else if (process.env.NODE_ENV !== 'production') {
   // 其他标签的 v-model 不被支持,抛出警告
 }

 // 返回 true 表示需要框架提供运行时辅助
 // ensure runtime directive metadata
 return true
}

2.2.1 genDefaultModel

方法位置:src/platforms/web/compiler/directives/model.js -> function genDefaultModel

方法参数:

  1. elast 节点对象
  2. valuev-model 绑定的值
  3. modifiers:指令修饰符

方法作用:处理 input 标签的默认 type=text/textarea 时的 v-model 指令所需要的运行时辅助程序:这部分就是大家所熟知的给 input 绑定 input 事件,待事件触发时更新 v-model 指向的值,然后触发 Vue 的数据观察,执行 patching 页面就更新了;

function genDefaultModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
): ?boolean {
  // 获取 input 绑定的 type
  const type = el.attrsMap.type
  
  if (process.env.NODE_ENV !== 'production') {
    // v-model 和 value 属性不能同时出现,抛出警告
  }

  // 获取修饰符 lazy, number, trim 
  const { lazy, number, trim } = modifiers || {}
  const needCompositionGuard = !lazy && type !== 'range'
  
  // 根据 type 和 lazy 确定要给 input 绑定的事件类型
  const event = lazy
    ? 'change'
    : type === 'range'
      ? RANGE_TOKEN
      : 'input'

  // 事件 handler 中的取值表达式
  let valueExpression = '$event.target.value'
  if (trim) {
    // 如果 trim 修饰符存在,则给输入框中的新值进行 trim 操作
    valueExpression = `$event.target.value.trim()`
  }
  if (number) {
    // 如果 number 修饰符存在,则转成数字,_n 也是渲染帮助函数
    valueExpression = `_n(${valueExpression})`
  }
  
  // 生成事件处理函数代码
  let code = genAssignmentCode(value, valueExpression)
  if (needCompositionGuard) {
    code = `if($event.target.composing)return;${code}`
  }

  // input 只能识别 value 属性,用以展示输入框的值
  addProp(el, 'value', `(${value})`)
  
  // 这一步就是大家熟悉的 input 绑定事件的过程
  addHandler(el, event, code, null, true)
  if (trim || number) {
     // 如果是 trim、number 修饰符存在还要绑定 blur 事件,在输入框失去焦点时重新渲染
     // 重新渲染后就是 trim 或者 转成数字后的新值
    addHandler(el, 'blur', '$forceUpdate()')
  }
}

2.2.2 genAssignmentCode

方法位置:src/compiler/directives/model.js -> function genAssignmentCode

方法参数:

  1. valueinput 元素绑定的值
  2. assignment:要赋给 input 的新值

方法作用:生成 v-model 指令 value 的 赋值语句 的帮助函数;

export function genAssignmentCode (
  value: string,
  assignment: string
): string {
  const res = parseModel(value) // 解析取值表达式形式的 value,例如 v-model="obj.value"
  if (res.key === null) {
    // 如果 key 为 null 说明 v-model 绑定的不是 obj.value 形式的值
    return `${value}=${assignment}`
  } else {
    // value 绑定了取值表达式 obj.val ,需要调用 $set() 进行更新
    return `$set(${res.exp}, ${res.key}, ${assignment})`
  }
}

2.3 v-html 和 html 模块

方法位置:src/platforms/web/compiler/directives/html.js -> function html

方法参数:

  1. elast 节点对象
  2. dirv-html 绑定的值

方法作用:将 v-html 绑定的值变成 el.innerHTML 属性,值是 _s(当前 v-html 绑定的值) 方法的返回值_s 也是个渲染函数的帮助函数;

注意,v-html 指令是不需要运行时辅助程序的,所以没有返回值,或者说返回 undefined

export default function html (el: ASTElement, dir: ASTDirective) {
  if (dir.value) {
    addProp(el, 'innerHTML', `_s(${dir.value})`, dir)
  }
}

2.4 v-text 和 text 模块

方法位置:src/platforms/web/compiler/directives/text.js -> function text

方法参数:

  1. elast 节点对象
  2. dirv-text 指令绑定的值

方法作用:将 v-text 绑定的值变成 el.textContext 属性,值是 _s(指令绑定的值) 方法的返回值_s 是渲染函数帮助函数;

注意,v-text 指令是不需要运行时辅助程序的,所以没有返回值,或者说返回 undefined;

export default function text (el: ASTElement, dir: ASTDirective) {
  if (dir.value) {
    addProp(el, 'textContent', `_s(${dir.value})`, dir)
  }
}

三、总结

本篇小作文详细讨论了 genDirectives 方法,它负责调用相应的处理方法处理 el.directives 中的指令,并且返回对应的指令是否需要运行时的辅助标识符;

处理指令的方法来自 baseOptions.directives(其实在创建编译器的时候可以传入其他的指令编译处理方法),baseOptions.directives = { text, html, model }

  • text 方法处理 v-text,将 v-text 绑定的值处理并保存到 el.textContent 属性;
  • html 方法处理 v-html,将 v-html 绑定的值处理并保存到 el.innerHTML 属性;
  • model 方法处理 v-model,这个方法根据 el.component、el.tag、inputtype 值来处理各种类型的 v-model 指令。v-model 是需要运行时负责的,所谓运行时辅助就是给使用 v-model 的元素绑定不同事件 handlerhandler 的核心就是更新 v-model 绑定的值,在这个过程中处理 trim/number/lazy 等修饰符;