Vue编译优化实现流程详解
作者:volit_
动态节点收集与补丁标志
1.传统diff算法的问题
对于一个普通模板文件,如果只是标签中的内容发生了变化,那么最简单的更新方法很明显是直接替换标签中的文本内容。但是diff算法很明显做不到这一点,它会重新生成一棵虚拟DOM树,然后对两棵虚拟DOM树进行比较。很明显,与直接替换标签中的内容相比,传统diff算法需要做很多无意义的操作,如果能够去除这些无意义的操作,将会省下一笔很大的性能开销。其实,只要在模板编译时,标记出哪些节点是动态的,哪些是静态的,然后再通过虚拟DOM传递给渲染器,渲染器就能根据这些信息,直接修改对应节点,从而提高运行时性能。
2.Block和PatchFlags
对于一个传统的模板:
<div> <div> foo </div> <p> {{ bar }} </p> </div>
在这个模板中,只用{{ bar }}是动态内容,因此在bar变量发生变化时,只需要修改p标签内的内容就行了。因此我们在这个模板对于的虚拟DOM中,加入patchFlag属性,以此来标签模板中的动态内容。
const vnode = { tag: 'div', children: [ { tag: 'div', children: 'foo' }, { tag: 'p', children: ctx.bar, patchFlag: 1 }, ] }
对于不同的数值绑定,我们分别用不同的patch值来表示:
- 数字1,代表节点有动态的textContent
- 数字2,代表节点有动态的class绑定
- 数字3,代表节点有动态的style绑定
- 数字4,其他…
我们可以新建一个枚举类型来表示这些值:
enum PatchFlags { TEXT: 1, CLASS, STYLE, OTHER }
这样我们就在虚拟DOM的创建阶段,将动态节点提取出来:
const vnode = { tag: 'div', children: [ { tag: 'div', children: 'foo' }, { tag: 'p', children: ctx.bar, patchFlag: PatchFlags.TEXT }, ], dynamicChildren: [ { tag: 'p', children: ctx.bar, patchFlag: PatchFlags.TEXT }, ] }
3.收集动态节点
首先我们创建收集动态节点的逻辑。
const dynamicChildrenStack = []; // 动态节点栈 let currentDynamicChildren = null; // 当前动态节点集合 function openBlock() { // 创建一个新的动态节点栈 dynamicChildrenStack.push((currentDynamicChildren = [])); } function closeBlock() { // openBlock创建的动态节点集合弹出 currentDynamicChildren = dynamicChildrenStack.pop(); }
然后,我们在创建虚拟节点的时候,对动态节点进行收集。
function createVNode(tag, props, children, flags) { const key = props && props.key; props && delete props.key; const vnode = { tag, props, children, key, patchFlags: flags } if(typeof flags !== 'undefined' && currentDynamicChildren) { currentDynamicChildren.push(vnode); } return vnode; }
然后我们修改组件渲染函数的逻辑。
render() { return (openBlock(), createBlock('div', null, [ createVNode('p', { class: 'foo' }, null, 1), createVNode('p', { class: 'bar' }, null) ])); } function createBlock(tag, props, children) { const block = createVNode(tag, props, children); block.dynamicChildren = currentDynamicChildren; closeBlock(); return block; }
4.渲染器运行时支持
function patchElement(n1, n2) { const el = n2.el = n1.el; const oldProps = n1.props; const newProps = n2.props; // ... if(n2.dynamicChildren) { // 如果有动态节点数组,直接更新动态节点数组 patchBlockChildren(n1, n2); } else { patchChildren(n1, n2, el); } } function pathcBlockChildren(n1, n2) { for(let i = 0; i < n2.dynamicChildren.length; i++) { patchElement(n1.dynamicChildren[i], n2.dynamicChildren[i]); } }
由于我们标记了不同的动态节点类型,因此我们可以针对性的完成靶向更新。
function patchElement(n1, n2) { const el = n2.el = n1.el; const oldProps = n1.props; const newProps = n2.props; if(n2.patchFlags) { if(n2.patchFlags === 1) { // 只更新内容 } else if(n2.patchFlags === 2) { // 只更新class } else if(n2.patchFlags === 3) { // 只更新style } else { // 更新所有 for(const k in newProps) { if(newProps[key] !== oldProps[key]) { patchProps(el, key, oldProps[k], newProps[k]); } } for(const k in oldProps) { if(!key in newProps) { patchProps(el, key, oldProps[k], null); } } } } patchChildren(n1, n2, el); }
5.Block树
组件的根节点必须作为Block角色,这样,从根节点开始的所有动态子代节点都会被收集到根节点的dynamicChildren数组中。除了根节点外,带有v-if、v-for这种结构化指令的节点,也会被作为Block角色,这些Block角色共同构成一棵Block树。
静态提升
假设有以下模板
<div> <p> static text </p> <p> {{ title }} </p> </div>
默认情况下,对应的渲染函数为:
function render() { return (openBlock(), createBlock('div', null, [ createVNode('p', null, 'static text'), createVNode('p', null, ctx.title, 1 /* TEXT */) ])) }
在这段代码中,当ctx.title属性变化时,内容为静态文本的p标签节点也会跟着渲染一次,这很明显式不必要的。因此,我们可以使用“静态提升”,即将静态节点,提取到渲染函数之外,这样渲染函数在执行的时候,只是保持了对静态节点的引用,而不会重新创建虚拟节点。
const hoist1 = createVNode('p', null, 'static text'); function render() { return (openBlock(), createBlock('div', null, [ hoist1, createVNode('p', null, ctx.title, 1 /* TEXT */) ])) }
除了静态节点,对于静态props我们也可以将其进行静态提升处理。
const hoistProps = { foo: 'bar', a: '1' }; function render() { return (openBlock(), createBlock('div', null, [ hoist1, createVNode('p', hoistProps, ctx.title, 1 /* TEXT */) ])) }
预字符化
除了对节点进行静态提升外,我们还可以对于纯静态的模板进行预字符化。对于这样一个模板:
<templete> <p></p> <p></p> <p></p> <p></p> <p></p> ... <p></p> <p></p> <p></p> <p></p> </templete>
我们完全可以将其预处理为:
const hoistStatic = createStaticVNode('<p></p><p></p><p></p><p></p>...<p></p><p></p><p></p><p></p>'); render() { return (openBlock(), createBlock('div', null, [ hoistStatic ])); }
这么做的优势:
- 大块的静态内容可以通过innerHTML直接设置,在性能上具有一定优势
- 减少创建虚拟节点带来的额外开销
- 减少内存占用
缓存内联事件处理函数
当为组件添加内联事件时,每次新建一个组件,都会为该组件重新创建并绑定一个新的内联事件函数,为了避免这方面的无意义开销,我们可以对内联事件处理函数进行缓存。
function render(ctx, cache) { return h(Comp, { onChange: cache[0] || cache[0] = ($event) => (ctx.a + ctx.b); }) }
v-once
v-once指令可以是组件只渲染一次,并且即使该组件绑定了动态参数,也不会更新。它与内联事件一样,也是使用了缓存,同时通过setBlockTracking(-1)阻止该VNode被Block收集。
v-once的优点:
- 避免组件更新时重新创建虚拟DOM带来的性能开销
- 避免无用的Diff开销
到此这篇关于Vue编译优化实现流程详解的文章就介绍到这了,更多相关Vue编译优化内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!