1、vue解决了什么问题
解决了用 DOM API 操作 UI 过于反人类的问题。
React 通过 UI = f(data) 解决。
Vue 通过 Reactive 响应式数据解决。
2、MVVM的理解
MVVM分为Model、View、ViewModel三者。
- Model:代表数据模型,数据和业务逻辑都在Model层中定义;
- View:代表UI视图,负责数据的展示;
- ViewModel:就是与界面(view)对应的Model。因为,数据库结构往往是不能直接跟界面控件一一对应上的,所以,需要再定义一个数据对象专门对应view上的控件。而ViewModel的职责就是把model对象封装成可以显示和接受输入的界面数据对象。
Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。
简单的说,ViewModel就是View与Model的连接器,View与Model通过ViewModel实现双向绑定。
3、如何实现一个自定义组件,不同组件之间如何通信的?
组件需要注册后才可以使用,有全局注册和局部注册两种方式在实例创建前通过
<script>
Vue.component('自定义标签名称',{
//选项
});
var app = new Vue({
el:'#app'
})
</script>
来注册全局组件,不必把每个组件都注册到全局,在实例中,使用components选项可以局部注册组件,注册后的组件只有在该实例作用域下有效,组件中也可以使用components选项来注册组件,使组件可以嵌套。
<script>
var Child = {
template:'<div>局部注册组件内容</div>'
}
var app = new Vue({
el:'#app',
components:{
'my-component':Child
}
})
</script>
组件关系可分为父子组件通信、兄弟组件通信、跨级组件通信
父子组件通信:
父组件向子组件通信,通过props传递数据
子组件向父组件传递数据时,用到自定义事件,子组件用 e m i t ( ) 触 发 事 件 , 父 组 件 用 emit()触发事件,父组件用 emit()触发事件,父组件用on()监听子组件的事件,父组件也可以直接在子组件的自定义标签上使用v-on来监听
<div id="app">
{{message}}
<my-component></my-component>
</div>
<script>
Vue.component('my-component',{
template:'<button @click="event"></button>',
methods:{
event:function() {
this.$dispatch('on-message','来自内部组件的数据');
}
}
})
var app = new Vue({
el:'#app',
data:{
message:''
},
events:{
'on-message':function(msg) {
this.message = msg;
}
}
})
</script>
非父子组件通信:
在Vue.js 1.x中,提供
d
i
s
p
a
t
c
h
(
)
和
dispatch()和
dispatch()和broadcast()两个方法。
d
i
s
p
a
t
c
h
(
)
用
于
向
上
级
派
发
事
件
,
只
要
是
它
的
父
级
(
一
级
或
多
级
以
上
)
,
都
可
以
在
V
u
e
实
例
的
e
v
e
n
t
s
选
项
内
接
收
.
dispatch()用于向上级派发事件,只要是它的父级(一级或多级以上),都可以在Vue实例的events选项内接收.
dispatch()用于向上级派发事件,只要是它的父级(一级或多级以上),都可以在Vue实例的events选项内接收.broadcast()由上级向下级广播事件。
但在Vue.js 2.x中都废弃了(不能解决兄弟组件通信问题)
在Vue.js 2.x中,推荐使用一个空的vue实例作为中央事件总线(bus),也就是一个中介
<div id="app">
{{message}}
<component-a></component-a>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var bus = new Vue();
Vue.component('component-a',{
template:'<button @click="event">传递事件</button>',
methods:{
event:function() {
bus.$emit('on-message','来自组件component-a的内容')
}
}
})
var app = new Vue({
el:'#app',
data:{
message:''
},
mounted: function() {
var _this = this;
bus.$on('on-message',function(msg) {
_this.message = msg;
})
}
})
</script>
这种方法实现了任何组件间的通信,如果深入使用,可以扩展bus实例,给它添加data、computed、methods等选项,这些都是可以公用的
除了bus外,还有两种方法可以实现组件间通信,父链和子组件索引
父链
在子组件中,使用this.$parent可以直接访问该组件的父实例或组件,父组件也可以通过this.$children访问它所有的子组件
<div id="app">
{{message}}
<component-a></component-a>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var bus = new Vue();
Vue.component('component-a',{
template:'<button @click="event">传递事件</button>',
methods:{
event:function() {
//访问到父链后,可以做任何操作,比如直接修改数据
this.$parent.message = "来自组件component-a的内容"
}
}
})
var app = new Vue({
el:'#app',
data:{
message:''
}
})
</script>
父子组件最好还是通过props和$emit来通信
子组件索引
4、nextTick
5、Vue的生命周期
vue的生命周期里边有八个生命周期钩子函数分别是:
- beforeCreat() 创建前
- created()创建
- beforeMount()挂载前
- mounted()挂载
- beforeupdate()更改前
- updated()更改
- beforeDestroy()销毁前
- destroyed()销毁
先来一张官方的生命周期图镇贴
生命周期函数理解
6、虚拟dom的原理
7、双向绑定的原理?数据劫持?
1.1 数据劫持
1.1.1 如何监控一个数据
vue可以直接通过v-model这个指令来实现双向绑定,这是react和小程序都没有,小程序是单向绑定,只能将data中的对象和基本数据类型展示在视图上,却没有办法通过视图来控制data中的数据,需要通过this.setData({})给出一个对象,重新设置数据,达到视图更新。
要达到如图1-1的效果,就要对数据进行监控,只有监控了数据的变化,在数据变化之后,通知视图去自主更新,这就是双向绑定的思路。这个思路很明显涉及到“监控” “更新”两个关键词,就可以联想到观察者模式。
观察一个数据,一旦数据变化,就通知视图执行更新操作。
思路一下子就明了,数据变化还好说,就是拿出一个变量存储旧值,一旦获取到新值,新值与旧值不同时,数据就发生了变化。可问题在于,不可能随时随地对数据进行监控,每分每秒都在取得数据的值去与旧值做对比。
只有当这个数据在被使用时,我们才监控他,拿旧值与新值做对比。
这个过程叫做让数据变为可观察,是通过Object.defineProperty() 来实现。
1.1.2 如何使用Object的静态方法定义属性
Object.defineProperty(obj, prop, descriptor)
- obj 要在其上定义属性的对象。
- prop 要定义或修改的属性的名称。
- descriptor 将被定义或修改的属性描述符。
被这样定义的属性,所有的数据描述符默认为false,也就是不可删除,不可写,不可枚举
属性描述符
MDN文档上有提到
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。
let obj = {
name:1
}
Object.defineProperty(obj,'school',{
configurable:true,//表示configurable可以被删除
writable:true,//为true之后,便可以修改
// enumerable:true,//修改之后才可以被枚举,在遍历时被访问到
value:'zfpx'
})
// delete obj.school;
obj.school = "修改值"
console.log(obj)
for(var key in obj){
console.log(key)
}
只有开启数据描述符为true之后,属性才可被删除,被写入,被枚举打印
getter-setter
这就是数据可监控的关键,使用Object.defineProperty(obj, prop, descriptor)定义的属性,一旦属性被使用,就会被读取,就会调用get函数,一旦属性被写入,就会调用set函数,即可以知道数据一旦发生写入,变化,就可以在set函数中通知视图更新。
由于,
存储描述符get set参数和数据描述符的writeable value存在冲突,二选其一
Object.defineProperty(obj, "school", {
configurable: true, //表示configurable可以被删除
// writable: true,
enumerable: true, //修改之后才可以被枚举,在遍历时被访问到
// value: "zfpx",
get() {
console.log("调用了get方法");
return value;
},
set(newVal) {
console.log("调用了set方法");
value = newVal;
}
});
如图 1-2
1.1.3 数据劫持
知道了get和set的妙用,就可以对数据进行劫持了。
劫持的概念
说白了,就是拿到某数据,持有这个数据,可以操作增删改,也可以不操作,重点在持有他
监听
一旦数据被传入Vue实例就需要对data整个对象实行监听,
这里需要对data中的数据类型进行判断
如果是data中的属性是基本数据类型,只需要监控就好了
如果data中的属性是对象,则需要遍历对象下的所有属性,进行监控
可又有一个疑问,data的属性是对象A,A的属性还包含对象B,B有对象C,所以不能是遍历,而是递归,递归整个对象属性树
<body>
<div id="app">
<p>姓名是{{ name.firstName }}</p>
<div>年龄是{{ age }}</div>
{{ name }}
</div>
<script type="vue.js"></script>
<script type="text/javascript">
let vm = new Vue({
el: "#app",
data: {
name: {
firstName: "姓氏章",
lastName: "名字"
},
age: 12 //通过Obj.defineProperty实现()或者Obj.defineProperties()实现
}
});
</script>
</body>
数据绑定(传入{ 对象的data挂载在vue实例上)
/**
*Vue入口
*@{options} 限定为一个对象,接受这个{}对象
* */
function Vue(options = {}) {
this.$options = options; // 将所有属性挂载在vue实例$options上
var data = (this._data = this.$options.data); //将{}对象的data挂载vue实例上
observe(data);
}
/**
*观察对象变化,如果最开始传入的data是基本数据类型,已经被劫持了,不需要递归再去对属性进行监控
*@{data} 被观察的对象或属性
*/
function observe(data) {
if (typeof data !== "object") return null;
return new Observe(data);
}
class Observe {
constructor(data) {
this.start(data);
}
start(data) {
for (let key in data) {
let val = data[key];
// 如果data中包含属性是对象,则需要递归对象的中属性,进行数据劫持
// 如果data中的属性就是普通数据类型,递归退出 -- 递归出口
Object.defineProperty(data, key, {
enumrable: true,
get() {
console.log("调用get方法");
return val;
},
// 会在数据改变的时候直接设置
set(newVal) {
console.log("调用set方法");
//数据并没有改变
if (newVal === val) {
return;
}
val = newVal;
}
});
}
}
}
上述代码实现了数据劫持和监控数据的功能
接下来是
数据代理(this代理传入的{ }对象去调用data)
编译模板(读取文本节点中的字符串,抽成属性名,通过字面量的形式访问到属性值,去掉双大括号,显示到节点上)
到编译模板这一步,是实现了单向绑定,也就是data中的数据被显示在网页上,如同又通过视图,譬如input输入框,改变data的值。
8、组件通信
1、父->子
方法一、props/$emit
父组件 A 通过 props 的方式向子组件 B 传递,B to A 通过在 B 组件中 $emit, A 组件中 v-on 的方式实现。
接下来我们通过一个例子,说明父组件如何向子组件传递值:在子组件 Users.vue 中如何获取父组件 App.vue 中的数据
users:["Henry","Bucky","Emily"]
//App.vue父组件
<template>
<div id="app">
<users v-bind:users="users"></users>//前者自定义名称便于子组件调用,后者要传递数据名
</div>
</template>
<script>
import Users from "./components/Users"
export default {
name: 'App',
data(){
return{
users:["Henry","Bucky","Emily"]
}
},
components:{
"users":Users
}
}
//users子组件
<template>
<div class="hello">
<ul>
<li v-for="user in users">{{user}}</li>//遍历传递过来的值,然后呈现到页面
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props:{
users:{ //这个就是父组件中子标签自定义名字
type:Array,
required:true
}
}
}
</script>
总结:父组件通过 props 向下传递数据给子组件。注:组件中的数据共有三种形式:data、props、computed
2、子->父
子组件向父组件传值(通过事件形式)
接下来我们通过一个例子,说明子组件如何向父组件传递值:当我们点击“Vue.js Demo”后,子组件向父组件传递值,文字由原来的“传递的是一个值”变成“子向父组件传值”,实现子组件向父组件值的传递。
// 子组件
<template>
<header>
<h1 @click="changeTitle">{{title}}</h1>//绑定一个点击事件
</header>
</template>
<script>
export default {
name: 'app-header',
data() {
return {
title:"Vue.js Demo"
}
},
methods:{
changeTitle() {
this.$emit("titleChanged","子向父组件传值");//自定义事件 传递值“子向父组件传值”
}
}
}
</script>
// 父组件
<template>
<div id="app">
<app-header v-on:titleChanged="updateTitle" ></app-header>//与子组件titleChanged自定义事件保持一致
// updateTitle($event)接受传递过来的文字
<h2>{{title}}</h2>
</div>
</template>
<script>
import Header from "./components/Header"
export default {
name: 'App',
data(){
return{
title:"传递的是一个值"
}
},
methods:{
updateTitle(e){ //声明这个函数
this.title = e;
}
},
components:{
"app-header":Header,
}
}
</script>
总结:子组件通过 events 给父组件发送消息,实际上就是子组件把自己的数据发送到父组件。
3、非父子组件
9、Proxy 相比于 defineProperty 的优势
vue3.0尝鲜 – 摒弃Object.defineProperty,基于 Proxy 的观察者机制探索
10、watch computed区别
computed:计算属性
1、计算属性是由data中的已知值,得到的一个新值。
2、这个新值只会根据已知值的变化而变化,其他不相关的数据的变化不会影响该新值。
3、计算属性不在data中,计算属性新值的相关已知值在data中。
4、别人变化影响我自己。
watch:监听数据的变化
1、监听data中数据的变化
2、监听的数据就是data中的已知值
3、我的变化影响别人
little-demo:演示watch和computed的区别
<div id="app">
<input type="text" v-model="name" />
<span v-show="isShow">请输入3-6个字符</span>
<br />
<input type="text" v-model="todoName" />
</div>
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data: {
name: "zs",
todoName: "ls"
},
computed: {
isShow() {
//当this.name的长度小于3或者大于6时显示提示内容(我根据name的变化而变化)
if (this.name.length >= 3 && this.name.length <= 6) {
return false;
} else {
return true;
}
}
},
watch: {
//监听data中的name,如果发生了变化,就把变化的值给data中的todoName(我影响了别人)
name(newVal) {
this.todoName = newVal;
}
}
});
</script>
11、virtual dom 原理实现
–
12、vue-router(hash, HTML5 新增的 pushState
从Vue-router到html5的pushState
最近在用vue的时候突然想到一个问题
首先,我们知道vue实现的单页应用中一般不会去刷新页面,因为刷新之后页面中的vuex数据就不见了。
其次,我们也知道一般情况下,url变更的时候,比如指定location.href、history.push、replace等,页面就会刷新。
那么问题来了,vue页面的页面跳转时怎么实现的?没刷新页面么?没刷新页面,又要改变url,加载新内容怎么做的?
去翻了一下vue-router的源码,找到这样一段
export class HTML5History extends History {
...
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
...
}
再看看方法内部
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
const history = window.history
try {
if (replace) {
history.replaceState({ key: _key }, '', url)
} else {
_key = genKey()
history.pushState({ key: _key }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
答案就是html5在history中新增加的方法:pushState和replaceState。这两个又是干啥的呢?(两个十分类似,以下以pushState为例说明,区别和push与replace一致)
HTML5的pushState()
首先看看这个是干什么的
- pushState方法就是向history中push一条记录,更改页面url,但是不刷新页面,不刷新页面,不刷新页面。不刷新页面,这点很关键,这和下面的操作很相似
window.location.href = window.location.href + '#a=b
知道干嘛的了,再看看API怎么用的
history.pushState(state, title, url);
popstate中的
- title这个参数目前没什么用处,可能是给以后预留的参数,暂时用null就好了
- url很明显,就是替换后的url了。url可以接受绝对地址和相对地址,设置绝对地址的时候,要保证域名和当前域名一致,否则汇报如下错误
Uncaught DOMException: Failed to execute 'pushState' on 'History': A history state object with URL 'https://www.baidu.com/' cannot be created in a document with origin 'https://mocard-aliyun1.chooseway.com:8443' and URL 'https://mocard-aliyun1.chooseway.com:8443/views/h5/indexasdasd'.
at History.pushState (https://aixuedaiimg.oss-cn-hangzhou.aliyuncs.com/static/m/js/alog/v1.0.0/alog.min.js:1:23259)
at <anonymous>:1:9
HTML5的popstate()
- popstate与pushState相对应,主要在页面url变更的时候触发,一般绑定在window对象下
window.addEventListener('popstate', e => {
console.log('popstate', )
})
前面pushState中传入的state对象,可以在这边接收到,并根据需要去做一些处理。
说到这,vue-router是怎么实现页面“刷新”但不刷新的就知道了吧。
vue-router就是利用pushState这个属性,在页面前进的时候动态改变history的内容,添加一条记录,接着location跟着改变。同时根据router前往的路由获取对应的js资源文件并挂载到目标dom上实现页面内容的更新,但是页面本身并没有刷新。
单页应用,如何实现其路由功能—路由原理
单页面路由即在前端单页面实现的一种路由,由于React,Vue等框架的火热,我们可以很容易构建一个用户体验良好的单页面应用,但是如果我们要在浏览器改变路由的时候,在不请求服务器的情况下渲染不同的内容,就要类似于后端的路由系统,在前端也实现一套完整的路由系统