前端主流框架: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 Vnodetag: 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, # bindingvalueName: "message" # 绑定的数据为:data.message}]}
生成dom的时候,添加对message的拦截监听,数据更新时我们可以找到对应的nodeIndex;
// 假设这是一个生成 DOM 的过程,包括 innerHTML 和事件监听function generateDOM(astObject) {const {dom,binding = []} = astObject;// 生成 DOM,这里假装当前节点是 baseDombaseDom.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、局部属性的变更,拿到key2、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通知,将变化的组件渲染到页面上;
ng:脏检查

