Vue设计与实现(一)

413 阅读6分钟

大家好,我是王大傻,近期拜读了VueJS设计与实现这本书。也有了一点小小的感悟,特此分享给大家

首先呢,全书从视图层框架的范式入手,引出了命令式和声明式两种概念,那么什么是命令式?什么又是声明式呢

image.png

命令式和声明式

命令式:顾名思义,按照命令来执行一些任务,也就是关注的是执行过程,比如我们现在设想一个场景:我们需要页面中一个id为test的div标签并且给他的背景颜色改为红色,再然后给他绑定一个点击事件,点击时候提示“执行成功”。那么我们通过命令来描述一下这段话:

  1. 获取id为test的div
  2. 设置他的背景颜色为红色
  3. 绑定一个点击事件
  4. 设置事件触发时提示执行成功

那么对应的代码

const testNode = document.getElementById('test');
testNode.style.backGround = 'red'
testNode.onClick = () => {
    alert('执行成功')
}
/*
  对应Jq版本
*/
$("#test").css('background':'red').click(()=>{
    alert('执行成功')
})
​

通过代码的描述,我们大致就可以知道了Jq就是一个命令式的视图层框架,因为他执行的每一个步骤都可以通过代码描述出来,我们在读代码同时也就明白了具体做了什么。那么既然命令式这么好用,为什么还会有声明式一说呢,带着疑问我们来看一下这段代码

<div @click = "()=>alert('执行成功')" :style="{'background':'red'}"/> 

这段代码就是声明式的写法,我们可以看到,不需要我们操作DOM,也不需要我们去获取元素等这些操作,我们只需要在具体的标签上通过框架的规则告诉它我们想要做的事情即可,这样一来框架的底层就自动帮我们去处理好这一系列逻辑,Vue就是声明式的视图层框架。

那么问题又来了,既然命令式的代码具有更好的可读性,而声明式代码具有更好的使用性。那么抛开其他不谈,但从性能方面来讲,谁会更胜一筹呢。答案是声明式代码。为什么呢?因为命令式代码我们是通过直接操作DOM进行修改,并没有掺杂其他的转换成分,

  • 命令式代码性能消耗 = A

但是声明式就不同了 ,因为首先我们真实的HTML标签中肯定是没有这些特殊的语法,

  • 声明式代码性能消耗 = 转换性能 + A(因为无论如何最后还是要执行命令式的代码)

那么我们肯定需要将这个语法转换为浏览器可以识别的进行操作,也就是我们提及的虚拟DOM。那么虚拟DOM对比真实DOM又有哪些差异呢?

image.png

虚拟DOM的性能

上面提到了性能的差异,借用原文中的一句话就是 声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗。那么如果说我们用了虚拟DOM反而增加了性能消耗,那么我们为什么还要继续使用呢?答案是提效,技术本身就是为了业务拓展做积累的,那么如果可以提高我们的业务产出效率,增加点性能消耗也是无可厚非的。而虚拟DOM的本身就是通过JS对象形式去描述页面中的DOM,我们尽力做到差异最小也是从根本上的提升。实际情况中,当我们使用真实的DOM操作,如:

const htmlNode = `
<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>
`
document.getElementByTagsName('div')[0].innerHTML = htmlNode

而虚拟DOM则是:

const htmlNode = {
    tag:'ul',
    content:null,
    children:[
        { tag:'li',children:null,content:'1'},
        { tag:'li',children:null,content:'2'},
        { tag:'li',children:null,content:'3'},
        { tag:'li',children:null,content:'4'},
        { tag:'li',children:null,content:'5'}
    ]
}
function Render(obj,root){
    const el = document.createElement(obj.tag)
    el.innerText = !!obj.content ?  obj.content : ''
    if(typeof obj.children === 'string'){
        const text = document.createTextNode(obj.children);
        el.append(text)
    }else if(obj.children){
        obj.children.forEach(item => Render(item,el))
    }
    root.appendChild(el)
}
Render(htmlNode,document.body)

可以明显看出,真实DOM就是我们实在的HTML标签,我们通过innerHtml的方式去放进我们页面具体容器中,而虚拟DOM则是通过JS去描述DOM并通过一个渲染函数将其中的描述转换为真实DOM。那么问题来了?当我们真实的使用Vue时候,除非特定情况下,我们也不会经常使用render函数去处理我们的逻辑,那么这里就引入了另一个概念,运行时以及编译时。

image.png

运行时和编译时

上面我们说了虚拟DOM是用JS代码来描述具体的真实DOM,那么这样做是否真的大大降低了性能,其实不是的,当我们用真实DOM时候 ,假如我们需要改某个数据,那我们就需要将整个模板字符串去更改后重新加入到页面里面,反观我们的虚拟DOM,我们只需要找到需要更改的位置,并且找到其对应最小的改变进行更改JS描述层代码,并通过我们render函数指定去更改到具体内容里面,这样看来,当一个页面存在大量的DOM元素需要不停更改时候,其实虚拟DOM相比起来会有更好的性能。

那么正如我们所提及到render函数,我们不可能每一次都自己定义一个虚拟的节点操作并把它放进页面,我们自己使用时候也要大量创建树形数据,如果业务复杂的话,反而更加的低效了。在此情况下,我们就引入了运行时和编译时。那么显而易见的是,我们刚才的Render函数其实就是一个运行时的函数,他会在代码运行时候做一些操作。而编译时呢?编译时其实是和运行时相反的概念。 我们运行时是对我们自己编写的树形数据进行处理从而得到真实DOM,反其道而行的话,我们编译时就是当我们拿到用户写的DOM时候能不能通过一定的转换将其变为我们的树形虚拟DOM也就是我们的JS树形描述DOM的结构。肯定是可以的,相对Vue而言,其实对应的就是Compiler函数,那么运行时+编译时就是我们最终的Vue版本,我们通过一个个vue结尾的文件来做具体的页面,在运行时,vue内部通过一系列操作将我们的单文件组件处理成为Render函数供我们使用,同样的道理,在我们书写时候,首先会通过Compile函数来将我们的模板代码转换为特定格式VNode。

image.png