当前位置: 代码迷 >> 综合 >> 【vue】4、directives、keep alive、vue实现双向绑定原理、$refs、$nextTick
  详细解决方案

【vue】4、directives、keep alive、vue实现双向绑定原理、$refs、$nextTick

热度:76   发布时间:2024-01-24 21:12:40.0

自定义指令directives

这是局部的,全局的需要Vue.
directives:{color:{ //自定义指令名bind:function(el,binding,vnode){ //bind是钩子函数el.style.color=binging.value...el:是当前元素,默认传进来的binding:是自定义指令的参数,一般在function中使用binding.value;可有可没有vnode:虚拟dom节点}
}<div v-color='red'>abc</color>

1. 钩子函数

  • bind: 当每次将该自定义指令绑定时自动调用;且只调用一次。
  • unbind:当元素与该指令解绑时
  • inserted:当该元素被渲染到(添加到)html页面的Dom中时
  • update:所有组件的vnode更新时 (vnode就是虚拟dom节点)
  • componentUpdated:该组件的vnode及子vnode全部全部更新后调用

2.钩子函数的一些参数

  • el:指令所绑定的元素,可以用来直接操作 DOM 。
  • binding:一个对象,包含以下属性:
    • name:指令名,不包括 v- 前缀。binding.name
    • value:指令的绑定值,例如:<p v-my-directive="1 + 1">中,绑定值为2。binding.value。同时也可以<p v-my-directive="{id:1,text:'abc'}"> 这样的话传入的是一个对象,那么就是binding.value.text。
    • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    • arg:传给指令的参数(:),可选。例如 v-my-directive:foo 中,参数为 "foo"
    • modifiers:一个包含修饰符(.)的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
  • vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。
  • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

3. 钩子函数能够动态定义指令的参数:arg

<p v-my-directives:[arg]='value'> example 注意引号是否存在 </p>data:{arg:'left',value:'200px'
}

4. 简写方法

上面的那个自定义指令例子是全写,简写指的是不注明钩子函数,此时默认绑定bind和update

directive:{ //指令:v-colorcolor:function(...){....} //默认绑定bind和update
}

Keep-alive

https://www.jianshu.com/p/9523bb439950

  • 定义

在开发中经常有从列表跳到详情页,然后返回详情页的时候需要缓存列表页的状态(比如滚动位置信息),这个时候就需要保存状态,要缓存状态。

keep-alive不仅仅是能够保存页面/组件的状态这么简单,它还可以避免组件反复创建和渲染,有效提升系统性能。总的来说,keep-alive用于保存组件的渲染状态。

<keep-alive><com1> 或者 <router-view>
</keep-alive>
  • 属性 props

  1. include:定义缓存白名单
  2. exclude:定义缓存黑名单
  3. max:最多能缓存多少组件
  4. activated 和 deactivate 生命周期钩子
  • 实现步骤

1.a,b,c会被keep-alive
<keep-alive include='a,b,c'><router-view>
</keep-alive>2.
<keep-alive><router-view v-if='this.$router.meta.keepAlive'></router-view>
</keep-alive>
同时给router设置meta:
export default{path:'',component:'',meta:{keepAlive:true}
}
  1. 第一步:获取keep-alive包裹着的第一个子组件对象及其组件名;
  2. 第二步:根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则执行第三步;
  3. 第三步:根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该key在this.keys中的位置(更新key的位置是实现LRU置换策略的关键),否则执行第四步;
  4. 第四步:在this.cache对象中存储该组件实例并保存key值,之后检查缓存的实例数量是否超过max设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key);
  5. 第五步:最后并且很重要,将该组件实例的keepAlive属性值设置为true。
  • 生命周期过程

         设置了keep-alive的组件:
第一次进入: beforeRouteEnter => created => ... => activated => ... => deactivated
后续进入:beforeRouteEnter => activated => deactivated,
只有第一次进入该组件时,才会走created钩子,需要缓存的组件中activated时每次都会走的钩子函数

  • 页面滚动问题

https://juejin.im/post/5b2ce07ce51d45588a7dbf76

在router实例里面添加scrollBehavior方法

const router=new VueRouter({routes:[{path:"/",component:Home}],scrollBehavior(to,form,savedPosition){if(savedPosition){点击按钮时返回的页面会滚动过到之前按钮return savedPosition}else{设置返回值来指定页面的滚动位置,表示在用户切换路由时让是所有页面都返回到顶部位置return {x:0,y:0}}}
})

如果是通过vue路由进行的页面切换。例如a前往b,首先判断a是不是通过keep-alive缓存的组件,如果是,则在a路由的meta中添加一个savedPosition字段,并且值为a的滚动位置。最后return的是页面需要回滚的位置。如此一来,如果打开一个页面,该页面的组件路由中meta.savedPosition为undefined的话,则页面滚动到(0,0)的位置。那么如果打开一个页面,它的路由的meta.savedPosition有值的话,则滚动到上次浏览的位置,因为meta.savedPosition保存的就是上次浏览的位置。


Vue实现双向绑定

1.首先背下来什么是MVVM

2.实现步骤

首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。因此接下去我们执行以下3个步骤,实现数据的双向绑定:

1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。

并且实现一个订阅者管理器。这个管理器中有两个方法:添加订阅者方法和通知订阅者方法。还有一个target属性和一个subs数组。target是当前的订阅者。subs数组是订阅者数组。

下面的代码最好背下来。

function defineReactive(data, key, val) {observe(val); var dep = new Dep(); // 背下来:定义一个订阅器Object.defineProperty(data, key, {enumerable: true,  // 背下来configurable: true,  // 背下来get: function() { // 背下来if (dep.target) {  // 背下来dep.addSub(dep.target); // // 背下来:在这里添加一个订阅者}return val;// 背下来},set: function(newVal) {if (val === newVal) {return;}val = newVal;dep.notify(); // // 背下来:如果数据变化,通知所有订阅者}});
}function Dep () {this.subs = [];
}
Dep.prototype = {addSub: function(sub) { // 背下来:添加订阅者this.subs.push(sub);},notify: function() {this.subs.forEach(function(sub) { // 背下来:通知更新订阅者sub.update();});}
};

2.实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。

3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。


$refs和ref

ref可以加到dom上也可以加到组件上,ref相当于name的作用。

<com ref='com'></com>
<div ref='div1'>div test</div>this.$refs.com
this.$refs.div1
  • $refs是当前组件的ref的集合。但是this.$refs. 不能写道模板里<template>,因为他们不是响应式的
  • 在created及beforecreated之前,$refs获取不到。

三大作用:

1. 父组件的vue js代码中获得子组件的data、method、computed

子组件.vue
data:{msg:'son'
},
methods:{test_son:function(){ ... }
}
父组件.vue
<com ref='com'></com>methods:{test_father:function(){this.$refs.com.test_son()console.log(this.$refs.com.msg)}
}

2.子组件获得父组件的方法:通过this.$emit('触发父组件methods的v-on名')

点击button
父组件.vue
<com ref='com' @test2='test_father'></com>
<button @test1='get_son'>click</button>methods:{get_son:function(){this.$refs.com.test_son()},test_father:function(){console.log('output father')}
}
子组件.vue
methods:{test_son:function(){this.$emit('test2')}
}

$ref 和 v-for一起使用:(ref=变量,前别忘记加:)

$refs 和 v-for一起用的时候比较特殊,$refs 会受到 v-if、v-for、v-show 这些语句的影响

<body>
<div  id="app"><ul ><li v-for='item in array' :name='item.name' :ref='item.name'>{{item.name}}</li></ul><button @click='c1'>click</button>
</div><script>
var vm=new Vue({el:'#app',data:{array:[{key:1,name:'one'},{key:2,name:'two'}]},methods:{c1:function(){console.log(this.$refs.one)}}
})</script>
</body>

点击按钮之后输出:这个li莫名是个数组,获得不到每个li的name,所以这种写法不对

(这里注意一下,如果是:ref='item.key',下面就是undefined了。因为$refs只接收字符串)

下面这种写法,跟上面比就改了两句话

<body>
<div  id="app"><ul ><li v-for='item in array' :name='item.name' :ref='item.tag'>{{item.name}}</li></ul><button @click='c1'>click</button>
</div><script>
var vm=new Vue({el:'#app',data:{array:[{key:1,name:'one',tag:'array'},{key:2,name:'two',tag:'array'}]},methods:{c1:function(){console.log(this.$refs.array[0])var q=this.$refs.array[0];console.log(q.getAttribute('name'))}}
})</script>
</body>

(这里有用模板匹配的方法,我没看:https://blog.csdn.net/LzzMandy/article/details/91372406)



$nextTick()

(和前面的event loop一起食用更加)

nextTick(function(){...}),是将其内部的回调函数function延迟在下一次dom更新数据后调用。同一事件循环DOM 更新会引起nextTick回调函数触发

<template><div class="hello"><div><button id="firstBtn" @click="testClick()" ref="aa">{{testMsg}}</button></div></div>
</template><script>
export default {name: 'HelloWorld',data () {return {testMsg:"原始值",}},methods:{testClick:function(){this.testMsg="修改后的值";this.$nextTick(function(){console.log(that.$refs.aa.innerText);  //输出:“修改后的值”});//这里正常应该输出“原始值”}}
}
</script>

nextTick()的用处:

  1. Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中,原因是在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳。
  2. 当项目中你想在改变DOM元素的数据后基于新的dom做点什么,对新DOM一系列的js操作都需要放进Vue.nextTick()的回调函数中
  3. 一些插件

nextTick()原理:

Vue是异步执行dom更新的,一旦观察到数据变化,Vue就会开启一个队列,然后把在同一个event loo的数据变化的推送进这个队列。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。

当你修改 vm.msg = '新值',DOM 并不会马上更新,而是在异步队列被清除,也就是下一个事件循环开始时执行更新时才会进行必要的DOM更新。此时你想要根据更新的 DOM 状态去做某些事情,就会出现问题。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。

简单的流程:同步代码执行 -> 查找异步队列,推入执行栈,执行Vue.nextTick[事件循环1] ->查找异步队列,推入执行栈,执行Vue.nextTick[事件循环2]...

 

主线程完成同步环境执行,查询任务队列,提取队首的任务,放入主线程中执行;执行完毕,再重复该操作,该过程称为事件循环。而主线程的每次读取任务队列操作,是一个事件循环的开始。

 

 

 

 

 

 

 

 

 

 

 

  相关解决方案