前端主流框架:React、Vue等,区别于jq的核心思想是抽象了data->view这一层,我们不再手动调dom的api去维护dom的更新,抽象了data这一层来反应页面视图的状态,修改也是修改data,然后利用框架data与dom的绑定,去驱动dom的修改。

下面,我们从设计者的角度,看下这类MVVM框架考虑,他解决了什么问题以支持data->dom的能力?
首先可以确定的是:这类框架的设计里最主要的是 模板引擎如何做到修改data就能精准驱动更新dom的?
其次当然还有 事件系统、性能优化(异步更新队列、任务调度等等)

下面我们解析实现模板引擎这块,如何解决的数据绑定问题
以Vue为例,渲染一块内容,会经过以下流程:
1、解析语法生成AST
2、根据AST结果,完成data数据初始化;
3、根据AST结果和data数据绑定情况,生成virtual dom;
4、将virtual dom生成真正的dom元素插入到页面中,此时页面会被渲染;

渲染器(模板引擎)

解析模板生成AST

AST语法树: 通过解析源代码,将源代码的语句映射到树上的每个节点,这样的数叫抽象语法树AST

解析元素、指令v-if、v-for等、属性ref/slot/component等、父子节点关系等内容,得到一个AST对象

1、捕获特定语法 parse函数
2、ast生成dom元素

  1. <div>
  2. <a>123</a>
  3. <p>456<span>789</span></p>
  4. </div>

模板引擎在语法分析、语义分析之后,得到一个ast对象

  1. thisDiv = {
  2. dom: {
  3. type: "dom",
  4. ele: "div",
  5. nodeIndex: 0,
  6. children: [{
  7. type: "dom",
  8. ele: "a",
  9. nodeIndex: 1,
  10. children: [{
  11. type: "text",
  12. value: "123"
  13. }]
  14. },
  15. {
  16. type: "dom",
  17. ele: "p",
  18. nodeIndex: 2,
  19. children: [{
  20. type: "text",
  21. value: "456"
  22. },
  23. {
  24. type: "dom",
  25. ele: "span",
  26. nodeIndex: 3,
  27. children: [{
  28. type: "text",
  29. value: "789"
  30. }]
  31. }
  32. ]
  33. }
  34. ]
  35. }
  36. };

ast对象维护了我们需要的有用信息:
image.png

ast生成dom

image.png
元素属性动态化: 读取ast信息,将标识打在元素上;
绑定元素更新的触发函数: 事件监听使得元素支持内容更新、节点更新;

这个过程vue根据ast对象生成一段可执行的代码

  1. /**
  2. * 根据 AST 对象生成一个元素
  3. * 该代码片段基于 Vue2.x 版本
  4. */
  5. function genElement(el: ASTElement): string {
  6. // 根据该元素是否有相关的指令、属性语法对象,来进行对应的代码生成
  7. if (el.staticRoot && !el.staticProcessed) {
  8. return genStatic(el);
  9. } else if (el.once && !el.onceProcessed) {
  10. return genOnce(el);
  11. } else if (el.for && !el.forProcessed) {
  12. return genFor(el);
  13. } else if (el.if && !el.ifProcessed) {
  14. return genIf(el);
  15. } else if (el.tag === "template" && !el.slotTarget) {
  16. return genChildren(el) || "void 0";
  17. } else if (el.tag === "slot") {
  18. return genSlot(el);
  19. } else {
  20. // component 或者 element 的代码生成
  21. let code;
  22. if (el.component) {
  23. code = genComponent(el.component, el);
  24. } else {
  25. const data = el.plain ? undefined : genData(el);
  26. const children = el.inlineTemplate ? null : genChildren(el, true);
  27. code = `_c('${el.tag}'${
  28. data ? `,${data}` : "" // data
  29. }${
  30. children ? `,${children}` : "" // children
  31. })`;
  32. }
  33. // 模块转换
  34. for (let i = 0; i < transforms.length; i++) {
  35. code = transforms[i](el, code);
  36. }
  37. # 返回最后拼装好的可执行的代码
  38. return code;
  39. }
  40. }

模板渲染一般分为两种方式:
way1、字符串模板: str = ${xxx}文字; node.innerHTML = str;
===> 追寻节点更新方式:需要将nodeIndex绑定在节点属性上,数据更新时便于找到该节点进行内容更新;
way2、节点模板: createElement(); appendChild(); textContent 动态插入节点
===> 追寻节点更新方式:创建节点时保留节点引用,直接作用进行数据更新;

这两种方式都有一定的性能损耗,
way1: 字符串模板,找节点消耗计算;
way2:节点模板,需要管理特别多的节点和引用保存;

所以聪明的开发者提出了virtual dom

引擎给div赋能

上述通过ast转换,表面看是 div -> div的过程;
但这样的解析不是多此一举,因为在这个过程,模板引擎还实现了以下功能:
image.png

最重要的是:数据绑定、事件绑定。使得之后的data->view变得可能;
使用了模板引擎,我们可以隔离最最底层的操作dom,之后对dom的更新只需要更新和dom绑定的data即可;
除此以外,模板引擎还帮我们处理了很多低效又重复的工作,如:

  • 浏览器兼容
  • 全局事件的统一管理和维护
  • 模板更新的虚拟DOM机制
  • 树状管理组织组件

性能提升: 虚拟DOM

虚拟dom设计,可以分成3个过程:
1、js对象模拟dom树,得到一棵虚拟dom
2、数据变更时,生成新的虚拟dom树,新旧dom diff,得到差异
3、差异更新应用到真正的dom树上

dom元素属性很多,而vnode无需那么多,可以大大降低对象内存,diff计算量

  1. # type Vnode
  2. tag: string | void;
  3. data: VNodeData | void;
  4. children: ?Array<VNode>;
  5. text: string | void;
  6. elm: Node | void;
  7. ns: string | void;
  8. ....

dom diff 算法

patch

将差异应用到dom树上,如 节点替换、移动、删除、文本内容改变等;

数据绑定及更新

数据绑定

  1. <div>{{ message }}</div>
  2. new Vue({
  3. data: {
  4. message: "测试文本"
  5. }
  6. });

data的数据是如何与dom上的 {{message}}绑定的,
由上文ast分析可知:image.png

  1. <div>{{ message }}</div>
  1. 得到的ast对象
  2. thisDiv = {
  3. dom: {
  4. type: "dom",
  5. ele: "div",
  6. nodeIndex: 0,# dom 通过nodeindex 建立索引进行连接
  7. children: [{
  8. type: "text",
  9. value: ""
  10. }]
  11. },
  12. binding: [{
  13. type: "dom",
  14. nodeIndex: 0, # binding
  15. valueName: "message" # 绑定的数据为:data.message
  16. }]
  17. }

生成dom的时候,添加对message的拦截监听,数据更新时我们可以找到对应的nodeIndex;

  1. // 假设这是一个生成 DOM 的过程,包括 innerHTML 和事件监听
  2. function generateDOM(astObject) {
  3. const {
  4. dom,
  5. binding = []
  6. } = astObject;
  7. // 生成 DOM,这里假装当前节点是 baseDom
  8. baseDom.innerHTML = getDOMString(dom); # getDOMString
  9. // 对于数据绑定的,来进行监听更新吧
  10. baseDom.addEventListener("data:change", (name, value) => {
  11. // 寻找匹配的数据绑定
  12. const obj = binding.find(x => x.valueName == name);
  13. // 若找到值绑定的对应节点,则更新其值。
  14. if (obj) {
  15. baseDom.find(`[data-node-index="${obj.nodeIndex}"]`).innerHTML = value;
  16. }
  17. });
  18. }
  1. # getDOMString 根据 ast产生的dom对象,生成字符串模板
  2. // 获取 DOM 字符串,这里简单拼成字符串
  3. function getDOMString(domObj) {
  4. // 无效对象返回''
  5. if (!domObj) return "";
  6. const {
  7. type,
  8. children = [],
  9. nodeIndex,
  10. ele,
  11. value
  12. } = domObj;
  13. if (type == "dom") {
  14. // 若有子对象,递归返回生成的字符串拼接
  15. const childString = "";
  16. children.forEach(x => {
  17. childString += getDOMString(x);
  18. });
  19. // dom 对象,拼接生成对象字符串
  20. return `<${ele} data-node-index="${nodeIndex}">${childString}</${ele}>`;
  21. } else if (type == "text") {
  22. // 若为 textNode,返回 text 的值
  23. return value;
  24. }
  25. }

这样完成了 模板初始化渲染 & 该node对数据变更的监听之后的updateview的函数,从而实现data变更即可,dom无需再care;

这里的方式就是,dom的更新是就近维护的;
dom自己的更新维护自己,同时也是自己直接监听data的更改,没有中间商赚差价;
而我写的那个在线excel,是要靠中间的engine统一调度更新;

  1. 明明渲染层,也实现了局部更新的方法,
  2. 但是就是卡在viewmodel这层,对data的变更:
  3. 1、局部属性的变更,拿到key
  4. 2celmm+当前cellmm的物理属性,定位位置、
  5. viewmodel emit:dataonchange; canvas: on:dataonchange
  6. 可是这样和将canvas的方法逐步挂在父组件上,抛给viewmodel调用有啥区别。。。
  7. 这样也不是理想中的 data->view,因为下一次其他方式的更新,比如merge,又要开放一条局部merge的命令出来
  8. 不可能像dom这样的 通用级别。
  9. 模板的写的render 模板 模板的实际渲染dom,几乎没两样。所以他们都响应式了。

数据属性拦截:vue

image.png
get: 对data进行访问时:收集依赖
set: 对data进行更改时,调用dep.notify()

手动set写入:react

在react里,可以使用renderDom.render(), this.setState(), this.forceUpdate, useState等方法触发状态更新,这些方法共享同一套状态更新机制:
找出变化的组件,每当更新发生时,reconciler会做如下工作:调用组件render方法将jsx转换为虚拟dom,
进行虚拟dom的diff 找到变化的虚拟dom。通知renderer;
renderer:接到reconciler通知,将变化的组件渲染到页面上;

ng:脏检查

image.png

参考资料

《被删-前端进阶》