蓝狮注册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。该钩子在服务器端渲染期间不被调用。
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()

0 Comments
Leave a Reply