likes
comments
collection
share

一个前端框架的诞生需要几步

作者站长头像
站长
· 阅读数 42

框架诞生需要几步

声明式 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需要的数据结构形式。

一个前端框架的诞生需要几步

大家熟知的VueReact都是采用这种形式。

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
评论
请登录