没有网站可以做百度快照怎么做,网站制作论文文献综述,菜鸟html教程,wordpress七牛云cdn本文转载于 SegmentFault 社区社区专栏#xff1a;山外de楼作者#xff1a;山外de楼前言 vue 是如何将编译器中的代码转换为页面真实元素的#xff1f;这个过程涉及到模板编译成 AST 语法树#xff0c;AST 语法树构建渲染函数#xff0c;渲染函数生成虚拟 dom#xff0c;… 本文转载于 SegmentFault 社区社区专栏山外de楼作者山外de楼 前言 vue 是如何将编译器中的代码转换为页面真实元素的这个过程涉及到模板编译成 AST 语法树AST 语法树构建渲染函数渲染函数生成虚拟 dom虚拟 dom 编译成真实 dom 这四个过程。本文着重分析后两个过程。 整体流程解读代码之前先看一张 vue 编译和渲染的整体流程图:vue 会把用户写的代码中的 标签中的代码解析成 AST 语法树再将处理后的 AST 生成相应的 render 函数render 函数执行后会得到与模板代码对应的虚拟 dom最后通过虚拟 dom 中新旧 vnode 节点的对比和更新渲染得到最终的真实 dom。有了这个整体的概念我们再来结合源码分析具体的数据渲染过程。 从 vm.$mount 开始vue 中是通过 mount 实例方法去挂载 vm 的数据渲染的过程就发生在vm.mount 阶段。在这个方法中最终会调用 mountComponent方法来完成数据的渲染。我们结合源码看一下其中的几行关键代码 updateComponent () { vm._update(vm._render(), hydrating) // 生成虚拟dom并更新真实dom }这是在 mountComponent 方法的内部会定义一个 updateComponent方法在这个方法中 vue 会通过 vm._render()函数生成虚拟 dom并将生成的 vnode 作为第一个参数传入 vm._update()函数中进而完成虚拟 dom 到真实 dom 的渲染。第二个参数 hydrating是跟服务端渲染相关的在浏览器中不需要关心。这个函数最后会作为参数传入到 vue 的 watch 实例中作为 getter函数用于在数据更新时触发依赖收集完成数据响应式的实现。这个过程不在本文的介绍范围内在这里只要明白当后续 vue 中的 data 数据变化时都会触发 updateComponent 方法完成页面数据的渲染更新。具体的关键代码如下 new Watcher(vm, updateComponent, noop, {before () {if (vm._isMounted !vm._isDestroyed) {// 触发beforeUpdate钩子 callHook(vm, beforeUpdate) } } }, true /* isRenderWatcher */) 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// 触发mounted钩子 callHook(vm, mounted) }return vm}代码中还有一点需要注意的是在代码结束处会做一个判断当 vm 挂载成功后会调用 vue 的 mounted 生命周期钩子函数。这也就是为什么我们在 mounted 钩子中执行代码时vm 已经挂载完成的原因。 vm._render()接下来具体分析 vue 生成虚拟 dom 的过程。前面说了这一过程是调用 vm._render()方法来完成的该方法的核心逻辑是调用 vm.$createElement 方法生成 vnode代码如下vnode render.call(vm._renderProxy, vm.$createElement)其中 vm.renderProxy是个代理代理 vm做一些错误处理vm.$createElement是创建 vnode 的真正方法该方法的定义如下vm.$createElement (a, b, c, d) createElement(vm, a, b, c, d, true)可见最终调用的是 createElement方法来实现生成 vnode 的逻辑。在进一步介绍 createElement 方法之前我们先理清楚两个个关键点1. render的函数来源2. vnode到底是什么render 方法的来源在 vue 内部其实定义了两种 render 方法的来源一种是如果用户手写了 render 方法那么 vue 会调用这个用户自己写的 render 方法即下面代码中的 vm.$createElement另外一种是用户没有手写 render 方法那么vue内部会把 template 编译成 render 方法即下面代码中的 vm._c。不过这两个 render 方法最终都会调用 createElement 方法来生成虚拟 dom。// bind the createElement fn to this instance// so that we get proper render context inside it.// args order: tag, data, children, normalizationType, alwaysNormalize// internal version is used by render functions compiled from templates vm._c (a, b, c, d) createElement(vm, a, b, c, d, false)// normalization is always applied for the public version, used in// user-written render functions. vm.$createElement (a, b, c, d) createElement(vm, a, b, c, d, true) vnode 类vnode 就是用一个原生的 js 对象去描述 dom 节点的类。因为浏览器操作 dom 的成本是很高的所以利用 vnode 生成虚拟 dom 比创建一个真实 dom 的代价要小很多。vnode 类的定义如下export default class VNode { tag: string | void; // 当前节点的标签名 data: VNodeData | void; // 当前节点对应的对象 children: ?Array; // 当前节点的子节点 text: string | void; // 当前节点的文本 elm: Node | void; // 当前虚拟节点对应的真实dom节点 ..../*创建一个空VNode节点*/export const createEmptyVNode (text: string ) {const node new VNode() node.text text node.isComment truereturn node }/*创建一个文本节点*/export function createTextVNode (val: string | number) {return new VNode(undefined, undefined, undefined, String(val)) } ....可以看到 vnode 类中仿照真实 dom 定义了很多节点属性和一系列生成各类节点的方法。通过对这些属性和方法的操作来达到模仿真实 dom 变化的目的。createElement有了前面两点的知识储备接下来回到 createElement 生成虚拟 dom 的分析。createElement 方法中的代码很多这里只介绍跟生成虚拟 dom 相关的代码。该方法总体来说就是创建并返回一个 vnode 节点。在这个过程中可以拆分成三件事情1. 子节点的规范化处理2. 根据不同的情形创建不同的 vnode 节点类型3. vnode 创建后的处理。下面开始分析这 3 个步骤子节点的规范化处理 if (normalizationType ALWAYS_NORMALIZE) { children normalizeChildren(children) } else if (normalizationType SIMPLE_NORMALIZE) { children simpleNormalizeChildren(children) }为什么会有这个过程是因为传入的参数中的子节点是 any 类型而 vue 最终生成的虚拟 dom 实际上是一个树状结构每一个 vnode 可能会有若干个子节点这些子节点应该也是 vnode 类型。所以需要对子节点处理将子节点统一处理成一个 vnode 类型的数组。同时还需要根据 render 函数的来源不同对子节点的数据结构进行相应处理。创建 vnode 节点这部分逻辑是对 tag 标签在不同情况下的处理梳理一下具体的判断case如下1. 如果传入的 tag 标签是字符串则进一步进入下列第 2 点和第 3 点判断如果不是字符串则创建一个组件类型 vnode 节点。2. 如果是内置的标签则创建一个相应的内置标签 vnode 节点。3. 如果是一个组件标签则创建一个组件类型 vnode 节点。4. 其他情况下则创建一个命名空间未定义的 vnode 节点。 let vnode, nsif (typeof tag string) {let Ctor// 获取tag的名字空间 ns (context.$vnode context.$vnode.ns) || config.getTagNamespace(tag)// 判断是否是内置的标签如果是内置的标签则创建一个相应节点if (config.isReservedTag(tag)) {// platform built-in elementsif (process.env.NODE_ENV ! production isDef(data) isDef(data.nativeOn)) { warn(The .native modifier for v-on is only valid on components but it was used on ., context ) } vnode new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context )// 如果是组件则创建一个组件类型节点// 从vm实例的option的components中寻找该tag存在则就是一个组件创建相应节点Ctor为组件的构造类 } else if ((!data || !data.pre) isDef(Ctor resolveAsset(context.$options, components, tag))) {// component vnode createComponent(Ctor, data, context, children, tag) } else {// unknown or unlisted namespaced elements// check at runtime because it may get assigned a namespace when its// parent normalizes children//其他情况在运行时检查因为父组件可能在序列化子组件的时候分配一个名字空间 vnode new VNode(tag, data, children, undefined, undefined, context ) } } else {// direct component options / constructor// tag不是字符串的时候则是组件的构造类创建一个组件节点 vnode createComponent(tag, data, context, children) }vnode 创建后的处理这部分同样也是一些 if/else 分情况的处理逻辑1. 如果 vnode 成功创建且是一个数组类型则返回创建好的 vnode 节点2. 如果 vnode 成功创建且有命名空间则递归所有子节点应用该命名空间3. 如果 vnode 没有成功创建则创建并返回一个空的 vnode 节点 if (Array.isArray(vnode)) {// 如果vnode成功创建且是一个数组类型则返回创建好的vnode节点return vnode } else if (isDef(vnode)) {// 如果vnode成功创建且名字空间则递归所有子节点应用该名字空间if (isDef(ns)) applyNS(vnode, ns)if (isDef(data)) registerDeepBindings(data)return vnode } else {// 如果vnode没有成功创建则创建空节点return createEmptyVNode() }vm._update()vm._update()做的事情就是把 vm._render()生成的虚拟 dom 渲染成真实 dom。_update()方法内部会调用 vm.__patch__ 方法来完成视图更新最终调用的是 createPatchFunction方法该方法的代码量和逻辑都非常多它定义在 src/core/vdom/patch.js文件中。下面介绍下具体的 patch 流程和流程中用到的重点方法重点方法1. createElm该方法会根据传入的虚拟 dom 节点创建真实的 dom 并插入到它的父节点中2. sameVnode判断新旧节点是否是同一节点。3. patchVnode当新旧节点是相同节点时调用该方法直接修改节点在这个过程中会利用 diff 算法循环进行子节点的的比较,进而进行相应的节点复用或者替换。4. updateChildren方法diff 算法的具体实现过程 patch 流程第一步判断旧节点是否存在如果不存在就调用 createElm() 创建一个新的 dom 节点否则进入第二步判断。 if (isUndef(oldVnode)) {// empty mount (likely as component), create new root element isInitialPatch true createElm(vnode, insertedVnodeQueue) }第二步通过 sameVnode() 判断新旧节点是否是同一节点如果是同一个节点则调用 patchVnode() 直接修改现有的节点否则进入第三步判断。const isRealElement isDef(oldVnode.nodeType)if (!isRealElement sameVnode(oldVnode, vnode)) {// patch existing root node/*是同一个节点的时候直接修改现有的节点*/ patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)}第三步如果新旧节点不是同一节点则调用 createElm()创建新的 dom并更新父节点的占位符同时移除旧节点。else { .... createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition // keep-alive HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // update parent placeholder node element, recursively/*更新父的占位符节点*/if (isDef(vnode.parent)) {let ancestor vnode.parent const patchable isPatchable(vnode)while (ancestor) {for (let i 0; i cbs.destroy.length; i) { cbs.destroy[i](ancestor) /*调用destroy回调*/ } ancestor.elm vnode.elm if (patchable) { for (let i 0; i cbs.create.length; i) { cbs.create[i](emptyNode, ancestor) /*调用create回调*/ } // #6513 // invoke insert hooks that may have been merged by create hooks. // e.g. for directives that uses the inserted hook. const insert ancestor.data.hook.insertif (insert.merged) { // start at index 1 to avoid re-invoking component mounted hookfor (let i 1; i insert.fns.length; i) {insert.fns[i]() } } } else { registerRef(ancestor) } ancestor ancestor.parent } } // destroy old nodeif (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) /* 删除旧节点 */ } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) /* 调用destroy钩子 */ }}第四步返回 vnode.elm即最后生成的虚拟 dom 对应的真实 dom将 vm.$el 赋值为这个 dom 节点完成挂载。其中重点的过程在第二步和第三步中特别是 diff 算法对新旧节点的比较和更新很有意思。其他注意点sameVnode 的实际应用在 patch 的过程中如果两个节点被判断为同一节点会进行复用。这里的判断标准是1. key 相同2. tag(当前节点的标签名)相同3. isComment(是否为注释节点)相同4. data 的属性相同平时写 vue 时会遇到一个组件中用到了 A 和 B 两个相同的子组件可以来回切换。有时候会出现改变了 A 组件中的值切到 B 组件中发现 B 组件的值也被改变成和 A 组件一样了。这就是因为 vue 在 patch 的过程中判断出了 A 和 B 是 sameVnode直接进行复用引起的。根据源码的解读可以很容易地解决这个问题就是给 A 和 B 组件分别加上不同的 key 值避免 A 和 B 被判断为同一组件。虚拟 DOM 如何映射到真实的 DOM 节点vue 为平台做了一层适配层浏览器平台的代码在 /platforms/web/runtime/node-ops.js。不同平台之间通过适配层对外提供相同的接口虚拟 dom 映射转换真实 dom 节点的时候只需要调用这些适配层的接口即可不需要关心内部的实现。最后通过上述的源码和实例的分析我们完成了 Vue 中数据渲染的完整解读。- END -