草庐IT

Vue源码剖析

Echoyya、 2023-03-28 原文

Vue 响应式数据

什么是响应式数据:数据变了,视图能更新,反之视图更新,数据要不要更新,不归响应式数据管。
Vue 在内部实现了一个最核心的defineReactive方法,借助了Object.defineProperty,核心就是劫持属性(只会劫持已经存在的属性),把所有的属性,重新的添加了 getter 和 setter,因此在用户取值和设置值的时候,可以进行一些操作。

  • 对象:多层对象需要通过递归来实现劫持。
  • 数组:考虑性能原因没有用 defineProperty 对数组的每一项进行劫持,而是选择重写数组的(push,shift,pop,unshift,sort,splice,reverse)方法,数组中如果是对象数据类型也会进行递归劫持,数组的索引和长度变化是无法监控到的。

Vue 中如何进行依赖收集

  • 每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher,当属性变化后会通知自己对应的 watcher 去更新
  • 默认在初始化时会调用 render 函数,此时会触发属性依赖收集 dep.depend()
  • 当属性发生修改时会触发 watcher 更新 dep.notify()

Vue 在初始化的时候会进行挂载$mount操作,会进行编译操作,最终会走到render function,当组件进行渲染时会去取值,取值getter时,调用dep.depend()收集这个 watcher,存放在Dep中,当我们去更改值setter,调用dep.notify()去通知这个 watcher 去更新,实际上 watcher 中存放的就是组件的update函数.更新的时候,就会走到虚拟 dom 相关的方法。

Vue 中模板编译原理

模板编译原理实际上就是 将 template 转换成 render 函数,大致可分为以下三步:

  1. 将 template 模板转换成 ast 语法树 - parserHTML
  • 定义一个 stack 栈,存放标签的父子关系
  • 通过正则匹配模板字符串,不停的解析,不停的删除,直至字符串解析完成,
  • 得到 ast 树,(存放标签名,子节点,及属性列表)
  1. 对静态语法做静态标记 static,会递归遍历子节点进行标记,组件和插槽不属于静态语法 - markUp
  • 只有在第一次编译时,会进行静态标记,不是每次渲染都标记
  • 静态标记主要是用来做 diff 优化的,静态节点跳过 diff 操作
  • 子节点有一个变化,父节点都不是静态的
  1. 生成代码,核心就是拼接字符串(_c,_v,_s),最终加上with语法 - codeGen

Vue 生命周期钩子

  • Vue 的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法。
  • 内部会对钩子函数进行处理,将钩子函数维护成数组的形式
  • 首先会采用策略模式,对 hook 进行合并 mergeHook(),合并成队列,然后依次调用
function mergeHook(parentVal, childVal) {
  const res = childVal // 儿子有
    ? parentVal
      ? parentVal.concat(childVal) // 父亲也有,就是合并
      : Array.isArray(childVal) // 儿子是数组
      ? childVal
      : [childVal] // 不是数组包装成数组
    : parentVal;
  return res ? dedupeHooks(res) : res;
}
  1. beforeCreate 在实例初始化 init 之后,数据初始化(data observer)之前调用,拿不到响应式的状态,可以拿到$on、$events 以及一些父子关系。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。
  2. created 数据初始化完毕后调用,实例已经创建完成。完成数据观测(data observer),属性和方法的运算,可以直接用响应式数据。但是没有$el,不能进行 dom 操作。
  3. beforeMount 在挂载开始之前被调用(在 mountComponent 方法中被调用):之后相关的 render 函数首次被调用。
  4. mounted el 被新创建的真实的 vm.$el 替换,并挂载到实例上后调用该钩子。此阶段可以获取渲染后的节点。
  5. beforeUpdate 数据更新前调用,在创建 Watcher 时会传一个 before 方法,它里面会调用 beforeUpdate 钩子,每次页面更新都会去调用当前的渲染 watcher,会判断有没有 before 方法,有的话就会调用 beforeUpdate, 发生在虚拟 DOM 重新渲染和打补丁 patch 之前。然后再去执行 watcer.run()真实的更新方法。
  6. updated 执行完 watcer.run()之后,调用 updated 钩子,表示 dom 已完成更新。 (执行数据更改导致的虚拟 DOM 重新渲染和打补丁)。注意避免在此期间更新数据,因为可能会导致为无限循环的更新。
  7. beforeDestroy 实例销毁之前调用。仅作为实例即将的信号,实例仍然完全可用。之后会进行一系列的卸载操作。执行真正的卸载(从父节点中移除、清空自己的 watcher、卸载所有的属性、标记当前组件销毁状态、把虚拟节点也销毁掉、然后调 destroyed)。可以在这时进行一些收尾工作如清除定时器等。
  8. destroyed 实例销毁后调用。移除所有的事件监听器(否则会导致内存泄漏),销毁所有子实例。设置当前虚拟节点的父节点为 null。该钩子在服务器端渲染期间不被调用。

Vue 组件 data 为什么必须是个函数?

组件复用,需要每个组件中都有自己的 data,这样组件之间才不会相互干扰,组件中的 data 如果写成对象形式,就使多个组件实例会共享一份 data,一个数据变化后,会影响其他实例中的数据。
因此每次使用组件时都会对组件进行实例化操作后,调用 data 函数返回一个对象作为组件的数据源。这样可以保证多个组件间数据互不影响。

而根实例(new Vue())采用单例模式,且不需要任何的合并操作,所以根实例的 data 属性可以是函数,也可以是对象,实际上源码中根本的判断条件为 vm 属性,只有根才有 vm 属性,组件和 mixin 都没有 vm 属性,因此可以作为判断条件,区分 data 是否为函数。并给出相关报错信息。

nextTick 原理

当用户修改了数据后并不会马上更新视图,更新 DOM 时是异步执行的,只要侦听到数据变化,Vue 将开启一个任务队列,并缓冲同一时间循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。而 $nextTick 中的方法会被放到更新队列的后面,在下次 DOM 更新循环结束之后执⾏延迟回调,视图需要等队列中所有任务完成之后,再统一进行更新。在修改数据之后使⽤ $nextTick,则可以在回调中获取更新后的 DOM。

Vue 在内部对异步队列尝试使用原生的 Promise.then(微任务)、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0)(宏任务)代替。

set 方法实现原理

  1. 如果目标不存在或者是原始类型,直接报错,cannot set reactive property on undefined,null,or primitive value
  2. 如果是数组,Vue.set(arr,1,100),调用重写的target.splice(key,1,val)方法,可以更新视图
  3. 如果是对象,看这个对象本身有没有这个值,如果有就直接更新就好,因为他本身就是响应式的,
  4. 如果是根实例,或者根数据 data 时,会报错提示 应该在初始化时声明该数据
  5. 如果不是响应式数据,也不需要将其定义成响应式属性 Vue.set({},'age',18),相当于这个对象本身就不是响应式的,就直接赋值,也不需要更新视图
  6. 最后就把调用的属性定义成响应式的即可。调用defineReactive(ob.value,key,val)
  7. 通知视图更新 ob.dep.notify()

因此 Vue.set 实际上就是两个方法的集合,target.splice(key,1,val)defineReactive(ob.value,key,val),

虚拟 dom 的作用

是什么:Virtual DOM 就是用 js 对象来描述真实 DOM 结构,是对真实 DOM 的抽象。
为什么:由于直接操作 DOM 性能低,但是 js 层的操作效率高,可以将 DOM 操作转化成对象操作,最终通过 diff 算法比对新旧 vdom 的差异进行更新 DOM(减少了对真实 DOM 的操作)。
边操作 dom 边获取视图,每次操作 dom 都可能会引起 dom 的回流和重绘,导致性能不高,有了 vdom 就可以把所有的操作都放在 vdom 上,最终把更新和一系列的逻辑批量的同步到真实 dom 上,
好处:虚拟 DOM 不依赖真实平台环境从而也可以实现跨平台。比如 nodejs 就没有 Dom,想要实现 SSR 就需要借助 Vdom

diff 算法的实现原理

Vue 的 diff 算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较(双指针分别指向新旧的结尾)。

  1. 先比较是否是相同节点,判断属性 key + tag
  2. 相同节点比较属性,并复用老节点
  3. 比较儿子节点,考虑老节点和新节点儿子的情况
  4. 优化比较:头头、尾尾、头尾、尾头
  5. 比对查找进行复用

diff 的复杂度 是 O(n),当一方子元素的头尾相等时,结束循环,(因为同层比较,内部只有一层循环).子元素嵌套时,递归同层比较
如果不能匹配到的话,就会根据当前的老的索引 key 创建一个映射表,拿新的去里面找,如果能找到就复用,找不到就创建新的,最终把老的多余的删掉,

Vue 中 key 的作用和原理

  1. Vue 在 patch 过程中通过 key 可以判断两个虚拟节点是否是相同节点。 (可以复用老节点)
  2. 无 key 会导致更新的时候出问题,比如 unshift 变成 push 效果,并更新所有节点,有 key 时,就可以节点复用,仅做节点的移动即可。
  3. 尽量不要采用索引作为 key,而是使用数据的唯一标识

vue 初渲染流程

  • vue 初始化流程 _init:
  1. 默认会调用 vue._init 方法将用户的参数挂在到$options 选项上,vm.$options。(vue 调用的方法使用原型扩展的形式)
  2. vue 会根据用户的参数进行数据的初始化,data props computed watch 等 ,在外界是无法访问的,可以通过 vm._data 访问到用户的数据。
  3. 对数据进行观测,对象(递归使用 Object.defineProperty),数组(方法重写,切片编程),劫持到用户的操作,观测的目的是用户修改数据时 -> 更新视图
  4. 将数据代理到 vm 对象上,vm.xxx => vm._data.xxx
  • vue 挂载流程 $mount:
  1. 判断用户是否传入了 el 属性, 内部会调用$mount 方法,用户也可以自行调用该方法
  2. 处理模板优先级 render / template / outerHTML
  3. 将模板编译成函数, 步骤: parseHTML 解析模板 -> ast 语法树, generate 解析语法树生成 code -> new Function 生成 render 函数
  4. 通过 render 方法,生成虚拟 dom + 真实的数据 => 真实的 dom
  5. 根据虚拟节点渲染真实的节点

vue 更新流程 依赖收集实现过程

  1. vue 中使用了观察者模式,默认组件渲染的时候,会创建一个 watcher,并且会渲染视图
  2. 当渲染视图的时候,会取 data 中的数据,会走每个属性的 get 方法,就让这个属性的 dep 记录 watcher
  3. 同时让 watcher 也记住 dep,dep 和 watcher 是多对多的关系,因为一个属性可以对应多个视图,一个视图对应多个数据
  4. 如果数据发生变化,会通知对应属性的 dep,一次通知存放的 watcher 去更新

一个属性对应一个 dep, 一个 dep 对应多个 watcher(数据多页面共享)
一个组件对应一个 watcher,一个 watcher 可以对应多个 dep(多个属性)
观察者模式: dep 收集 watcher,变化时一次通知,watcher 是观察者,dep 是被观察者
dep 用来收集渲染逻辑(watcher),watcher 中存放的是组件的 update 函数。数据变化通知 dep 中的 watcher 去执行对应的 update 方法
页面重新渲染逻辑:只有当页面模板中用到的数据(就是写在 render 中的数据) 发生改变时,才会调用 update 方法

vue 异步更新的实现流程

开启一个异步队列并将更新的 watcher 去重,将用户的$nextTick 和内部的更新逻辑, 合并为一个 Promise.then,依次执行(多个 nextTick 是一个 promise.then)
nextTick 用一个异步任务,将多个方法维持一个队列里,执行时机遵循 js 的 eventloop 机制,具体的执行时机 ,要看底层用的是那个方法,因为 vue 考虑了浏览器的兼容性,vue 中对 nextTick 做了很多兼容性处理,promise 微任务 > MutationObserver(h5 的 api 微任务) > setImmediate > setTimeout

组件的初始化流程

  1. 第一步:创造组件的虚拟节点,创建虚拟节点的时候,内部会去调用 Vue.extend 方法,产生组件的构造函数 Ctor
  2. 第二步:给组件添加钩子函数,data.hook = {init},合并 mergeOptions (自己的组件.proto = 全局的组件),最终返回了一个虚拟节点
  3. 第三步:页面开始渲染,渲染的时候,会去调用 patch 方法,并且根据当前的虚拟节点,转换成真实节点,这时会去调用 createElm,创造真实节点。
  4. 第四步:创造真实节点的时候发现,如果这个节点是组件,就会调用组件的 createCompontent => 调用 hook.init 方法,
  5. 第五步: 此时 init 方法,会 new Ctor(),之后会进行子组件的初始化操作 this._init
  6. 第六步:最终再去调用组件的挂载操作$mount,产生一个$el 真实节点,对应组件模板渲染后的结果。
  7. 第七步:将组件的 vnode.componentInstance.$el 插入到父标签中

keep-alive 实现原理

keep-alive 组件是一个抽象组件, 也是一个虚拟组件, 不会被记录到父子组件关系当中,一般用在路由组件的外层, 主要为了缓存组件, 为频繁挂载销毁,提供缓存功能节约性能,

  • 包含 include 属性,添加白名单,表示那些组件需要缓存,切换过后才会进行缓存,并不是将白名单中的 name 直接全部缓存。
  • 包含 exclude 属性,添加黑名单,表示那些组件不用缓存
  • max = x 最多缓存几个组件, 如果超过最大限制 需要删除第一个, 在增加最新的 LRU
  1. created 钩子:创造一个对象 cache 来缓存组件,key[],表示缓存的是谁
  2. render():渲染
  3. mounted():挂载,通过 watch Api 监控 include 和 exclude 做缓存处理,pruneCache

render

获取 keep-alive 中的所有子组件,获取插槽中的第一个,根据组件的名称, 判断 include 和 exclude, 拿到后把组件的实例缓存起来
拿到组件的 key 用来做缓存,如果有缓存 获取缓存的实例,ABA,=>shift 以后再 push
缓存组件 会缓存子组件,缓存的是父节点的 el, 其中包含着所有子组件渲染后完整的结果。
第一次渲染完毕后,会把虚拟节点进行标记直接返回一个组件,keep-alive 最终渲染的结果就是第一个子组件

mounted

缓存中存放了 {组件的 key : 组件的实例},复用的时候,直接使用缓存中,组件的实例
如果超过最大限制 需要删除第一个,在增加最新的,遵循 LRU 原则(Least Recently Used 即最近最久未使用的)

组件更新

每次切换组件,都会进行组件的初始化流程 init 方法,第一次组件渲染时,会在组件虚拟节点上挂载 componentIntance 属性和 keepalive 标记
更新时会再次调用 init 方法,此时会判断虚拟节点的属性和 keepalive 标记,进行 prepatch 方法,对会组件插槽中的内容进行比较。
会判断组件是否需要进行强制更新,会比较新老节点,去执行当前实例的强制更新方法,vm.$forceUpdate ,实际走的就是 keep-alive 的 render()

有关Vue源码剖析的更多相关文章

  1. UE4 源码阅读:从引擎启动到Receive Begin Play - 2

    一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame

  2. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  3. elasticsearch源码关于TransportSearchAction【阶段三】 - 2

    1.回顾.TransportServicepublicclassTransportServiceextendsAbstractLifecycleComponentTransportService:方法:1publicfinalTextendsTransportResponse>voidsendRequest(finalTransport.Connectionconnection,finalStringaction,finalTransportRequestrequest,finalTransportRequestOptionsoptions,TransportResponseHandlerT>

  4. (附源码)vue3.0+.NET6实现聊天室(实时聊天SignalR) - 2

    参考文章搭建文章gitte源码在线体验可以注册两个号来测试演示图:一.整体介绍  介绍SignalR一种通讯模型Hub(中心模型,或者叫集线器模型),调用这个模型写好的方法,去发送消息。  内容有:    ①:Hub模型的方法介绍    ②:服务器端代码介绍    ③:前端vue3安装并调用后端方法    ④:聊天室样例整体流程:1、进入网站->调用连接SignalR的方法2、与好友发送消息->调用SignalR的自定义方法 前端通过,signalR内置方法.invoke()  去请求接口3、监听接受方法(渲染消息)通过new signalR.HubConnectionBuilder().on

  5. Cesium源码解析一(terrain文件的加载、解析与渲染全过程梳理) - 2

    快速导航(持续更新中…)Cesium源码解析一(terrain文件的加载、解析与渲染全过程梳理)Cesium源码解析二(metadataAvailability的含义)Cesium源码解析三(metadata元数据拓展中行列号的分块规则解析)Cesium源码解析四(Quantized-Mesh(.terrain)格式文件在CesiumJS和UE中加载情况的对比)目录1.前言2.本篇的由来3.terrain文件的加载3.1更新环境3.2更新和执行渲染命令3.3数据优化3.4结束当前帧4.总结1.前言  目前市场上三维比较火的实现方案主要有两种,b/s的方案主要是Cesium,c/s的方案主要是u

  6. vue 实现内容超出两行显示展开更多功能,可依据需求自定义任意行数! - 2

    平时开发中我们经常会遇到这样的需求,在一个不限高度的盒子中会有很多内容,如果全部显示用户体验会非常不好,所以可以先折叠起来,当内容达到一定高度时,显示展开更多按钮,点击即可显示全部内容,先来看看效果图: 这样做用户体验瞬间得到提升,接下来看看具体细节。0">主要操作在内容这里{{item.username}},……展开更多样式大家可依据自己项目需求进行设计,这里就不贴了,主要说几个关键的。1、在data中定义三个属性isShowMore:false, //控制展开更多的显示与隐藏textHeight:null, //框中内容的高度status:false, //内容状态是否打开2.计算内容是否

  7. vue3.0 + vite2.0+如何兼容低版本浏览器 - 2

    这里写自定义目录标题一、问题二、解决三、解决方案四、打包预览一、问题在使用vue3.2和vite2开发一个移动端或者钉钉端H5微服务iosapp内置浏览器打开没问题安卓app内置浏览器打开空白页面vconsole打印出现报错globalthisundefind二、解决内置浏览器版本比较低打印出来是63vue3代码不兼容低版本浏览器三、解决方案步骤一:vite.config.ts里build.target配置项指定构建目标为es2015或者步骤二:安装@vitejs/plugin-legacy安装完报错也还在指定版本可以解决“@vitejs/plugin-legacy”:“1.8.0”,步骤三:

  8. 停车系统源码-基于springboot+uniapp开源项目 - 2

    Iparking停车收费管理系统-可商用介绍Iparking是一款基于springBoot的停车收费管理系统,支持封闭车场和路边车场,支持微信支付宝多种支付渠道,支持多种硬件,涵盖了停车场管理系统的所有基础功能。技术栈Springboot,MybatisPlus,Beetl,Mysql,Redis,RabbitMQ,UniApp功能云端功能序号模块功能描述1系统管理菜单管理配置系统菜单2系统管理组织管理管理组织机构3系统管理角色管理配置系统角色,包含数据权限和功能权限配置4系统管理用户管理管理后台用户5系统管理租户管理多租户管理6系统管理公众号配置租户公众号配置7系统管理操作日志审计日志8系统

  9. 打通源码,高效定位代码问题|云效工程师指北 - 2

    大家好,我叫胡飞虎,花名虎仔,目前负责云效旗下产品Codeup代码托管的设计与开发。代码作为企业最核心的数据资产,除了被构建、部署之外还有更大的价值。为了帮助企业和团队挖掘更多源代码价值以赋能日常代码研发、运维等工作,云效代码团队在大数据和智能化方向进行了一系列的探索和实践(例如代码搜索与推荐),本文主要介绍我们如何通过直接打通源代码来提高研发与运维效率。随着微服务架构的流行,一个业务流程需要多个微服务共同完成。一旦出现问题,运维人员在面对数量多、调用链路复杂的情况下,很难快速锁定导致问题发生的罪魁祸首:代码。为了提高排查效率,目前常见的解决方案是:链路跟踪+日志分析工具相结合。即通过链路跟踪

  10. Android Studio开发之使用内容组件Content获取通讯信息讲解及实战(附源码 包括添加手机联系人和发短信) - 2

    运行有问题或需要源码请点赞关注收藏后评论区留言一、利用ContentResolver读写联系人在实际开发中,普通App很少会开放数据接口给其他应用访问。内容组件能够派上用场的情况往往是App想要访问系统应用的通讯数据,比如查看联系人,短信,通话记录等等,以及对这些通讯数据及逆行增删改查。首先要给AndroidMaifest.xml中添加响应的权限配置 下面是往手机通讯录添加联系人信息的例子效果如下分成三个步骤先查出联系人的基本信息,然后查询联系人号码,再查询联系人邮箱代码 ContactAddActivity类packagecom.example.chapter07;importandroid

随机推荐