前端主流框架: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元素
<div>
<a>123</a>
<p>456<span>789</span></p>
</div>
模板引擎在语法分析、语义分析之后,得到一个ast对象
thisDiv = {
dom: {
type: "dom",
ele: "div",
nodeIndex: 0,
children: [{
type: "dom",
ele: "a",
nodeIndex: 1,
children: [{
type: "text",
value: "123"
}]
},
{
type: "dom",
ele: "p",
nodeIndex: 2,
children: [{
type: "text",
value: "456"
},
{
type: "dom",
ele: "span",
nodeIndex: 3,
children: [{
type: "text",
value: "789"
}]
}
]
}
]
}
};
ast对象维护了我们需要的有用信息:
ast生成dom
元素属性动态化: 读取ast信息,将标识打在元素上;
绑定元素更新的触发函数: 事件监听使得元素支持内容更新、节点更新;
这个过程vue根据ast对象生成一段可执行的代码
/**
* 根据 AST 对象生成一个元素
* 该代码片段基于 Vue2.x 版本
*/
function genElement(el: ASTElement): string {
// 根据该元素是否有相关的指令、属性语法对象,来进行对应的代码生成
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el);
} else if (el.once && !el.onceProcessed) {
return genOnce(el);
} else if (el.for && !el.forProcessed) {
return genFor(el);
} else if (el.if && !el.ifProcessed) {
return genIf(el);
} else if (el.tag === "template" && !el.slotTarget) {
return genChildren(el) || "void 0";
} else if (el.tag === "slot") {
return genSlot(el);
} else {
// component 或者 element 的代码生成
let code;
if (el.component) {
code = genComponent(el.component, el);
} else {
const data = el.plain ? undefined : genData(el);
const children = el.inlineTemplate ? null : genChildren(el, true);
code = `_c('${el.tag}'${
data ? `,${data}` : "" // data
}${
children ? `,${children}` : "" // children
})`;
}
// 模块转换
for (let i = 0; i < transforms.length; i++) {
code = transforms[i](el, code);
}
# 返回最后拼装好的可执行的代码
return code;
}
}
模板渲染一般分为两种方式:
way1、字符串模板: str = ${xxx}文字
; node.innerHTML = str;
===> 追寻节点更新方式:需要将nodeIndex绑定在节点属性上,数据更新时便于找到该节点进行内容更新;
way2、节点模板: createElement(); appendChild(); textContent 动态插入节点
===> 追寻节点更新方式:创建节点时保留节点引用,直接作用进行数据更新;
这两种方式都有一定的性能损耗,
way1: 字符串模板,找节点消耗计算;
way2:节点模板,需要管理特别多的节点和引用保存;
所以聪明的开发者提出了virtual dom
引擎给div赋能
上述通过ast转换,表面看是 div -> div的过程;
但这样的解析不是多此一举,因为在这个过程,模板引擎还实现了以下功能:
最重要的是:数据绑定、事件绑定。使得之后的data->view变得可能;
使用了模板引擎,我们可以隔离最最底层的操作dom,之后对dom的更新只需要更新和dom绑定的data即可;
除此以外,模板引擎还帮我们处理了很多低效又重复的工作,如:
- 浏览器兼容
- 全局事件的统一管理和维护
- 模板更新的虚拟DOM机制
- 树状管理组织组件
性能提升: 虚拟DOM
虚拟dom设计,可以分成3个过程:
1、js对象模拟dom树,得到一棵虚拟dom
2、数据变更时,生成新的虚拟dom树,新旧dom diff,得到差异
3、差异更新应用到真正的dom树上
dom元素属性很多,而vnode无需那么多,可以大大降低对象内存,diff计算量
# type Vnode
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
....
dom diff 算法
patch
将差异应用到dom树上,如 节点替换、移动、删除、文本内容改变等;
数据绑定及更新
数据绑定
<div>{{ message }}</div>
new Vue({
data: {
message: "测试文本"
}
});
data的数据是如何与dom上的 {{message}}绑定的,
由上文ast分析可知:
<div>{{ message }}</div>
得到的ast对象
thisDiv = {
dom: {
type: "dom",
ele: "div",
nodeIndex: 0,# dom 通过nodeindex 建立索引进行连接
children: [{
type: "text",
value: ""
}]
},
binding: [{
type: "dom",
nodeIndex: 0, # binding
valueName: "message" # 绑定的数据为:data.message
}]
}
生成dom的时候,添加对message的拦截监听,数据更新时我们可以找到对应的nodeIndex;
// 假设这是一个生成 DOM 的过程,包括 innerHTML 和事件监听
function generateDOM(astObject) {
const {
dom,
binding = []
} = astObject;
// 生成 DOM,这里假装当前节点是 baseDom
baseDom.innerHTML = getDOMString(dom); # getDOMString
// 对于数据绑定的,来进行监听更新吧
baseDom.addEventListener("data:change", (name, value) => {
// 寻找匹配的数据绑定
const obj = binding.find(x => x.valueName == name);
// 若找到值绑定的对应节点,则更新其值。
if (obj) {
baseDom.find(`[data-node-index="${obj.nodeIndex}"]`).innerHTML = value;
}
});
}
# getDOMString: 根据 ast产生的dom对象,生成字符串模板
// 获取 DOM 字符串,这里简单拼成字符串
function getDOMString(domObj) {
// 无效对象返回''
if (!domObj) return "";
const {
type,
children = [],
nodeIndex,
ele,
value
} = domObj;
if (type == "dom") {
// 若有子对象,递归返回生成的字符串拼接
const childString = "";
children.forEach(x => {
childString += getDOMString(x);
});
// dom 对象,拼接生成对象字符串
return `<${ele} data-node-index="${nodeIndex}">${childString}</${ele}>`;
} else if (type == "text") {
// 若为 textNode,返回 text 的值
return value;
}
}
这样完成了 模板初始化渲染 & 该node对数据变更的监听之后的updateview的函数,从而实现data变更即可,dom无需再care;
这里的方式就是,dom的更新是就近维护的;
dom自己的更新维护自己,同时也是自己直接监听data的更改,没有中间商赚差价;
而我写的那个在线excel,是要靠中间的engine统一调度更新;
明明渲染层,也实现了局部更新的方法,
但是就是卡在viewmodel这层,对data的变更:
1、局部属性的变更,拿到key
2、celmm+当前cellmm的物理属性,定位位置、
viewmodel emit:dataonchange; canvas: on:dataonchange;
可是这样和将canvas的方法逐步挂在父组件上,抛给viewmodel调用有啥区别。。。
这样也不是理想中的 data->view,因为下一次其他方式的更新,比如merge,又要开放一条局部merge的命令出来
不可能像dom这样的 通用级别。
模板的写的render 模板 和 模板的实际渲染dom,几乎没两样。所以他们都响应式了。
数据属性拦截:vue
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通知,将变化的组件渲染到页面上;