在底层实现上,Vue.js会将模板编译成虚拟DOM渲染函数,渲染函数的执行就会产生最新状态下的vnode,然后使用这个vnode进行重新渲染视图
模板编译的作用:输入模板,输出渲染函数
Vue.js中将模板编译成渲染函数的步骤:
将模板解析为AST 解析器完成遍历AST标记静态节点 优化器完成使用AST生成渲染函数 代码生成器完成备注:AST即抽象语法树,是用于描述一个节点信息的JavaScript对象
整体pipeline:模板->解析器->优化器->代码生成器->渲染函数
解析器的作用:解析器将模板解析成AST,解析器内部又有很多小的解析器,比如HTML解析器,文本解析器,过滤解析器
AST(abstract syntax tree):是用JavaScript中的对象来描述一个节点(对象中的属性保存了节点的各种数据),一个对象表示一个节点
<div> <p>{{name}}</p> </div>以上的html代码转换成AST后
{ tag: "div", type: 1, plain: true, parent: undefined, attrsList: [], attrsMap: {}, children: [ { tag: "p", type: 1, parent: {tag: "div"}, children: [{ type: 2, text: "{{name}}", expression: "_s(name)" }] //... } ] }当很多个独立的节点通过parent属性和children属性连在一起,就变成了一棵树,而这样一个用对象描述的节点树其实就是AST
将节点连成一棵树,或者说构建AST层级关系我们只需要维护一个栈即可,用栈来记录层级关系,栈顶元素一定是下一个入栈元素的父元素,下一个入栈元素就是栈顶元素的子元素
这个层级关系也可以理解为DOM的深度
基于HTML解析器的逻辑,当开始解析一个标签时,把当前节点入栈,当解析结束时,把栈顶元素弹出
那么,我们如何知道什么时候开始解析,什么时候解析结束?-----> 钩子函数
解析器中HTML解析器是最主要的,用于解析HTML,并且在解析过程中会不断触发各种钩子函数
start end chars commet start 解析到一个元素标签开始标签或文本节点时触发end 解析元素结束标签时触发chars 解析文本时触发comment 解析到注释时触发 function createASTElement(tag, attrs, parent){ return { type: 1, tag, attrsList: attrs, parent, children: [] } } parseHTML(template, { start(tag, attrs, unary){ // 参数分别为 标签名,标签的属性以及是否是自闭和标签 let element = createASTElement(tag, attrs, currentParent) } end(){} chars(text){ let element = {type: 3, text} } comment(text){ let element = {type: 3, text, isComment: true} } })当解析到一个开始标签或文本节点时,会产生一个AST节点对象,并把其压入栈中
当解析到一个结束标签时,将栈顶元素弹出
解析HTML过程就是循环匹配的过程,简单来说就是用HTML模板字符串来循环,每轮循环都从HTML代码中截取一小段字符串
如何精确地匹配或截取那些字符串呢? -----> 需要正则表达式的帮助
优化器的作用:遍历AST,检测出所有静态子树(即永远都不会发生变化的DOM节点)并给其打上标记
静态子树是指那些在AST中永远都不会发生变化的节点,比如纯文本节点,并且静态节点的特征是除了自身是静态节点外,它的子节点必须是静态节点
标记静态子树的好处
每次重新渲染时,不需要为静态子树创建新节点,而是克隆已存在的静态子树在虚拟DOM中打补丁的过程中将跳过比对和更新过程优化器内部实现主要分为两个步骤
在AST中找出所有静态节点并打上标记(static)在AST中找出所有静态根节点并打上标记(staticRoot)那么,标记是什么呢?
// 在AST对象中会新增如下两个属性 static: true, staticRoot: false补充知识:关于AST中type类型
type===1 元素节点type===2 带变量的动态文本节点type===3 不带变量的纯文本节点 function isStatic(node){ if (node.type === 2){ return false; } if (node.type === 3){ //不带变量的纯文本节点直接返回是静态节点 return true; } return !!(node.pre || ( !node.hasBindings && !node.if && !node.for && !isBuiltInTag(node.tag) && isPlatformReversedTag(node.tag) && !isDerectChildOfTemplateFor(node) && Object.keys(node).every(isStaticKey) )) } function markStatic(node){ node.static = isStatic(node) if(node.type === 1){ //即元素节点 可能存在子节点 for(let i = 0, l = node.children.length; i < l; i++){ //遍历孩子 const child = node.children[i]; markStatic(child); //递归mark } if (!child.static){ // 如果子节点是动态节点,则其父节点也修改成动态节点 node.static = false; } } }静态节点有个特点就是静态节点的所有子节点也都必须为静态节点,那么我们找到第一个静态节点,就一定是一颗静态子树的根节点
如果找到了第一个静态节点,将其判定为静态根节点,标记staticRoot为true,那么不会继续向他的子级继续寻找
另外:除了节点不是静态节点我们不会标记为静态根节点,还有
如果一个静态节点的子节点只有一个文本节点没有子节点的静态节点我们不会将它标记为静态根节点,因为这两种情况,优化成本大于收益
代码生成器的作用:是模板编译的最后一步,它将AST转换成渲染函数中的内容(这个内容也可以称作代码字符串)
首先看个例子
<div id="el"> <p>Hello {{name}}</p> </div> with(this){return _c("div", {attrs: {"id": "el"}}, [_c("p", [_v("Hello" + _s(name))])])}代码生成器通过AST递归生成代码字符串,可以发现代码字符串是嵌套的函数调用,函数_c中又有执行其他函数
三种节点创建方法与别名
_c createElement 创建元素节点_v createTextVNode 创建文本节点_e createEmptyVNode 创建注释节点其中,_c方法中有三个参数
标签名 String属性 Object子节点列表 Array代码生成器步骤:genElement->genData->genChildren->genNode(genNode中调用genElement,genComment,genText)
// el是AST节点 function genElement(el, state){ // plain是在编译过程中发现的,当节点没有属性,将设置plain为true const data = el.plain ? undefined : genData(el, state); const children = genChildren(el, state) } function genData(el, state){ let data = "{"; if(el.key){ data += `key:${el.key}` } //... 将el中的各属性进行拼接 } function genChildren(el, state){ const children = el.children; if(children.length){ // 生成子节点并以都好拼接 // 模板字符串 return `[${children.map(c=>genNode(c, state)).join(',')}]` } } function genNode(node, state){ if(node.type === 1){ return genElement(node, state); } if(node.type === 3 && node.isComment){ return genComment(node); } if(node.type === 2){ return genText(node); } } function genComment(node){ return `_e(${JSON.stringify(node.text)})`; } function genText(node){ return `_v(${text.type===2? node.expression : JSON.stringify(node.text)})` }JSON.stringify()可以包装一层字符串