「这是我参与2022首次更文挑战的第28天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
上一篇小作文在说 parse 生成 ast 后的下一个阶段 generate,这个阶段会将 ast 变成渲染函数代码字符串。这个过程的核心方法是 generate() 方法,而 generate 方法的核心又是 genElment 方法,genElement 方法的第一个步骤就是处理静态根节点即 genStatic 方法,提升静态渲染函数到 staticRenderFns 数组中;
前文说道了 genData() 方法中会调用 genDirectives 方法优先处理指令,这个方法又是理解 Vue 双向数据绑定的一个小细节,本篇小作文的篇幅将会送给它。
二、genDirectives
方法位置:src/compiler/codegen/index.js -> function genDirectives
方法参数
el,ast节点对象;state:CodegenState实例对象
方法作用:调用 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 的时候会利用 pluckModuleFunction 从 options.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
方法参数:
- el
,ast` 节点对象 dir:ast节点上使用的指令及其详细信息;从上面genDirectives可以看出dir是el.directives的项,el.directives中的项是前面parseHTML时调用prcessAttrs时调用addDirective()将节点上的指令添加的;_warn: 警告信息提示方法
方法作用:获取指令的详细信息,分情况处理 select、input、以及 input 标签不同的 type,给 v-model 在运行时代码中为元素绑定不同的事件处理 handler 以实现不同类型的双向绑定;具体分为以下步骤:
-
从
dir获取指令详细数据:指令绑定的值value,修饰符modifier,标签名tag,以及type; -
处理
el是自定义组件且使用了v-model情况,调用genComponentModel -
处理
el的是select的双向绑定,调用genSelect -
处理
type = radio的input的双向绑定 -
处理
input type默认值text 和 textarea的情况,这个情况也是我们最常见的情况,调用genDefaultModel方法处理; -
如果
el.tag不是平台保留标签,就将按照自定义组件的方式处理,调用genComponentModel,return false; -
最后,这个方法的返回值是标识当前这个
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
方法参数:
el,ast节点对象value:v-model绑定的值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
方法参数:
value,input元素绑定的值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
方法参数:
el,ast节点对象dir:v-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
方法参数:
el,ast节点对象dir:v-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、input的type值来处理各种类型的v-model指令。v-model是需要运行时负责的,所谓运行时辅助就是给使用v-model的元素绑定不同事件handler,handler的核心就是更新v-model绑定的值,在这个过程中处理trim/number/lazy等修饰符;