vue原理解析(三):实现一个VDOM, 必须了解的vue原理面试题(建议收藏)

817 阅读7分钟

实现一个VDOM

VDOM:简单来说就是一个js对象,它可以用来描述当前DOM长什么样子。 优点:

  • 1.大多数情况下,提供了比暴力刷新整个dom树更好的性能。因为js的执行速度是非常快的,但是操作dom十分消耗性能
  • 2.使用vdom之后,只有最后异步需要依赖dom api,意味着只要能使用js的地方就可以使用vdom去描述当前视图,实现了跨平台。 前面已经提到,AST经过渲染函数render处理, render函数的函数体一个形如:
ƒ anonymous() {
    with(this){return _c("div", {}, [_v("1233"),_c("p", {}, [_v("测试解析器")])]) }
}
// VNode结构如下
class VNode {
  constructor(tag, attrs, children, text) {
    this.tag = tag
    this.attrs = attrs
    this.children = children
    this.text = text
  }
}
// 这个render函数执行需要三个工具函数
class MVue{
    ...
  // 创建元素节点
  _c(tag, attrs, children) {
    return new VNode(tag, attrs, children)
  }

  // 创建纯文本节点
  _v(text) {
    return new VNode(null, null, null, text)
  }

  // 创建带变量的文本节点
  _s(value) {
    if(value === null || value === undefined){
      return ''
    } else if(typeof value === 'object'){
      return JSON.stringify(value)
    } else {
      return String(value)
    }
  }
}

至此,执行render函数会生成一个VNode, 注意这里实现的是一个简单编译过程,没有考虑到标签属性等问题。接下来的问题就转换成VNode如何转换成真实dom.

// 将vdom转换成真实的dom
function createElm(vnode){
  // 如果是文本节点
  if(!vnode.tag){
    const el = document.createTextNode(vnode.text)
    vnode.elm = el
    return el
  }
  // 否则的话 是元素接节点
  const el = document.createElement(vnode.tag)
  vnode.elm = el
  vnode.children.map(createElm).forEach(childDom => {
    el.appendChild(childDom)
  })
  return el
}

生成一个真实dom之后,如何更新视图呢?

  • 1.实现一个$mount函数,初次挂载到真实dom时调用,将原来的初始化render watcher的逻辑搬到$mount中

  • 2.实现一个_update, 接收一个新vdom,然后对比新旧vdom并更新真实dom,render watcher中不再暴力更新dom, 而是调用_update函数

$mount(el) {
    this.$el = document.querySelector(el);
    this._watcher = new Watcher(this,() =>{
      // 对比新旧vdom并更新真实dom
      this._update(this.$options.render.call(this))
    },() => {})
  }
// 更新dom结构
_update(vnode) {
    if(this._vnode) {
      patch(this._vnode, vnode)
    } else {
      // 第一次挂载vue实例
      patch(this.$el, vnode)
    }

    this._vnode = vnode
  }

patch函数接收两个参数:旧的vdom和新的vdom

  • 1.第一次挂载时 旧的vdom是一个真实dom,单独处理

  • 2.更新时 分以下几种情况:

    1. 新节点不存在,则删除对应的dom
    2. 新旧节点不一样或者文本不一样,则调用createElm生成新的dom,并替换旧dom
    3. 旧节点不存在,则调用createElm生成新的dom,并在原来的dom后添加新dom
    4. 递归
//patch函数实现
function patch(oldNode, newNode) {
  const isRealElement = oldNode.nodeType
  if(isRealElement) {
    let parent = oldNode.parentNode
    parent.replaceChild(createElm(newNode), oldNode)
    return
  }
  // 当前vnode对应的真实dom
  let el = oldNode.elm
  if(newNode) {
    // 将原来的dom重新赋值给新的vnode.elm
    newNode.elm = el
  }
  // 当前vnode对应的真实dom的父级元素
  let parent = el.parentNode
  if( !newNode ){
    // 删除
    parent.removeChild(el)
  } else if(!sameNode(newNode, oldNode)) {
    // 新旧节点标签不一样或者文本不一样,则调用createElm生成新的dom,并替换旧dom
    newNode.elm =el
    parent.replaceChild(createElm(newNode), el)
  } else if(newNode.children) {
    // 追加
    const oldLength = oldNode.children.length
    const newLength = newNode.children.length
    for(let i = 0; i < oldLength || i < newLength;i++) {
      if( i >= oldLength){
        // 旧节点不存在,新节点存在,则调用createElm生成新dom,在原dom后添加新dom
        el.appendChild(createElm(newNode.children[i]))
      }else {
        // 递归
        patch(oldNode.children[i], newNode.children[i])
      }
    }
  }
}

// 判断是否是相同节点,标签和文本内容都相同则为相同节点
function sameNode(newNode, oldNode) {
  return (newNode.tag === oldNode.tag && newNode.text === oldNode.text)
}

一大波干货来袭

了解原理的目的不仅仅是更好的的使用框架,更重要的是提高自己的编程能力,了解到原理之后,最好是能口述出来,能与面试官侃侃而谈,接下来整理的内容是vue开发者在面试过程中常见的vue相关面试题,建议收藏:

SPA的优势

优点:

  • 1.良好的交互体验
  • 2.前后端分离的工作模式
  • 3.减轻服务端压力,服务端只要响应数据即可 缺点:
  • 1.首屏加载缓慢 使用vue-router懒加载
  • 2.异步加载组件
  • 3.不利于SEO

如何理解MVVM

全称(Model View ViewModel)即数据驱动视图,在Vue中被称为MVVM,在React中被称为setState.这里的View指的就是视图层,即与dom结构相关,model是数据层,用来提供数据,在MVVM的框架中,我们只需要关注数据的变化,而无需去操作dom。

为什么data必须是一个函数

.vue文件在使用的时候实际上会转换成一个class,所以说如果data是一个对象的话,那么 变量的管理将会十分混乱。写成函数的形式,正是利用的js中的函数作用域。使得变量之间不会相互影响。

VUE2.X如何实现响应式

在vue2.x中是使用Object.defineProperty这个api实现响应式的。用这个api实现的响应式有几个与生俱来的缺点:

  • 1.一次性深度监听,当data中数据多或者层级多是十分消耗性能的
  • 2.无法监听新增的属性和删除的属性,所以在vue2.x中提供了$set$delete方法
  • 3.无法监听数组的变化,vue采取的措施是重写了数组原型上的方法。 在vue3.0中使用proxy实现响应式避免了这些问题,但是proxy的兼容性相对差一些,并且无法被polyfill,在某些浏览器中还是无法使用,vue3.0向下做了兼容,无法识别proxy的时候,就使用Object.defineProperty.

虚拟DOM和diff算法

首先我们需要明确的一点就是为什么会有Virtual DOM这个概念, VDOM并不是vue一家框架所独有的,前端框架基本都有这个概念。因为操作一个真实DOM是十分消耗性能的,而在V8引擎下,js的执行速度是非常快的,可以说两者根本就不是同一个数量级的,还有一个优点:vdom只在最后一步操作真正的DOM,能很好的实现跨平台,所以我们采取的措施尽量是用js操作代替dom操作。这时候就出现了VDOM的概念,VDOM是用js来描述一个真实的DOM结构。DIFF1.只同级比较,不跨级比较 2.tag不同则直接销毁重建,不再深度比较 3.tag和key都相同则认为是相同节点,不再深度比较

模板编译(组件渲染和更新的过程)

敲黑板:vue中template中的内容并不是html结构,虽然看起来和html十分相似,但是html是没有指令和插值的。其实vue在parser的时候会将template中的内容当成一个字符串进行处理。遍历这个字符串对template中的内容进行处理。

在webpack环境中vue-loader会在编译的时候将template编译成一个render函数,而在非webpack环境中会通过vue template compiler 在执行时将template编译成一个render函数。

组件初次渲染过程:

  1. 解析模板为render函数
  2. 触发响应式 监听data属性 getter、setter
  3. 执行render函数 生成vnode,执行patch(elem, vnode)

组件更新过程:

  1. 触发setter 更新数据
  2. 重新执行render函数 生成newVnode
  3. patch(vnode, newVnode)

用vnode描述一个Dom结构

//这样一段html结构转化成VNode
<div class='container'>
   <p>dom</p>
   <ul style="{ font-size: '20px' }">
       <li>a</li>
   </ul>
</div>
{
  tag: 'div',
  props: {
    className: 'container'
  },
  children: [
    {
      tag: 'p',
      children: 'dom'
    },
    {
      tag: 'ul',
      props:{
        style: 'font-size: 20px'
      },
      children:[
        {
          tag: 'li',
          children: 'a'
        }
      ]
    }
  ]
}

前端路由

前端路由分成两种模式:hash模式和history模式。

  • 一. hash模式 是利用hash的特点:①hash变化会触发网页跳转,即浏览器的前进和后退 ② hash变化不会刷新页面,满足SPA的需求 ③ hash永远不会提交到server端。 利用onHashChange方法可以监听到hash的变化,从而实现SPA。
  • 二、history模式,h5提供的history api,可以实现无刷新的跳转页面,这里需要掌握pushState和onPopState两个api。history模式是需要后端配合的,无论跳转到哪个path,都需要重定向到index.html。 其实很多情况下,考虑到投入产出比,是无需使用history模式的。这里也给出一些建议吧:对于to B的系统推荐使用hash 简单易用 对url规范不敏感;to C 的系统,可以考虑使用histroy 模式。

vue项目常见性能优化

  1. 合理使用v-if 和v-show;
  2. 合理使用computed;
  3. v-for加key,避免和v-if同时使用;
  4. 自定义事件、Dom事件及其销毁;
  5. 合理使用keep-alive;
  6. 合理使用异步组件;
  7. data层级不要嵌套太深,使用vue-loader在开发环境做模板编译。