前言:

  • 本文围绕virtual-dom展开,vue/react借助Virtual DOM 带来了 分层设计
  • 不管是.vue文件还是jsx文件都借助virtual-dom来描述实际的dom结构,二者都有一个render实现的过程
  • 什么是渲染器,如何实现
  • 多端渲染带来的可能性

image.png
渲染器示意图

1:模拟实现渲染器

所谓渲染器,简单的说就是将 Virtual DOM 渲染成特定平台下真实 DOM 的工具(就是一个函数,通常叫 render),渲染器的工作流程分为两个阶段:mount 和 patch,如果旧的 VNode 存在,则会使用新的 VNode 与旧的 VNode 进行对比,试图以最小的资源开销完成 DOM 的更新,这个过程就叫 patch,或“打补丁”。如果旧的 VNode 不存在,则直接将新的 VNode 挂载成全新的 DOM,这个过程叫做 mount。

1.1:渲染器

需要将基于实际框架(vue/react)描述的文档结构用js对象来描述

不管是.vue文件还是jsx文件和我们在控制台看到的实际dom结构还是有一定距离的,当然具体的框架会有相应解析,在web层面当然都是我们所熟悉的dom文档结构。

为了简化框架的解析过程。我们目标落在实现由实际的dom结构到保存在内存中由js对象描述的virtual-dom

  • dom结构可以抽象成树形数据结构
  • 实际打印dom结构
  • 模拟由virtual-dom到实际dom结构过程

1.1.1: 一个抽象virtual-dom的例子

// 一个ul-li列表可以如下表示
<ul id='list'>
  <li class='item'>Item 1</li>
  <li class='item'>Item 2</li>
  <li class='item'>Item 3</li>
</ul>

// 树形数据
var element = {
  tagName: 'ul', // 节点标签名
  props: { // DOM的属性,用一个对象存储键值对
    id: 'list'
  },
  children: [ // 该节点的子节点
    {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
  ]
}

这里的element就是virtual-dom的样子,只不过实际中从最外层标签开始,结构比这复杂而已!

1.1.2: BFS/DFS遍历dom结构

为了验证dom文档结构可以抽象成以上的javascript对象,可以实际的遍历dom结构,以DFS遍历树形结构为例,打印当前页面的tagName,classList, 层级

const DFS = function(node) {
    if (!node) {
        return
    }
    let deep = arguments[1] || 1
    console.log(`${node.nodeName}.${node.classList}  ${deep}`)
    if (!node.children.length) {
        return
    }
    Array.from(node.children).forEach((item) => DFS(item, deep + 1))
}
// 在body标签上加了test id属性
var aimNode = document.getElementById('test')
DFS(aimNode)

1.1.3: 由virtual-dom到真实的dom结构

Vue 的 render 方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node即virtual-dom,体会一下和这里render的区别

  • 确定基本的vNode类,

    function Vnode (tagName, props, children) {
      this.tagName = tagName
      this.props = props
      this.children = children
    }
    // 添加render方法
    Vnode.prototype.render = function () {
      var el = document.createElement(this.tagName) // 根据tagName构建
      var props = this.props
    
      for (var propName in props) { // 设置节点的DOM属性
        var propValue = props[propName]
        el.setAttribute(propName, propValue)
      }
    
      var children = this.children || []
    
      children.forEach(function (child) {
        var childEl = (child instanceof Vnode)
          ? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
          : document.createTextNode(child) // 如果字符串,只构建文本节点
        el.appendChild(childEl)
      })
      return el
    }
    
    // 实例化ul,是一个virtual-dom对象
    var ul = new Vnode('ul', {id: 'list'}, [
      new Vnode('li', {class: 'item'}, ['Item 1']),
      new Vnode('li', {class: 'item'}, ['Item 2']),
      new Vnode('li', {class: 'item'}, ['Item 3'])
    ])
    // 挂载到body
    var ulRoot = ul.render()
    document.body.appendChild(ulRoot)
    

    2:render的过程

2.1: virtual-dom参与哪些流程

function render(vnode, container) {
// 获取vnode
  const prevVNode = container.vnode
  if (prevVNode == null) {
    if (vnode) {
      // 没有旧的 VNode,只有新的 VNode。使用 `mount` 函数挂载全新的 VNode
      mount(vnode, container)
      // 将新的 VNode 添加到 container.vnode 属性下,这样下一次渲染时旧的 VNode 就存在了
      container.vnode = vnode
    }
  } else {
    if (vnode) {
      // 有旧的 VNode,也有新的 VNode。则调用 `patch` 函数打补丁
      patch(prevVNode, vnode, container)
      // 更新 container.vnode
      container.vnode = vnode
    } else {
      // 有旧的 VNode 但是没有新的 VNode,这说明应该移除 DOM,在浏览器中可以使用 removeChild 函数。
      container.removeChild(prevVNode.el)
      container.vnode = null
    }
  }
}

2.1.1: 挂载mount

模拟由vnode到实际dom的过程,见前文render方法

2.1.2 :更新patch

3:多端渲染

3.1:render可以不走向dom

前面的例子是Virtual DOM渲染为 Web 平台的真实 DOM,由于面向浏览器,渲染器内部需要调用浏览器提供的 DOM 编程接口

  • document.createElement
  • el.appendChild
  • document.body.appendChild

为了实现多端渲染,render方法不需要再强依赖DOM 编程接口

相应的操作节点的接口由具体平台暴露,满足类似与dom节点的增删改查

节点:可以理解成对应平台的展示单元,如web端展示的是dom

function specialRenderer(options) {
  const {
    hanlde: {
      createElement: platformCreateElement,
      appendChild: platformAppendChild,
      insertBefore: platformInsertBefore,
      removeChild: platformRemoveChild,
      parentNode: platformParentNode,
      nextSibling: platformNextSibling,
      querySelector: platformQuerySelector
    }
  } = options
}

Vue3 提供了一个叫做 @vue/runtime-test 的包,其作用是方便开发者在无 DOM 环境时有能力对组件的渲染内容进行测试。

3.2:Taro多端实现猜想

Taro官方文档

Taro 是一套遵循 React 语法规范的 多端开发 解决方案。现如今市面上端的形态多种多样,Web、React-Native、微信小程序等各种端大行其道,当业务要求同时在不同的端都要求有所表现的时候,针对不同的端去编写多套代码的成本显然非常高,这时候只编写一套代码就能够适配到多端的能力就显得极为需要。

使用 Taro,我们可以只书写一套代码,再通过 Taro 的编译工具,将源代码分别编译出可以在不同端(微信/百度/支付宝/字节跳动/QQ小程序、快应用、H5、React-Native 等)运行的代码。
image.png

Taro多端实现猜想

  • 基于Taro的UI描述,dom结构还是会借助virtual-dom描述
  • 要实现web,多家小程序的编译实现,应该是在render的阶段判断具体环境,提供类似操作的dom(平台元素)的接口
  • 现在回顾 React 的 Learn Once, Write Anywhere 口号,实际上强调的就是它对各种不同渲染层的支持

3.3:多端渲染具体实现

too simple sometimes naive

基于渲染层的一点认识开始去调研市面上多端渲染框架的原理,结果实现大相径庭。

探讨:基于react的Taro是如何实现一套代码,多端运行?

  • 前面的讨论其实并不是没有意义,只不过render落脚点不一样,基于UI层面的描述是可以实现的
  • 但是没有考虑到的问题包括
  • Framewrok —— 通俗来说,完成一个 App 应用交互任务所需规范,例如生命周期(onLoad、onShow)、模块化与数据管理等。
  • Library —— 可以理解就是“方法封装集合”
  • 待补充。。。

3.3.1标准的 Language统一

image.png

业务代码统一约束,借助babel输出多端代码

3.3.2:Framework/Library处理

  • 业界处理思想不一
  • Chameleon:在各个端运行时分别实现了 Framework 统一,在各个端尽量使用原有框架,方便利用其生态,这样很多组件可以直接用起来。(folk?)

    • 可以借助babel - babel/polyfill基于语法和api的处理加以理解
  • taro: 挑选了微信小程序的组件库和 API 来作为 Taro 的运行时标准

4 :总结

image.png

再次回顾一下这张图,基本思想就是借助Virtual DOM 带来了 分层设计,每一步的单独处理都可以自成一家之言。框架多端编译的概念层出不穷,

站在开发者的角度知道背后实际是做了哪些改动就可以根据自己的兴趣选择方向。

文中对一些方法和操作做了简化,目的在与梳理流程,知识点包括不限于:

  • 将Dom结构描述为js对象--生成virtual-dom
  • 由js对象生成实际的dom结构-- render渲染器
  • 将更新的js对象与之前展开diff比较 -- 实现diff算法
  • 比较结果patch应用到实际的dom树上更新Dom结构 -- 差量更新
  • 实现自定义render函数对接多平台
  • 目前实现多端渲染的思路

5:参考阅读

VirtualDOM和基本DFS:https://zhuanlan.zhihu.com/p/64187708

babel-runtime使用与性能优化

渲染器解读:http://hcysun.me/vue-design/zh/renderer-advanced.html

跨端框架架构解读:https://segmentfault.com/a/1190000018307526

框架和库的区别:https://zhuanlan.zhihu.com/p/26078359


currygolden
31 声望1 粉丝

人生如逆旅,我亦是行人