深入 Vue2 模板渲染底层思想

2023-05-26 0 504

在采用 Vue2 的操作过程,有时候看 API 极难认知 Vue 译者的价值观,这使得我想去深入细致介绍 Vue 下层的价值观,介绍完下层的许多价值观,就能更快的足用架构,尽管网路上早已有许多源码导出的文件格式,但我真的多于他们亲自动手了,就能更为深第一印象。

Vue2.0 和 1.0 模版图形的差别

Vue 2.0 中模版图形与 Vue 1.0 全然相同,1.0 中选用的 DocumentFragment ,而 2.0 中先进经验 React 的 Virtual DOM。如前所述 Virtual DOM,2.0 还能全力支持服务器端图形(SSR),也全力支持 JSX 句法(升级版的 render 表达式)。

科学知识普及化

在早已开始写作源码以后,先介绍许多相关的科学知识:AST 计算机系统程序,VNode 计算机系统程序,createElement 的难题,render表达式。

AST 计算机系统程序

AST 的全名是 Abstract Syntax Tree(抽象化句法树),是源码的抽象化句法结构的抽象化表达方式,计算机系统学科专业中校对基本原理的基本概念。而vue是将模版标识符态射为AST计算机系统程序,展开句法导出。

他们看呵呵 Vue 2.0 源码中AST 计算机系统程序的表述:

declare type ASTNode = ASTElement | ASTText | ASTExpressiondeclare type ASTElement = { // 相关原素的许多表述 type: 1; tag: string; attrsList: Array<{ name: string; value: string }>; attrsMap: { [key: string]: string | null }; parent: ASTElement | void; children: Array<ASTNode>; //……}declare type ASTExpression = { type: 2; expression: string; text: string; static?: boolean;}declare type ASTText = { type: 3; text: string; static?: boolean;}

他们看到 ASTNode 有三种形式:ASTElement,ASTText,ASTExpression。用属性 type 区分。

VNode 数据结构

下面是 Vue 2.0 源码中VNode 计算机系统程序的表述 (带注释的跟下面介绍的内容相关):

constructor { this.tag = tag //原素标签 this.data = data //属性 this.children = children //子原素列表 this.text = text this.elm = elm //对应的真实 DOM 原素 this.ns = undefined this.context = context this.functionalContext = undefined this.key = data && data.key this.componentOptions = componentOptions this.componentInstance = undefined this.parent = undefined this.raw = false this.isStatic = false //是否被标记为静态节点 this.isRootInsert = true this.isComment = false this.isCloned = false this.isOnce = false}

真实 DOM 存在什么难题,为什么要用虚拟 DOM

他们为什么不直接采用原生 DOM 原素,而是采用真实 DOM 原素的简化版 VNode,最大的原因是 document.createElement 这个方法创建的真实 DOM 原素会带来性能上的损失。他们来看一个 document.createElement 方法的例子

let div = document.createElement(div);for(let k in div) { console.log(k);}

打开 console 运行呵呵上面的标识符,会发现打印出来的属性多达 228 个,而这些属性有 90% 多对他们来说都是无用的。VNode 是简化版的真实 DOM 原素,关联着真实的dom,比如属性elm,只包括他们需要的属性,并新增了许多在 diff 操作过程中需要采用的属性,例如 isStatic。

render 表达式

这个表达式是通过校对模版文件得到的,其运行结果是 VNode。render 表达式 与 JSX 类似,Vue 2.0 中除了 Template 也支持 JSX 的写法。大家能采用Vue.compile(template)方法校对下面这段模版。

<div> <header> <h1>I am a template!</h1> </header> <p v-if=”message”> {{ message }} </p> <p v-else> No message. </p></div>

方法会返回一个对象,对象中有 render 和 staticRenderFns 两个值。看呵呵生成的 render表达式

(function() { with(this){ return _c(div,{ //创建一个 div 原素 attrs:{“id”:”app”} //div 添加属性 id },[ _m(0), //静态节点 header,此处对应 staticRenderFns 数组索引为 0 的 render 表达式 _v(” “), //空的文本节点 (message) //三元表达式,判断 message 是否存在 //如果存在,创建 p 原素,原素里面有文本,值为 toString(message) ?_c(p,[_v(“\n “+_s(message)+”\n “)]) //如果不存在,创建 p 原素,原素里面有文本,值为 No message. :_c(p,[_v(“\n No message.\n “)]) ] ) }})

要看懂上面的 render 表达式,只需要介绍 _c,_m,_v,_s 这几个表达式的表述,其中 _c 是 createElement(创建原素),_m 是 renderStatic(图形静态节点),_v 是 createTextVNode(创建文本 dom),_s 是 toString (转换为字符串)

除了 render 表达式,还有一个 staticRenderFns 数组,这个数组中的表达式与 VDOM 中的 diff 算法优化相关,他们会在校对阶段给后面不会发生变化的 VNode 节点打上 static 为 true 的标签,那些被标记为静态节点的 VNode 就会单独生成 staticRenderFns 表达式

(function() { //上面 render 表达式 中的 _m(0) 会调用这个方法 with(this){ return _c(header,[_c(h1,[_v(“Im a template!”)])]) }})

模版图形操作过程(重要的表达式介绍)

介绍完许多基础科学知识后,接下来他们讲解下模版的图形操作过程

深入 Vue2 模板渲染底层思想

$mount 表达式tions 表达式。

compileToFunctions表达式,主要将 template 校对成 render 表达式。首先读缓存,没有缓存就调用 compile 方法拿到 render 表达式 的字符串形式,再通过 new Function 的方式生成 render 表达式。

// 有缓存的话就直接在缓存里面拿const key = options && options.delimiters ? String(options.delimiters) + template : templateif (cache[key]) { return cache[key]}const res = {}const compiled = compile(template, options) // compile 后面会详细讲res.render = makeFunction(compiled.render) //通过 new Function 的方式生成 render 表达式并缓存const l = compiled.staticRenderFns.lengthres.staticRenderFns = new Array(l)for (let i = 0; i < l; i++) { res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i])}……}return (cache[key] = res) // 记录至缓存中

compile表达式是将 template 校对成 render 表达式的字符串形式,后面一小节他们会详细讲到。

完成render方法的生成后,会进入_mount中展开DOM更新。该方法的核心逻辑如下:

// 触发 beforeMount 生命周期钩子callHook(vm, beforeMount)// 重点:新建一个 Watcher 并赋值给 vm._watchervm._watcher = new Watcher(vm, function updateComponent () { vm._update(vm._render(), hydrating)}, noop)hydrating = false// manually mounted instance, call mounted on self// mounted is called for render-created child components in its inserted hookif (vm.$vnode == null) { vm._isMounted = true callHook(vm, mounted)}return vm

首先会 new 一个 watcher 对象(主要是将模版与数据建立联系),在watcher对象创建后,会运行传入的方法 vm._update(vm._render(), hydrating) 。其中的vm._render()主要作用是运行前面 compiler 生成的render方法,并返回一个vNode对象。vm.update() 则会对比新的 vdom 和当前 vdom,并把差异的部分图形到真正的 DOM 树上。

compile

上文中提到 compile 表达式是将 template 校对成 render 表达式 的字符串形式。

export function compile ( template: string, options: CompilerOptions): CompiledResult { const AST = parse(template.trim(), options) //1. parse optimize(AST, options) //2.optimize const code = generate(AST, options) //3.generate return { AST, render: code.render, staticRenderFns: code.staticRenderFns }}

这个表达式主要有三个步骤组成:parse,optimize 和 generate,分别输出一个包含 AST,staticRenderFns 的对象和 render表达式 的字符串。

parse表达式,主要功能是将 template字符串导出成 AST。前面表述了ASTElement的计算机系统程序,parse 表达式是将template里的结构(指令,属性,标签等)转换为AST形式存进ASTElement中,最后导出生成AST。

optimize表达式(src/compiler/optimizer.js)主要功能是标记静态节点,为后面 patch 操作过程中对比新旧 VNode 树形结构做优化。被标记为 static 的节点在后面的 diff 算法中会被直接忽略,不做详细的比较。

generate表达式(src/compiler/codegen/index.js)主要功能是根据 AST 结构拼接生成 render 表达式的字符串。

const code = AST ? genElement(AST) : _c(“div”) staticRenderFns = prevStaticRenderFnsonceCount = prevOnceCountreturn { render: `with(this){return ${code}}`, //最外层包一个 with(this) 之后返回 staticRenderFns: currentStaticRenderFns}

其中genElement表达式(src/compiler/codegen/index.js)是会根据 AST 的属性调用相同的方法生成字符串返回。

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 code }}

以上是 compile 表达式中三个核心步骤的介绍,compile 之后他们得到了 render 表达式 的字符串形式,后面通过 new Function 得到真正的图形表达式。数据发现变化后,会执行 Watcher 中的_update表达式(src/core/instance/lifecycle.js),_update 表达式会执行这个图形表达式,输出一个新的 VNode 树形结构的数据。然后在调用 patch 表达式,拿这个新的 VNode 与旧的 VNode 展开对比,多于发生了变化的节点才会被更新到真实 DOM 树上。

patch

OM 价值观的核心。snabbdom 的算法为了 DOM 操作跨层级增删节点较少的这一目标展开优化,它只会在同层级展开, 不会跨层级比较。

想更为深入细致 VNode diff 算法基本原理的,能观看(导出 Vue2.0 的 diff 算法

总结

compile 表达式主要是将 template 转换为 AST,优化 AST,再将 AST 转换为 render表达式;render表达式 与数据通过 Watcher 产生关联;在数据发生变化时调用 patch 表达式,执行此 render 表达式,生成新 VNode,与旧 VNode 展开 diff,最终更新 DOM 树。
举报/反馈

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务