一个前端框架的诞生需要几步
框架诞生需要几步
声明式 vs. 命令式
首先你会选择声明式还是命令式的范式编程?
- 命令式:关注过程
- 声明式:关注结果
举例:
你想实现的功能是:
给id为app的div元素设置文本内容为'hello world',并且点击的时候可以弹出'ok'
命令式
如果要完成你的功能,命令式需要这样实现:
1.获取id为app的div元素
const div = document.getElementById('app');
2. 设置文本内容为'hello world'
div.innerText = 'hello world';
3.点击的时候可以弹出'ok'
div.addEventListner('click', () => { alert('ok') });
有一个很有名的命令式框架叫jQuery,它的作用就是简化写原生JavaScript的代码量:
$('#app')
.text('hello world')
.on('click', () => { alert('ok') });
命令式框架需要做的事就是把这些你常用的api进行封装,但是功能是如何实现的它不关心,需要框架使用者关注过程是否正确。
声明式
声明式要实现这个功能,用户只需要“声明”一下想要的结果:
// 以Vue为例
<div @click="()=>alert('ok')">hello world</div>
我最终想要的结果就是我所写的这样,至于实现过程我不关心,这是声明式框架内部做的事。
以上是声明式和命令式的区别。
对于用户来说,声明式是最方便的,不关注过程没有心智负担。框架内部肯定是命令式的写法,来将用户的输入做转化。
进一步可以想到:一个框架诞生的第一步就是怎么去实现用户的声明式编程
实现结构
一个前端框架的结构:
- 输入:用户的输入,按照框架作者设计的语法规则
- 转化:框架需要实现的部分,将输入转化为浏览器可以运行的代码
输入
假设框架设计的input输入规则是这样:
对于HTML<div><span>hello world</span></div>
需要输入下面的结构:
const obj = {
tag: 'div',
children: [
{
tag: 'span',
children: 'hello world'
}
]
};
这里结构一目了然,tag表示HTML标签名,children来循环表示嵌套的HTML元素。
React里使用JSX作为输入,Vue里主要使用模板语法作为输入。
转化
然后我们需要将上面的input转化为浏览器可用的代码。
关于转化这一步,选择有三种:
- 纯运行时
- 运行时 + 编译时
- 纯编译时。
1. 纯运行时
此时有一个Render
函数,它可以直接将输入的input
渲染到浏览器中。
简单实现一下:
function Render(root, input) {
const el = document.createElement(input.tag)
if (typeof input.children === 'string') {
const text = document.createTextNode(input.children)
el.appendChild(text)
} else if (input.children) {
input.children.forEach(child => render(el, child))
}
root.appendChild(el)
}
只经过Render就完成渲染的就是纯运行时。
2. 编译时+运行时
多了编译时,就是多了一个compiler
函数。
👆🏻上面输入input
的类HTML数据结构对输入的人来讲非常麻烦,所以input
就采用最初的HTML
形式,然后使用compiler
函数编译为render
需要的数据结构形式。
3.纯编译时
既然可以实现compiler
函数将HTML编译为类HTML对象的功能,纯编译时就是一步到位,使用compiler
函数将HTML编译为最终的命令式代码:
const div = document.createElement('div')
const span = document.createElement('span')
span.innerText = 'hello world'
div.appendChild(span)
document.body.appendChild(div)
Svelte就是一个纯编译的框架。
一些thinking
-
纯运行时对于用户来说不友好,并且有编译过程的话就可以分析每次输入的内容进而进行优化,所以框架基本不会只是运行时。
-
纯编译时 比 编译时+运行时 少了一个步骤,那是不是纯编译时会更快呢?Svelte框架认为会更快。
如果只是运行我文章里的demo,纯编译时肯定比编译时+运行时快。但是对于真实项目中,如果有大量的DOM元素时,Svelte还暂时没有被证实性能会更好。纯编译时做法可能有失灵活性。
而编译时+运行时的框架,比如Vue3在保持灵活性的基础上能够尽可能地去优化,性能不输纯编译时。
Vue里的编译时+运行时
用Vue来举例。
编译器
大家用Vue一般都会使用模板语法来作为输入,也就是使用template
。
<template>
<div @click="handler">
click me
</div>
</template>
<script>
export default {
method: {
handler: () => {...}
}
}
</script>
此时编译器会将template
里的HTML编译为类HTML数据结构(Vue里也把这个叫虚拟DOM),render
函数里会返回编译结果:
<script>
export default {
method: {
handler: () => {...}
},
render() { // template的编译结果
return {
tag: 'div',
props: {
onClick: handler
},
children: 'click me'
}
}
}
</script>
那在Vue里为了方便虚拟DOM的编写,专门写了一个h
函数来简化虚拟DOM的书写,所以最终在Vue里是这样:
<script>
export default {
method: {
handler: () => {...}
},
render() {
return h('div', {onClick: handler}, 'click me')
}
}
</script>
然后渲染器会将render
函数返回的虚拟DOM渲染为真实的DOM,这一步就是运行时。
渲染器
在渲染器渲染前,要考虑两个问题:
- 我们需要考虑的是什么时候需要渲染?
当【页面的数据变化 <-> 页面UI变化】时,就需要重新渲染,怎么监听到变化呢?Vue专门写了响应系统来解决这个问题,响应系统也是一个比较大的话题了(挖坑1。
- 我们需要渲染哪些数据? Vue对虚拟DOM进行diff算法计算来找出变化的部分,然后只对变化的部分进行渲染。diff算法也是一个比较经典话题了(挖坑2。
回到渲染器上。
此时经过上面两个问题,我们得到了此时应该渲染的虚拟DOM,然后就是将它渲染为真实的DOM了。
这里渲染器的实现其实和上面的Render
函数差不多的思路:
function Render(root, input) {
const el = document.createElement(input.tag)
if (typeof input.children === 'string') {
const text = document.createTextNode(input.children)
el.appendChild(text)
} else if (input.children) {
input.children.forEach(child => render(el, child))
}
root.appendChild(el)
}
Vue里的渲染器代码虽然看起来很多,但是基本思路就是这样的。
参考
《Vue.js设计与实现》 霍春阳
转载自:https://juejin.cn/post/7156258977737768996