沐游虞笔记
  • 前端面试题

    • HTML面试题汇总(无答案)
    • HTML面试题汇总
    • CSS 面试题汇总(无答案)
    • CSS 面试题汇总
    • javascript 面试题汇总(无答案)
    • javascript 面试题汇总
    • promise 面试题(无答案)
    • promise 面试题
    • 浏览器面试题汇总(无答案)
    • 浏览器面试题汇总
    • 网络面试题汇总(无答案)
    • 网络面试题汇总
    • 工程化面试题汇总(无答案)
    • 工程化面试题汇总
    • VUE面试题汇总(无答案)
    • VUE面试题汇总
  • 直播课文件

    • 静态页面学习指导
    • 属性的计算过程
    • 层叠继承规则总结
    • BFC
    • JS基础知识回顾
    • DOM 事件的传播机制
    • DOM 事件的注册和移除
    • 阻止事件默认行为
    • 基础领航考试题
    • 基础领航考试题(答案)
    • 2024前端发展
    • JS核心概念学习指导
    • 第三方库与工程化学习指导
    • Vue入门学习指导
    • vue进阶学习指导
    • 前端性能优化
  • 笔面试环节知识讲解

    • 目录
    • 图像处理
    • 图像处理(面试)
    • Webpack构建优化
    • Webpack构建优化(面试)
    • TTS性能优化
    • TTS性能优化(面试)
    • 实时协作
    • 实时协作(面试)
    • 网页复制图片到剪贴板
    • 网页复制图片到剪贴板(面试)
    • vite插件
    • vite插件(面试)
    • 表单数据同步与保持
    • 表单数据同步与保持(面试)
    • 优化虚拟列表
    • 优化虚拟列表(面试)
    • 微前端解决巨石应用
    • 微前端解决巨石应用(面试)
    • DNS解析与优化
    • DNS解析与优化(面试)
    • 前端监控
    • 前端监控(面试)
    • 12.跨标签页通信
    • 12.跨标签页通信(面试)
    • 13.Vite相关优化
    • 13.Vite相关优化(面试)
    • 14.计时器节流问题
    • 14.计时器节流问题(面试)
    • 15.多文件预览支持
    • 15.多文件预览支持(面试)
    • 16.defer优化白屏时间
    • 16.defer优化白屏时间(面试)
  • Vue3整体变化
  • Vue2响应式回顾
  • Vue3响应式变化
  • nextTick实现原理
  • 两道代码题
  • Vue运行机制
  • 渲染器核心功能
  • 事件绑定与更新
  • computed面试题
  • watch面试题
  • 图解双端diff
  • 图解快速dff
  • 最长递增子序列
  • 模板编译器
  • 模板编译提升
  • 组件name作用
  • 路由传参方式
  • 基础篇

    • 序章React介绍
    • JSX基础语法
    • React基本介绍
    • 表单
    • 生命周期
    • 组件与事件绑定
    • 组件状态与数据传递
    • Hooks
    • React--redux介绍
    • React-router介绍
  • 就业篇

    • 属性默认值和类型验证
    • 高阶组件
    • Ref
    • Context
    • Render Props
    • Portals
    • 错误边界
    • 组件渲染性能优化
    • 前端框架的理解
    • Reacti和Vue描述页面的区别
    • 前端框架的分类
    • 虚拟DoM
    • React整体架构
    • React渲染流程
    • Fiber双缓冲
    • MessageChannel
    • Scheduleri调度普通任务
    • Scheduleri调度延时任务
    • 最小堆
    • React中的位运算
    • beginWork工作流程
    • completeWork工作流程
    • 图解diff算法
    • commit工作流程
    • lane模型
    • React中的事件
    • Hooks原理
    • useStateuseReducer.
    • effect相关hook
    • useCallbackuseMemo
    • useRef
    • Update
    • 性能优化策略之eagerState
    • 性能优化策略之bailout
    • bailoutContextAPl
    • 性能优化对日常开发启示
  • 前端监控概述
  • 错误监控
  • 数据上报
  • 页面性能监控
  • 用户行为收集与埋点
  • CSS3手册
  • HTML5手册
  • JavaScript语言提升

    • es补充
    • 事件循环
    • promise基础
    • Promise的链式调用
    • Promise的静态方法
    • async和await
    • Promise相关面试题
  • 网络

    • 客户端与服务器
    • 关于 Apifox 的使用
  • git文档
  • 工程化

    • CommonJS
    • ES module
    • npm文档(包管理)
    • Lass笔记
    • webpack工具
  • canvas详解
  • uinapp笔记
  • 自动化测试
  • oauth2令牌

    • 认识Oauth2
    • 三方应用实现github授权
    • 微信三方应用登录实现
    • 支付宝沙箱支付功能
  • 前端面试题

    • HTML面试题汇总(无答案)
    • HTML面试题汇总
    • CSS 面试题汇总(无答案)
    • CSS 面试题汇总
    • javascript 面试题汇总(无答案)
    • javascript 面试题汇总
    • promise 面试题(无答案)
    • promise 面试题
    • 浏览器面试题汇总(无答案)
    • 浏览器面试题汇总
    • 网络面试题汇总(无答案)
    • 网络面试题汇总
    • 工程化面试题汇总(无答案)
    • 工程化面试题汇总
    • VUE面试题汇总(无答案)
    • VUE面试题汇总
  • 直播课文件

    • 静态页面学习指导
    • 属性的计算过程
    • 层叠继承规则总结
    • BFC
    • JS基础知识回顾
    • DOM 事件的传播机制
    • DOM 事件的注册和移除
    • 阻止事件默认行为
    • 基础领航考试题
    • 基础领航考试题(答案)
    • 2024前端发展
    • JS核心概念学习指导
    • 第三方库与工程化学习指导
    • Vue入门学习指导
    • vue进阶学习指导
    • 前端性能优化
  • 笔面试环节知识讲解

    • 目录
    • 图像处理
    • 图像处理(面试)
    • Webpack构建优化
    • Webpack构建优化(面试)
    • TTS性能优化
    • TTS性能优化(面试)
    • 实时协作
    • 实时协作(面试)
    • 网页复制图片到剪贴板
    • 网页复制图片到剪贴板(面试)
    • vite插件
    • vite插件(面试)
    • 表单数据同步与保持
    • 表单数据同步与保持(面试)
    • 优化虚拟列表
    • 优化虚拟列表(面试)
    • 微前端解决巨石应用
    • 微前端解决巨石应用(面试)
    • DNS解析与优化
    • DNS解析与优化(面试)
    • 前端监控
    • 前端监控(面试)
    • 12.跨标签页通信
    • 12.跨标签页通信(面试)
    • 13.Vite相关优化
    • 13.Vite相关优化(面试)
    • 14.计时器节流问题
    • 14.计时器节流问题(面试)
    • 15.多文件预览支持
    • 15.多文件预览支持(面试)
    • 16.defer优化白屏时间
    • 16.defer优化白屏时间(面试)
  • Vue3整体变化
  • Vue2响应式回顾
  • Vue3响应式变化
  • nextTick实现原理
  • 两道代码题
  • Vue运行机制
  • 渲染器核心功能
  • 事件绑定与更新
  • computed面试题
  • watch面试题
  • 图解双端diff
  • 图解快速dff
  • 最长递增子序列
  • 模板编译器
  • 模板编译提升
  • 组件name作用
  • 路由传参方式
  • 基础篇

    • 序章React介绍
    • JSX基础语法
    • React基本介绍
    • 表单
    • 生命周期
    • 组件与事件绑定
    • 组件状态与数据传递
    • Hooks
    • React--redux介绍
    • React-router介绍
  • 就业篇

    • 属性默认值和类型验证
    • 高阶组件
    • Ref
    • Context
    • Render Props
    • Portals
    • 错误边界
    • 组件渲染性能优化
    • 前端框架的理解
    • Reacti和Vue描述页面的区别
    • 前端框架的分类
    • 虚拟DoM
    • React整体架构
    • React渲染流程
    • Fiber双缓冲
    • MessageChannel
    • Scheduleri调度普通任务
    • Scheduleri调度延时任务
    • 最小堆
    • React中的位运算
    • beginWork工作流程
    • completeWork工作流程
    • 图解diff算法
    • commit工作流程
    • lane模型
    • React中的事件
    • Hooks原理
    • useStateuseReducer.
    • effect相关hook
    • useCallbackuseMemo
    • useRef
    • Update
    • 性能优化策略之eagerState
    • 性能优化策略之bailout
    • bailoutContextAPl
    • 性能优化对日常开发启示
  • 前端监控概述
  • 错误监控
  • 数据上报
  • 页面性能监控
  • 用户行为收集与埋点
  • CSS3手册
  • HTML5手册
  • JavaScript语言提升

    • es补充
    • 事件循环
    • promise基础
    • Promise的链式调用
    • Promise的静态方法
    • async和await
    • Promise相关面试题
  • 网络

    • 客户端与服务器
    • 关于 Apifox 的使用
  • git文档
  • 工程化

    • CommonJS
    • ES module
    • npm文档(包管理)
    • Lass笔记
    • webpack工具
  • canvas详解
  • uinapp笔记
  • 自动化测试
  • oauth2令牌

    • 认识Oauth2
    • 三方应用实现github授权
    • 微信三方应用登录实现
    • 支付宝沙箱支付功能
  • 课程导读-必看
  • Vue3整体变化
  • Vue2响应式回顾
  • Vue3响应式变化
  • nextTick实现原理
  • 两道代码题
  • Vue运行机制
  • 渲染器核心功能
    • 事件绑定与更新
    • computed面试题
    • watch面试题
    • 图解双端diff
    • 图解快速diff
    • 最长递增子序列
    • 模板编译器
    • 模板编译提升
    • 组件name作用
    • Vue项目性能优化
    • 路由传参方式
    • vue3笔面试题汇总
    luzhichang
    2024-09-27
    目录

    渲染器核心功能

    # 渲染器核心功能

    面试题:说一说渲染器的核心功能是什么?

    渲染器的核心功能,是根据拿到的 vnode,进行节点的挂载与更新。

    挂载属性

    vnode:

    const vnode = {
      type: 'div',
      // props 对应的就是节点的属性
      props: {
        id: 'foo'
      },
      children: [
        type: 'p',
        children: 'hello'
      ]
    }
    

    渲染器内部有一个 mountElement 方法:

    function mountElement(vnode, container){
      // 根据节点类型创建对应的DOM节点
      const el = document.createElement(vnode.type);
      
      // 省略children的处理
      
      // 对属性的处理
      if(vnode.props){
        for(const key in vnode.props){
          el.setAttribute(key, vnode.props[key])
        }
      }
      
      insert(el, container);
    }
    

    除了使用setAttribute方法来设置属性以外,也可以使用DOM对象的方式:

    if(vnode.props){
      for(const key in vnode.props){
        // el.setAttribute(key, vnode.props[key])
        el[key] = vnode.props[key];
      }
    }
    

    思考🤔:哪种设置方法好?两种设置方法有区别吗?应该使用哪种来设置?

    HTML Attributes

    Attributes 是元素的初始属性值,在 HTML 标签中定义,用于描述元素的初始状态。

    • 在元素被解析的时候,只会初始化一次
    • 只能是字符串值,而且这个值仅代表初始的状态,无法反应运行时的变化
    <input type="text" id="username" value="John">
    

    DOM Properties

    Properties 是 JavaScript 对象上的属性,代表了 DOM 元素在 内存中 的实际状态。

    • 反应的是 DOM 元素的当前状态
    • 属性类型可以是字符串、数字、布尔值、对象之类的

    很多 HTML attributes 在 DOM 对象上有与之相同的 DOM Properties,例如:

    HTML attributes DOM properties
    id="username" el.id
    type="text" el.type
    value="John" el.value

    但是,两者并不总是相等的,例如:

    HTML attributes DOM properties
    class="foo" el.className

    还有很多其他的情况:

    • HTML attributes 有但是 DOM properties 没有的属性:例如 aria-* 之类的HTML Attributes
    • DOM properties 有但是 HTML attributes 没有的属性:例如 el.textContent
    • 一个 HTML attributes 关联多个 DOM properties 的情况:例如 value="xxx" 和 el.value 以及 el.defaultValue 都有关联

    另外,在设置的时候,不是单纯的用某一种方式,而是两种方式结合使用。因为需要考虑很多特殊情况:

    1. disabled
    2. 只读属性

    1. disabled

    模板:我们想要渲染的按钮是非禁用状态

    <button :disabled="false">Button</button>
    

    vnode:

    const vnode = {
      type: 'button',
      props: {
        disable: false
      }
    }
    

    通过 el.setAttribute 方法来进行设置会遇到的问题:最终渲染出来的按钮就是禁用状态

     el.setAttribute('disabled', 'false')
    

    解决方案:优先设置 DOM Properties

    遇到新的问题:本意是要禁用按钮

    <button disabled>Button</button>
    
    const vnode = {
      type: 'button',
      props: {
        disable: ''
      }
    }
    
    el.disabled = ''
    

    在对 DOM 的 disabled 属性设置值的时候,任何非布尔类型的值都会被转为布尔类型:

    el.disabled = false
    

    最终渲染出来的按钮是非禁用状态。

    渲染器内部的实现,不是单独用 HTML Attribute 或者 DOM Properties,而是两者结合起来使用,并且还会考虑很多的细节以及特殊情况,针对特殊情况做特殊处理。

    function mountElement(vnode, container) {
      const el = createElement(vnode.type);
      // 省略 children 的处理
    
      if (vnode.props) {
        for (const key in vnode.props) {
          // 用 in 操作符判断 key 是否存在对应的 DOM Properties
          if (key in el) {
            // 获取该 DOM Properties 的类型
            const type = typeof el[key];
            const value = vnode.props[key];
            // 如果是布尔类型,并且 value 是空字符串,则将值矫正为 true
            if (type === "boolean" && value === "") {
              el[key] = true;
            } else {
              el[key] = value;
            }
          } else {
            // 如果要设置的属性没有对应的 DOM Properties,则使用 setAttribute 函数设置属性
            el.setAttribute(key, vnode.props[key]);
          }
        }
      }
      insert(el, container);
    }
    

    2. 只读属性

    <input form="form1"/>
    

    例如 el.form,但是这个属性是只读的,所以这种情况,又只能使用 setAttribute 方法来设置

    function shouldSetAsProps(el, key, value) {
      // 特殊处理
      // 遇到其他特殊情况再进行重构
      if (key === "form" && el.tagName === "INPUT") return false;
      // 兜底
      return key in el;
    }
    
    function mountElement(vnode, container) {
      const el = createElement(vnode.type);
      // 省略 children 的处理
    
      if (vnode.props) {
        for (const key in vnode.props) {
          const value = vnode.props[key];
    
          if (shouldSetAsProps(el, key, value)) {
            const type = typeof el[key];
            if (type === "boolean" && value === "") {
              el[key] = true;
            } else {
              el[key] = value;
            }
          } else {
            el.setAttribute(key, value);
          }
        }
      }
      insert(el, container);
    }
    

    shouldSetAsProps 这个方法返回一个布尔值,由布尔值来决定是否使用 DOM Properties 来设置。

    还可以进一步优化,将属性的设置提取出来:

    function shouldSetAsProps(el, key, value) {
      // 特殊处理
      if (key === "form" && el.tagName === "INPUT") return false;
      // 兜底
      return key in el;
    }
    
    /**
     *
     * @param {*} el 元素
     * @param {*} key 属性
     * @param {*} prevValue 旧值
     * @param {*} nextValue 新值
     */
    function patchProps(el, key, prevValue, nextValue) {
      if (shouldSetAsProps(el, key, nextValue)) {
        const type = typeof el[key];
        if (type === "boolean" && nextValue === "") {
          el[key] = true;
        } else {
          el[key] = nextValue;
        }
      } else {
        el.setAttribute(key, nextValue);
      }
    }
    
    function mountElement(vnode, container) {
      const el = createElement(vnode.type);
      // 省略 children 的处理
    
      if (vnode.props) {
        for (const key in vnode.props) {
          // 调用 patchProps 函数即可
          patchProps(el, key, null, vnode.props[key]);
        }
      }
      insert(el, container);
    }
    

    class处理

    class 本质上也是属性的一种,但是在 Vue 中针对 class 做了增强,因此 Vue 模板中的 class 的值可能会有这么一些情况:

    情况一:字符串值

    <template>
    	<p class="foo bar"></p>
    </template>
    
    const vnode = {
      type: "p",
      props: {
        class: "foo bar",
      },
    };
    

    情况二:对象值

    <template>
    	<p :class="cls"></p>
    </template>
    <script setup>
    import { ref } from 'vue'
    const cls = ref({
      foo: true,
      bar: false
    })
    </script>
    
    const vnode = {
      type: "p",
      props: {
        class: { foo: true, bar: false },
      },
    };
    

    情况三:数组值

    <template>
    	<p :class="arr"></p>
    </template>
    <script setup>
    import { ref } from 'vue'
    const arr = ref([
      'foo bar',
      {
        baz: true
      }
    ])
    </script>
    
    const vnode = {
      type: "p",
      props: {
        class: ["foo bar", { baz: true }],
      },
    };
    

    这里首先第一步就是需要做参数归一化,统一成字符串类型。Vue内部有一个方法 normalizeClass 就是做 class 的参数归一化的。

    function isString(value) {
      return typeof value === "string";
    }
    
    function isArray(value) {
      return Array.isArray(value);
    }
    
    function isObject(value) {
      return value !== null && typeof value === "object";
    }
    
    function normalizeClass(value) {
      let res = "";
      if (isString(value)) {
        res = value;
      } else if (isArray(value)) {
        // 如果是数组,递归调用 normalizeClass
        for (let i = 0; i < value.length; i++) {
          const normalized = normalizeClass(value[i]);
          if (normalized) {
            res += (res ? " " : "") + normalized;
          }
        }
      } else if (isObject(value)) {
        // 如果是对象,则检查每个 key 是否为真值
        for (const name in value) {
          if (value[name]) {
            res += (res ? " " : "") + name;
          }
        }
      }
      return res;
    }
    
    console.log(normalizeClass("foo")); // 'foo'
    console.log(normalizeClass(["foo", "bar"])); // 'foo bar'
    console.log(normalizeClass({ foo: true, bar: false })); // 'foo'
    console.log(normalizeClass(["foo", { bar: true }])); // 'foo bar'
    console.log(normalizeClass(["foo", ["bar", "baz"]])); // 'foo bar baz'
    
    const vnode = {
      type: "p",
      props: {
        class: normalizeClass(["foo bar", { baz: true }]),
      },
    };
    
    const vnode = {
      type: "p",
      props: {
        class: 'foo bar baz',
      },
    };
    

    设置class的时候,设置方法也有多种:

    1. setAttribute
    2. el.className:这种方式效率是最高的
    3. el.classList
    function patchProps(el, key, prevValue, nextValue) {
      // 对 class 进行特殊处理
      if (key === "class") {
        el.className = nextValue || "";
      } else if (shouldSetAsProps(el, key, nextValue)) {
        const type = typeof el[key];
        if (type === "boolean" && nextValue === "") {
          el[key] = true;
        } else {
          el[key] = nextValue;
        }
      } else {
        el.setAttribute(key, nextValue);
      }
    }
    

    子节点的挂载

    除了对自身节点的处理,还需要对子节点进行处理,不过处理子节点时涉及到 diff 计算。

    function mountElement(vnode, container) {
      const el = createElement(vnode.type);
      
      // 针对子节点进行处理
      if (typeof vnode.children === "string") {
        // 如果 children 是字符串,则直接将字符串插入到元素中
        setElementText(el, vnode.children);
      } else if (Array.isArray(vnode.children)) {
        // 如果 children 是数组,则遍历每一个子节点,并调用 patch 函数挂载它们
        vnode.children.forEach((child) => {
          patch(null, child, el);
        });
      }
      insert(el, container);
    }
    

    面试题:说一说渲染器的核心功能是什么?

    参考答案:

    渲染器最最核心的功能是处理从虚拟 DOM 到真实 DOM 的渲染过程,这个过程包含几个阶段:

    1. 挂载:初次渲染时,渲染器会将虚拟 DOM 转化为真实 DOM 并插入页面。它会根据虚拟节点树递归创建 DOM 元素并设置相关属性。
    2. 更新:当组件的状态或属性变化时,渲染器会计算新旧虚拟 DOM 的差异,并通过 Patch 过程最小化更新真实 DOM。
    3. 卸载:当组件被销毁时,渲染器需要将其从 DOM 中移除,并进行必要的清理工作。

    每一个步骤都有大量需要考虑的细节,就拿挂载来讲,光是处理元素属性如何挂载就有很多需要考虑的问题,比如:

    1. 最终设置属性的时候是用 setAttribute 方法来设置,还是用给 DOM 对象属性赋值的方式来设置
    2. 遇到像 disabled 这样的特殊属性该如何处理
    3. class、style 这样的多值类型,该如何做参数的归一化,归一为哪种形式
    4. 像 class 这样的属性,设置的方式有哪种,哪一种效率高

    另外,渲染器和响应式系统是紧密结合在一次的,当组件首次渲染的时候,组件里面的响应式数据会和渲染函数建立依赖关系,当响应式数据发生变化后,渲染函数会重新执行,生成新的虚拟 DOM 树,渲染器随即进入更新阶段,根据新旧两颗虚拟 DOM 树对比来最小化更新真实 DOM,这涉及到了 Vue 中的 diff 算法。diff 算法这一块儿,Vue2 采用的是双端 diff,Vue3 则是做了进一步的优化,采用的是快速 diff 算法。diff 这一块儿需要我展开说一下么?


    -EOF-

    Vue运行机制
    事件绑定与更新

    ← Vue运行机制 事件绑定与更新→

    Theme by Vdoing | Copyright © 2021-2024 蜀ICP备2024068710号-1
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式