Jinuss's blog Jinuss's blog
首页
  • 源码合集

    • Leaflet源码分析
    • Openlayers源码合集
    • vue3源码
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • 学习
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

东流

Web、WebGIS技术博客
首页
  • 源码合集

    • Leaflet源码分析
    • Openlayers源码合集
    • vue3源码
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • 学习
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • reactivity响应式

  • runtime-core运行时核心模块

  • runtime-dom运行时DOM模块

    • createApp创建实例解析
    • patchProp属性补丁解析
      • 概览
      • 更新方法
        • patchClass
        • patchStyle
        • patchEvent
        • patchDOMProp
        • patchAttr
        • shared.isOn
        • shared.isModeListener
  • 5.18源码学习》
  • runtime-dom运行时DOM模块
东流
2025-09-16
目录

patchProp属性补丁解析

# 概览

在《createApp创建实例解析》中提到pathProp用于更新DOM元素的属性,本文会详细解析下patchProp方法中是如何更新不同类型的属性。

const patchProp = (el, key, prevValue, nextValue, namespace, parentComponent) => {
    // el:目标DOM元素 key:属性名 preValue:旧值 nextValue:新值 namespace:命名空间  parentComponent:父组件实例 
    
    // isSVG表示是否是SVG元素
    const isSVG = namespace === 'svg';
    if (key === 'class') {
        // 处理class属性
        patchClass(el, nextValue, isSVG);
    } else if (key === "style") {
        // 处理style样式属性
        patchStyle(key, prevValue, nextValue);
    } else if (shared.isOn(key)) {
        // 处理事件监听器,isOn判断属性名即事件名称是否以on开头
        if (!shared.isModeListener(key)) {
          // isModeListener过滤掉onUpdate特殊事件 
          patchEvent(el, key, prevValue, nextValue, parentComponent);
        }
    } else if (key[0] === '.' ? (key = key.slice(1), true) : 
               key[0] === "^" ? (key = key.slice(1), false) : 
               shouldSetAsProp(el, key, nextValue, isSVG)) {
        // 处理DOM属性        
        patchDOMProp(el, key, nextValue);
        
        if (!el.tagName.includes("-") && (key === "value" || key === "checked" || key === "selected")) {
        // 处理特殊属性 value/checked/selected  
          patchAttr(el, key, nextValue, isSVG, parentComponent, key !== "value")
        }
    } else if (el._isVueCE && (/[A-Z]/.test(key) || !shared.isstring(nextValue))) {
        // 处理Web Components属性
        patchDOMProp(el, shared, camelize(key), nextValue, parentComponent, key)
    } else {
        // 处理其他属性
        if (key === 'true-value') {
            el._trueValue = nextValue;
        } else if (key === 'false-value') {
            el._falseValue = nextValue;
        }
        patchAttr(el, key, nextValue, isSVG)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 更新方法

# patchClass

patchClass主要处理class属性,先判断是否存在动画class类名,若存在,则将其一并加入到value中;然后判断value是否为null。若value为null,则移除元素的class属性,否则判断元素是否是SVG元素,若是SVG元素,则设置class属性的值;否则设置className的值。

patchClass源码如下:

function patchClass(el, value, isSVG) {
  const transitionClasses = el[vtcKey];
  if (transitionClasses) {
    value = (value ? [value, ...transitionClasses] : [...transitionClasses]).join(" ");
  }
  if (value == null) {
    el.removeAttribute("class");
  } else if (isSVG) {
    el.setAttribute("class", value);
  } else {
    el.className = value;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

patchClass的源码中有注释提到理论上设置className的值比设置属性setAttribute速度要快。

# patchStyle

patchStyle用于更新设置元素的style属性。

// display属性正则
const displayRE = /(^|;)\s*display\s*:/;

function patchStyle(el, prev, next) {
  // 获取元素的style值
  const style = el.style;
  // isCssString表示style的新值是否字符串
  const isCssString = shared.isString(next);
  
  // 表示是否需要控制display属性
  let hasControlledDisplay = false;
  if (next && !isCssString) {
    // 若新值存在并且新值不是一个字符串
    if (prev) {
      // 若style属性存在旧值

      if (!shared.isString(prev)) {
        // 若旧值不是一个字符串,则遍历旧值
        for (const key in prev) {
          // 若在新值中旧值对应key的值为null,则调用setStyle方法,将其置空
          if (next[key] == null) {
            setStyle(style, key, "");
          }
        }
      } else {
        // 若旧值是一个字符串,则调用split方法将其转为数组进行遍历
        for (const prevStyle of prev.split(";")) {
          // 获取旧值中的key
          const key = prevStyle.slice(0, prevStyle.indexOf(":")).trim();
          // 同上,判断旧值中的key在新值中是否为null,为null则调用setStyle
          if (next[key] == null) {
            setStyle(style, key, "");
          }
        }
      }
    }
    // 遍历新值
    for (const key in next) {
      // 若新值中存在display属性,则将hasControlledDisplay变量设为true
      if (key === "display") {
        hasControlledDisplay = true;
      }
      // 然后调用setStyle方法设置样式的新属性
      setStyle(style, key, next[key]);
    }
  } else {
    if (isCssString) {
      // 若style的新值时字符串
      if (prev !== next) {
        // 若新值不等于旧值
        const cssVarText = style[CSS_VAR_TEXT];
        if (cssVarText) {
          // 判断样式中是否存在css文本变量,则拼接新值
          next += ";" + cssVarText;
        }
        // 设置样式的cssText属性
        style.cssText = next;
        // 通过正则匹配,修改hasControlledDisplay
        hasControlledDisplay = displayRE.test(next);
      }
    } else if (prev) {
      // 若旧值存在,则移除元素的style属性
      el.removeAttribute("style");
    }
  }
  // 判断el元素是否存在v-show指令的属性
  if (vShowOriginalDisplay in el) {
    // 设置元素上的属性值
    el[vShowOriginalDisplay] = hasControlledDisplay ? style.display : "";
    if (el[vShowHidden]) {
      // 若元素上设置了v-show指令,且其值为false,则el[vShowHidden]为true
      style.display = "none";
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

# patchEvent

patchEvent用于更新或设置元素上绑定的事件。封装了addEventListener/removeEventListener用于绑定或解绑元素上的事件监听。

patchEvent源码实现如下:

// veiKey用于存储el元素上存储事件调用器invokers的键
const veiKey = Symbol("_vei");
function patchEvent(el, rawName, prevValue, nextValue, instance = null) {
  // rawName 是原始事件名 如onClick或onClickOnce
  const invokers = el[veiKey] || (el[veiKey] = {});
  // 获取或初始化事件调用器对象invokers
  const existingInvoker = invokers[rawName];
  if (nextValue && existingInvoker) {
    // 若当前事件名已存在调用器且信值存在,则更新调用器的值
    existingInvoker.value = nextValue;
  } else {
    // 通过parseName解析出事件名和修饰符
    const [name, options] = parseName(rawName);
    if (nextValue) {
      // 若新值存在,则调用createInvoker创建一个新的调用器
      const invoker = invokers[rawName] = createInvoker(
        nextValue,
        instance
      );
      // 事件监听绑定到元素上
      addEventListener(el, name, invoker, options);
    } else if (existingInvoker) {
      // 若新值不存在且存在旧的调用器,则需要移除事件监听
      removeEventListener(el, name, existingInvoker, options);
      // 清除调用器引用
      invokers[rawName] = void 0;
    }
  }
}

// 绑定监听
function addEventListener(el, event, handler, options) {
  el.addEventListener(event, handler, options);
}
// 移除监听
function removeEventListener(el, event, handler, options) {
  el.removeEventListener(event, handler, options);
}

let cachedNow = 0; // 时间戳
const p = Promise.resolve(); // 微任务

// getNow用于获取当前时间
const getNow = () => cachedNow || (p.then(() => cachedNow = 0), cachedNow = Date.now());

// 创建一个调用器函数
function createInvoker(initialValue, instance) {
  // initialValue:初始值 instance:实例

  // 定义调用器函数invoker
  const invoker = (e) => {
    // 判断事件是否存在时间戳,若不存在,则将其设置为当前时间
    if (!e._vts) {
      e._vts = Date.now();
    } else if (e._vts <= invoker.attached) {
      // 若事件的时间戳小于或等于调用器绑定的时间,则跳过执行
      return;
    }
    // 调用callWithAsyncErrorHandling执行事件,并处理异步错误
    runtimeCore.callWithAsyncErrorHandling(
      // 处理事件传播
      patchStopImmediatePropagation(e, invoker.value),
      instance,
      5,
      [e]
    );
  };
  // 存储当前的事件处理函数
  invoker.value = initialValue;
  // 记录调用器被绑定的时间
  invoker.attached = getNow();
  return invoker;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

# patchDOMProp

patchDOMProp用于在el元素上设置属性,该属性是DOM对象上的JavasScript属性,而不是HTML标签上的特性,这两者是有区别的。

function patchDOMProp(el, key, value, parentComponent, attrName) {
  if (key === "innerHTML" || key === "textContent") {
    // 处理innerHTML和textContent属性
    if (value != null) {
      // unsafeToTrustedHTML 是标记信任的HTML字符串
      el[key] = key === "innerHTML" ? unsafeToTrustedHTML(value) : value;
    }
    return;
  }
  const tag = el.tagName;
  // 处理value属性并且不是<progress>标签,也不是自定义元素
  if (key === "value" && tag !== "PROGRESS" && // custom elements may use _value internally
  !tag.includes("-")) {
    // 获取旧值
    const oldValue = tag === "OPTION" ? el.getAttribute("value") || "" : el.value;
    // 获取新值,兼容处理复选框
    const newValue = value == null ? (
      el.type === "checkbox" ? "on" : ""
    ) : String(value);
    // 判断,若新值不等于旧值,或者元素上不存在_value属性
    if (oldValue !== newValue || !("_value" in el)) {
      // 设置value属性为新值
      el.value = newValue;
    }
    // 若value是null,则移除value属性
    if (value == null) {
      el.removeAttribute(key);
    }
    el._value = value;
    return;
  }
  // 处理其他属性
  let needRemove = false;
  if (value === "" || value == null) {
    const type = typeof el[key];
    if (type === "boolean") {
      value = shared.includeBooleanAttr(value);
    } else if (value == null && type === "string") {
      value = "";
      needRemove = true;
    } else if (type === "number") {
      value = 0;
      needRemove = true;
    }
  }
  try {
    el[key] = value;
  } catch (e) {
  }
  needRemove && el.removeAttribute(attrName || key);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# patchAttr

patchAttr用于处理普通HTML属性和SVG的xlink属性

const xlinkNS = "http://www.w3.org/1999/xlink";
function patchAttr(el, key, value, isSVG, instance, isBoolean = shared.isSpecialBooleanAttr(key)) {
  if (isSVG && key.startsWith("xlink:")) {
    // 若是SVG,且key是xlink:开头
    if (value == null) {
      // 若value是null,则移除属性
      el.removeAttributeNS(xlinkNS, key.slice(6, key.length));
    } else {
      // 设置xlink属性
      el.setAttributeNS(xlinkNS, key, value);
    }
  } else {
    // 普通属性处理
    if (value == null || isBoolean && !shared.includeBooleanAttr(value)) {
      // 若值value为null或undefined,或者是布尔属性且值不应该存在,则移除属性
      el.removeAttribute(key);
    } else {
      // 否则设置属性
      el.setAttribute(
        key,
        isBoolean ? "" : shared.isSymbol(value) ? String(value) : value
      );
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# shared.isOn

isOn用于检测key事件名是否是以on为前缀,第三个字符为大写的。

const isOn = (key) => key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 && (key.charCodeAt(2) > 122 || key.charCodeAt(2) < 97);
1

# shared.isModeListener

isModeListener用于判断事件名是否以onUpdate:开头。

const isModelListener = (key) => key.startsWith("onUpdate:");
1
编辑 (opens new window)
上次更新: 2025/09/22, 01:09:15
createApp创建实例解析

← createApp创建实例解析

最近更新
01
patch中的双端比较快速算法
09-25
02
patchDOM元素的更新解析
09-25
03
patch中DOM元素的挂载解析
09-25
更多文章>
Theme by Vdoing | Copyright © 2024-2025 东流 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式