Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

小程序框架原理总结 #43

Open
lei4519 opened this issue Mar 25, 2024 · 0 comments
Open

小程序框架原理总结 #43

lei4519 opened this issue Mar 25, 2024 · 0 comments

Comments

@lei4519
Copy link
Owner

lei4519 commented Mar 25, 2024

实现方案分类

  • 编译型:Taro 1、2
    • 通过 babel 对代码进行转换,编译结束后完全是小程序的代码。
  • 半编译、半运行时:uni-app、mpvue
    • 模板层编译转换、运行时做语法兼容
  • 运行时:Remax、Taro next、Kbone
    • 纯运行时,模板层只有一个固定的模板 wxml,通过运行时生成的 vnode 来渲染视图。

编译型

Taro 1、2 可以让我们使用类 react 语法开发小程序,之所以说是类 react 语法,是因为这些“react”代码在编译完成之后,会被全部编程小程序的原生语法,里面是没有 react 参与的。

这也导致在 Taro 1、2 中写 JSX 时,需要遵照官方文档中的规范,否则运行时就会出现问题。

并且由于 JSX 的灵活性,将 JSX 转换成小程序模板时,是一件工作量非常大的事情。

Taro 就是用穷举法硬生生的将 JSX 全部转换成了模板。但即使这样,依然会有问题,比如无法实时的享受 react 的新语法、新特性,大多数 react 生态都无法直接使用。即使用了穷举法,转换过程中还是会出现各种问题,Taro 的大多数 commit 提交都是有关于模板转换的。

半编译、半运行时

小程序的设计很明显是借鉴了 vue,不管是模板的写法还是逻辑层的写法。所以相比起 JSX 转换的困难,vue 代码在转换时就轻松很多了。

所以像 mpvue、uni-app 就是利用这一特性,将 vue 的模板转换成了小程序的 wxml,这是其编译型的特性。

在编译时,并没有将逻辑层的代码转换成小程序原生的语法,而是完全的保留了 vue 的能力,这是其运行时的特性。

由于完全保留了 vue 的能力,也使得上述纯编译型框架的痛点得以解决。我们可以享受最新的语法特性,不用担心由于写法问题导致转换错误,可以方便的复用 vue 生态。

那现在留下的疑问就是:vue 是浏览器中的运行时框架,它是怎么去操作改变小程序视图的呢?

运行时原理

首先,vue 在浏览器中的运行流程如下:

new Vue() -> render() -> patch() -> 浏览器视图
  1. 初始化 vue,监听响应式等工作。
  2. 调用 render 函数(手写或者通过 vue-loader 编译 template 生成),拿到虚拟 dom(vnode)
  3. patch 函数用来做 diff 工作,然后将需要显示在视图中的 vnode,使用原生的 DOM API 插入到 DOM 树中。

观察上述流程,发现只在第三步中涉及到了浏览器相关的东西。所以 1、2 步是完全可以在小程序中运行的。

所以只需要对第三步做适配,就可以将 vue 运行在小程序中了。

那如何做适配呢?当 patch 函数 diff 出需要更新渲染的 vnode,就会去调用更新视图的 API,在浏览器中那是 DOM 系列的 API。而在小程序中,其只为我们提供了 setData 这个用来更改视图的方法,所以很明显,我们只需要在 diff 结束后,调用 setData 来更新视图就可以了。

这就又引出了另一个问题,patch 出来的是 vnode,而我们在模板中用的是 data 中的数据,所以肯定不能直接将 vnode 传到 setData 中,应该给 setData 传入 data 中改变的数据才对,那我们怎么才能拿到 data 中改变的数据呢?

说起改变的数据,可能第一反应是 diff,我们将老的数据保存一份,当数据改变之后。对新旧数据进行 diff,然后拿到差异数据,传入 setData 更新视图。

这样当然是可行的,我们只需要将 patch 中对比 vnode 的代码干掉,换成 diff 新旧数据的代码,这样 diff 结束后将结果传入 setData 即可。

但是这么做会有一个性能问题,如果 data 中有很多的数据,而在视图中我们只用到了一个简单的变量,这时对 data 数据进行全量 diff 就是一个很浪费时间的工作了。

所以我们可以用另一个方案来更高效的实现,让我们回到上一个问题,patch 出来的是 vnode,而传入 setData 的是 data 数据。那我们可不可以通过 vnode 来获取到 data 中改变的数据呢?

答案是可以的,因为我们会编译模板层的代码,所以在编译时,我们完全可以将相关的信息记录下来。

不废话,上代码:

<!-- vue template -->
<view>
  <view>{{ text }}</view>
  <view v-for="item in list">{{ item }}</view>
</view>

<!-- 编译后的wxml -->
<view>
  <view>{{ text }}</view>
  <view wx:for="list" wx:for-item="item" wx:for-index="i">{{ list[i] }}</view>
</view>

<!-- template编译的render函数 -->
render(h) {
  return h('view', null,
    [
      h('view', {path: 'text'}, text),
      list.map((_, i) => {
        return h('view', {path: 'list[i]'}, list[i])
      })
    ])
}

通过观察上面的三段代码,我们可以看到,在编译时,我们是可以感知到模板中使用的响应式数据,相对于 data 的访问路径。所以我们就可以将这些路径记录在 vnode 中,这样通过 patch 之后的 vnode,我们就可以获取到对应数据的路径了。

并且通过这种方式,我们也可以避免给视图层传递无用的数据,每次 diff 之后,只会将视图层中用的数据传入 setData。

运行时

上面的半编译、半运行时框架只能用于 vue,对 react 而言,其灵活的语法特性和底层架构注定与之无缘。难道说 react 就不能再小程序中运行了吗?

我们来回顾一下上一节中提到的

vue 的 patch 函数 diff 之后的结果是 vnode,而模板中用的是 data 数据,所以我们不能将 vnode 传给 setData,而应该找到对应的数据传给 setData。

再来回顾一下第一节中所说的

JSX 的灵活性导致转换成 wxml 时的工作量巨大,并且要严格遵守规范,否则就会出问题。

放飞你的大脑,我们来刚一下

vue 的 patch 函数 diff 之后的结果是 vnode,而模板中用的是 data 数据,所以我们不能将 vnode 传给 setData

那就是说,如果模板中没有使用 data 数据,那我就可以将 vnode 传入 setData 喽。

JSX 的灵活性导致转换成 wxml 时的工作量巨大,并且要严格遵守规范,否则就会出问题

转换起来那么难,那我干脆不转了。JSX 最后生成的是什么?vnode 啊,我直接把 vnode 传给 setData 喽。

所以现在的问题变成了,小程序模板可以根据 vnode 对象来渲染视图吗?

答案是可以,方案就是小程序的 template 语法可以动态递归的调用渲染。

<template name="tpl_view">
  <view class="{{ className }}">
    <block wx:for="{{children}}">
        <template is="tpl_view" data="{{ item }}"></template>
    </block>
  </view>
</template>

以上为伪代码,只为方便理解,实际上微信小程序不允许模板调用自身。

上面定义了一个模板,我们只需要构建如下的数据结构,将其传入模板内,这个模板就可以根据数据生成对应结构的 view 元素

const data = {
  className: '1',
  children: [
    {
      className: '1-1'
      children: [
        {
          className: '1-1-1',
          children: []
        }
      ]
    }
  ]
}

也就是说,如果我们将小程序的所有基础组件,全部都使用模板的形式定义一遍。那我们就可以传入一个描述对象(vnode,注意这个 vnode 不是 vue 或者 react 的),让 wxml 根据这个对象生成视图。

这也是目前 Taro next、Remax 在视图层的实现方式,如果打开 Taro next 编写的小程序代码就会发现里面有一个 base.wxml,其中就是实现了所有的小程序基础组件的模板。

到此为止,视图层我们已经解决了,但是这个解决方案好像和 react 没有什么关系,理论了只要能产生 wxml 渲染模板的 vnode,任何框架都可以这样实现。

事实上也是,用这种方案,可以很轻松的同时支持 Vue 和 React 这两种框架。Taro next 就是如此。

接下来就说一说 react 的事

实现原理

react 的源码相比 vue 更加复杂,但整体的思想并无太多差距,可以简单总结为

React.render() -> reconciler() -> renderer() -> 浏览器视图
  1. React.render 做初始化工作
  2. reconciler 做 diff 工作,找出需要更新的 vnode
  3. renderer 负责调用浏览器 API 将 vnode 渲染

看过 vue 的实现之后,我们不难猜到,这里要动刀的肯定是 renderer 这一步了。

那如何做呢?别忘了我们的核心点是什么:构建出 wxml 用来遍历渲染的 vnode。

我们重新翻译一下上面的第三步:

renderer 负责将需要更新的虚拟 vnode,通过 DOM API 转换成真实的 dom。

然后想想我们怎么实现呢?

renderer 负责将需要更新的虚拟 vnode,通过【自定义 API】转换成 wxml 用来遍历渲染的 vnode。

是的,就是这么“简单”,我们只需要将原本操作生成 DOM 的那套 API,更改成操作生成 vnode 的 API 就可以啦~~~ 完结!撒花❀❀❀❀❀❀❀❀❀❀❀


开玩笑,当然没那么“简单”,但是思想就是这样,不管是 vue 还是 react,其实我们需要做的就是将更改 DOM 的 API,换成更改 vnode 的 API。

举个例子:

// DOM API
function createElement(type) {
  return document.createElement(type)
}

// vnode API
function createElement(type) {
  return {
    template_name: 'tpl_' + type
  }
}

还有一个好消息就是,不管是 React 还是 Vue,当需要操作 DOM 时,都不会直接在代码中调用 DOM API,而是将操作统一封装到了工具函数中,这样我们就可以很方便的对这些 API 进行改写了。

更具体的实现,这里就不在赘述,可以看下面的参考链接去详细了解。

参考资料

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant