「这是我参与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
等修饰符;