【深入浅出地学习Vue】——vue2

VUE2

如今,AI盛行,我一直困惑于:既然AI如此强大,对于原理性的东西,是否还有必要继续深入学习?尤其现在VUE3成为趋势,还有必要继续研究VUE2么。虽然到现在,困惑也没有全然解决,但是,如果困惑还在,那就先行动吧,至少在对vue2的深入学习中,能够学习到不少关于框架的整体设计思想、核心原理的设计思路,从而在学习vue3的时候能够有一个整体性的认知,毕竟以史为鉴,可以知兴衰,把握了vue2的相关优势与缺点,才能够对vue3的出现与流行有一个更清晰的认知。而且,如今ai的盛行,使得前端工程师不得不面临一个身份的转换,我们不能再是重复性工作的搬运者,我们需要理解业务、形成产品思维,进行架构设计与技术决策,关注用户体验与创意设计,着重于沟通协作与团队贡献。因此,对于原理性的东西就必须理解到位,不能停留在能用、会用的层面,因为这个程度,ai轻而易举就能做到,而对于在深层次中出现的问题,就只有当我们把握了原理性知识,才能够准确地定位发现问题并解决问题,如此才不会被ai全然替代。

对象的变化检测

Object.defineProperty()

使用Object.defineProperty()进行数据变化追踪,同时收集依赖,即在getter中收集依赖,在setter中触发依赖。

js 复制代码
var data = {};
function defineReactive(data, key, val){
  var dep = []; // 依赖
  // 变化追踪
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      this.dep.push(window.target); // 收集依赖(假定依赖为window.target的一个函数)
      return data[key];
    },
    set: function(newVal) {
      if(val === newVal) {
        return
      }
      for (let i = 0; i < dep.length; i++) {
        dep[i](val, newVal); // 触发依赖
      }
      data[key] = newVal
    }
  })
}

在完成依赖收集之后,依赖触发这个任务则交由watcher完成

watcher相当于一个中介角色,数据变化时通知到它,它在通知其他地方。

对于依赖的收集,该如何处理?

首先考虑的是一个数组dep,在getter中将依赖收集至dep中,在setter中循环dep触发依赖。纯数组收集依赖会存在耦合情况,因此创建Dep类来实现依赖的管理,包括收集依赖、删除依赖、向依赖发送通知等。

依赖是什么?

所谓的收集依赖,就是当属性发生变化后,需要通知的地方。 要通知用到数据的地方可能会有很多,而且类型不同,其所用之处可能是模板,也可能是用户所写的一个监听属性,因此watcher由此产生------即抽象出来的一个可以集中处理不同情况的类,当数据变化时,直接通知该watcher,再由watcher通知到其他地方

Watcher的原理是:先把自己设置到全局唯一的指定位置(例如window.target),然后读取数据,因为读取了数据,即触发数据的getter,在getter中就会从全局唯一指定位置读取当前正在读取数据的Watcher,并把这个Watcher收集到Dep中去。通过这样的方式,Watcher可以主动去订阅任意一个数据的变化。

递归侦测数据中所有属性

Observer类递归侦测所有key。 Observer类会附加到每一个被侦测的object上, 一旦被附加上,Observer会将object所有属性转换为getter/setter形式 来收集属性的依赖,并且当属性发生变化时会通知这些依赖。

关于无法监听Object类型数据变化的问题

Vue.js通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性 ,所以会导致在对象上直接新增属性、删除属性时无法侦测到变化。为解决这个问题vue.js提供了vm. <math xmlns="http://www.w3.org/1998/Math/MathML"> s e t 与 v m . set与vm. </math>set与vm.delete两个api。

Data、Observer、Dep和Watcher之间的关系

Data通过Observer转换成了getter/setter的形式来追踪变化。当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。当数据发生变化时,会触发setter,从而向Dep中的依赖(Watcher)发送通知。Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。

数组的变化检测

数组的不同的变化侦测方式

由于Object数据的侦测方式是通过getter/setter实现的,所以通过Array原型上的方法来改变数组的内容时,Object的侦测方式显然时行不通的。

所以对于数组,使用拦截器覆盖Array原型,将拦截器方法挂载到数组属性上,从而实现侦听

数组的拦截器

拦截器其实就是一个和Array.prototype一样的Object,里面包含的属性一模一样,但这个Object中某些可以改变数组自身内容的方法是经过处理的(Observer)。

数组的依赖收集

创建拦截器即得到了一种能力:当数组的内容发生变化时得到通知的能力。 当该通知谁?即通知Dep中的依赖(Watcher)。 因此,需要对数组的依赖进行收集。

Array的依赖也和Object一样,在定义响应式数据(defineReactive)中收集。

Array也是在getter中收集依赖,在拦截器中触发依赖。

【注意】数组的依赖列表存放在Observer中。(依赖必须在getter和拦截器中都可以访问到)。在将数据进行响应式转换之前通过__ob__属性判断数据是否已转换为响应式数据,若没转换,则使用Observer进行数据转换,并且新增一个__ob__属性。

数组中的元素的变化侦测

所有响应式的子数据都需要侦测(在Observer新定义一个转换方法observeArray将数组子集转换为响应式)。而且数组新增的元素也需要转换为响应式(在拦截器中,收集不同的方法所操作的新增元素,然后同样调用observeArray将元素转换为响应式的)。

数据的变化检测实现方式所带来的问题

  • 通过下标修改元素时无法监测到数组的变化
  • 通过修改数组长度操作数组时无法监测到数组的变化

变化检测相关api

vm.$watch

用于观察一个表达式或computed函数(可能会涉及多个响应式数据)在vue.js实例上的变化

用法: vm.$watch(expOrFn: String | Function, callback: Function | Object, [options]: Object)

返回值:{Function}unwatch,取消观察函数,用来停止触发回调。

【注意】对于watcher监听类,由于需要返回一个取消观察函数unwatch,因此需要在watcher中记录其实例被收集在dep中的情况,并在用户调用unwatch的时候,将对应依赖在dep中进行移除。

vm.$set

用于在object上设置一个属性,若object是响应式的,vue.js会保证属性被创建后也是响应式的,并且触发视图更新。

用法:vm.$set(target: Object | Array, key: String | Number, value: any)

返回值: {Function} unwatch

vm.$delete

删除对象的属性,若对象为响应式的,需确保删除能触发更新视图。

用法:vm.$delete(target: Object | Array, key: String | Number)

虚拟DOM

虚拟DOM是将状态映射为视图的一种解决方案,它的运作原理是使用状态生成虚拟节点,再由虚拟节点渲染出视图。

虚拟DOM在vue.js中的作用是:生成vnode,对比新旧vnode,并通过对比结果,只对需要更新的部分来操作DOM从而更新视图。

什么是虚拟DOM,为什么需要虚拟DOM

随着页面中需实现的功能变得复杂,程序中要维护的状态变得繁多,DOM操作变得频繁,若此时频繁直接操作DOM,会使得程序中的状态难以管理,代码逻辑变得混乱。

状态 可以是js中的任意数据类型。本质上,将状态作为输入,并生成DOM输出到页面上显示出来,这个过程叫做渲染

程序运行时,状态在不断发生变化(用户操作、请求等异步行为),每当状态发生变化时,都需要重新渲染。全部DOM重新渲染还是有什么更好的解决方案?虚拟DOM便是其中一种解决方案。

虚拟DOM的解决方式是通过状态生成一个虚拟节点树,然后使用虚拟节点树进行渲染。在渲染之前,会使用新生成的虚拟节点树和上一次生成的虚拟节点树进行对比,只渲染不同的部分。

在Vue.js 1.0中,当状态变化时,在一定程度上使知道哪些节点使用了该状态,由此可以对节点直接进行更新操作,根本不需要比对。但由于粒度太细,每一个绑定对应一个watcher来观察状态变化,因此会有内存开销以及依赖追踪的开销,当状态被越来越多的节点使用时,开销会很大。

在Vue.js 2.0中,监听粒度改为中等,即引入虚拟DOM。组件级别为一个watcher实例 ,也就是即便组件内有10个节点使用了某个状态,但也只有一个watcher在观察状态的变化。当状态变化时,只通知到组件,组件内通过虚拟DOM进行比对与渲染

vue.js中的虚拟DOM

vue.js使用模板描述状态与DOM之间的映射关系, vue.js通过编译将模板转换成渲染函数, 执行渲染函数得到一个虚拟节点树, 然后使用虚拟节点渲染页面。

为避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点vnode与上一次使用的虚拟节点oldvnode做对比(涉及到核心算法patch),找出真正需要更新的节点来进行DOM操作,从而避免操作其他无任何改动的DOM。

VNode

VNode是一个类,可以生成不同类型的vnode实例,不同类型的vnode代表不同类型的真实DOM。vnode本质上为一个js的普通对象,从VNode类实例化而来。vnode可以理解为节点描述对象,他描述了应该怎样创建真实DOM节点。

vnode-> create to Dom -> insert to 视图

VNode节点类型

  • 注释节点 只有两个有效属性,text,isComment

  • 文本节点 只有一个text属性

  • 元素节点 四个有效属性,tag------节点名称,data------节点数据,children------当前节点子节点列表,context------当前组件的vue实例

  • 组件节点 与元素节点类似,有以下独有属性:componentOptions------组件节点选项参数,componentInstance------组件实例,即vue实例

  • 函数式组件 与组件节点类似,有两个独有属性:functionalContext,functionalOptions

  • 克隆节点 克隆节点为将现有节点的属性复制到新节点中,让新创建的节点和被克隆的节点属性保持一致,从而实现克隆效果。它的作用为优化静态节点和插槽节点。

    以静态节点为例,因其内容不会改变,所以除了首次渲染需要执行渲染函数生成vnode之外,后续更新不需要再执行渲染函数生成vnode,直接使用创建克隆节点的方式克隆旧的vnode从而进行渲染,由此一定程度上提升性能。

patch

将vnode渲染成真实DOM。 通过patch算法计算出真正需要更新的DOM节点,最大限度地减少DOM操作(因为DOM操作的执行速度远不如js的运行速度快,也就是使用js的计算成本来换取DOM的操作成本),从而显著提高性能。

patch的过程其实就是创建节点、删除节点、修改节点的过程。

patch的过程

当oldVNode不存在时,直接使用vnode渲染视图; 当oldVNode和vnode都存在但不是同一个节点时,使用vnode创建的DOM元素替换旧的DOM元素; 当oldVNode和vnode为同一个节点,使用更详细的对比操作对真实DOM节点进行更新。

创建节点的过程

vnode为元素节点------>通过createElement创建元素节点, vnode为注释节点------>通过createComment船舰注释节点, 否则通过createTextNode文本节点, 然后将创建的节点插入到指定父节点中。

删除节点的过程

【注意】在vue.js中,对于节点的一系列操作,都不是直接调用类似parent.removeChild(child)这种原生的DOM操作方法,而是将节点操作封装成函数并放在指定的节点操作方法对象中,这主要时为了实现跨平台渲染,让框架的渲染机制和DOM解耦。

更新节点的过程

当新旧两个节点为同一个节点时才需要更新元素节点。 在更新节点时,需判断节点是否为静态节点,若是,则不需要进行更新操作。

若新虚拟节点有文本属性,且新旧虚拟节点的文本属性不一样,可直接把视图中真实DOM节点的内容改为新虚拟节点的文本。 若新虚拟节点无文本属性,即为元素节点,需判断新旧虚拟节点是否有children属性,若都有,则进行更详细的对比更新,若新虚拟节点有children,旧虚拟节点无children,说明旧虚拟节点为空节点或文本节点,旧虚拟节点为文本节点时将文本清空,然后循环遍历新虚拟节点的children并创建为真实DOM元素节点并插入到视图中的DOM节点下。

新创建的虚拟节点无text、children属性时,即为空节点,若旧虚拟节点有文本或子节点,则进行删除操作,达到视图中的空标签目的。

更新子节点的过程

更新子节点分为4种操作:更新节点、新增节点、删除节点、移动节点位置。

新旧两个子节点列表进行循环对比,循环新子节点列表(newChildren),每循环到一个新子节点,在旧子节点列表中(oldChildren)查找是否有相同节点,若有,做更新操作 ,若无,说明新子节点为新增节点,创建节点并做插入操作 ,若新子节点和找到的旧子节点位置不同,则需进行移动操作

创建子节点

【注意】对于新增的子节点,需要把创建的节点插入到oldChildren中所有未处理节点的前面。 为什么不是插入到已处理节点的后面?因为如果存在多个新增子节点,由于是使用虚拟节点做对比,而非真实DOM节点做对比,已处理的节点列表中并不包括新插入的节点,这时用"插入到已处理节点后面"的逻辑来插入新子节点,明显会存在插入位置错误的问题。

更新子节点

当一个子节点同时存在于新旧子节点列表中且位置相同,直接更新子节点。

移动子节点

当一个子节点同时存在于新旧子节点列表中,但是位置不同,需要在真实DOM中将这个节点的位置以新虚拟节点的位置为基准进行移动。

那么如何确定新虚拟节点移动的目标位置? 由于两个子节点列表在对比的时候,是从左到右循环newChildren,对于每一个子节点,在oldChildren中查找对应的节点进行对比,即在newChildren中当前被循环到的节点左边的都是被处理过的,即该节点的位置为所有未处理节点的第一个节点

删除子节点

本质上为删除那些oldChildren中存在但newChildren中不存在的节点。

当newChildren中所有节点都被循环一遍之后,即循环结束之后,若oldChildren中还有剩余的未被处理的节点,那么这些节点就是被废弃、需要删除的节点。

优化策略

如果newChildren与oldChildren的所有节点的位置都是相同的,这时节点的位置是可预测的,即可以尝试使用相同位置的两个节点对比是否为同一个节点,若是,则直接进入更新节点操作,若不是,则再用循环方式来查找节点,这样能很大程度地提升执行速度。这种快速查找节点的方式成为快捷查找,共有四种查找方式:

其中, 【新前】newChildren中所有未处理的第一个节点 【新后】newChildren中所有未处理的最后一个节点 【旧前】oldChildren中所有未处理的第一个节点 【旧后】oldChildren中所有未处理的最后一个节点

  • 新前与旧前 若两个节点属于位置相同的同一个节点,直接更新节点, 否则进行第二种对比查找方式。

  • 新后与旧后 若两个节点属于位置相同的同一个节点,直接更新节点, 否则进行第三种对比查找方式。

  • 新后与旧前 若两个节点属于位置不同的同一个节点,更新节点且移动节点(把当前节点移动到oldChildren中所有未处理节点的最后面), 否则进行第四种对比查找方式。

  • 新前与旧后 若两个节点属于位置不同的同一个节点,更新节点且移动节点(把当前节点移动到oldChildren中所有未处理节点的最前面), 否则进行循环对比查找方式。

    已更新过的节点无论是节点内容还是节点位置都是正确的,无需再进行更改,因此只需要在所有未更新的节点区间内进行移动和更新操作即可。

哪些节点是未处理过的

由于优化策略,从前到后的循环方式不再适用,需采用从两边向中间的循环方式。

两边向中间循环的方式,准备四个变量:oldStartIdx,oldEndIdx,(oldChildren开始和结束位置的下标)newStartIdx,newEndIdx(newChildren开始和结束位置的下标)

js 复制代码
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
}

事实上,只有newChildren与oldChildren中有一个循环完毕,就会退出循环,当新旧子节点 节点数量不一致时,会导致结束后仍然有未处理的节点,即此循环无法覆盖所有节点,但这种方式可以减少循环次数,从而提高性能。

而对于无法覆盖的剩余节点,即若oldChildren先循环完毕,此时newChildren还有剩余节点,说明这些节点都是新增节点(范围为newStartIdxnewEndIdx),直接进行插入节点到DOM上即可。若newChildren先循环完毕,此时oldChildren中有剩余节点,说明这些节点都是废弃节点(范围为oldStartIdxoldEndIdx),直接进行节点删除即可。

模板编译原理

渲染函数是创建HTML最原始的方法。模板最终会通过编译转换为渲染函数,渲染函数执行后会得到一份vnode用于虚拟DOM的渲染。

模板编译的主要目标为生成渲染函数。

模板编译的作用:模板经过模板编译之后生成渲染函数,渲染函数执行后会生成虚拟DOM(VNODE),通过VNODE渲染生成真实DOM。

vue 的模板语法

vue.js的模板语法声明式的描述数据状态与DOM之间的绑定关系,然后通过模板生成真实DOM以展示在用户页面。

模板编译成渲染函数的过程

  • 模板解析为AST(抽象语法树)--解析器(html解析器、文本解析器、过滤器解析器)

  • 遍历AST标记静态内容 -- 优化器(主要作用是避免一些无用功来提升性能)

  • 使用AST生成渲染函数 -- 代码生成器

    模板 =》 解析器 =》 优化器 =》 代码生成器 =》 渲染函数

    AST使用js对象来描述节点,一个对象代表一个节点,对象中的属性用来保存节点中所需的各种数据。

解析器

【注意】当模板被解析器解析成AST时,会根据不同元素类型设置不同的type值,type的具体取值如下: 1:元素节点; 2:带变量的动态文本节点; 3:不带变量的纯文本节点。

解析器的作用

解析器作用是通过模板获取到AST。

【注意】AST是用js中的对象来描述一个节点所形成的一个节点树,一个对象表示一个节点,节点中的属性用来保存节点所需的各种数据。

生成AST需要借助到HTML解析器。

HTML解析器触发不同的钩子函数以构建不同的节点。 通过栈获取当前构建节点的父节点,并将当前构建节点添加到父节点的后面,html解析完毕后,会获得一个具有DOM层级关系的AST。

HTML解析器内部原理

HTML解析器的作用为解析HTML,它在解析HTML的过程中会不断出发各种钩子函数:包括开始标签钩子函数、结束标签钩子函数、文本钩子函数、注释钩子函数。

解析器是从前向后解析的。

解析器会一小段一小段的截取模板字符串,然后每截取一小段字符串,根据它的类型来调用不同的钩子函数,直至模板字符串为空停止解析。

文本类型分为带变量,不带变量的文本,带变量文本需用文本解析器进行二次加工。

js 复制代码
parseHtml(template, {
  // 开始标签钩子函数
  start(tag,attrs, unary) {
    // j解析到变迁的开始位置时,触发该函数
  },
  // 结束标签钩子函数
  end() {
    // 解析到标签的结束位置时,触发该函数
  },
  // 文本钩子函数
  charts(text) {
    
  },
  // 注释钩子函数
  comment(text) {
    // 解析到注释时,触发该函数
  }
})

构建AST的层级关系

注意,抽象语法树AST是具有层级关系的,一个AST节点具有父节点和子节点。要构建AST的层级关系,只需要维护一个栈(stack)即可,用栈来记录层级关系,这个层级关系也可以理解为DOM的深度。

HTML解析器在解析HTML时,是从前向后解析。每当遇到开始标签时触发start钩子函数,并把当前构建的节点推入栈中;每当触发end钩子函数时,就从栈中弹出一个节点;这样就保证每当触发start钩子函数时,栈的最后一个节点就是当前正在构建的节点的父节点。

HTML解析器的运行原理

解析HTML模板的过程即为循环的过程,简而言之就是循环HTML模板字符串,每轮循环从HTML模板中截取一小段字符串,重复以上过程,直到HTML模板字符串被截成一个空字符串时结束循环,解析完毕。

解析器如何明确知道它该截取那些字符串?比如开始标签、结束标签、HTML注释、DOCTYPE、条件注释、文本等。

  • 截取开始标签 借助正则表达式分辨模板的开始位置是否符合开始标签的特征,从而截取开始标签,在截取到开始标签后,还要解析出标签名(tagName)、属性(attrs)、以及自闭合标识(unary),以此作为参数传递给start钩子函数。
  • 截取结束标签 同样是借助正则表达式判断模板是否符合结束标签特征,截取掉结束标签并且触发end钩子函数。
  • 截取注释 注意,注释的钩子函数可以通过选项配置,只有options.shouldKeepComment为真时,才会触发钩子函数,否则直接去模板,不触发钩子函数
  • 截取条件注释 条件注释不触发钩子函数,只需截取掉即可。所以vue.js中的条件注释其实没有用。
  • 截取DOCTYPE DOCTYPE与条件注释相同,不需要触发钩子函数。
  • 截取文本 如果HTML模板的第一个字符不是<,那它一定是文本,而只需要找到下一个<在什么位置,在该位置之前的则都属于文本。 注意,需要解决模板为这种的特殊情况:1<2,具体思路为:如果将<前面的字符截取完之后,剩余的模板字符串不符合任何需要被解析的片段的类型,则说明该<是文本的一部分。 最后,在截取完文本之后,需要将截取的文本作为参数回传并触发chars钩子函数。
  • 纯文本内容元素的处理 script、style、textarea这三种元素叫做纯文本内容元素。

文本解析器

会发现,在前文的HTML解析器中已经对文本进行解析了。为什么还要使用文本解析器对文本进行二次加工?

因为HTML模板中的文本分为两种类型:纯文本和带变量的文本。

在HTML解析器中,并不会区分文本是否为带变量的文本。具体来说,就是当HTML解析器解析到文本时,会触发chars函数,在chars函数中,构建文本类型的AST,并将其添加到父节点的children属性中。在构建文本AST时,根据文本的类型,若是带变量的文本,则需要借助文本解析器对它进行二次加工。

js 复制代码
// HTML解析器
parseHtml(template, {
  // 开始标签钩子函数
  start(tag,attrs, unary) {
    // j解析到变迁的开始位置时,触发该函数
  },
  // 结束标签钩子函数
  end() {
    // 解析到标签的结束位置时,触发该函数
  },
  // 文本钩子函数
  charts(text) {
    text = text.trim();
    if(text) {
      const children = currentParent.children;
      let expression;
      // 使用文本解析器判断文本是否为带变量的文本
      if(expression = parseText(text)) {
        children.push({
          type: 2,
          expression,
          text,
        })
      } else {
        // 纯文本
        children.push({
          type: 3,
          text
        })
      }
    }
  },
  // 注释钩子函数
  comment(text) {
    // 解析到注释时,触发该函数
  }
})
文本解析器的运行原理

使用正则表达式匹配文本,用于校验文本中是否有花括号语法变量({{xxx}),若是纯文本则返回undefined,否则对文本进行二次加工。

小拓展

由于此小节的解析器中使用了大量的正则表达式,所以做一个拓展

正则表达式模式匹配和验证的相关方法
  • test():RegExp 对象方法 用于快速测试字符串是否匹配正则表达式 语法:regexpObj.test(str) 其中regexpObj------正则表达式对象,str------被测试的字符串 返回值:布尔值

  • exec(): RegExp 对象方法 在字符串中执行匹配搜索,返回一个包含匹配结果的数组,如果没有匹配则返回null, 可获取到详细匹配信息,适合需要多次匹配或处理捕获组的场景 语法:regexpObj.exec(str) 其中regexpObj------正则表达式对象,str------被测试的字符串 返回值:数组 或 null 返回值数组arr包含以下额外属性 arr[0]: 完整的匹配文本 arr[1],arr[2], ...: 捕获组(如果有) arr.index:匹配到的字符位于原始字符串的索引位置 arr.input: 原始字符串 arr.groups: 命名捕获组对象

  • match(): String对象方法 在字符串上调用,根据正则表达式返回匹配结果。返回值的格式取决于正则表达式是否有带g标志。 语法:str.match(regexpObj); 返回值: 正则没有g标志,返回值类似于exec()的返回的数组(包含捕获组、index、input等); 正则没有g标志,返回一个包含所有匹配的子字符串的数组(不包含捕获组、index等信息)。

  • matchAll(): String对象方法,ES2020新增 在字符串上调用,解决 match 带 g 时无法获取捕获组的问题。它返回一个迭代器,每次迭代返回一个类似 exec 的结果数组(包含捕获组)。 语法:str.matchAll(regexpObj); 返回值:迭代器,使用扩展运算符可以得到所有匹配(包含捕获组)的结果数组

正则表达式对象的自身属性
  • lastIndex 每个正则表达式对象都有一个 lastIndex 属性,它用于指定下一次匹配开始的起始索引。这个属性主要在与全局(g)或粘性(y)标志一起使用时发挥作用。 若正则表达式带有g: 每次成功调用exec或test之后,lastIndex会被设置为匹配文本的下一个字符的索引位置,若匹配失败,lastIndex重置为0,可手动修改lastIndex的值来控制下一次搜索的起始位置。

    若正则表达式带有y: 匹配必须从lastIndex指定位置开始,即就算正则表达式中使用了^来匹配行首,也是会匹配指定的lastIndex索引位置。若匹配成功,lastIndex更新为匹配文本的下一个字符索引位置,否则重置为0。

优化器

优化器的作用

优化器的作用是在AST中标记静态子树。

标记静态子树好处:

  • 每次重新渲染的时候,无需为静态子树创建新节点
  • 在虚拟DOM中的打补丁(patching)过程可以跳过。

优化器内部实现步骤

在AST中找出所有静态节点 并打上标记;然后在AST中找出所有静态根节点并打上标记。(均采用递归方式从上向下标记)

静态节点即永远都不会发生变化的节点(在AST中即static为true的节点)。 静态根节点即如果一个节点下面所有子节点都是静态节点,并且它的父级式动态节点,那么它就是静态根节点(在AST中即staticRoot为true的节点)。

在AST的节点对象中,不同元素的类型根据节点元素的type判断,当type为1时表示的是元素节点,如果元素节点只用了指令v-pre,则说明是一个静态节点,否则需满足以下调节才会认为该元素节点为静态节点:

  • 不可以使用动态绑定语法,即标签上不能有v-、@、:开头的属性。
  • 不可使用v-if、v-for、v-else指令
  • 不能是vue内置标签(commonent、slot)
  • 不能是组件,即标签名必须为保留标签。(div为保留标签,list不是保留标签)
  • 当前节点父节点不能是带v-for指令的template标签。
  • 节点中不存在动态节点才会有的属性
找出所有静态节点并标记

具体方式为从根节点开始,进行静态节点判断,然后一层一层递归遍历处理子节点,直到所有节点被处理结束后程序结束。

【注意】在子节点打完标记后,需对其进行静态节点的判断,若该子节点为非静态节点,需将它的父节点的static设置为false。

找出所有静态根节点并标记

与静态节点标记过程类似,不同点在于若当前节点被判定为静态根节点,将不再继续向下寻找。 【注意】有个特殊情况是,若元素节点只有一个文本节点,则该节点不会被标记为静态根节点,因为其优化成本大于利益。

【......持续学习中】

学习出处[深入浅出vue]
相关推荐
求知若饥2 小时前
webpage-channel 让不同页面通信像组件通信一样简便
前端·typescript·node.js
图扑软件2 小时前
图扑 HT 帧动画 | 3D 动态渲染设计与实现
前端·javascript·3d·动画·数字孪生
终端鹿2 小时前
Pinia 与 Vue Router 权限控制实战(衔接Pinia基础篇)
前端·javascript·vue.js
啥咕啦呛2 小时前
3个月前端转全栈计划
前端
BradyC2 小时前
laya编译内存溢出问题
前端
木斯佳2 小时前
前端八股文面经大全:阿里云AI应用开发一面(2026-03-20)·面经深度解析
前端·人工智能·阿里云·ai·智能体·流式打印
我叫黑大帅2 小时前
JS中的两大定时器
前端·javascript·面试
掘金安东尼3 小时前
⏰前端周刊第 458 期v2026.3.24
前端·javascript·面试
前端付豪3 小时前
实现必要的流式输出(Streaming)
前端·后端·agent