「ROAD 6」浏览器原理-CSS计算


主要分析将 DOM 树加上 CSS 规则的步骤。

STEP 0: 环境准备

npm install css

CSS

STEP 1: 收集 CSS 规则

  • 遇到 style 标签时,需要将 CSS 规则保存收集;
  • 调用 CSS parser 分析 CSS 规则;
  • 必须仔细研究此库分析 CSS 规则的格式;

STEP 2: 添加调用

  • 创建元素之后,立即计算 CSScssComputer.computeCss(element);
  • 理论上,当分析一个元素时,所有的 CSS 规则已经收集完毕;
  • 在真实浏览器中,可能遇到写在 body 内的 style 标签,需要重新进行 CSS 计算的情况,但在这里我们进行忽略(CSS 重新计算,重排,重绘,所以需要在编写代码时需要将 style 写在所有元素最前面);

STEP 3: 获取父元素序列

let elements = stack.slice().reverse();
  • 在 computeCSS 函数内,必须知道元素的所有父元素才能判断元素与规则是否匹配;
  • 从上一步骤的 stack 内可以获取本元素的所有元素;
  • 因为我们首先获取的事“当前元素”,所以获取和计算父元素匹配的顺序是从内向外

STEP 4: 拆分选择器

  • 选择器也需要从当前元素向外排列;
  • 复杂选择器需要拆成针对单个元素的选择器,用循环来匹配父元素队列

STEP 5: 计算选择器与元素匹配

  • 根据选择器的类型和元素属性,计算是否与当前元素匹配;
  • 该 toy-browser 仅实现三种基本选择器,实际浏览器中要处理更复杂的复合浏览器;

STEP 6: 生成 computed 属性

  • 一旦选择匹配,就应用选择器到元素上,形成 computedStyle;

PS:可能会涉及到选择器的优先级(specificity)

STEP 7: 确定规则覆盖关系

选择器优先级的计算和比较。

基本代码

// css-computed.js
const css = require("css");
let rules = [];

// **** 添加CSS规则 **** //
function addCSSRules(text) {
  let ast = css.parse(text);
  // console.log(JSON.stringify(ast, null, "    "));
  rules.push(...ast.stylesheet.rules);
}

// *** 针对对象:元素 **** //
function computeCss(element, stack) {
  // console.log(JSON.stringify(rules, null, " "));
  // & 使用slice避免污染原始stack
  let elements = stack.slice().reverse();
  // console.log(elements)

  if (!element.computeStyle) element.computeStyle = {};

  // 遍历所有规则
  for (let rule of rules) {
    // console.log(JSON.stringify(rule, null, " "));
    // 全部选择器
    let selectorParts = rule.selectors[0].split(" ").reverse();

    // ! 匹配当前元素
    if (!match(element, selectorParts[0])) continue;

    console.log(
      "matched element: " + element.tagName + " and " + selectorParts[0]
    );

    // ^ 已经匹配到当前“元素”

    let matched = false; // 每条规则的标识

    // & 双循环检查父元素是否匹配
    let j = 1;
    for (let i = 0; i < elements.length; i++) {
      if (match(elements[i], selectorParts[j])) {
        j++;
      }
    }
    if (j >= selectorParts.length) matched = true;

    if (matched) {
      // ^ 计算当前的选择器优先级
      let sp = specificity(rule.selectors[0]);
      // 如果匹配到,将规则加入element
      // console.log(`Element: ${element}, rule: ${rule}`);
      let computeStyle = element.computeStyle;
      for (let declaration of rule.declarations) {
        if (!computeStyle[declaration.property]) {
          computeStyle[declaration.property] = {};
        }
        // ^ 可能会出现样式覆盖
        computeStyle[declaration.property].value = declaration.value;
        if (!computeStyle[declaration.property].specificity) {
          computeStyle[declaration.property].specificity = sp;
        } else if (
          (compare(computeStyle[declaration.property].specificity), sp)
        ) {
          computeStyle[declaration.property].specificity = sp;
        }
        computeStyle[declaration.property].value = declaration.value;
      }
      element.computeStyle = computeStyle;
      // {color: {value: …}, font-size: {value: …}}
    }
  }
}

// *** 匹配元素 *** //
function match(element, selector) {
  if (!selector || !element.attributes) return false;

  // ^ 简单处理,仅判断id选择器,class选择器和标签选择器
  if (selector.charAt(0) === "#") {
    let attr = element.attributes.filter((attr) => attr.name === "id");
    if (attr && attr.value === selector.replace("#", "")) {
      return true;
    }
  } else if (selector.charAt(0) === ".") {
    let attr = element.attributes.filter((attr) => attr.name === "class");
    if (attr && attr.value === selector.replace(".", "")) {
      return true;
    }
    // ? class还可以使用空格,所以后续可以补充有空格的情况匹配
    // ? 难道不会被split嘛?
  } else if (element.tagName === selector) {
    return true;
  }
}

// *** 计算优先级 *** //
function specificity(selector) {
  // 从高到低
  // 0 表示行内样式,最高优先级
  let p = [0, 0, 0, 0]; // ! 表示当前的优先级
  let selectorParts = selector.split(" ");
  for (let part of selectorParts) {
    if (part.charAt(0) === "#") {
      p[1] += 1;
    } else if (part.charAt(0) === ".") {
      p[2] += 1;
    } else {
      p[3] += 1;
    }
  }
  return p;
}

// *** 比较优先级 *** //
function compare(sp1, sp2) {
  if (sp1[0] - sp2[0]) return sp1[0] - sp2[0];
  if (sp1[1] - sp2[1]) return sp1[1] - sp2[1];
  if (sp1[2] - sp2[2]) return sp1[2] - sp2[2];
  return sp1[3] - sp2[3];
}

module.exports = {
  rules,
  addCSSRules,
  computeCss,
};

文章作者: 阿汪同学
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 阿汪同学 !
评论
  目录