JavaScript 基础 · 279/290
1. Ajax、Axios 和 Fetch 的区别 2. 数组 length 为 0 时访问元素 3. addEventListener 第三个参数 4. Array.prototype.slice.apply(arguments) 5. Animation、Transition、Transform 区别 6. 伪数组与真数组的转换 7. 类数组对象 8. 数组按属性值排序 9. 箭头函数与普通函数的区别 10. AST 抽象语法树 11. attribute 与 property 的区别 12. Babel 编译工具 13. async/await 原理解析 14. 异步编程实现方式 15. forEach 中使用 await 16. 异步任务批量执行结果获取 17. Base64 编码为何增大数据量 18. BigInt 数据类型 19. JavaScript 二分查找实现 20. JavaScript 实现二叉排序树 21. bind 连续调用的 this 绑定 22. 浏览器缓存机制 23. bind、call、apply 的区别与实现 24. 浏览器进程和线程 25. callee 与 caller 属性 26. 浏览器版本检测 27. Canvas 黑块计数 28. Canvas 跨域图片数据获取 29. Canvas 与 WebGL 区别 30. 浏览器从输入网址到页面显示的过程 31. 判断对象是否为空 32. 摄氏度转华氏度 33. JavaScript 变量与函数提升 34. 移动端点击穿透问题 35. JavaScript 变量提升与函数提升 36. setTimeout 在循环中的执行时机 37. 转盘组件设计与防刷方案 38. Promise.resolve 返回 Promise 的执行顺序 39. 静态资源 CDN 部署 40. typeof 运算符的执行顺序 41. typeof 和暂时性死区的执行分析 42. 箭头函数与普通函数的 this 和 new 行为 43. this 指向判断 44. Cookie 过期时间设置为 0 的行为 45. typeof 和暂时性死区的执行分析 46. 浏览器跨域限制的原因 47. Company Culture Fit 48. Cookie、LocalStorage 和 SessionStorage 的区别 49. 前端跨域请求解决方案 50. 统计 HTML 页面标签种类数 51. 浏览器标签页间通信 52. 什么是跨域 53. 前端跨页面通信方法 54. CSS 和 JS 文件是否阻塞页面渲染 55. CSR 与 SSR 渲染模式 56. CSS 动画与 JS 动画对比 57. JavaScript vs CSS 动画 58. 十进制转二进制 59. 判断 PC 端还是移动端 60. 深拷贝与浅拷贝 61. document.write 与 innerHTML 的区别 62. document.ready vs window.onload 63. JavaScript 实现拖拽功能 64. DOM 和 BOM 的区别 65. Element Dimension Properties 66. 空数组调用 reduce 67. enumerable 属性忽略规则 68. devDependencies 与 dependencies 的区别 69. 判断脚本运行环境(浏览器或 Node) 70. 判断空对象的方法 71. ES6 Decorator 装饰器 72. 面试必问问题 73. eval 函数的作用与弊端 74. 事件捕获与冒泡执行顺序 75. 事件代理 76. 解释 JavaScript 事件循环 77. 执行上下文与作用域链 78. React setState 批量更新机制 79. 事件冒泡和捕获机制 80. ES6+ 新特性概览 81. 函数是一等公民 82. 浮点数精度问题:0.1 + 0.2 83. 表达式 123['toString'].length + 123 的输出 84. flexible.js 移动端适配原理 85. forEach 循环中断 86. 前端路由的实现与应用 87. 前端动画实现方式 88. 前端面试流程 89. 前端性能优化指标与检测方法 90. 前端路由历史栈结构 91. JavaScript 函数声明方式及区别 92. 函数调用方式 93. 获取页面滚动距离 94. 函数原型链输出结果 95. 生成 1-10000 的数组 96. 函数式编程概念 97. 全局函数与全局变量 98. 堆与栈的区别 99. HTTP/2 多路复用原理 100. HTTPS 握手过程 101. 立即执行函数表达式(IIFE) 102. isNaN 与 Number.isNaN 的区别 103. 图片搜索系统设计 104. 面试关键点 105. 实现可迭代对象 106. JavaScript 内置对象 107. Iterator 迭代器与 Generator 生成器 108. JavaScript 的组成部分 109. JavaScript 内置对象 110. 闭包:概念、应用与内存管理 111. JavaScript 垃圾回收机制 112. JavaScript 错误类型 113. JavaScript 数据类型与检测方法 114. JavaScript 动态生成海报 115. JavaScript 对象定义方法 116. Promise 原理与用法 117. JavaScript 请求取消 118. JavaScript 单线程模型 119. JavaScript 继承方式对比 120. JavaScript 对象生命周期 121. this 的指向规则 122. jQuery.fn.init 返回的 this 123. jQuery 对象特点 124. Job Fit Assessment 125. JS 文件对 DOM 和 CSSOM 的阻塞 126. JSBridge 原理 127. JSON 基础 128. 0-1 背包问题 129. 大文件上传实现 130. Job Handover and Benefits 131. setTimeout 循环输出问题 132. 低代码平台技术实现 133. 宏任务与微任务的执行优先级 134. map(parseInt) 返回值问题 135. map(parseInt) 输出分析 136. 实现 LRU 缓存 137. map 与 filter 的区别 138. Map、Set、WeakMap、WeakSet 的区别 139. Math.ceil 和 Math.floor 的区别 140. 使用 Math 方法获取数组最值 141. Math.random() 在中奖概率计算中的安全问题 142. Math 取整方法的区别 143. 内存泄漏的场景与检测 144. 最大子序和 145. 合并连续数字 146. 什么是微前端 147. MessageChannel 的用法和应用场景 148. MessageChannel 的用途和场景 149. 微前端应用隔离 150. 实现 mergePromise 函数 151. 微前端解决的问题 152. 移动端样式适配 153. 模块化方案对比:CommonJS、AMD、ES Module 154. mouseEnter 和 mouseOver 的区别 155. 为按钮绑定多个 onclick 事件 156. 微前端技术方案 157. 移动端点击延迟问题 158. 多 tab 页通信方案 159. 实现 Promise.all 方法 160. 实现数组的 map 方法 161. 原生 JavaScript 知识体系 162. 计算机网络模型 163. new fn 与 new fn() 的区别 164. Node ES Module 为什么需要文件扩展名 165. new 操作符的执行过程 166. 不冒泡的事件类型 167. 非递归遍历二叉树 168. null 与 undefined 的区别 169. npm 包管理器 170. 空值合并运算符(??)使用场景 171. 数字转汉语输出函数 172. Object.create 与 new 的区别 173. 数字转中文 174. 数字分隔符的相等性判断 175. JavaScript 创建对象的方法 176. 对象解构赋值的实现方式 177. 面向对象编程 178. 让对象支持数组解构赋值 179. 面向对象编程思想 180. Object 与 Map 的区别 181. 面向对象与面向过程编程 182. OOP Three Principles 183. 前端页面截图实现 184. 页面生命周期事件 185. 轮询机制实现 186. 个人经历 187. Portal 子组件的事件冒泡 188. Personal Qualities 189. pnpm 包管理工具 190. 什么是 Polyfill 191. postMessage 跨域通信 192. 防止重复提交 193. 实现数组笛卡尔积 194. 前端面试中的问题分析方法 195. 防止按钮重复点击 196. 阻止事件冒泡和默认行为 197. 项目难点分析 198. 项目流程 199. 项目经验 200. Promise.all 异常处理 201. 中断 Promise 的方法 202. Promise 并发控制 203. Promise.then 的错误处理 204. Promise then 与 catch 的区别 205. Promise.all 和 Promise.allSettled 的区别 206. Promise 构造函数的执行时机 207. Promise 异步加载图片 208. 使用 Promise 实现定时输出 209. 用 Promise 实现红绿灯交替 210. Property Descriptors 211. 属性遍历方法 212. Proxy Basic Usage 213. Proxy 与 Object.defineProperty 214. 原型与原型链 215. PWA 渐进式网络应用 216. React Fiber 架构与 Vue 的差异 217. React 废弃三个生命周期钩子的原因 218. React 事件与原生事件执行顺序 219. 扫码登录实现方案 220. React Portals 的用途 221. React 与 React-DOM 的关系 222. reduce 方法用途 223. React render 方法原理与触发时机 224. ES6 Reflect 对象的用途 225. Reflect 对象的使用 226. React 为什么不直接使用 requestIdleCallback 227. 正则表达式常用方法 228. RESTful 接口规范 229. requestIdleCallback 与 requestAnimationFrame 230. 简历投递的最佳时间段 231. Script 标签位置的影响 232. 滚动公告组件的鼠标交互 233. 顺序执行异步任务 234. JavaScript 脚本延迟加载方式 235. 浏览器同源策略 236. Script 标签中的 export 报错 237. Service Worker 是什么 238. 用 setTimeout 实现 setInterval 239. setTimeout 零延迟的应用场景 240. setTimeout 运行机制 241. 实现 sleep 函数 242. 单线程与异步的关系 243. SPA 首屏加载优化 244. 严格模式的限制 245. 字符串压缩 246. 字符串长度计算(支持表情符号) 247. 实现字符串 repeat 方法 248. 字符串 toString() 方法调用 249. 同步与异步的区别 250. 尾调用优化与尾递归 251. toPrecision、toFixed 和 Math.round 的区别 252. target 与 currentTarget 的区别 253. JavaScript 倒计时偏差纠正 254. TCP 三次握手和四次挥手 255. 实现二叉树所有路径 256. try...catch 捕获异步错误 257. Tree Shaking 原理 258. typeof NaN 的结果 259. JavaScript 类型转换机制 260. TypeScript 访问修饰符 261. TypeScript 泛型的使用 262. TypeScript:Interface 与 Type 的区别 263. TypeScript 方法重载 264. TypeScript 中的 is 关键字 265. TypeScript 变量声明方式 266. TypeScript 命名空间与模块 267. undefined 与 ReferenceError 的区别 268. 获取 URL 资源文件后缀 269. TypeScript 与 JavaScript 的区别 270. URL 参数编码的必要性 271. JavaScript 版本号排序实现 272. var、let、const 的区别 273. JavaScript 变量提升机制 274. 虚拟 DOM 渲染处理 275. 虚拟列表实现 276. Vue created 与 mounted 的时间差 277. Vue 生命周期中发起请求的最佳位置 278. Vite 工作原理 279. Vue 页面渲染流程 280. Vue2 数组变化检测问题 281. Webpack 5 模块联邦 282. Web Worker 基础 283. WebSocket 心跳机制 284. WebSocket 协议 285. Vue 3 响应式原理 286. WebSocket 低版本浏览器兼容方案 287. Webpack 5 升级要点 288. WebSocket 与 HTTP 的区别 289. 页面白屏原因与优化 290. XML 与 JSON 的区别

Vue 页面渲染流程

Vue 从挂载组件到生成真实 DOM 的完整渲染过程

问题

Vue 是如何将模板渲染成页面的?从 new Vue() 开始,到页面显示内容,中间经历了什么?

解答

挂载组件($mount)

Vue 通过 new 关键字实例化时,会调用 _init 方法进行初始化:

// src/core/instance/index.js
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

_init 内部会调用 $mount 挂载组件,$mount 实际调用的是 mountComponent

// src/core/instance/lifecycle.js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  callHook(vm, 'beforeMount')

  let updateComponent
  // 核心渲染方法
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  
  // 创建渲染 Watcher,数据变化时重新渲染
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  
  callHook(vm, 'mounted')
  return vm
}

updateComponent 是渲染视图的核心方法,包含两个关键步骤:

  • vm._render() 创建并返回 VNode
  • vm._update() 将 VNode 转为真实 DOM

构建 VNode(_render)

_render 方法用于构建组件的 VNode:

// src/core/instance/render.js
Vue.prototype._render = function () {
  const vm = this
  const { render, _parentVnode } = vm.$options
  
  // 执行 render 函数生成 VNode
  vnode = render.call(vm._renderProxy, vm.$createElement)
  
  return vnode
}

在初始化时,Vue 会为实例绑定两个创建 VNode 的方法:

// src/core/instance/render.js
export function initRender (vm) {
  // 模板编译生成的 render 函数使用
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // 用户手写 render 函数使用
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}

createElement

createElement 是创建 VNode 的核心方法:

// src/core/vdom/create-elemenet.js
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  // 参数重载处理
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

_createElement 是实际创建 VNode 的方法:

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // 规范化 children 为 VNode 数组
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // 平台内置元素,创建普通 VNode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 已注册组件,创建组件类型 VNode
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // 未知标签
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // tag 是组件类型,直接创建组件 VNode
    vnode = createComponent(tag, data, context, children)
  }
  
  return vnode
}

children 规范化有两种方式:

// 编译生成的 render 函数使用,只需简单扁平化
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}

// 用户手写 render 函数使用,需要完整规范化
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

生成真实 DOM(_update)

_update 方法将 VNode 转换为真实 DOM:

// src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  
  vm._vnode = vnode
  
  if (!prevVnode) {
    // 初始渲染
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
  } else {
    // 更新渲染
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  
  // 更新引用
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
}

__patch__ 方法在 web 平台的定义:

// src/platforms/web/runtime/index.js
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop

patchcreatePatchFunction 创建:

// src/core/vdom/patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

export function createPatchFunction (backend) {
  let i, j
  const cbs = {}
  const { modules, nodeOps } = backend

  // 收集各模块的钩子函数
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  
  // 返回 patch 函数
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // ...
  }
}

这里有两个重要对象:

  • nodeOps:封装的原生 DOM 操作方法
  • modules:各模块的钩子函数(指令、ref 等)

patch 过程

初始渲染时,oldVnode 是根节点的真实 DOM:

return function patch (oldVnode, vnode, hydrating, removeOnly) {
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }

  let isInitialPatch = false
  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      if (isRealElement) {
        // 将真实 DOM 转为 VNode
        oldVnode = emptyNodeAt(oldVnode)
      }
      
      // 获取父节点
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)
      
      // 创建新节点
      createElm(
        vnode,
        insertedVnodeQueue,
        parentElm,
        nodeOps.nextSibling(oldElm)
      )
      
      // 移除旧节点
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0)
      }
    }
  }
  
  // 触发 insert 钩子
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

createElm

createElm 是将 VNode 转为真实 DOM 的核心方法:

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // 尝试创建组件节点
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  
  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  
  if (isDef(tag)) {
    // 创建元素节点
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    
    // 创建子节点
    createChildren(vnode, children, insertedVnodeQueue)
    
    // 调用 create 钩子
    if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue)
    }
    
    // 插入父节点
    insert(parentElm, vnode.elm, refElm)
  } else if (isTrue(vnode.isComment)) {
    // 注释节点
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    // 文本节点
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

创建子节点的过程:

function createChildren (vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    for (let i = 0; i < children.length; ++i) {
      // 递归创建子节点
      createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
    }
  } else if (isPrimitive(vnode.text)) {
    // 文本内容
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
  }
}

插入节点:

function insert (parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (nodeOps.parentNode(ref) === parent) {
        nodeOps.insertBefore(parent, elm, ref)
      }
    } else {
      nodeOps.appendChild(parent, elm)
    }
  }
}

Vue 通过递归调用 createElm 创建节点树,最深的子节点会先调用 insert 插入,所以整个节点树的插入顺序是”先子后父”。

最后触发 insert 钩子:

function invokeInsertHook (vnode, queue, initial) {
  if (isTrue(initial) && isDef(vnode.parent)) {
    vnode.parent.data.pendingInsert = queue
  } else {
    for (let i = 0; i < queue.length; ++i) {
      queue[i].data.hook.insert(queue[i])
    }
  }
}

关键点

  • 渲染流程分为三个阶段:挂载组件($mount)、构建 VNode(_render)、生成真实 DOM(_update
  • createElement 根据 tag 类型创建不同的 VNode,包括普通元素、组件、未知标签三种
  • children 规范化有两种方式:编译生成的 render 使用 simpleNormalizeChildren,手写 render 使用 normalizeChildren
  • createElm 通过递归创建节点树,插入顺序是”先子后父”,最深的子节点最先插入
  • 初始渲染时,Vue 不是替换根节点,而是在其后插入新节点,再移除旧节点