vue深入理解(1)
本文章内容主要来源于《vue.js设计与实现》
视图层框架设计
命令式和声明式
范式上,视图层框架通常分为命令式和范式
JQuery就是典型的命令式框架,命令式框架的一大特点就是关注过程
例子:
$('#app') // 获取app.text('hello world') // 设置文本内容.on('click', () => { alert('ok') })
原生JS实现例子:
const div = document.querySelector('#app')
div.innerText = 'hello world'
div.addEventListener('click', () => { alert('ok') })
声明式框架更加关注结果
vue.js例子:
<div @click="() => alert('ok')">hello world
</div>
可以看出在vue.js实现就是提供的是一个“结果”,怎么去实现的这个过程我们并不关心这是由vue.js帮我们完成的
也就是Vue封装了这个过程
- vue的内部实现一定是命令式的,而暴露给用户的则更加声明式
两者性能与可维护性比较
命令式代码的性能优于声明式的性能
若要实现修改文本内容如上面代码,只需要直接调用相关命令即可
div.innerText = 'nihao'
理论上命令式代码可以做到极致的代码优化(因为明确知道哪些发生更改,只需要做必要的修改)
对于框架而言,为了实现最优的更新性能,需要找到前后差异并只更新变化的地方
把直接修改消耗的性能定义为A,把找出差异的性能消耗定义为B
- 命令式代码的更新性能消耗=A
- 声明式代码的更新性能消耗=B + A
声明式代码的可维护性更强(命令式代码需要维护实现目标的整个过程)
虚拟DOM性能
该部分主要是初步介绍虚拟DOM,主要讲述为什么选择虚拟DOM
由于声明式代码的更新性能消耗=B + A
为了最小化找出差异的性能消耗,所以让声明式代码的性能无限接近于命令式消耗 -------- 虚拟DOM就是为了最小化找出差异这一步的性能消耗
理论上采用虚拟DOM的更新技术的性能不可能比原生JS操作DOM更高
虚拟DOM在vue中使用就是为了写声明式代码,还能够保证应用程序的性能下限
以上说的原生JS指的是类似于document.creatElement之类的DOM操作,并不包含innerHTML
innerHTML操作页面
使用innerHTML创建页面需要构建一段HTML字符串,然后将字符串赋值给DOM元素的innerHTML属性
- 但深层并非这句话这么简单,为了渲染出页面,首先要把字符串解析成DOM树(这是一个DOM层面的计算)
涉及DOM的计算要远比JS层面的计算性能差,纯JS层面的操作比DOM操作快得多,甚至不在一个数量级
使用innerHTML更新页面的过程是重新构建HTML字符串,再重新设置DOM元素的innerHTML属性(相当于销毁所有旧的DOM元素,再 全量 创建新的DOM元素)
虚拟DOM操作页面
使用虚拟DOM创建页面首先创建JS对象(该对象为真实DOM的映照),然后递归地遍历DOM树并创建真实DOM
使用虚拟DOM更新页面需要重新创建虚拟DOM树,然后比较新旧虚拟DOM,找到变化地元素并更新它
两者相比
虚拟DOM在 JS层面的计算 要比创建页面是多出一个Diff的性能消耗,但不会产生数量级的差异 ;然后DOM层面的计算,虚拟DOM在更新页面时只会更新必要的元素,但innerHTML要全量更新
性能总结
运行时、编译时
编译时(Compile Time)和运行时(Runtime)指的是程序在不同的阶段进行的不同类型的处理。
js 编译是什么呢?
现代前端开发,我们都是使用 react、vue、webpack 等工具,其实就是把这些框架代码编译成了 js 代码,那么编译时其实是指的,webpack 等工具把 react、vue 编译为 js 的过程。运行时自然不用说,就是浏览器解释 js 代码的过程。
纯运行时框架例子
// 渲染 dom
function render (node, root) {const el = document.createElement(node.tag)if (typeof node.children === 'string') {const text = document.createTextNode(node.children)el.appendChild(text)}if (Array.isArray(node.children)) {node.children.forEach(child => render(child, el)) // 递归地处理节点的渲染}root.appendChild(el)
}const node = {tag: 'div', // tag代表标签名称children: [ // children可以是一个数组,代表子节点{tag: 'h1',children: 'hello' // children也可以是一段文本,代表文本子节点}]
}render(node, document.body)
用户在使用render函数渲染内容时,直接为render函数提供了一个树型结构的数据对象
但若是用户需求是手写树型结构的数据对象太麻烦了,而且不直观,想要支持用类似HTML的方式描述树型结构的数据对象,所以引入编译的手段,把HTML标签编译成树型结构的数据对象
纯编译时框架例子
<div><h1>hello</h1>
</div>
经过编译
const div = document.createElement('div')
const h1 = document.createElement('h1')
h1.innerText = 'hello'
div.appendChild(h1)
document.body.appendChild(div)
直接将HTML字符串编译成命令式代码过程
编译时+运行时框架例子
编写一个Compiler的程序,作用是把HTML字符串编译成树型结构的数据对象
const html = '
<div><span>hello</span>
</div>
'
// 调用Compiler编译得到树形结构的数据对象
const obj = Compiler(html)
// 再调用render进行渲染
render(obj, document.bod)