Skip to content

Vue 之 Q&A

源码结构

src
  ├── compiler        # 编译相关 
  ├── core            # 核心代码 
  ├── platforms       # 不同平台的支持
  ├── server          # 服务端渲染
  ├── sfc             # .vue 文件解析
  ├── shared          # 共享代码

vue 流程简易说明

  • 第一步:解析模板成 render 函数
  • 第二步:响应式开始监听
  • 第三步:首次渲染,显示页面,且绑定依赖
  • 第四步:data 属性变化,触发重新 render

1. 把模板解析为 render 函数

  • 运用 with
  • 模板中的所有信息都被 render 函数包含
  • 模板中用到的 data 中的属性,都变成了 JS 变量
  • 模板中的 v-model v-for v-on 都变成了 JS 逻辑(表达式)
  • render 函数返回 vnode

2. 响应式开始监听

  • Object.defineProperty 响应式系统
  • 将 data 的属性代理到 vm 上

3. 首次渲染,显示页面且绑定依赖

  • 初次渲染,执行 updateComponent, 执行 vm._render()
  • 执行 render 函数,会访问到 data 下的数据
  • 会被响应式的 get 方法监听到
  • 执行 updateComponent,会走到 vdom 的 patch 方法
  • patch 将 vnode 渲染成 DOM,初次渲染完成

4. data 属性变化,触发 rerender

  • 修改属性,被响应式的 set 监听到
  • set 中执行 updateComponent
  • updateComponent 重新执行 vm._render()
  • 生成的 vnode 和 prevVnode ,通过 patch 进行对比
  • 渲染到 html 中

模板语法如何被渲染成 html

  • 模版语法(字符串) -> 抽象语法树(JS对象) -> 渲染函数(h函数) -> 虚拟节点(vnode) -> diff & patch -> 界面(html)

模版语法 -> AST

  • template 模板 通过 Compile 编译 得到 render函数。
  • compile 编译可以分成 parse、optimize 与 generate 三个阶段,最终需要得到 render function。
  • Vue.js 2.x 的编译会经过三个过程:template 解析生成 AST ——> AST 优化 ——> AST 生成 code。
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options) // 1. parse

  if (options.optimize !== false) {
    optimize(ast, options) // 2. optimize
  }

  const code = generate(ast, options) //  3. generate
  // code.render: "with(this){return _c('div',{staticClass:"hello"},[_c('h1',[_v(_s(msg))])])}"

  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

AST -> Vnode

// 输入: String 即上一步的 code.render
// 输出: vnode 树
function _createElement(
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
){
  vnode = new VNode(tag, data, children, undefined, undefined, context)

  return vnode
}

如何实现 Vue 数据双向绑定

  • vue中视图上出现很多 {{message}}v-modelv-text 等等模板,我们要对其进行编译。
  • 数据变化的时候,会动态更新到视图上,使用的 Object.defineProperty(),进行数据劫持。
  • 通过 Watcher 观察数据的变化,然后重新编译模板,渲染到视图上

vue-lifecycle

其他 Q&A

render 函数和 vdom

  • updateComponent 中实现了 vdom 的 patch
  • 页面首次渲染执行 updateComponent
  • data 中每次修改属性,执行 updateComponent
// 1.首次渲染 2.每次修改属性
function updateComponent(){
  // vm._render即上面的render函数,返回vnode
  vm._update(vm._render())
}

vm._update(vnode) {
  const prevVnode = vm._vnode;
  vm._vnode = vnode;
  if(!prevVnode){
    vm.$el = vm._patch_(vm.$el, vnode); // 第一次没有值
  } else {
    vm.$el = vm._patch_(prevVnode, vnode); // 有值的情况
  }
}

patch 函数

当 vnode 和 oldVnode 都存在时

  • 3.1 如果 oldVnode 不是真实节点,并且 vnode 和 oldVnode 是同一节点时,说明是需要比较新旧节点,则调用 patchVnode 进行patch。

  • 3.2 如果oldVnode是真实节点时

    • 3.2.1 如果oldVnode是元素节点,且含有data-server-rendered属性时,移除该属性,并设置hydrating为true。
    • 3.2.2 如果hydrating为true时,调用hydrate方法,将Virtural DOM与真实DOM进行映射,然后将oldVnode设置为对应的Virtual DOM。
  • 3.3 如果oldVnode是真实节点时或vnode和oldVnode不是同一节点时,找到oldVnode.elm的父节点,根据vnode创建一个真实的DOM节点,并插入到该父节点中的oldVnode.elm位置。如果组件根节点被替换,遍历更新父节点element。然后移除旧节点。

使用 SFC(单文件组件)预编译模板

  • 当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在 运行时 被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。
  • 预编译模板 最简单的方式就是使用单文件组件 SFC —— 相关的 构建 设置会自动把预编译处理好,所以 构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串

利用 Object.freeze() 提升性能

  • 当你把一个 普通的 JavaScript 对象 传给 Vue 实例的  data  选项,Vue 将遍历此对象所有的属性,并使用  Object.defineProperty  把这些属性全部转为 getter/setter,这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。
  • 但 Vue 在遇到像 Object.freeze() 这样被设置为不可配置之后的对象属性时,不会为对象加上 setter getter 等数据劫持的方法
  • 由于 Object.freeze() 会把对象冻结,所以比较适合展示类的场景,如果你的数据属性需要改变,可以重新替换成一个新的 Object.freeze()的对象。

优化无限列表性能

  • 可以参考 Google 工程师的文章 Complexities of an Infinite Scroller 来尝试自己实现一个虚拟的滚动列表来优化性能,主要使用到的技术是 DOM 回收、墓碑元素和滚动锚定。

组件懒加载优化超长应用内容初始渲染性能

  • 在初始渲染的时候不可见区域的模块也会执行和渲染,带来一些额外的性能开销。
  • 使用组件懒加载在不可见时只需要渲染一个骨架屏,不需要真正渲染组件。