一、vue的双向绑定原理是什么?里面的关键点在哪里?
Vue通过Object.defineProperty()数据劫持和发布订阅者模式实现双向绑定。
首先,要有一个订阅者类,用于管理如何更新dom元素;然后,要有一个发布者类,发布者类用于收集每个订阅者的信息,和通知订阅者更新。
vue在创建文档碎片的时候,创建一个订阅者对象(该对象定义了如何更新内容的方法),并同时将这个对象存进发布者对象的数组里。
通过Object.defineProperty()方法劫持数据对象属性的getter\setter(取值\赋值)操作,
只要我们为vue中data数据重新赋值了,这个赋值动作会被vue监听到,通过发布者把数据的变化通知到每个订阅者(dom元素),订阅者根据最新数据,更新自己的内容(重新渲染自己的dom)
1、reduce()函数介绍
用法:
1、数值的累加计算
2、链式获取对象属性的值
arr.reduce((prev,cur,index,arr)=>{},init)
prev:上一次调用 callbackFn 时的返回值。在第一次调用时,若指定了初始值 init,其值则为 init,否则为数组索引为 0 的元素 arr[0]。
cur:数组中正在处理的元素。在第一次调用时,若指定了初始值 init,其值则为数组索引为 0 的元素 arr[0],否则为 arr[1]。
index:数组中正在处理的元素的索引。若指定了初始值 init,则起始索引号为 0,否则从索引 1 起始。
arr:用于遍历的数组。
2、发布订阅模式
(1)Dep类
负责进行依赖收集
首先有个数组存放所有订阅信息
其次,提供一个向数组中追加订阅信息的方法,add
然后,还要提供一个循环,循环触发数组中的每一个订阅信息
(2)Watcher类
负责订阅一些事件
(注:只要我们为vue中data数据重新赋值了,这个赋值动作会被vue监听到
然后 vue 要把数据的变化通知到每个订阅者(dom元素)
接下来,订阅者要根据最新数据,更新自己的内容(重新渲染自己的dom))
3、Object.defineProperty()
使用Object.defineProperty()实现数据劫持对象属性取值和赋值的操作
4、简单实现vue的双向绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h3>姓名是:{{name}}</h3>
<h3>年龄是:{{ age }}</h3>
<h3>info.a的值是:{{info.a}}</h3>
<div>name的值:<input type="text" v-model="name" /></div>
<div>info.a的值:<input type="text" v-model="info.a" /></div>
</div>
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
name: 'zs',
age: 20,
info: {
a: 'a1',
c: 'c1'
}
}
})
console.log(vm)
</script>
</body>
</html>
class Vue {
constructor(options) {
this.$data = options.data
// 调用数据劫持的方法
Observe(this.$data)
// 属性代理
Object.keys(this.$data).forEach((key) => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return this.$data[key]
},
set(newValue) {
this.$data[key] = newValue
},
})
})
// 调用模板编译的函数
Compile(options.el, this)
}
}
// 定义一个数据劫持的方法
function Observe(obj) {
// 这是递归的终止条件
if (!obj || typeof obj !== 'object') return
const dep = new Dep()
// 通过 Object.keys(obj) 获取到当前 obj 上的每个属性
Object.keys(obj).forEach((key) => {
// 当前被循环的 key 所对应的属性值
let value = obj[key]
// 把 value 这个子节点,进行递归
Observe(value)
// 需要为当前的 key 所对应的属性,添加 getter 和 setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 只要执行了下面这一行,那么刚才 new 的 Watcher 实例,
// 就被放到了 dep.subs 这个数组中了
Dep.target && dep.addSub(Dep.target)
return value
},
set(newVal) {
value = newVal
Observe(value)
// 通知每一个订阅者更新自己的文本
dep.notify()
},
})
})
}
// 对 HTML 结构进行模板编译的方法
function Compile(el, vm) {
// 获取 el 对应的 DOM 元素
vm.$el = document.querySelector(el)
// 创建文档碎片,提高 DOM 操作的性能
const fragment = document.createDocumentFragment()
while ((childNode = vm.$el.firstChild)) {
fragment.appendChild(childNode)
}
// 进行模板编译
replace(fragment)
vm.$el.appendChild(fragment)
// 负责对 DOM 模板进行编译的方法
function replace(node) {
// 定义匹配插值表达式的正则
const regMustache = /\{\{\s*(\S+)\s*\}\}/
// 证明当前的 node 节点是一个文本子节点,需要进行正则的替换
if (node.nodeType === 3) {
// 注意:文本子节点,也是一个 DOM 对象,如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
const text = node.textContent
// 进行字符串的正则匹配与提取
const execResult = regMustache.exec(text)
console.log(execResult)
if (execResult) {
const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
node.textContent = text.replace(regMustache, value)
// 在这个时候,创建 Watcher 类的实例
new Watcher(vm, execResult[1], (newValue) => {
node.textContent = text.replace(regMustache, newValue)
})
}
// 终止递归的条件
return
}
// 判断当前的 node 节点是否为 input 输入框
if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
// 得到当前元素的所有属性节点
const attrs = Array.from(node.attributes)
const findResult = attrs.find((x) => x.name === 'v-model')
if (findResult) {
// 获取到当前 v-model 属性的值 v-model="name" v-model="info.a"
const expStr = findResult.value
const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
node.value = value
// 创建 Watcher 的实例
new Watcher(vm, expStr, (newValue) => {
node.value = newValue
})
// 监听文本框的 input 输入事件,拿到文本框最新的值,把最新的值,更新到 vm 上即可
node.addEventListener('input', (e) => {
const keyArr = expStr.split('.')
const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)
const leafKey = keyArr[keyArr.length - 1]
obj[leafKey] = e.target.value
})
}
}
// 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
node.childNodes.forEach((child) => replace(child))
}
}
// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
constructor() {
// 今后,所有的 watcher 都要存到这个数组中
this.subs = []
}
// 向 subs 数组中,添加 watcher 的方法
addSub(watcher) {
this.subs.push(watcher)
}
// 负责通知每个 watcher 的方法
notify() {
this.subs.forEach((watcher) => watcher.update())
}
}
// 订阅者的类
class Watcher {
// cb 回调函数中,记录着当前 Watcher 如何更新自己的文本内容
// 但是,只知道如何更新自己还不行,还必须拿到最新的数据,
// 因此,还需要在 new Watcher 期间,把 vm 也传递进来(因为 vm 中保存着最新的数据)
// 除此之外,还需要知道,在 vm 身上众多的数据中,哪个数据,才是当前自己所需要的数据,
// 因此,必须在 new Watcher 期间,指定 watcher 对应的数据的名字
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
// ↓↓↓↓↓↓ 下面三行代码,负责把创建的 Watcher 实例存到 Dep 实例的 subs 数组中 ↓↓↓↓↓↓
Dep.target = this
key.split('.').reduce((newObj, k) => newObj[k], vm)
Dep.target = null
}
// watcher 的实例,需要有 update 函数,从而让发布者能够通知我们进行更新!
update() {
const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
this.cb(value)
}
}
二、vue生命周期
1、什么是生命周期
Vue 实例从创建到销毁的过程就是生命周期。从开始创建,初始化数据,编译模板,挂载 Dom 到渲染,更新到渲染,销毁等一系列过程,称之为生命周期
Vue生命周期共有8个阶段,分别为:beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroye
2、生命周期八个阶段及作用
(1)beforeCreate
组件实例刚被创建,组件属性计算之前。
(2)created
组件实例刚被创建,属性已绑定,但 Dom 还未生成。
(3)beforeMount
模板编译/挂载之前。在挂载开始之前被调用,相关的 render 函数首次被调用,实例已完成以下的配置: 编译模板,把 data 里面的数据和模板生成 html,此时注意还没有挂载到 Html 页面上。
(4)mounted
模板编译/挂载之后。在 el 被新创建的 vm.el 替换,并挂载到实例上去之后被调用,实例已完成以下的配置:用上面编译好的的 html 内容替换 l 属性指向的 Dom 对象。完成模板中的 html 渲染到 html 页面中。此过程进行 ajax 交互。
(5)beforeUpdate
组件更新之前。在数据更新之前调用,发生在虚拟 Dom 重新渲染打补丁之前,可以在钩子函数中进一步的更改状态,不会出大附加的重渲染过程。
(6)updated
组件更新之后。在由于数据更改导致的虚拟 Dom 重新渲染和打补丁之后调用。调用是,组件 Dom 已经更新,所以可以执行依赖于 Dom 的操作,然而在大多数的情况下,应该避免在此期间更改状态,因为这可能会导致更新无线循环,该钩子函数在服务器端渲染期间不被调用。
(7)beforeDestroy
组件销毁前调用。在示例销毁之前调用,实例仍然完全可用。
(8)destroyed
组件销毁后调用。 在实例销毁之后调用。调用后,所有的时间监听会被移除,所有的子实例也会被销毁,该钩子函数在服务器端渲染器件不被调用
三、Vuex有几个属性及作用?
1、说明
Vuex是Vue.js的全局状态管理库。包含五个核心属性:state、getters、mutations、actions和modules。
2、五个核心属性
(1)state
state:vuex的基础数据,存储变量
(2)getters
getters:用于获取State中的状态,相当于state的计算属性。
(3)mutations
mutations:用于修改state中的数据,是唯一可以修改state的地方。mutations接收state作为第一个参数,接收payload作为第二个参数。mutation必须是同步函数,因为它们不能处理异步行为,异步行为应该放在Action中处理
(4)actions
actions:用于异步操作和提交mutations,在actions中可以进行任何异步操作,最后再提交(commit)到mutations中同步修改state。actions接收context作为第一个参数,其中包含了state、getters和commit等属性。
(5)modules
模块化vuex,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理和维护。
//使用方法示例
const store = new Vuex.Store({
modules: {
cart: {
state: {
items: []
},
mutations: {
pushProductToCart (state, payload) {
state.items.push({
id: payload.id,
quantity: 1
})
}
},
actions: {
addProductToCart ({ state, commit }, product) {
const cartItem = state.items.find(item => item.id === product.id)
if (!cartItem) {
commit('pushProductToCart', product)
}
}
},
getters: {
cartItems: state => {
return state.items
}
}
}
}
})
四、vue的导航守卫有哪些?
1、什么是导航守卫
vue-router提供的导航守卫主要用于导航的过程中重定向或取消路由、或者添加权限验证、数据获取等业务逻辑。
导航守卫分为三类:全局守卫、路由独享守卫和组件内守卫。
每一个导航守卫都有三个参数:to、from和next。
- to:表示即将要进入的目标
- from:当前导航正要离开的路由
- next:函数,以下是next的常用方法
- next(): 进行管道中的下一个钩子
- next(false): 中断当前导航
- next( ./xx ): 中断当前导航,并跳转至设置好的路径
2、导航守卫
(1)全局前置守卫
当从一个路由跳转到另一个路由的时候触发此守卫,它是跳转前触发的。
router.beforeEach((to, from, next) =>{
if(to.name !== 'login) {
next({name:'login})
}else {
next()
}
})
(2)路由独享守卫
beforeEnter 守卫 只在进入路由时触发,不会在 params、query 或 hash 改变时触发
你可以直接在路由配置上定义 beforeEnter 守卫,你也可以将一个函数数组传递给 beforeEnter
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from, next) => {
// reject the navigation
return false
},
},
{
path: '/users/:id',
component: UserDetails,
beforeEnter: [removeQueryParams, removeHash],
},
]
(3)组件内的守卫
你可以在路由组件内直接定义路由导航守卫
可用的配置 API:
- beforeRouteEnter
- beforeRouteUpdate
- beforeRouteLeave
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this` !
// 因为当守卫执行时,组件实例还没被创建!
//不过,你可以通过传一个回调给 next 来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数
next(vm => {
// 通过 `vm` 访问组件实例
})
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
},
}
五、Vue的监听属性和计算属性有什么区别?
- 计算属性computed在使用时,一定要注意,函数里面的变量都会被监听,只要里面的某一个值变动,便会将整个函数执行一遍,视图也会更新。 而 watch 只是监听某一个值,若是监听的值里面也有很多变量,也会全部监听
- 计算后的属性可不在 data 中定义,如果定义会报错,因为对应的computed作为计算属性定义并返回对应的结果给这个变量,变量不可被重复定义和赋值。 而 watch 监听 data 中定义的变量变化
1、computed
特性:
- 是计算值,每一个计算属性都包含一个getter和一个setter
- 应用:就是简化tempalte里面{{}}计算和处理props或$emit的传值
- 具有缓存性,页面重新渲染值不变化,计算属性会立即返回之前的计算结果,而不必再次执行函数
2、watch
特性:
1.是观察的动作,
2.应用:监听props,$emit或本组件的值执行异步操作
3.无缓存性,页面重新渲染时值不变化也会执行
用法:
- watch监控自身属性变化
- watch监控路由对象
new Vue({
el: '#app',
router: router, //开启路由对象
watch: {
'$route': function(newroute, oldroute) {
console.log(newroute, oldroute);
//可以在这个函数中获取到当前的路由规则字符串是什么
//那么就可以针对一些特定的页面做一些特定的处理
}
}
})
watch监听对象的单个属性
watch如果想要监听对象的单个属性的变化,必须用computed作为中间件转化,因为computed可以取到对应的属性值。
data(){
return{
'first':{
second:0
}
}
},
computed:{
secondChange(){
return this.first.second }
},
watch:{
secondChange(){
console.log('second属性值变化了')
}
},
3、简单实现computed和watch
function defineReactive(data, key, val, fn) {
let subs = [] // 新增
Object.defineProperty(data, key, {
configurable: true,// 当前属性,允许被循环
enumerable: true,//当前属性,允许被配置 delete
get: function() {
// 新增
if (data.$target) {
subs.push(data.$target)
}
return val },
set: function(newVal) {
if (newVal === val) return
fn && fn(newVal)
// 新增
if (subs.length) {
// 用 setTimeout 因为此时 this.data 还没更新
setTimeout(() => {
subs.forEach(sub => sub())
}, 0)
}
val = newVal },
})
}
//computed 实现
function computed(ctx, obj) {
let keys = Object.keys(obj)
let dataKeys = Object.keys(ctx.data)
dataKeys.forEach(dataKey => {
defineReactive(ctx.data, dataKey, ctx.data[dataKey])
})
let firstComputedObj = keys.reduce((prev, next) => {
ctx.data.$target = function() {
ctx.setData({ [next]: obj[next].call(ctx) })
}
prev[next] = obj[next].call(ctx)
ctx.data.$target = null
return prev }, {})
ctx.setData(firstComputedObj)
}
//watch实现
function watch(ctx, obj) {
Object.keys(obj).forEach(key => {
defineReactive(ctx.data, key, ctx.data[key], function(value) {
obj[key].call(ctx, value)
})
})
}