petite-vue源码剖析-属性绑定`v-bind`的工作原理
关于指令(directive)
属性绑定、事件绑定和v-modal底层都是通过指令(directive)实现的,那么什么是指令呢?我们一起看看Directive的定义吧。
//文件 ./src/directives/index.ts
export interface Directive<T = Element> {
(ctx: DirectiveContext<T>): (() => void) | void
}
指令(directive)其实就是一个接受参数类型为DirectiveContext并且返回cleanup
函数或啥都不返回的函数。那么DirectiveContext有是如何的呢?
//文件 ./src/directives/index.ts
export interface DirectiveContext<T = Element> {
el: T
get: (exp?: string) => any // 获取表达式字符串运算后的结果
effect: typeof rawEffect // 用于添加副作用函数
exp: string // 表达式字符串
arg?: string // v-bind:value或:value中的value, v-on:click或@click中的click
modifiers?: Record<string, true> // @click.prevent中的prevent
ctx: Context
}
深入v-bind的工作原理
walk方法在解析模板时会遍历元素的特性集合el.attributes,当属性名称name匹配v-bind或:时,则调用processDirective(el, 'v-bind', value, ctx)对属性名称进行处理并转发到对应的指令函数并执行。
//文件 ./src/walk.ts
// 为便于阅读,我将与v-bind无关的代码都删除了
const processDirective = (
el: Element,
raw, string, // 属性名称
exp: string, // 属性值:表达式字符串
ctx: Context
) => {
let dir: Directive
let arg: string | undefined
let modifiers: Record<string, true> | undefined // v-bind有且仅有一个modifier,那就是camel
if (raw[0] == ':') {
dir = bind
arg = raw.slice(1)
}
else {
const argIndex = raw.indexOf(':')
// 由于指令必须以`v-`开头,因此dirName则是从第3个字符开始截取
const dirName = argIndex > 0 ? raw.slice(2, argIndex) : raw.slice(2)
// 优先获取内置指令,若查找失败则查找当前上下文的指令
dir = builtInDirectives[dirName] || ctx.dirs[dirName]
arg = argIndex > 0 ? raw.slice(argIndex) : undefined
}
if (dir) {
// 由于ref不是用于设置元素的属性,因此需要特殊处理
if (dir === bind && arg === 'ref') dir = ref
applyDirective(el, dir, exp, ctx, arg, modifiers)
}
}
当processDirective根据属性名称匹配相应的指令和抽取入参后,就会调用applyDirective来通过对应的指令执行操作。
//文件 ./src/walk.ts
const applyDirective = (
el: Node,
dir: Directive<any>,
exp: string,
ctx: Context,
arg?: string
modifiers?: Record<string, true>
) => {
const get = (e = exp) => evaluate(ctx.scope, e, el)
// 指令执行后可能会返回cleanup函数用于执行资源释放操作,或什么都不返回
const cleanup = dir({
el,
get,
effect: ctx.effect,
ctx,
exp,
arg,
modifiers
})
if (cleanup) {
// 将cleanup函数添加到当前上下文,当上下文销毁时会执行指令的清理工作
ctx.cleanups.push(cleanup)
}
}
现在我们终于走到指令bind执行阶段了
//文件 ./src/directives/bind.ts
// 只能通过特性的方式赋值的属性
const forceAttrRE = /^(spellcheck|draggable|form|list|type)$/
export const bind: Directive<Element & { _class?: string }> => ({
el,
get,
effect,
arg,
modifiers
}) => {
let prevValue: any
if (arg === 'class') {
el._class = el.className
}
effect(() => {
let value = get()
if (arg) {
// 用于处理v-bind:style="{color:'#fff'}" 的情况
if (modifiers?.camel) {
arg = camelize(arg)
}
setProp(el, arg, value, prevValue)
}
else {
// 用于处理v-bind="{style:{color:'#fff'}, fontSize: '10px'}" 的情况
for (const key in value) {
setProp(el, key, value[key], prevValue && prevValue[key])
}
// 删除原视图存在,而当前渲染的新视图不存在的属性
for (const key in prevValue) {
if (!value || !(key in value)) {
setProp(el, key, null)
}
}
}
prevValue = value
})
}
const setProp = (
el: Element & {_class?: string},
key: string,
value: any,
prevValue?: any
) => {
if (key === 'class') {
el.setAttribute(
'class',
normalizeClass(el._class ? [el._class, value] : value) || ''
)
}
else if (key === 'style') {
value = normalizeStyle(value)
const { style } = el as HTMLElement
if (!value) {
// 若`:style=""`则移除属性style
el.removeAttribute('style')
}
else if (isString(value)) {
if (value !== prevValue) style.cssText = value
}
else {
// value为对象的场景
for (const key in value) {
setStyle(style, key, value[key])
}
// 删除原视图存在,而当前渲染的新视图不存在的样式属性
if (prevValue && !isString(prevValue)) {
for (const key in prevValue) {
if (value[key] == null) {
setStyle(style, key, '')
}
}
}
}
}
else if (
!(el instanceof SVGElement) &&
key in el &&
!forceAttrRE.test(key)) {
// 设置DOM属性(属性类型可以是对象)
el[key] = value
// 留给`v-modal`使用的
if (key === 'value') {
el._value = value
}
} else {
// 设置DOM特性(特性值仅能为字符串类型)
/* 由于`<input v-modal type="checkbox">`元素的属性`value`仅能存储字符串,
* 通过`:true-value`和`:false-value`设置选中和未选中时对应的非字符串类型的值。
*/
if (key === 'true-value') {
;(el as any)._trueValue = value
}
else if (key === 'false-value') {
;(el as any)._falseValue = value
}
else if (value != null) {
el.setAttribute(key, value)
}
else {
el.removeAttribute(key)
}
}
}
const importantRE = /\s*!important/
const setStyle = (
style: CSSStyleDeclaration,
name: string,
val: string | string[]
) => {
if (isArray(val)) {
val.forEach(v => setStyle(style, name, v))
}
else {
if (name.startsWith('--')) {
// 自定义属性
style.setProperty(name, val)
}
else {
if (importantRE.test(val)) {
// 带`!important`的属性
style.setProperty(
hyphenate(name),
val.replace(importantRE, ''),
'important'
)
}
else {
// 普通属性
style[name as any] = val
}
}
}
}
总结
通过本文我们以后不单可以使用v-bind:style绑定单一属性,还用通过v-bind一次过绑定多个属性,虽然好像不太建议这样做>_<
后续我们会深入理解v-on事件绑定的工作原理,敬请期待。
petite-vue源码剖析-属性绑定`v-bind`的工作原理的更多相关文章
- petite-vue源码剖析-双向绑定`v-model`的工作原理
前言 双向绑定v-model不仅仅是对可编辑HTML元素(select, input, textarea和附带[contenteditable=true])同时附加v-bind和v-on,而且还能利用 ...
- petite-vue源码剖析-事件绑定`v-on`的工作原理
在书写petite-vue和Vue最舒服的莫过于通过@click绑定事件,而且在移除元素时框架会帮我们自动解除绑定.省去了过去通过jQuery的累赘.而事件绑定在petite-vue中就是一个指令(d ...
- Spark源码剖析(八):stage划分原理与源码剖析
引言 对于Spark开发人员来说,了解stage的划分算法可以让你知道自己编写的spark application被划分为几个job,每个job被划分为几个stage,每个stage包括了你的哪些代码 ...
- Vue源码解析---数据的双向绑定
本文主要抽离Vue源码中数据双向绑定的核心代码,解析Vue是如何实现数据的双向绑定 核心思想是ES5的Object.defineProperty()和发布-订阅模式 整体结构 改造Vue实例中的dat ...
- [Vue源码]一起来学Vue双向绑定原理-数据劫持和发布订阅
有一段时间没有更新技术博文了,因为这段时间埋下头来看Vue源码了.本文我们一起通过学习双向绑定原理来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫 ...
- 源码剖析@ApiImplicitParam对@RequestParam的required属性的侵入性
问题起源 使用SpringCloud构建项目时,使用Swagger生成相应的接口文档是推荐的选项,Swagger能够提供页面访问,直接在网页上调试后端系统的接口, 非常方便.最近却遇到了一个有点困惑的 ...
- petite-vue源码剖析-逐行解读@vue/reactivity之reactive
在petite-vue中我们通过reactive构建上下文对象,并将根据状态渲染UI的逻辑作为入参传递给effect,然后神奇的事情发生了,当状态发生变化时将自动触发UI重新渲染.那么到底这是怎么做到 ...
- 逐行剖析Vue源码(一)——写在最前面
1. 前言 博主作为一名前端开发,日常开发的技术栈是Vue,并且用Vue开发也有一年多了,对其用法也较为熟练了,但是对各种用法和各种api使用都是只知其然而不知其所以然,因此,有时候在排查bug的时候 ...
- 菜鸟nginx源码剖析 框架篇(一) 从main函数看nginx启动流程(转)
俗话说的好,牵牛要牵牛鼻子 驾车顶牛,处理复杂的东西,只要抓住重点,才能理清脉络,不至于深陷其中,不能自拔.对复杂的nginx而言,main函数就是“牛之鼻”,只要能理清main函数,就一定能理解其中 ...
随机推荐
- VC++ 启用内存泄露检测
_CrtDumpMemoryLeaks()就是检测从程序开始到执行该函数进程的堆使用情况,通过使用_CrtDumpMemoryLeaks()我们可以进行简单的内存泄露检测. #include &quo ...
- Linux 常见文件管理命令
Linux文件系统 根目录:/ 从根目录开始,下面有一堆小目录 root:根用户的目录 bin:可执行文件命令 etc:配置文件 var:日志 lib:安装包或头文件,库文件 home:所有用户的家目 ...
- ApacheCN C# 译文集 20211124 更新
C# 代码整洁指南 零.前言 一.C# 代码标准和原则 二.代码审查--过程和重要性 三.类.对象和数据结构 四.编写整洁的函数 五.异常处理 六.单元测试 七.端到端系统测试 八.线程和并发 九.设 ...
- Gitee 自已提交的代码提交人头像为他人、码云上独自开发的项目显示为 2 个开发者
简介 自己写的代码提交到码云(Gitee)上却变成了两个人,一个被正确的代码提交统计了,另一个却没有,并且确信自己输入的Gitee账号是自己绑定的邮箱,具体如下: 解决办法 查看自己的用户名 git ...
- js静态成员和实例成员
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- JavaScript检查Date对象是否为Invalid Date
使用Date()构造日期对象,如果传入非日期格式的字符串,仍然能构造出Date对象. 在chrome控制台 >var date = new Date("hello"); &g ...
- php截取字符串,避免乱码
转载请注明来源:https://www.cnblogs.com/hookjc/ 1. 截取GB2312中文字符串 <?php//截取中文字符串 function mysubstr($str, $ ...
- 随机数类 Random
import java.util.Random; /* 随机数类 Random 需求: 编写一个函数随机产生四位的验证码. */ public class Demo5 { public static ...
- WebGPU 中消失的 FBO 和 RBO
目录 1 WebGL 中的 FBO 与 RBO 1.1 帧缓冲对象(FramebufferObject) 1.2 颜色附件与深度模板附件的真正载体 1.3 FBO/RBO/WebGLTexture 相 ...
- suse 12 利用缓存创建本地源供内网服务使用
文章目录 服务端获取 添加源 刷新源 清除缓存 安装软件 获取rpm包 客户端测试 zypper --help 前言: 其实,咱也不知道为啥写了这篇博客,咱就是想学一学suse,咱也不会,咱也只能学, ...