class和style的使用
对象语法
可以:class=”{ active: isActive }”, 也可以是多个对象共存。
<div
class="static"
v-bind:class="{ active: isActive, 'text-danger': hasError }"
></div>
data: {
isActive: true,
hasError: false
}
数组语法
把一个数字绑定给:classs=”[cls1, cls2]”
在数组语法中可以使用三元表达式
<div v-bind:class="[activeClass, errorClass]"></div>
data: {
activeClass: 'active',
errorClass: 'text-danger'
}
动态组件 & 异步组件
动态组件
在动态组件上可以使用keep-alive来回切换组件的渲染,并且减少对组件的加载。
组件在 <keep-alive>
内被切换,它的 activated
和 deactivated
这两个生命周期钩子函数将会被对应执行。
activated:组件实例被激活显示
deactivated:组件实例隐藏,失去激活。
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>
异步组件
大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。
// 局部注册组件
components: {
'my-component': () => import('./my-async-component')
}
// 异步工厂函数导入异步组件
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})
异步更新数据nextTick
vue中数据更新是异步,更改data后不能利可获取修改后的DOM元素。要想获得更新后的DOM使用nextTick。
// 在vue中数据是异步更新,设置数据后,没法里面取到更新的DOM
this.message = "hello world";
const textContent = document.getElementById("text").textContent;
// 直接获取,不是最新的DOM节点
console.log(textContent === "hello world"); // false
// 必须使用nextTick回调才能取到最新值
this.$nextTick(() => {
const textContent = document.getElementById("text").textContent;
console.warn(textContent === "hello world"); // true
});
数据更新是vue的执行过程
1.触发data.set
2.调用Dep.notify
3.Dep会遍历所有相关的watcher 然后执行update方法
class Watcher{
// 4.执行更新操作
update(){
queueWatcher(this)
}
}
const queue = [];
function queueWatcher(watcher: Watcher) {
// 5. 将当前 Watcher 添加到异步队列
queue.push(watcher);
// 6. 执行异步队列,并传入回调
nextTick(flushSchedulerQueue);
}
// 更新视图的具体方法
function flushSchedulerQueue() {
let watcher
// 排序,先渲染父节点,再渲染子节点
// 这样可以避免不必要的子节点渲染,如:父节点中 v-if 为 false 的子节点,就不用渲染了
queue.sort((a, b) => a.id - b.id);
// 遍历所有 Watcher 进行批量更新。
for (let index = 0; index < queue.length; index++) {
watcher = queue[index];
// 更新 DOM
watcher.run();
}
}
从以上第6步可以看出,把具体的更新方法 flushSchedulerQueue 传给 nextTick 进行调用,接下来分析nextTick方法
const callbacks = [];
//第2至35行,主要判断timerFunc对象的不同环境下的兼容性
let timerFunc;
// 判断是否兼容 Promise
if (typeof Promise !== "undefined") {
timerFunc = () => {
Promise.resolve().then(flushCallbacks);
};
// 判断是否兼容 MutationObserver
// https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
} else if (typeof MutationObserver !== "undefined") {
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
// 判断是否兼容 setImmediate
// 该方法存在一些 IE 浏览器中
} else if (typeof setImmediate !== "undefined") {
// 这是一个宏任务,但相比 setTimeout 要更好
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 如果以上方法都不知道,使用 setTimeout 0
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
// nextTick方法的定义
function nextTick(cb?: Function, ctx?: Object) {
let _resolve;
// 1.将传入的 flushSchedulerQueue 方法添加到回调数组
callbacks.push(() => {
cb.call(ctx);
});
// 2.执行异步任务
// 此方法会根据浏览器兼容性,选用不同的异步策略,在timerFunc内部执行flushCallbacks
timerFunc();
}
// 异步执行完后,执行所有的回调方法,也就是执行 flushSchedulerQueue
function flushCallbacks() {
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
}
$set更改对象的值
通过下标修改数组的一个元素,必须使用$set来修改,直接修改不起作用
<template>
<div>
<p>{{arr}}</p>
<button @click="update">改变数组</button>
</div>
</template>
<script>
export default {
data(){
return {arr:[1,2,3]}
},
methods:{
update(){
this.$set(this.arr, 0 ,5)
}
}
}
</script>
组件间数据通信
父子组件:props属性传递
// 父组件
<template>
<Son :xing={xing}> </Son>
</template>
<script>
export default {
data(){
return {
xing:"张"
}
}
}
</script>
// 子组件Son中可以接收到props的xing
<script>
export default {
props:['xing'],
mounted(){
console.log("获取父组件传递过来的姓:",xing)
}
}
</script>
//props的对象定义形式
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
slot插槽传值
基本使用
定义一个包含插槽slot的组件navigation-link
<template>
<a v-bind:href="url" class="nav-link">
<slot></slot>
</a>
</template>
当使用组件时slot会被替换为组件标签内定义的内容。插槽内可以包含任何模板代码。
<navigation-link url="/profile">
<!-- 添加一个 Font Awesome 图标 -->
<span class="fa fa-user"></span>
Your Profile
</navigation-link>
// 或者
<navigation-link url="/profile">
<!-- 添加一个图标的组件 -->
<font-awesome-icon name="user"></font-awesome-icon>
Your Profile
</navigation-link>
使用navigation-link组件时,只能访问当前组件中的数据。
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
具名插槽
定义base-layout模板,主要使用slot的name属性
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
使用base-layout组件
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
作用域插槽
使用v-slot绑定数据
定义一个current-user组件模板,并添加slot属性标签
<span>
<slot v-bind:user="user">
{{ user.lastName }}
</slot>
</span>
有时候让插槽的内容能够访问到标签组件里的数据很有用,可能想替换掉子模板中备用内容。绑定在
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
<template v-slot:other="otherSlotProps">
...
</template>
</current-user>
如果只用一个插槽出现,可以使用简单的语法
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
</current-user>
子父组件:this.$emit触发事件更新父组件
vue只允许数据的单项数据传递,这时候我们可以通过自定义事件,触发事件来通知父组件来改变数据,从而达到子组件改变父组件数据
子组件:child
<template>
<div @click="up">
{{msg}}
</div>
</template>
<script>
export default{
data(){
return:{}
},
props:["msg"],
methods:{
up(){
// 子组件的$emit方法,更改父组件中的msg数据
this.$emit("upToPar","child event")
}
}
}
</script>
父组件
<template>
<div>
<child @upToPar="change" :msg="msg"></child> //监听子组件触发的upup事件,然后调用change方法
</div>
</tempalte>
<script>
export default{
data(){
return:{
msg:"this is default message;"
}
},
methods:{
change(msg){ //参数msg是在子组件的$emit函数中的第二个参数
this.msg = msg //this.msg是data中的msg
}
}
}
</script>
非父子间组件:EventBus
EventBus通过在main中新建一个公用的Hub对象(Vue的实例)
在main.js中创建
export let Hub = new Vue(); //创建事件中心
组件comA触发
<div @click="changeHub">click Hub event</div>
<script>
import {Hub} from "../main.js"
export default{
methods: {
changeHub() {
Hub.$emit('eventName','hehe'); //Hub触发事件
}
}
}
</script>
在组件comB接收
import {Hub} from "../main.js"
created() {
Hub.$on('eventName', () => { //Hub接收事件
console.log("this is Hub $on message,count follow child Component number")
});
},
beforeDestory(){
Hub.$off('eventName')
}
多层级父子关系provide-inject
App.vue
<Root>
<Father>
<Son>
<Grandson></Grandson>
</Son>
</Father>
</Root>
定义个root根组件
<template>
<div class="root">
这是root根组件
<p>这是root组件中的rootA{{rootA}}</p>
<slot></slot>
</div>
</template>
<script>
import { setInterval } from 'timers';
export default {
data () {
return {
rootA:"rootA",
};
},
provide(){
return{
rootA:this.rootA
}
},
methods: {
rootFun(){
console.log("rootA",this.rootA);
}
}
}
</script>
父组件Father
<template>
<div class="father">
这里是father父组件
<p>这是从root组件的provide中获取的数据rootA:{{rootA}}</p>
<b>{{$parent.rootA}}</b>
<slot></slot>
</div>
</template>
<script>
export default {
inject:["rootA"],
}
</script>
子组件Son
<template>
<div class="son">
这里是son儿组件
<p>这是从root组件的provide中获取的数据rootA:{{rootA}}</p>
<slot></slot>
</div>
</template>
<script>
export default {
inject:["rootA"]
}
</script>
孙组件grandSon
<template>
<div class="grandson">
这里是grandson孙组件
<p>这是从root组件的provide中获取的数据rootA:{{rootA}}</p>
</div>
</template>
<script>
export default {
inject:["rootA"]
}
</script>
复杂组件间传值:vuex
事件
事件修饰符
- stop:阻止事件冒泡传播
- prevent: v-on:submit.prevent , 提交事件时不重载页面
- capture:添加事件监听器时使用事件捕获模式
- self:只当在event.target是当前元素是自身时,触发函数。事件不是从内部元素触发
- once:点击事件只会触发一次
按键修饰符
<input v-on:keyup.enter="submit"> // 点击enter进行提交
<input v-on:keyup.page-down="onPageDown"> //点击pagedown按键松开时触发事件
过滤器
注册组件内过滤器
Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 **v-bind**
表达式
<!-- 在双花括号中 -->
{{ message | capitalize }}
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | capitalize"></div>
//对date日期定义过滤器格式化
{{ dateStr | formatDate}}
可以在组件的script中定义
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
},
formatDate(value) {
return format(value, "yyyy-MM-DD HH:mm:ss");
}
}
注册全局过滤器
有些过滤器使用的很频繁,比如上面提到的日期过滤器,在很多地方都要使用,这时候如果在每一个要用到的组件里面都去定义一遍,就显得有些多余了,这时候就可以考虑Vue.filter
注册全局过滤器
对于全局过滤器,一般建议在项目里面添加filters
目录,然后在filters目录里面添加
// filters\index.js
import Vue from 'vue'
import { format } from '@/utils/date'
Vue.filter('formatDate', value => {
return format(value, 'yyyy-MM-DD HH:mm:ss')
})
使用的时候将该filters/index.js引入到main.js中
vuex的使用
vue-router的使用
基本使用
vue单页面开发,vue-router在前端进行页面逻辑的跳转。
路由的两种模式:hash和history
- hash —— 地址栏 URL 中有 # 符号(此 hash 不是密码学里的散列运算)。
比如URL:http://www.abc.com/#/hello,hash 的值为 #/hello。它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。使用 window.location.hash 读取 # 值。这个属性可读可写。读取时,可以用来判断网页状态是否改变;每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,可以回到上个位置。 - history —— 利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。(需要特定浏览器支持)
这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。
hash 和 history的使用场景
一般场景下,hash 和 history 都可以,除非你更在意颜值, # 符号夹杂在 URL 里看起来确实有些丑陋。
history 的优点
另外,根据 Mozilla Develop Network 的介绍,调用 history.pushState() 相比于直接修改 hash,存在以下优势:
- pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;
- pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;
- pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;
- pushState() 可额外设置 title 属性供后续使用。
history 的缺陷
SPA 虽然在浏览器里游刃有余,但真要通过 URL 向后端发起 HTTP 请求时,两者的差异就来了。尤其在用户手动输入 URL 后回车,或者刷新(重启)浏览器的时候。
- hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://www.abc.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。
- history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.abc.com/book/id。如果后端缺少对 /book/id 的路由处理,将返回 404 错误。Vue-Router 官网里如此描述:“这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回一个 index.html 页面,这个页面就是你 app 依赖的页面。”
在页面中是router-link进行导航,使用router-view进行相应组件的展示
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
router和route的区别
- router:表示整个项目下的路由器对象,有push方法,进行路由跳转
- route:表示当前页面的组件路由信息,可以获取当前页面对象的name、path、query、params等。
子路由嵌套
比如新闻标签下存在热门新闻、最新新闻
创建router对象<ul>
<li><router-link to="/">home</router-link></li>
<li><router-link to="/news">news</router-link></li>
<ol>
<li><router-link to="/news/hot">hotNews</router-link></li>
<li><router-link to="/news/latest">latestNews</router-link></li>
</ol>
<li><router-link to="/info">info</router-link></li>
</ul>
<router-view><router-view>
export default new VueRouter({
mode: 'history',
base : __dirname,
routes:[
{path:'/', component:Home},
{path:'/news', component:Child,
//在子路由的children中不能设置/,否则会被当做跟路由渲染
children: [
{path: ' ', component:News},
{path: 'hot', component:Hot},
{path: 'latest', component:Latest}
]},
{path:'/info', component:Info}
]
})
路由传参
name和params配合传参
在template模板中,通过:to绑定对象传参
必须在router对象中设置对应的name属性<ol> <!--通过设置:to,可以在这里面进行参数设置并传递到子页面-->
<li><router-link :to="{name:'HotNews',params:{ num :3}}">hotNews</li>
<li><router-link :to="{name:'LatestNews',params:{ num :5}}">latestNews</li>
</ol>
然后就可以在页面中读取到数据export default new VueRouter({
mode: 'history',
base : __dirname,
routes:[
{path:'/',name:'Home', component:Home},
{path:'/news', component:Child,
children: [ <!--这些name参数值,就可以显示在App.vue文件中-->
{path: '/', name:'News', component:News},
{path: 'hot', name:'HotNews',component:Hot},
{path: 'latest', name:'LatestNews', component:Latest}
]},
{path:'/info', name:'Info', component:Info}
]
})
<template>
<div>
<h2>子路由+通过绑定:to传递给子页面的参数{{$route.params.num}}</h2> <!--{{ $route.params.num就可接收到在App.vue的to中设置的参数}}-->
<router-view></router-view>
</div>
</template>
path和query配合传参
取得路径url中query的值,可以配合path使用
页面模板中定义path和query的值
新建router对象<li><router-link :to="{path:'/users/小明',query:{aaa:'bbb'}}">xiaoming</router-link></li>
在子页面文件可以读取到参数数据{
path:"/users/:username",
name:"users",
component: ()=> import(/* webpackChunkName: "user" */ '../views/User.vue')
}
path后的参数使用params查询,query的参数用query获取<p>{{$route.params.username}}+{{$route.query.aaa}}</p> //显示 小明+bbb
重命名和alias别名
在定义的router对象中
alias别名可以给一个组件页面定义多个名称{path: 'third', redirect:'News'} //直接重定向到News组件
// router.js
{
path: '/info',
name: 'Info',
components: {
default: Info,
left: Hot,
right: Latest
},
alias: ['/xiaoxi', '/xinxi']
}
// App.vue
<li><router-link to="xiaoxi">xiaoxi</router-link></li>
<li><router-link to="xinxi">xinxi</router-link></li>
路由守卫
全局守卫
router.beforeEach是全局的路由守卫,所有路由访问必经此方法const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// to: Route: 即将要进入的目标 路由对象
// from: Route: 当前导航正要离开的路由
// next: Function: 一定要调用该方法来 resolve 这个钩子。
})
路由router配置信息中设置独享守卫
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// to: Route: 即将要进入的目标 路由对象
// from: Route: 当前导航正要离开的路由
// next: Function: 一定要调用该方法来 resolve 这个钩子。
}
}
]
})
组件内守卫
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
在路由守卫时做权限校验和设置title
权限校验,配置接口请求的返回值和路由守卫router.beforeEach((to, from, next) => {
if(to.path === 'b页面路径'){
axios({
method:'post',
url:'http://xxx.request.com.cn/login',
data:{}
})
.then(response => {
if(response.data.success){
if(response.data.data.elecAccStatus != 2){
next(false);
}else{
/* 从其他页面到b页面没问题走这里能进入 */
next();
}
}else{
/*b页面二次刷新接口不通走这里 */
next(false);
}
})
.catch(error => {
next(false);
})
}
})
在router.js的路由配置中设置meta:{title:”index”},然后在router对象的beforeEach守卫做设置
let router = new Router({
routes: [
{
path: "/index",
name: "index",
component: () =>
import(/* webpackChunkName: "index" */ "@/views/common/index.vue"),
meta:{
title:'index'
}
},
]
});
router.beforeEach((to, from, next) => {
if (to.meta.title) {
document.title = to.meta.title
} else {
document.title = "other"
}
next();
});
自定义指令
除了默认设置的核心指令( v-model 和 v-show ), Vue 也允许注册自定义指令。下面我们注册一个全局指令 v-focus, 该指令的功能是在页面加载时,元素获得焦点:
<template>
<div>
<input value="自动获取焦点" v-focus />
</div>
</template>
<script>
// 注册一个全局自定义指令 v-focus
Vue.directive('focus', {
// bind钩子函数,只调用一次,指令第一次绑定到元素时调用
bind(){ console.log("bind 钩子") },
// inserted: 被绑定元素插入父节点时调用
inserted(el){
el.focus()
}
})
// 注册一个数的平方指令
Vue.directive("n",{
bind(el,binding){
el.textContent = Math.pow(binding.value, 2)
},
update(el,binding){
el.textContent = Math.pow(binding.value, 2)
}
})
</script>
指令定义函数提供了几个钩子函数(可选):
bind
: 只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个在绑定时执行一次的初始化动作。inserted
: 被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于 document 中)。update
: 被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新(详细的钩子函数参数见下)。componentUpdated
: 被绑定元素所在模板完成一次更新周期时调用。unbind
: 只调用一次, 指令与元素解绑时调用。
钩子函数的参数有:
- el: 指令所绑定的元素,可以用来直接操作 DOM 。
- binding: 一个对象,包含以下属性:
- name: 指令名,不包括
v-
前缀。 - value: 指令的绑定值, 例如:
v-my-directive="1 + 1"
, value 的值是2
。 - oldValue: 指令绑定的前一个值,仅在
update
和componentUpdated
钩子中可用。无论值是否改变都可用。 - expression: 绑定值的表达式或变量名。 例如
v-my-directive="1 + 1"
, expression 的值是"1 + 1"
。 - arg: 传给指令的参数。例如
v-my-directive:foo
, arg 的值是"foo"
。 - modifiers: 一个包含修饰符的对象。 例如:
v-my-directive.foo.bar
, 修饰符对象 modifiers 的值是{ foo: true, bar: true }
。
- name: 指令名,不包括
- vnode: Vue 编译生成的虚拟节点。
- oldVnode: 上一个虚拟节点,仅在
update
和componentUpdated
钩子中可用。
typescript的支持
引入并定义的对ts支持的组件类需要的对象
import { Component, Vue, Prop, Watch } from "vue-property-decorator";
import { Route } from "vue-router";
class tsCom extends Vue{ ... }
export default tsCom;
@Component属性
引入外部组件、定义过滤器方法、父组件属性的传值都写在该类中
@Component({
// 组件注册
components: {
'another-vue': AnotherVue
},
// 过滤器
filters: {
filterNumberToString(value: Number) {
// 对数字类型进行千分位格式化
return Number(value).toLocaleString();
}
},
// 属性传递
props: {
hideHeader: {
type: Boolean,
required: false,
default: false // 默认属性的默认值
}
}
})
在组件内定义数据,类似于data的数据
@Prop({
type: Boolean,
required: false,
default: false // 默认属性的默认值
})
//只有组件内部使用
private hideHeader!: boolean | undefined;
// 继承该组件的子组件可以使用
protected userList?: string[] = ["a", "b", "c"]; // 其他没有默认值的传值
public oneKeyObj: Object = { name: "key", age: 1 };
selfKey: string = "自己的一个变量";
生命周期钩子
created() {console.log("created 创建阶段")}
mounted() {
console.log("mounted 挂载阶段");
}
updated(){}
beforeDestory(){}
计算属性computed
get computedKey() {
return this.userList.length;
}
监听器watch
// 监听器,监听计算数据的变化
@Watch("computedKey")
getcomputedKey(newVal: any) {
console.log("computedKey.length newVal", newVal);
}
// 监听路由变化, immediate表示立即执行一次,deep表示对对象深度监听。
@Watch("$route", { immediate: true, deep:true })
private changeRouter(route: Route) {
console.log("监听路由route对象变化", route);
}
// 导航守卫函数
beforeRouteEnter(to: Route, from: Route, next: () => void): void {
console.log("beforeRouteEnter", to, from);
next();
}
beforeRouteLeave(to: Route, from: Route, next: () => void): void {
console.log("beforeRouteLeave", to, from);
next();
}
方法的定义
addText() {
this.selfKey += ",追加文字!";
}
nowDate(){
console.log(Date.now())
}
// 子组件向父组件发送事件
@Emit()
private sendMsg():string{
console.log("sendmsg");
msg = "father"
// 事件的返回值为传递的参数
return "this is send to father message"
}
Emit()转换的js是将事件名称用-连接的事件方法
send-msg(){
this.$emit("send-msg");
console.log("sendmsg");
msg = "father"
// 事件的返回值为传递的参数
return "this is send to father message"
}
vue2数据绑定源码分析
vue2使用Object.defineProperty方法把data对象的全部属性转化成getter/setter,当属性被访问或修改时通知变化。
将数据变成可以响应式
const students = {
name:"北鸟南游",
age:16
}
// 创建响应式函数
function defineReactive(obj, key, val){
Object.defineProperty(obj, key, {
get(){
console.log(`读取了${key}的值为${val}`);
//源码中是下面一句:
// if (Dep.target) { dep.depend(); } 作用进行依赖收集
return val
},
set(newVal){
console.log(`设置了${key}的值为${newVal}`);
val = newVal
// 源码中是下面一句,对dep进行派发更新
// dep.notify();
}
})
}
// 将对象的每一个属性都转为可观察的属性
function observable (obj) {
const keys = Object.keys(obj);
keys.forEach((key) => {
defineReactive(obj, key, obj[key])
})
// 一定必须要把这个对象返回出去
return obj
}
students = observable(students)
此时在控制台访问student的name会打印get读取的信息,设置age信息,会打印set的设置信息。
设置监听器watcher
Vue源码中Watcher类的简化版
class Watcher {
constructor(vm: Component, expOrFn: string | Function) {
// 将 vm._render 方法赋值给 getter。
// 这里的 expOrFn 其实就是 vm._render,后文会讲到。
this.getter = expOrFn;
this.value = this.get();
}
get() {
// 给 Dep.target 赋值为当前 Watcher 对象
Dep.target = this;
// this.getter 其实就是 vm._render,this.getter的执行就是Watcher的更新
// vm._render 用来生成虚拟 dom、执行 dom-diff、更新真实 dom。
const value = this.getter.call(this.vm, this.vm);
return value;
}
addDep(dep: Dep) {
// 将当前的 Watcher 添加到 Dep 收集池中
dep.addSub(this);
}
update() {
// 开启异步队列,批量更新 Watcher
queueWatcher(this);
}
run() {
// 和初始化一样,会调用 get 方法,更新视图
const value = this.get();
}
}
Watcher存在的意义:响应式数据value发生了变化,希望Watcher能够触发视图更新,将响应式数据的key与Watcher建立相对应关系。下面实验一个简化版的Watcher
// obj和key相当于vm实例,cb相当于expOrFn,数据变化后进行的更新变化
function watcher(obj, key, cb) {
const onComputedUpdate = (val) => {
console.log(`我是${val}学生`)
}
Object.defineProperty(obj, key, {
get() {
let val = cb()
onComputedUpdate(val)
return val
},
set() {
console.error("观察者属性不能被赋值")
}
})
}
let student = {
name: '北鸟南游',
age: 16
}
watcher(student, 'level', () => {
return student.age > 16 ? "大" : '小'
})
console.log(student.level);
student.age = 18
console.log(student.level);
运行结果:伴随着age数据的变化,level会有不同结果值渲染。
我是小学生
小
我是大学生
大
添加依赖收集
现在已存在了响应式数据observable和观察者watcher的函数,接下来是怎么把两者建立联系。假如说能够在响应式对象的getter/setter里能够执行监听器watcher的
onComponentUpdate()方法,就可以实现让对象主动出发渲染更新的功能。
由于watcher内的onComponentUpdate()需要接收回调函数的返回值作为参数,但是响应式对象内没有这个回调函数,需要借助一个第三方对象这个回调函数传递给响应式对象里面,即把watcher对象传递给响应式对象。第三方全局对象Dep就应运而生。
Dep对象依赖收集器对象,Dep 对象用于依赖收集(收集监听器watcher内 回调函数的值以及onComponentUpdate()方法),它实现了一个发布订阅模式,完成了响应式数据 Data 和观察者 Watcher 的订阅。
定义一个Dep对象
const Dep = {
target: null //Vue源码中Dep.target是Watcher的一个实例对象
}
Dep的target用来存放监听器Watcher及监听器里的onComponentUpdate()方法。
重写上面简化版的watcher函数
const onComputedUpdate=(val)=> {
console.log(`我是${val}学生`);
}
function watcher(obj, key, cb) {
// 1定义一个函数,稍后将全局Dep.target对象指向该函数,将cb的回调返回值可以传递给响应式对象
const onDepUpdate = () => {
let val = cb()
onComputedUpdate(val)
}
Object.defineProperty(obj, key, {
get() {
Dep.target = onDepUpdate
// 2 执行cb()的过程中会用到Dep.target,
// 3 当cb()执行完重置Dep.target为null
let val = cb()
Dep.target = null
return val
}
})
}
在监听器内部定义了一个新的onDepUpdate()方法,这个方法很简单,就是把监听器回调函数的值以及onComputedUpdate()给打包到一块,然后赋值给Dep.target。这一步非常关键,通过这样的操作,依赖收集器就获得了监听器的回调值以及onComputedUpdate()方法。作为全局变量,Dep.target理所当然的能够被可观测对象的getter/setter所使用。
改写响应式方法函数observable,将Dep.target添加到函数内
const Dep = {
target: null
}
function observable(obj) {
let keys = Object.keys(obj)
let deps = []
keys.forEach(key => {
let val = obj[key]
Object.defineProperty(obj, key, {
get() {
if (Dep.target && deps.indexOf(Dep.target) < 0) {
deps.push(Dep.target)
}
return val
},
set(newVal) {
val = newVal
deps.forEach(dep => {
dep()
})
}
})
})
return obj
}
在observable函数内定义一个空数组deps。当obj的key的getter被触发时,就给deps添加Dep.target。此时Dep.target已经被赋值为onDepUpdate,让响应式对象可以访问到watcher。当响应式对象obj中key的setter被触发时,就调用deps中所保存的Dep.target方法,也就自动触发监听器内部的onComponentUpdate()函数。
deps被定义为数组而不是一个基本变量,是因为同一个属性key可以被多个watcher所依赖,可能存在多个Dep.target。定义deps数组,如果当前属性setter被触发,可以批量调用多个watcher的更新方法deps.forEach的操作。
可以添加实验测试:
const student = observable({
name: "kk",
age: 16
})
watcher(student, "level", () => {
return student.age > 16 ? "大" : "小"
})
console.log(student.level);
student.age =18
运行结果:
小
我是大学生
给响应式对象student的age属性重新赋值,会主动触发onComputedUpdate()方法的指向。打印出“我是大学生”
改写成类class的形式
把依赖收集器对象的功能进行聚合,把Dep执行的方法放到Dep类中。
class Dep {
constructor() {
this.deps = []
}
depend() {
if (Dep.target && this.deps.indexOf(Dep.target) < 0)
this.deps.push(Dep.target)
}
notify() {
this.deps.forEach(dep => {
dep()
})
}
}
Dep.target = null
class Observable{
constructor(obj){
this.obj = obj
return this.main()
}
main(){
let _self = this
let keys = Object.keys(this.obj)
let dep = new Dep()
keys.forEach(key=>{
let val = this.obj[key]
Object.defineProperty(this.obj,key,{
get(){
dep.depend()
return val
},
set(newVal){
val = newVal
dep.notify()
}
})
})
return this.obj
}
}
const onComputedUpdate=(val)=>{
console.log(`我是${val}学生`);
}
class Watcher{
constructor(obj,key,cb){
this.obj = obj
this.key = key
this.cb = cb
this.watcherFun()
}
watcherFun(){
let _self = this
const onDepUpdate=()=>{
let val = this.cb()
onComputedUpdate(val)
}
//简化版watcher的get方法
Object.defineProperty(this.obj,this.key,{
get(){
Dep.target = onDepUpdate
let val = _self.cb()
Dep.target = null
return val
}
})
}
}
let student = new Observable({
name:'北鸟南游',
age:16
})
new Watcher(student,'level',()=>{
return student.age >16 ?"大":"小"
})
console.log(student.level);
student.age = 18
简化vue数据绑定更新的流程,理清了响应对象data、watcher及Dep的职责和关系。
更详细的vue响应式原理分析参考https://lmjben.github.io/blog/library-vue-flow.html