共计 5031 个字符,预计需要花费 13 分钟才能阅读完成。
前言:
- 本文围绕 virtual-dom 展开,vue/react 借助 Virtual DOM 带来了 分层设计
- 不管是.vue 文件还是 jsx 文件都借助 virtual-dom 来描述实际的 dom 结构,二者都有一个 render 实现的过程
- 什么是渲染器,如何实现
- 多端渲染带来的可能性
渲染器示意图
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
-
var patches = diff(tree, newTree) 这里需要介绍 diff 算法
- 解析 snabbdom 源码,教你实现精简的 Virtual DOM 库
- 不同框架的实现有所差异
- v3.0 更新 diff 算法
- patch(root, patches) 把 patches 作用在之前的 dom 结构上,实现差量更新
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 等)运行的代码。
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 统一
业务代码统一约束,借助 babel 输出多端代码
3.3.2:Framework/Library 处理
- 业界处理思想不一
-
Chameleon:在各个端运行时分别实现了 Framework 统一,在各个端尽量使用原有框架,方便利用其生态,这样很多组件可以直接用起来。(folk?)
- 可以借助 babel – babel/polyfill 基于语法和 api 的处理加以理解
- taro: 挑选了微信小程序的组件库和 API 来作为 Taro 的运行时标准
4 : 总结
再次回顾一下这张图,基本思想就是借助 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