概述
文章目录
- vue相关原理进阶
- - 1 - 整体目标
- - 2 - 数据响应式
- - 2.1 响应式是什么
- - 2.2 如何实现数据响应式
- - 2.3 实现对象属性拦截
- - 2.4 优化1 - get 和 set 联动
- - 2.5 优化2 - 更加通用的劫持方案
- - 2.6 响应式总结
- - 3 - 数据的变化反应到视图
- - 3.1 命令式操作视图
- - 3.2 声明式操作视图
- - 3.3 总结
- - 4 - 视图的变化反应到数据
- - 5 - 现存架构的问题
- - 6 - 发布订阅模式优化
- - 6.1 优化思路思考
- - 6.2 理解发布订阅模式
- - 6.3 收集更新函数
- - 6.4 触发更新函数
- - 7 - 整体总结
vue相关原理进阶
- 1 - 整体目标
-
了解 Object.defineProperty 实现响应式
-
了解指令编译的基础原理
-
清楚Observe/watcher/deo 具体指什么
-
了解发布订阅模式以及其解决的问题
- 2 - 数据响应式
- 2.1 响应式是什么
一旦数据发生变化,我们可以立刻知道,并且做一些你想完成的事
- 发送一个网络请求
- 打印一段文字
- 操作一个DOM
- 2.2 如何实现数据响应式
在javascript里实现数据响应式 一般有两种方案,分别对应着vue2.x,和vue3.x 使用的方式,他们分别是:
-
对象属性拦截 (vue2.x)
Objcet.defineProperty
-
对象整体代理 (vue3.x)
Proxy
- 2.3 实现对象属性拦截
// 1. 字面量定义
let data = {
name: " giao桑 "
}
data.name = '要洗~' // 普通字面量定义是无法检测到name属性发生了变化
// 2. Object.defineProperty
let data1 = {}
Object.defineProperty(data1, 'name', {
// 当我们设置 name 属性的时候自动调用的方法
// 并且属性 最新的值 会被当成实参传进来
set(...r) {
// 设置属性 data.name = 'new name' data['name'] = 'new name '
console.log('name set', r)
// 只要修改了name属性 就会执行该函数
// 如果想要在name变化的时候 完成一些自己的事情
// 都可以在这儿执行!
// 1. ajax ()
// 2 操作 一块 DOM 区域
},
// 访问name属性 的时候 会自动调用的方法
// 并且 get的返回值 就是你拿到的值
get(...r) {
// 访问属性 data.name data['name']
console.log('name get', r)
}
})
data1.name = 'new name~'
let a = data1.name
- 2.4 优化1 - get 和 set 联动
let data = {}
Object.defineProperty(data,'name',{
get(){
console.log('data get',data)
},
set(newValue){
console.log('data set',newValue)
}
})
出现问题
- data 设置name属性之后 再次访问name属性 直接返回了一个固定的值
- 并且 set函数中拿到新值之后没有做任何操作
解决方案:
-
通过声明一个中间变量,让get函数中return 出去这个变量
-
并且在set函数中把最新的值设置到这个中间变量身上,起到一个set和get数据的效果
let data = {}
// 中间变量
let _name = 'giaogiaogiao '
Object.defineProperty(data,'name',{
get(){
console.log('data get',data)
return _name
},
set(newValue){
console.log('data set',newValue)
_name = newValue
}
})
- 2.5 优化2 - 更加通用的劫持方案
开发 vue 项目的时候 都是提前把响应式数据放到 data 选项中
/*
* data(){
* return { name:"giao" }
* }
* */
// 所以 一般情况下,响应式数据 都是提前写好的,并且对象的属性很多
let data = {
name: ' wo giao ',
info: " xi huan gou dan ",
age: '20'
}
如何把这个提前生命的对象 巴黎变得所有属性都变成响应式的?
// 1. 遍历对象的每一个属性
Object.keys(data).forEach((key) => {
console.log(key, data[key])
defineReactive(data, key, data[key])
})
// 2. 将每一个属性转为响应式数据
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
get() {
console.log('get',key)
return value
},
set(newValue) {
console.log('set',key,newValue)
value = newValue
}
})
}
总结:
- 尽量不要再一个函数中书写大量逻辑,而应该是 按照功能差分成多个小函数 ,然后再组合取来 便于维护
- 对象遍历的时候 每遍历一次 调用一次defineReactive 函数 形成了多个独立的函数作用域 在每一个独立的函数作用域中 set 和 get 操作的都是独立的value 值 互不影响(闭包特性 1.创建私有作用域 2.延长变量生命周期 )
- 函数定义形参相当于在函数内部声明了和形参名字相对应的变量 (局部) 并且初始值为undefined
- 函数调用传入实参的时候,相当于给内部声明好的变量 做了赋值操作
- 函数调用完毕 本来应该内部所有的变量都会被回收,但是如果内部有其他函数 使用了当前变量 则形成了闭包 不会被回收
- 内部由于有其他方法 引用了value 属性 所以defineReactive 函数 的执行并不会导致value 变量的销毁 会一直常驻内存中
- 由于闭包特性 每一个传入下来的value都会常驻内存 相当于 中间变量, 是为了set get 联动 从而实现数据的响应式
- 2.6 响应式总结
- 所谓的响应式 其实就是拦截数据的访问和设置 ,插入一些我们自己想要做的事情
- 在js 中 能实现响应式拦截的方法有两种 Object.defineProperty 和 Proxy 对象代理
- 回归到vue 2.x 中的data 配置项 只要放到了data 里的数据 不管层级多深,都会进行递归响应式处理,所以要求我们 如非必要,尽量不要添加太多冗余数据在data中。(消耗性能)
- 需要了解vue3.x 中解决了 2 中 对于数据响应式处理的无端性能消耗,使用的手段是proxy 劫持对象整体 + 惰性代理 ( 用到了才进行响应式处理)
- 3 - 数据的变化反应到视图
想要把数据反应到视图中 本质上还是操作DOM
- 3.1 命令式操作视图
/* 命令式 m -> v */
// 1.准备数据
let data =(function (){
return {
name:"giao 桑",
age:99
}
})()
// 2. 将每一个属性转为响应式数据
Object.keys(data).forEach((key) => {
console.log(key, data[key])
defineReactive(data, key, data[key])
})
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
get() {
console.log('get',key)
return value
},
set(newValue) {
// set 函数的执行 不会自动判断 两次修改的值是否相等
// 因为DOM 操作 是非常消耗内存的, 所以当新值和旧值相等的时候 不应该进行set 逻辑
if(newValue === value) return
console.log('set',key,newValue)
value = newValue
// 将最新的值 反应都视图中去 关键code
// 操作DOM
document.querySelector('#app p').innerText = newValue
}
})
}
document.querySelector('#app p').innerText = newValue
- 3.2 声明式操作视图
/*
* v-text 指令式
* */
// 1 先通过标识 查找把数据放到对应的DOM元素
// 2 数据变化之后再次执行将最新的值放到对应的DOM上
let data = {
name: "giao sang",
age:"888"
}
function compile() {
let app = document.getElementById('app');
// 拿到所有节点
const nodes = app.childNodes// 拿到所有的节点 包括文本节点
// 筛选出元素节点 nodeType === 3 文本节点 nodeType === 1 元素节点
nodes.forEach(node => {
if (node.nodeType === 1) {
// 筛选 v-text 属性 p
const attrs = node.attributes
Array.from(attrs).forEach(attr => {
const nodeName = attr.nodeName
const nodeValue = attr.nodeValue
if (nodeName === 'v-text') {
node.innerText = data[nodeValue]
}
})
}
})
}
Object.keys(data).forEach((key) => {
defineReactive(data, key, data[key])
})
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
get() {
console.log('get', key)
return value
},
set(newValue) {
if (newValue === value) return
console.log('set', key, newValue)
value = newValue
compile()
}
})
}
compile()
- 3.3 总结
- 先通过标识 查找把数据放到对应的DOM元素
- 数据变化之后再次执行将最新的值放到对应的DOM上
- 4 - 视图的变化反应到数据
目标:将data中的message 属性对应的值渲染到input上面 ,同时input值发生改变之后,可以反向修改message的值。
/*
* v-model 双向绑定的实现
* M - V
* V - M
* */
let data = {
name: "giao sang",
age:"888"
}
function compile() {
let app = document.getElementById('app');
// 拿到所有节点
const nodes = app.childNodes// 拿到所有的节点 包括文本节点
// 筛选出元素节点 nodeType === 3 文本节点 nodeType === 1 元素节点
nodes.forEach(node => {
// 筛选元素节点
if (node.nodeType === 1) {
// 元素节点的属性
const attrs = node.attributes
Array.from(attrs).forEach(attr => {
const nodeName = attr.nodeName
const nodeValue = attr.nodeValue
if (nodeName === 'v-text') {
node.innerText = data[nodeValue]
}
// 实现 v-model
if(nodeName === 'v-model'){
// 调用dom操作 给 input 标签绑定数据
node.value = data[nodeValue]
// 监听 input事件 在事件回调中 拿到最新的输入值 赋值给绑定的属性
node.addEventListener('input',(e)=>{
let val = e.target.value
data[nodeValue] = val
})
}
})
}
})
}
Object.keys(data).forEach((key) => {
defineReactive(data, key, data[key])
})
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
get() {
console.log('get', key)
return value
},
set(newValue) {
if (newValue === value) return
console.log('set', key, newValue)
value = newValue
compile()
}
})
}
compile()
- 5 - 现存架构的问题
面临的问题:
- 不管你修改了哪个属性,其他属性也会跟着一起更新
- 只修改了name 的时候 应该只有name 相关的更新操作执行 而不是所有的属性一起更新
期望:
- 哪个属性进行了实质性的修改,哪个属性的对应的编译部分得到更新 - “精准更新”
- 6 - 发布订阅模式优化
- 6.1 优化思路思考
*
发布订阅模式 (自定义事件)
*
* 优化思路:
* 1. 数据变化之后,实际上执行那部分代码就可以了
* node.innerText = data[nodeValue]
* 2. 要完成以上代码 我们需要关注两个问题
* node:当前要操作哪个dom元素
* nodeValue 我们要去data中查找的对象属性名是谁 'name' 'age'
* 3. 正常逻辑下 函数的执行之后 内部所有的变量都会被销毁
* 为了保证 node nodevalue 属性名 不被销毁
* 我们需要引入闭包机制 强制让他们存储与内存
* 之所以不能被销毁 是因为 应用跑起来之后 偶们一直在操作数据 ,一直在操作数据对应的dom
* ()=>{
* node.innerText = data[nodeValue]
* }
* 这样可以借助函数引用外层函数的node 变量和nodeValue 变量从而使他们一直在内存中 不被销毁
* 所以才可以一直对他们操作
* 4. 由于我们响应式数据 绑定到的dom节点可能有多个 所以 node 节点可能存在多个
* 一旦响应式属性 name 发生变化 与 name 属性相关的所有dom节点都需要进行一轮更新
* 所以属性和更新函数之间是一个一对多的关系
*
- 6.2 理解发布订阅模式
let btn = document.getElementById('btn')
// btn.onclick = function (){
// console.log('click !!!!!')
// }
// btn.onclick = function (){
// console.log('so difficult!')
// }
// 以上实现 并不能完成俩回调函数的同时绑定
// 它是一个 一对一 的实现 一个时间 -> 回调函数
// 优化 从一对一 一对多
btn.addEventListener('click', () => {
console.log('click')
})
btn.addEventListener('click', () => {
console.log('click me!!')
})
// 这种模式下 可以实现 同一事件 对应多个回调函数 实现了关键的 一对多
// 这种优化 使用的就是发布订阅模式
/*
* 1. 浏览器实现了一个方法 叫做addEventListener
* 2. 这个方法 接收两个参数 参数1 代表 事件类型 参数2 代表 回调函数
* 3. {
* click:['回调函数1','回调函数2']
* }
* 4. 当鼠标点击的时候 通过事件类型click 去数据结构中找到存放了所有相关回调函数的数组 然后遍历 都执行一边 从而实现一对多
* */
/*
* 实现一个自己的自定义事件 收集和 触发架构
* 1.定义一个方法 接收两个参数 参数1 为 事件名称 参数2 为 回调函数
* 只要方法一执行 就收集回调函数到对应的位置上去
* 2.
* */
// 模拟鼠标点击 主动通过程序去触发收集起来的事件
// 需要通过事件名称 找到对应的回调函数数组 然后遍历执行即可
// 优化 把所有和事件相关的数据结构 以及方法 收敛到对象中
const Dep = {
map: {},
Collect(eventName, fn) {
if (!this.map[eventName]) {
this.map[eventName] = []
}
this.map[eventName].push(fn)
},
trigger(eventName) {
this.map[eventName].forEach(fn => fn())
}
}
Dep.Collect('han',()=>{
console.log('啦啦啦')
})
Dep.Collect('lao',()=>{
console.log('lululu')
}) let btn = document.getElementById('btn')
// btn.onclick = function (){
// console.log('click !!!!!')
// }
// btn.onclick = function (){
// console.log('so difficult!')
// }
// 以上实现 并不能完成俩回调函数的同时绑定
// 它是一个 一对一 的实现 一个时间 -> 回调函数
// 优化 从一对一 一对多
btn.addEventListener('click', () => {
console.log('click')
})
btn.addEventListener('click', () => {
console.log('click me!!')
})
// 这种模式下 可以实现 同一事件 对应多个回调函数 实现了关键的 一对多
// 这种优化 使用的就是发布订阅模式
/*
* 1. 浏览器实现了一个方法 叫做addEventListener
* 2. 这个方法 接收两个参数 参数1 代表 事件类型 参数2 代表 回调函数
* 3. {
* click:['回调函数1','回调函数2']
* }
* 4. 当鼠标点击的时候 通过事件类型click 去数据结构中找到存放了所有相关回调函数的数组 然后遍历 都执行一边 从而实现一对多
* */
/*
* 实现一个自己的自定义事件 收集和 触发架构
* 1.定义一个方法 接收两个参数 参数1 为 事件名称 参数2 为 回调函数
* 只要方法一执行 就收集回调函数到对应的位置上去
* 2.
* */
// 模拟鼠标点击 主动通过程序去触发收集起来的事件
// 需要通过事件名称 找到对应的回调函数数组 然后遍历执行即可
// 优化 把所有和事件相关的数据结构 以及方法 收敛到对象中
const Dep = {
map: {},
Collect(eventName, fn) {
if (!this.map[eventName]) {
this.map[eventName] = []
}
this.map[eventName].push(fn)
},
trigger(eventName) {
this.map[eventName].forEach(fn => fn())
}
}
Dep.Collect('han',()=>{
console.log('啦啦啦')
})
Dep.Collect('lao',()=>{
console.log('lululu')
})
- 6.3 收集更新函数
function Collect(eventName, fn) {
if (!this.map[eventName]) {
this.map[eventName] = []
}
this.map[eventName].push(fn)
},
- 6.4 触发更新函数
// 引入发布订阅模式
/*
* 发布订阅模式思路
* 1. 针对每一个响应式属性 收集与之相关的更新函数
* 2. 响应式属性更新之后 通过属性名 找到与之绑定在一起的所有更新函数 进行触发执行
* */
const Dep = {
map: {},
Collect(eventName, fn) {
if (!this.map[eventName]) {
this.map[eventName] = []
}
this.map[eventName].push(fn)
},
trigger(eventName) {
this.map[eventName].forEach(fn => fn())
}
}
let data = {
name: "giao sang",
age:"888"
}
function compile() {
let app = document.getElementById('app');
// 拿到所有节点
const nodes = app.childNodes// 拿到所有的节点 包括文本节点
// 筛选出元素节点 nodeType === 3 文本节点 nodeType === 1 元素节点
nodes.forEach(node => {
// 筛选元素节点
if (node.nodeType === 1) {
// 元素节点的属性
const attrs = node.attributes
Array.from(attrs).forEach(attr => {
const nodeName = attr.nodeName
const nodeValue = attr.nodeValue
if (nodeName === 'v-text') {
node.innerText = data[nodeValue]
// 收集更新函数 订阅操作 当对象对应属性修改的时候 执行回调
Dep.Collect(nodeValue,()=>{
console.log(`当前您修改了 ${nodeValue}`)
node.innerText = data[nodeValue]
})
}
// 实现 v-model
if(nodeName === 'v-model'){
// 调用dom操作 给 input 标签绑定数据
node.value = data[nodeValue]
// 收集更新函数
Dep.Collect(nodeValue,()=>{
node.value = data[nodeValue]
})
// 监听 input事件 在事件回调中 拿到最新的输入值 赋值给绑定的属性
node.addEventListener('input',(e)=>{
let val = e.target.value
data[nodeValue] = val
})
}
})
}
})
}
Object.keys(data).forEach((key) => {
defineReactive(data, key, data[key])
})
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
get() {
return value
},
set(newValue) {
if (newValue === value) return
value = newValue
// compile()
// 在这里 进行精准更新 -> 通过data 中的属性名 找到对应的更新函数依次执行
Dep.trigger(key)
}
})
}
compile()
console.log(Dep)
- 7 - 整体总结
1. Object.defineProperty
ES 6 提供原生的方法
Object.defineProperty(data,'name',()=>{
// 访问 data.name 属性 会自动调用 返回值 为 data.name的值
get(){
},
// 设置一个对象属性的时候 自动调用的函数 会把设置的最新的值 当成实参传入set函数
// data.name = 'hahaha'
set(newValue){
// 只要数据发生变化 我们可以做我们想做的任何事情
// ajax console.log() 操作dom
}
})
核心API
2. 数据反应到视图
数据的变化可以引起视图的变化 (通过操作dom把数据放到对应的位置上去 如果数据变化之后 就用数据最新的值 在重新放一次)
方案一 : 命令式操作
1. document.querySelector('#app p').innerText = data.name
2. set 函数中重新执行一下 document.querySelector('#app p').innerText
方案二 : 声明式操作
v-text 指令的实现
<p v-text='name'></p>
核心逻辑 : 通过 “ 模板编译” 找到标记了v-text 的元素 然后把 对应的数据通过操作dom api放上去
<div id='app'>
<p v-text='name'></p>
</div>
1. 通过app 根元素找到所有的子节点(元素节点 文本节点) -> dom.nodeChilds
2. 通过节点类型筛选出元素节点 p -> nodeType 1 元素节点 3 文本节点
3. 通过v-text 找到需要设置的具体的节点 <p v-text></p>
4. 找到 绑定了 v-text 标记的元素 拿到他身上所有的属性 id class v-text
5. 通过 v-text = 'name' 拿到指令类型 'v-text' 拿到需要绑定的数据属性名 ‘name’
6. 判断当前是v-text 指令 让后通过操作dom api 把name 属性对应的值放上去 node.innerText = data[name]
以上整个过程称之为 模板编译
3.视图的变化反应到数据
imput 元素 v-model 双向绑定
M-V
V-M
1. M-V
1. 通过app 根元素找到所有的子节点(元素节点 文本节点) -> dom.nodeChilds
2. 通过节点类型筛选出元素节点 p -> nodeType 1 元素节点 3 文本节点
3. 通过v-text 找到需要设置的具体的节点 <p v-text></p>
4. 找到 绑定了 v-text 标记的元素 拿到他身上所有的属性 id class v-text
5. 通过 v-model = 'name' 拿到指令类型 'v-model' 拿到需要绑定的数据属性名 ‘name’
6. 判断当前是v-model 指令 让后通过操作dom api 把name 属性对应的值放上去 node.value = data[name]
v-model 和 v-text 除了指令类型不一致 使用的dom api 不一致 其他的步骤都是完全一致的
2. V-M
本质: 事件监听 在回调函数中 拿到 input 中 输入的最新的值 然后赋值给绑定的属性
node.addEventListener ('input',(e)={
data[name] = e.target.value
})
以上 总结
1. 数据的响应式
2. 数据变化影响视图
3.视图变化影像数据
4.指令是如何是实现的
优化工作:
1. 通用的数据响应式处理
data(){
return{
name:“cp”,
age:28
}
}
基于现成的数据 然后都处理成响应式数据
Object.keys(data).forEach(key=>{
// key 属性名
// data[key] 属性值
// data 源对象
// 将所有的key都转成get和set的形式
defineReactive(data,key,data[key])
})
function defineReactive(data,key,value){
Object.defineProperty(data,key,{
get(){
return value
}
set(newValue){
value = newValue
}
})
}
2. 发布订阅
问题:
<div>
<p v-text='name'> </p>
<p v-text='name'> </p>
<input v-model='name'> </input>
</div>
name 发生改变之后 我需要做的事情是 更新两个p标签 而现在不管更新了哪个数据 所有的标签都会被重新
操作赋值 无法做到精准更新
解决的问题
1. 数据发生变化之后 最关键的代码是什么?
node.innerText = data[name]
2. 设计一个存储结构
每一个响应式数据可能被多个标签绑定 是一个 一对多的关系
{
name :[()=>{node(p1).innerText = data[name],}]
}
发布订阅(自定义事件) 解决的问题就是 ‘1 对多的问题’
实现简单的发布订阅模式
浏览器的事件模型
dom.addEventListener('click',()=>{})
只要调用click 事件 所有绑定的回调函数都会执行 显然是一个一对多的关系
const Dep = {
map:{},
collect(eventName,fn){
if(!data[eventName]){
this.map[eventName] = []
}
this.map[eventName].push(fn)
},
trigger(eventName){
this.map[eventName].forEach(fn=>fn())
}
}
使用发布订阅模式 进行优化
先前的写法 不管是哪个数据发生变化 都是粗暴地执行 compile 函数
使用发布订阅模式之后 ,compile 函数初次执行的时候 完成更新函数的收集
然后在数据变化的时候 通过数据的key 找到相对应的更新函数 依次执行 达到精准更新的效果
node.innerText = data[name]
2. 设计一个存储结构
每一个响应式数据可能被多个标签绑定 是一个 一对多的关系
{
name :[()=>{node(p1).innerText = data[name],}]
}
发布订阅(自定义事件) 解决的问题就是 ‘1 对多的问题’
实现简单的发布订阅模式
浏览器的事件模型
dom.addEventListener('click',()=>{})
只要调用click 事件 所有绑定的回调函数都会执行 显然是一个一对多的关系
const Dep = {
map:{},
collect(eventName,fn){
if(!data[eventName]){
this.map[eventName] = []
}
this.map[eventName].push(fn)
},
trigger(eventName){
this.map[eventName].forEach(fn=>fn())
}
}
使用发布订阅模式 进行优化
先前的写法 不管是哪个数据发生变化 都是粗暴地执行 compile 函数
使用发布订阅模式之后 ,compile 函数初次执行的时候 完成更新函数的收集
然后在数据变化的时候 通过数据的key 找到相对应的更新函数 依次执行 达到精准更新的效果
最后
以上就是腼腆睫毛为你收集整理的【前端进阶】Vue 高级的全部内容,希望文章能够帮你解决【前端进阶】Vue 高级所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复