Vue2 原理学习笔记

185 阅读13分钟

一、前言

  • 1.重点学习和我们使用vue相关联的原理。比如vdom、模板渲染等。

二、Vue基本使用

1.指令、插槽

考察方向

  • 1.插值、表达式
  • 2.指令、动态属性
  • 3.v-hmlt 指令:存在xss风险,会覆盖子组件

xss攻击 :> Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。

vue 使用的是双花括号的插值表达式


// 插值表达式
<div>{{name}}</div>

指令

常用的指令有 v-bind v-on 和 v-html等

条件语句

在 v-show v-if指令和表达式中,延伸出下面两个比较常见的面试问题

1. v-show 和 v-if的区别?
    v-show 使用的是css中的display来控制显示隐藏
    v-if是通过条件判断是否删除或者渲染对应的模块,这里会有dom的操作,相对消耗性能。

2. v-if 和v-show的使用场景?
    当需要控制的模块需要经常切换显示隐藏,就使用v-show或者keepalive组件
    当不那么频繁使用,或者需要用到组件的生命周期的时候,就使要用v-if

v-for 循环列表渲染

  1. 如何遍历对象?也可以使用v-for
  2. 在遍历的时候,key是diff算法提高效率的方法。不能随便给,比如给index,或者随机数。
  3. v-if和v-for不能一起使用,影响性能。

v-for 的运算层级比v=if高; v-for比v-if优先级高,所以使用的话,每次v-for都会执行v-if,造成不必要的计算,影响性能,尤其是当之需要渲染很小一部分的时候。

总结:v-if比v-for优先级高,一起使用在性能上会造成极大的浪费,并且官网也并不推荐我们这样做,所以我们可以选择使用computed过滤掉列表中不需要显示的项目。

computed 和 watch

  1. 使用了computed 计算的值,是有缓存的,也就是data中的数据不变,就不会重新计算。
  2. watch 如何进行深度监听

watch 有个特点,组件一开始挂载是不会执行。而且不会对对象的时候,不会进行深度监听。 这两个问题,vue都提供了解决方式如下:

watch: {
  obj: {
    handler(newVal, oldVal) {
      console.log('obj.a changed');
    },
    immediate: true,  // 为true时,挂载就会执行
    deep: true // 开启深度监听,监听对象的时候,属性变化也会被监听到
  }
}

注意:vue监听对象的时候,是拿不到oldvalue的

事件

事件注意的几点

  1. vue中的event参数,是原生的参数
  2. vue中的事件是绑定到当前元素的
<template>
    <div>
        <p>{{num}}</p>
        <button @click="increment1">+1</button>
        <button @click="increment2(2, $event)">+2</button>
    </div>
</template>

<script>
export default {
    data() {
        return {
            num: 0
        }
    },
    methods: {
        increment1(event) {
            // eslint-disable-next-line
            console.log('event', event, event.__proto__.constructor) // 是原生的 event 对象
            // eslint-disable-next-line
            console.log(event.target)
            // eslint-disable-next-line
            console.log(event.currentTarget) // 注意,事件是被注册到当前元素的,和 React 不一样
            this.num++

            // 1. event 是原生的
            // 2. 事件被挂载到当前元素
            // 和 DOM 事件一样
        },
        increment2(val, event) {
            // eslint-disable-next-line
            console.log(event.target)
            this.num = this.num + val
        },
        loadHandler() {
            // do some thing
        }
    },
    mounted() {
        window.addEventListener('load', this.loadHandler)
    },
    beforeDestroy() {
        //【注意】用 vue 绑定的事件,组建销毁时会自动被解绑
        // 自己绑定的事件,需要自己销毁!!!
        window.removeEventListener('load', this.loadHandler)
    }
}
</script>

事件修饰符及按键修饰符


<!--阻止单击事件继续传播-->
<a @click.stop="doThis"></a>

<!--阻止默认行为,提交事件不重载页面-->
<form @click.prevent="doThis"></from>

<!--串联修饰符-->
<a @click.prevent.stop="doThis"></a>

<!--也可以只有修饰符-->
<a @click.prevent.stop></a>

<!--添加事件监听时,使用事件捕获模式-->
<a @click.capture="doThis"></a>

<!--只有当event.target是当前元素时,才触发处理函数-->
<a @click.self="doThis"></a>

按键修饰符

<!--即使alt和ctrl同时按下也会除服-->
<button @click.ctrl="doThis"></button>
<!--只有ctrl按下才触发-->
<button @click.ctrl.exact="doThis"></button>
<!--没有任何系统修饰符被按下的时候触发-->
<button @click.ctrl.exact="doThis"></button>

表单

v-model
常见的表单项:textarea,checkbox,radio,select等
修饰符:lazy,number,trim

组件的使用

组件通讯

1. 父子组件通讯

props父组件向子组件 emit,子组件派发事件,父组件监听(emit,子组件派发事件,父组件监听(no)

2. 组建通信,自定义事件

1.vue本身就实现自定义事件系统,有on,on,emit,及$off方法。所以如果不使用Vuex,可以使用另一个vue实例作为自定义事件,用于兄弟组件之间的传值。也就是平时所说的eventbus。 vue的observe函数也能实现自定义事件。

3.组件生命周期

生命周期分为三个阶段

  • 挂载阶段
    graph TD
    实例化(new Vue)-->step1(初始化事件和生命周期及渲染)
    step1--触发beforeCreate函数-->step2(初始化inject provide state data methods等属性)
    step2--触发created函数 '此时data和methods等属性和方法初始化完成  可以访问调用' -->step3(是否有el对象)
    step3--没有-->step3-1(就需要调用vm.$mount函数进行挂载)
    step3--有--> step4(判断是否有template选项)
    step3-1-->step4
    step4--有-->step5-1(有就将模板转化为render函数)
    step4--没有-->step5-2(则将el父级作为模板)
     step5-1-->step6(在挂载前 首次调用render函数 生成虚拟dom)
     step5-2-->step6
     step6--触发beforeMount函数--> step7(创建vue实例下的虚拟$el  并替换成真正的dom)
     step7--触发mounted函数--> step8(挂载完成 dom已渲染到页面 可以进行dom操作)
     
  • 更新阶段
graph TD
setp1(挂载)--监听数据变化-->step2(触发beforeUpdate方法)
step2-->step3(虚拟dom调用patch方法  即用diff算法进行比对 得到最新的dom tree 然后重新渲染)
step3--触发update方法-->setp1
  • 销毁阶段
graph TD
step1(挂载完成)--触发vm.$beforeDestroy-->step2(开始销毁前 触发vm.$destroy 此时还能访问data和methods等)
step2--> step3(清除子组件 - watcher - 事件监听器等)
step3-->step4(销毁组件 此时无法访问data和methods等)
step4-->step5(触发destroy钩子函数)

4. 父子组件生命周期调用顺序

vue组件是从外到内实例化的,但渲染是从内到外的 假设有index和list两个组件,index组件作为父组件,list作为子组件,生命周期调用顺序如下:

graph TD
step1(index created)-->step2(list组件created)
step2--> step3(list组件mounted)
step3-->step4(index组件mounted)

父子组件更新过程

graph TD
step1(index beforeUpdate)-->step2(list组件 beforeUpdate)
step2--> step3(list组件updated)
step3-->step4(index组件updated)

Vue高级特性

  1. 自定义v-model
  2. 动态、异步组件 动态组件

动态组件使用的是component标签,需要使用:is="component-name"的写法

使用场景 需要根据数据,动态渲染的场景。即组件类型不定。

比如新闻详情页,在渲染内容过程中,有text组件、image组件、video组件、等等。这时候就可以用到动态组件。

异步组件

异步组件 即使用import()函数引入的组件

当按需加载,或异步加载大组件使用

  1. $nextTick

vue 是异步渲染的

data改变之后,dom不会立刻渲染

nextTick会在DOM渲染之后被触发,可以在这里nextTick会在DOM渲染之后被触发,可以在这里nextTick的回调中获取新的DOM节点。

  1. keep-alive

缓存组件,当组件需要频繁切换,但不需要重复渲染的时候使用

它是Vue常见的性能优化

  1. slot
默认插槽
具名插槽
作用域插槽
  1. mixin

mixin可以将多个组件公用的方法和逻辑抽离出来。

但mixin并不是完美的解决方案,存在以下问题:

1. 变量来源不明确,不利于阅读
2. 多mixin会造成命名冲突
3. mixin和组件可能出现多对多的关系,复杂度较高

5.vuex

vuex的api

  1. dispatch, 派发action
  2. commit 提交一个mutation
  3. 辅助函数,mapState、mapGetters、mapActions、mapMutations

6.路由(vue-router)

vue-router有两种模式,hash模式和H5 history模式

hash模式

hash —— 即地址栏 URL 中的 # 符号。 比如这个 URL:www.abc.com/#/hello,has… 的值为 #/hello。它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面

hash模式是通过hashchange改变事件来实现路由页面的切换

history模式

hash模式带#号,history模式的url更好看,相对安全一些。需要后端配合。 HTML5引入了history.pushState()和history.replaceState()方法,他们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate配合使用。

<!--
stateObject:当浏览器跳转到新的状态时,将触发popState事件,该事件将携带这个stateObject参数的副本
title:所添加记录的标题
url:所添加记录的url(可选的)
-->
window.history.pushState(stateObject,title,url)
window.history,replaceState(stateObject,title,url)
<!--1. 路由配置:动态路由-->
const router = new VueRouter({
    routes: [
        {
            // 动态路径参数 以冒号开头。对应user/10的路由格式
            path: user/:id,
            component: User
        }
    ]
})
<!--路由懒加载,即:用import函数引入的组件-->

三)vue原理

一)Vue原理考察的范围

原理主要考察以下六个方面:

  • 1.组件化
  • 2.响应式原理
  • 3.Vdom 和diff算法
  • 4.模板编译
  • 5.渲染过程
  • 6.前端路由

二)组件化

1. 组件化基础

A、很很久以前的组件化用的是后端渲染,当代的组件化用的是数据驱动视图。

2. 数据驱动视图

析前端开发中的 MVC/MVP/MVVM 模式

  • 传统的组件,只是渲染,更新还依赖于操作DOM

  • 现代的组件,使用的是数据驱动视图。例如,vue使用的是MVVM (model,view,view model)

  • React的数据驱动视图---React setState

3. vue的mvvm模型

M:model 数据模块层 提供数据

V:view 视图层 渲染数据

VM: ViewModel 视图模型层 调用数据,渲染视图

由数据来驱动视图(不需要多考虑DOM操作,把重心放在VM上)

mvvm分为三块,分别是model(数据模型),view(视图)和viewModel(视图模型)

数据模型负责数据,视图模型负责渲染展示界面,viewmodel负责连接数据模型和视图。

mvvm优点

  1. 低耦合。视图(View)可以独立于Model变化和修改,
    一个ViewModel可以绑定到不同的"View"上,
    当View变化的时候Model可以不变,
    当Model变化的时候View也可以不变。
  1. 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
  1. 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel)
  1. 可测试。界面素来是比较难于测试的,测试可以针对ViewModel来写。
sequenceDiagram
participant view
participant viewModel
participant model
view->>model: dom listener(视图通过事件监听修改model的数据)
model->>view: directive(object对象通过指令,修改view的视图展示)

4. MVC

MVC是一种架构模式,它将应用抽象为3个部分:模型(数据)、视图、控制器(分发器)。

  • M: model 数据模型层 提供数据

  • V: view 视图层 显示页面

  • C: controller 控制层 调用数据渲染视图

即模型-视图-控制器。M和V指的意思和MVVM中的M和V意思⼀样。C即Controller指的是页⾯业务逻辑。使⽤用MVC的目的就是将M和V的代码分离。‘MVC是单向通信。也就是View跟Model,必须通过Controller来承上启下。 controller,让它来定义用户界面对用户输入的响应方式,它连接模型和视图,用于控制应用程序的流程,处理用户的行为和数据上的改变。

一个事件发生的过程(通信单向流动)

  • 1、用户在视图 V 上与应用程序交互

  • 2、控制器 C 触发相应的事件,要求模型 M 改变状态(读写数据)

  • 3、模型 M 将数据发送到视图 V ,更新数据,展现给用户

MVC模式的业务逻辑主要集中在Controller,而前端的View其实已经具备了独立处理用户事件的能力,当每个事件都流经Controller时,这层会变得十分臃肿。而且MVC中View和Controller一般是一一对应的,捆绑起来表示一个组件,视图与控制器间的过于紧密的连接让Controller的复用性成了问题,如果想多个View共用一个Controller该怎么办呢?这里有一个解决方案MVP

5. MVP

MVP(Model-View-Presenter)是MVC模式的改良,由IBM的子公司Taligent提出。和MVC的相同之处在于:Controller/Presenter负责业务逻辑,Model管理数据,View负责显示。

虽然在MVC里,View是可以直接访问Model的,但MVP中的View并不能直接使用Model,而是通过为Presenter提供接口,让Presenter去更新Model,再通过观察者模式更新View。 与MVC相比,MVP模式通过解耦View和Model,完全分离视图和模型使职责划分更加清晰;由于View不依赖Model,可以将View抽离出来做成组件,它只需要提供一系列接口提供给上层操作。

Presenter作为View和Model之间的“中间人”,除了基本的业务逻辑外,还有大量代码需要对从View到Model和从Model到View的数据进行“手动同步”,这样Presenter显得很重,维护起来会比较困难。而且由于没有数据绑定,如果Presenter对视图渲染的需求增多,它不得不过多关注特定的视图,一旦视图需求发生改变,Presenter也需要改动。


三、vue响应式

1. 响应式概述

  • vue响应式的实现

核心API: Object.defineProperty

如何实现,如下代码所示。

object.defineProperty有一些缺点: 1.监听劫持数据,需要不断的遍历,如果对象层级较深,需耗费较多的资源和性能。

2.不能监听数组,需要特殊处理。

3.无法监听新增的属性和删除的属性,需要使用vue.set和vue,delete

4.Vue本身无法监听数组的变化,用的是重写数组方法来做到监听变化

vue3.0中,使用了proxy代替defineProperty来进行数据拦截,不需要遍历即可对对象的属性进行监听,并且可以监听数据的变化。但是有兼容性问题,不能使用polyfill。


/* 重写数组方法 */
// 复制原型
const arrayOldProperty = Array.prototype

// 根据复制的原型新建一个对象
const arrProto = Object.create(arrayOldProperty)
// 重写这个新对象的数组方法
const methodNameList = ['push','slice','splice','shift','unshift', 'pop']
console.log('methodNameList', methodNameList.forEach)
methodNameList.forEach(methodName =>{
	// 重写方法,不能给原型,否则会污染原生对象
	arrProto[methodName] = function () {
		// 调用原生的方法
		arrayOldProperty[methodName].call(this,...arguments)
		// 然后调用更新视图的方法
		updateView(...arguments)
	}
});

// 更新视图
function updateView(value) {
	console.log('update view', value)
}
/* 监听数据 */
function observe(obj) {
	// 不是对象,不作监听
	if(typeof obj !== 'object') {
		return obj
	}
	// 如果是数组,则修改一下它指向的原型
	if(Array.isArray(obj)) {
		obj.__proto__ = arrProto
	}
	// 如果是对象则进行监听, 遍历
	Object.keys(obj).forEach(key =>{
		defineReactive(obj, key, obj[key])
	})

}

/* 拦截数据 */
function defineReactive(target,key, value) {
	// 如果value是对象的话,需要对象要做深度监听
	observe(value)
	// 数据拦截监听
	Object.defineProperty(target, key, {
		get() {
			// 获取直接返回
			return value
		},
		set(newValue) {
			// 设置新值之后,更新视图
			observe(newValue)
			value = newValue
			// 设置的新值有可能是对象,所以这里也要监听
			// 更新视图,这就是所谓的数据拦截操作
			updateView(value)
		}
	})
}

const data = {
	arr: [1],
	a: 123,
	b: 456,
	c: {
		d: 852
	}
}

observe(data)
data.a = 789
data.c.d = 258
data.arr.push(88)


四、虚拟dom

一)为什么使用虚拟dom

以往的dom操作中

  • dom的操作非常耗费性能
  • 以前jquery可以自行控制DOM的操作实际,但非常繁琐
  • Vue和React是数据驱动视图,简化了DOM操作,那么是如何有效控制dom的?
  • vdom用js模拟DOM结构,利用js执行速度比操作dom更快的特性,从而达到性能优化的目的。

五、diff算法

(一)、diff算法概述

  1. diff即对比,是一个广泛的概念,如linux的diff命令,git的diff等等
  2. 两个js对象也可以做diff,如github.com/cujojs/jiff
  3. 两棵树做diff,如这里vdom diff
graph TD
step1(A)-->step2(B)
step1-->step3(C)
step2-->step4(D)
step2-->step5(E)
step3-->step6(F)
step3-->step7(G)


S1(A)-->S2(B)
S1-->S3(C)
S2-->S4(D)
S2-->S5(H)
S3-->S6(F)
S3-->S7(I)

假如对上面的这两棵树今夕diff,算法复杂度为O(n^3)
第一:遍历tree1,第二遍历tree2
第三:排序
1000个节点,要计算一亿次,算法不可用
因此需要将算法优化到o(n)

将diff算法优化到o(n)

  1. 只比较同一层级,不跨级比较
  2. tag不相同,则直接删掉重建,不再深度比较
  3. tag和key相同,则认为是相同节点,不再深度比较。

使用key和不使用key

在新节点和老节点进行对比时,有key的话不用一个个对比;没有key的话,需要遍历进行对比。

diff算法总结

  1. patchVnode方法对节点进行对比
  2. addVnodes添加虚拟dom,removeVnodes,删除虚拟dom
  3. updateChildren,更新字节,这里能体现key的重要性。

vdom 和 diff算法总结

  1. vue源码中的细节过程不重要,比如updateChildren的过程也不重要,不需要深究。
  2. vdom的核心是h(渲染函数),vnode、patch、diff、key等
  3. vdom存在价值更重要:数据驱动视图、控制DOM操作

六、模板编译

1. 前置知识:js的with语法

const obj = {a:1,b:2}
console.log(obj.a)
console.log(obj.b)
console.log(obj.c) // undefined


// 使用with语法,能改变{}内部变量的查找方式,
// 会将{}内的自由变量,当做 obj 的属性来查找
with (obj) {
    console.log(obj.a)
    console.log(obj.b)
    console.log(obj.c) // 会报错
}

总结

  • 能改变{}内部自由变量的查找规则,当做obj的属性来查找
  • 如果找到obj的属性会报错
  • with要慎用,因为它打破了作用域的规则,易读性变差。

2. 模板编译

  • 模板编译不是html代码,它还居右插值、指令、js表达式,能实现判断、循环。
  • html是标签语言,只有js才能实现判断、循环(图灵完备的语言)
  • 因此,模板一定是转换为某种js代码,及模板编译。
// 使用vue-template-compiler编译后会获得如下代码
const template = <div>{{message}}<div>
with(this){
    return _c('div',[
        _v((_s(message)))
    ])
}

模板编译流程

  • 模板编译为render函数,执行render函数,返回vnode
  • 基于vnode在执行patch和diff 进行节点的比对。最后渲染成真实dom
  • 使用webpack vue-loader,会在开发环境下编译模板。