实现一个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.更新时 分以下几种情况:
- 新节点不存在,则删除对应的dom
- 新旧节点不一样或者文本不一样,则调用createElm生成新的dom,并替换旧dom
- 旧节点不存在,则调用createElm生成新的dom,并在原来的dom后添加新dom
- 递归
//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函数。
组件初次渲染过程:
- 解析模板为render函数
- 触发响应式 监听data属性 getter、setter
- 执行render函数 生成vnode,执行patch(elem, vnode)
组件更新过程:
- 触发setter 更新数据
- 重新执行render函数 生成newVnode
- 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项目常见性能优化
- 合理使用v-if 和v-show;
- 合理使用computed;
- v-for加key,避免和v-if同时使用;
- 自定义事件、Dom事件及其销毁;
- 合理使用keep-alive;
- 合理使用异步组件;
- data层级不要嵌套太深,使用vue-loader在开发环境做模板编译。