> 文章列表 > 7-vue-1

7-vue-1

7-vue-1

谈谈你对MVVM的理解

为什么要有这些模式,目的:职责划分、分层(将Model层、View层进行分类)借鉴后端思想,对于前端而已,就是如何将数据同步到页面上

MVC模式 代表:Backbone + underscore + jquery

7-vue-1

  • 传统的 MVC 指的是,用户操作会请求服务端路由,路由会调用对应的控制器来处理,控制器会获取数据。将结果返回给前端,页面重新渲染
  • MVVM:传统的前端会将数据手动渲染到页面上, MVVM 模式不需要用户收到操作 dom 元素,将数据绑定到 viewModel 层上,会自动将数据渲染到页面中,视图变化会通知 viewModel层 更新数据。ViewModel 就是我们 MVVM 模式中的桥梁

MVVM模式 映射关系的简化,隐藏了controller

7-vue-1

MVVMModel-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModelModel层代表数据模型,View代表UI组件ViewModelViewModel层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。

  • Model: 代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。我们可以把Model称为数据层,因为它仅仅关注数据本身,不关心任何行为
  • View: 用户操作界面。当ViewModelModel进行更新的时候,会通过数据绑定更新到View
  • ViewModel: 业务逻辑层,View需要什么数据,ViewModel要提供这个数据;View有某些操作,ViewModel就要响应这些操作,所以可以说它是Model for View.

总结MVVM模式简化了界面与业务的依赖,解决了数据频繁更新。MVVM 在使用当中,利用双向绑定技术,使得 Model 变化时,ViewModel 会自动更新,而 ViewModel 变化时,View 也会自动变化。

我们以下通过一个 Vue 实例来说明 MVVM 的具体实现

<!-- View 层 --><div id="app"><p>{{message}}</p><button v-on:click="showMessage()">Click me</button>
</div>
// ViewModel 层var app = new Vue({el: '#app',data: {  // 用于描述视图状态   message: 'Hello Vue!', },methods: {  // 用于描述视图行为  showMessage(){let vm = this;alert(vm.message);}},created(){let vm = this;// Ajax 获取 Model 层的数据ajax({url: '/your/server/data/api',success(res){vm.message = res;}});}
})
// Model 层{"url": "/your/server/data/api","res": {"success": true,"name": "test","domain": "www.baidu.com"}
}

----------@----------

谈谈你对SPA单页面的理解

SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTMLJavaScriptCSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  • 基于上面一点,SPA 相对对服务器压力小;
  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理

缺点:

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScriptCSS 统一加载,部分页面按需加载;
  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势

单页应用与多页应用的区别

单页面应用(SPA) 多页面应用(MPA)
组成 一个主页面和多个页面片段 多个主页面
刷新方式 局部刷新 整页刷新
url模式 哈希模式 历史模式
SEO搜索引擎优化 难实现,可使用SSR方式改善 容易实现
数据传递 容易 通过urlcookielocalStorage等传递
页面切换 速度快,用户体验良好 切换加载资源,速度慢,用户体验差
维护成本 相对容易 相对复杂

实现一个SPA

  • 监听地址栏中hash变化驱动界面变化
  • pushsate记录浏览器的历史,驱动界面发送变化

7-vue-1

  1. hash 模式 :核心通过监听url中的hash来进行路由跳转
// 定义 Router  
class Router {  constructor () {  this.routes = {}; // 存放路由path及callback  this.currentUrl = '';  // 监听路由change调用相对应的路由回调  window.addEventListener('load', this.refresh, false);  window.addEventListener('hashchange', this.refresh, false);  }  route(path, callback){  this.routes[path] = callback;  }  push(path) {  this.routes[path] && this.routes[path]()  }  
}  // 使用 router  
window.miniRouter = new Router();  
miniRouter.route('/', () => console.log('page1'))  
miniRouter.route('/page2', () => console.log('page2'))  miniRouter.push('/') // page1  
miniRouter.push('/page2') // page2  
  1. history模式history 模式核心借用 HTML5 history apiapi 提供了丰富的 router 相关属性先了解一个几个相关的api
  • history.pushState 浏览器历史纪录添加记录
  • history.replaceState修改浏览器历史纪录中当前纪录
  • history.popStatehistory 发生变化时触发
// 定义 Router  
class Router {  constructor () {  this.routes = {};  this.listerPopState()  }  init(path) {  history.replaceState({path: path}, null, path);  this.routes[path] && this.routes[path]();  }  route(path, callback){  this.routes[path] = callback;  }  push(path) {  history.pushState({path: path}, null, path);  this.routes[path] && this.routes[path]();  }  listerPopState () {  window.addEventListener('popstate' , e => {  const path = e.state && e.state.path;  this.routers[path] && this.routers[path]()  })  }  
}  // 使用 Router  window.miniRouter = new Router();  
miniRouter.route('/', ()=> console.log('page1'))  
miniRouter.route('/page2', ()=> console.log('page2'))  // 跳转  
miniRouter.push('/page2')  // page2  

题外话:如何给SPA做SEO

  1. SSR服务端渲染

将组件或页面通过服务器生成html,再返回给浏览器,如nuxt.js

  1. 静态化

目前主流的静态化主要有两种:

  • 一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中

  • 另外一种是通过WEB服务器的 URL Rewrite的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现URL静态化的效果

  1. 使用Phantomjs针对爬虫处理

原理是通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。下面是大致流程图

7-vue-1

----------@----------

Vue2.x 响应式数据原理

整体思路是数据劫持+观察者模式

对象内部通过 defineReactive 方法,使用 Object.defineProperty 来劫持各个属性的 settergetter(只会劫持已经存在的属性),数组则是通过重写数组7个方法来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)

Object.defineProperty基本使用

function observer(value) { // proxy reflectif (typeof value === 'object' && typeof value !== null)for (let key in value) {defineReactive(value, key, value[key]);}
}function defineReactive(obj, key, value) {observer(value);Object.defineProperty(obj, key, {get() { // 收集对应的key 在哪个方法(组件)中被使用return value;},set(newValue) {if (newValue !== value) {observer(newValue);value = newValue; // 让key对应的方法(组件重新渲染)重新执行}}})
}
let obj1 = { school: { name: 'poetry', age: 20 } };
observer(obj1);
console.log(obj1)

源码分析

7-vue-1

class Observer {// 观测值constructor(value) {this.walk(value);}walk(data) {// 对象上的所有属性依次进行观测let keys = Object.keys(data);for (let i = 0; i < keys.length; i++) {let key = keys[i];let value = data[key];defineReactive(data, key, value);}}
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {observe(value); // 递归关键// --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止//   思考?如果Vue数据嵌套层级过深 >>性能会受影响Object.defineProperty(data, key, {get() {console.log("获取值");//需要做依赖收集过程 这里代码没写出来return value;},set(newValue) {if (newValue === value) return;console.log("设置值");//需要做派发更新过程 这里代码没写出来value = newValue;},});
}
export function observe(value) {// 如果传过来的是对象或者数组 进行属性劫持if (Object.prototype.toString.call(value) === "[object Object]" ||Array.isArray(value)) {return new Observer(value);}
}

说一说你对vue响应式理解回答范例

  • 所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制
  • MVVM框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理
  • vue为例说明,通过数据响应式加上虚拟DOMpatch算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的DOM操作,从而大大提升开发效率,降低开发难度
  • vue2中的数据响应式会根据数据类型来做不同处理,如果是 对象则采用Object.defineProperty()的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是数组则通过覆盖数组对象原型的7个变更方法 ,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete这样特殊的api才能生效;对于es6中新产生的MapSet这些数据结构不支持等问题
  • 为了解决这些问题,vue3重新编写了这一部分的实现:利用ES6Proxy代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊api,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了

----------@----------

Vue3.x 响应式数据原理

Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。

proxy基本用法

// proxy默认只会代理第一层对象,只有取值再次是对象的时候再次代理,不是一上来就代理,提高性能。不像vue2.x递归遍历每个对象属性
let handler = {set(target, key, value) {return Reflect.set(target, key, value);},get(target, key) {if (typeof target[key] == 'object' && target[key] !== null) {return new Proxy(target[key], handler); // 懒代理,只有取值再次是对象的时候再次代理,提高性能}return Reflect.get(target, key);}
}
let obj = { school: { name: 'poetry', age: 20 } };
let proxy = new Proxy(obj, handler);// 返回对象的代理
proxy.school

----------@----------

说说你对 proxy 的理解,Proxy 相比于 defineProperty 的优势

Object.defineProperty() 的问题主要有三个:

  • 不能监听数组的变化 :无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应
  • 必须遍历对象的每个属性 :只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果属性值是对象,还需要深度遍历。Proxy 可以劫持整个对象,并返回一个新的对象
  • 必须深层遍历嵌套的对象

Proxy的优势如下:

  • 针对对象: 针对整个对象,而不是对象的某个属性 ,所以也就不需要对 keys 进行遍历
  • 支持数组:Proxy 不需要对数组的方法进行重载,省去了众多 hack,减少代码量等于减少了维护成本,而且标准的就是最好的
  • Proxy的第二个参数可以有 13 种拦截方:不限于applyownKeysdeletePropertyhas等等是Object.defineProperty不具备的
  • Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改
  • Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利

proxy详细使用点击查看(opens new window)

Object.defineProperty的优势如下:

兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平

defineProperty的属性值有哪些

Object.defineProperty(obj, prop, descriptor)// obj 要定义属性的对象
// prop 要定义或修改的属性的名称
// descriptor 要定义或修改的属性描述符Object.defineProperty(obj,"name",{value:"poetry", // 初始值writable:true, // 该属性是否可写入enumerable:true, // 该属性是否可被遍历得到(for...in, Object.keys等)configurable:true, // 定该属性是否可被删除,且除writable外的其他描述符是否可被修改get: function() {},set: function(newVal) {}
})

相关代码如下

import { mutableHandlers } from "./baseHandlers"; // 代理相关逻辑
import { isObject } from "./util"; // 工具方法export function reactive(target) {// 根据不同参数创建不同响应式对象return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {if (!isObject(target)) {return target;}const observed = new Proxy(target, baseHandler);return observed;
}const get = createGetter();
const set = createSetter();function createGetter() {return function get(target, key, receiver) {// 对获取的值进行放射const res = Reflect.get(target, key, receiver);console.log("属性获取", key);if (isObject(res)) {// 如果获取的值是对象类型,则返回当前对象的代理对象return reactive(res);}return res;};
}
function createSetter() {return function set(target, key, value, receiver) {const oldValue = target[key];const hadKey = hasOwn(target, key);const result = Reflect.set(target, key, value, receiver);if (!hadKey) {console.log("属性新增", key, value);} else if (hasChanged(value, oldValue)) {console.log("属性值被修改", key, value);}return result;};
}
export const mutableHandlers = {get, // 当获取属性时调用此方法set, // 当修改属性时调用此方法
};

Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?

判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。

监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?

我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger

----------@----------

Vue中如何检测数组变化

前言

Vue 不能检测到以下数组的变动:

  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength

Vue 提供了以下操作方法

// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

分析

数组考虑性能原因没有用 defineProperty 对数组的每一项进行拦截,而是选择对 7 种数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写(AOP 切片思想)

所以在 Vue 中修改数组的索引和长度是无法监控到的。需要通过以上 7 种变异方法修改数组才会触发数组对应的 watcher 进行更新

  • 用函数劫持的方式,重写了数组方法,具体呢就是更改了数组的原型,更改成自己的,用户调数组的一些方法的时候,走的就是自己的方法,然后通知视图去更新
  • 数组里每一项可能是对象,那么我就是会对数组的每一项进行观测,(且只有数组里的对象才能进行观测,观测过的也不会进行观测)

原理

Vuedata 中的数组,进行了原型链重写。指向了自己定义的数组原型方法,这样当调用数组api 时,可以通知依赖更新,如果数组中包含着引用类型。会对数组中的引用类型再次进行监控。

7-vue-1

手写简版分析

let oldArray = Object.create(Array.prototype);
['shift', 'unshift', 'push', 'pop', 'reverse','sort'].forEach(method => {oldArray[method] = function() { // 这里可以触发页面更新逻辑console.log('method', method)Array.prototype[method].call(this,...arguments);}
});
let arr = [1,2,3];
arr.__proto__ = oldArray;
arr.unshift(4);

源码分析

// 拿到数组原型拷贝一份
const arrayProto = Array.prototype 
// 然后将arrayMethods继承自数组原型
// 这里是面向切片编程思想(AOP)--不破坏封装的前提下,动态的扩展功能
export const arrayMethods = Object.create(arrayProto) 
const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]methodsToPatch.forEach(function (method) { // 重写原型方法 const original = arrayProto[method] // 调用原数组的方法 def(arrayMethods, method, function mutator (...args) { // 这里保留原型方法的执行结果const result = original.apply(this, args) // 这句话是关键// this代表的就是数据本身 比如数据是{a:[1,2,3]} 那么我们使用a.push(4)  this就是a  ob就是a.__ob__ 这个属性就是上段代码增加的 代表的是该数据已经被响应式观察过了指向Observer实例const ob = this.__ob__ // 这里的标志就是代表数组有新增操作let insertedswitch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break }// 如果有新增的元素 inserted是一个数组 调用Observer实例的observeArray对数组每一项进行观测if (inserted) ob.observeArray(inserted) ob.dep.notify() // 当调用数组方法后,手动通知视图更新 return result }) 
})this.observeArray(value) // 进行深度监控

vue3:改用 proxy ,可直接监听对象数组的变化

----------@----------

Vue中如何进行依赖收集?

  • 每个属性都有自己的dep属性,存放他所依赖的watcher,当属性变化之后会通知自己对应的watcher去更新
  • 默认会在初始化时调用render函数,此时会触发属性依赖收集 dep.depend
  • 当属性发生修改时会触发watcher更新dep.notify()

7-vue-1

依赖收集简版

let obj = { name: 'poetry', age: 20 };class Dep {constructor() {this.subs = [] // subs [watcher]}depend() {this.subs.push(Dep.target)}notify() {this.subs.forEach(watcher => watcher.update())}
}
Dep.target = null;
observer(obj); // 响应式属性劫持// 依赖收集  所有属性都会增加一个dep属性,
// 当渲染的时候取值了 ,这个dep属性 就会将渲染的watcher收集起来
// 数据更新 会让watcher重新执行// 观察者模式// 渲染组件时 会创建watcher
class Watcher {constructor(render) {this.get();}get() {Dep.target = this;render(); // 执行renderDep.target = null;}update() {this.get();}
}
const render = () => {console.log(obj.name); // obj.name => get方法
}// 组件是watcher、计算属性是watcher
new Watcher(render);function observer(value) { // proxy reflectif (typeof value === 'object' && typeof value !== null)for (let key in value) {defineReactive(value, key, value[key]);}
}
function defineReactive(obj, key, value) {// 创建一个deplet dep = new Dep();// 递归观察子属性observer(value);Object.defineProperty(obj, key, {get() { // 收集对应的key 在哪个方法(组件)中被使用if (Dep.target) { // watcherdep.depend(); // 这里会建立 dep 和watcher的关系}return value;},set(newValue) {if (newValue !== value) {observer(newValue);value = newValue; // 让key对应的方法(组件重新渲染)重新执行dep.notify()}}})
}// 模拟数据获取,触发getter
obj.name = 'poetries'// 一个属性一个dep,一个属性可以对应多个watcher(一个属性可以在任何组件中使用、在多个组件中使用)
// 一个dep 对应多个watcher 
// 一个watcher 对应多个dep (一个视图对应多个属性)
// dep 和 watcher是多对多的关系

----------@----------

Vue实例挂载的过程中发生了什么

简单

TIP

分析

挂载过程完成了最重要的两件事:

  • 初始化
  • 建立更新机制

把这两件事说清楚即可!

回答范例

  1. 挂载过程指的是app.mount()过程,这个过程中整体上做了两件事:初始化建立更新机制
  2. 初始化会创建组件实例、初始化组件状态,创建各种响应式数据
  3. 建立更新机制这一步会立即执行一次组件更新函数,这会首次执行组件渲染函数并执行patch将前面获得vnode转换为dom;同时首次执行渲染函数会创建它内部响应式数据之间和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数

来看一下源码,在src/core/instance/index.js

function Vue (options) {if (process.env.NODE_ENV !== 'production' &&!(this instanceof Vue)) {warn('Vue is a constructor and should be called with the `new` keyword')}this._init(options)
}

可以看到 Vue 只能通过 new 关键字初始化,然后会调用 this._init 方法, 该方法在 src/core/instance/init.js 中定义

Vue.prototype._init = function (options?: Object) {const vm: Component = this// a uidvm._uid = uid++let startTag, endTag/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {startTag = `vue-perf-start:${vm._uid}`endTag = `vue-perf-end:${vm._uid}`mark(startTag)}// a flag to avoid this being observedvm._isVue = true// merge optionsif (options && options._isComponent) {// optimize internal component instantiation// since dynamic options merging is pretty slow, and none of the// internal component options needs special treatment.initInternalComponent(vm, options)} else {vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)}/* istanbul ignore else */if (process.env.NODE_ENV !== 'production') {initProxy(vm)} else {vm._renderProxy = vm}// expose real selfvm._self = vminitLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm, 'beforeCreate')initInjections(vm) // resolve injections before data/propsinitState(vm)initProvide(vm) // resolve provide after data/propscallHook(vm, 'created')/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {vm._name = formatComponentName(vm, false)mark(endTag)measure(`vue ${vm._name} init`, startTag, endTag)}if (vm.$options.el) {vm.$mount(vm.$options.el)}
}

Vue 初始化主要就干了几件事情,合并配置初始化生命周期初始化事件中心初始化渲染初始化 datapropscomputedwatcher

----------@----------

vue2.x详细

1. 分析

首先找到vue的构造函数

源码位置:src\\core\\instance\\index.js

function Vue (options) {if (process.env.NODE_ENV !== 'production' &&!(this instanceof Vue)) {warn('Vue is a constructor and should be called with the `new` keyword')}this._init(options)
}

options是用户传递过来的配置项,如data、methods等常用的方法

vue构建函数调用_init方法,但我们发现本文件中并没有此方法,但仔细可以看到文件下方定定义了很多初始化方法

initMixin(Vue);     // 定义 _init
stateMixin(Vue);    // 定义 $set $get $delete $watch 等
eventsMixin(Vue);   // 定义事件  $on  $once $off $emit
lifecycleMixin(Vue);// 定义 _update  $forceUpdate  $destroy
renderMixin(Vue);   // 定义 _render 返回虚拟dom

首先可以看initMixin方法,发现该方法在Vue原型上定义了_init方法

源码位置:src\\core\\instance\\init.js

Vue.prototype._init = function (options?: Object) {const vm: Component = this// a uidvm._uid = uid++let startTag, endTag/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {startTag = `vue-perf-start:${vm._uid}`endTag = `vue-perf-end:${vm._uid}`mark(startTag)}// a flag to avoid this being observedvm._isVue = true// merge options// 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法if (options && options._isComponent) {// optimize internal component instantiation// since dynamic options merging is pretty slow, and none of the// internal component options needs special treatment.initInternalComponent(vm, options)} else { // 合并vue属性vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)}/* istanbul ignore else */if (process.env.NODE_ENV !== 'production') {// 初始化proxy拦截器initProxy(vm)} else {vm._renderProxy = vm}// expose real selfvm._self = vm// 初始化组件生命周期标志位initLifecycle(vm)// 初始化组件事件侦听initEvents(vm)// 初始化渲染方法initRender(vm)callHook(vm, 'beforeCreate')// 初始化依赖注入内容,在初始化data、props之前initInjections(vm) // resolve injections before data/props// 初始化props/data/method/watch/methodsinitState(vm)initProvide(vm) // resolve provide after data/propscallHook(vm, 'created')/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {vm._name = formatComponentName(vm, false)mark(endTag)measure(`vue ${vm._name} init`, startTag, endTag)}// 挂载元素if (vm.$options.el) {vm.$mount(vm.$options.el)}}

仔细阅读上面的代码,我们得到以下结论:

  • 在调用beforeCreate之前,数据初始化并未完成,像dataprops这些属性无法访问到
  • 到了created的时候,数据已经初始化完成,能够访问dataprops这些属性,但这时候并未完成dom的挂载,因此无法访问到dom元素
  • 挂载方法是调用vm.$mount方法

initState方法是完成props/data/method/watch/methods的初始化

源码位置:src\\core\\instance\\state.js

export function initState (vm: Component) {// 初始化组件的watcher列表vm._watchers = []const opts = vm.$options// 初始化propsif (opts.props) initProps(vm, opts.props)// 初始化methods方法if (opts.methods) initMethods(vm, opts.methods)if (opts.data) {// 初始化data  initData(vm)} else {observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch)}
}

我们和这里主要看初始化data的方法为initData,它与initState在同一文件上

function initData (vm: Component) {let data = vm.$options.data// 获取到组件上的datadata = vm._data = typeof data === 'function'? getData(data, vm): data || {}if (!isPlainObject(data)) {data = {}process.env.NODE_ENV !== 'production' && warn('data functions should return an object:\\n' +'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',vm)}// proxy data on instanceconst keys = Object.keys(data)const props = vm.$options.propsconst methods = vm.$options.methodslet i = keys.lengthwhile (i--) {const key = keys[i]if (process.env.NODE_ENV !== 'production') {// 属性名不能与方法名重复if (methods && hasOwn(methods, key)) {warn(`Method "${key}" has already been defined as a data property.`,vm)}}// 属性名不能与state名称重复if (props && hasOwn(props, key)) {process.env.NODE_ENV !== 'production' && warn(`The data property "${key}" is already declared as a prop. ` +`Use prop default value instead.`,vm)} else if (!isReserved(key)) { // 验证key值的合法性// 将_data中的数据挂载到组件vm上,这样就可以通过this.xxx访问到组件上的数据proxy(vm, `_data`, key)}}// observe data// 响应式监听data是数据的变化observe(data, true /* asRootData */)
}

仔细阅读上面的代码,我们可以得到以下结论:

  • 初始化顺序:propsmethodsdata
  • data定义的时候可选择函数形式或者对象形式(组件只能为函数形式)

关于数据响应式在这就不展开详细说明

上文提到挂载方法是调用vm.$mount方法

源码位置:

Vue.prototype.$mount = function (el?: string | Element,hydrating?: boolean
): Component {// 获取或查询元素el = el && query(el)/* istanbul ignore if */// vue 不允许直接挂载到body或页面文档上if (el === document.body || el === document.documentElement) {process.env.NODE_ENV !== 'production' && warn(`Do not mount Vue to <html> or <body> - mount to normal elements instead.`)return this}const options = this.$options// resolve template/el and convert to render functionif (!options.render) {let template = options.template// 存在template模板,解析vue模板文件if (template) {if (typeof template === 'string') {if (template.charAt(0) === '#') {template = idToTemplate(template)/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && !template) {warn(`Template element not found or is empty: ${options.template}`,this)}}} else if (template.nodeType) {template = template.innerHTML} else {if (process.env.NODE_ENV !== 'production') {warn('invalid template option:' + template, this)}return this}} else if (el) {// 通过选择器获取元素内容template = getOuterHTML(el)}if (template) {/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {mark('compile')}/***  1.将temmplate解析ast tree*  2.将ast tree转换成render语法字符串*  3.生成render方法*/const { render, staticRenderFns } = compileToFunctions(template, {outputSourceRange: process.env.NODE_ENV !== 'production',shouldDecodeNewlines,shouldDecodeNewlinesForHref,delimiters: options.delimiters,comments: options.comments}, this)options.render = renderoptions.staticRenderFns = staticRenderFns/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {mark('compile end')measure(`vue ${this._name} compile`, 'compile', 'compile end')}}}return mount.call(this, el, hydrating)
}

阅读上面代码,我们能得到以下结论:

  • 不要将根元素放到body或者html
  • 可以在对象中定义template/render或者直接使用templateel表示元素选择器
  • 最终都会解析成render函数,调用compileToFunctions,会将template解析成render函数

template的解析步骤大致分为以下几步:

  • html文档片段解析成ast描述符
  • ast描述符解析成字符串
  • 生成render函数

生成render函数,挂载到vm上后,会再次调用mount方法

源码位置:src\\platforms\\web\\runtime\\index.js

// public mount method
Vue.prototype.$mount = function (el?: string | Element,hydrating?: boolean
): Component {el = el && inBrowser ? query(el) : undefined// 渲染组件return mountComponent(this, el, hydrating)
}

调用mountComponent渲染组件

export function mountComponent (vm: Component,el: ?Element,hydrating?: boolean
): Component {vm.$el = el// 如果没有获取解析的render函数,则会抛出警告// render是解析模板文件生成的if (!vm.$options.render) {vm.$options.render = createEmptyVNodeif (process.env.NODE_ENV !== 'production') {/* istanbul ignore if */if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||vm.$options.el || el) {warn('You are using the runtime-only build of Vue where the template ' +'compiler is not available. Either pre-compile the templates into ' +'render functions, or use the compiler-included build.',vm)} else {// 没有获取到vue的模板文件warn('Failed to mount component: template or render function not defined.',vm)}}}// 执行beforeMount钩子callHook(vm, 'beforeMount')let updateComponent/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {updateComponent = () => {const name = vm._nameconst id = vm._uidconst startTag = `vue-perf-start:${id}`const endTag = `vue-perf-end:${id}`mark(startTag)const vnode = vm._render()mark(endTag)measure(`vue ${name} render`, startTag, endTag)mark(startTag)vm._update(vnode, hydrating)mark(endTag)measure(`vue ${name} patch`, startTag, endTag)}} else {// 定义更新函数updateComponent = () => {// 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_rendervm._update(vm._render(), hydrating)}}// we set this to vm._watcher inside the watcher's constructor// since the watcher's initial patch may call $forceUpdate (e.g. inside child// component's mounted hook), which relies on vm._watcher being already defined// 监听当前组件状态,当有数据变化时,更新组件new Watcher(vm, updateComponent, noop, {before () {if (vm._isMounted && !vm._isDestroyed) {// 数据更新引发的组件更新callHook(vm, 'beforeUpdate')}}}, true /* isRenderWatcher */)hydrating = false// manually mounted instance, call mounted on self// mounted is called for render-created child components in its inserted hookif (vm.$vnode == null) {vm._isMounted = truecallHook(vm, 'mounted')}return vm
}

阅读上面代码,我们得到以下结论:

  • 会触发boforeCreate钩子
  • 定义updateComponent渲染页面视图的方法
  • 监听组件数据,一旦发生变化,触发beforeUpdate生命钩子

updateComponent方法主要执行在vue初始化时声明的renderupdate方法

render的作用主要是生成vnode

源码位置:src\\core\\instance\\render.js

// 定义vue 原型上的render方法
Vue.prototype._render = function (): VNode {const vm: Component = this// render函数来自于组件的optionconst { render, _parentVnode } = vm.$optionsif (_parentVnode) {vm.$scopedSlots = normalizeScopedSlots(_parentVnode.data.scopedSlots,vm.$slots,vm.$scopedSlots)}// set parent vnode. this allows render functions to have access// to the data on the placeholder node.vm.$vnode = _parentVnode// render selflet vnodetry {// There's no need to maintain a stack because all render fns are called// separately from one another. Nested component's render fns are called// when parent component is patched.currentRenderingInstance = vm// 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNodevnode = render.call(vm._renderProxy, vm.$createElement)} catch (e) {handleError(e, vm, `render`)// return error render result,// or previous vnode to prevent render error causing blank component/* istanbul ignore else */if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {try {vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)} catch (e) {handleError(e, vm, `renderError`)vnode = vm._vnode}} else {vnode = vm._vnode}} finally {currentRenderingInstance = null}// if the returned array contains only a single node, allow itif (Array.isArray(vnode) && vnode.length === 1) {vnode = vnode[0]}// return empty vnode in case the render function errored outif (!(vnode instanceof VNode)) {if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {warn('Multiple root nodes returned from render function. Render function ' +'should return a single root node.',vm)}vnode = createEmptyVNode()}// set parentvnode.parent = _parentVnodereturn vnode
}

_update主要功能是调用patch,将vnode转换为真实DOM,并且更新到页面中

源码位置:src\\core\\instance\\lifecycle.js

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {const vm: Component = thisconst prevEl = vm.$elconst prevVnode = vm._vnode// 设置当前激活的作用域const restoreActiveInstance = setActiveInstance(vm)vm._vnode = vnode// Vue.prototype.__patch__ is injected in entry points// based on the rendering backend used.if (!prevVnode) {// initial render// 执行具体的挂载逻辑vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)} else {// updatesvm.$el = vm.__patch__(prevVnode, vnode)}restoreActiveInstance()// update __vue__ referenceif (prevEl) {prevEl.__vue__ = null}if (vm.$el) {vm.$el.__vue__ = vm}// if parent is an HOC, update its $el as wellif (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {vm.$parent.$el = vm.$el}// updated hook is called by the scheduler to ensure that children are// updated in a parent's updated hook.}

2. 结论

  • new Vue的时候调用会调用_init方法
    • 定义 $set$get$delete$watch 等方法
    • 定义 $on$off$emit$off等事件
    • 定义 _update$forceUpdate$destroy生命周期
  • 调用$mount进行页面的挂载
  • 挂载的时候主要是通过mountComponent方法
  • 定义updateComponent更新函数
  • 执行render生成虚拟DOM
  • _update将虚拟DOM生成真实DOM结构,并且渲染到页面中

----------@----------

理解Vue运行机制全局概览

全局概览

首先我们来看一下笔者画的内部流程图。

7-vue-1

大家第一次看到这个图一定是一头雾水的,没有关系,我们来逐个讲一下这些模块的作用以及调用关系。相信讲完之后大家对Vue.js内部运行机制会有一个大概的认识。

初始化及挂载

7-vue-1

new Vue() 之后。 Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。其中最重要的是通过 Object.defineProperty 设置 settergetter 函数,用来实现「 响应式 」以及「 依赖收集 」,后面会详细讲到,这里只要有一个印象即可。

初始化之后调用 $mount 会挂载组件,如果是运行时编译,即不存在 render function 但是存在 template 的情况,需要进行「 编译 」步骤。

编译

compile编译可以分成 parseoptimizegenerate 三个阶段,最终需要得到 render function。

7-vue-1

1. parse

parse 会用正则等方式解析 template 模板中的指令、class、style等数据,形成AST。

2. optimize

optimize 的主要作用是标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当 update 更新界面时,会有一个 patch 的过程, diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能。

3. generate

generate 是将 AST 转化成 render function字符串的过程,得到结果是 render 的字符串以及 staticRenderFns 字符串。

  • 在经历过 parseoptimizegenerate 这三个阶段以后,组件中就会存在渲染 VNode 所需的 render function 了。

响应式

接下来也就是 Vue.js 响应式核心部分。

7-vue-1

这里的 gettersetter 已经在之前介绍过了,在 init 的时候通过 Object.defineProperty 进行了绑定,它使得当被设置的对象被读取的时候会执行 getter 函数,而在当被赋值的时候会执行 setter 函数。

  • render function 被渲染的时候,因为会读取所需对象的值,所以会触发 getter 函数进行「 依赖收集 」,「 依赖收集 」的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Depsubs 中。形成如下所示的这样一个关系。

7-vue-1

在修改对象的值的时候,会触发对应的 settersetter 通知之前「 依赖收集 」得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图,当然这中间还有一个 patch 的过程以及使用队列来异步更新的策略,这个我们后面再讲。

Virtual DOM

我们知道,render function 会被转化成 VNode 节点。Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。

比如说下面这样一个例子:

{tag: 'div',                 /*说明这是一个div标签*/children: [                 /*存放该标签的子节点*/{tag: 'a',           /*说明这是一个a标签*/text: 'click me'    /*标签的内容*/}]
}

渲染后可以得到

<div><a>click me</a>
</div>

这只是一个简单的例子,实际上的节点有更多的属性来标志节点,比如 isStatic (代表是否为静态节点)、 isComment (代表是否为注释节点)等。

更新视图

7-vue-1

  • 前面我们说到,在修改一个对象值的时候,会通过 setter -> Watcher -> update 的流程来修改对应的视图,那么最终是如何更新视图的呢?
  • 当数据变化后,执行 render function 就可以得到一个新的 VNode 节点,我们如果想要得到新的视图,最简单粗暴的方法就是直接解析这个新的 VNode 节点,然后用 innerHTML 直接全部渲染到真实 DOM 中。但是其实我们只对其中的一小块内容进行了修改,这样做似乎有些「 浪费 」。
  • 那么我们为什么不能只修改那些「改变了的地方」呢?这个时候就要介绍我们的「 patch 」了。我们会将新的 VNode 与旧的 VNode 一起传入 patch 进行比较,经过 diff 算法得出它们的「 差异 」。最后我们只需要将这些「 差异 」的对应 DOM 进行修改即可。

再看全局

7-vue-1

回过头再来看看这张图,是不是大脑中已经有一个大概的脉络了呢?

----------@----------

如何理解Vue中模板编译原理

Vue 的编译过程就是将 template 转化为 render 函数的过程

  • 解析生成AST树template模板转化成AST语法树,使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理
  • 标记优化 对静态语法做静态标记 markup(静态节点如div下有p标签内容不会变化) diff来做优化 静态节点跳过diff操作
    • Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用
    • 等待后续节点更新,如果是静态的,不会在比较children
  • 代码生成 编译的最后一步是将优化后的AST树转换为可执行的代码

回答范例

思路

  • 引入vue编译器概念
  • 说明编译器的必要性
  • 阐述编译器工作流程

回答范例

  1. Vue中有个独特的编译器模块,称为compiler,它的主要作用是将用户编写的template编译为js中可执行的render函数。
  2. 之所以需要这个编译过程是为了便于前端能高效的编写视图模板。相比而言,我们还是更愿意用HTML来编写视图,直观且高效。手写render函数不仅效率底下,而且失去了编译期的优化能力。
  3. Vue中编译器会先对template进行解析,这一步称为parse,结束之后会得到一个JS对象,我们称为 抽象语法树AST ,然后是对AST进行深加工的转换过程,这一步成为transform,最后将前面得到的AST生成为JS代码,也就是render函数

可能的追问

  1. Vue中编译器何时执行?

7-vue-1

new Vue()之后。 Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期、事件、 propsmethodsdatacomputedwatch等。其中最重要的是通过 Object.defineProperty 设置 settergetter 函数,用来实现「响应式」以及「依赖收集」

  • 初始化之后调用 $mount 会挂载组件,如果是运行时编译,即不存在 render function 但是存在 template 的情况,需要进行「编译」步骤

  • compile编译可以分成 parseoptimizegenerate 三个阶段,最终需要得到 render function

  1. React有没有编译器?

react 使用babelJSX语法解析

<div id="app"></div>
<script>let vm = new Vue({el: '#app',template: `<div>// <span>hello world</span> 是静态节点<span>hello world</span>    // <p>{{name}}</p> 是动态节点<p>{{name}}</p></div>`,data() {return { name: 'test' }}});
</script>

源码分析

export function compileToFunctions(template) {// 我们需要把html字符串变成render函数// 1.把html代码转成ast语法树  ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法// 很多库都运用到了ast 比如 webpack babel eslint等等let ast = parse(template);// 2.优化静态节点:对ast树进行标记,标记静态节点if (options.optimize !== false) {optimize(ast, options);}// 3.通过ast 重新生成代码// 我们最后生成的代码需要和render函数一样// 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))// _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本let code = generate(ast);//   使用with语法改变作用域为this  之后调用render函数可以使用call改变this 方便code里面的变量取值let renderFn = new Function(`with(this){return ${code}}`);return renderFn;
}

----------@----------

Vue complier 实现

  • 模板解析这种事,本质是将数据转化为一段 html ,最开始出现在后端,经过各种处理吐给前端。随着各种 mv* 的兴起,模板解析交由前端处理。
  • 总的来说,Vue complier 是将 template 转化成一个 render 字符串。

可以简单理解成以下步骤:

  • parse 过程,将 template 利用正则转化成AST 抽象语法树。
  • optimize 过程,标记静态节点,后 diff 过程跳过静态节点,提升性能。
  • generate 过程,生成 render 字符串

----------@----------

Vue的生命周期方法有哪些

  1. Vue 实例有一个完整的生命周期,也就是从开始创建初始化数据编译模版挂载Dom -> 渲染更新 -> 渲染卸载等一系列过程,我们称这是Vue的生命周期
  2. Vue生命周期总共分为8个阶段创建前/后载入前/后更新前/后销毁前/后

beforeCreate => created => beforeMount => Mounted => beforeUpdate => updated => beforeDestroy => destroyedkeep-alive下:activated deactivated

生命周期vue2 生命周期vue3 描述
beforeCreate beforeCreate 在实例初始化之后,数据观测(data observer) 之前被调用。
created created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el
beforeMount beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用
mounted mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
beforeUpdate beforeUpdate 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
updated updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子
beforeDestroy beforeUnmount 实例销毁之前调用。在这一步,实例仍然完全可用
destroyed unmounted 实例销毁后调用。调用后, Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。

其他几个生命周期

生命周期vue2 生命周期vue3 描述
activated activated keep-alive专属,组件被激活时调用
deactivated deactivated keep-alive专属,组件被销毁时调用
errorCaptured errorCaptured 捕获一个来自子孙组件的错误时被调用
- renderTracked 调试钩子,响应式依赖被收集时调用
- renderTriggered 调试钩子,响应式依赖被触发时调用
- serverPrefetch ssr only,组件实例在服务器上被渲染前调用
  1. 要掌握每个生命周期内部可以做什么事
  • beforeCreate 初始化vue实例,进行数据观测。执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
  • created 组件初始化完毕,可以访问各种数据,获取接口数据等
  • beforeMount 此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项上
  • mounted 实例已经挂载完成,可以进行一些DOM操作
  • beforeUpdate 更新前,可用于获取更新前各种状态。此时view层还未更新,可用于获取更新前各种状态。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
  • updated 完成view层的更新,更新后,所有状态已是最新。可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。
  • destroyed 可以执行一些优化操作,清空定时器,解除绑定事件
  • vue3 beforeunmount:实例被销毁前调用,可用于一些定时器或订阅的取消
  • vue3 unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器

7-vue-1

<div id="app">{{name}}</div>
<script>const vm = new Vue({data(){return {name:'poetries'}},el: '#app',beforeCreate(){// 数据观测(data observer) 和 event/watcher 事件配置之前被调用。console.log('beforeCreate');},created(){// 属性和方法的运算, watch/event 事件回调。这里没有$elconsole.log('created')},beforeMount(){// 相关的 render 函数首次被调用。console.log('beforeMount')},mounted(){// 被新创建的 vm.$el 替换console.log('mounted')},beforeUpdate(){//  数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。console.log('beforeUpdate')},updated(){//  由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。console.log('updated')},beforeDestroy(){// 实例销毁之前调用 实例仍然完全可用console.log('beforeDestroy')},destroyed(){ // 所有东西都会解绑定,所有的事件监听器会被移除console.log('destroyed')}});setTimeout(() => {vm.name = 'poetry';setTimeout(() => {vm.$destroy()  }, 1000);}, 1000);
</script>
  1. 组合式API生命周期钩子

你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。

下表包含如何在 setup() 内部调用生命周期钩子:

选项式 API Hook inside setup
beforeCreate 不需要*
created 不需要*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered

因为 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写

export default {setup() {// mountedonMounted(() => {console.log('Component is mounted!')})}
}

setupcreated谁先执行?

  • beforeCreate:组件被创建出来,组件的methodsdata还没初始化好
  • setup:在beforeCreatecreated之间执行
  • created:组件被创建出来,组件的methodsdata已经初始化好了

由于在执行setup的时候,created还没有创建好,所以在setup函数内我们是无法使用datamethods的。所以vue为了让我们避免错误的使用,直接将setup函数内的this执行指向undefined

import { ref } from "vue"
export default {// setup函数是组合api的入口函数,注意在组合api中定义的变量或者方法,要在template响应式需要return{}出去setup(){let count = ref(1)function myFn(){count.value +=1}return {count,myFn}},}
  1. 其他问题
  • 什么是vue生命周期? Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。

  • vue生命周期的作用是什么? 它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。

  • vue生命周期总共有几个阶段? 它可以总共分为8个阶段:创建前/后、载入前/后、更新前/后、销毁前/销毁后。

  • 第一次页面加载会触发哪几个钩子? 会触发下面这几个beforeCreatecreatedbeforeMountmounted

  • 你的接口请求一般放在哪个生命周期中? 接口请求一般放在mounted中,但需要注意的是服务端渲染时不支持mounted,需要放到created

  • DOM 渲染在哪个周期中就已经完成?mounted中,

    • 注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted
    mounted: function () {this.$nextTick(function () {// Code that will run only after the// entire view has been rendered})}
    
----------@----------
### 父组件可以监听到子组件的生命周期吗比如有父组件 `Parent` 和子组件 `Child`,如果父组件监听到子组件挂载 `mounted` 就做一些逻辑处理,可以通过以下写法实现:```js
// Parent.vue
<Child @mounted="doSomething"/>// Child.vue
mounted() {
this.$emit("mounted");
}

以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:

//  Parent.vue
<Child @hook:mounted="doSomething" ></Child>doSomething() {console.log('父组件监听到 mounted 钩子函数 ...');
},//  Child.vue
mounted(){console.log('子组件触发 mounted 钩子函数 ...');
},    // 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...     

当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:createdupdated 等都可以监听

----------@----------

Vue生命周期钩子是如何实现的

  • vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法
  • 内部会对钩子函数进行处理,将钩子函数维护成数组的形式

Vue 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)

<script>// Vue.options 中会存放所有全局属性// 会用自身的 + Vue.options 中的属性进行合并// Vue.mixin({//     beforeCreate() {//         console.log('before 0')//     },// })debugger;const vm = new Vue({el: '#app',beforeCreate: [function() {console.log('before 1')},function() {console.log('before 2')}]});console.log(vm);
</script>

相关代码如下

export function callHook(vm, hook) {// 依次执行生命周期对应的方法const handlers = vm.$options[hook];if (handlers) {for (let i = 0; i < handlers.length; i++) {handlers[i].call(vm); //生命周期里面的this指向当前实例}}
}// 调用的时候
Vue.prototype._init = function (options) {const vm = this;vm.$options = mergeOptions(vm.constructor.options, options);callHook(vm, "beforeCreate"); //初始化数据之前// 初始化状态initState(vm);callHook(vm, "created"); //初始化数据之后if (vm.$options.el) {vm.$mount(vm.$options.el);}
};// 销毁实例实现
Vue.prototype.$destory = function() {// 触发钩子callHook(vm, 'beforeDestory')// 自身及子节点remove() // 删除依赖watcher.teardown() // 删除监听vm.$off() // 触发钩子callHook(vm, 'destoryed')
}

原理流程图

7-vue-1

----------@----------

Vue 的父子组件生命周期钩子函数执行顺序

  • 渲染顺序 :先父后子,完成顺序:先子后父
  • 更新顺序 :父更新导致子更新,子更新完成后父
  • 销毁顺序 :先父后子,完成顺序:先子后父

加载渲染过程

beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted子组件先挂载,然后到父组件

子组件更新过程

beforeUpdate->子 beforeUpdate->子 updated->父 updated

父组件更新过程

beforeUpdate->父 updated

销毁过程

beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

之所以会这样是因为Vue创建过程是一个递归过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;子组件首次创建时会添加mounted钩子到队列,等到patch结束再执行它们,可见子组件的mounted钩子是先进入到队列中的,因此等到patch结束执行这些钩子时也先执行。

7-vue-1

function patch (oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return }let isInitialPatch = false const insertedVnodeQueue = [] // 定义收集所有组件的insert hook方法的数组 // somthing ... createElm( vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) )// somthing... // 最终会依次调用收集的insert hook invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);return vnode.elm
}function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { // createChildren 会递归创建儿子组件 createChildren(vnode, children, insertedVnodeQueue) // something... 
} 
// 将组件的vnode插入到数组中 
function invokeCreateHooks (vnode, insertedVnodeQueue) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode) }i = vnode.data.hook // Reuse variable if (isDef(i)) { if (isDef(i.create)) i.create(emptyNode, vnode) if (isDef(i.insert)) insertedVnodeQueue.push(vnode) } 
} 
// insert方法中会依次调用mounted方法 
insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (!componentInstance._isMounted) { componentInstance._isMounted = true callHook(componentInstance, 'mounted') } 
}
function invokeInsertHook (vnode, queue, initial) { // delay insert hooks for component root nodes, invoke them after the // element is really inserted if (isTrue(initial) && isDef(vnode.parent)) { vnode.parent.data.pendingInsert = queue } else { for (let i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]); // 调用insert方法 } } 
}Vue.prototype.$destroy = function () { callHook(vm, 'beforeDestroy') // invoke destroy hooks on current rendered tree vm.__patch__(vm._vnode, null) // 先销毁儿子 // fire destroyed hook callHook(vm, 'destroyed') 
}

----------@----------

Vue.mixin的使用场景和原理

  • 在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立,可以通过 Vuemixin 功能抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用 mergeOptions 方法进行合并,采用策略模式针对不同的属性进行合并。当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”;如果混入的数据和本身组件的数据冲突,会以组件的数据为准
  • mixin有很多缺陷如:命名冲突、依赖问题、数据来源问题

基本使用

<script>// Vue.optionsVue.mixin({ // 如果他是对象 每个组件都用mixin里的对象进行合并data(){return {a: 1,b: 2}}});// Vue.extendVue.component('my',{ // 组件必须是函数 Vue.extend  => render(xxx)data(){return {x:1}}}) // 没有 new 没有实例  _init()// const vm = thisnew Vue({el:'#app',data(){ // 根可以不是函数 return {c:3}}})
</script>

相关源码

export default function initMixin(Vue){Vue.mixin = function (mixin) {//   合并对象this.options=mergeOptions(this.options,mixin)};
}
};// src/util/index.js
// 定义生命周期
export const LIFECYCLE_HOOKS = ["beforeCreate","created","beforeMount","mounted","beforeUpdate","updated","beforeDestroy","destroyed",
];// 合并策略
const strats = {};
// mixin核心方法
export function mergeOptions(parent, child) {const options = {};// 遍历父亲for (let k in parent) {mergeFiled(k);}// 父亲没有 儿子有for (let k in child) {if (!parent.hasOwnProperty(k)) {mergeFiled(k);}}//真正合并字段方法function mergeFiled(k) {// strats合并策略if (strats[k]) {options[k] = strats[k](parent[k], child[k]);} else {// 默认策略options[k] = child[k] ? child[k] : parent[k];}}return options;
}

----------@----------

Vue组件data为什么必须是个函数?

  • 根实例对象data可以是对象也可以是函数 (根实例是单例),不会产生数据污染情况
  • 组件实例对象data必须为函数 一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数,

简版理解

// 1.组件的渲染流程 调用Vue.component -> Vue.extend -> 子类 -> new 子类
// Vue.extend 根据用户定义产生一个新的类
function Vue() {}
function Sub() { // 会将data存起来this.data = this.constructor.options.data();
}
Vue.extend = function(options) {Sub.options = options; // 静态属性return Sub;
}
let Child = Vue.extend({data:()=>( { name: 'zf' })
});// 两个组件就是两个实例, 希望数据互不感染
let child1 = new Child();
let child2 = new Child();console.log(child1.data.name);
child1.data.name = 'poetry';
console.log(child2.data.name);// 根不需要 任何的合并操作   根才有vm属性 所以他可以是函数和对象  但是组件mixin他们都没有vm 所以我就可以判断 当前data是不是个函数

相关源码

// 源码位置 src/core/global-api/extend.js
export function initExtend (Vue: GlobalAPI) {Vue.extend = function (extendOptions: Object): Function {extendOptions = extendOptions || {}const Super = thisconst SuperId = Super.cidconst cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})if (cachedCtors[SuperId]) {return cachedCtors[SuperId]}const name = extendOptions.name || Super.options.nameif (process.env.NODE_ENV !== 'production' && name) {validateComponentName(name)}const Sub = function VueComponent (options) {this._init(options)}// 子类继承大Vue父类的原型Sub.prototype = Object.create(Super.prototype)Sub.prototype.constructor = SubSub.cid = cid++Sub.options = mergeOptions(Super.options,extendOptions)Sub['super'] = Super// For props and computed properties, we define the proxy getters on// the Vue instances at extension time, on the extended prototype. This// avoids Object.defineProperty calls for each instance created.if (Sub.options.props) {initProps(Sub)}if (Sub.options.computed) {initComputed(Sub)}// allow further extension/mixin/plugin usageSub.extend = Super.extendSub.mixin = Super.mixinSub.use = Super.use// create asset registers, so extended classes// can have their private assets too.ASSET_TYPES.forEach(function (type) {Sub[type] = Super[type]})// enable recursive self-lookupif (name) { Sub.options.components[name] = Sub // 记录自己 在组件中递归自己  -> jsx}// keep a reference to the super options at extension time.// later at instantiation we can check if Super's options have// been updated.Sub.superOptions = Super.optionsSub.extendOptions = extendOptionsSub.sealedOptions = extend({}, Sub.options)// cache constructorcachedCtors[SuperId] = Subreturn Sub}
}

----------@----------

nextTick在哪里使用?原理是?

  • nextTick 中的回调是在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM
  • 在修改数据之后立即使用这个方法,获取更新后的 DOM
  • 主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法

nextTick 方法主要是使用了宏任务和微任务,定义了一个异步方法.多次调用 nextTick 会将方法存入队列中,通过这个异步方法清空当前队列。所以这个 nextTick 方法就是异步方法

根据执行环境分别尝试采用

  • 先采用Promise
  • Promise不支持,再采用MutationObserver
  • MutationObserver不支持,再采用setImmediate
  • 如果以上都不行则采用setTimeout
  • 最后执行flushCallbacks,把callbacks里面的数据依次执行

7-vue-1

回答范例

  1. nextTick 中的回调是在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM

  2. Vue有个异步更新策略,意思是如果数据变化,Vue不会立刻更新DOM,而是开启一个队列,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新。这一策略导致我们对数据的修改不会立刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使用nextTick

  3. 开发时,有两个场景我们会用到nextTick

  • created中想要获取DOM

  • 响应式数据变化后获取DOM更新后的状态,比如希望获取列表更新后的高度

  1. nextTick签名如下:function nextTick(callback?: () => void): Promise<void>

所以我们只需要在传入的回调函数中访问最新DOM状态即可,或者我们可以await nextTick()方法返回的Promise之后做这件事

  1. Vue内部,nextTick之所以能够让我们看到DOM更新后的结果,是因为我们传入的callback会被添加到队列刷新函数(flushSchedulerQueue)的后面,这样等队列内部的更新函数都执行完毕,所有DOM操作也就结束了,callback自然能够获取到最新的DOM值

基本使用

const vm = new Vue({el: '#app',data() {return { a: 1 }}
}); // vm.$nextTick(() => {// [nextTick回调函数fn,内部更新flushSchedulerQueue]
//     console.log(vm.$el.innerHTML)
// })// 是将内容维护到一个数组里,最终按照顺序顺序。 第一次会开启一个异步任务vm.a = 'test'; // 修改了数据后并不会马上更新视图
vm.$nextTick(() => {// [nextTick回调函数fn,内部更新flushSchedulerQueue]console.log(vm.$el.innerHTML)
})// nextTick中的方法会被放到 更新页面watcher的后面去

相关代码如下

7-vue-1

// src/core/utils/nextTick
let callbacks = [];
let pending = false;
function flushCallbacks() {pending = false; //把标志还原为false// 依次执行回调for (let i = 0; i < callbacks.length; i++) {callbacks[i]();}
}
let timerFunc; //定义异步方法  采用优雅降级
if (typeof Promise !== "undefined") {// 如果支持promiseconst p = Promise.resolve();timerFunc = () => {p.then(flushCallbacks);};
} else if (typeof MutationObserver !== "undefined") {// MutationObserver 主要是监听dom变化 也是一个异步方法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);};
} else if (typeof setImmediate !== "undefined") {// 如果前面都不支持 判断setImmediatetimerFunc = () => {setImmediate(flushCallbacks);};
} else {// 最后降级采用setTimeouttimerFunc = () => {setTimeout(flushCallbacks, 0);};
}export function nextTick(cb) {// 除了渲染watcher  还有用户自己手动调用的nextTick 一起被收集到数组callbacks.push(cb);if (!pending) {// 如果多次调用nextTick  只会执行一次异步 等异步队列清空之后再把标志变为falsepending = true;timerFunc();}
}

数据更新的时候内部会调用nextTick

// src/core/observer/scheduler.jsexport function queueWatcher (watcher: Watcher) {const id = watcher.idif (has[id] == null) {has[id] = trueif (!flushing) {queue.push(watcher)} else {// if already flushing, splice the watcher based on its id// if already past its id, it will be run next immediately.let i = queue.length - 1while (i > index && queue[i].id > watcher.id) {i--}queue.splice(i + 1, 0, watcher)}// queue the flushif (!waiting) {waiting = trueif (process.env.NODE_ENV !== 'production' && !config.async) {flushSchedulerQueue()return}// 把更新方法放到数组中维护[nextTick回调函数,更新函数flushSchedulerQueue]/*** vm.a = 'test'; // 修改了数据后并不会马上更新视图vm.$nextTick(() => {// [fn,更新]console.log(vm.$el.innerHTML)})*/nextTick(flushSchedulerQueue)}}
}

----------@----------

computed和watch区别

  1. 当页面中有某些数据依赖其他数据进行变动的时候,可以使用计算属性computed

Computed本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。 适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理

7-vue-1

<template>{{fullName}}</template>
export default {data(){return {firstName: 'zhang',lastName: 'san',}},computed:{fullName: function(){return this.firstName + ' ' + this.lastName}}
}
  1. watch用于观察和监听页面上的vue实例,如果要在数据变化的同时进行异步操作或者是比较大的开销,那么watch为最佳选择

Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销

7-vue-1

<template>{{fullName}}</template>
export default {data(){return {firstName: 'zhang',lastName: 'san',fullName: 'zhang san'}},watch:{firstName(val) {this.fullName = val + ' ' + this.lastName},lastName(val) {this.fullName = this.firstName + ' ' + val}}
}

computed:

  • computed是计算属性,也就是计算值,它更多用于计算值的场景
  • computed具有缓存性,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算
  • computed适用于计算比较消耗性能的计算场景

watch:

  • 更多的是「观察」的作用,类似于某些数据的监听回调,用于观察props $emit或者本组件的值,当数据变化时来执行回调进行后续操作
  • 无缓存性,页面重新渲染时值不变化也会执行

小结:

  • computedwatch都是基于watcher来实现的
  • computed属性是具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行
  • watch是监控值的变化,当值发生变化时调用其对应的回调函数
  • 当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为computed
  • 如果你需要在某个数据变化时做一些事情,使用watch来观察这个数据变化

回答范例

思路分析

  • 先看computed, watch两者定义,列举使用上的差异
  • 列举使用场景上的差异,如何选择
  • 使用细节、注意事项
  • vue3变化

computed特点:具有响应式的返回值

const count = ref(1)
const plusOne = computed(() => count.value + 1)

watch特点:侦测变化,执行回调

const state = reactive({ count: 0 })
watch(() => state.count,(count, prevCount) => {/* ... */}
)

回答范例

  1. 计算属性可以从组件数据派生出新数据,最常见的使用方式是设置一个函数,返回计算之后的结果,computedmethods的差异是它具备缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执行副作用,常见用法是传递一个函数,执行副作用,watch没有返回值,但可以执行异步操作等复杂逻辑
  2. 计算属性常用场景是简化行内模板中的复杂表达式,模板中出现太多逻辑会是模板变得臃肿不易维护。侦听器常用场景是状态变化之后做一些额外的DOM操作或者异步操作。选择采用何用方案时首先看是否需要派生出新值,基本能用计算属性实现的方式首选计算属性.
  3. 使用过程中有一些细节,比如计算属性也是可以传递对象,成为既可读又可写的计算属性。watch可以传递对象,设置deepimmediate等选项
  4. vue3watch选项发生了一些变化,例如不再能侦测一个点操作符之外的字符串形式的表达式; reactivity API中新出现了watchwatchEffect可以完全替代目前的watch选项,且功能更加强大

基本使用

// src/core/observer:45;// 渲染watcher  /  computed watcher  /  watch
const vm = new Vue({el: '#app',data: {firstname:'张',lastname:'三'},computed:{ // watcher  =>   firstname lastname// computed 只有取值时才执行// Object.defineProperty .getfullName(){ // firstName lastName 会收集fullName计算属性return this.firstname + this.lastname}},watch:{firstname(newVal,oldVal){console.log(newVal)}}
});setTimeout(() => {debugger;vm.firstname = '赵'
}, 1000);

相关源码

// 初始化state
function initState (vm: Component) {vm._watchers = []const opts = vm.$optionsif (opts.props) initProps(vm, opts.props)if (opts.methods) initMethods(vm, opts.methods)if (opts.data) {initData(vm)} else {observe(vm._data = {}, true /* asRootData */)}// 初始化计算属性if (opts.computed) initComputed(vm, opts.computed) // 初始化watchif (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch)}
}// 计算属性取值函数
function createComputedGetter (key) {return function computedGetter () {const watcher = this._computedWatchers && this._computedWatchers[key]if (watcher) {if (watcher.dirty) { // 如果值依赖的值发生变化,就会进行重新求值watcher.evaluate(); // this.firstname lastname}if (Dep.target) { // 让计算属性所依赖的属性 收集渲染watcherwatcher.depend()}return watcher.value}}
}// watch的实现
Vue.prototype.$watch = function (expOrFn: string | Function,cb: any,options?: Object): Function {const vm: Component = thisdebugger;if (isPlainObject(cb)) {return createWatcher(vm, expOrFn, cb, options)}options = options || {}options.user = trueconst watcher = new Watcher(vm, expOrFn, cb, options) // 创建watcher,数据更新调用cbif (options.immediate) {try {cb.call(vm, watcher.value)} catch (error) {handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)}}return function unwatchFn () {watcher.teardown()}
}

7-vue-1

----------@----------

vue3中 watch、watchEffect区别

  • watch是惰性执行,也就是只有监听的值发生变化的时候才会执行,但是watchEffect不同,每次代码加载watchEffect都会执行(忽略watch第三个参数的配置,如果修改配置项也可以实现立即执行)
  • watch需要传递监听的对象,watchEffect不需要
  • watch只能监听响应式数据:ref定义的属性和reactive定义的对象,如果直接监听reactive定义对象中的属性是不允许的(会报警告),除非使用函数转换一下。其实就是官网上说的监听一个getter
  • watchEffect如果监听reactive定义的对象是不起作用的,只能监听对象中的属性

看一下watchEffect的代码

<template>
<div>请输入firstName:<input type="text" v-model="firstName">
</div>
<div>请输入lastName:<input type="text" v-model="lastName">
</div>
<div>请输入obj.text:<input type="text" v-model="obj.text">
</div><div>【obj.text】 {{obj.text}}</div>
</template><script>
import {ref, reactive, watch, watchEffect} from 'vue'
export default {name: "HelloWorld",props: {msg: String,},setup(props,content){let firstName = ref('')let lastName = ref('')let obj= reactive({text:'hello'})watchEffect(()=>{console.log('触发了watchEffect');console.log(`组合后的名称为:${firstName.value}${lastName.value}`)})return{obj,firstName,lastName}}
};
</script>

7-vue-1 7-vue-1

改造一下代码

watchEffect(()=>{console.log('触发了watchEffect');// 这里我们不使用firstName.value/lastName.value ,相当于是监控整个ref,对应第四点上面的结论console.log(`组合后的名称为:${firstName}${lastName}`)
})

7-vue-1

watchEffect(()=>{console.log('触发了watchEffect');console.log(obj);
})

7-vue-1

稍微改造一下

let obj = reactive({text:'hello'
})
watchEffect(()=>{console.log('触发了watchEffect');console.log(obj.text);
})

7-vue-1

再看一下watch的代码,验证一下

let obj= reactive({text:'hello'
})
// watch是惰性执行, 默认初始化之后不会执行,只有值有变化才会触发,可通过配置参数实现默认执行
watch(obj, (newValue, oldValue) => {// 回调函数console.log('触发监控更新了new',  newValue);console.log('触发监控更新了old',  oldValue);
},{// 配置immediate参数,立即执行,以及深层次监听immediate: true,deep: true
})

7-vue-1

  • 监控整个reactive对象,从上面的图可以看到 deep 实际默认是开启的,就算我们设置为false也还是无效。而且旧值获取不到。
  • 要获取旧值则需要监控对象的属性,也就是监听一个getter,看下图
    7-vue-1
    7-vue-1

总结

  • 如果定义了reactive的数据,想去使用watch监听数据改变,则无法正确获取旧值,并且deep属性配置无效,自动强制开启了深层次监听。
  • 如果使用 ref 初始化一个对象或者数组类型的数据,会被自动转成reactive的实现方式,生成proxy代理对象。也会变得无法正确取旧值。
  • 用任何方式生成的数据,如果接收的变量是一个proxy代理对象,就都会导致watch这个对象时,watch回调里无法正确获取旧值。
  • 所以当大家使用watch监听对象时,如果在不需要使用旧值的情况,可以正常监听对象没关系;但是如果当监听改变函数里面需要用到旧值时,只能监听 对象.xxx`属性 的方式才行

watch和watchEffect异同总结

体验

watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数

const count = ref(0)watchEffect(() => console.log(count.value))
// -> logs 0
​
count.value++
// -> logs 1

watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数

const state = reactive({ count: 0 })
watch(() => state.count,(count, prevCount) => {/* ... */}
)

回答范例

  1. watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数
  2. watchEffect(effect)是一种特殊watch,传入的函数既是依赖收集的数据源,也是回调函数。如果我们不关心响应式数据变化前后的值,只是想拿这些数据做些事情,那么watchEffect就是我们需要的。watch更底层,可以接收多种数据源,包括用于依赖收集的getter函数,因此它完全可以实现watchEffect的功能,同时由于可以指定getter函数,依赖可以控制的更精确,还能获取数据变化前后的值,因此如果需要这些时我们会使用watch
  3. watchEffect在使用时,传入的函数会立刻执行一次。watch默认情况下并不会执行回调函数,除非我们手动设置immediate选项
  4. 从实现上来说,watchEffect(fn)相当于watch(fn,fn,{immediate:true})

watchEffect定义如下

export function watchEffect(effect: WatchEffect,options?: WatchOptionsBase
): WatchStopHandle {return doWatch(effect, null, options)
}

watch定义如下

export function watch<T = any, Immediate extends Readonly<boolean> = false>(source: T | WatchSource<T>,cb: any,options?: WatchOptions<Immediate>
): WatchStopHandle {return doWatch(source as any, cb, options)
}

很明显watchEffect就是一种特殊的watch实现。

----------@----------

Watch中的deep:true是如何实现的

当用户指定了 watch 中的deep属性为 true 时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前 watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新

源码相关

get () { pushTarget(this) // 先将当前依赖放到 Dep.target上 let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { if (this.deep) { // 如果需要深度监控 traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法 }popTarget() 
}

----------@----------

Vue computed 实现

  • 建立与其他属性(如:dataStore)的联系;
  • 属性改变后,通知计算属性重新计算

实现时,主要如下

  • 初始化 data, 使用 Object.defineProperty 把这些属性全部转为 getter/setter
  • 初始化 computed, 遍历 computed 里的每个属性,每个 computed 属性都是一个 watch 实例。每个属性提供的函数作为属性的 getter,使用 Object.defineProperty 转化。
  • Object.defineProperty getter 依赖收集。用于依赖发生变化时,触发属性重新计算。
  • 若出现当前 computed 计算属性嵌套其他 computed 计算属性时,先进行其他的依赖收集

----------@----------

watch 原理

watch 本质上是为每个监听属性 setter 创建了一个 watcher,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deepimmediate,对应原理如下

  • deep:深度监听对象,为对象的每一个属性创建一个 watcher,从而确保对象的每一个属性更新时都会触发传入的回调函数。主要原因在于对象属于引用类型,单个属性的更新并不会触发对象 setter,因此引入 deep 能够很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,避免性能浪费。
  • immediate:在初始化时直接调用回调函数,可以通过在 created 阶段手动调用回调函数实现相同的效果

----------@----------

Vue.set的实现原理

  • 给对应和数组本身都增加了dep属性
  • 当给对象新增不存在的属性则触发对象依赖的watcher去更新
  • 当修改数组索引时,我们调用数组本身的splice去更新数组(数组的响应式原理就是重新了splice等方法,调用splice就会触发视图更新)

基本使用

以下方法调用会改变原始数组:push(), pop(), shift(), unshift(), splice(), sort(), reverse(),Vue.set( target, key, value )

  • 调用方法:Vue.set(target, key, value )
    • target:要更改的数据源(可以是对象或者数组)
    • key:要更改的具体数据
    • value :重新赋的值
<div id="app">{{user.name}} {{user.age}}</div>
<div id="app"></div>
<script>// 1. 依赖收集的特点:给每个属性都增加一个dep属性,dep属性会进行收集,收集的是watcher// 2. vue会给每个对象也增加一个dep属性const vm = new Vue({el: '#app',data: { // vm._data  user: {name:'poetry'}}});// 对象的话:调用defineReactive在user对象上定义一个age属性,增加到响应式数据中,触发对象本身的watcher,ob.dep.notify()更新 // 如果是数组 通过调用 splice方法,触发视图更新vm.$set(vm.user, 'age', 20); // 不能给根属性添加,因为给根添加属性 性能消耗太大,需要做很多处理// 修改肯定是同步的 -> 更新都是一步的  queuewatcher
</script>

相关源码

// src/core/observer/index.js 44
export class Observer { // new Observer(value)value: any;dep: Dep;vmCount: number; // number of vms that have this object as root $dataconstructor (value: any) {this.value = valuethis.dep = new Dep() // 给所有对象类型增加dep属性}
}
// src/core/observer/index.js 201
export function set (target: Array<any> | Object, key: any, val: any): any {// 1.是开发环境 target 没定义或者是基础类型则报错if (process.env.NODE_ENV !== 'production' &&(isUndef(target) || isPrimitive(target))) {warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}// 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)if (Array.isArray(target) && isValidArrayIndex(key)) {target.length = Math.max(target.length, key)// 利用数组的splice变异方法触发响应式  target.splice(key, 1, val)return val}// 3.如果是对象本身的属性,则直接添加即可if (key in target && !(key in Object.prototype)) {target[key] = val // 直接修改属性值  return val}// 4.如果是Vue实例 或 根数据data时 报错,(更新_data 无意义)const ob = (target: any).__ob__if (target._isVue || (ob && ob.vmCount)) {process.env.NODE_ENV !== 'production' && warn('Avoid adding reactive properties to a Vue instance or its root $data ' +'at runtime - declare it upfront in the data option.')return val}// 5.如果不是响应式的也不需要将其定义成响应式属性if (!ob) {target[key] = valreturn val}// 6.将属性定义成响应式的defineReactive(ob.value, key, val)// 通知视图更新ob.dep.notify()return val
}

我们阅读以上源码可知,vm.$set 的实现原理是:

  • 如果目标是数组 ,直接使用数组的 splice 方法触发相应式;
  • 如果目标是对象 ,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 gettersetter 的功能所调用的方法)

----------@----------

Vue为什么需要虚拟DOM?优缺点有哪些

由于在浏览器中操作 DOM是很昂贵的。频繁的操作 DOM,会产生一定的性能问题。这就是虚拟 Dom 的产生原因。Vue2Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM 本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象

优点:

  • 保证性能下限 : 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
  • 无需手动操作 DOM : 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
  • 跨平台 : 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

缺点:

  • 无法进行极致优化:虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
  • 首次渲染大量DOM时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。

虚拟 DOM 实现原理?

虚拟 DOM 的实现原理主要包括以下 3 部分:

  • JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
  • diff 算法 — 比较两棵虚拟 DOM 树的差异;
  • pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。

说说你对虚拟 DOM 的理解?回答范例

思路

  • vdom是什么
  • 引入vdom的好处
  • vdom如何生成,又如何成为dom
  • 在后续的diff中的作用

回答范例

  1. 虚拟dom顾名思义就是虚拟的dom对象,它本身就是一个 JavaScript 对象,只不过它是通过不同的属性去描述一个视图结构

  2. 通过引入vdom我们可以获得如下好处:

  • 将真实元素节点抽象成 VNode,有效减少直接操作 dom 次数,从而提高程序性能

    • 直接操作 dom 是有限制的,比如:diffclone 等操作,一个真实元素上有许多的内容,如果直接对其进行 diff 操作,会去额外 diff 一些没有必要的内容;同样的,如果需要进行 clone 那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到 JavaScript 对象上,那么就会变得简单了
    • 操作 dom 是比较昂贵的操作,频繁的dom操作容易引起页面的重绘和回流,但是通过抽象 VNode 进行中间处理,可以有效减少直接操作dom的次数,从而减少页面重绘和回流
  • 方便实现跨平台

    • 同一 VNode 节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android)变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等
    • Vue3 中允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染
  1. vdom如何生成?在vue中我们常常会为组件编写模板 - template, 这个模板会被编译器 - compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用render函数,返回的对象就是虚拟dom。但它们还不是真正的dom,所以会在后续的patch过程中进一步转化为dom

7-vue-1

  1. 挂载过程结束后,vue程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render,此时就会生成新的vdom,和上一次的渲染结果diff就能得到变化的地方,从而转换为最小量的dom操作,高效更新视图

为什么要用vdom?案例解析

现在有一个场景,实现以下需求:

[    { name: "张三", age: "20", address: "北京"},    { name: "李四", age: "21", address: "武汉"},    { name: "王五", age: "22", address: "杭州"},
]

将该数据展示成一个表格,并且随便修改一个信息,表格也跟着修改。 用jQuery实现如下:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Document</title>
</head><body><div id="container"></div><button id="btn-change">改变</button><script src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script><script>const data = [{name: "张三",age: "20",address: "北京"},{name: "李四",age: "21",address: "武汉"},{name: "王五",age: "22",address: "杭州"},];//渲染函数function render(data) {const $container = $('#container');$container.html('');const $table = $('<table>');// 重绘一次$table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>'));data.forEach(item => {//每次进入都重绘$table.append($(`<tr><td>${item.name}</td><td>${item.age}</td><td>${item.address}</td></tr>`))})$container.append($table);}$('#btn-change').click(function () {data[1].age = 30;data[2].address = '深圳';render(data);});</script>
</body>
</html>
  • 这样点击按钮,会有相应的视图变化,但是你审查以下元素,每次改动之后,table标签都得重新创建,也就是说table下面的每一个栏目,不管是数据是否和原来一样,都得重新渲染,这并不是理想中的情况,当其中的一栏数据和原来一样,我们希望这一栏不要重新渲染,因为DOM重绘相当消耗浏览器性能。
  • 因此我们采用JS对象模拟的方法,将DOM的比对操作放在JS层,减少浏览器不必要的重绘,提高效率。
  • 当然有人说虚拟DOM并不比真实的DOM快,其实也是有道理的。当上述table中的每一条数据都改变时,显然真实的DOM操作更快,因为虚拟DOM还存在jsdiff算法的比对过程。所以,上述性能优势仅仅适用于大量数据的渲染并且改变的数据只是一小部分的情况。

如下DOM结构:

<ul id="list"><li class="item">Item1</li><li class="item">Item2</li>
</ul>

映射成虚拟DOM就是这样:

{tag: "ul",attrs: {id:"list"},children: [{tag: "li",attrs: { className: "item" },children: ["Item1"]}, {tag: "li",attrs: { className: "item" },children: ["Item2"]}]
} 

使用snabbdom实现vdom

这是一个简易的实现vdom功能的库,相比vuereact,对于vdom这块更加简易,适合我们学习vdomvdom里面有两个核心的api,一个是h函数,一个是patch函数,前者用来生成vdom对象,后者的功能在于做虚拟dom的比对和将vdom挂载到真实DOM

简单介绍一下这两个函数的用法:

h('标签名', {属性}, [子元素])
h('标签名', {属性}, [文本])
patch(container, vnode) // container为容器DOM元素
patch(vnode, newVnode)

现在我们就来用snabbdom重写一下刚才的例子:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Document</title>
</head>
<body><div id="container"></div><button id="btn-change">改变</button><script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.min.js"></script><script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script><script>let snabbdom = window.snabbdom;// 定义patchlet patch = snabbdom.init([snabbdom_class,snabbdom_props,snabbdom_style,snabbdom_eventlisteners]);//定义hlet h = snabbdom.h;const data = [{name: "张三",age: "20",address: "北京"},{name: "李四",age: "21",address: "武汉"},{name: "王五",age: "22",address: "杭州"},];data.unshift({name: "姓名", age: "年龄", address: "地址"});let container = document.getElementById('container');let vnode;const render = (data) => {let newVnode = h('table', {}, data.map(item => { let tds = [];for(let i in item) {if(item.hasOwnProperty(i)) {tds.push(h('td', {}, item[i] + ''));}}return h('tr', {}, tds);}));if(vnode) {patch(vnode, newVnode);} else {patch(container, newVnode);}vnode = newVnode;}render(data);let btnChnage = document.getElementById('btn-change');btnChnage.addEventListener('click', function() {data[1].age = 30;data[2].address = "深圳";//re-renderrender(data);})</script>
</body>
</html>

7-vue-1

你会发现, 只有改变的栏目才闪烁,也就是进行重绘 ,数据没有改变的栏目还是保持原样,这样就大大节省了浏览器重新渲染的开销

vue中使用h函数生成虚拟DOM返回

const vm = new Vue({el: '#app',data: {user: {name:'poetry'}},render(h){// h()// h(App)// h('div',[])let vnode = h('div',{},'hello world');return vnode}
});

----------@----------

Vue中diff算法原理

DOM操作是非常昂贵的,因此我们需要尽量地减少DOM操作。这就需要找出本次DOM必须更新的节点来更新,其他的不更新,这个找出的过程,就需要应用diff算法

vuediff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式+双指针(头尾都加指针)的方式进行比较。

简单来说,Diff算法有以下过程

  • 同级比较,再比较子节点(根据keytag标签名判断)
  • 先判断一方有子节点和一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
  • 比较都有子节点的情况(核心diff)
  • 递归比较子节点
  • 正常Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以VueDiff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
  • Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比ReactDiff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅
  • 在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升

7-vue-1

vue3中采用最长递增子序列来实现diff优化

回答范例

思路

  • diff算法是干什么的
  • 它的必要性
  • 它何时执行
  • 具体执行方式
  • 拔高:说一下vue3中的优化

回答范例

  1. Vue中的diff算法称为patching算法,它由Snabbdom修改而来,虚拟DOM要想转化为真实DOM就需要通过patch方法转换
  2. 最初Vue1.x视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOMpatching算法支持,但是这样粒度过细导致Vue1.x无法承载较大应用;Vue 2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,此时就需要引入patching算法才能精确找到发生变化的地方并高效更新
  3. vuediff执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数获得最新的虚拟DOM,然后执行patch函数,并传入新旧两次虚拟DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作
  4. patch过程是一个递归过程,遵循深度优先、同层比较的策略;以vue3patch为例
  • 首先判断两个节点是否为相同同类节点,不同则删除重新创建
  • 如果双方都是文本则更新文本内容
  • 如果双方都是元素节点则递归更新子元素,同时更新元素属性
  • 更新子节点时又分了几种情况
    • 新的子节点是文本,老的子节点是数组则清空,并设置文本;
    • 新的子节点是文本,老的子节点是文本则直接更新文本;
    • 新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;
    • 新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节blabla
  • vue3中引入的更新策略:静态节点标记等

vdom中diff算法的简易实现

以下代码只是帮助大家理解diff算法的原理和流程

  1. vdom转化为真实dom
const createElement = (vnode) => {let tag = vnode.tag;let attrs = vnode.attrs || {};let children = vnode.children || [];if(!tag) {return null;}//创建元素let elem = document.createElement(tag);//属性let attrName;for (attrName in attrs) {if(attrs.hasOwnProperty(attrName)) {elem.setAttribute(attrName, attrs[attrName]);}}//子元素children.forEach(childVnode => {//给elem添加子元素elem.appendChild(createElement(childVnode));})//返回真实的dom元素return elem;
}
  1. 用简易diff算法做更新操作
function updateChildren(vnode, newVnode) {let children = vnode.children || [];let newChildren = newVnode.children || [];children.forEach((childVnode, index) => {let newChildVNode = newChildren[index];if(childVnode.tag === newChildVNode.tag) {//深层次对比, 递归过程updateChildren(childVnode, newChildVNode);} else {//替换replaceNode(childVnode, newChildVNode);}})
}

----------@----------

Vue的diff算法详细分析

1. 是什么

diff 算法是一种通过同层的树节点进行比较的高效算法

其有两个特点:

  • 比较只会在同层级进行, 不会跨层级比较
  • 在diff比较的过程中,循环从两边向中间比较

diff 算法在很多场景下都有应用,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较

2. 比较方式

diff整体策略为:深度优先,同层比较

  1. 比较只会在同层级进行, 不会跨层级比较

7-vue-1

  1. 比较的过程中,循环从两边向中间收拢

7-vue-1

下面举个vue通过diff算法更新的例子:

新旧VNode节点如下图所示:

7-vue-1

第一次循环后,发现旧节点D与新节点D相同,直接复用旧节点D作为diff后的第一个真实节点,同时旧节点endIndex移动到C,新节点的 startIndex 移动到了 C

7-vue-1

第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,diff 后创建了 C 的真实节点插入到第一次创建的 D 节点后面。同时旧节点的 endIndex 移动到了 B,新节点的 startIndex 移动到了 E

7-vue-1

第三次循环中,发现E没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex 移动到了 A。旧节点的 startIndexendIndex 都保持不动

7-vue-1

第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 diff 后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex 移动到了 B,新节点的startIndex 移动到了 B

7-vue-1

第五次循环中,情形同第四次循环一样,因此 diff 后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex移动到了 C,新节点的 startIndex 移动到了 F

7-vue-1

新节点的 startIndex 已经大于 endIndex 了,需要创建 newStartIdxnewEndIdx 之间的所有节点,也就是节点F,直接创建 F 节点对应的真实节点放到 B 节点后面

7-vue-1

3. 原理分析

当数据发生改变时,set方法会调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁,更新相应的视图

源码位置:src/core/vdom/patch.js

function patch(oldVnode, vnode, hydrating, removeOnly) {if (isUndef(vnode)) { // 没有新节点,直接执行destory钩子函数if (isDef(oldVnode)) invokeDestroyHook(oldVnode)return}let isInitialPatch = falseconst insertedVnodeQueue = []if (isUndef(oldVnode)) {isInitialPatch = truecreateElm(vnode, insertedVnodeQueue) // 没有旧节点,直接用新节点生成dom元素} else {const isRealElement = isDef(oldVnode.nodeType)if (!isRealElement && sameVnode(oldVnode, vnode)) {// 判断旧节点和新节点自身一样,一致执行patchVnodepatchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)} else {// 否则直接销毁及旧节点,根据新节点生成dom元素if (isRealElement) {if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {oldVnode.removeAttribute(SSR_ATTR)hydrating = true}if (isTrue(hydrating)) {if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {invokeInsertHook(vnode, insertedVnodeQueue, true)return oldVnode}}oldVnode = emptyNodeAt(oldVnode)}return vnode.elm}}
}

patch函数前两个参数位为oldVnodeVnode ,分别代表新的节点和之前的旧节点,主要做了四个判断:

  • 没有新节点,直接触发旧节点的destory钩子
  • 没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用 createElm
  • 旧节点和新节点自身一样,通过 sameVnode 判断节点是否一样,一样时,直接调用 patchVnode去处理这两个节点
  • 旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点

下面主要讲的是patchVnode部分

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {// 如果新旧节点一致,什么都不做if (oldVnode === vnode) {return}// 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化const elm = vnode.elm = oldVnode.elm// 异步占位符if (isTrue(oldVnode.isAsyncPlaceholder)) {if (isDef(vnode.asyncFactory.resolved)) {hydrate(oldVnode.elm, vnode, insertedVnodeQueue)} else {vnode.isAsyncPlaceholder = true}return}// 如果新旧都是静态节点,并且具有相同的key// 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上// 也不用再有其他操作if (isTrue(vnode.isStatic) &&isTrue(oldVnode.isStatic) &&vnode.key === oldVnode.key &&(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {vnode.componentInstance = oldVnode.componentInstancereturn}let iconst data = vnode.dataif (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {i(oldVnode, vnode)}const oldCh = oldVnode.childrenconst ch = vnode.childrenif (isDef(data) && isPatchable(vnode)) {for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)}// 如果vnode不是文本节点或者注释节点if (isUndef(vnode.text)) {// 并且都有子节点if (isDef(oldCh) && isDef(ch)) {// 并且子节点不完全一致,则调用updateChildrenif (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)// 如果只有新的vnode有子节点} else if (isDef(ch)) {if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')// elm已经引用了老的dom节点,在老的dom节点上添加子节点addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)// 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh} else if (isDef(oldCh)) {removeVnodes(elm, oldCh, 0, oldCh.length - 1)// 如果老节点是文本节点} else if (isDef(oldVnode.text)) {nodeOps.setTextContent(elm, '')}// 如果新vnode和老vnode是文本节点或注释节点// 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以} else if (oldVnode.text !== vnode.text) {nodeOps.setTextContent(elm, vnode.text)}if (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)}}

patchVnode主要做了几个判断:

  • 新节点是否是文本节点,如果是,则直接更新dom的文本内容为新节点的文本内容
  • 新节点和旧节点如果都有子节点,则处理比较更新子节点
  • 只有新节点有子节点,旧节点没有,那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新DOM,并且添加进父节点
  • 只有旧节点有子节点而新节点没有,说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把DOM 删除

子节点不完全一致,则调用updateChildren

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {let oldStartIdx = 0 // 旧头索引let newStartIdx = 0 // 新头索引let oldEndIdx = oldCh.length - 1 // 旧尾索引let newEndIdx = newCh.length - 1 // 新尾索引let oldStartVnode = oldCh[0] // oldVnode的第一个childlet oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个childlet newStartVnode = newCh[0] // newVnode的第一个childlet newEndVnode = newCh[newEndIdx] // newVnode的最后一个childlet oldKeyToIdx, idxInOld, vnodeToMove, refElm// removeOnly is a special flag used only by <transition-group>// to ensure removed elements stay in correct relative positions// during leaving transitionsconst canMove = !removeOnly// 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {// 如果oldVnode的第一个child不存在if (isUndef(oldStartVnode)) {// oldStart索引右移oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left// 如果oldVnode的最后一个child不存在} else if (isUndef(oldEndVnode)) {// oldEnd索引左移oldEndVnode = oldCh[--oldEndIdx]// oldStartVnode和newStartVnode是同一个节点} else if (sameVnode(oldStartVnode, newStartVnode)) {// patch oldStartVnode和newStartVnode, 索引左移,继续循环patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)oldStartVnode = oldCh[++oldStartIdx]newStartVnode = newCh[++newStartIdx]// oldEndVnode和newEndVnode是同一个节点} else if (sameVnode(oldEndVnode, newEndVnode)) {// patch oldEndVnode和newEndVnode,索引右移,继续循环patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)oldEndVnode = oldCh[--oldEndIdx]newEndVnode = newCh[--newEndIdx]// oldStartVnode和newEndVnode是同一个节点} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right// patch oldStartVnode和newEndVnodepatchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)// 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))// oldStart索引右移,newEnd索引左移oldStartVnode = oldCh[++oldStartIdx]newEndVnode = newCh[--newEndIdx]// 如果oldEndVnode和newStartVnode是同一个节点} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left// patch oldEndVnode和newStartVnodepatchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)// 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)// oldEnd索引左移,newStart索引右移oldEndVnode = oldCh[--oldEndIdx]newStartVnode = newCh[++newStartIdx]// 如果都不匹配} else {if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)// 尝试在oldChildren中寻找和newStartVnode的具有相同的key的VnodeidxInOld = isDef(newStartVnode.key)? oldKeyToIdx[newStartVnode.key]: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)// 如果未找到,说明newStartVnode是一个新的节点if (isUndef(idxInOld)) { // New element// 创建一个新VnodecreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)// 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove} else {vnodeToMove = oldCh[idxInOld]/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {warn('It seems there are duplicate keys that is causing an update error. ' +'Make sure each v-for item has a unique key.')}// 比较两个具有相同的key的新节点是否是同一个节点//不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。if (sameVnode(vnodeToMove, newStartVnode)) {// patch vnodeToMove和newStartVnodepatchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)// 清除oldCh[idxInOld] = undefined// 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm// 移动到oldStartVnode.elm之前canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)// 如果key相同,但是节点不相同,则创建一个新的节点} else {// same key but different element. treat as new elementcreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)}}// 右移newStartVnode = newCh[++newStartIdx]}}

while循环主要处理了以下五种情景:

  • 当新老 VNode 节点的 start 相同时,直接 patchVnode ,同时新老 VNode 节点的开始索引都加 1
  • 当新老 VNode 节点的 end相同时,同样直接 patchVnode ,同时新老 VNode 节点的结束索引都减 1
  • 当老 VNode 节点的 start 和新 VNode 节点的 end 相同时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldEndVnode 的后面,同时老 VNode 节点开始索引加 1,新 VNode 节点的结束索引减 1
  • 当老 VNode 节点的 end 和新 VNode 节点的 start 相同时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldStartVnode 的前面,同时老 VNode 节点结束索引减 1,新 VNode 节点的开始索引加 1
  • 如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况:
    • 从旧的 VNodekey 值,对应 index 序列为 value 值的哈希表中找到与 newStartVnode 一致 key 的旧的 VNode 节点,再进行patchVnode,同时将这个真实 dom移动到 oldStartVnode 对应的真实 dom 的前面
    • 调用 createElm 创建一个新的 dom 节点放到当前 newStartIdx 的位置

小结

  • 当数据发生改变时,订阅者watcher就会调用patch给真实的DOM打补丁
  • 通过isSameVnode进行判断,相同则调用patchVnode方法
  • patchVnode做了以下操作:
    • 找到对应的真实dom,称为el
    • 如果都有都有文本节点且不相等,将el文本节点设置为Vnode的文本节点
    • 如果oldVnode有子节点而VNode没有,则删除el子节点
    • 如果oldVnode没有子节点而VNode有,则将VNode的子节点真实化后添加到el
    • 如果两者都有子节点,则执行updateChildren函数比较子节点
  • updateChildren主要做了以下操作:
    • 设置新旧VNode的头尾指针
    • 新旧头尾指针进行比较,循环向中间靠拢,根据情况调用patchVnode进行patch重复流程、调用createElem创建一个新节点,从哈希表寻找 key一致的VNode 节点再分情况操作

----------@----------

既然Vue通过数据劫持可以精准探测数据变化,为什么还需要虚拟DOM进行diff检测差异

  • 响应式数据变化,Vue确实可以在数据变化时,响应式系统可以立刻得知。但是如果给每个属性都添加watcher用于更新的话,会产生大量的watcher从而降低性能
  • 而且粒度过细也得导致更新不准确的问题,所以vue采用了组件级的watcher配合diff来检测差异

----------@----------

请说明Vue中key的作用和原理,谈谈你对它的理解

7-vue-1

  • key是为Vue中的VNode标记的唯一id,在patch过程中通过key可以判断两个虚拟节点是否是相同节点,通过这个key,我们的diff操作可以更准确、更快速
  • diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行比对,然后检出差异
  • 尽量不要采用索引作为key
  • 如果不加key,那么vue会选择复用节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug
  • 更准确 :因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
  • 更快速key的唯一性可以被Map数据结构充分利用,相比于遍历查找的时间复杂度O(n)Map的时间复杂度仅仅为O(1),比遍历方式更快。

源码如下:

function createKeyToOldIdx (children, beginIdx, endIdx) {let i, keyconst map = {}for (i = beginIdx; i <= endIdx; ++i) {key = children[i].keyif (isDef(key)) map[key] = i}return map
}

回答范例

分析

这是一道特别常见的问题,主要考查大家对虚拟DOMpatch细节的掌握程度,能够反映面试者理解层次

思路分析:

  • 给出结论,key的作用是用于优化patch性能
  • key的必要性
  • 实际使用方式
  • 总结:可从源码层面描述一下vue如何判断两个节点是否相同

回答范例:

  1. key的作用主要是为了更高效的更新虚拟DOM
  2. vuepatch过程中 判断两个节点是否是相同节点是key是一个必要条件 ,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能
  3. 实际使用中在渲染一组列表时key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,这可能导致一些隐蔽的bugvue中在使用相同标签元素过渡切换时,也会使用key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果
  4. 从源码中可以知道,vue判断两个节点是否相同时主要判断两者的key标签类型(如div)等,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的

如果不使用 keyVue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为 Vuevnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速

7-vue-1

diff程可以概括为:oldChnewCh各有两个头尾的变量StartIdxEndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldChnewCh至少有一个已经遍历完了,就会结束比较,这四种比较方式就是旧尾新头旧头新尾

相关代码如下

// 判断两个vnode的标签和key是否相同 如果相同 就可以认为是同一节点就地复用
function isSameVnode(oldVnode, newVnode) {return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;
}// 根据key来创建老的儿子的index映射表  类似 {'a':0,'b':1} 代表key为'a'的节点在第一个位置 key为'b'的节点在第二个位置
function makeIndexByKey(children) {let map = {};children.forEach((item, index) => {map[item.key] = index;});return map;
}
// 生成的映射表
let map = makeIndexByKey(oldCh);

----------@----------

Vue组件为什么只能有一个根元素

vue3中没有问题

Vue.createApp({components: {comp: {template: `<div>root1</div><div>root2</div>`}}
}).mount('#app')
  1. vue2中组件确实只能有一个根,但vue3中组件已经可以多根节点了。
  2. 之所以需要这样是因为vdom是一颗单根树形结构,patch方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom
  3. vue3中之所以可以写多个根节点,是因为引入了Fragment的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个Fragment节点,把多个根节点作为它的children。将来patch的时候,如果发现是一个Fragment节点,则直接遍历children创建或更新

----------@----------

谈一谈对Vue组件化的理解

  • 组件化开发能大幅提高开发效率、测试性、复用性等
  • 常用的组件化技术:属性、自定义事件、插槽
  • 降低更新频率,只重新渲染变化的组件
  • 组件的特点:高内聚、低耦合、单向数据流

----------@----------

Vue组件渲染和更新过程

渲染组件时,会通过 Vue.extend 方法构建子组件的构造函数,并进行实例化。最终手动调用$mount() 进行挂载。更新组件时会进行 patchVnode 流程,核心就是diff算法

7-vue-1

----------@----------

异步组件是什么?使用场景有哪些?

分析

因为异步路由的存在,我们使用异步组件的次数比较少,因此还是有必要两者的不同。

体验

大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们

import { defineAsyncComponent } from 'vue'
// defineAsyncComponent定义异步组件,返回一个包装组件。包装组件根据加载器的状态决定渲染什么内容
const AsyncComp = defineAsyncComponent(() => {// 加载函数返回Promisereturn new Promise((resolve, reject) => {// ...可以从服务器加载组件resolve(/* loaded component */)})
})
// 借助打包工具实现ES模块动态导入
const AsyncComp = defineAsyncComponent(() =>import('./components/MyComponent.vue')
)

回答范例

  1. 在大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们。
  2. 我们不仅可以在路由切换时懒加载组件,还可以在页面组件中继续使用异步组件,从而实现更细的分割粒度。
  3. 使用异步组件最简单的方式是直接给defineAsyncComponent指定一个loader函数,结合ES模块动态导入函数import可以快速实现。我们甚至可以指定loadingComponenterrorComponent选项从而给用户一个很好的加载反馈。另外Vue3中还可以结合Suspense组件使用异步组件。
  4. 异步组件容易和路由懒加载混淆,实际上不是一个东西。异步组件不能被用于定义懒加载路由上,处理它的是vue框架,处理路由组件加载的是vue-router。但是可以在懒加载的路由组件中使用异步组件

----------@----------

为什么要使用异步组件

  1. 节省打包出的结果,异步组件分开打包,采用jsonp的方式进行加载,有效解决文件过大的问题。
  2. 核心就是包组件定义变成一个函数,依赖import() 语法,可以实现文件的分割加载。
components:{ AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([]) 
}

原理

export function ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { // async component let asyncFactory if (isUndef(Ctor.cid)) { asyncFactory = Ctor Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默认调用此函数时返回 undefiend // 第二次渲染时Ctor不为undefined if (Ctor === undefined) { return createAsyncPlaceholder( // 渲染占位符 空虚拟节点 asyncFactory, data, context, children, tag ) } } 
}
function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void { if (isDef(factory.resolved)) { // 3.在次渲染时可以拿到获取的最新组件 return factory.resolved }const resolve = once((res: Object | Class<Component>) => { factory.resolved = ensureCtor(res, baseCtor) if (!sync) { forceRender(true) //2. 强制更新视图重新渲染 } else { owners.length = 0 } })const reject = once(reason => { if (isDef(factory.errorComp)) { factory.error = true forceRender(true) } })const res = factory(resolve, reject)// 1.将resolve方法和reject方法传入,用户调用 resolve方法后 sync = false return factory.resolved 
}

----------@----------

函数式组件优势和原理

函数组件的特点

  1. 函数式组件需要在声明组件是指定 functional:true
  2. 不需要实例化,所以没有this,this通过render函数的第二个参数context来代替
  3. 没有生命周期钩子函数,不能使用计算属性,watch
  4. 不能通过$emit 对外暴露事件,调用事件只能通过context.listeners.click的方式调用外部传入的事件
  5. 因为函数式组件是没有实例化的,所以在外部通过ref去引用组件时,实际引用的是HTMLElement
  6. 函数式组件的props可以不用显示声明,所以没有在props里面声明的属性都会被自动隐式解析为prop,而普通组件所有未声明的属性都解析到$attrs里面,并自动挂载到组件根元素上面(可以通过inheritAttrs属性禁止)

优点

  1. 由于函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件
  2. 函数式组件结构比较简单,代码结构更清晰

使用场景:

  • 一个简单的展示组件,作为容器组件使用 比如 router-view 就是一个函数式组件
  • “高阶组件”——用于接收一个组件作为参数,返回一个被包装过的组件

例子

Vue.component('functional',{ // 构造函数产生虚拟节点的functional:true, // 函数式组件 // data={attrs:{}}render(h){return h('div','test')}
})
const vm = new Vue({el: '#app'
})

源码相关

// functional component
if (isTrue(Ctor.options.functional)) { // 带有functional的属性的就是函数式组件return createFunctionalComponent(Ctor, propsData, data, context, children)
}// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on // 处理事件
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn // 处理原生事件// install component management hooks onto the placeholder node
installComponentHooks(data) // 安装组件相关钩子 (函数式组件没有调用此方法,从而性能高于普通组件)

----------@----------

Vue组件之间通信方式有哪些

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。 Vue 组件间通信只要指以下 3 类通信父子组件通信隔代组件通信兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信

组件传参的各种方式

7-vue-1

组件通信常用方式有以下几种

  • props / $emit 适用 父子组件通信
    • 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的
  • ref$parent / $children(vue3废弃) 适用 父子组件通信
    • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
    • $parent / $children:访问访问父组件的属性或方法 / 访问子组件的属性或方法
  • EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信
    • 这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件
  • $attrs / $listeners(vue3废弃) 适用于 隔代组件通信
    • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( classstyle 除外 )。当一个组件没有声明任何 prop时,这里会包含所有父作用域的绑定 ( classstyle 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用
    • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
  • provide / inject 适用于 隔代组件通信
    • 祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题, 不过它的使用场景,主要是子组件获取上级组件的状态 ,跨级组件间建立了一种主动提供与依赖注入的关系
  • $root 适用于 隔代组件通信 访问根组件中的属性或方法,是根组件,不是父组件。$root只对根组件有用
  • Vuex 适用于 父子、隔代、兄弟组件通信
    • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )
    • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
    • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

根据组件之间关系讨论组件通信最为清晰有效

  • 父子组件:props/$emit/$parent/ref
  • 兄弟组件:$parent/eventbus/vuex
  • 跨层级关系:eventbus/vuex/provide+inject/$attrs + $listeners/$root

下面演示组件之间通讯三种情况: 父传子、子传父、兄弟组件之间的通讯

1. 父子组件通信

使用props,父组件可以使用props向子组件传递数据。

父组件vue模板father.vue:

<template><child :msg="message"></child>
</template><script>
import child from './child.vue';
export default {components: {child},data () {return {message: 'father message';}}
}
</script>

子组件vue模板child.vue:

<template><div>{{msg}}</div>
</template><script>
export default {props: {msg: {type: String,required: true}}
}
</script>

回调函数(callBack)

父传子:将父组件里定义的method作为props传入子组件

// 父组件Parent.vue:
<Child :changeMsgFn="changeMessage">
methods: {changeMessage(){this.message = 'test'}
}
// 子组件Child.vue:
<button @click="changeMsgFn">
props:['changeMsgFn']

子组件向父组件通信

父组件向子组件传递事件方法,子组件通过$emit触发事件,回调给父组件

父组件vue模板father.vue:

<template><child @msgFunc="func"></child>
</template><script>
import child from './child.vue';
export default {components: {child},methods: {func (msg) {console.log(msg);}}
}
</script>

子组件vue模板child.vue:

<template><button @click="handleClick">点我</button>
</template><script>
export default {props: {msg: {type: String,required: true}},methods () {handleClick () {//........this.$emit('msgFunc');}}
}
</script>

2. provide / inject 跨级访问祖先组件的数据

父组件通过使用provide(){return{}}提供需要传递的数据

export default {data() {return {title: '我是父组件',name: 'poetry'}},methods: {say() {alert(1)}},// provide属性 能够为后面的后代组件/嵌套的组件提供所需要的变量和方法provide() {return {message: '我是祖先组件提供的数据',name: this.name, // 传递属性say: this.say}}
}

子组件通过使用inject:[“参数1”,”参数2”,…]接收父组件传递的参数

<template><p>曾孙组件</p><p>{{message}}</p>
</template>
<script>
export default {// inject 注入/接收祖先组件传递的所需要的数据即可 //接收到的数据 变量 跟data里面的变量一样 可以直接绑定到页面 {{}}inject: [ "message","say"],mounted() {this.say();},
};
</script>

3. $parent + $children 获取父组件实例和子组件实例的集合

  • this.$parent 可以直接访问该组件的父实例或组件
  • 父组件也可以通过 this.$children 访问它所有的子组件;需要注意 $children 并不保证顺序,也不是响应式的
<!-- parent.vue -->
<template>
<div><child1></child1>   <child2></child2> <button @click="clickChild">$children方式获取子组件值</button>
</div>
</template>
<script>
import child1 from './child1'
import child2 from './child2'
export default {data(){return {total: 108}},components: {child1,child2  },methods: {funa(e){console.log("index",e)},clickChild(){console.log(this.$children[0].msg);console.log(this.$children[1].msg);}}
}
</script>
<!-- child1.vue -->
<template><div><button @click="parentClick">点击访问父组件</button></div>
</template>
<script>
export default {data(){return {msg:"child1"}},methods: {// 访问父组件数据parentClick(){this.$parent.funa("xx")console.log(this.$parent.total);}}
}
</script>
<!-- child2.vue -->
<template><div>child2</div>
</template>
<script>
export default {data(){return {msg: 'child2'}}
}
</script>

4. $attrs + $listeners多级组件通信

$attrs 包含了从父组件传过来的所有props属性

// 父组件Parent.vue:
<Child :name="name" :age="age"/>// 子组件Child.vue:
<GrandChild v-bind="$attrs" />// 孙子组件GrandChild
<p>姓名:{{$attrs.name}}</p>
<p>年龄:{{$attrs.age}}</p>

$listeners包含了父组件监听的所有事件

// 父组件Parent.vue:
<Child :name="name" :age="age" @changeNameFn="changeName"/>// 子组件Child.vue:
<button @click="$listeners.changeNameFn"></button>

5. ref 父子组件通信

// 父组件Parent.vue:
<Child ref="childComp"/>
<button @click="changeName"></button>
changeName(){console.log(this.$refs.childComp.age);this.$refs.childComp.changeAge()
}// 子组件Child.vue:
data(){return{age:20}
},
methods(){changeAge(){this.age=15}
}

6. 非父子, 兄弟组件之间通信

vue2中废弃了broadcast广播和分发事件的方法。父子组件中可以用props$emit()。如何实现非父子组件间的通信,可以通过实例一个vue实例Bus作为媒介,要相互通信的兄弟组件之中,都引入Bus,然后通过分别调用Bus事件触发和监听来实现通信和参数传递。Bus.js可以是这样:

// Bus.js// 创建一个中央时间总线类  
class Bus {  constructor() {  this.callbacks = {};   // 存放事件的名字  }  $on(name, fn) {  this.callbacks[name] = this.callbacks[name] || [];  this.callbacks[name].push(fn);  }  $emit(name, args) {  if (this.callbacks[name]) {  this.callbacks[name].forEach((cb) => cb(args));  }  }  
}  // main.js  
Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上  
// 另一种方式  
Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能  
<template><button @click="toBus">子组件传给兄弟组件</button>
</template><script>
export default{methods: {toBus () {this.$bus.$emit('foo', '来自兄弟组件')}}
}
</script>

另一个组件也在钩子函数中监听on事件

export default {data() {return {message: ''}},mounted() {this.$bus.$on('foo', (msg) => {this.message = msg})}
}

7. $root 访问根组件中的属性或方法

  • 作用:访问根组件中的属性或方法
  • 注意:是根组件,不是父组件。$root只对根组件有用
var vm = new Vue({el: "#app",data() {return {rootInfo:"我是根元素的属性"}},methods: {alerts() {alert(111)}},components: {com1: {data() {return {info: "组件1"}},template: "<p>{{ info }} <com2></com2></p>",components: {com2: {template: "<p>我是组件1的子组件</p>",created() {this.$root.alerts()// 根组件方法console.log(this.$root.rootInfo)// 我是根元素的属性}}}}}
});

8. vuex

  • 适用场景: 复杂关系的组件数据传递
  • Vuex作用相当于一个用来存储共享变量的容器

7-vue-1

  • state用来存放共享变量的地方
  • getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值
  • mutations用来存放修改state的方法。
  • actions也是用来存放修改state的方法,不过action是在mutations的基础上进行。常用来做一些异步操作

小结

  • 父子关系的组件数据传递选择 props$emit进行传递,也可选择ref
  • 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传递
  • 祖先与后代组件数据传递可选择attrslisteners或者 ProvideInject
  • 复杂关系的组件数据传递可以通过vuex存放共享的变量

----------@----------

组件中写name属性的好处

可以标识组件的具体名称方便调试和查找对应属性

// 源码位置 src/core/global-api/extend.js// enable recursive self-lookup
if (name) { Sub.options.components[name] = Sub // 记录自己 在组件中递归自己  -> jsx
}

----------@----------

Vue.extend 作用和原理

官方解释:Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并

  • extend是构造一个组件的语法器。然后这个组件你可以作用到Vue.component这个全局注册方法里还可以在任意vue模板里使用组件。 也可以作用到vue实例或者某个组件中的components属性中并在内部使用apple组件。
  • Vue.component你可以创建 ,也可以取组件。

相关代码如下

export default function initExtend(Vue) {let cid = 0; //组件的唯一标识// 创建子类继承Vue父类 便于属性扩展Vue.extend = function (extendOptions) {// 创建子类的构造函数 并且调用初始化方法const Sub = function VueComponent(options) {this._init(options); //调用Vue初始化方法};Sub.cid = cid++;Sub.prototype = Object.create(this.prototype); // 子类原型指向父类Sub.prototype.constructor = Sub; //constructor指向自己Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的optionsreturn Sub;};
}

----------@----------

Vue中如何扩展一个组件

此题属于实践题,考察大家对vue常用api使用熟练度,答题时不仅要列出这些解决方案,同时最好说出他们异同

答题思路:

  • 按照逻辑扩展和内容扩展来列举
    • 逻辑扩展有:mixinsextendscomposition api
    • 内容扩展有slots
  • 分别说出他们使用方法、场景差异和问题。
  • 作为扩展,还可以说说vue3中新引入的composition api带来的变化

回答范例:

  1. 常见的组件扩展方法有:mixinsslotsextends
  2. 混入mixins是分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项
// 复用代码:它是一个配置对象,选项和组件里面一样
const mymixin = {methods: {dosomething(){}}
}
// 全局混入:将混入对象传入
Vue.mixin(mymixin)// 局部混入:做数组项设置到mixins选项,仅作用于当前组件
const Comp = {mixins: [mymixin]
}
  1. 插槽主要用于vue组件中的内容分发,也可以用于组件扩展

子组件Child

<div><slot>这个内容会被父组件传递的内容替换</slot>
</div>

父组件Parent

<div><Child>来自父组件内容</Child>
</div>

如果要精确分发到不同位置可以使用具名插槽,如果要使用子组件中的数据可以使用作用域插槽

  1. 组件选项中还有一个不太常用的选项extends,也可以起到扩展组件的目的
// 扩展对象
const myextends = {methods: {dosomething(){}}
}
// 组件扩展:做数组项设置到extends选项,仅作用于当前组件
// 跟混入的不同是它只能扩展单个对象
// 另外如果和混入发生冲突,该选项优先级较高,优先起作用
const Comp = {extends: myextends
}
  1. 混入的数据和方法不能明确判断来源且可能和当前组件内变量产生命名冲突,vue3中引入的composition api,可以很好解决这些问题,利用独立出来的响应式模块可以很方便的编写独立逻辑并提供响应式的数据,然后在setup选项中组合使用,增强代码的可读性和维护性。例如
// 复用逻辑1
function useXX() {}
// 复用逻辑2
function useYY() {}
// 逻辑组合
const Comp = {setup() {const {xx} = useXX()const {yy} = useYY()return {xx, yy}}
}

----------@----------

子组件可以直接改变父组件的数据么,说明原因

这是一个实践知识点,组件化开发过程中有个单项数据流原则,不在子组件中修改父组件是个常识问题

思路

  • 讲讲单项数据流原则,表明为何不能这么做
  • 举几个常见场景的例子说说解决方案
  • 结合实践讲讲如果需要修改父组件状态应该如何做

回答范例

  1. 所有的 prop 都使得其父子之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。另外,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器控制台中发出警告
const props = defineProps(['foo'])
// ❌ 下面行为会被警告, props是只读的!
props.foo = 'bar'
  1. 实际开发过程中有两个场景会想要修改一个属性:

这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data,并将这个 prop 用作其初始值:

const props = defineProps(['initialCounter'])
const counter = ref(props.initialCounter)

这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性:

const props = defineProps(['size'])
// prop变化,计算属性自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())
  1. 实践中如果确实想要改变父组件属性应该emit一个事件让父组件去做这个变更。注意虽然我们不能直接修改一个传入的对象或者数组类型的prop,但是我们还是能够直接改内嵌的对象或属性

----------@----------

什么是递归组件?举个例子说明下?

分析

递归组件我们用的比较少,但是在TreeMenu这类组件中会被用到。

体验

组件通过组件名称引用它自己,这种情况就是递归组件

<template><li><div> {{ model.name }}</div><ul v-show="isOpen" v-if="isFolder"><!-- 注意这里:组件递归渲染了它自己 --><TreeItemclass="item"v-for="model in model.children":model="model"></TreeItem></ul></li>
<script>
export default {name: 'TreeItem',// ...
}
</script>

回答范例

  1. 如果某个组件通过组件名称引用它自己,这种情况就是递归组件。
  2. 实际开发中类似TreeMenu这类组件,它们的节点往往包含子节点,子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。
  3. 使用递归组件时,由于我们并未也不能在组件内部导入它自己,所以设置组件name属性,用来查找组件定义,如果使用SFC,则可以通过SFC文件名推断。组件内部通常也要有递归结束条件,比如model.children这样的判断。
  4. 查看生成渲染函数可知,递归组件查找时会传递一个布尔值给resolveComponent,这样实际获取的组件就是当前组件本身

原理

递归组件编译结果中,获取组件时会传递一个标识符 _resolveComponent("Comp", true)

const _component_Comp = _resolveComponent("Comp", true)

就是在传递maybeSelfReference

export function resolveComponent(name: string,maybeSelfReference?: boolean
): ConcreteComponent | string {return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name
}

resolveAsset中最终返回的是组件自身:

if (!res && maybeSelfReference) {// fallback to implicit self-referencereturn Component
}

----------@----------

Vue中组件和插件有什么区别

1. 组件是什么

组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue中每一个.vue文件都可以视为一个组件

组件的优势

  • 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现
  • 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单
  • 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级

2. 插件是什么

插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:

  • 添加全局方法或者属性。如: vue-custom-element
  • 添加全局资源:指令/过滤器/过渡等。如 vue-touch
  • 通过全局混入来添加一些组件选项。如vue-router
  • 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
  • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如vue-router

3. 两者的区别

两者的区别主要表现在以下几个方面:

  • 编写形式
  • 注册形式
  • 使用场景

3.1 编写形式

编写组件

编写一个组件,可以有很多方式,我们最常见的就是vue单文件的这种格式,每一个.vue文件我们都可以看成是一个组件

vue文件标准格式

<template>
</template>
<script>
export default{ ...
}
</script>
<style>
</style>

我们还可以通过template属性来编写一个组件,如果组件内容多,我们可以在外部定义template组件内容,如果组件内容并不多,我们可直接写在template属性上

<template id="testComponent">     // 组件显示的内容<div>component!</div>   
</template>Vue.component('componentA',{ template: '#testComponent'  template: `<div>component</div>`  // 组件内容少可以通过这种形式
})

编写插件

vue插件的实现应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象

MyPlugin.install = function (Vue, options) {// 1. 添加全局方法或 propertyVue.myGlobalMethod = function () {// 逻辑...}// 2. 添加全局资源Vue.directive('my-directive', {bind (el, binding, vnode, oldVnode) {// 逻辑...}...})// 3. 注入组件选项Vue.mixin({created: function () {// 逻辑...}...})// 4. 添加实例方法Vue.prototype.$myMethod = function (methodOptions) {// 逻辑...}
}

3.2 注册形式

组件注册

vue组件注册主要分为全局注册局部注册

全局注册通过Vue.component方法,第一个参数为组件的名称,第二个参数为传入的配置项

Vue.component('my-component-name', { /* ... */ })

局部注册只需在用到的地方通过components属性注册一个组件

const component1 = {...} // 定义一个组件export default {components:{component1   // 局部注册}
}

插件注册

插件的注册通过Vue.use()的方式进行注册(安装),第一个参数为插件的名字,第二个参数是可选择的配置项

Vue.use(插件名字,{ /* ... */} )

注意的是:

注册插件的时候,需要在调用 new Vue() 启动应用之前完成

Vue.use会自动阻止多次注册相同插件,只会注册一次

4. 使用场景

  • 组件 (Component) 是用来构成你的 App 的业务模块,它的目标是 App.vue
  • 插件 (Plugin) 是用来增强你的技术栈的功能模块,它的目标是 Vue 本身

简单来说,插件就是指对Vue的功能的增强或补充

----------@----------

为什么Vue采用异步渲染

Vue 是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能, Vue 会在本轮数据更新后,在异步更新视图。核心思想 nextTick

7-vue-1

源码相关

dep.notify() 通知 watcher进行更新, subs[i].update 依次调用 watcherupdatequeueWatcherwatcher 去重放入队列, nextTickflushSchedulerQueue )在下一tick中刷新watcher队列(异步)

update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this); // 当数据发生变化时会将watcher放到一个队列中批量更新 }
}export function queueWatcher (watcher: Watcher) { const id = watcher.id // 会对相同的watcher进行过滤 if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- }queue.splice(i + 1, 0, watcher) }// queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return }nextTick(flushSchedulerQueue) // 调用nextTick方法 批量的进行更新 } } 
}

----------@----------

v-if和v-show区别

  • v-show隐藏则是为该元素添加css--display:nonedom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除
  • 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换
  • 编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染
  • v-showfalse变为true的时候不会触发组件的生命周期
  • v-iffalse变为true的时候,触发组件的beforeCreatecreatebeforeMountmounted钩子,由true变为false的时候触发组件的beforeDestorydestoryed方法
  • 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗

v-show与v-if的使用场景

  • v-ifv-show 都能控制dom元素在页面的显示
  • v-if 相比 v-show 开销更大的(直接操作dom节点增加与删除)
  • 如果需要非常频繁地切换,则使用 v-show 较好
  • 如果在运行时条件很少改变,则使用 v-if 较好

v-show与v-if原理分析

  1. v-show原理

不管初始条件是什么,元素总是会被渲染

我们看一下在vue中是如何实现的

代码很好理解,有transition就执行transition,没有就直接设置display属性

// https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.ts
export const vShow: ObjectDirective<VShowElement> = {beforeMount(el, { value }, { transition }) {el._vod = el.style.display === 'none' ? '' : el.style.displayif (transition && value) {transition.beforeEnter(el)} else {setDisplay(el, value)}},mounted(el, { value }, { transition }) {if (transition && value) {transition.enter(el)}},updated(el, { value, oldValue }, { transition }) {// ...},beforeUnmount(el, { value }) {setDisplay(el, value)}
}
  1. v-if原理

v-if在实现上比v-show要复杂的多,因为还有else else-if 等条件需要处理,这里我们也只摘抄源码中处理 v-if 的一小部分

返回一个node节点,render函数通过表达式的值来决定是否生成DOM

// https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(/^(if|else|else-if)$/,(node, dir, context) => {return processIf(node, dir, context, (ifNode, branch, isRoot) => {// ...return () => {if (isRoot) {ifNode.codegenNode = createCodegenNodeForBranch(branch,key,context) as IfConditionalExpression} else {// attach this branch's codegen node to the v-if root.const parentCondition = getParentCondition(ifNode.codegenNode!)parentCondition.alternate = createCodegenNodeForBranch(branch,key + ifNode.branches.length - 1,context)}}})}
)

----------@----------

v-if和v-for哪个优先级更高

  • 实践中不应该把v-forv-if放一起
  • vue2中,v-for的优先级是高于v-if,把它们放在一起,输出的渲染函数中可以看出会先执行循环再判断条件,哪怕我们只渲染列表中一小部分元素,也得在每次重渲染的时候遍历整个列表,这会比较浪费;另外需要注意的是在vue3中则完全相反,v-if的优先级高于v-for,所以v-if执行时,它调用的变量还不存在,就会导致异常
  • 通常有两种情况下导致我们这样做:
    • 为了过滤列表中的项目 (比如 v-for="user in users" v-if="user.isActive")。此时定义一个计算属性 (比如 activeUsers),让其返回过滤后的列表即可(比如users.filter(u=>u.isActive)
    • 为了避免渲染本应该被隐藏的列表 (比如 v-for="user in users" v-if="shouldShowUsers")。此时把 v-if 移动至容器元素上 (比如 ulol)或者外面包一层template即可
  • 文档中明确指出永远不要把 v-ifv-for 同时用在同一个元素上,显然这是一个重要的注意事项
  • 源码里面关于代码生成的部分,能够清晰的看到是先处理v-if还是v-for,顺序上vue2vue3正好相反,因此产生了一些症状的不同,但是不管怎样都是不能把它们写在一起的

vue2.x源码分析

在vue模板编译的时候,会将指令系统转化成可执行的render函数

编写一个p标签,同时使用v-ifv-for

<div id="app"><p v-if="isShow" v-for="item in items">{{ item.title }}</p>
</div>

创建vue实例,存放isShowitems数据

const app = new Vue({el: "#app",data() {return {items: [{ title: "foo" },{ title: "baz" }]}},computed: {isShow() {return this.items && this.items.length > 0}}
})

模板指令的代码都会生成在render函数中,通过app.$options.render就能得到渲染函数

ƒ anonymous() {with (this) { return _c('div', { attrs: { "id": "app" } }, _l((items), function (item) { return (isShow) ? _c('p', [_v("\\n" + _s(item.title) + "\\n")]) : _e() }), 0) }
}
  • _lvue的列表渲染函数,函数内部都会进行一次if判断
  • 初步得到结论:v-for优先级是比v-if高
  • 再将v-forv-if置于不同标签
<div id="app"><template v-if="isShow"><p v-for="item in items">{{item.title}}</p></template>
</div>

再输出下render函数

ƒ anonymous() {with(this){return _c('div',{attrs:{"id":"app"}},[(isShow)?[_v("\\n"),_l((items),function(item){return _c('p',[_v(_s(item.title))])})]:_e()],2)}
}

这时候我们可以看到,v-forv-if作用在不同标签时候,是先进行判断,再进行列表的渲染

我们再在查看下vue源码

源码位置:\\vue-dev\\src\\compiler\\codegen\\index.js

export function genElement (el: ASTElement, state: CodegenState): string {if (el.parent) {el.pre = el.pre || el.parent.pre}if (el.staticRoot && !el.staticProcessed) {return genStatic(el, state)} else if (el.once && !el.onceProcessed) {return genOnce(el, state)} else if (el.for && !el.forProcessed) {return genFor(el, state)} else if (el.if && !el.ifProcessed) {return genIf(el, state)} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {return genChildren(el, state) || 'void 0'} else if (el.tag === 'slot') {return genSlot(el, state)} else {// component or element...
}

在进行if判断的时候,v-for是比v-if先进行判断

最终结论:v-for优先级比v-if

----------@----------

Vue的事件绑定原理

原生事件绑定是通过 addEventListener 绑定给真实元素的,组件事件绑定是通过 Vue 自定义的$on 实现的。如果要在组件上使用原生事件,需要加.native 修饰符,这样就相当于在父组件中把子组件当做普通 html 标签,然后加上原生事件。

$on$emit 是基于发布订阅模式的,维护一个事件中心,on 的时候将事件按名称存在事件中心里,称之为订阅者,然后 emit 将对应的事件进行发布,去执行事件中心里的对应的监听器

EventEmitter(发布订阅模式–简单版)

// 手写发布订阅模式 EventEmitter
class EventEmitter {constructor() {this.events = {};}// 实现订阅on(type, callBack) {if (!this.events) this.events = Object.create(null);if (!this.events[type]) {this.events[type] = [callBack];} else {this.events[type].push(callBack);}}// 删除订阅off(type, callBack) {if (!this.events[type]) return;this.events[type] = this.events[type].filter(item => {return item !== callBack;});}// 只执行一次订阅事件once(type, callBack) {function fn() {callBack();this.off(type, fn);}this.on(type, fn);}// 触发事件emit(type, ...rest) {this.events[type] && this.events[type].forEach(fn => fn.apply(this, rest));}
}// 使用如下
const event = new EventEmitter();const handle = (...rest) => {console.log(rest);
};event.on("click", handle);event.emit("click", 1, 2, 3, 4);event.off("click", handle);event.emit("click", 1, 2);event.once("dbClick", () => {console.log(123456);
});
event.emit("dbClick");
event.emit("dbClick");

源码分析

7-vue-1

  1. 原生 dom 的绑定
  • Vue 在创建真是 dom 时会调用 createElm ,默认会调用 invokeCreateHooks
  • 会遍历当前平台下相对的属性处理代码,其中就有 updateDOMListeners 方法,内部会传入 add 方法
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) { if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) { return }const on = vnode.data.on || {} const oldOn = oldVnode.data.on || {} target = vnode.elm normalizeEvents(on) updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context) target = undefined 
}
function add ( name: string, handler: Function, capture: boolean, passive: boolean ) {target.addEventListener( // 给当前的dom添加事件 name, handler, supportsPassive ? { capture, passive } : capture ) 
}

vue 中绑定事件是直接绑定给真实 dom 元素的

  1. 组件中绑定事件
export function updateComponentListeners ( vm: Component, listeners: Object, oldListeners: ?Object ) {target = vm updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)target = undefined 
}
function add (event, fn) { target.$on(event, fn) 
}

组件绑定事件是通过 vue 中自定义的 $on 方法来实现的

----------@----------

Vue 是如何实现数据双向绑定的

Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:

7-vue-1

  • 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。
  • Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化

Vue 主要通过以下 4 个步骤来实现数据双向绑定的

  • 实现一个监听器 Observer :对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 settergetter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化
  • 实现一个解析器 Compile :解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新
  • 实现一个订阅者 WatcherWatcher 订阅者是 ObserverCompile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数
  • 实现一个订阅器 Dep :订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理

7-vue-1

Vue 数据双向绑定原理图

7-vue-1

----------@----------

双向绑定的原理是什么

我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成

  • 数据层(Model):应用的数据及业务逻辑
  • 视图层(View):应用的展示效果,各类UI组件
  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来

而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理

理解ViewModel

它的主要职责就是:

  • 数据变化后更新视图
  • 视图变化后更新数据

当然,它还有两个主要部分组成

  • 监听器(Observer):对所有数据的属性进行监听
  • 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

----------@----------

实现双向绑定

我们还是以Vue为例,先来看看Vue中的双向绑定流程是什么的

  1. new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe
  2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile
  3. 同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
  4. 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher
  5. 将来data中数据⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

流程图如下:

7-vue-1

先来一个构造函数:执行初始化,对data执行响应化处理

class Vue {  constructor(options) {  this.$options = options;  this.$data = options.data;  // 对data选项做响应式处理  observe(this.$data);  // 代理data到vm上  proxy(this);  // 执行编译  new Compile(options.el, this);  }  
}  

data选项执行响应化具体操作

function observe(obj) {  if (typeof obj !== "object" || obj == null) {  return;  }  new Observer(obj);  
}  class Observer {  constructor(value) {  this.value = value;  this.walk(value);  }  walk(obj) {  Object.keys(obj).forEach((key) => {  defineReactive(obj, key, obj[key]);  });  }  
}  

编译Compile

对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

7-vue-1

class Compile {  constructor(el, vm) {  this.$vm = vm;  this.$el = document.querySelector(el);  // 获取dom  if (this.$el) {  this.compile(this.$el);  }  }  compile(el) {  const childNodes = el.childNodes;   Array.from(childNodes).forEach((node) => { // 遍历子元素  if (this.isElement(node)) {   // 判断是否为节点  console.log("编译元素" + node.nodeName);  } else if (this.isInterpolation(node)) {  console.log("编译插值⽂本" + node.textContent);  // 判断是否为插值文本 {{}}  }  if (node.childNodes && node.childNodes.length > 0) {  // 判断是否有子元素  this.compile(node);  // 对子元素进行递归遍历  }  });  }  isElement(node) {  return node.nodeType == 1;  }  isInterpolation(node) {  return node.nodeType == 3 && /\\{\\{(.*)\\}\\}/.test(node.textContent);  }  
}  

依赖收集

视图中会用到data中某key,这称为依赖。同⼀个key可能出现多次,每次都需要收集出来用⼀个Watcher来维护它们,此过程称为依赖收集多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知

7-vue-1

实现思路

  1. defineReactive时为每⼀个key创建⼀个Dep实例
  2. 初始化视图时读取某个key,例如name1,创建⼀个watcher1
  3. 由于触发name1getter方法,便将watcher1添加到name1对应的Dep
  4. name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新
// 负责更新视图  
class Watcher {  constructor(vm, key, updater) {  this.vm = vm  this.key = key  this.updaterFn = updater  // 创建实例时,把当前实例指定到Dep.target静态属性上  Dep.target = this  // 读一下key,触发get  vm[key]  // 置空  Dep.target = null  }  // 未来执行dom更新函数,由dep调用的  update() {  this.updaterFn.call(this.vm, this.vm[this.key])  }  
}  

声明Dep

class Dep {  constructor() {  this.deps = [];  // 依赖管理  }  addDep(dep) {  this.deps.push(dep);  }  notify() {   this.deps.forEach((dep) => dep.update());  }  
} 

创建watcher时触发getter

class Watcher {  constructor(vm, key, updateFn) {  Dep.target = this;  this.vm[this.key];  Dep.target = null;  }  
}  

依赖收集,创建Dep实例

function defineReactive(obj, key, val) {  this.observe(val);  const dep = new Dep();  Object.defineProperty(obj, key, {  get() {  Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例  return val;  },  set(newVal) {  if (newVal === val) return;  dep.notify(); // 通知dep执行更新方法  },  });  
}  

----------@----------

v-model实现原理

我们在 vue 项目中主要使用 v-model 指令在表单 inputtextareaselect 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖(可以看成是value + input方法的语法糖),v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:

  • texttextarea 元素使用 value 属性和 input 事件
  • checkboxradio 使用 checked 属性和 change 事件
  • select 字段将 value 作为 prop 并将 change 作为事件

所以我们可以v-model进行如下改写:

<input v-model="sth" />
<!-- 等同于 -->
<input :value="sth" @input="sth = $event.target.value" />

当在input元素中使用v-model实现双数据绑定,其实就是在输入的时候触发元素的input事件,通过这个语法糖,实现了数据的双向绑定

  • 这个语法糖必须是固定的,也就是说属性必须为value,方法名必须为:input
  • 知道了v-model的原理,我们可以在自定义组件上实现v-model
//Parent
<template>{{num}}<Child v-model="num">
</template>
export default {data(){return {num: 0}}
}//Child
<template><div @click="add">Add</div>
</template>
export default {props: ['value'], // 属性必须为valuemethods:{add(){// 方法名为inputthis.$emit('input', this.value + 1)}}
}

原理

会将组件的 v-model 默认转化成value+input

const VueTemplateCompiler = require('vue-template-compiler'); 
const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></el- checkbox>'); // 观察输出的渲染函数:
// with(this) { 
//     return _c('el-checkbox', { 
//         model: { 
//             value: (check), 
//             callback: function ($$v) { check = $$v }, 
//             expression: "check" 
//         } 
//     }) 
// }
// 源码位置 core/vdom/create-component.js line:155function transformModel (options, data: any) { const prop = (options.model && options.model.prop) || 'value' const event = (options.model && options.model.event) || 'input' ;(data.attrs || (data.attrs = {}))[prop] = data.model.value const on = data.on || (data.on = {}) const existing = on[event] const callback = data.model.callback if (isDef(existing)) { if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) {on[event] = [callback].concat(existing) } } else { on[event] = callback } 
}

原生的 v-model,会根据标签的不同生成不同的事件和属性

const VueTemplateCompiler = require('vue-template-compiler'); 
const ele = VueTemplateCompiler.compile('<input v-model="value"/>');// with(this) { 
//     return _c('input', { 
//         directives: [{ name: "model", rawName: "v-model", value: (value), expression: "value" }], 
//         domProps: { "value": (value) },
//         on: {"input": function ($event) { 
//             if ($event.target.composing) return;
//             value = $event.target.value
//         }
//         }
//     })
// }

编译时:不同的标签解析出的内容不一样 platforms/web/compiler/directives/model.js

if (el.component) { genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime return false 
} else if (tag === 'select') { genSelect(el, value, modifiers) 
} else if (tag === 'input' && type === 'checkbox') { genCheckboxModel(el, value, modifiers) 
} else if (tag === 'input' && type === 'radio') { genRadioModel(el, value, modifiers) 
} else if (tag === 'input' || tag === 'textarea') { genDefaultModel(el, value, modifiers) 
} else if (!config.isReservedTag(tag)) { genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime return false 
}

运行时:会对元素处理一些关于输入法的问题 platforms/web/runtime/directives/model.js

inserted (el, binding, vnode, oldVnode) { if (vnode.tag === 'select') { // #6903 if (oldVnode.elm && !oldVnode.elm._vOptions) { mergeVNodeHook(vnode, 'postpatch', () => { directive.componentUpdated(el, binding, vnode) }) } else { setSelected(el, binding, vnode.context) }el._vOptions = [].map.call(el.options, getValue) } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) { el._vModifiers = binding.modifiers if (!binding.modifiers.lazy) { el.addEventListener('compositionstart', onCompositionStart) el.addEventListener('compositionend', onCompositionEnd) // Safari < 10.2 & UIWebView doesn't fire compositionend when // switching focus before confirming composition choice // this also fixes the issue where some browsers e.g. iOS Chrome// fires "change" instead of "input" on autocomplete. el.addEventListener('change', onCompositionEnd) /* istanbul ignore if */ if (isIE9) { el.vmodel = true }}}
}

----------@----------

Vue中修饰符.sync与v-model的区别

sync的作用

  • .sync修饰符可以实现父子组件之间的双向绑定,并且可以实现子组件同步修改父组件的值,相比较与v-model来说,sync修饰符就简单很多了
  • 一个组件上可以有多个.sync修饰符
<!-- 正常父传子 -->
<Son :a="num" :b="num2" /><!-- 加上sync之后的父传子 -->
<Son :a.sync="num" :b.sync="num2" /><!-- 它等价于 -->
<Son :a="num" :b="num2" @update:a="val=>num=val" @update:b="val=>num2=val" 
/><!-- 相当于多了一个事件监听,事件名是update:a, -->
<!-- 回调函数中,会把接收到的值赋值给属性绑定的数据项中。 -->

7-vue-1

v-model的工作原理

<com1 v-model="num"></com1>
<!-- 等价于 -->
<com1 :value="num" @input="(val)=>num=val"></com1>
  • 相同点
    • 都是语法糖,都可以实现父子组件中的数据的双向通信
  • 区别点
    • 格式不同:v-model="num", :num.sync="num"
    • v-model: @input + value
    • :num.sync: @update:num
    • v-model只能用一次;.sync可以有多个

----------@----------

什么是作用域插槽

插槽

  • 创建组件虚拟节点时,会将组件儿子的虚拟节点保存起来。当初始化组件时,通过插槽属性将儿子进行分类{a:[vnode],b[vnode]}
  • 渲染组件时会拿对应的 slot 属性的节点进行替换操作。(插槽的作用域为父组件)
<app><div slot="a">xxxx</div><div slot="b">xxxx</div>
</app> slot name="a" 
slot name="b"

作用域插槽

  • 作用域插槽在解析的时候不会作为组件的孩子节点。会解析成函数,当子组件渲染时,会调用此函数进行渲染。(插槽的作用域为子组件)
  • 普通插槽渲染的作用域是父组件,作用域插槽的渲染作用域是当前子组件。

7-vue-1

// 插槽const VueTemplateCompiler = require('vue-template-compiler'); 
let ele = VueTemplateCompiler.compile(` <my-component> <div slot="header">node</div> <div>react</div> <div slot="footer">vue</div> </my-component> `
)// with(this) { 
//     return _c('my-component', [_c('div', { 
//         attrs: { "slot": "header" },
//         slot: "header" 
//     }, [_v("node")] // _文本及诶点 )
//     , _v(" "), 
//     _c('div', [_v("react")]), _v(" "), _c('div', { 
//         attrs: { "slot": "footer" },
//         slot: "footer" }, [_v("vue")])]) 
// }const VueTemplateCompiler = require('vue-template-compiler');
let ele = VueTemplateCompiler.compile(` <div><slot name="header"></slot> <slot name="footer"></slot> <slot></slot> </div> `
);with(this) { return _c('div', [_v("node"), _v(" "), _t(_v("vue")])]), _v(" "), _t("default")], 2) 
}
//  _t定义在 core/instance/render-helpers/index.js
// 作用域插槽:
let ele = VueTemplateCompiler.compile(` <app><div slot-scope="msg" slot="footer">{{msg.a}}</div> </app> `
);// with(this) { 
//     return _c('app', { scopedSlots: _u([{ 
//         // 作用域插槽的内容会被渲染成一个函数 
//         key: "footer", 
//         fn: function (msg) { 
//             return _c('div', {}, [_v(_s(msg.a))]) } }]) 
//         })
//     } 
// }const VueTemplateCompiler = require('vue-template-compiler');
VueTemplateCompiler.compile(` <div><slot name="footer" a="1" b="2"></slot> </div> `);// with(this) { return _c('div', [_t("footer", null, { "a": "1", "b": "2" })], 2) }

----------@----------

keep-alive 使用场景和原理

  • keep-aliveVue 内置的一个组件, 可以实现组件缓存 ,当组件切换时不会对当前组件进行卸载。 一般结合路由和动态组件一起使用 ,用于缓存组件
  • 提供 includeexclude 属性, 允许组件有条件的进行缓存 。两者都支持字符串或正则表达式,include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include
  • 对应两个钩子函数 activateddeactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated
  • keep-alive 的中还运用了 LRU(最近最少使用) 算法,选择最近最久未使用的组件予以淘汰
  • <keep-alive></keep-alive> 包裹动态组件时,会缓存不活动的组件实例,主要用于保留组件状态或避免重新渲染
  • 比如有一个列表和一个详情,那么用户就会经常执行打开详情=>返回列表=>打开详情…这样的话列表和详情都是一个频率很高的页面,那么就可以对列表组件使用<keep-alive></keep-alive>进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是重新渲染

关于keep-alive的基本用法

<keep-alive><component :is="view"></component>
</keep-alive>

使用includesexclude

<keep-alive include="a,b"><component :is="view"></component>
</keep-alive><!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/"><component :is="view"></component>
</keep-alive><!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']"><component :is="view"></component>
</keep-alive>

匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值),匿名组件不能被匹配

设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activateddeactivated):

  • 首次进入组件时:beforeRouteEnter > beforeCreate > created> mounted > activated > … … > beforeRouteLeave > deactivated
  • 再次进入组件时:beforeRouteEnter >activated > … … > beforeRouteLeave > deactivated

使用场景

使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用keepalive

举个栗子:

当我们从首页–>列表页–>商详页–>再返回,这时候列表页应该是需要keep-alive

首页–>列表页–>商详页–>返回到列表页(需要缓存)–>返回到首页(需要缓存)–>再次进入列表页(不需要缓存),这时候可以按需来控制页面的keep-alive

在路由中设置keepAlive属性判断是否需要缓存

{path: 'list',name: 'itemList', // 列表页component (resolve) {require(['@/pages/item/list'], resolve)},meta: {keepAlive: true,title: '列表页'}
}

使用<keep-alive>

<div id="app" class='wrapper'><keep-alive><!-- 需要缓存的视图组件 --> <router-view v-if="$route.meta.keepAlive"></router-view></keep-alive><!-- 不需要缓存的视图组件 --><router-view v-if="!$route.meta.keepAlive"></router-view>
</div>

思考题:缓存后如何获取数据

解决方案可以有以下两种:

  • beforeRouteEnter:每次组件渲染的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){next(vm=>{console.log(vm)// 每次进入路由执行vm.getData()  // 获取数据})
},
  • actived:在keep-alive缓存的组件被激活的时候,都会执行actived钩子
// 注意:服务器端渲染期间avtived不被调用
activated(){this.getData() // 获取数据
},

扩展补充:LRU 算法是什么?

7-vue-1

LRU 的核心思想是如果数据最近被访问过,那么将来被访问的几率也更高,所以我们将命中缓存的组件 key 重新插入到 this.keys 的尾部,这样一来,this.keys 中越往头部的数据即将来被访问几率越低,所以当缓存数量达到最大值时,我们就删除将来被访问几率最低的数据,即 this.keys 中第一个缓存的组件

相关代码

keep-alivevue中内置的一个组件

源码位置:src/core/components/keep-alive.js

export default {name: "keep-alive",abstract: true, //抽象组件props: {include: patternTypes, //要缓存的组件exclude: patternTypes, //要排除的组件max: [String, Number], //最大缓存数},created() {this.cache = Object.create(null); //缓存对象  {a:vNode,b:vNode}this.keys = []; //缓存组件的key集合 [a,b]},destroyed() {for (const key in this.cache) {pruneCacheEntry(this.cache, key, this.keys);}},mounted() {//动态监听include  excludethis.$watch("include", (val) => {pruneCache(this, (name) => matches(val, name));});this.$watch("exclude", (val) => {pruneCache(this, (name) => !matches(val, name));});},render() {const slot = this.$slots.default; //获取包裹的插槽默认值 获取默认插槽中的第一个组件节点const vnode: VNode = getFirstComponentChild(slot); //获取第一个子组件// 获取该组件节点的componentOptionsconst componentOptions: ?VNodeComponentOptions =vnode && vnode.componentOptions;if (componentOptions) {// 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tagconst name: ?string = getComponentName(componentOptions);const { include, exclude } = this;// 不走缓存 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnodeif (// not included  不包含(include && (!name || !matches(include, name))) ||// excluded  排除里面(exclude && name && matches(exclude, name))) {//返回虚拟节点return vnode;}const { cache, keys } = this;// 获取组件的key值const key: ?string =vnode.key == null? // same constructor may get registered as different local components// so cid alone is not enough (#3269)componentOptions.Ctor.cid +(componentOptions.tag ? `::${componentOptions.tag}` : ""): vnode.key;// 拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存if (cache[key]) {//通过key 找到缓存 获取实例vnode.componentInstance = cache[key].componentInstance;// make current key freshestremove(keys, key); //通过LRU算法把数组里面的key删掉keys.push(key); //把它放在数组末尾} else {cache[key] = vnode; //没找到就换存下来keys.push(key); //把它放在数组末尾// prune oldest entry  //如果超过最大值就把数组第0项删掉if (this.max && keys.length > parseInt(this.max)) {pruneCacheEntry(cache, keys[0], keys, this._vnode);}}vnode.data.keepAlive = true; //标记虚拟节点已经被缓存}// 返回虚拟节点return vnode || (slot && slot[0]);},
};

可以看到该组件没有template,而是用了render,在组件渲染的时候会自动执行render函数

this.cache是一个对象,用来存储需要缓存的组件,它将以如下形式存储:

this.cache = {'key1':'组件1','key2':'组件2',// ...
}

在组件销毁的时候执行pruneCacheEntry函数

function pruneCacheEntry (cache: VNodeCache,key: string,keys: Array<string>,current?: VNode
) {const cached = cache[key]/* 判断当前没有处于被渲染状态的组件,将其销毁*/if (cached && (!current || cached.tag !== current.tag)) {cached.componentInstance.$destroy()}cache[key] = nullremove(keys, key)
}

mounted钩子函数中观测 includeexclude 的变化,如下:

mounted () {this.$watch('include', val => {pruneCache(this, name => matches(val, name))})this.$watch('exclude', val => {pruneCache(this, name => !matches(val, name))})
}

如果includeexclude 发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache函数,函数如下

function pruneCache (keepAliveInstance, filter) {const { cache, keys, _vnode } = keepAliveInstancefor (const key in cache) {const cachedNode = cache[key]if (cachedNode) {const name = getComponentName(cachedNode.componentOptions)if (name && !filter(name)) {pruneCacheEntry(cache, key, keys, _vnode)}}}
}

在该函数内对this.cache对象进行遍历,取出每一项的name值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry函数将其从this.cache对象剔除即可

关于keep-alive的最强大缓存功能是在render函数中实现

首先获取组件的key值:

const key = vnode.key == null? 
componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key

拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存,如下:

/* 如果命中缓存,则直接从缓存中拿 vnode 的组件实例 */
if (cache[key]) {vnode.componentInstance = cache[key].componentInstance/* 调整该组件key的顺序,将其从原来的地方删掉并重新放在最后一个 */remove(keys, key)keys.push(key)
} 

直接从缓存中拿 vnode 的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys中最后一个

this.cache对象中没有该key值的情况,如下:

/* 如果没有命中缓存,则将其设置进缓存 */
else {cache[key] = vnodekeys.push(key)/* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */if (this.max && keys.length > parseInt(this.max)) {pruneCacheEntry(cache, keys[0], keys, this._vnode)}
}

表明该组件还没有被缓存过,则以该组件的key为键,组件vnode为值,将其存入this.cache中,并且把key存入this.keys

此时再判断this.keys中缓存组件的数量是否超过了设置的最大缓存数量值this.max,如果超过了,则把第一个缓存组件删掉

----------@----------

怎么缓存当前的组件?缓存后怎么更新

缓存组件使用keep-alive组件,这是一个非常常见且有用的优化手段,vue3keep-alive有比较大的更新,能说的点比较多

思路

  • 缓存用keep-alive,它的作用与用法
  • 使用细节,例如缓存指定/排除、结合routertransition
  • 组件缓存后更新可以利用activated或者beforeRouteEnter
  • 原理阐述

回答范例

  1. 开发中缓存组件使用keep-alive组件,keep-alivevue内置组件,keep-alive包裹动态组件component时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM
<keep-alive><component :is="view"></component>
</keep-alive>
  1. 结合属性includeexclude可以明确指定缓存哪些组件或排除缓存指定组件。vue3中结合vue-router时变化较大,之前是keep-alive包裹router-view,现在需要反过来用router-view包裹keep-alive
<router-view v-slot="{ Component }"><keep-alive><component :is="Component"></component></keep-alive>
</router-view>
  1. 缓存后如果要获取数据,解决方案可以有以下两种
  • beforeRouteEnter:在有vue-router的项目,每次进入路由的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){next(vm=>{console.log(vm)// 每次进入路由执行vm.getData()  // 获取数据})
},
  • actived:在keep-alive缓存的组件被激活的时候,都会执行actived钩子
activated(){this.getData() // 获取数据
},
  1. keep-alive是一个通用组件,它内部定义了一个map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的vnode,如果该组件在map中存在就直接返回它。由于componentis属性是个响应式数据,因此只要它变化,keep-aliverender函数就会重新执行

----------@----------

Vue-router基本使用

mode

  • hash
  • history

跳转

  • 编程式(js跳转)this.$router.push('/')
  • 声明式(标签跳转) <router-link to=""></router-link>

vue路由传参数

  • 使用query方法传入的参数使用this.$route.query接受
  • 使用params方式传入的参数使用this.$route.params接受

占位

<router-view></router-view>

----------@----------

vue-router 动态路由是什么

我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果

const User = {template: "<div>User</div>",
};const router = new VueRouter({routes: [// 动态路径参数 以冒号开头{ path: "/user/:id", component: User },],
});

问题: vue-router 组件复用导致路由参数失效怎么办?

解决方法:

  1. 通过 watch 监听路由参数再发请求
watch: { //通过watch来监听路由变化"$route": function(){this.getData(this.$route.params.xxx);}
}
  1. :key 来阻止“复用”
<router-view :key="$route.fullPath" />

回答范例

  1. 很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由
  2. 例如,我们可能有一个 User 组件,它应该对所有用户进行渲染,但用户 ID 不同。在 Vue Router中,我们可以在路径中使用一个动态字段来实现,例如:{ path: '/users/:id', component: User },其中:id就是路径参数
  3. 路径参数 用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.params 的形式暴露出来。
  4. 参数还可以有多个,例如/users/:username/posts/:postId;除了 $route.params 之外,$route 对象还公开了其他有用的信息,如 $route.query$route.hash

----------@----------

router-link和router-view是如何起作用的

分析

vue-router中两个重要组件router-linkrouter-view,分别起到导航作用和内容渲染作用,但是回答如何生效还真有一定难度

回答范例

  1. vue-router中两个重要组件router-linkrouter-view,分别起到路由导航作用和组件内容渲染作用
  2. 使用中router-link默认生成一个a标签,设置to属性定义跳转path。实际上也可以通过custom和插槽自定义最终的展现形式。router-view是要显示组件的占位组件,可以嵌套,对应路由配置的嵌套关系,配合name可以显示具名组件,起到更强的布局作用。
  3. router-link组件内部根据custom属性判断如何渲染最终生成节点,内部提供导航方法navigate,用户点击之后实际调用的是该方法,此方法最终会修改响应式的路由变量,然后重新去routes匹配出数组结果,router-view则根据其所处深度deep在匹配数组结果中找到对应的路由并获取组件,最终将其渲染出来。

----------@----------

Vue-router 除了 router-link 怎么实现跳转

声明式导航

<router-link to="/about">Go to About</router-link>

编程式导航

// literal string path
router.push('/users/1')// object with path
router.push({ path: '/users/1' })// named route with params to let the router build the url
router.push({ name: 'user', params: { username: 'test' } })

回答范例

  • vue-router导航有两种方式:声明式导航和编程方式导航
  • 声明式导航方式使用router-link组件,添加to属性导航;编程方式导航更加灵活,可传递调用router.push(),并传递path字符串或者RouteLocationRaw对象,指定pathnameparams等信息
  • 如果页面中简单表示跳转链接,使用router-link最快捷,会渲染一个a标签;如果页面是个复杂的内容,比如商品信息,可以添加点击事件,使用编程式导航
  • 实际上内部两者调用的导航函数是一样的

----------@----------

Vue-router 路由模式有几种

vue-router3 种路由模式:hashhistoryabstract,对应的源码如下所示

switch (mode) {case 'history':this.history = new HTML5History(this, options.base)breakcase 'hash':this.history = new HashHistory(this, options.base, this.fallback)breakcase 'abstract':this.history = new AbstractHistory(this, options.base)breakdefault:if (process.env.NODE_ENV !== 'production') {assert(false, `invalid mode: ${mode}`)}
}

其中,3 种路由模式的说明如下:

  • hash: 使用 URL hash 值来作路由,支持所有浏览器
  • history : 依赖 HTML5 History API 和服务器配置
  • abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

----------@----------

Vue路由hash模式和history模式

1. hash模式

早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL# 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search'

https://interview2.poetries.top#search

hash 路由模式的实现主要是基于下面几个特性

  • URLhash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;
  • hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制 hash 的切换;
  • 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URLhash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URLhash 值;
  • 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)
window.addEventListener("hashchange", funcRef, false);

每一次改变 hashwindow.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了

特点 :兼容性好但是不美观

2. history模式

history采用HTML5的新特性;且提供了两个新方法: pushState()replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更

window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);

这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。

history 路由模式的实现主要基于存在下面几个特性:

  • pushStaterepalceState 两个 API 来操作实现 URL 的变化 ;
  • 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);
  • history.pushState()history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。

特点 :虽然美观,但是刷新会出现 404 需要后端进行配置

----------@----------

了解history有哪些方法吗?说下它们的区别

history 这个对象在html5的时候新加入两个api history.pushState()history.repalceState() 这两个API可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。

从参数上来说:

window.history.pushState(state,title,url)
//state:需要保存的数据,这个数据在触发popstate事件时,可以在event.state里获取
//title:标题,基本没用,一般传null
//url:设定新的历史纪录的url。新的url与当前url的origin必须是一样的,否则会抛出错误。url可以时绝对路径,也可以是相对路径。
//如 当前url是 https://www.baidu.com/a/,执行history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/,
//执行history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/window.history.replaceState(state,title,url)
//与pushState 基本相同,但她是修改当前历史纪录,而 pushState 是创建新的历史纪录

另外还有:

  • window.history.back() 后退
  • window.history.forward()前进
  • window.history.go(1) 前进或者后退几步

从触发事件的监听上来说:

  • pushState()replaceState()不能被popstate事件所监听
  • 而后面三者可以,且用户点击浏览器前进后退键时也可以

----------@----------

如何监听 pushState 和 replaceState 的变化呢?

利用自定义事件new Event()创建这两个事件,并全局监听:

<body><button onclick="goPage2()">去page2</button><div>Page1</div><script>let count = 0;function goPage2 () {history.pushState({ count: count++ }, `bb${count}`,'page1.html')console.log(history)}// 这个不能监听到 pushState// window.addEventListener('popstate', function (event) {//   console.log(event)// })function createHistoryEvent (type) {var fn = history[type]return function () {// 这里的 arguments 就是调用 pushState 时的三个参数集合var res = fn.apply(this, arguments)let e = new Event(type)e.arguments = argumentswindow.dispatchEvent(e)return res}}history.pushState = createHistoryEvent('pushState')history.replaceState = createHistoryEvent('replaceState')window.addEventListener('pushState', function (event) {// { type: 'pushState', arguments: [...], target: Window, ... }console.log(event)})window.addEventListener('replaceState', function (event) {console.log(event)})</script>
</body>

----------@----------

Vue路由的钩子函数

首页可以控制导航跳转,beforeEachafterEach等,一般用于页面title的修改。一些需要登录才能调整页面的重定向功能。

  • beforeEach主要有3个参数tofromnext
  • toroute即将进入的目标路由对象。
  • fromroute当前导航正要离开的路由。
  • nextfunction一定要调用该方法resolve这个钩子。执行效果依赖next方法的调用参数。可以控制网页的跳转

----------@----------

$route$router的区别

  • $route是“路由信息对象”,包括pathparamshashqueryfullPathmatchedname等路由信息参数。
  • $router是“路由实例”对象包括了路由的跳转方法,钩子函数等

----------@----------

vue-router 路由钩子函数是什么 执行顺序是什么

路由钩子的执行流程, 钩子函数种类有:全局守卫路由守卫组件守卫

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入

----------@----------

vue-router 有哪几种导航守卫

  • 全局守卫
  • 路由独享守卫
  • 路由组件内的守卫

全局守卫

vue-router全局有三个守卫

  • router.beforeEach 全局前置守卫 进入路由之前
  • router.beforeResolve 全局解析守卫(2.5.0+) 在beforeRouteEnter调用之后调用
  • router.afterEach 全局后置钩子 进入路由之后
// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next) => { next();
});
router.beforeResolve((to, from, next) => {next();
});
router.afterEach((to, from) => {console.log('afterEach 全局后置钩子');
});

路由独享守卫

如果你不想全局配置守卫的话,你可以为某些路由单独配置守卫

const router = new VueRouter({routes: [{path: '/foo',component: Foo,beforeEnter: (to, from, next) => { // 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖// ...}}]
})

路由组件内的守卫

  • beforeRouteEnter 进入路由前, 在路由独享守卫后调用 不能 获取组件实例 this,组件实例还没被创建
  • beforeRouteUpdate (2.2) 路由复用同一个组件时, 在当前路由改变,但是该组件被复用时调用 可以访问组件实例 this
  • beforeRouteLeave 离开当前路由时, 导航离开该组件的对应路由时调用,可以访问组件实例 this

----------@----------

vue-router守卫

导航守卫 router.beforeEach 全局前置守卫

  • to: Route: 即将要进入的目标(路由对象)
  • from: Route: 当前导航正要离开的路由
  • next: Function: 一定要调用该方法来 resolve 这个钩子。(一定要用这个函数才能去到下一个路由,如果不用就拦截)
  • 执行效果依赖 next 方法的调用参数。
  • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
  • next(false):取消进入路由,url地址重置为from路由地址(也就是将要离开的路由地址)
// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next) => { next();
});
router.beforeResolve((to, from, next) => {next();
});
router.afterEach((to, from) => {console.log('afterEach 全局后置钩子');
});

路由独享的守卫 你可以在路由配置上直接定义 beforeEnter 守卫

const router = new VueRouter({routes: [{path: '/foo',component: Foo,beforeEnter: (to, from, next) => {// ...}}]
})

组件内的守卫你可以在路由组件内直接定义以下路由导航守卫

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`// 比如还未保存草稿,或者在用户离开前,将setInterval销毁,防止离开之后,定时器还在调用。}
}

----------@----------

vue-router中如何保护路由

分析

路由保护在应用开发过程中非常重要,几乎每个应用都要做各种路由权限管理,因此相当考察使用者基本功。

体验

全局守卫:

const router = createRouter({ ... })
​
router.beforeEach((to, from) => {// ...// 返回 false 以取消导航return false
})

路由独享守卫:

const routes = [{path: '/users/:id',component: UserDetails,beforeEnter: (to, from) => {// reject the navigationreturn false},},
]

组件内的守卫:

const UserDetails = {template: `...`,beforeRouteEnter(to, from) {// 在渲染该组件的对应路由被验证前调用},beforeRouteUpdate(to, from) {// 在当前路由改变,但是该组件被复用时调用},beforeRouteLeave(to, from) {// 在导航离开渲染该组件的对应路由时调用},
}

回答

  • vue-router中保护路由的方法叫做路由守卫,主要用来通过跳转或取消的方式守卫导航。
  • 路由守卫有三个级别:全局路由独享组件级。影响范围由大到小,例如全局的router.beforeEach(),可以注册一个全局前置守卫,每次路由导航都会经过这个守卫,因此在其内部可以加入控制逻辑决定用户是否可以导航到目标路由;在路由注册的时候可以加入单路由独享的守卫,例如beforeEnter,守卫只在进入路由时触发,因此只会影响这个路由,控制更精确;我们还可以为路由组件添加守卫配置,例如beforeRouteEnter,会在渲染该组件的对应路由被验证前调用,控制的范围更精确了。
  • 用户的任何导航行为都会走navigate方法,内部有个guards队列按顺序执行用户注册的守卫钩子函数,如果没有通过验证逻辑则会取消原有的导航。

原理

runGuardQueue(guards)链式的执行用户在各级别注册的守卫钩子函数,通过则继续下一个级别的守卫,不通过进入catch流程取消原本导航

// 源码
runGuardQueue(guards).then(() => {// check global guards beforeEachguards = []for (const guard of beforeGuards.list()) {guards.push(guardToPromiseFn(guard, to, from))}guards.push(canceledNavigationCheck)return runGuardQueue(guards)}).then(() => {// check in components beforeRouteUpdateguards = extractComponentsGuards(updatingRecords,'beforeRouteUpdate',to,from)for (const record of updatingRecords) {record.updateGuards.forEach(guard => {guards.push(guardToPromiseFn(guard, to, from))})}guards.push(canceledNavigationCheck)// run the queue of per route beforeEnter guardsreturn runGuardQueue(guards)}).then(() => {// check the route beforeEnterguards = []for (const record of to.matched) {// do not trigger beforeEnter on reused viewsif (record.beforeEnter && !from.matched.includes(record)) {if (isArray(record.beforeEnter)) {for (const beforeEnter of record.beforeEnter)guards.push(guardToPromiseFn(beforeEnter, to, from))} else {guards.push(guardToPromiseFn(record.beforeEnter, to, from))}}}guards.push(canceledNavigationCheck)// run the queue of per route beforeEnter guardsreturn runGuardQueue(guards)}).then(() => {// NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>// clear existing enterCallbacks, these are added by extractComponentsGuardsto.matched.forEach(record => (record.enterCallbacks = {}))// check in-component beforeRouteEnterguards = extractComponentsGuards(enteringRecords,'beforeRouteEnter',to,from)guards.push(canceledNavigationCheck)// run the queue of per route beforeEnter guardsreturn runGuardQueue(guards)}).then(() => {// check global guards beforeResolveguards = []for (const guard of beforeResolveGuards.list()) {guards.push(guardToPromiseFn(guard, to, from))}guards.push(canceledNavigationCheck)return runGuardQueue(guards)})// catch any navigation canceled.catch(err =>isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)? err: Promise.reject(err))

源码位置(opens new window)

----------@----------

怎么实现路由懒加载呢

这是一道应用题。当打包应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问时才加载对应组件,这样就会更加高效

// 将
// import UserDetails from './views/UserDetails'
// 替换为
const UserDetails = () => import('./views/UserDetails')const router = createRouter({// ...routes: [{ path: '/users/:id', component: UserDetails }],
})

回答范例

  1. 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段
  2. 一般来说,对所有的路由都使用动态导入是个好主意
  3. component选项配置一个返回 Promise 组件的函数就可以定义懒加载路由。例如:{ path: '/users/:id', component: () => import('./views/UserDetails') }
  4. 结合注释 () => import(/* webpackChunkName: "group-user" */ './UserDetails.vue') 可以做webpack代码分块

----------@----------

Vue要做权限管理该怎么做?控制到按钮级别的权限怎么做?

分析

  • 综合实践题目,实际开发中经常需要面临权限管理的需求,考查实际应用能力。
  • 权限管理一般需求是两个:页面权限和按钮权限,从这两个方面论述即可。

7-vue-1

思路

  • 权限管理需求分析:页面和按钮权限
  • 权限管理的实现方案:分后端方案和前端方案阐述
  • 说说各自的优缺点

回答范例

  1. 权限管理一般需求是页面权限和按钮权限的管理

  2. 具体实现的时候分后端和前端两种方案:

  • 前端方案 会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个asyncRoutes数组,需要认证的页面在其路由的meta中添加一个roles字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过router.addRoutes(accessRoutes)方式动态添加路由即可

  • 后端方案 会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过addRoutes动态添加路由信息

  • 按钮权限的控制通常会实现一个指令,例如v-permission,将按钮要求角色通过值传给v-permission指令,在指令的moutned钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮

  1. 纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!

可能的追问

  1. 类似Tabs这类组件能不能使用v-permission指令实现按钮权限控制?
<el-tabs> <el-tab-pane label="⽤户管理" name="first">⽤户管理</el-tab-pane> <el-tab-pane label="⻆⾊管理" name="third">⻆⾊管理</el-tab-pane>
</el-tabs>
  1. 服务端返回的路由信息如何添加到路由器中?
// 前端组件名和组件映射表
const map = {//xx: require('@/views/xx.vue').default // 同步的⽅式xx: () => import('@/views/xx.vue') // 异步的⽅式
}
// 服务端返回的asyncRoutes
const asyncRoutes = [{ path: '/xx', component: 'xx',... }
]
// 遍历asyncRoutes,将component替换为map[component]
function mapComponent(asyncRoutes) {asyncRoutes.forEach(route => {route.component = map[route.component];if(route.children) {route.children.map(child => mapComponent(child))}})
}
mapComponent(asyncRoutes)

----------@----------

如果让你从零开始写一个vue路由,说说你的思路

思路分析:

首先思考vue路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。

  • 借助hash或者history api实现url跳转页面不刷新
  • 同时监听hashchange事件或者popstate事件处理跳转
  • 根据hash值或者state值从routes表中匹配对应component并渲染

回答范例:

一个SPA应用的路由需要解决的问题是 页面跳转内容改变同时不刷新 ,同时路由还需要以插件形式存在,所以:

  1. 首先我会定义一个createRouter函数,返回路由器实例,实例内部做几件事
  • 保存用户传入的配置项
  • 监听hash或者popstate事件
  • 回调里根据path匹配对应路由
  1. router定义成一个Vue插件,即实现install方法,内部做两件事
  • 实现两个全局组件:router-linkrouter-view,分别实现页面跳转和内容显示
  • 定义两个全局变量:$route$router,组件内可以访问当前路由和路由器实例

----------@----------

vuex是什么?怎么使用?哪种功能场景使用它?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。vuex 就是一个仓库,仓库里放了很多对象。其中 state 就是数据源存放地,对应于一般 vue 对象里面的 data 里面存放的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据发生改变,依赖这相数据的组件也会发生更新它通过 mapState 把全局的 stategetters 映射到当前组件的 computed 计算属性

  • vuex 一般用于中大型 web 单页应用中对应用的状态进行管理,对于一些组件间关系较为简单的小型应用,使用 vuex 的必要性不是很大,因为完全可以用组件 prop 属性或者事件来完成父子组件之间的通信,vuex 更多地用于解决跨组件通信以及作为数据中心集中式存储数据。
  • 使用Vuex解决非父子组件之间通信问题 vuex 是通过将 state 作为数据中心、各个组件共享 state 实现跨组件通信的,此时的数据完全独立于组件,因此将组件间共享的数据置于 State 中能有效解决多层级组件嵌套的跨组件通信问题

vuexState 在单页应用的开发中本身具有一个“数据库”的作用,可以将组件中用到的数据存储在 State 中,并在 Action 中封装数据读写的逻辑。这时候存在一个问题,一般什么样的数据会放在 State 中呢? 目前主要有两种数据会使用 vuex 进行管理:

  • 组件之间全局共享的数据
  • 通过后端异步请求的数据

7-vue-1

包括以下几个模块

  • stateVuex 使用单一状态树,即每个应用将仅仅包含一个store 实例。里面存放的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据发生改变,依赖这相数据的组件也会发生更新。它通过 mapState 把全局的 stategetters 映射到当前组件的 computed 计算属性
  • mutations:更改Vuexstore中的状态的唯一方法是提交mutation
  • gettersgetter 可以对 state 进行计算操作,它就是 store 的计算属性虽然在组件内也可以做计算属性,但是 getters 可以在多给件之间复用如果一个状态只在一个组件内使用,是可以不用 getters
  • actionaction 类似于 muation, 不同在于:action 提交的是 mutation,而不是直接变更状态action 可以包含任意异步操作
  • modules:面对复杂的应用程序,当管理的状态比较多时;我们需要将vuexstore对象分割成模块(modules)

7-vue-1

modules:项目特别复杂的时候,可以让每一个模块拥有自己的statemutationactiongetters,使得结构非常清晰,方便管理

7-vue-1

回答范例

思路

  • 给定义
  • 必要性阐述
  • 何时使用
  • 拓展:一些个人思考、实践经验等

回答范例

  1. Vuex 是一个专为 Vue.js 应用开发的 状态管理模式 + 库 。它采用集中式存储,管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
  2. 我们期待以一种简单的“单向数据流”的方式管理应用,即状态 -> 视图 -> 操作单向循环的方式。但当我们的应用遇到多个组件共享状态时,比如:多个视图依赖于同一状态或者来自不同视图的行为需要变更同一状态。此时单向数据流的简洁性很容易被破坏。因此,我们有必要把组件的共享状态抽取出来,以一个全局单例模式管理。通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。这是vuex存在的必要性,它和react生态中的redux之类是一个概念
  3. Vuex 解决状态管理的同时引入了不少概念:例如statemutationaction等,是否需要引入还需要根据应用的实际情况衡量一下:如果不打算开发大型单页应用,使用 Vuex 反而是繁琐冗余的,一个简单的 store 模式就足够了。但是,如果要构建一个中大型单页应用,Vuex 基本是标配。
  4. 我在使用vuex过程中感受到一些等

可能的追问

  1. vuex有什么缺点吗?你在开发过程中有遇到什么问题吗?
  • 刷新浏览器,vuex中的state会重新变为初始状态。解决方案-插件 vuex-persistedstate
  1. actionmutation的区别是什么?为什么要区分它们?
  • action中处理异步,mutation不可以
  • mutation做原子操作
  • action可以整合多个mutation的集合
  • mutation 是同步更新数据(内部会进行是否为异步方式更新数据的检测) $watch 严格模式下会报错
  • action 异步操作,可以获取数据后调佣 mutation 提交最终数据
  • 流程顺序:“相应视图—>修改State”拆分成两部分,视图触发ActionAction再触发Mutation`。
  • 基于流程顺序,二者扮演不同的角色:Mutation:专注于修改State,理论上是修改State的唯一途径。Action:业务代码、异步请求
  • 角色不同,二者有不同的限制:Mutation:必须同步执行。Action:可以异步,但不能直接操作State

----------@----------

Vuex中actions和mutations有什么区别

题目分析

  • mutationsactionsvuex带来的两个独特的概念。新手程序员容易混淆,所以面试官喜欢问。
  • 我们只需记住修改状态只能是mutationsactions只能通过提交mutation修改状态即可

回答范例

  1. 更改 Vuexstore 中的状态的唯一方法是提交 mutationmutation 非常类似于事件:每个 mutation 都有一个字符串的类型 (type)和一个 回调函数 (handler) 。Action 类似于 mutation,不同在于:Action可以包含任意异步操作,但它不能修改状态, 需要提交mutation才能变更状态
  2. 开发时,包含异步操作或者复杂业务组合时使用action;需要直接修改状态则提交mutation。但由于dispatchcommit是两个API,容易引起混淆,实践中也会采用统一使用dispatch action的方式。调用dispatchcommit两个API时几乎完全一样,但是定义两者时却不甚相同,mutation的回调函数接收参数是state对象。action则是与Store实例具有相同方法和属性的上下文context对象,因此一般会解构它为{commit, dispatch, state},从而方便编码。另外dispatch会返回Promise实例便于处理内部异步结果
  3. 实现上commit(type)方法相当于调用options.mutations[type](state)dispatch(type)方法相当于调用options.actions[type](store),这样就很容易理解两者使用上的不同了

实现

我们可以像下面这样简单实现commitdispatch,从而辨别两者不同

class Store {constructor(options) {this.state = reactive(options.state)this.options = options}commit(type, payload) {// 传入上下文和参数1都是state对象this.options.mutations[type].call(this.state, this.state, payload)}dispatch(type, payload) {// 传入上下文和参数1都是store本身this.options.actions[type].call(this, this, payload)}
}

----------@----------

怎么监听vuex数据的变化

分析

  • vuex数据状态是响应式的,所以状态变视图跟着变,但是有时还是需要知道数据状态变了从而做一些事情。
  • 既然状态都是响应式的,那自然可以watch,另外vuex也提供了订阅的API:store.subscribe()

回答范例

  1. 我知道几种方法:
  • 可以通过watch选项或者watch方法监听状态
  • 可以使用vuex提供的API:store.subscribe()
  1. watch选项方式,可以以字符串形式监听$store.state.xxsubscribe方式,可以调用store.subscribe(cb),回调函数接收mutation对象和state对象,这样可以进一步判断mutation.type是否是期待的那个,从而进一步做后续处理。
  2. watch方式简单好用,且能获取变化前后值,首选;subscribe方法会被所有commit行为触发,因此还需要判断mutation.type,用起来略繁琐,一般用于vuex插件中

实践

watch方式

const app = createApp({watch: {'$store.state.counter'() {console.log('counter change!');}}
})

subscribe方式:

store.subscribe((mutation, state) => {if (mutation.type === 'add') {console.log('counter change in subscribe()!');}
})

----------@----------

Vuex 页面刷新数据丢失怎么解决

体验

可以从localStorage中获取作为状态初始值:

const store = createStore({state () {return {count: localStorage.getItem('count')}}
})

业务代码中,提交修改状态同时保存最新值:虽说实现了,但是每次还要手动刷新localStorage不太优雅

store.commit('increment')
localStorage.setItem('count', store.state.count)

回答范例

  1. vuex只是在内存保存状态,刷新之后就会丢失,如果要持久化就要存起来
  2. localStorage就很合适,提交mutation的时候同时存入localStoragestore中把值取出作为state的初始值即可。
  3. 这里有两个问题,不是所有状态都需要持久化;如果需要保存的状态很多,编写的代码就不够优雅,每个提交的地方都要单独做保存处理。这里就可以利用vuex提供的subscribe方法做一个统一的处理。甚至可以封装一个vuex插件以便复用。
  4. 类似的插件有vuex-persistvuex-persistedstate,内部的实现就是通过订阅mutation变化做统一处理,通过插件的选项控制哪些需要持久化

原理

可以看一下vuex-persist (opens new window)内部确实是利用subscribe实现的

----------@----------

Vuex 为什么要分模块并且加命名空间

  • 模块 : 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 statemutationactiongetter、甚至是嵌套子模块
  • 命名空间 :默认情况下,模块内部的 actionmutationgetter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutationaction 作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getteractionmutation 都会自动根据模块注册的路径调整命名

----------@----------

你有使用过vuex的module吗?

const moduleA = {state: () => ({ ... }),mutations: { ... },actions: { ... },getters: { ... }
}
const moduleB = {state: () => ({ ... }),mutations: { ... },actions: { ... }
}
const store = createStore({modules: {a: moduleA,b: moduleB}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
store.getters.c // -> moduleA里的getters
store.commit('d') // -> 能同时触发子模块中同名mutation
store.dispatch('e') // -> 能同时触发子模块中同名action
  • 用过module,项目规模变大之后,单独一个store对象会过于庞大臃肿,通过模块方式可以拆分开来便于维护
  • 可以按之前规则单独编写子模块代码,然后在主文件中通过modules选项组织起来:reateStore({modules:{...}})
  • 不过使用时要注意访问子模块状态时需要加上注册时模块名:store.state.a.xxx,但同时gettersmutationsactions又在全局空间中,使用方式和之前一样。如果要做到完全拆分,需要在子块加上namespace选项,此时再访问它们就要加上命名空间前缀。
  • 很显然,模块的方式可以拆分代码,但是缺点也很明显,就是使用起来比较繁琐复杂,容易出错。而且类型系统支持很差,不能给我们带来帮助。pinia显然在这方面有了很大改进,是时候切换过去了

----------@----------

你觉得vuex有什么缺点

分析

相较于reduxvuex已经相当简便好用了。但模块的使用比较繁琐,对ts支持也不好。

体验

使用模块:用起来比较繁琐,使用模式也不统一,基本上得不到类型系统的任何支持

const store = createStore({modules: {a: moduleA}
})
store.state.a // -> 要带上 moduleA 的key,内嵌模块的话会很长,不得不配合mapState使用
store.getters.c // -> moduleA里的getters,没有namespaced时又变成了全局的
store.getters['a/c'] // -> 有namespaced时要加path,使用模式又和state不一样
store.commit('d') // -> 没有namespaced时变成了全局的,能同时触发多个子模块中同名mutation
store.commit('a/d') // -> 有namespaced时要加path,配合mapMutations使用感觉也没简化

回答范例

  1. vuex利用响应式,使用起来已经相当方便快捷了。但是在使用过程中感觉模块化这一块做的过于复杂,用的时候容易出错,还要经常查看文档
  2. 比如:访问state时要带上模块key,内嵌模块的话会很长,不得不配合mapState使用,加不加namespaced区别也很大,gettersmutationsactions这些默认是全局,加上之后必须用字符串类型的path来匹配,使用模式不统一,容易出错;对ts的支持也不友好,在使用模块时没有代码提示。
  3. 之前Vue2项目中用过vuex-module-decorators的解决方案,虽然类型支持上有所改善,但又要学一套新东西,增加了学习成本。pinia出现之后使用体验好了很多,Vue3 + pinia会是更好的组合

原理

下面我们来看看vuexstore.state.x.y这种嵌套的路径是怎么搞出来的

首先是子模块安装过程:父模块状态parentState上面设置了子模块名称moduleName,值为当前模块state对象。放在上面的例子中相当于:store.state['x'] = moduleX.state。此过程是递归的,那么store.state.x.y安装时就是:store.state['x']['y'] = moduleY.state

//源码位置 https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115
if (!isRoot && !hot) {// 获取父模块stateconst parentState = getNestedState(rootState, path.slice(0, -1))// 获取子模块名称const moduleName = path[path.length - 1]store._withCommit(() => {// 把子模块state设置到父模块上parentState[moduleName] = module.state})
}

----------@----------

用过pinia吗?有什么优点?

1. pinia是什么?

  • Vue3中,可以使用传统的Vuex来实现状态管理,也可以使用最新的pinia来实现状态管理,我们来看看官网如何解释pinia的:PiniaVue 的存储库,它允许您跨组件/页面共享状态。
  • 实际上,pinia就是Vuex的升级版,官网也说过,为了尊重原作者,所以取名pinia,而没有取名Vuex,所以大家可以直接将pinia比作为Vue3Vuex

2. 为什么要使用pinia?

  • Vue2Vue3都支持,这让我们同时使用Vue2Vue3的小伙伴都能很快上手。
  • pinia中只有stategetteraction,抛弃了Vuex中的MutationVuexmutation一直都不太受小伙伴们的待见,pinia直接抛弃它了,这无疑减少了我们工作量。
  • piniaaction支持同步和异步,Vuex不支持
  • 良好的Typescript支持,毕竟我们Vue3都推荐使用TS来编写,这个时候使用pinia就非常合适了
  • 无需再创建各个模块嵌套了,Vuex中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而pinia中每个store都是独立的,互相不影响。
  • 体积非常小,只有1KB左右。
  • pinia支持插件来扩展自身功能。
  • 支持服务端渲染

3. pinna使用

pinna文档(opens new window)

  1. 准备工作

我们这里搭建一个最新的Vue3 + TS + Vite项目

npm create vite@latest my-vite-app --template vue-ts
  1. pinia基础使用
yarn add pinia
// main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { createPinia } from "pinia";
const pinia = createPinia();const app = createApp(App);
app.use(pinia);
app.mount("#app");

2.1 创建store

//sbinsrc/store/user.ts
import { defineStore } from 'pinia'// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {// 其它配置项
})

创建store很简单,调用pinia中的defineStore函数即可,该函数接收两个参数:

  • name:一个字符串,必传项,该store的唯一id
  • options:一个对象,store的配置项,比如配置store内的数据,修改数据的方法等等。

我们可以定义任意数量的store,因为我们其实一个store就是一个函数,这也是pinia的好处之一,让我们的代码扁平化了,这和Vue3的实现思想是一样的

2.2 使用store

<!-- src/App.vue -->
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
console.log(store);
</script>

2.3 添加state

export const useUsersStore = defineStore("users", {state: () => {return {name: "test",age: 20,sex: "男",};},
});

2.4 读取state数据

<template><img alt="Vue logo" src="./assets/logo.png" /><p>姓名:{{ name }}</p><p>年龄:{{ age }}</p><p>性别:{{ sex }}</p>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
const name = ref<string>(store.name);
const age = ref<number>(store.age);
const sex = ref<string>(store.sex);
</script>

上段代码中我们直接通过store.age等方式获取到了store存储的值,但是大家有没有发现,这样比较繁琐,我们其实可以用解构的方式来获取值,使得代码更简洁一点

import { useUsersStore, storeToRefs } from "../src/store/user";
const store = useUsersStore();
const { name, age, sex } = storeToRefs(store); // storeToRefs获取的值是响应式的

2.5 修改state数据

<template><img alt="Vue logo" src="./assets/logo.png" /><p>姓名:{{ name }}</p><p>年龄:{{ age }}</p><p>性别:{{ sex }}</p><button @click="changeName">更改姓名</button>
</template>
<script setup lang="ts">
import child from './child.vue';
import { useUsersStore, storeToRefs } from "../src/store/user";
const store = useUsersStore();
const { name, age, sex } = storeToRefs(store);
const changeName = () => {store.name = "张三";console.log(store);
};
</script>

2.6 重置state

  • 有时候我们修改了state数据,想要将它还原,这个时候该怎么做呢?就比如用户填写了一部分表单,突然想重置为最初始的状态。
  • 此时,我们直接调用store$reset()方法即可,继续使用我们的例子,添加一个重置按钮
<button @click="reset">重置store</button>
// 重置store
const reset = () => {store.$reset();
};

当我们点击重置按钮时,store中的数据会变为初始状态,页面也会更新

2.7 批量更改state数据

如果我们一次性需要修改很多条数据的话,有更加简便的方法,使用store$patch方法,修改app.vue代码,添加一个批量更改数据的方法

<button @click="patchStore">批量修改数据</button>
// 批量修改数据
const patchStore = () => {store.$patch({name: "张三",age: 100,sex: "女",});
};
  • 有经验的小伙伴可能发现了,我们采用这种批量更改的方式似乎代价有一点大,假如我们state中有些字段无需更改,但是按照上段代码的写法,我们必须要将state中的所有字段例举出了。
  • 为了解决该问题,pinia提供的$patch方法还可以接收一个回调函数,它的用法有点像我们的数组循环回调函数了。
store.$patch((state) => {state.items.push({ name: 'shoes', quantity: 1 })state.hasChanged = true
})

2.8 直接替换整个state

pinia提供了方法让我们直接替换整个state对象,使用store$state方法

store.$state = { counter: 666, name: '张三' }

上段代码会将我们提前声明的state替换为新的对象,可能这种场景用得比较少

  1. getters属性
  • gettersdefineStore参数配置项里面的另一个属性
  • 可以把getter想象成Vue中的计算属性,它的作用就是返回一个新的结果,既然它和Vue中的计算属性类似,那么它肯定也是会被缓存的,就和computed一样

3.1 添加getter

export const useUsersStore = defineStore("users", {state: () => {return {name: "test",age: 10,sex: "男",};},getters: {getAddAge: (state) => {return state.age + 100;},},
})

上段代码中我们在配置项参数中添加了getter属性,该属性对象中定义了一个getAddAge方法,该方法会默认接收一个state参数,也就是state对象,然后该方法返回的是一个新的数据

3.2 使用getter

<template><p>新年龄:{{ store.getAddAge }}</p><button @click="patchStore">批量修改数据</button>
</template>
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
// 批量修改数据
const patchStore = () => {store.$patch({name: "张三",age: 100,sex: "女",});
};
</script>

上段代码中我们直接在标签上使用了store.gettAddAge方法,这样可以保证响应式,其实我们state中的name等属性也可以以此种方式直接在标签上使用,也可以保持响应式

3.3 getter中调用其它getter

export const useUsersStore = defineStore("users", {state: () => {return {name: "test",age: 20,sex: "男",};},getters: {getAddAge: (state) => {return state.age + 100;},getNameAndAge(): string {return this.name + this.getAddAge; // 调用其它getter},},
});

3.3 getter传参

export const useUsersStore = defineStore("users", {state: () => {return {name: "test",age: 20,sex: "男",};},getters: {getAddAge: (state) => {return (num: number) => state.age + num;},getNameAndAge(): string {return this.name + this.getAddAge; // 调用其它getter},},
});
<p>新年龄:{{ store.getAddAge(1100) }}</p>
  1. actions属性
  • 前面我们提到的stategetters属性都主要是数据层面的,并没有具体的业务逻辑代码,它们两个就和我们组件代码中的data数据和computed计算属性一样。
  • 那么,如果我们有业务代码的话,最好就是卸载actions属性里面,该属性就和我们组件代码中的methods相似,用来放置一些处理业务逻辑的方法。
  • actions属性值同样是一个对象,该对象里面也是存储的各种各样的方法,包括同步方法和异步方法

4.1 添加actions

export const useUsersStore = defineStore("users", {state: () => {return {name: "test",age: 20,sex: "男",};},getters: {getAddAge: (state) => {return (num: number) => state.age + num;},getNameAndAge(): string {return this.name + this.getAddAge; // 调用其它getter},},actions: {// 在实际场景中,该方法可以是任何逻辑,比如发送请求、存储token等等。大家把actions方法当作一个普通的方法即可,特殊之处在于该方法内部的this指向的是当前storesaveName(name: string) {this.name = name;},},
});

4.2 使用actions

使用actions中的方法也非常简单,比如我们在App.vue中想要调用该方法

const saveName = () => {store.saveName("poetries");
};

总结

pinia的知识点很少,如果你有Vuex基础,那么学起来更是易如反掌

pinia无非就是以下3个大点:

  • state
  • getters
  • actions

----------@----------

对Vue SSR的理解

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。

SSR也就是服务端渲染,也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端完成,然后再把 html 直接返回给客户端

  • 优点SSR 有着更好的 SEO、并且首屏加载速度更快
    • 因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面
    • 更快的内容到达时间(首屏加载更快): SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间
  • 缺点 : 开发条件会受到限制,服务器端渲染只支持 beforeCreatecreated 两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境。服务器会有更大的负载需求
    • 在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 ( high traffic ) 下使用,请准备相应的服务器负载,并明智地采用缓存策略

其基本实现原理

  • app.js 作为客户端与服务端的公用入口,导出 Vue 根实例,供客户端 entry 与服务端 entry 使用。客户端 entry 主要作用挂载到 DOM 上,服务端 entry 除了创建和返回实例,还进行路由匹配与数据预获取。
  • webpack 为客服端打包一个 Client Bundle ,为服务端打包一个 Server Bundle
  • 服务器接收请求时,会根据 url,加载相应组件,获取和解析异步数据,创建一个读取 Server BundleBundleRenderer,然后生成 html 发送给客户端。
  • 客户端混合,客户端收到从服务端传来的 DOM 与自己的生成的 DOM 进行对比,把不相同的 DOM 激活,使其可以能够响应后续变化,这个过程称为客户端激活 。为确保混合成功,客户端与服务器端需要共享同一套数据。在服务端,可以在渲染之前获取数据,填充到 stroe 里,这样,在客户端挂载到 DOM 之前,可以直接从 store里取数据。首屏的动态数据通过 window.__INITIAL_STATE__发送到客户端

Vue SSR 的实现,主要就是把 Vue 的组件输出成一个完整 HTML, vue-server-renderer 就是干这事的

Vue SSR需要做的事多点(输出完整 HTML),除了complier -> vnode,还需如数据获取填充至 HTML、客户端混合(hydration)、缓存等等。相比于其他模板引擎(ejs, jade 等),最终要实现的目的是一样的,性能上可能要差点

----------@----------

Vue 修饰符有哪些

vue中修饰符分为以下五种

  • 表单修饰符
  • 事件修饰符
  • 鼠标按键修饰符
  • 键值修饰符
  • v-bind修饰符

1. 表单修饰符

在我们填写表单的时候用得最多的是input标签,指令用得最多的是v-model

关于表单的修饰符有如下:

  • lazy

在我们填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同步

<input type="text" v-model.lazy="value">
<p>{{value}}</p>
  • trim

自动过滤用户输入的首空格字符,而中间的空格不会过滤

<input type="text" v-model.trim="value">
  • number

自动将用户的输入值转为数值类型,但如果这个值无法被parseFloat解析,则会返回原来的值

<input v-model.number="age" type="number">

2. 事件修饰符

事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符

  • .stop 阻止了事件冒泡,相当于调用了event.stopPropagation方法
<div @click="shout(2)"><button @click.stop="shout(1)">ok</button>
</div>
//只输出1
  • .prevent 阻止了事件的默认行为,相当于调用了event.preventDefault方法
<form v-on:submit.prevent="onSubmit"></form>
  • .capture 使用事件捕获模式,使事件触发从包含这个元素的顶层开始往下触发
<div @click.capture="shout(1)">obj1
<div @click.capture="shout(2)">obj2
<div @click="shout(3)">obj3
<div @click="shout(4)">obj4
</div>
</div>
</div>
</div>
// 输出结构: 1 2 4 3 
  • .self 只当在 event.target 是当前元素自身时触发处理函数
<div v-on:click.self="doThat">...</div>

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击

  • .once 绑定了事件以后只能触发一次,第二次就不会触发
<button @click.once="shout(1)">ok</button>
  • .passive 告诉浏览器你不想阻止事件的默认行为

在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符

<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成  -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>
  • 不要把 .passive.prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。
  • passive 会告诉浏览器你不想阻止事件的默认行为
  • native 让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件
<my-component v-on:click.native="doSomething"></my-component><!-- 使用.native修饰符来操作普通HTML标签是会令事件失效的 -->

3. 鼠标按钮修饰符

鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:

  • .left 左键点击
  • .right 右键点击
  • .middle 中键点击
<button @click.left="shout(1)">ok</button>
<button @click.right="shout(1)">ok</button>
<button @click.middle="shout(1)">ok</button>

4. 键盘事件的修饰符

键盘修饰符是用来修饰键盘事件(onkeyuponkeydown)的,有如下:

keyCode存在很多,但vue为我们提供了别名,分为以下两种:

  • 普通键entertabdeletespaceescupdownleftright…)
  • 系统修饰键ctrlaltmetashift…)
<!-- 只有按键为keyCode的时候才触发 -->
<input type="text" @keyup.keyCode="shout()">

还可以通过以下方式自定义一些全局的键盘码别名

Vue.config.keyCodes.f2 = 113

5. v-bind修饰符

v-bind修饰符主要是为属性进行操作,用来分别有如下:

  • async 能对props进行一个双向绑定
//父组件
<comp :myMessage.sync="bar"></comp> 
//子组件
this.$emit('update:myMessage',params);

以上这种方法相当于以下的简写

//父亲组件
<comp :myMessage="bar" @update:myMessage="func"></comp>
func(e){this.bar = e;
}//子组件js
func2(){this.$emit('update:myMessage',params);
}

使用async需要注意以下两点:

  • 使用sync的时候,子组件传递的事件名格式必须为update:value,其中value必须与子组件中props中声明的名称完全一致
  • 注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用
  • prop 设置自定义标签属性,避免暴露数据,防止污染HTML结构
<input id="uid" title="title1" value="1" :index.prop="index">
  • camel 将命名变为驼峰命名法,如将view-Box属性名转换为 viewBox
<svg :viewBox="viewBox"></svg>

应用场景

根据每一个修饰符的功能,我们可以得到以下修饰符的应用场景:

  • .stop:阻止事件冒泡
  • .native:绑定原生事件
  • .once:事件只执行一次
  • .self :将事件绑定在自身身上,相当于阻止事件冒泡
  • .prevent:阻止默认事件
  • .caption:用于事件捕获
  • .once:只触发一次
  • .keyCode:监听特定键盘按下
  • .right:右键

----------@----------

说说 vue 内置指令

7-vue-1

----------@----------

怎样理解 Vue 的单向数据流

数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会 防止从子组件意外改变父级组件的状态 ,从而导致你的应用的数据流向难以理解

注意 :在子组件直接用 v-model 绑定父组件传过来的 prop 这样是不规范的写法 开发环境会报警告

如果实在要改变父组件的 prop 值,可以在 data 里面定义一个变量 并用 prop 的值初始化它 之后用$emit 通知父组件去修改

有两种常见的试图改变一个 prop 的情形 :

  1. 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop用作其初始值
props: ['initialCounter'],
data: function () {return {counter: this.initialCounter}
}
  1. 这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性
props: ['size'],
computed: {normalizedSize: function () {return this.size.trim().toLowerCase()}
}

----------@----------

写过自定义指令吗?原理是什么

回答范例

  1. Vue有一组默认指令,比如v-modelv-for,同时Vue也允许用户注册自定义指令来扩展Vue能力

  2. 自定义指令主要完成一些可复用低层级DOM操作

  3. 使用自定义指令分为定义、注册和使用三步:

  • 定义自定义指令有两种方式:对象和函数形式,前者类似组件定义,有各种生命周期;后者只会在mounted和updated时执行

  • 注册自定义指令类似组件,可以使用app.directive()全局注册,使用{directives:{xxx}}局部注册

  • 使用时在注册名称前加上v-即可,比如v-focus

  1. 我在项目中常用到一些自定义指令,例如:
  • 复制粘贴 v-copy

  • 长按 v-longpress

  • 防抖 v-debounce

  • 图片懒加载 v-lazy

  • 按钮权限 v-premission

  • 页面水印 v-waterMarker

  • 拖拽指令 v-draggable

  1. vue3中指令定义发生了比较大的变化,主要是钩子的名称保持和组件一致,这样开发人员容易记忆,不易犯错。另外在v3.2之后,可以在setup中以一个小写v开头方便的定义自定义指令,更简单了

基本使用

当Vue中的核心内置指令不能够满足我们的需求时,我们可以定制自定义的指令用来满足开发的需求

我们看到的v-开头的行内属性,都是指令,不同的指令可以完成或实现不同的功能,对普通 DOM元素进行底层操作,这时候就会用到自定义指令。除了核心功能默认内置的指令 (v-modelv-show),Vue 也允许注册自定义指令

// 指令使用的几种方式:
//会实例化一个指令,但这个指令没有参数 
`v-xxx`// -- 将值传到指令中
`v-xxx="value"`  // -- 将字符串传入到指令中,如`v-html="'<p>内容</p>'"`
`v-xxx="'string'"` // -- 传参数(`arg`),如`v-bind:class="className"`
`v-xxx:arg="value"` // -- 使用修饰符(`modifier`)
`v-xxx:arg.modifier="value"` 

注册一个自定义指令有全局注册与局部注册

// 全局注册注册主要是用过Vue.directive方法进行注册
// Vue.directive第一个参数是指令的名字(不需要写上v-前缀),第二个参数可以是对象数据,也可以是一个指令函数
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {// 当被绑定的元素插入到 DOM 中时……inserted: function (el) {// 聚焦元素el.focus()  // 页面加载完成之后自动让输入框获取到焦点的小功能}
})// 局部注册通过在组件options选项中设置directive属性
directives: {focus: {// 指令的定义inserted: function (el) {el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能}}
}// 然后你可以在模板中任何元素上使用新的 v-focus property,如下:<input v-focus />

钩子函数

  1. bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  2. inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  3. update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。
  4. componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
  5. unbind:只调用一次,指令与元素解绑时调用。

所有的钩子函数的参数都有以下:

  • el:指令所绑定的元素,可以用来直接操作 DOM
  • binding:一个对象,包含以下 property
    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
    • vnodeVue 编译生成的虚拟节点
    • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用

除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行

<div v-demo="{ color: 'white', text: 'hello!' }"></div>
<script>Vue.directive('demo', function (el, binding) {console.log(binding.value.color) // "white"console.log(binding.value.text)  // "hello!"})
</script>

应用场景

使用自定义组件组件可以满足我们日常一些场景,这里给出几个自定义组件的案例:

  1. 防抖
// 1.设置v-throttle自定义指令
Vue.directive('throttle', {bind: (el, binding) => {let throttleTime = binding.value; // 防抖时间if (!throttleTime) { // 用户若不设置防抖时间,则默认2sthrottleTime = 2000;}let cbFun;el.addEventListener('click', event => {if (!cbFun) { // 第一次执行cbFun = setTimeout(() => {cbFun = null;}, throttleTime);} else {event && event.stopImmediatePropagation();}}, true);},
});
// 2.为button标签设置v-throttle自定义指令
<button @click="sayHello" v-throttle>提交</button>
  1. 图片懒加载

设置一个v-lazy自定义组件完成图片懒加载

const LazyLoad = {// install方法install(Vue,options){// 代替图片的loading图let defaultSrc = options.default;Vue.directive('lazy',{bind(el,binding){LazyLoad.init(el,binding.value,defaultSrc);},inserted(el){// 兼容处理if('InterpObserver' in window){LazyLoad.observe(el);}else{LazyLoad.listenerScroll(el);}},})},// 初始化init(el,val,def){// src 储存真实srcel.setAttribute('src',val);// 设置src为loading图el.setAttribute('src',def);},// 利用InterpObserver监听elobserve(el){let io = new InterpObserver(entries => {let realSrc = el.dataset.src;if(entries[0].isIntersecting){if(realSrc){el.src = realSrc;el.removeAttribute('src');}}});io.observe(el);},// 监听scroll事件listenerScroll(el){let handler = LazyLoad.throttle(LazyLoad.load,300);LazyLoad.load(el);window.addEventListener('scroll',() => {handler(el);});},// 加载真实图片load(el){let windowHeight = document.documentElement.clientHeightlet elTop = el.getBoundingClientRect().top;let elBtm = el.getBoundingClientRect().bottom;let realSrc = el.dataset.src;if(elTop - windowHeight<0&&elBtm > 0){if(realSrc){el.src = realSrc;el.removeAttribute('src');}}},// 节流throttle(fn,delay){let timer; let prevTime;return function(...args){let currTime = Date.now();let context = this;if(!prevTime) prevTime = currTime;clearTimeout(timer);if(currTime - prevTime > delay){prevTime = currTime;fn.apply(context,args);clearTimeout(timer);return;}timer = setTimeout(function(){prevTime = Date.now();timer = null;fn.apply(context,args);},delay);}}}
export default LazyLoad;
  1. 一键 Copy的功能
import { Message } from 'ant-design-vue';const vCopy = { ///*bind 钩子函数,第一次绑定时调用,可以在这里做初始化设置el: 作用的 dom 对象value: 传给指令的值,也就是我们要 copy 的值*/bind(el, { value }) {el.$value = value; // 用一个全局属性来存传进来的值,因为这个值在别的钩子函数里还会用到el.handler = () => {if (!el.$value) {// 值为空的时候,给出提示,我这里的提示是用的 ant-design-vue 的提示,你们随意Message.warning('无复制内容');return;}// 动态创建 textarea 标签const textarea = document.createElement('textarea');// 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域textarea.readOnly = 'readonly';textarea.style.position = 'absolute';textarea.style.left = '-9999px';// 将要 copy 的值赋给 textarea 标签的 value 属性textarea.value = el.$value;// 将 textarea 插入到 body 中document.body.appendChild(textarea);// 选中值并复制textarea.select();// textarea.setSelectionRange(0, textarea.value.length);const result = document.execCommand('Copy');if (result) {Message.success('复制成功');}document.body.removeChild(textarea);};// 绑定点击事件,就是所谓的一键 copy 啦el.addEventListener('click', el.handler);},// 当传进来的值更新的时候触发componentUpdated(el, { value }) {el.$value = value;},// 指令与元素解绑的时候,移除事件绑定unbind(el) {el.removeEventListener('click', el.handler);},
};export default vCopy;
  1. 拖拽
<div ref="a" id="bg" v-drag></div>directives: {drag: {bind() {},inserted(el) {el.onmousedown = (e) => {let x = e.clientX - el.offsetLeft;let y = e.clientY - el.offsetTop;document.onmousemove = (e) => {let xx = e.clientX - x + "px";let yy = e.clientY - y + "px";el.style.left = xx;el.style.top = yy;};el.onmouseup = (e) => {document.onmousemove = null;};};},},}

原理

  • 指令本质上是装饰器,是 vueHTML 元素的扩展,给 HTML 元素增加自定义功能。vue 编译 DOM 时,会找到指令对象,执行指令的相关方法。
  • 自定义指令有五个生命周期(也叫钩子函数),分别是 bindinsertedupdatecomponentUpdatedunbind

原理

  1. 在生成 ast 语法树时,遇到指令会给当前元素添加 directives 属性
  2. 通过 genDirectives 生成指令代码
  3. patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子
  4. 当执行指令对应钩子函数时,调用对应指令定义的方法

----------@----------