关于vue.js:前端一面必会vue面试题边面边更

35次阅读

共计 19550 个字符,预计需要花费 49 分钟才能阅读完成。

为什么要应用异步组件

  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 
}

谈谈你对 MVVM 的了解

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

MVC 模式 代表:Backbone + underscore + jquery

  • 传统的 MVC 指的是, 用户操作会申请服务端路由,路由会调用对应的控制器来解决,控制器会获取数据。将后果返回给前端, 页面从新渲染
  • MVVM:传统的前端会将数据手动渲染到页面上, MVVM 模式不须要用户收到操作 dom 元素, 将数据绑定到 viewModel 层上,会主动将数据渲染到页面中,视图变动会告诉 viewModel层 更新数据。ViewModel 就是咱们 MVVM 模式中的桥梁

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

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"
    }
}

如何保留页面的以后的状态

既然是要放弃页面的状态(其实也就是组件的状态),那么会呈现以下两种状况:

  • 前组件会被卸载
  • 前组件不会被卸载

那么能够依照这两种状况别离失去以下办法:

组件会被卸载:

(1)将状态存储在 LocalStorage / SessionStorage

只须要在组件行将被销毁的生命周期 componentWillUnmount(react)中在 LocalStorage / SessionStorage 中把以后组件的 state 通过 JSON.stringify() 贮存下来就能够了。在这外面须要留神的是组件更新状态的机会。

比方从 B 组件跳转到 A 组件的时候,A 组件须要更新本身的状态。然而如果从别的组件跳转到 B 组件的时候,实际上是心愿 B 组件从新渲染的,也就是不要从 Storage 中读取信息。所以须要在 Storage 中的状态退出一个 flag 属性,用来管制 A 组件是否读取 Storage 中的状态。

长处:

  • 兼容性好,不须要额定库或工具。
  • 简略快捷,根本能够满足大部分需要。

毛病:

  • 状态通过 JSON 办法贮存(相当于深拷贝),如果状态中有非凡状况(比方 Date 对象、Regexp 对象等)的时候会失去字符串而不是原来的值。(具体参考用 JSON 深拷贝的毛病)
  • 如果 B 组件后退或者下一页跳转并不是前组件,那么 flag 判断会生效,导致从其余页面进入 A 组件页面时 A 组件会从新读取 Storage,会造成很奇怪的景象

(2)路由传值

通过 react-router 的 Link 组件的 prop —— to 能够实现路由间传递参数的成果。

在这里须要用到 state 参数,在 B 组件中通过 history.location.state 就能够拿到 state 值,保留它。返回 A 组件时再次携带 state 达到路由状态放弃的成果。

长处:

  • 简略快捷,不会净化 LocalStorage / SessionStorage。
  • 能够传递 Date、RegExp 等非凡对象(不必放心 JSON.stringify / parse 的有余)

毛病:

  • 如果 A 组件能够跳转至多个组件,那么在每一个跳转组件内都要写雷同的逻辑。

组件不会被卸载:

(1)单页面渲染

要切换的组件作为子组件全屏渲染,父组件中失常贮存页面状态。

长处:

  • 代码量少
  • 不须要思考状态传递过程中的谬误

毛病:

  • 减少 A 组件保护老本
  • 须要传入额定的 prop 到 B 组件
  • 无奈利用路由定位页面

除此之外,在 Vue 中,还能够是用 keep-alive 来缓存页面,当组件在 keep-alive 内被切换时组件的 activated、deactivated 这两个生命周期钩子函数会被执行
被包裹在 keep-alive 中的组件的状态将会被保留:

<keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
</kepp-alive>

router.js

{
  path: '/',
  name: 'xxx',
  component: ()=>import('../src/views/xxx.vue'),
  meta:{keepAlive: true // 须要被缓存}
},

Vue 是如何收集依赖的?

在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由一般对象变成响应式对象,在这个过程中便会进行依赖收集的相干逻辑,如下所示∶

function defieneReactive (obj, key, val){const dep = new Dep();
  ...
  Object.defineProperty(obj, key, {
    ...
    get: function reactiveGetter () {if(Dep.target){dep.depend();
        ...
      }
      return val
    }
    ...
  })
}

以上只保留了要害代码,次要就是 const dep = new Dep()实例化一个 Dep 的实例,而后在 get 函数中通过 dep.depend() 进行依赖收集。(1)Dep Dep 是整个依赖收集的外围,其要害代码如下:

class Dep {
  static target;
  subs;

  constructor () {
    ...
    this.subs = [];}
  addSub (sub) {this.subs.push(sub)
  }
  removeSub (sub) {remove(this.sub, sub)
  }
  depend () {if(Dep.target){Dep.target.addDep(this)
    }
  }
  notify () {const subs = this.subds.slice();
    for(let i = 0;i < subs.length; i++){subs[i].update()}
  }
}

Dep 是一个 class,其中有一个关 键的动态属性 static,它指向了一个全局惟一 Watcher,保障了同一时间全局只有一个 watcher 被计算,另一个属性 subs 则是一个 Watcher 的数组,所以 Dep 实际上就是对 Watcher 的治理,再看看 Watcher 的相干代码∶

(2)Watcher

class Watcher {
  getter;
  ...
  constructor (vm, expression){
    ...
    this.getter = expression;
    this.get();}
  get () {pushTarget(this);
    value = this.getter.call(vm, vm)
    ...
    return value
  }
  addDep (dep){
        ...
    dep.addSub(this)
  }
  ...
}
function pushTarget (_target) {Dep.target = _target}

Watcher 是一个 class,它定义了一些办法,其中和依赖收集相干的次要有 get、addDep 等。

(3)过程

在实例化 Vue 时,依赖收集的相干过程如下∶
初 始 化 状 态 initState,这 中 间 便 会 通 过 defineReactive 将数据变成响应式对象,其中的 getter 局部便是用来依赖收集的。
初始化最终会走 mount 过程,其中会实例化 Watcher,进入 Watcher 中,便会执行 this.get() 办法,

updateComponent = () => {vm._update(vm._render())
}
new Watcher(vm, updateComponent)

get 办法中的 pushTarget 实际上就是把 Dep.target 赋值为以后的 watcher。

this.getter.call(vm,vm),这里的 getter 会执行 vm._render() 办法,在这个过程中便会触发数据对象的 getter。那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 办法,也就会执行 Dep.target.addDep(this)。方才 Dep.target 曾经被赋值为 watcher,于是便会执行 addDep 办法,而后走到 dep.addSub() 办法,便将以后的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目标是为后续数据变动时候能告诉到哪些 subs 做筹备。所以在 vm._render() 过程中,会触发所有数据的 getter,这样便曾经实现了一个依赖收集的过程。

常见的事件修饰符及其作用

  • .stop:等同于 JavaScript 中的 event.stopPropagation(),避免事件冒泡;
  • .prevent:等同于 JavaScript 中的 event.preventDefault(),避免执行预设的行为(如果事件可勾销,则勾销该事件,而不进行事件的进一步流传);
  • .capture:与事件冒泡的方向相同,事件捕捉由外到内;
  • .self:只会触发本人范畴内的事件,不蕴含子元素;
  • .once:只会触发一次。

Vue 单页利用与多页利用的区别

概念:

  • SPA 单页面利用(SinglePage Web Application),指只有一个主页面的利用,一开始只须要加载一次 js、css 等相干资源。所有内容都蕴含在主页面,对每一个功能模块组件化。单页利用跳转,就是切换相干组件,仅仅刷新部分资源。
  • MPA 多页面利用(MultiPage Application),指有多个独立页面的利用,每个页面必须反复加载 js、css 等相干资源。多页利用跳转,须要整页资源刷新。

参考 前端进阶面试题具体解答

Computed 和 Watch 的区别

对于 Computed:

  • 它反对缓存,只有依赖的数据产生了变动,才会从新计算
  • 不反对异步,当 Computed 中有异步操作时,无奈监听数据的变动
  • computed 的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于 data 申明过,或者父组件传递过去的 props 中的数据进行计算的。
  • 如果一个属性是由其余属性计算而来的,这个属性依赖其余的属性,个别会应用 computed
  • 如果 computed 属性的属性值是函数,那么默认应用 get 办法,函数的返回值就是属性的属性值;在 computed 中,属性有一个 get 办法和一个 set 办法,当数据发生变化时,会调用 set 办法。

对于 Watch:

  • 它不反对缓存,数据变动时,它就会触发相应的操作
  • 反对异步监听
  • 监听的函数接管两个参数,第一个参数是最新的值,第二个是变动之前的值
  • 当一个属性发生变化时,就须要执行相应的操作
  • 监听数据必须是 data 中申明的或者父组件传递过去的 props 中的数据,当发生变化时,会触发其余操作,函数有两个的参数:

    • immediate:组件加载立刻触发回调函数
    • deep:深度监听,发现数据外部的变动,在简单数据类型中应用,例如数组中的对象发生变化。须要留神的是,deep 无奈监听到数组和对象外部的变动。

当想要执行异步或者低廉的操作以响应一直的变动时,就须要应用 watch。

总结:

  • computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值产生扭转,下一次获取 computed 的值时才会从新计算 computed 的值。
  • watch 侦听器 : 更多的是 察看 的作用,无缓存性,相似于某些数据的监听回调,每当监听的数据变动时都会执行回调进行后续操作。

使用场景:

  • 当须要进行数值计算, 并且依赖于其它数据时,应该应用 computed,因为能够利用 computed 的缓存个性,防止每次获取值时都要从新计算。
  • 当须要在数据变动时执行异步或开销较大的操作时,应该应用 watch,应用 watch 选项容许执行异步操作 (拜访一个 API),限度执行该操作的频率,并在失去最终后果前,设置中间状态。这些都是计算属性无奈做到的。

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 时,能够告诉依赖更新,如果数组中蕴含着援用类型。会对数组中的援用类型再次进行监控。

手写简版剖析

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 inserted
        switch (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 的父子组件生命周期钩子函数执行程序

  • 渲染程序:先父后子,实现程序:先子后父
  • 更新程序:父更新导致子更新,子更新实现后父
  • 销毁程序:先父后子,实现程序:先子后父

加载渲染过程

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

子组件更新过程

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

父组件更新过程

beforeUpdate-> 父 updated

销毁过程

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

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

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 组件 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 = this
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (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 = Sub
    Sub.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 usage
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.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-lookup
    if (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.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

Vue 中给 data 中的对象属性增加一个新的属性时会产生什么?如何解决?

<template> 
   <div>
      <ul>
         <li v-for="value in obj" :key="value"> {{value}} </li>       </ul>       <button @click="addObjB"> 增加 obj.b</button>    </div>
</template>

<script>
    export default {data () {return {               obj: {                   a: 'obj.a'}           }        },       methods: {addObjB () {this.obj.b = 'obj.b'               console.log(this.obj)           }       }   }
</script>

点击 button 会发现,obj.b 曾经胜利增加,然而视图并未刷新。这是因为在 Vue 实例创立时,obj.b 并未申明,因而就没有被 Vue 转换为响应式的属性,天然就不会触发视图的更新,这时就须要应用 Vue 的全局 api $set():

addObjB () (this.$set(this.obj, 'b', 'obj.b')
   console.log(this.obj)
}

$set()办法相当于手动的去把 obj.b 解决成一个响应式的属性,此时视图也会跟着扭转了。

$nextTick 原理及作用

Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种利用。

nextTick 的外围是利用了如 Promise、MutationObserver、setImmediate、setTimeout 的原生 JavaScript 办法来模仿对应的微 / 宏工作的实现,实质是为了利用 JavaScript 的这些异步回调工作队列来实现 Vue 框架中本人的异步回调队列。

nextTick 不仅是 Vue 外部的异步队列的调用办法,同时也容许开发者在理论我的项目中应用这个办法来满足理论利用中对 DOM 更新数据机会的后续逻辑解决

nextTick 是典型的将底层 JavaScript 执行原理利用到具体案例中的示例,引入异步更新队列机制的起因∶

  • 如果是同步更新,则屡次对一个或多个属性赋值,会频繁触发 UI/DOM 的渲染,能够缩小一些无用渲染
  • 同时因为 VirtualDOM 的引入,每一次状态发生变化后,状态变动的信号会发送给组件,组件外部应用 VirtualDOM 进行计算得出须要更新的具体的 DOM 节点,而后对 DOM 进行更新操作,每次更新状态后的渲染过程须要更多的计算,而这种无用功也将节约更多的性能,所以异步渲染变得更加至关重要

Vue 采纳了数据驱动视图的思维,然而在一些状况下,依然须要操作 DOM。有时候,可能遇到这样的状况,DOM1 的数据产生了变动,而 DOM2 须要从 DOM1 中获取数据,那这时就会发现 DOM2 的视图并没有更新,这时就须要用到了 nextTick 了。

因为 Vue 的 DOM 操作是异步的,所以,在下面的状况中,就要将 DOM2 获取数据的操作写在 $nextTick 中。

this.$nextTick(() => {    // 获取数据的操作...})

所以,在以下状况下,会用到 nextTick:

  • 在数据变动后执行的某个操作,而这个操作须要应用随数据变动而变动的 DOM 构造的时候,这个操作就须要办法在 nextTick() 的回调函数中。
  • 在 vue 生命周期中,如果在 created()钩子进行 DOM 操作,也肯定要放在 nextTick() 的回调函数中。

因为在 created()钩子函数中,页面的 DOM 还未渲染,这时候也没方法操作 DOM,所以,此时如果想要操作 DOM,必须将操作的代码放在 nextTick() 的回调函数中。

v-if、v-show、v-html 的原理

  • v-if 会调用 addIfCondition 办法,生成 vnode 的时候会疏忽对应节点,render 的时候就不会渲染;
  • v-show 会生成 vnode,render 的时候也会渲染成实在节点,只是在 render 过程中会在节点的属性中批改 show 属性值,也就是常说的 display;
  • v-html 会先移除节点下的所有节点,调用 html 办法,通过 addProp 增加 innerHTML 属性,归根结底还是设置 innerHTML 为 v -html 的值。

assets 和 static 的区别

相同点: assetsstatic 两个都是寄存动态资源文件。我的项目中所须要的资源文件图片,字体图标,款式文件等都能够放在这两个文件下,这是相同点

不相同点:assets 中寄存的动态资源文件在我的项目打包时,也就是运行 npm run build 时会将 assets 中搁置的动态资源文件进行打包上传,所谓打包简略点能够了解为压缩体积,代码格式化。而压缩后的动态资源文件最终也都会搁置在 static 文件中跟着 index.html 一起上传至服务器。static 中搁置的动态资源文件就不会要走打包压缩格式化等流程,而是间接进入打包好的目录,间接上传至服务器。因为防止了压缩间接进行上传,在打包时会进步肯定的效率,然而 static 中的资源文件因为没有进行压缩等操作,所以文件的体积也就绝对于 assets 中打包后的文件提交较大点。在服务器中就会占据更大的空间。

倡议: 将我的项目中 template须要的款式文件 js 文件等都能够搁置在 assets 中,走打包这一流程。缩小体积。而我的项目中引入的第三方的资源文件如iconfoont.css 等文件能够搁置在 static 中,因为这些引入的第三方文件曾经通过解决,不再须要解决,间接上传。

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>

革新一下代码

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

略微革新一下

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

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

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

  • 监控整个 reactive 对象,从下面的图能够看到 deep 理论默认是开启的,就算咱们设置为 false 也还是有效。而且旧值获取不到。
  • 要获取旧值则须要监控对象的属性,也就是监听一个getter,看下图

总结

  • 如果定义了 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 实现。

Vue 模版编译原理晓得吗,能简略说一下吗?

简略说,Vue 的编译过程就是将 template 转化为 render 函数的过程。会经验以下阶段:

  • 生成 AST 树
  • 优化
  • codegen

首先解析模版,生成AST 语法树(一种用 JavaScript 对象的模式来形容整个模板)。应用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相干解决。

Vue 的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变动,对应的 DOM 也不会变动。那么优化过程就是深度遍历 AST 树,依照相干条件对树节点进行标记。这些被标记的节点 (动态节点) 咱们就能够 跳过对它们的比对,对运行时的模板起到很大的优化作用。

编译的最初一步是 将优化后的 AST 树转换为可执行的代码

v-for 为什么要加 key

如果不应用 key,Vue 会应用一种最大限度缩小动静元素并且尽可能的尝试就地批改 / 复用雷同类型元素的算法。key 是为 Vue 中 vnode 的惟一标记,通过这个 key,咱们的 diff 操作能够更精确、更疾速

更精确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 比照中能够防止就地复用的状况。所以会更加精确。

更疾速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历形式更快

你有对 Vue 我的项目进行哪些优化?

(1)代码层面的优化

  • v-if 和 v-show 辨别应用场景
  • computed 和 watch 辨别应用场景
  • v-for 遍历必须为 item 增加 key,且防止同时应用 v-if
  • 长列表性能优化
  • 事件的销毁
  • 图片资源懒加载
  • 路由懒加载
  • 第三方插件的按需引入
  • 优化有限列表性能
  • 服务端渲染 SSR or 预渲染

(2)Webpack 层面的优化

  • Webpack 对图片进行压缩
  • 缩小 ES6 转为 ES5 的冗余代码
  • 提取公共代码
  • 模板预编译
  • 提取组件的 CSS
  • 优化 SourceMap
  • 构建后果输入剖析
  • Vue 我的项目的编译优化

(3)根底的 Web 技术的优化

  • 开启 gzip 压缩
  • 浏览器缓存
  • CDN 的应用
  • 应用 Chrome Performance 查找性能瓶颈

Proxy 与 Object.defineProperty 优劣比照

Proxy 的劣势如下:

  • Proxy 能够间接监听对象而非属性;
  • Proxy 能够间接监听数组的变动;
  • Proxy 有多达 13 种拦挡办法, 不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
  • Proxy 返回的是一个新对象, 咱们能够只操作新的对象达到目标, 而 Object.defineProperty 只能遍历对象属性间接批改;

Proxy 作为新规范将受到浏览器厂商重点继续的性能优化,也就是传说中的新规范的性能红利;

Object.defineProperty 的劣势如下:

  • 兼容性好,反对 IE9,而 Proxy 的存在浏览器兼容性问题, 而且无奈用 polyfill 磨平,因而 Vue 的作者才申明须要等到下个大版本 (3.0) 能力用 Proxy 重写。

computed 的实现原理

computed 实质是一个惰性求值的观察者。

computed 外部实现了一个惰性的 watcher, 也就是 computed watcher,computed watcher 不会立即求值, 同时持有一个 dep 实例。

其外部通过 this.dirty 属性标记计算属性是否须要从新求值。

当 computed 的依赖状态产生扭转时, 就会告诉这个惰性的 watcher,

computed watcher 通过 this.dep.subs.length 判断有没有订阅者,

有的话, 会从新计算, 而后比照新旧值, 如果变动了, 会从新渲染。(Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 从新渲染,实质上是一种优化。)

没有的话, 仅仅把 this.dirty = true。(当计算属性依赖于其余数据时,属性并不会立刻从新计算,只有之后其余中央须要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)个性。)

正文完
 0