Vue深入了解
- MVVM
- v-model (双向数据绑定原理)
- 异步更新
- keep-alive原理
- $nextTick原理
- computed 和 watch 的区别
- css-scoped
- 虚拟DOM
- Vuex && Pinia
- Vue-router原理
- proxy 与 Object.defineProperty
- 组件通信方式
MVVM
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>mini MVVM</title></head><body><div id="app"><p>姓名: <span>{{ name }}</span></p><p>年龄: <span>{{ age }}</span></p></div><script>window.onload = function() {const vue = new Vue({el: '#app',data: {name: '加载中...',age: '加载中...'}})setTimeout(() => {vue.$data.name = '小明'vue.$data.age = 20}, 2000)}class Dep {constructor() {this.watchList = []}add(node) {this.watchList.push(node)}update(newValue) {this.watch.forEach((node) => {node.textContent = value})}}class Vue {constructor(options) {this.options = optionsthis.$data = options.datathis.$el = document.querySelector(options.el)this.obsever(this.$data)this.compile(this.$el) }/*[observe 函数]:利用Object.defineProperty把data中的属性变成响应式的,同时给每一个属性添加一个dep对象(用来存储对应的watcher观察者)首先我们会对需要响应式的 data 对象进行 for 循环遍历,为 data 的每一个 key 映射一个观察者对象在 ES6 中,for 循环每次执行,都可以形成闭包,因此这个观察者对象就存放在闭包中*/observer(data) {Object.keys(data).forEach((key) => {// 给data中的每一个属性添加一个dep对象(该对象用来存储对应的watcher观察者)const dep = new Dep()// 利用闭包 获取和设置属性的时候,操作的都是valuelet value = data[key]Object.defineProperty(data, key, {get() {// 观察者对象添加对应的dom节点Dep.target && dep.add(Dep.target)return value},set(newValue) {// 属性值变化时,更新观察者中所有节点value = newValuedep.update(value)}})})}/*[compile 函数]:我们从根节点向下遍历 DOM,遇到 mustache 形式的文本,则映射成 data.key 对应的值,同时记录到观察者中当遍历到 {{xxx}} 形式的文本,我们正则匹配出其中的变量,将它替换成 data 中的值当data的数据变化时,调用dep对象的update方法,更新所有观察者中的dom节点*/compile(dom) {const mustache = /\{\{(.*)\}\}/Array.from(dom.childNodes).forEach((child) => {// nodeType 为3时为文本节点,并且该节点的内容包含`mustache`(双大括号{{}})if(child.nodeType === 3 && mustache.test(child.textContent)) {const key = mustache.exec(child.textContent)[1].trim()const keyNoTrim = mustache.exec(child.textContent)[1]// 将该节点添加到对应的观察者对象中,在下面的的this.$data[key]中触发对应的get方法Dep.target = childlet value = this.$data[key]child.textContent = child.textContent.replace(`{{${keyNoTrim}}}`, value)Dep.target = null}// 递归遍历子节点if(child.childNodes.length) {this.compile(child)}})}}</script></body>
</html>
v-model (双向数据绑定原理)
采取数据劫持,通过Object.defineProperty()劫持各个属性,给各个属性添加getter和setter,数据变动时触发相应的回调
Observer:给数据加上getter和setter,改变数据时触发setter
Complie:模板解析,将模板中的变量替换成数据,绑定更新函数
Watcher:订阅者,是Observer和Complie之间通信的桥梁,往订阅器中添加自己,有一个update方法,当属性变动通知时,调用update方法,触发complie中绑定的更新函数
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>v-model</title></head><body><div id="app"><div>年龄: <span>{{ info.person.name }}</span></div><p>{{ job }}</p><input v-model="job" placeholder="请输入工作" type="text" /></div></body><script>window.onload = function () {const vue = new Vue({el: '#app',data: {info: {person: {name: '加载中',},},job: '程序猿',},})setTimeout(() => {vue.info.person.name = '小明'}, 2000)}class Dep {constructor() {this.watchList = []}add(node){this.watchList.push(node)}update(value) {this.watchList.forEach((node) => {if(node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') {node.value = value } else {node.textContent = value}})}}class Vue {constructor(options){this.options = optionsthis.$data = options.datathis.$el = document.querySelector(options.el)this.observer(this.$data)this.compile(this.$el, this)this.proxy(this.$data, this)}observer(data) {if(data && typeof data === 'object') {const _this = thisObject.keys(data).forEach((key) => {const dep = new Dep()let value = data[key]// 数据劫持,对data增加了递归和设置新值的劫持,让data中每一层数据都是响应式的_this.observer(data[key])Object.defineProperty(data, key, {get(){ Dep.target && dep.add(Dep.target)return value}set(newValue) {value = newValue// 数据劫持,对data增加了递归和设置新值的劫持,让data中每一层数据都是响应式的_this.observer(newValue)dep.upadte(value)}})})}}compile(dom, vm) {const mustache = /\{\{(.*)\}\}/Array.from(dom.childNodes).forEach((child) => {if(child.nodeType === 1) {Array.from(child.attributes).forEach((attr) => {if(attr.name.includes('v-model')) {Dep.target = childchild.value = vm.$data[attr.value]Dep.target = null//给input元素绑定input事件,当输入值变化会触发对应属性的dep.update方法,通知对应的观察者发生变化child.addEventLister('input', (e) => {vm.$data[attr.value] = e.target.value})}})}if(child.nodeType === 3 && mustache.test(child.textContent)) {const key = mustache.exec(child.textContent)[1].trim()const keyNoTrim = mustache.exec(child.textContent)[1]const keyList = key.split('.')Dep.target = childlet value = vm.$dataketList.forEach((item) => value = value[item])child.textContent = child.textContent.replace(`{{${keyNoTrim}}}`, value)Dep.target = null}if(child.childNodes.length) {this.compile(child, vm)}})}// 增加了数据代理,通过this.info.person.name就可以直接修 $data对应的值,实现了this对this.$data的代理proxy(data, vm) {Object.keys(data).forEach((key) => {Object.defineProperty(vm, key, {get() {return data[key]},set(newValue) {data[key] = newValue}})})}}</script>
</html>
异步更新
Vue数据更新频繁,但dom只会更新一次,为什么?1、Vue更新dom是异步更新,当Vue的数据更新后,不会立即更新dom2、侦听到数据变化,Vue会开启一个队列, 并缓存在同一事件循环中发生的所有数据变更3、同一个watcher被多次出发,只会被推入队列中一次,避免重复修改相同的dom4、同步任务执行完,执行异步watcher队列任务,一次性更新dom
keep-alive原理
缓存策略时LRU,组件切换时,保存一些组件的状态,防止多次渲染三大属性:include、exclude、max- 根据include/exclude配置的组件名,与对应组件的name进行条件匹配
- 根据组件ID和tag生成缓存的key,在缓存对象中查找是否已经缓存,存在取出并更新
- 检查是都超过了max设置的值,超过的话,根据LRU缓存策略,删除最近最久没有使用的组件
- 将KeepAlive属性更改为true,actived和deactivated两个钩子函数会用到
$nextTick原理
本质是对JavaScript执行原理EventLoop的一种应用
核心是模拟对应的微/宏任务的实现,利用JavaScript的异步回调任务队列来实现Vue框架自己的异步回调队列Vue.$nextTick 为什么优先使用微任务实现:根据 event loop 与浏览器更新渲染时机,宏任务 → 微任务 → 渲染更新,使用微任务,本次event loop轮询就可以获取到更新的dom如果使用宏任务,要到下一次event loop中,才能获取到更新的dom
computed 和 watch 的区别
computed关键点:computed属性用于创建派生数据,这些数据是基于响应式依赖自动计算的。它们提供了缓存机制,只有当依赖项变化时,计算属性才会重新计算。computed适合于声明性地描述数据如何从其他数据派生,常用于视图渲染优化watch关键点:watch用于侦听响应式数据的变化,并在变化发生时执行定义的逻辑。它不具备缓存机制,每次数据变化都会触发回调函数。watch适合于执行复杂的业务逻辑,如异步请求、DOM操作,或者在数据变化时执行条件性响应。computed是声明式的,用于计算并缓存视图所需的数据,它根据响应式数据的变化自动重新计算并提供缓存。只有当其依赖的响应式数据变化时,才会重新执行计算。computed在开始时自动建立依赖关系,默认第一次加载的时候就开始监听watch是命令式的,用于监听响应式数据的变化,每次变化都会触发执行预定义的回调函数。watch默认在开始时不执行监听,除非设置immediate: true,这允许在数据变化时立即执行回调computed原理:1、初始化计算属性时,遍历计算computed对象,给每一个计算属性分别生成一个computed watcher, 并将watcher的dirty设置为true,初始化时不会立即计算,只有在获取计算的值时才会进行计算2、初始化时将Dep.target设置成当前的computer watcher,将computed watcher 添加到所依赖的data值对应的dep中,然后计算computed对应的值,然后将dirty改为false3、当所依赖的data中的值发生变化时,调用set方法触发dep 的notify方法,将watcher中dirty设置为true4、下次获取计算属性的值时,如果dirty为true,重新计算值5、dirty是控制缓存的关键,当依赖的data发生变化时,dirty设置为true,再次获取值时,就会重新计算值watch原理:1、遍历watch对象, 给其中每一个watch属性,生成对应的user watcher2、调用watcher中的get方法,将Dep.target设置成当前的user watcher,并将user watcher添加到监听data值对应的dep中(依赖收集的过程)3、当所监听data中的值发生变化时,会调用set方法触发dep的notify方法,执行watcher中定义的方法4、设置成deep:true的情况,递归遍历所监听的对象,将user watcher添加到对象中每一层key值的dep对象中,这样无论当对象的中哪一层发生变化,wacher都能监听到。通过对象的递归遍历,实现了深度监听功能
css-scoped
原理:编译时,给每一个Vue文件生成一个唯一的id,将此id添加到当前文件的所有html标签上如:<div class="demo"></div>会被编译成<div class="demo" data-v-27e4e96e></div>编译style标签时,将css选择器改造成为属性选择器如:.demo{color: red;}会被编译成.demo[data-v-27e4e96e]{color: red;}
虚拟DOM
什么是虚拟Dom使用JS对象模拟真实DOM节点,但是对比真实DOM更加轻量级1、前端性能的优化,尽量减少真实DOM的操作,频繁的操作DOM会导致浏览器的回流会重绘2、使用虚拟DOM,当数据变化,页面需要更新的时候,通过diff算法,对新旧的虚拟dom节点进行对比,比较两棵树的差异生成差异对象,一次性对DOM进行批量操作3、虚拟DOM本质上是JS对象,使用虚拟DOM可以进行更方便的跨平台操作
// 真实 转 虚拟function dom2Json(dom) {if (!dom.tagName) returnlet obj = {}obj.tag = dom.tagNameobj.props = {}Array.from(dom.attributes).forEach((attr) => {obj.props[attr.name] = attr.value})obj.children = []dom.childNodes.forEach((item) => {// 去除空的节点dom2Json(item) && obj.children.push(dom2Json(item))})return obj}class Element {constructor(type, props, children) {this.type = typethis.props = propsthis.children = children}}// 虚拟 转 真实function render(domObj) {let el = document.querySelector(domObj.type)Object.keys(domObj.props).forEach((key) => {let value = domObj.props[key]switch (key) {case 'value':if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {el.value = value} else {el.setAttribute(key, value)}breakcase 'style':el.style.cssText = valuebreakdefault:el.setAttribute(key, value)}})domObj.childeren.forEach((child) => {child =child instanceof Element? render(child): document.createTextNode(child)})return el}
Vuex && Pinia
Vuex 与 Pinia 的区别语法和结构:Vuex 的语法相对较为复杂,而 Pinia 的语法更加简洁和直观。模块系统:Vuex 支持模块系统,可以将状态拆分成多个模块进行管理,而 Pinia 也提供了类似的功能,但更加灵活和易于使用。类型支持:Pinia 提供了更好的类型支持,可以在代码中获得更好的类型推断和提示。开发体验:Pinia 在开发体验上更加友好,提供了更多的辅助函数和工具,使开发更加高效。
VueX的原理1、store本质就是一个没有template的组件2、利用mixin机制在beforeCreate钩子前混入VuexInit方法3、VuexInit方法实现将store 注册到当前组件的$store中4、state 相当于组件内的data,定义在state上的变量相当于定义在组件的data中的变量,都是响应式的5、当页面中使用了state中的数据,就是依赖收集的过程,6、当state中的数据发生变化,就通过调用对应属性的dep对象的notify方法,去修改视图变化
Vue-router原理
1、创建的页面路由会与该页面形成一个路由表(key-value模式,key为路由,value为页面)2、通过监听浏览器地址栏URL的变化,匹配路由表,将对应路由的页面替换旧页面,达到无需刷新的效果3、目前单页面使用的路由有两种实现方式: hash 模式、history 模式4、hash模式(路由中带#号),通过hashchange事件来监听路由的变化window.addEventListener('hashchange', ())=>{})5、history 模式,利用了pushState() 和replaceState() 方法,实现往history中添加新的浏览记录、或替换对应的浏览记录通过popstate事件来监听路由的变化,window.addEventListener('popstate', ())=>{})
proxy 与 Object.defineProperty
1)初始化性能优化:Vue 2 在初始化响应式数据时,会递归遍历对象的所有属性并使用 Object.defineProperty为每个属性添加 getter 和 setter。这样的初始化过程会产生大量的 getter 和 setter,对于大规模的对象或数据,初始化时间会较长。Vue 3 中,使用 Proxy 对象进行拦截,初始化性能得到了显著提升,因为 Proxy 是在整个对象级别上进行拦截,无需遍历每个属性。
2)深层属性监听优化:Vue 2 中,对于深层嵌套的属性,需要通过递归方式为每个属性添加响应式处理,这在大型对象上可能会导致性能下降。Vue 3 中,Proxy 可以递归地拦截整个对象的操作,无需为每个属性单独处理,从而提高了深层属性监听的性能。
3)删除属性性能优化:Vue 2 中,当删除一个属性时,需要通过 Vue.$delete 或者 Vue.delete 方法来触发更新。这是因为 Vue 2 使用的 Object.defineProperty 无法拦截属性的删除操作。Vue 3 中,使用 Proxy 可以直接拦截属性的删除操作,从而简化了删除属性的处理逻辑,并提高了性能。
4)动态添加属性性能优化:Vue 2 中,动态添加新属性需要通过 Vue.set 方法来触发更新,否则新添加的属性将不会是响应式的。Vue 3 中,Proxy 可以直接拦截动态添加属性的操作,并将其设置为响应式属性,无需额外的处理方法,提高了性能和代码的简洁性。
组件通信方式
-
通信的种类
- 父组件向子组件通信
- 子组件向父组件通信
- 隔代组件间通信
- 兄弟组件间通信
-
实现通信的方式
- props
- vue自定义事件
- 消息订阅与发布
- vuex
- slot
- 依赖注入
-
方式一:props
- 通过一般属性实现父向子通信
- 通过函数属性实现子向父通信
- 缺点:隔代组件和兄弟组件间通信比较麻烦
-
方式二:vue自定义组件
-
vue内置实现,可以代替函数类型的props
a.绑定监听:<MyComp @eventName=“callback”>b.触发(分发)事件: this.$emit(“eventName” , data)
-
适用于子组件与父组件通信居多,可以利用事件总线,进行兄弟组件间的通信,类似于vuex
-
-
方式三:消息订阅发布
-
需要引入消息订阅与发布的实现库,如: pubsub-js
a.订阅消息:PubSub.subscribe(‘msg’, (msg,data)=>{})b.发布消息: PubSub.publish(‘msg’, data)
-
优点:此方式可实现任意关系组件间通信
-
-
方式四:vuex
- 是什么:vuex是vue官方提供的集中式管理vue多组件共享状态数据的vue插件
- 优点:对组件间关系没有限制,且相比于pubsub库管理更集中,更方便
-
方式五:slot
-
是什么:专门用来实现父向子传递带数据的标签
a.子组件
b.父组件
-
注意:通信的标签模板是在父组件中解析好后再传递给子组件的
-
-
方式六:依赖注入
- provide、inject
provide() { return { num: this.num }; } inject: ['num']
注意: 依赖注入所提供的属性是非响应式的。