精华内容
下载资源
问答
  • 主要介绍了Vue 中的compile操作方法,非常不错,具有参考借鉴价值,需要的朋友参考下吧
  • Vue.extend、Vue.component、new Vue以及Vue.compile1、Vue.extend(options):2、Vue.component(options):3、new Vue创建==根实例== Vue.extend(vue扩展构造器)、Vue.component(vue全局组件注册)、new Vue(创建vue...

    Vue.extend、Vue.component、new Vue以及动态编译Vue.compile

    先说说Vue.extend(vue扩展构造器)、Vue.component(vue全局组件注册)、new Vue(创建vue根实例)三者之间的关系。

    野路子理解:

    1、Vue.extend(options):

    1、根据官方文档的描述是用来创建Vue“子类”的api,也就是创建一个新的构造函数
    2、该构造函数里面包含了对Vue初始化函数的调用,另外Vue.extend里通过Object.create()以Vue的原型对象为原型创建了新的对象当做该构造函数的原型,这样做的好处是更改原型属性时,避免对Vue的原型造成污染同时又继承了Vue的原型属性。
    3、options是共有的选项数据,也就是只要由该构造函数创建的实例都会有这些选项,和mixin一毛一样。另外实例初始化时还可以传递实例单独的options选项。
    4、因为生成的构造函数在new调用时,函数里调用了Vue的初始化方法,所以生成的是一个正了八经的Vue实例对象。

    2、Vue.component(options):

    1、用来注册全局组件,官网上有三种用法:

    // 1、注册组件,传入一个扩展过的构造器
    Vue.component(‘my-component’, Vue.extend({ /* … */ }))

    // 2、注册组件,传入一个选项对象 (自动调用 Vue.extend)
    Vue.component(‘my-component’, { /* … */ })

    // 3、获取注册的组件 (始终返回构造器)
    var MyComponent = Vue.component(‘my-component’)

    1、根据第1、2用法再加上options参数和Vue.extend的options可以一毛一样,由此得出注册组件就是根据Vue.extend的选项options创建一个新的构造函数,和注册时填写的id名称一一对应。
    2、可以这么理解:每一次组件实例化,其实都是根据这个新的构造函数来创建。当然实际要考虑的更多。

    3、new Vue()

    官方文档中经常见到:

    通过 new Vue 创建的根 Vue 实例

    刚开始以为new Vue()才是根实例,后来看了源码发现,这只是代表了一个组件树结构的根组件。不信可以测一下 Vue.extend返回的构造函数创建的实例也可以是根实例。
    不过通常应用中的根实例都是通过new Vue()创建的:
    1、spa应用的根实例只有一个。
    2、稍微老点的系统可能一个页面一个根实例。

    4、Vue.compile

    这个api不常用,但是用起来也挺爽。根据一次实际应用来介绍它。
    需求:
    根据配置页面的模板字符串动态改变页面显示内容。
    比如模板字符串可能配成以下几种:
    this.templateStr=

    1、“item.name”;
    2、“getName(item.id)”;
    3、“‘我的名字是’+getName(item.id)”;

    上面三种都是vue中的模板表达式,由于都是字符串形式,所以这里就可以用Vue.compile来编译,具体如下:

    	// 组合要编译的字符串
    	const templateStr = `<span>{{this.templateStr}}</span>`;
    	// 调用Vue.compile获取render函数
    	const templateRes = Vue.compile(templateStr);
    	// render函数执行需要Vue实例,否则报错,并且,模板中的各种属性、方法都会去这个实例里面找,所以。。。实例可以随便搞了
    	// 这样
    	const node1 = templateRes.render.call(this);
    	// 这样
    	const Ctor = Vue.extend();
    	const vueInstance = new Ctor({
    		data: function() {
    			return {
    				item: {
    					name: 'zhangsan',
    					id: '1'
    				}
    			}
    		},
    		methods: {
    			getName(id){
    				return 'zhangsan' + id;
    			}
    		}
    	});
    	const node2 = templateRes.render.call(vueInstance);
    

    代码中node1、node2就是vnode实例。
    提示:render函数执行需要Vue实例,否则报错,并且,模板中的各种属性、方法都会去这个实例里面找。

    展开全文
  • 前言 在上一篇文章中,我们分析了在编译过程静态节点的提升。...那么,今天,我们就来揭秘「Vue3」compile 和 runtime 结合的 patch过程 究竟是如何实现的! 什么是 shapeFlag 说起「Vue3」的 patc

    前言

    在上一篇文章中,我们分析了在编译过程静态节点的提升。并且,在文章的结尾也说了,下一篇文章将会介绍 patch 过程。

    说起「Vue3」的 patch 过程,其中最为津津乐道的就是靶向更新。靶向更新,顾名思义,即更新的过程是带有目标性的直接性的。而,这也是和静态节点提升一样,是「Vue3」针对 VNode 更新性能问题的一大优化。

    那么,今天,我们就来揭秘「Vue3」compile 和 runtime 结合的 patch过程 究竟是如何实现的!

    什么是 shapeFlag

    说起「Vue3」的 patch,老生常谈的就是 patchFlag。所以,对于 shapeFlag 我想大家可能有点蒙,这是啥?

    ShapeFlag 顾名思义,是对具有形状的元素进行标记,例如普通元素、函数组件、插槽、keep alive 组件等等。它的作用是帮助 Rutime 时的 render 的处理,可以根据不同 ShapeFlag 的枚举值来进行不同的 patch 操作。

    在「Vue3」源码中 ShapeFlagpatchFlag 一样被定义为枚举类型,每一个枚举值以及意义会是这样:

    组件创建过程

    了解过「Vue2.x」源码的同学应该知道第一次 patch 的触发,就是在组件创建的过程。只不过此时,oldVNodenull,所以会表现为挂载的行为。因此,在认知靶向更新的过程之前,不可或缺地是我们需要知道组件是怎么创建的

    既然说 patch 的第一次触发会是组件的创建过程,那么在「Vue3」中组件的创建过程会是怎么样的?它会经历这么四个过程

    在之前,我们讲过 compile 编译过程会将我们的 template 转化为可执行代码,即 render 函数。而,compiler 生成的 render 函数会绑定在当前组件实例render 属性上。例如,此时有这样的 template 模板:

    <div><div>hi vue3</div><div>{{msg}}</div></div>
    

    它经过 compile 编译处理后生成的 render 函数会是这样:

    const _Vue = Vue
    const _hoisted_1 = _createVNode("div", null, "hi vue3", -1 /* HOISTED */)
    
    function render(_ctx, _cache) {
      with (_ctx) {
        const { createVNode: _createVNode, toDisplayString: _toDisplayString, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
    
        return (_openBlock(), _createBlock(_Fragment, null, [
          _createVNode("div", null, [
            _hoisted_1,
            _createVNode("div", null, _toDisplayString(msg), 1 /* TEXT */)
          ])
        ]))
      }
    }
    

    这个 render 函数真正执行的时机是在安装全局的渲染函数对应 effect 的时候,即 setupRenderEffect。而渲染 effect 会在组件创建时更新时触发

    这个时候,可能又有同学会问什么是 effecteffect 并不是「Vue3」的新概念,它的本质是「Vue2.x」源码中的 watcher,同样地,effect也会负责依赖收集派发更新

    有兴趣了解「Vue3」依赖收集和派发更新过程的同学可以看一下这篇文章4k+ 字分析 Vue 3.0 响应式原理(依赖收集和派发更新)

    setupRenderEffect 函数对应的伪代码会是这样:

    function setupRenderEffect() {
        instance.update = effect(function componentEffect() {
          // 组件未挂载
          if (!instance.isMounted) {
            // 创建组件对应的 VNode tree
            const subTree = (instance.subTree = renderComponentRoot(instance))
            ...
            instance.isMounted = true
          } else {
            // 更新组件
            ...
          }
      }
    

    可以看到,组件的创建会命中 renderComponentRoot(instance) 的逻辑,此时 renderComponentRoot(instance) 会调用 instance 上的 render 函数,然后为当前组件实例构造整个 VNode Tree,即这里的 subTreerenderComponentRoot 函数对应的伪代码会是这样:

    function renderComponentRoot(instance) {
      const {
        ...
        render,
        ShapeFlags,
        ...
      } = instance
      if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
        ...
        result = normalizeVNode(
          render!.call(
            proxyToUse,
            proxyToUse!,
            renderCache,
            props,
            setupState,
            data,
            ctx
          )
        )
        ...
      }
    }
    

    可以看到,在 renderComponentRoot 中,如果当前 ShapeFlagsSTATEFUL_COMPONENT 时会命中调用 render 的逻辑。这里的 render 函数,就是上面我们所说的 compile 编译后生成的可执行代码。它最终会返回一个 VNode Tree,它看起来会是这样:

    {
      ...
      children: (2) [{}, {}],
      ...
      dynamicChildren: (2) [{}, {}],
      ...
      el: null,
      key: null,
      patchFlag: 64,
      ...
      shapeFlag: 16,
      ...
      type: Symbol(Fragment),
      ...
    }
    

    了解过何为靶向更新的同学应该知道,它的实现离不开 VNode Tree 上的 dynamicChildren 属性,dynamicChildren 则是用来承接整个 VNode Tree 中的所有动态节点, 而标记动态节点的过程又是在 compile 编译的 transform 阶段,可以说是环环相扣,所以,这也是我们常说的「Vue3」RuntimeCompile巧妙结合

    显然在「Vue2.x」是不具备构建 VNodedynamicChildren 属性的条件。那么,「Vue3」又是如何生成的 dynamicChildren

    Block VNode 创建过程

    Block VNode

    Block VNode 是「Vue3」针对靶向更新而提出的概念,它的本质是动态节点对应的 VNode。而,VNode 上的 dynamicChildren 属性则是衍生于 Block VNode,因此,它也就是充当着靶向更新中的靶的角色

    这里,我们再回到前面所提到的 compiler 编译时生成 render 函数,它返回的结果:

    (_openBlock(), _createBlock(_Fragment, null, [
      _createVNode("div", null, [
        _hoisted_1,
        _createVNode("div", null, _toDisplayString(msg), 1 /* TEXT */)
      ])
    ]))
    

    需要注意的是 openBlock 必须写在 createBlock 之前,因为在 Block Tree 中的 Children 总是会在 createBlock 之前执行。

    可以看到有两个和 Block 相关的函数:_openBlock()_createBlock()。实际上,它们分别对应着源码中的 openBlock()_createBlock() 函数。那么,我们分别来认识一下这两者:

    openBlock

    openBlock 会为当前 Vnode 初始化一个数组 currentBlock 来存放 BlockopenBlock 函数的定义十分简单,会是这样:

    function openBlock(disableTracking = false) {
        blockStack.push((currentBlock = disableTracking ? null : []));
    }
    

    openBlock 函数会有一个形参 disableTracking,它是用来判断是否初始化 currentBlock。那么,在什么情况下不需要创建 currentBlock

    当存在 v-for 形成的 VNode 时,它的 render 函数中的 openBlock() 函数形参 disableTracking 就是 true。因为,它不需要靶向更新,来优化更新过程,即它在 patch 时会经历完整的 diff 过程。

    换个角理解,为什么这么设计?靶向更新的本质是为了从一颗存在动态、静态节点的 VNode Tree 中筛选出动态的节点形成 Block Tree,即 dynamicChildren,然后在 patch 时实现精准、快速的更新。所以,显然 v-for 形成的 VNode Tree 它不需要靶向更新

    这里,大家可能还会有一个疑问,为什么创建好的 Block VNode 又被 push 到了 blockStack 中?它又有什么作用?有兴趣的同学可以去试一下 v-if 场景,它最终会构造一个 Block Tree,有兴趣的同学可以看一下这篇文章Vue3 Compiler 优化细节,如何手写高性能渲染函数

    createBlock

    createBlock 则负责创建 Block VNode,它会调用 createVNode 方法来依次创建 Block VNodecreateBlock 函数的定义:

    function createBlock(type, props, children, patchFlag, dynamicProps) {
        const vnode = createVNode(type, props, children, patchFlag, dynamicProps, true);
        // 构造 `Block Tree`
        vnode.dynamicChildren = currentBlock || EMPTY_ARR;
        closeBlock();
        if (shouldTrack > 0 && currentBlock) {
            currentBlock.push(vnode);
        }
        return vnode;
    }
    

    可以看到在 createBlock 中仍然会调用 createVNode 创建 VNode。而 createVNode 函数本质上调用的是源码中的 _createVNode 函数,它的类型定义看起来会是这样:

    function _createVNode(
      type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
      props: (Data & VNodeProps) | null = null,
      children: unknown = null,
      patchFlag: number = 0,
      dynamicProps: string[] | null = null,
      isBlockNode = false
    ): VNode {}
    

    当我们调用 _createVNode() 创建 Block VNode 时,需要传入的 isBlockNodetrue,它用来标识当前 VNode 是否为 Block VNode,从而避免 Block VNode 依赖自己的情况发生,即就不会将当前 VNode 加入到 currentBlock 中。其对应的伪代码会是这样:

    function _createVNode() {
      ...
      if (
        shouldTrack > 0 &&
        !isBlockNode &&
        currentBlock &&
        patchFlag !== PatchFlags.HYDRATE_EVENTS &&
        (patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT)
      ) {
        currentBlock.push(vnode)
      }
      ...
    }
    

    所以,只有满足上面的 if 语句中的所有条件的 VNode,才能作为 Block Node,它们对应的具体含义会是这样:

    • sholdTrack 大于 0,即没有 v-once 指令下的 VNode
    • isBlockNode 是否为 Block Node
    • currentBlock 为数组时才创建 Block Node,对于 v-for 场景下,curretBlocknull,它不需要靶向更新。
    • patchFlag 有意义且不为 32 事件监听,只有事件监听情况时事件监听会被缓存。
    • shapeFlags 是组件的时候,必须为 Block Node,这是为了保证下一个 VNode 的正常卸载。

    至于,再深一层次探索为什么?有兴趣的同学可以自行去了解。

    小结

    讲完 VNode 的创建过程,我想大家都会意识到一点,如果使用手写 render 函数的形式开发,我们就需要对 createBlockopenBlock 等函数的概念有一定的认知。因为,只有这样,我们写出的 render 函数才能充分利用好靶向更新过程,实现的应用更新性能也是最好的

    patch 过程

    对比 Vue2.x 的 patch

    前面,我们也提及了 patch 是组件创建和更新的最后一步,有时候它也会被称为 diff。在
    「Vue2.x」中它的 patch 过程会是这样:

    • 同一级 VNode 间的比较,判断这两个新旧 VNode 是否属于同一个引用,是则不进行后续比较,不是则对比每一级的 VNode
    • 比较过程,分别定义四个指针指向新旧VNode 的首尾,循环条件为头指针索引小于尾指针索引
    • 匹配成功则将旧 VNode 的当前匹配成功的真实 DOM 移动到对应新 VNode 匹配成功的位置。
    • 匹配不成功,则将新 VNode 中的真实 DOM 节点插入到旧 VNode 的对应位置中,即,此时是创建旧 VNode 中不存在的 DOM 节点。
    • 不断递归,直到 VNodechildren 不存在为止。

    粗略一看,就能明白「Vue2.x」patch 是一个硬比较的过程。所以,这也是它的缺陷所在,无法合理地处理大型应用情况下的 VNode 更新。

    Vue3 的 patch

    虽然「Vue3」的 patch 没有像 compile 一样会重新命名一些例如 baseCompiletransform 阶段性的函数。但是,其内部的处理相对于「Vue2.x」变得更为智能

    它会利用 compile 阶段的 typepatchFlag 来处理不同情况下的更新,这也可以理解为是一种分而治之的策略。其对应的伪代码会是这样:

    function patch(...) {
      if (n1 && !isSameVNodeType(n1, n2)) {
        ...
      }
      if (n2.patchFlag === PatchFlags.BAIL) {
        ...
      }
      const { type, ref, shapeFlag } = n2
      switch (type) {
        case Text:
          processText(n1, n2, container, anchor)
          break
        case Comment:
          processCommentNode(n1, n2, container, anchor)
          break
        case Static:
          if (n1 == null) {
            mountStaticNode(n2, container, anchor, isSVG)
          } else if (__DEV__) {
            patchStaticNode(n1, n2, container, isSVG)
          }
          break
        case Fragment:
          processFragment(...)
          break
        default:
          if (shapeFlag & ShapeFlags.ELEMENT) {
            processElement(...)
          } else if (shapeFlag & ShapeFlags.COMPONENT) {
            processComponent(...)
          }else if (shapeFlag & ShapeFlags.TELEPORT) {
            ;(type as typeof TeleportImpl).process(...)
          } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
            ;(type as typeof SuspenseImpl).process(...)
          } else if (__DEV__) {
            warn('Invalid VNode type:', type, `(${typeof type})`)
          }
    }
    

    可以看到,除开文本、静态、文档碎片、注释等 VNode 会根据 type 处理。默认情况下,都是根据 shapeFlag 来处理诸如组件、普通元素、TeleportSuspense 组件等。所以,这也是为什么文章开头会介绍 shapeFlag 的原因。

    并且,从 render 阶段创建 Block VNodepatch 阶段根据特定 shapeFlag 的不同处理,在一定程度上,shapeFlag 具有和 patchFlag 一样的价值

    这里取其中一种情况,当 ShapeFlagELEMENT 时,我们来分析一下 processElement 是如何处理 VNodepatch 的。

    processElement

    同样地 processElement 会处理挂载的情况,即 oldVNodenull 的时候。processElement 函数的定义:

    const processElement = (
        n1: VNode | null,
        n2: VNode,
        container: RendererElement,
        anchor: RendererNode | null,
        parentComponent: ComponentInternalInstance | null,
        parentSuspense: SuspenseBoundary | null,
        isSVG: boolean,
        optimized: boolean
      ) => {
        isSVG = isSVG || (n2.type as string) === 'svg'
        if (n1 == null) {
          mountElement(
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        } else {
          patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
        }
      }
    

    其实,个人认为 oldVNode 改为 n1newVNode 改为 n2,这命名是否有点仓促?

    可以看到,processElement 在处理更新的情况时,实际上会调用 patchElement 函数。

    patchElement

    patchElement 会处理我们所熟悉的 props、生命周期、自定义事件指令等。这里,我们不会一一分析每一种情况会发生什么。我们就以文章开头提的靶向更新为例,它是如何处理的?

    其实,对于靶向更新的处理很是简单,即如果此时 n2newVNode) 的 dynamicChildren 存在时,直接"梭哈",一把更新 dynamicChildren,不需要处理其他 VNode。它对应的伪代码会是这样:

    function patchElement(...) {
      ...
      if (dynamicChildren) {
        patchBlockChildren(
          n1.dynamicChildren!,
          dynamicChildren,
          el,
          parentComponent,
          parentSuspense,
          areChildrenSVG
        )
        ...
      }
      ...
    }
    

    所以,如果 n2dynamicChildren 存在时,则会调用 patchBlockChildren 方法。而,patchBlockChildren 方法实际上是基于 patch 方法的一层封装。

    patchBlockChildren

    patchBlockChildren 会遍历 newChildren,即 dynamicChildren 来处理每一个同级别的 oldVNodenewVNode,以及它们作为参数来调用 patch 函数。以此类推,不断重复上述过程。

    const patchBlockChildren: PatchBlockChildrenFn = (
        oldChildren,
        newChildren,
        fallbackContainer,
        parentComponent,
        parentSuspense,
        isSVG
      ) => {
        for (let i = 0; i < newChildren.length; i++) {
          const oldVNode = oldChildren[i]
          const newVNode = newChildren[i]
    
          const container =
            oldVNode.type === Fragment ||
            !isSameVNodeType(oldVNode, newVNode) ||
            oldVNode.shapeFlag & ShapeFlags.COMPONENT ||
            oldVNode.shapeFlag & ShapeFlags.TELEPORT
              ? hostParentNode(oldVNode.el!)!
              : fallbackContainer
          patch(
            oldVNode,
            newVNode,
            container,
            null,
            parentComponent,
            parentSuspense,
            isSVG,
            true
          )
        }
      }
    

    大家应该会注意到,此时还会获取当前 VNode 需要挂载的容器,因为 dynamicChildren 有时候会是跨层级的,并不是此时的 VNode 就是它的 parent。具体会分为两种情况:

    1. oldVNode 的父节点作为容器

    • 当此时 oldVNode 的类型为文档碎片时。
    • oldVNodenewVNode 不是同一个节点时。
    • shapeFlagteleportcomponent 时。

    2. 初始调用 patch 的容器

    • 除开上述情况,都是以最初的 patch 方法传入的VNode 的挂载点作为容器。

    具体每一种情况为什么需要这样处理,讲起来又将是长篇大论,预计会放在下一篇文章中和大家见面。

    写在最后

    本来初衷是想化繁为简,没想到最后还是写了 3k+ 的字。因为,「Vue3」将 compileruntime 结合运用实现了诸多优化。所以,已经不可能出现如「Vue2.x」一样分析 patch 只需要关注 runtime,不需要关注在这之前的 compile 做了一些奠定基调的处理。因此,文章总会不可避免地有点晦涩,这里建议想加深印象的同学可以结合实际栗子单步调式一番。

    往期文章回顾

    从编译过程,理解 Vue3 静态节点提升(源码分析)

    从零到一,带你彻底搞懂 vite 中的 HMR 原理(源码分析)

    ❤️ 爱心三连击

    通过阅读,如果你觉得有收获的话,可以爱心三连击!!!

    展开全文
  • Vue编译 从Babel / Sass / Stylus将Vue单文件组件中的块编译为JS / CSS。 为什么采用这种方法 ...vue-compile example.vue -o output.vue # normalize a directory # non .vue files will be simply copie
  • Vue 中的compile操作方式

    千次阅读 2018-12-22 09:36:51
    这篇文章主要介绍了Vue 中的compile操作方法,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。 在 Vue 里,模板编译也是非常重要的一部分,里面也非常复杂...

    这篇文章主要介绍了Vue 中的compile操作方法,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。

    在 Vue 里,模板编译也是非常重要的一部分,里面也非常复杂,这次探究不会深入探究每一个细节,而是走一个全景概要,来吧,大家和我一起去一探究竟。

    初体验

    我们看了 Vue 的初始化函数就会知道,在最后一步,它进行了 vm.mount(el) 的操作,而这个mount 在两个地方定义过,分别是在 entry-runtime-with-compiler.js(简称:eMount) 和 runtime/index.js(简称:rMount) 这两个文件里,那么这两个有什么区别呢?

    // entry-runtime-with-compiler.js
    const mount = Vue.prototype.$mount // 这个 $mount 其实就是 rMount
    Vue.prototype.$mount = function (
     el?: string | Element,
     hydrating?: boolean
    ): Component {
     const options = this.$options
     if (!options.render) {
     ...
    //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860
     if(template) {
      const { render, staticRenderFns } = compileToFunctions(template, {
      shouldDecodeNewlines,
      shouldDecodeNewlinesForHref,
      delimiters: options.delimiters,
      comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
     }
     ...
     }
     return mount.call(this, el, hydrating)
    }//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860
    复制代码

    其实 eMount 最后还是去调用的 rMount,只不过在 eMount 做了一定的操作,如果你提供了 render 函数,那么它会直接去调用 rMount,如果没有,它就会去找你有没有提供 template,如果你没有提供 template,它就会用 el 去查询 dom 生成 template,最后通过编译返回了一个 render 函数,再去调用 eMount。 从上面可以看出,最重要的一部分就是 compileToFunctions 这个函数,它最后返回了 render 函数,关于这个函数,它有点复杂,我画了一张图来看一看它的关系,可能会有误差,希望大侠们可以指出。

    编译三步走

    看一下这个编译的整体过程,我们其实可以发现,最核心的部分就是在这里传进去的 baseCompile 做的工作: parse: 第一步,我们需要将 template 转换成抽象语法树(AST)。 optimizer: 第二步,我们对这个抽象语法树进行静态节点的标记,这样就可以优化渲染过程。 generateCode: 第三步,根据 AST 生成一个 render 函数字符串。 好了,我们接下来就一个一个慢慢看。

    解析器

    在解析器中有一个非常重要的概念 AST,大家可以去自行了解一下。 在 Vue 中,ASTNode 分几种不同类型,关于 ASTNode 的定义在 flow/compile.js 里面,请看下图:

    我们用一个简单的例子来说明一下:

    <div id="demo">
     <h1>Latest Vue.js Commits</h1>
     <p>{{1 + 1}}</p>
    </div>
    复制代码

    我们想一想这段代码会生成什么样的 AST 呢?

    我们这个例子最后生成的大概就是这么一棵树,那么 Vue 是如何去做这样一些解析的呢?我们继续看。 在 parse 函数中,我们先是定义了非常多的全局属性以及函数,然后调用了 parseHTML 这么一个函数,这也是 parse 最核心的函数,这个函数会不断的解析模板,填充 root,最后把 root(AST) 返回回去。

    parseHTML

    在这个函数中,最重要的是 while 循环中的代码,而在解析过程中发挥重要作用的有这么几个正则表达式。

    const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
    const ncname = '[a-zA-Z_][\\w\\-\\.]*'
    const qnameCapture = `((?:${ncname}\\:)?${ncname})`
    const startTagOpen = new RegExp(`^<${qnameCapture}`)
    const startTagClose = /^\s*(\/?)>/
    const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
    const doctype = /^<!DOCTYPE [^>]+>/i
    const comment = /^<!\--/
    const conditionalComment = /^<!\[/
    //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860
    复制代码

    Vue 通过上面几个正则表达式去匹配开始结束标签、标签名、属性等等。 关于 while 的详细注解我放在我仓库里了,有兴趣的可以去看看。 在 while 里,其实就是不断的去用 html.indexOf('<') 去匹配,然后根据返回的索引的不同去做不同的解析处理:

    • __等于 0:__这就代表这是注释、条件注释、doctype、开始标签、结束标签中的某一种
    • __大于等于 0:__这就说明是文本、表达式
    • __小于 0:__表示 html 标签解析完了,可能会剩下一些文本、表达式

    parse 函数就是不断的重复这个工作,然后将 template 转换成 AST,在解析过程中,其实对于标签与标签之间的空格,Vue 也做了优化处理,有些元素之间的空格是没用的。 compile 其实要说要说非常多的篇幅,但是这里只能简单的理一下思路,具体代码还需要各位下去深扣。

    优化器

    从代码中的注释我们可以看出,优化器的目的就是去找出 AST 中纯静态的子树: 把纯静态子树提升为常量,每次重新渲染的时候就不需要创建新的节点了 在 patch 的时候就可以跳过它们 optimize 的代码量没有 parse 那么多,我们来看看:

    export function optimize (root: ?ASTElement, options: CompilerOptions) {
     // 判断 root 是否存在
     if (!root) return
     // 判断是否是静态的属性
     // 'type,tag,attrsList,attrsMap,plain,parent,children,attrs'
     isStaticKey = genStaticKeysCached(options.staticKeys || '')
     // 判断是否是平台保留的标签,html 或者 svg 的
     isPlatformReservedTag = options.isReservedTag || no
     // 第一遍遍历: 给所有静态节点打上是否是静态节点的标记
     markStatic(root)
     // 第二遍遍历:标记所有静态根节点
     markStaticRoots(root, false)
    }//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860
    复制代码

    下面两段代码我都剪切了一部分,因为有点多,这里就不贴太多代码了,详情请参考我的仓库。

    第一遍遍历

    function markStatic (node: ASTNode) {
     node.static = isStatic(node)
     if (node.type === 1) {
     ...
     }
    }
    复制代码

    其实 markStatic 就是一个递归的过程,不断地去检查 AST 上的节点,然后打上标记。 刚刚我们说过,AST 节点分三种,在 isStatic 这个函数中我们对不同类型的节点做了判断:

    function isStatic (node: ASTNode): boolean {
     if (node.type === 2) { // expression
     return false
     }
     if (node.type === 3) { // text
     return true
     }
     return !!(node.pre || (
     !node.hasBindings && // no dynamic bindings
     !node.if && !node.for && // not v-if or v-for or v-else
     !isBuiltInTag(node.tag) && // not a built-in
     isPlatformReservedTag(node.tag) && // not a component
     !isDirectChildOfTemplateFor(node) &&
     Object.keys(node).every(isStaticKey)
     ))//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860
    }
    复制代码

    可以看到 Vue 对下面几种情况做了处理:

    当这个节点的 type 为 2,也就是表达式节点的时候,很明显它不是一个静态节点,所以返回 false 当 type 为 3 的时候,也就是文本节点,那它就是一个静态节点,返回 true 如果你在元素节点中使用了 v-pre 或者使用了

     标签,就会在这个节点上加上 pre 为 true,那么这就是个静态节点
    如果它是静态节点,那么需要它不能有动态的绑定、不能有 v-if、v-for、v-else 这些指令,不能是 slot 或者 component 标签、不是我们自定义的标签、没有父节点或者元素的父节点不能是带 v-for 的 template、 这个节点的属性都在 type,tag,attrsList,attrsMap,plain,parent,children,attrs 里面,满足这些条件,就认为它是静态的节点。
    接下来,就开始对 AST 进行递归操作,标记静态的节点,至于里面做了哪些操作,可以到上面那个仓库里去看,这里就不展开了。

    第二遍遍历

    第二遍遍历的过程是标记静态根节点,那么我们对静态根节点的定义是什么,首先根节点的意思就是他不能是叶子节点,起码要有子节点,并且它是静态的。在这里 Vue 做了一个说明,如果一个静态节点它只拥有一个子节点并且这个子节点是文本节点,那么就不做静态处理,它的成本大于收益,不如直接渲染。 同样的,我们在函数中不断的递归进行标记,最后在所有静态根节点上加上 staticRoot 的标记,关于这段代码也可以去上面的仓库看一看。

    代码生成器

    在这个函数中,我们将 AST 转换成为 render 函数字符串,代码量还是挺多的,我们可以来看一看。

    export function generate (
     ast: ASTElement | void,
     options: CompilerOptions
    ): CodegenResult {
     // 这就是编译的一些参数
     const state = new CodegenState(options)
     // 生成 render 字符串
     const code = ast ? genElement(ast, state) : '_c("div")'
     return {
     render: `with(this){return $[code]}`,
     staticRenderFns: state.staticRenderFns
     }//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860
    }
    复制代码

    可以看到在最后代码生成阶段,最重要的函数就是 genElement 这个函数,针对不同的指令、属性,我们会选择不同的代码生成函数。最后我们按照 AST 生成拼接成一个字符串,如下所示:

    with(this){return _c('div',{attrs:{"id":"demo"}},[(1>0)?_c('h1',[_v("Latest Vue.js Commits")]):_e(),...}
    复制代码

    在 render 这个函数字符串中,我们会看到一些函数,那么这些函数是在什么地方定义的呢?我们可以在 core/instance/index.js 这个文件中找到这些函数:

    // v-once
    target._o = markOnce
    // 转换
    target._n = toNumber
    target._s = toString
    // v-for
    target._l = renderList
    // slot
    target._t = renderSlot
    // 是否相等
    target._q = looseEqual
    // 检测数组里是否有相等的值
    target._i = looseIndexOf
    // 渲染静态树
    target._m = renderStatic
    // 过滤器处理
    target._f = resolveFilter
    // 检查关键字
    target._k = checkKeyCodes
    // v-bind
    target._b = bindObjectProps
    // 创建文本节点
    target._v = createTextVNode
    // 创建空节点
    target._e = createEmptyVNode
    // 处理 scopeslot
    target._u = resolveScopedSlots
    // 处理事件绑定
    target._g = bindObjectListeners
    // 创建 VNode 节点
    vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
    //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860
    复制代码

    在编译结束后,我们根据不同的指令、属性等等去选择需要调用哪一个处理函数,最后拼接成一个函数字符串。 我们可以很清楚的看到,最后生成了一个 render 渲染字符串,那么我们要如何去使用它呢?其实在后面进行渲染的时候,我们进行了 new Function(render) 的操作,然后我们就能够正常的使用 render 函数了。

    结语

    感谢您的观看,如有不足之处,欢迎批评指正。

    本次给大家推荐一个免费的学习群,里面概括移动应用网站开发,css,html,webpack,vue node angular以及面试资源等。 对web开发技术感兴趣的同学,欢迎加入Q群:864305860,不管你是小白还是大牛我都欢迎,还有大牛整理的一套高效率学习路线和教程与您免费分享,同时每天更新视频资料。 最后,祝大家早日学有所成,拿到满意offer,快速升职加薪,走上人生巅峰。

    转载于:https://juejin.im/post/5c1e05a76fb9a049f7462f2b

    展开全文
  • Vue Compile原理分析

    2020-09-22 19:20:45
    VueCompile是一个非常复杂的内容,Compile的主要作用是解析模板,生成渲染模板的render, 而render的作用主要是为了生成VNode, Compile主要分为3大块: parse 接受template原始模板,按着模板的节点和数据生成对应...

    Vue中Compile是一个非常复杂的内容,Compile的主要作用是解析模板,生成渲染模板的render, 而render的作用主要是为了生成VNode, Compile主要分为3大块:

    1. parse 接受template原始模板,按着模板的节点和数据生成对应的ast
    2. optimize 遍历ast的每一个节点,标记静态节点,这样就知道哪部分不会变化,于是在页面需要更新时,减少去对比这部分DOM,提升性能
    3. generate 把前两步生成完善的ast,组成render字符串,然后将render字符串通过new Function的方式转换成渲染函数

    1. Compile从新建实例到Compile结束的主要流程

    Vue编译template, 生成render发生在$mount这个函数中

    Vue.prototype.$mount = function(el) {
        var options = this.$options;
        if (!options.render) {
            // 获取template模板
            var tpl = options.template;
            if (tpl) {
                tpl = document.querySelector(tpl).innerHTML;
            }
            if (tpl) {
                var ref = compileToFunctions(tpl, {}, this);
                options.render = ref.render;
                options.staticRenderFns = ref.staticRenderFns;
            }
        }
        // 执行上面生成的render, 生成DOM,挂载DOM
        return mount.call(this, el);
    }
    

    compileToFunctions的生成流程

    // compileToFunctions是通过createCompiler执行返回的
    var ref$1 = createCompiler();
    var { compileToFunctions } = ref$1;
    

    createCompiler的生成流程

    // createCompiler 是通过 createCompilerCreator 生成的
    // createCompilerCreator 会传入一个 baseCompile 的函数
    var createCompiler = createCompilerCreator(
        function baseCompile(template, options) {
            var ast = parse(template.trim(), options);
            if (options.optimize !== false) {
                optimize(ast, options);
            }
            var code = generate(ast, options);
            return {
                ast,
                render: code.render,
                staticRenderFns: code.staticRenderFns
            }
        }
    );
    
    function createCompilerCreator (baseCompile) {
        return function () {
            // 作用是合并选项,并且调用 baseCompile
            function compile(template) {
                var compiled = baseCompile(template);
                return compiled;
            }
    
            return {
                compile,
                // compileToFunctions 用来生成 render 和 staticRenderFns
                // compileToFunctions 其内核就是 baseCompile
                compileToFunctions: createCompileToFunctionFn(compile)
            }
        }
    }
    
    function createCompileToFunctionFn (compile) {
        // 作为缓存,防止每次都重新编译
        // template 字符串 作为 key
        // 值为 render 和 staticRenderFns
        var cache = Object.create(null);
        return function compileToFunctions(template, options, vm) {
            var key = template;
    
            // 有缓存的时候直接取出缓存中的结果即可
            if (cache[key]) return cache[key];
            // compile 是 createCompilerCreator 传入的 compile
            var compiled = compile(template, options);
            var res = {
                // 将字符串render解析成函数
                render: new Function(compiled.render),
                staticRenderFns: compiled.staticRenderFns.map(function(code) {
                    return new Function(code, fnGenErrors);
                })
            };
            cache[key] = res;
            return cache[key];
        }
    }
    

    2. parse主要流程

    parse的主要作用就是将template字符串转化为ast,ast为抽象语法树,是一种以树形结果来表示模板语法结构,比如:

    {
        tag: 'div',
        type: 1, // 1->节点; 2->表达式,比如{{isShow}}; 3->纯文本
        children: [{
            type: 3,
            text: '11'
        }]
    }
    

    parse整个流程非常复杂,需要一步步深入

    // pase接收template字符串
    function parse(template) {
        // 缓存模板中解析的每个节点的ast
        // 是一个数组存放模板中按顺序从头到尾每个标签的AST
        // 主要用来理清节点父子关系
        var stack = [];
        // 根节点,是ast
        var root;
        // 当前解析标签的父节点
        // 这样才知道当前解析节点的父节点是谁,才把这个节点添加给响应节点的children
        // 根节点没有父节点,所以是undefined
        var currentParent;
    
        // parseHTML 负责 template 中的标签匹配,再传入 start, end, chars 等方法
        parseHTML(template, {
            start, // 处理头标签
            end, // 处理尾标签
            chars // 处理文本
        });
    
        return root;
    }
    
    function parseHTML(html, options) {
        while(html) {
            // 寻找 < 的起始位置
            var textEnd = html.indexOf('<'),
                text,
                rest,
                next;
            
            // 如果模板起始位置是标签开头 <
            // 如果匹配到 <, 那这个有可能是头标签上的,也有可能是尾标签上的
            if (textEnd === 0) {
    
                // 如果是尾标签得到 <
                // 比如html = '</div>', 匹配出 endTagMatch=['</div>', 'div']
                var endTagMatch = html.match(endTag);
                if (endTagMatch) {
                    // endTagMatch[0]='</a>'
                    html = html.substring(endTagMatch[0].length);
                    // 处理尾标签的方法
                    options.end();
                    continue;
                }
    
                // 如果是起始标签的 <
                // parseStartTag作用是,匹配标签存在的属性,截断template
                // 比如: html='<div></div>'
                // parseStartTag处理之后, startTagMatch={tagName: 'div', attrs: []}
                var startTagMatch = parseStartTag();
                // 匹配到起始标签之后
                if (startTagMatch) {
                    // 处理起始标签
                    options.start(...);
                    continue;
                }
            }
    
            // 模板起始位置不是 <, 而是文字
            if (textEnd >= 0) {
                text = html.substring(0, textEnd);
                html = html.substring(n);
            }
    
            // 处理文字
            if (options.chars && text) {
                options.chars(text);
            }
        }
    }
    

    处理头标签, 当parseHTML匹配到一个首标签,都会把该标签的信息传递给start

    function start(tag, attrs, unary) {
        // 创建AST节点
        var element = createASTElement(tag, attrs, currentParent);
    
        // 设置根节点,一个模板仅有一个根节点
        if (!root) {
            root = element;
        }
    
        // 处理父子关系
        if (currentParent) {
            currentParent.children.push(element);
            element.parent = currentParent;
        }
    
        // 不是单标签(input, img), 都需要保存到stack
        if (!unary) {
            currentParent = element;
            stack.push(element);
        }
    }
    
    function createASTElement(tag, attrs, parent) {
        // 创建一个AST结构,保存数据
        /*
            模板上的属性经过parseHTML解析成一个数组
            [{ name: 'hoho', value: '333'},{ name: 'href', value: '444' }]
            makeAttrMap可以将其转化成如下结构
            { hoho: '333', href: '444' }
        */
        return {
            type: 1,
            tag,
            attrsList: attrs,
            attrsMap: makeAttrsMap(attrs),
            parent,
            children: []
        }
    }
    

    处理尾标签匹配到尾标签,比如</div>的时候,就会调用传入的end方法

    function end() {
        // 标签解析结束,移除该标签
        /*
            比如有如下一段html:
            <div>
                <section></section>
                <p></p>
            </div>
            stack匹配到两个头标签之后 stack=['div', 'section']
            然后匹配到</section>, 则移除stack中的section, 并且重设 currentParent
            stack=['div']
            currentParent='div'
            再匹配到</p>, p的父节点就是div了,父子顺序就正确了
        */
        stack.length -= 1;
        currentParent = stack[stack.length - 1];
    }
    

    处理文本字符串,当parseHTML去匹配<的时候,发现template开头到<还有一段距离,那么这段距离就是文本了,这段文本会给chars方法处理

    // chars的主要作用就是为父节点添加文本子节点
    // 文本子节点有两种类型
    // 1. 普通型,直接存为文本子节点
    // 2. 表达式型,需要经过parseText处理
    /*
        比如表达式{{isShow}}会被解析成
        {
            expression: toString(isShow),
            tokens: [{@binding: "isShow"}]
        }
        其主要是为了把表达式isShow拿到,方便从实例上获取
    */
    function chars(text) {
        // 必须存在根节点,不能用文字开头
        if (!currentParent) return;
        var children = currentParent.children;
    
        // 通过parseText解析成字符串,判断是否含有双括号表达式,比如{{isShow}}
        // 如果含有表达式,会存放多一点信息
        var res = parseText(text);
        if (res) {
            children.push({
                type: 2,
                expression: res.expression,
                tokens: res.tokens,
                text,
            });
        }
        // 普通字符串,直接存在字符串节点
        else if (!children.length || children[children.length - 1].text !== ' ) {
            children.push({
                type: 3,
                text,
            });
        }
    }
    

    下面来看一个完整的Parse流程

    <div>11</div>
    
    1. 开始循环template,匹配到第一个头标签div, 传入start, 生成对应的AST,该div的ast变成根节点root,并设置其为当前父节点currentParent, 保存节点存储数组stack
    stack = [{ tag: 'div', children: [] }]
    

    第一轮处理结束,template截断到第一次匹配到的位置11</div>

    1. 开始第二次遍历,开始匹配到<, 发现<不在开头,从开头位置到<有一段普通字符串,调用chars,传入字符串,发现其没有双括号等表达式,直接给父节点添加简单子节点
    currentParent.children.push({ type: 3, text: '11' });
    

    此时

    stack = [{ tag: 'div', children: [{ type: 3, text: '11' }] }]
    

    第二轮结束,template截断到刚刚匹配完的字符串,此时template=</div>

    1. 开始第三轮遍历,继续寻找<, 发现就在开头,但是这是一个结束标签,标签名是div。这个标签匹配完毕,也会从stack中移除,第3次遍历结束,template继续阶段,此时template为空,遍历结束。
    {
        tag: 'div',
        type: 1,
        children: [{ type: 3, text: '11' }]
    }
    

    3. 标签解析

    上一小节讲到了是通过parseHTML这个方法来对template进行循环遍历的。就是不断的将模板字符串匹配然后截断,直到字符串为空,其中和阶段有关的一个重要函数就是advance

    // 截断模板字符串,并保存当前的位置
    function advance(n) {
        index += n;
        html = html.substring(n);
    }
    
    • 如果 < 在template开头
      如果是尾标签的<, 那么交给parseEndTag处理
      如果是头标签的>, 那么交给parseStartTag处理

    • 如果 < 不在template开头,那么表明开头到 < 的这段位置是字符串,也需要用到advance去截断字符串。

    template = '<div>111</div>';
    parseHTML(template);
    

    parseStartTag作用是处理头标签

    1. 把头标签的所有信息及合起来,包括属性,标签名
    2. 匹配完成之后,同样调用advance去截断template
    3. 把标签信息返回
    function parseStartTag() {
        // html ='<div name=1>111</div>'
        // start = ["<div", "div", index: 0]
        var start = html.match(startTagOpen);
        if (start) {
            // 存储本次头标签的信息
            var match = {
                tagName: start[1],
                attrs: [],
                start: index
            }
        }
    
        // start[0]是<div
        // 阶段之后template="name=1 >111</div>"
        advance(start[0].length);
    
        var end, attr;
        // 循环匹配属性内容,保存属性列表
        // 直到template遍历到头标签的 >
        while (
            // 匹配不到头标签的 > , 开始匹配 属性内容
            !(end = html.match(startTagClose))
            // 开始匹配属性内容
            // attr = ["name=1", "name", "="]
            && (attr = html.match(attribute))
        ) {
            advance(attr[0].length);
            match.attrs.push(attr);
        }
    
        // 匹配到起始标签的 > , 标签属性那些已经匹配完毕了
        // 返回收集到的标签信息
        if (end) {
            advance(end[0].length);
            // 如果是单标签,那么 unarySlash 的值是 / , 比如 <input />
            match.unarySlash = end[1];
            match.end = index;
            return match;
        }
    }
    

    经过以上的parseStartTag会处理返回如下内容

    // <div name=1></div>
    {
        tagName: 'div',
        // 头标签中的属性信息
        attrs: [
            [" name=1", "name", "=", undefined, undefined, "1"]
        ],
        unarySlash: "", // 表示这个表示是否是单标签
        start: 0, // 头标签的 < 在template中的位置
        end: 12 // 头标签的 > 在template中的位置
    }
    

    通过parseStartTag返回的头信息,最后传给了handleStartTag

    function handleStartTag(match) {
        var tagName = match.tagName;
        var unarySlash = match.unarySlash;
        // 判断是不是单标签,input, img这些
        var unary = isUnaryTag$$1(tagName) || !!unarySlash;
        var l = match.attrs.length;
        var attrs = new Array(l);
    
        // 把属性数组转化成对象
        for (let i = 0; i < l; i++) {
            var args = match.attrs[i];
            var value = args[3] || args[4] || args[5] || '';
            attrs[i] = {
                name: args[1],
                value
            }
        }
    
        // 不是单标签才存到stack
        if (!unary) {
            stack.push({
                tag: tagName,
                attrs
            });
        }
    
        if (options.start) {
            options.start(
                tagName, attrs, unary,
                match.start, match.end
            );
        }
    }
    

    当使用endTag这个正则成功匹配到尾标签时,会调用parseEndTag

    function parseEndTag(tagName, start, end) {
        var pos, lowerCasedTagName;
        if (tagName) {
            for (pos = stack.length - 1; pos >= 0; pos--) {
                if (stack[pos].tagName === tagName) break;
            }
        }
        else {
            // 如果没有提供标签签名,那么关闭所有存在stack中的起始标签
            pos = 0;
        }
    
        // 批量 stack pop 位置后的所有标签
        if (pos >= 0) {
            // 关闭 pos 位置之后所有的起始标签,避免有些标签没有尾标签
            // 比如stack.length = 7, pos = 5, 那么就关闭最后两个
            for (var i = stack.length - 1; i >= pos; i--) {
                if (options.end) {
                    options.end(stack[i].tag, start, end);
                }
            }
            // 匹配完闭合标签之后,就把匹配的标签头给移除了
            stack.length = pos;
        }
    }
    
    /*
        简要流程如下
        <div>
            <header>
                <span></span>
            </header>
        </div>
        一开始会匹配到3个头标签 stack = [div, header, span]
        然后开始匹配到span, 然后去stack末尾去找span
        确定span在stack中的位置pos后,批量闭合stack的pos之后所有的标签
    */
    

    4.属性解析

    处理class分为两种,一种静态的class, 一种动态的class

    function transformNode(el, options) {
        var staticClass = getAndRemoveAttr(el, 'class');
        if (staticClass) {
            el.staticClass = JSON.stringify(staticClass);
        }
        // 处理动态class
        var classBinding = getBindingAttr(el, 'class', false);
        if (classBinding) {
            el.classBinding = classBinding;
        }
    }
    
    /*
        <span class="a" :class="b"></span>
        转化为下面的
        {    
            classBinding: "b"
            staticClass: ""a""
            tag: "span"
            type: 1
        }
    */
    

    处理style几乎和class一模一样

    function transformNode$1(el, options) {    
        var staticStyle = getAndRemoveAttr(el, 'style');    
        if (staticStyle) {        
            // 比如绑定 style="height:0;width:0"
            // parseStyleText 解析得到对象 { height:0,width:0 }
            el.staticStyle = JSON.stringify(parseStyleText(staticStyle));
        }  
        // :style="{height:a}" 解析得 {height:a}
        var styleBinding = getBindingAttr(el, 'style', false);    
        if (styleBinding) {
            el.styleBinding = styleBinding;
        }
    }
    
    /*
        <span style="width:0" :style="{height: a}"></span>
        转化成下面的
        {
            staticStyle: "{"width":"0"}"
            styleBinding: "{height:a}"
            tag: "span"
            type: 1
        }
    */
    

    解析v-for用的是processFor

    function processFor(el) {
        var exp = getAndRemoveAttr(el, 'v-for');
        if (exp) {
            // 比如指令是v-for="(item, index) in arr"
            // res = {for: "arr", alias: "item", iterator1: "index"}
            var res = parseFor(exp);
            if (res) {
                // 把res和el属性合并起来
                extend(el, res);
            }
        }
    }
    
    /*
        <div v-for="(item, index) in arr"></div>
        可以转化为
        {
            alias: "item",    
            for: "arr",    
            iterator1: "index",    
            tag: "div",    
            type: 1,
        }
    */
    

    解析v-if用的是processIf

    function processIf(el) {
        var exp = getAndRemoveAttr(el, 'v-if');
        if (exp) {
            el.if = exp;
            (el.ifConditions || el.ifConditions = []).push({
                exp,
                block: el
            })
        } else {
            // 对于 v-else 和 v-else-if 没有做太多的处理
            // 这二者会调用之后的 processIfConditions
            if (getAndRemoveAttr(el, 'v-else') !== null) {
                el.else = true;
            }
            var elseif = getAndRemoveAttr(el, 'v-else-if');
            if (el.elseif) {
                el.elseif = elseif;
            }
        }
    }
    
    function processIfConditions(el, parent) {
        var prev = findPrevElement(parent.children);
        if (prev && prev.if) {
            (prev.ifConditions || prev.ifConditions = []).push({
                exp: el.elseif,
                block: el
            });
        }
    }
    
    /*
        <div>
            <p></p>
            <div v-if="a"></div>
            <strong v-else-if="b"></strong>
            <span v-else></span>
        </div>
        解析之后生成如下
        {
            tag: "header",    
            type: 1,    
            children:[{        
                tag: "header",        
                type: 1,        
                if: "a",        
                ifCondition:[
                    {exp: "a", block: {header的ast 节点}}
                    {exp: "b", block: {strong的ast 节点}}
                    {exp: undefined, block: {span的ast节点}}
                ]
            },{        
                tag: "p"
                type: 1
            }]
        }
    */
    

    slot的解析是通过processSlot来解析的

    function processSlot(el) {
        if (el.tag === 'slot') {
            el.slotName = el.attrsMap.name;
        }
        else {
            var slotScope = getAndRemoveAttr(el, 'slot-scope');
            el.slotScope = slotScope;
    
            // slot的名字
            var slotTarget = el.attrsMap.slot;
            if (slotTarget) {
                el.slotTarget = slotTarget === '""'
                    ? '"default"'
                    : slotTarget
            }
        }
    }
    
    /*
        <span>
            <slot name="header" :a="num" :b="num"></slot>
        </span>
        以上模板解析成
        {
            {  
            tag: "span"
            type: 1
            children:[{  
                attrsMap: {name: " header", :a: "num", :b: "num"}
                slotName: "" header""
                tag: "slot"
                type: 1
            }]
        }
    
        父组件模板
        <div>
            <child >
                <p slot="header" slot-scope="c"> {{ c }}</p>
            </child>
        </div>
        解析成
        {    
            children: [{        
                tag: "child",        
                type: 1,        
                children: [{            
                    slotScope: "c",            
                    slotTarget: ""header "",            
                    tag: "p",            
                    type: 1
                }]
            }],    
            tag: "div",    
            type: 1
        }
    */
    

    Vue自带属性v-, :, @三种符号的属性名,会分开处理

    先来看:的情况,经过Vue的处理,:开头的属性会被放入el.props或者el.attrs中。

    当我们给指令添加了.prop的时候

    <!-- 这个属性会被存放到el.props中 -->
    <div :name.props="myName"></div>
    

    props是直接添加到DOM属性上的,attrs是直接显示在标签上的。添加props的时候,需要转化成驼峰法,因为DOM元素的props不支持-连接的。

    当匹配到@或者v-on时,属于事件添加,没有太多处理

    <div @click="aaa" @keyup="bbb"></div>
    <!-- 
        {
            events: {
                click: { value: 'aaa' },
                keyup: { value: 'bbb' }
            }
        }
     -->
    

    v-开头的会全部保存到el.directives

    <div v-a:key="bbb"></div>
    <!-- 
        {
            directives: [{
                arg: "key",
                modifiers: undefined,
                name: "a",
                rawName: "v-a:key",
                value: "bbb"
            }]
        }
     -->
    

    普通属性直接存放进el.attrs

    <div bbb="ccc"></div>
    <!-- 
        {
            attrs: [{
                name: 'bbb',
                value: 'ccc'
            }]
        }
     -->
    

    总体源码如下

    var onRE = /^@|^v-on:/;
    var dirRE = /^v-|^@|^:/;
    var bindRE = /^:|^v-bind:/;
    var modifierRE = /\.[^.]+/g;
    var argRE = /:(.*)$/;
    function processAttrs(el) {    
        var list = el.attrsList;    
        var i, l, name, rawName, value, modifiers, isProp;   
        for (i = 0, l = list.length; i < l; i++) {
            name = rawName = list[i].name;
            value = list[i].value;        
            // 判断属性是否带有 'v-' , '@' , ':'
            if (dirRE.test(name)) {            
                // mark element as dynamic
                el.hasBindings = true;       
                // 比如 v-bind.a.b.c = "xxzxxxx"
                // 那么 modifiers = {a: true, b: true, c: true}
                modifiers = parseModifiers(name);            
                // 抽取出纯名字
                if (modifiers) {    
                    // name = "v-bind.a.b.c = "xxzxxxx" "
                    // 那么 name= v-bind
                    name = name.replace(modifierRE, '');
                }        
                // 收集动态属性,v-bind,可能是绑定的属性,可能是传入子组件的props
                // bindRE = /^:|^v-bind:/
                if (bindRE.test(name)) {   
                    // 抽取出纯名字,比如 name= v-bind
                    // 替换之后,name = bind
                    name = name.replace(bindRE, '');
                    isProp = false;      
                    if (modifiers) {   
                        // 直接添加到 dom 的属性上
                        if (modifiers.prop) {
                            isProp = true;    
                            // 变成驼峰命名
                            name = camelize(name);                        
                            if (name === 'innerHtml')   
                                name = 'innerHTML'; 
                        }      
                        // 子组件同步修改
                        if (modifiers.sync) {
                            addHandler(el,      
                                // 得到驼峰命名                      
                                "update:" + camelize(name), 
                                // 得到 "value= $event"
                                genAssignmentCode(value, "$event")
                            );
                        }
                    }  
                    // el.props 的作用上面有说,这里有部分是 表单的必要属性都要保存在 el.props 中
                    if (
                         isProp ||
                         // platformMustUseProp 判断这个属性是不是要放在 el.props 中
                         // 比如表单元素 input 等,属性是value selected ,checked 等
                         // 比如 tag=input,name=value,那么value 属性要房子啊 el.props 中
                         (!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name))
                    ) {
                        (el.props || (el.props = [])).push({ 
                            name, 
                            value
                        });
                    } 
    
                    // 其他属性放在 el.attrs 中
                    else {
                        (el.attrs || (el.attrs = [])).push({ 
                            name, 
                            value
                        });
                    }
                }            
                // 收集事件,v-on , onRE = /^@|^v-on:/
                else if (onRE.test(name)) {    
                    // 把 v-on 或者 @ 去掉,拿到真正的 指令名字
                    // 比如 name ="@click" , 替换后 name = "click"
                    name = name.replace(onRE, '');
                    addHandler(el, name, value, modifiers, false);
                }            
                // 收集其他指令,比如 "v-once",
                else { 
                    // 把v- 去掉,拿到真正的 指令名字
                    name = name.replace(dirRE, '');                
                    // name = "bind:key" , argMatch = [":a", "a"]
                    var argMatch = name.match(argRE);                
                    var arg = argMatch && argMatch[1];    
                    if (arg) {                    
                        // 比如 name = "bind:key" ,去掉 :key
                        // 然后 name = "bind"
                        name = name.slice(0, -(arg.length + 1));
                    }
                    (el.directives || (el.directives = [])).push({ 
                        name, 
                        rawName, 
                        value, 
                        arg, 
                        modifiers
                    });
                }
            } else {
    
                (el.attrs || (el.attrs = [])).push({ 
                    name, 
                    value
                });
            }
        }
    }
    

    optimize是Compile的三大步骤之一,是一个性能优化的手段

    // ... parse
    var ast = parse(template.trim(), options);
    if (options.optimize !== false) {
        optimize(ast, options);
    }
    // ...generate
    

    它能遍历AST子树,检测纯静态的子树,即永不需要更改的DOM,Vue内部进行Optimize的方法就是给节点加上static属性。

    function optimize(root, options) {
        if (!root) return;
        makeStatic$1(root);
        makeStaticRoots(root);
    }
    

    先来看看Vue是如何判断static节点的

    function isStatic(node) {
        // 文字表达式
        if (node.type === 2) return false;
        // 纯文本
        if (node.type === 3) return true;
        return node.pre || ( // 如果添加了v-pre指令,表明节点不需要解析了
            !node.hasBindings && // 不能存在指令,事件等
            !node.if && // 不能存在v-if
            !node.for && // 不能存在v-for
            !['slot', 'component'].indexOf(node.tag) > -1 && // 节点名称不能是slot和component
            isPlatformReserverdTag(node.tag) && // 需要时正常的HTML标签
            !isDirectChildOfTemplateFor(node) && // 父辈节点不能是template或者带有v-for
            Object.keys(node).every(isStaticKey) // 该节点所有的属性都需要是静态节点的静态属性
        );
    }
    

    上面提到的makeStatic$1方法,主要用来标记节点是否是静态节点

    // 标记节点是否是静态节点
    
    function markStatic$1(node) {
        node.static = isStatic(node);    
        if (node.type !== 1) return
        // 不要将组件插槽内容设置为静态。
        // 这就避免了
        // 1、组件无法更改插槽节点
        // 2、静态插槽内容无法热加载
        if (        
            // 正常 thml 标签 才往下处理,组件之类的就不可以
            !isPlatformReservedTag(node.tag) &&
            // 标签名是 slot 才往下处理
            node.tag !== 'slot' &&
            // 有 inline-tempalte 才往下处理
            node.attrsMap['inline-template'] == null
        ) {
            return
        }
        // 遍历所有孩子,如果孩子 不是静态节点,那么父亲也不是静态节点
        var l = node.children.length
        for (var i = 0;i < l; i++) {        
            var child = node.children[i];  
            // 递归设置子节点,子节点再调用子节点
            markStatic$1(child);        
            if (!child.static) {
                node.static = false;
            }
        }
        if (node.ifConditions) {    
            var c = node.ifConditions.length  
            for (var j = 1; j < c; j++) {    
                // block 是 节点的 ast
                var block = node.ifConditions[j].block;
                markStatic$1(block);  
                if (!block.static) {
                    node.static = false;
                }
            }
        }
    }
    

    第二步就是标记静态根节点

    // 标记根节点是否是静态节点
    
    function markStaticRoots(node) {    
        if (node.type === 1) return
        // 要使一个节点符合静态根的条件,它应该有这样的子节点
        // 不仅仅是静态文本。否则,吊装费用将会增加
        // 好处大于坏处,最好总是保持新鲜。
        if (        
            // 静态节点
            node.static &&        
            // 有孩子
            node.children.length &&        
            // 孩子有很多,或者第一个孩子不是纯文本
            ! (node.children.length === 1 && node.children[0].type === 3)
        ) {
            node.staticRoot = true;        
            return
        }
        else {
            node.staticRoot = false;
        }    
        if (node.children) {    
            var l = node.children.length    
            for (var i = 0; i < l; i++) {
                markStaticRoots(
                    node.children[i]
                );
            }
        }
    }
    

    markStatic$1 这个函数只是为 markStaticRoots 服务的,是为了先把每个节点都处理之后,更加方便快捷静态根节点。

    被判断为静态根节点的条件

    1. 该节点所有的子孙节点都是静态节点
    2. 必须存在子节点
    3. 子节点不能是纯文本节点。Vue不会将这种节点标记为静态节点,如果将这种节点也标记为静态节点,会起到负优化的作用,下面讨论为什么给纯文本节点标记为静态节点,是一种负优化

    首先标记为静态节点需要维护静态模板存储对象,这个信息存储在_staticTrees中。随着静态根节点的增加,这个存储对象会越来越大,那么占用的内存也会越来越多,势必要增加一些不必要的存储

    其实这个问题涉及到 render 和 静态 render 的合作,

    <div>
        <span>
            <strong>我是静态文本</strong>
        </span>
        <span v-if="testStaticRender"></span>
    </div>
    

    生成的render函数是这样的

    with(this) {
        return _c('div', [
            // 这个函数就是去获取静态模板的,这样会产生很多额外的调用
            _m(0),
            (testStaticRender ? _c('span') : _e())
        ])
    }
    

    genarate

    generate的作用是根据生成的AST节点,拼接成字符串,而这个字符串可以被转化为函数,函数执行后,就会生成VNode

    // options用来传入一些判断函数或者指令
    function generate(ast, options) {
        // CodegenState 给实例初始化编译状态
        var state = new CodegenState(options);
        // genElement 将AST转化为字符串
        var code = ast ? genElement(ast, state) : '_c("div")';
        return {
            render: "with(this){ return " + code + "}",
            staticRenderFns: state.staticRenderFns
        }
    }
    
    function CodegenState(options) {
        this.options = options;      
        // class$1 用于处理AST中的class
        // style$1 用于处理AST中的style
        this.dataGenFns = [ class$1.genData, style$1.genData];      
        this.directives = { on , bind, cloak, model,text ,html]
        // 用来存放静态根节点的render函数
        this.staticRenderFns = [];
    }
    

    genElement是AST拼接成字符串的重点函数,主要是处理各种节点,并且拼接起来

    // 这个里面主要是一个个的处理函数
    function genElement(el, state) {    
        if (
            el.staticRoot && !el.staticProcessed
        ) {
            // 拼接静态节点
            return genStatic(el, state)
        }    
        else if (
            el.for && !el.forProcessed
        ) {        
            return genFor(el, state)
        }    
        else if (
            el.if && !el.ifProcessed
        ) {        
            return genIf(el, state)
        }    
        else if (el.tag === 'slot') {        
            return genSlot(el, state)
        }    
        else {    
            var code;  
            // 处理 is 绑定的组件
            if (el.component) {
                code = genComponent(el.component, el, state);
            }    
            // 上面所有的解析完之后,会走到这一步
            else {  
                // 当 el 不存在属性的时候,el.plain = true
                var data = el.plain ? undefined : genData$2(el, state);  
                // 处理完父节点,遍历处理所有子节点
                var children = genChildren(el, state);
                code = `_c(
                    '${el.tag}'
                    ${data ?  ("," + data) : ''} 
                    ${children ? ("," + children) : ''}
                )`
            }      
            return code
        }
    }
    

    拼接静态节点

    function genStatic(el, state) {
        el.staticProcessed = true;
        state.staticRenderFns.push(
            "with(this){ return " + genElement(el, state) + "}"
        );
        return `_m(${
            state.staticRenderFns.length - 1
        })`;
    }
    

    拼接v-if节点

    // el.ifCondition 是用来存放条件数组的
    function genIf(el, state) {
        el.isProcessed = true;
        return genIfConditions(
            el.ifConditions.slice(),
            state
        );
    }
    
    /*
        <div>
            <p v-if="isShow"></p>
            <span v-else-if="isShow == 2"></span>
            <section v-else></section>
        </div>
        会编译成如下
        {    
            tag:"div",    
            children:[{        
                tag:"p",        
                ifCondition:[{            
                    exp: "isShow",            
                    block: {..p 的 ast 节点}
                },{            
                    exp: "isShow==2",            
                    block: {..span 的 ast 节点}
                },{            
                    exp: undefined,            
                    block: {..section 的 ast 节点}
                }]
            }]
        }
    */
    

    7. 事件拼接

    function genData$2(el, state) {
        var data = '{';
        // 组件自定义事件,比如`<div @click="a"></div>`
        if (el.events) {
            data += genHandlers(el.events, false) + ',';
        }
        // 原生DOM事件,比如 `@click.native`
        if (el.nativeEvents) {
            data += genHandlers(el.nativeEvents, true) + ',';
        }
        data = data.replace(/,$/, '') + '}';
        return data;
    }
    

    从上面的函数可以知道,不管是组件自定义事件还是原生DOM事件,都是调用的genHandlers

    function genHandlers(events, isNative) {
        var res = isNative ? 'nativeOn:{' : 'on:{';
        var handler = events[name];
        for (var name in events) {
            res += ` ${name}:${genHandler(name, handler)}, `
        }
        return res.slice(0, -1) + '}';
    }
    

    修饰符内部配置

    var modifierCode = {
        stop: '$event.stopPropagation();',
        prevent: '$event.preventDefault();',
        ctrl: genGuard("!$event.ctrlKey"),
        shift: genGuard("!$event.shiftKey"),   
        alt: genGuard("!$event.altKey"),
        meta: genGuard("!$event.metaKey"),
        self: genGuard("$event.target !== $event.currentTarget"),
        left: genGuard("'button' in $event && $event.button !== 0"),
        middle: genGuard("'button' in $event && $event.button !== 1"),
        right: genGuard("'button' in $event && $event.button !== 2")
    };
    var genGuard = function(condition) {    
        return ` if ( ${ condition } ) return null `
    };
    
    /*
        比如添加了stop修饰符的,会这么拼接
        "function($event ){ " + 
            "$event.stopPropagation();" + 
            " return "+ aaa +"($event);" +
        "}"
    */
    

    键盘修饰符

    // keys是一个数组,保存的是添加的修饰符,可以是数字,可以是键名
    function genKeyFilter(keys) {
        var key = keys.map(genFilterCode).join('&&');
        return `if( !('button' in $event) && ${ key } )
            return null `;
    }
    
    function genFilterCode(key) {
        var keyVal = parseInt(key);
        // 如果key是数字,那直接返回字符串
        if (keyVal) {
            return "$event.keyCode!==" + keyVal
        }
        // 如果key是键名,比如`enter`
        // 这个键名可能不在keyCodes, keyNames中,可以支持自定义
        var keyCode = keyCodes[key]; // 获取键值,keyName="Enter"
        var keyName = keyNames[key]; // 获取键名,keyCode=13
    
        // $event.keyCode 是按下的键的值
        // $event.key 是按下键的名
        // 比如我们按下字母`V`, 那此时的keyCode是86, key是'v'
        return `
            _k(
                $event.keyCode , 
                ${ key } , ${ keyCode }, 
                ${ $event.key } , ${ keyName }
            )
        `
    }
    
    // 返回的这个_k本体其实就是`checkKeyCodes`函数
    function checkKeyCodes(
        eventKeyCode, key, keyCode, 
        eventKeyName, keyName
    ) {    
        // 比如 key 传入的是自定义名字  aaaa
        // keyCode 从Vue 定义的 keyNames 获取 aaaa 的实际数字
        // keyName 从 Vue 定义的 keyCode 获取 aaaa 的别名
        // 并且以用户定义的为准,可以覆盖Vue 内部定义的
        var mappedKeyCode = config.keyCodes[key] || keyCode;    
        // 该键只在 Vue 内部定义的 keyCode 中 
        if (keyName && eventKeyName && !config.keyCodes[key]) {        
            return isKeyNotMatch(keyName, eventKeyName)
        }
        // 该键只在 用户自定义配置的 keyCode 中
        else if (mappedKeyCode) {        
            return isKeyNotMatch(mappedKeyCode, eventKeyCode)
        }
        // 原始键名
        else if (eventKeyName) {        
            return hyphenate(eventKeyName) !== key
        }
    }
    

    核心的genHandler方法

    function genHandler(name, handler) {
        // 没有绑定回调,返回一个空函数
        if (!handler) {
            return 'function(){}';
        }
        // 如果绑定的是数组,则逐个递归一遍
        if (Array.isArray(handler)) {
            return "[" + handler.map(handler => {
                return genHandler(name, handler);
            }).join(",") + "]";
        }
        // 开始解析单个回调
        var isMethodPath = simplePathRE.test(handler.value);    
        var isFunctionExpression = fnExpRE.test(handler.value);
    
        // 没有modifier
        if (!handler.modifiers) {
            if (isMethodPath || isFunctionExpression) {
                return handler.value;
            }
            // 内连语句,需要包裹一层
            return "function($event){" + handler.value + ";}";
        }
        else {
            var code = "";
            var genModifierCode = ""; // 保存内部修饰符
            for (var key in handler.modifier) {
                if (modifierCode[key]) {
                    
                }
                // 精确修饰符
                else if (key === 'exact') {
    
                }
                // 普通按键
                else {
                    keys.push(key);
                }
            }
        }
    
        // 开始拼接事件回调
        // 拼接Vue定义外的按键修饰符
        if (keys.length) {
            code += genKeyFilter(keys);
        }
        // 把prevent和stop这样的修饰符在按键过滤之后执行
        if (genModifierCode) {
            code += genModifierCode;
        }
        // 事件主体回调
        var handlerCode = isMethodPath ?
            // 执行你绑定的函数
            "return " + handler.value + "($event)" :
            (
                isFunctionExpression
                ? "return " + handler.value + "$event"
                : handler.value
            );
        return `function($event){
            ${code + handlerCode}
        }`
    }
    

    拼接事件回调的3个重点

    1. 拼接按键的修饰符
    2. 拼接内置修饰符
    3. 拼接事件回调
    展开全文
  • compile
  • 1、创建vue项目后安装less,执行 npm install less less-loader –save-dev 下载版本为:less-loader@6.1.0 , less@3.11.3,重启服务报错,报错信息如下: 2、报错原因 less 本版太高需要降低版本,执行代码 先...
  • Vue源码(compile)

    2021-05-05 20:03:48
    compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图 代码如下 function Compile(el, vm) { ...
  • 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理... 【Vue原理】Compile - 白话版 终于到了要讲 compile 白话的时候了,大家准备好了吗,白话版肯定不会很复杂啦...
  • 一、Vue.use Vue.use(plugin); (1)参数 { Object | Function }plugin (2)用法 安装Vue.js插件。如果插件是一个对象,必须提供install方法。如果插件是一个函数,它会被作为install方法。调用install方法时...
  • vue源码—04编译compile

    2019-05-19 15:02:40
  • compile函数 compile函数的主要作用是编译模板。 function compile ( template: string, options?: CompilerOptions ): CompiledResult { //创建一个finalOptions const finalOptions = Object.create(base...
  • Vue.compile( template )

    2020-04-18 19:40:39
    Vue.compile( template ) Arguments: {string} template Usage: Compiles a template string into a render function. Only available in the full build. var res = Vue.compile('<div><span>{{...
  • vue-compile概述

    2017-10-18 09:16:00
    Vue的核心可以分为三个大块:数据处理和双向绑定、模板编译、虚拟dom。 前面我们对第一部分的主要内容双向绑定做了一个分析讲解,接下来我们说一说模板编译。 这一部分的内容比较多,也比较复杂。由于所涉及的情况...
  • Vue中你不知道但却很实用的黑科技

    千次阅读 2016-12-05 18:13:14
    最近数月一直投身于 iView 的开源工作中,完成了大大小小 30 多个 UI 组件,在 Vue 组件化开发中积累了不少经验。其中也有很多带有技巧性和黑科技的组件,这些特性有的是 Vue 文档中提到但却容易被忽略的,有的更是...
  • 1、创建vue项目后安装less,执行npm install less less-loader --save-dev 下载版本为:less-loader@6.1.0 ,less@3.11.3,重启服务报错,报错信息如下: 2、报错原因 less 本版太高需要降低版本,执行代码 先...
  • 如何关闭vue-cli3+中的代码校验 在vue项目的根目录下创建 vue.config.js目录 module.exports = { lintOnSave: false //false为关闭true为开启 }
  • vue 判断首次进入还是刷新

    千次阅读 2020-02-28 14:09:10
    1.这个方法必须刷新了才是刷新,否则永远都是首次被加载,且刷新过后执行的永远都是页面被刷新,自我感觉不好使 if (window.performance.navigation.type == 1) { console.log("页面被刷新") ...
  • Vue】运行vue项目Failed to compile.

    万次阅读 2019-09-22 21:42:59
    重装电脑,重新搭建环境,搭好...Failed to compile. ./node_modules/element-ui/packages/form/src/label-wrap.vue?vue&type=script&lang=js& (./node_modules/vue-loader/lib??vue-loader-options!./...
  • 简单实现Vue之指令解析器Compile类 创建html文件 文件名为index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta ...
  • 当参考vue-element-admin 源码的时候 人家面包屑用的 path-to-regexp。引用的时候也是在当前页面 import pathToRegexp from ‘path-to-regexp’ 直接 这么导入 到时候直接 pathToRegexp点什么什么什么。 我发现到了...
  • runtime-compile: 流程:template->ast(抽象语法树)->render->vdom->ui (性能低,代码多) runtime-only: 流程: render->vdom->ui (性能高,代码少) ...
  • vue面试题总汇

    千次阅读 多人点赞 2019-02-22 15:26:53
    vue面试题总汇 vue的底层原理? vue组件之间的通信? JS中判断数据类型的方法有几种? 最常见的判断方法:typeof 判断已知对象类型的方法: instanceof 根据对象的constructor判断: constructor 无敌万能的...
  • vue经典面试题

    千次阅读 2019-10-19 21:32:11
    Vue面试中,经常会被问到的面试题/Vue知识点整理 726 看看面试题,只是为了查漏补缺,看看自己那些方面还不懂。切记不要以为背了面试题,就万事大吉了,最好是理解背后的原理,这样面试的时候才能侃侃而谈。不然,...
  • vue面试题

    千次阅读 多人点赞 2019-08-12 17:02:53
    vue面试题 1.Vue和react的相同与不同 相同点: 都支持服务器端渲染 都有virtual DOM,组件化开发,通过props参数进行父子组件数据的传递,都实现webComponent规范 数据驱动视图 都有支持native的方案,react native...
  • 这部分内容不算 Vue.js 的响应式核心,只是用来编译的,笔者认为在精力有限的情况下不需要追究其全部的实现细节,能够把握如何解析的大致流程即可。 由于解析过程比较复杂,直接上代码可能会导致不了解这部分内容的...
  • 具体错误如下: 94% asset optimization ERROR Failed to compile with 1 errors 14:27:08 This dependency was not foun...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 11,284
精华内容 4,513
关键字:

compilevue

vue 订阅