Vue源码剖析
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 函数
,大致可分为以下三步:
- 将 template 模板转换成 ast 语法树 - parserHTML
- 定义一个 stack 栈,存放标签的父子关系
- 通过正则匹配模板字符串,不停的解析,不停的删除,直至字符串解析完成,
- 得到 ast 树,(存放标签名,子节点,及属性列表)
- 对静态语法做静态标记 static,会递归遍历子节点进行标记,组件和插槽不属于静态语法 - markUp
- 只有在第一次编译时,会进行静态标记,不是每次渲染都标记
- 静态标记主要是用来做 diff 优化的,静态节点跳过 diff 操作
- 子节点有一个变化,父节点都不是静态的
- 生成代码,核心就是拼接字符串(_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;
}
- beforeCreate 在实例初始化 init 之后,数据初始化(data observer)之前调用,拿不到响应式的状态,可以拿到$on、$events 以及一些父子关系。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。
- created 数据初始化完毕后调用,实例已经创建完成。完成数据观测(data observer),属性和方法的运算,可以直接用响应式数据。但是没有$el,不能进行 dom 操作。
- beforeMount 在挂载开始之前被调用(在 mountComponent 方法中被调用):之后相关的 render 函数首次被调用。
- mounted el 被新创建的真实的 vm.$el 替换,并挂载到实例上后调用该钩子。此阶段可以获取渲染后的节点。
- beforeUpdate 数据更新前调用,在创建 Watcher 时会传一个 before 方法,它里面会调用 beforeUpdate 钩子,每次页面更新都会去调用当前的渲染 watcher,会判断有没有 before 方法,有的话就会调用 beforeUpdate, 发生在虚拟 DOM 重新渲染和打补丁 patch 之前。然后再去执行 watcer.run()真实的更新方法。
- updated 执行完 watcer.run()之后,调用 updated 钩子,表示 dom 已完成更新。 (执行数据更改导致的虚拟 DOM 重新渲染和打补丁)。注意避免在此期间更新数据,因为可能会导致为无限循环的更新。
- beforeDestroy 实例销毁之前调用。仅作为实例即将的信号,实例仍然完全可用。之后会进行一系列的卸载操作。执行真正的卸载(从父节点中移除、清空自己的 watcher、卸载所有的属性、标记当前组件销毁状态、把虚拟节点也销毁掉、然后调 destroyed)。可以在这时进行一些收尾工作如清除定时器等。
- 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 方法实现原理
- 如果目标不存在或者是原始类型,直接报错,cannot set reactive property on undefined,null,or primitive value
- 如果是数组,Vue.set(arr,1,100),调用重写的
target.splice(key,1,val)
方法,可以更新视图 - 如果是对象,看这个对象本身有没有这个值,如果有就直接更新就好,因为他本身就是响应式的,
- 如果是根实例,或者根数据 data 时,会报错提示 应该在初始化时声明该数据
- 如果不是响应式数据,也不需要将其定义成响应式属性 Vue.set({},'age',18),相当于这个对象本身就不是响应式的,就直接赋值,也不需要更新视图
- 最后就把调用的属性定义成响应式的即可。调用
defineReactive(ob.value,key,val)
- 通知视图更新
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 算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较(双指针分别指向新旧的结尾)。
- 先比较是否是相同节点,判断属性 key + tag
- 相同节点比较属性,并复用老节点
- 比较儿子节点,考虑老节点和新节点儿子的情况
- 优化比较:头头、尾尾、头尾、尾头
- 比对查找进行复用
diff 的复杂度 是 O(n),当一方子元素的头尾相等时,结束循环,(因为同层比较,内部只有一层循环).子元素嵌套时,递归同层比较
如果不能匹配到的话,就会根据当前的老的索引 key 创建一个映射表,拿新的去里面找,如果能找到就复用,找不到就创建新的,最终把老的多余的删掉,
Vue 中 key 的作用和原理
- Vue 在 patch 过程中通过 key 可以判断两个虚拟节点是否是相同节点。 (可以复用老节点)
- 无 key 会导致更新的时候出问题,比如 unshift 变成 push 效果,并更新所有节点,有 key 时,就可以节点复用,仅做节点的移动即可。
- 尽量不要采用索引作为 key,而是使用数据的唯一标识
vue 初渲染流程
- vue 初始化流程 _init:
- 默认会调用 vue._init 方法将用户的参数挂在到$options 选项上,vm.$options。(vue 调用的方法使用原型扩展的形式)
- vue 会根据用户的参数进行数据的初始化,data props computed watch 等 ,在外界是无法访问的,可以通过 vm._data 访问到用户的数据。
- 对数据进行观测,对象(递归使用 Object.defineProperty),数组(方法重写,切片编程),劫持到用户的操作,观测的目的是用户修改数据时 -> 更新视图
- 将数据代理到 vm 对象上,vm.xxx => vm._data.xxx
- vue 挂载流程 $mount:
- 判断用户是否传入了 el 属性, 内部会调用$mount 方法,用户也可以自行调用该方法
- 处理模板优先级 render / template / outerHTML
- 将模板编译成函数, 步骤: parseHTML 解析模板 -> ast 语法树, generate 解析语法树生成 code -> new Function 生成 render 函数
- 通过 render 方法,生成虚拟 dom + 真实的数据 => 真实的 dom
- 根据虚拟节点渲染真实的节点
vue 更新流程 依赖收集实现过程
- vue 中使用了观察者模式,默认组件渲染的时候,会创建一个 watcher,并且会渲染视图
- 当渲染视图的时候,会取 data 中的数据,会走每个属性的 get 方法,就让这个属性的 dep 记录 watcher
- 同时让 watcher 也记住 dep,dep 和 watcher 是多对多的关系,因为一个属性可以对应多个视图,一个视图对应多个数据
- 如果数据发生变化,会通知对应属性的 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
组件的初始化流程
- 第一步:创造组件的虚拟节点,创建虚拟节点的时候,内部会去调用 Vue.extend 方法,产生组件的构造函数 Ctor
- 第二步:给组件添加钩子函数,data.hook = {init},合并 mergeOptions (自己的组件.proto = 全局的组件),最终返回了一个虚拟节点
- 第三步:页面开始渲染,渲染的时候,会去调用 patch 方法,并且根据当前的虚拟节点,转换成真实节点,这时会去调用 createElm,创造真实节点。
- 第四步:创造真实节点的时候发现,如果这个节点是组件,就会调用组件的 createCompontent => 调用 hook.init 方法,
- 第五步: 此时 init 方法,会 new Ctor(),之后会进行子组件的初始化操作 this._init
- 第六步:最终再去调用组件的挂载操作$mount,产生一个$el 真实节点,对应组件模板渲染后的结果。
- 第七步:将组件的 vnode.componentInstance.$el 插入到父标签中
keep-alive 实现原理
keep-alive 组件是一个抽象组件, 也是一个虚拟组件, 不会被记录到父子组件关系当中,一般用在路由组件的外层, 主要为了缓存组件, 为频繁挂载销毁,提供缓存功能节约性能,
- 包含 include 属性,添加白名单,表示那些组件需要缓存,切换过后才会进行缓存,并不是将白名单中的 name 直接全部缓存。
- 包含 exclude 属性,添加黑名单,表示那些组件不用缓存
- max = x 最多缓存几个组件, 如果超过最大限制 需要删除第一个, 在增加最新的 LRU
- created 钩子:创造一个对象 cache 来缓存组件,key[],表示缓存的是谁
- render():渲染
- 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源码剖析的更多相关文章
- petite-vue源码剖析-逐行解读@vue/reactivity之reactive
在petite-vue中我们通过reactive构建上下文对象,并将根据状态渲染UI的逻辑作为入参传递给effect,然后神奇的事情发生了,当状态发生变化时将自动触发UI重新渲染.那么到底这是怎么做到 ...
- 逐行剖析Vue源码(一)——写在最前面
1. 前言 博主作为一名前端开发,日常开发的技术栈是Vue,并且用Vue开发也有一年多了,对其用法也较为熟练了,但是对各种用法和各种api使用都是只知其然而不知其所以然,因此,有时候在排查bug的时候 ...
- 大白话Vue源码系列(01):万事开头难
阅读目录 Vue 的源码目录结构 预备知识 先捡软的捏 Angular 是 Google 亲儿子,React 是 Facebook 小正太,那咱为啥偏偏选择了 Vue 下手,一句话,Vue 是咱见过的 ...
- 大白话Vue源码系列(03):生成AST
阅读目录 AST 节点定义 标签的正则匹配 解析用到的工具方法 解析开始标签 解析结束标签 解析文本 解析整块 HTML 模板 未提及的细节 本篇探讨 Vue 根据 html 模板片段构建出 AST ...
- 大白话Vue源码系列(03):生成render函数
阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...
- 大白话Vue源码系列(04):生成render函数
阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...
- vue源码实现的整体流程解析
一.前言 最近一直在使用vue做项目,闲暇之余查阅了一些关于vue实现原理的资料,一方面对所了解到的知识做个总结,另外一方面希望能对看到此文章的同学有所帮助.本文如有不足之处,还请过往的大佬批评指正. ...
- 手牵手,从零学习Vue源码 系列一(前言-目录篇)
系列文章: 手牵手,从零学习Vue源码 系列一(前言-目录篇) 手牵手,从零学习Vue源码 系列二(变化侦测篇) 手牵手,从零学习Vue源码 系列三(虚拟DOM篇) 陆续更新中... 预计八月中旬更新 ...
- 手牵手,从零学习Vue源码 系列二(变化侦测篇)
系列文章: 手牵手,从零学习Vue源码 系列一(前言-目录篇) 手牵手,从零学习Vue源码 系列二(变化侦测篇) 陆续更新中... 预计八月中旬更新完毕. 1 概述 Vue最大的特点之一就是数据驱动视 ...
- 07.ElementUI 2.X 源码学习:源码剖析之工程化(二)
0x.00 前言 项目工程化系列文章链接如下,推荐按照顺序阅读文章 . 1️⃣ 源码剖析之工程化(一):项目概览.package.json.npm script 2️⃣ 源码剖析之工程化(二):项目构 ...
随机推荐
- NumPy 分割与搜索数组详解
NumPy 分割数组 NumPy 提供了 np.array_split() 函数来分割数组,将一个数组拆分成多个较小的子数组. 基本用法 语法: np.array_split(array, indic ...
- java学习之旅(day.20)
注解和反射 注释comment:给人看 注解annotation:不仅可以给人看,还能给程序看,甚至能被其他程序读取 注解入门 什么是注解 注解的作用: 不是程序本身,可以对程序作出解释(这一点和注释 ...
- C# 使用大数组内存溢出的解决办法
在实际开发中,需要读取文件转成byte数组,文件大小四五百兆,采用win10系统,我那台电脑系统版本非常老了,一直没升级,读取文件时,就会出现OutOfMemeory异常,时不时的出现.我程序用的an ...
- Anagrams(字谜)
描述 Most crossword puzzle(猜字谜) fans are used to anagrams(字谜)--groups of words with the same letters i ...
- CSS操作——文本属性
1.font-style(字体样式风格) /* 属性值: normal:设置字体样式为正体.默认值. italic:设置字体样式为斜体.这是选择字体库中的斜体字. oblique:设置字体样式为斜体. ...
- claude3国内API接口对接
众所周知,由于地理位置原因,Claude3不对国内开放,而国内的镜像网站使用又贵的离谱! 因此,团队萌生了一个想法:为什么不创建一个一站式的平台,让用户能够通过单一的接口与多个模型交流呢?这样,用户就 ...
- iPhoneX 适配总结
一.iPhoneX适配第一步,根据iPhoneX的屏幕像素大小,引入对应的启动图,告诉系统,app兼容iPhoneX 需要在launchimage中引入一张 1125*2436的png,app将默认展 ...
- 第一个java的应用程序
编写java第一个程序 class HelloWorld { public static void main(String[] args){ System.out.print("Hello ...
- kettle从入门到精通 第三十六课 kettle carte 集群
1.carte服务可以单体运行也可以集群方式运行,今天我们一起来学习下carte的集群模式部署和使用.本次示例用一个master和两个slave从节点演示. carte-config-master-8 ...
- C#.NET 4.8 WEBP 转 GIF
C#.NET 4.8 WEBP 转 GIF 项目是.NET 4.8. nuget 引用 Magick.NET-Q16-AnyCPU ,版本:7.14.5.高版本,如:12.2 已经不支持.NET FR ...