class和style的使用
对象语法
可以:class=”{ active: isActive }”, 也可以是多个对象共存。
<divclass="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中数据是异步更新,设置数据后,没法里面取到更新的DOMthis.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];// 更新 DOMwatcher.run();}}
从以上第6步可以看出,把具体的更新方法 flushSchedulerQueue 传给 nextTick 进行调用,接下来分析nextTick方法
const callbacks = [];//第2至35行,主要判断timerFunc对象的不同环境下的兼容性let timerFunc;// 判断是否兼容 Promiseif (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 0timerFunc = () => {setTimeout(flushCallbacks, 0);};}// nextTick方法的定义function nextTick(cb?: Function, ctx?: Object) {let _resolve;// 1.将传入的 flushSchedulerQueue 方法添加到回调数组callbacks.push(() => {cb.call(ctx);});// 2.执行异步任务// 此方法会根据浏览器兼容性,选用不同的异步策略,在timerFunc内部执行flushCallbackstimerFunc();}// 异步执行完后,执行所有的回调方法,也就是执行 flushSchedulerQueuefunction 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.jsimport 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-focusVue.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() {// 开启异步队列,批量更新 WatcherqueueWatcher(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 = 18console.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为nulllet val = cb()Dep.target = nullreturn 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 = newValdeps.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 = nullclass Observable{constructor(obj){this.obj = objreturn this.main()}main(){let _self = thislet 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 = newValdep.notify()}})})return this.obj}}const onComputedUpdate=(val)=>{console.log(`我是${val}学生`);}class Watcher{constructor(obj,key,cb){this.obj = objthis.key = keythis.cb = cbthis.watcherFun()}watcherFun(){let _self = thisconst onDepUpdate=()=>{let val = this.cb()onComputedUpdate(val)}//简化版watcher的get方法Object.defineProperty(this.obj,this.key,{get(){Dep.target = onDepUpdatelet val = _self.cb()Dep.target = nullreturn 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
