自学内容网 自学内容网

Vue深入了解

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 = options
this.$data = options.data
this.$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()
// 利用闭包 获取和设置属性的时候,操作的都是value
let value = data[key]
Object.defineProperty(data, key, {
get() {
// 观察者对象添加对应的dom节点
Dep.target && dep.add(Dep.target)
return value
},
set(newValue) {
// 属性值变化时,更新观察者中所有节点
value = newValue
dep.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 = child
let 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 = options
this.$data = options.data
this.$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 = this
Object.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 = child
child.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 = child
let value = vm.$data
ketList.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的数据更新后,不会立即更新dom
  2、侦听到数据变化,Vue会开启一个队列, 并缓存在同一事件循环中发生的所有数据变更
  3、同一个watcher被多次出发,只会被推入队列中一次,避免重复修改相同的dom
  4、同步任务执行完,执行异步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改为false
3、当所依赖的data中的值发生变化时,调用set方法触发dep 的notify方法,将watcher中dirty设置为true
4、下次获取计算属性的值时,如果dirty为true,重新计算值
    5、dirty是控制缓存的关键,当依赖的data发生变化时,dirty设置为true,再次获取值时,就会重新计算值
 
  watch原理:
    1、遍历watch对象, 给其中每一个watch属性,生成对应的user watcher
    2、调用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) return
let obj = {}
obj.tag = dom.tagName
obj.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 = type
this.props = props
this.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)
}
break
case 'style':
el.style.cssText = value
break
default:
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']
    

    注意: 依赖注入所提供的属性是非响应式的。


原文地址:https://blog.csdn.net/SuihideOmelet/article/details/128133203

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!