3 Star 4 Fork 1

SamuelSue / 前端面试

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
面试-框架篇.md 29.51 KB
一键复制 编辑 原始数据 按行查看 历史
SamuelSue 提交于 2021-06-30 00:39 . Vue篇完成

面试-框架篇

Vue原理

1. 如何理解MVVM

MVVM

MVVM: M(Model),V(View),VM(ViewModel),即视图和数据模型之间通过ViewModel实现了双向绑定,数据的修改会引发视图的更新,这里视图表示的就是DOM元素,即数据驱动视图,而DOM通过事件监听可以来触发数据的修改。

2. 如何监听数据的变化(对象和数组)

对于对象(data),核心API就是-Object.defineProperty(obj, prop, descriptor),通过对descriptor对象中的set函数做劫持,达到对于对象属性改变的监听。Vue通过defineReactive函数对于对象进行深度监听。

对于数组,考虑到不能污染全局Array的原型,Vue在监听数组时采用劫持数组对象实例的原型,重写了数组push,pop,shift,unshift,splice,sort,reverse方法,再其执行时触发视图更新的逻辑。

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        updateView() // 触发视图更新
        oldArrayProperty[methodName].call(this, ...arguments)
        // Array.prototype.push.call(this, ...arguments)
    }
})

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
    // 深度监听
    observer(value)

    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 深度监听
                observer(newValue)

                // 设置新值
                // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
                value = newValue

                // 触发更新视图
                updateView()
            }
        }
    })
}

// 监听对象属性
function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 不是对象或数组
        return target
    }

    // 污染全局的 Array 原型
    // Array.prototype.push = function () {
    //     updateView()
    //     ...
    // }

    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }

    // 重新定义各个属性(for in 也可以遍历数组)
    for (let key in target) {
        defineReactive(target, key, target[key])
    }
}

// 准备数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        address: '北京' // 需要深度监听
    },
    nums: [10, 20, 30]
}

// 监听数据
observer(data)

Object.defineProperty的缺点

  • 深度监听需要递归到底,一次性计算量大
  • 无法监听新增属性/删除属性(Vue.set, Vue.delete)

对于数组监听的局限

  • 无法监听arr[index]的赋值,需要使用Vue.set
  • 无法监听arr.length的赋值

3. VDOM(Snabbdom)原理

为什么要有vDOM? 因为DOM操作非常耗费性能,考虑把DOM的计算转移到JS中计算,因为JS的执行速度较快,于是就产生了vDOM,即通过JS模拟DOM结构,计算出最小的变更,操作DOM。

Vue2的VDOM参考了Snabbdom,所以通过Snabbdom来了解VDOM会比较直观

// 官方例子
import {
  init,
  classModule,
  propsModule,
  styleModule,
  eventListenersModule,
  h,
} from "snabbdom";

const patch = init([
  // Init patch function with chosen modules
  classModule, // makes it easy to toggle classes
  propsModule, // for setting properties on DOM elements
  styleModule, // handles styling on elements with support for animations
  eventListenersModule, // attaches event listeners
]);
// 上面是初始化,不用深入

const container = document.getElementById("container");

// h函数(类似Vue的render函数,返回值为一个vnode)
const vnode = h("div#container.two.classes", { on: { click: someFn } }, [
  h("span", { style: { fontWeight: "bold" } }, "This is bold"),
  " and this is just normal text",
  h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);
// Patch into empty DOM element – this modifies the DOM as a side effect
// patch函数是关键,有两种情况,一种是patch(DOMElement,vnode),类似初次渲染vnode到DOM结点上
patch(container, vnode);

// 新的vnode
const newVnode = h(
  "div#container.two.classes",
  { on: { click: anotherEventHandler } },
  [
    h(
      "span",
      { style: { fontWeight: "normal", fontStyle: "italic" } },
      "This is now italic type"
    ),
    " and this is still just normal text",
    h("a", { props: { href: "/bar" } }, "I'll take you places!"),
  ]
);
// Second `patch` invocation
// patch的第二种情况,即VNode数据改变,patch(oldVnode,newVNode);
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state

VNode的diff算法

传统的diff算法,时间复杂度太高,几乎不可用,vue的diff算法将时间复杂度优化到O(N),具体优化措施为

  • 只比较同一层级,不跨级比较
  • tag不相同,直接删掉重建,不再深度比较
  • tag和key相同,认为两者都相同,即为同一结点,不再深度比较

diff关键-patch过程

return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
        let i: number, elm: Node, parent: Node;

        // 用于收集所有插入的元素
        const insertedVnodeQueue: VNodeQueue = [];

        // 先调用 pre 回调
        for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

        // 如果老节点非 vnode , 则创建一个空的 vnode
        if (!isVnode(oldVnode)) {
            oldVnode = emptyNodeAt(oldVnode);
        }

        // 如果是同个节点,则进行修补
        if (sameVnode(oldVnode, vnode)) {
            patchVnode(oldVnode, vnode, insertedVnodeQueue);
        } else {
            // 不同 Vnode 节点则新建
            elm = oldVnode.elm as Node;
            parent = api.parentNode(elm);

            createElm(vnode, insertedVnodeQueue);

            // 插入新节点,删除老节点
            if (parent !== null) {
                api.insertBefore(
                    parent,
                    vnode.elm as Node,
                    api.nextSibling(elm)
                );
                removeVnodes(parent, [oldVnode], 0, 0);
            }
        }

        // 遍历所有收集到的插入节点,调用插入的钩子,
        for (i = 0; i < insertedVnodeQueue.length; ++i) {
            (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks)
                .insert as any)(insertedVnodeQueue[i]);
        }
        // 调用post的钩子
        for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();

        return vnode;
    };
  • 触发 pre 钩子
  • 如果老节点非 vnode, 则新创建空的 vnode
  • 新旧节点为 sameVnode 的话,则调用 patchVnode 更新 vnode , 否则创建新节点
  • 触发收集到的新元素 insert 钩子
  • 触发 post 钩子

diff关键-patchVnode 过程

  • 触发 prepatch 钩子
  • 触发 update 钩子, 这里主要为了更新对应的 module 内容
  • 非文本节点的情况 , 调用 updateChildren 更新所有子节点
  • 文本节点的情况 , 直接 api.setTextContent(elm, vnode.text as string);
function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    // 执行 prepatch hook
    const hook = vnode.data?.hook;
    hook?.prepatch?.(oldVnode, vnode);

    // 设置 vnode.elem
    const elm = vnode.elm = oldVnode.elm!;
  
    // 旧 children
    let oldCh = oldVnode.children as VNode[];
    // 新 children
    let ch = vnode.children as VNode[];

    if (oldVnode === vnode) return;
  
    // hook 相关
    if (vnode.data !== undefined) {
      for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
      vnode.data.hook?.update?.(oldVnode, vnode);
    }

    // vnode.text === undefined (vnode.children 一般有值, 非文本节点)
    if (isUndef(vnode.text)) {
      // 新旧都有 children
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
      // 新 children 有,旧 children 无 (旧 text 有)
      } else if (isDef(ch)) {
        // 清空 text
        if (isDef(oldVnode.text)) api.setTextContent(elm, '');
        // 添加 children
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
      // 旧 child 有,新 child 无
      } else if (isDef(oldCh)) {
        // 移除 children
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      // 旧 text 有
      } else if (isDef(oldVnode.text)) {
        api.setTextContent(elm, '');
      }

    // else : vnode.text !== undefined (vnode.children 无值)
    } else if (oldVnode.text !== vnode.text) {
      // 移除旧 children
      if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      }
      // 设置新 text
      api.setTextContent(elm, vnode.text!);
    }
    hook?.postpatch?.(oldVnode, vnode);
  }

diff关键-updateChildren

patchVnode 里面最重要的方法,也是整个 diff 里面的最核心方法

updateChildren 主要的逻辑如下:

  1. 优先处理特殊场景,先对比两端。也就是
    • 旧 vnode 头 vs 新 vnode 头
    • 旧 vnode 尾 vs 新 vnode 尾
    • 旧 vnode 头 vs 新 vnode 尾
    • 旧 vnode 尾 vs 新 vnode 头
  2. 首尾不一样的情况,寻找 key 相同的节点,找不到则新建元素(addVnodes函数里面有一个createElm,用于创建DOM)
  3. 如果找到 key,但是,元素选择器变化了,也新建元素(addVnodes函数里面有一个createElm)
  4. 如果找到 key,并且元素选择没变, 则移动元素
  5. 两个列表对比完之后,清理多余的元素,新增添加的元素

不提供 key 的情况下,如果只是顺序改变的情况,例如第一个移动到末尾。这个时候,会导致其实更新了后面的所有元素。这也是为什么v-for生成的结点要有key

4. 模板编译

前置知识——with语法

const obj = {a:100,b:200};
with(obj){
    // with会改变{}作用域内自由变量的查找方式,这里将自由变量当做obj的属性来查找
    console.log(a); // 100
    console.log(b); // 200
    console.log(c);  // 报错
}

模板中的条件渲染、指令、循环、事件都是编译成JS代码来计算和执行的,利用vue-template-compiler可以查看模板编译后的代码

// // 表达式
const template = `<p>{{flag ? message : 'no message found'}}</p>`
const res = compiler.compile(template)
console.log(res.render)
// // with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}

// // 条件
const template = `
     <div>
         <p v-if="flag === 'a'">A</p>
         <p v-else>B</p>
     </div>
 `
// with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}

// // 从 vue 源码中找到缩写函数的含义
// function installRenderHelpers (target) {
//     target._o = markOnce;
//     target._n = toNumber;
//     target._s = toString;
//     target._l = renderList;
//     target._t = renderSlot;
//     target._q = looseEqual;
//     target._i = looseIndexOf;
//     target._m = renderStatic;
//     target._f = resolveFilter;
//     target._k = checkKeyCodes;
//     target._b = bindObjectProps;
//     target._v = createTextVNode;
//     target._e = createEmptyVNode;
//     target._u = resolveScopedSlots;
//     target._g = bindObjectListeners;
//     target._d = bindDynamicKeys;
//     target._p = prependModifier;
// }

/* _c 是vue的createElement,类似h函数,返回值是一个vNode

模板编译的结果是一个render函数,执行render函数返回vnode,基于vnode再执行patch和diff

5. Vue组件是如何渲染和更新的

初次渲染:

  1. 解析模板为render函数
  2. 触发响应式,监听data属性getter(收集依赖,被Watcher监听),setter
  3. 执行render函数,生成vnode, 执行patch(elem, vnode)

更新过程:

  1. 修改data,触发setter(此前在getter中已被监听)
  2. 重新执行render函数,生成newVnode
  3. patch(vnode,newVnode)

Vue渲染与更新

6. Vue的异步渲染

new Vue({
    template: '<div>{{val}}</div>', 
    data(){
        return {
            val: 'init'
        }
    },
    mounted(){
        this.val = '第一次修改';
        this.val = '第二次修改'
      }  
})

上面这一段代码中,在mounted里给val属性进行了两次赋值,如果页面渲染与数据的变化完全同步的话,页面应该是在mounted里有两次渲染。

而由于Vue内部的渲染机制,实际上页面只会渲染一次,把第一次的赋值所带来的的响应与第二次的赋值所带来的的响应进行一次合并,将最终的val只做一次页面渲染。

整个过程可以简述为以下过程:

  1. 数据重新赋值,触发setter回调中的dep.notify()
  2. dep.notify()中依次调用watcher.update()。 dep中的subs存储的就是watcher。
  notify () { // 通知存储的依赖更新
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    // 循环watcher,发布订阅模式
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // 调用watcher中的update方法
    }
  }
  1. *watcher.update()中会使用queueWatcher()*将watcher通过id去重(多次修改同一数据,只会触发一次渲染)放到队列中

    export function queueWatcher (watcher: Watcher) {
      const id = watcher.id // 过滤watcher,每个watcher有一个id,多个属性依赖同一个watcher
      if (has[id] == null) { // 如果没有就会添加进去
        has[id] = true
        if (!flushing) {
          queue.push(watcher) // 并且将watcher放到队列中去
        } else {
          let i = queue.length - 1
          while (i > index && queue[i].id > watcher.id) {
            i--
          }
          queue.splice(i + 1, 0, watcher)
        }
        if (!waiting) {
          waiting = true
     
          if (process.env.NODE_ENV !== 'production' && !config.async) {
            flushSchedulerQueue() // 会做一个清空queue的操作
            return
          }
          nextTick(flushSchedulerQueue) // 在下一个tick中刷新watcher队列
        }
      }
    }
  2. 在nextTick回调中执行flushSchedulerQueue(), 取出每一个队列中的watcher,调用watcher.run()

    function flushSchedulerQueue () {
      currentFlushTimestamp = getNow()
      flushing = true
      let watcher, id
      queue.sort((a, b) => a.id - b.id)
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        if (watcher.before) {
          watcher.before() // 触发一个before方法
        }
        id = watcher.id
        has[id] = null
        watcher.run() // 开始执行watcher,执行完页面就渲染完成啦
        if (process.env.NODE_ENV !== 'production' && has[id] != null) {
          circular[id] = (circular[id] || 0) + 1
          if (circular[id] > MAX_UPDATE_COUNT) {
            warn(
              'You may have an infinite update loop ' + (
                watcher.user
                  ? `in watcher with expression "${watcher.expression}"`
                  : `in a component render function.`
              ),
              watcher.vm
            )
            break
          }
        }
      }
      const activatedQueue = activatedChildren.slice()
      const updatedQueue = queue.slice()
      resetSchedulerState()
      callActivatedHooks(activatedQueue)
      callUpdatedHooks(updatedQueue) // 更新完成后会调用updated钩子
      if (devtools && config.devtools) {
        devtools.emit('flush')
      }
    }
  3. *watcher.run()*中会调用wacher.get(), 然后调用实例的getter方法,实例的getter方法实际是在实例化的时候传入的函数,也就是下面vm的真正更新函数_update。实例的_update函数执行后,将会把两次的虚拟节点传入传入vm的 patch 方法执行渲染操作。

    Vue.prototype._update = function (vnode, hydrating) {  var vm = this;  ... var prevVnode = vm._vnode; vm._vnode = vnode; if (!prevVnode) {   // initial render  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);  } else {    // updates    vm.$el = vm.__patch__(prevVnode, vnode);   } ...};

7. 前端路由的原理

hash路由

// hash变化不会引起页面刷新window.onhashchange = (event)=>{	// event.oldURL    // event.newURL    // location.hash}

H5 history路由

  • 使用url规范的路由,但是跳转的时候不会刷新页面

  • history.pushState(state, title, url)

  • window.onpopstate

$('.btn').click(()=>{    const state = {name:'history路由'};    history.pushState(state, '标题参数', 'page1'); // 这里路由跳转了})// 监听路由器前进 or 后退window.onpopstate = (event) => {    console.log(event.state, location.pathname)}

history路由的实现需要后端配合,否则手动刷新页面的时候会发送http请求到后端,而不是前端处理路由。

Vue面试真题

1. 为何在v-for中使用key

首先,key必须具有唯一性,不要使用index或者random。

diff算法中,通过key和tagName来判断是否是sameNode,这样不需要大量的销毁重建DOM,可以做更加细化的判断,减少渲染次数,提升渲染性能

2. Vue组件如何通信

父子组件: props | $emit事件

非父子组件: eventBus,vuex, provide/inject

3. 为何data必须是一个函数, return一个对象

当一个组件被定义,data必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例

也就是说,在很多页面中,定义的组件可以复用在多个页面

如果data是一个纯碎的对象,则所有的实例将共享引用同一份data数据对象,无论在哪个组件实例中修改data,都会影响到所有的组件实例

如果data是函数,每次创建一个新实例后,调用data函数,从而返回初始数据的一个全新副本数据对象

这样每复用一次组件,会返回一份新的data数据,类似于给每个组件实例创建一个私有的数据空间,让各个组件的实例各自独立,互不影响,保持低耦合

Vue3部分

1. Vue3对比Vue2的优势

  • 性能更好
  • 体积更小
  • 更好的ts支持
  • 更好的代码组织(Composition API)
  • 更好的代码抽离(Composition API)

2. Vue3的生命周期

Options API生命周期:

  • beforeDestroy -> beforeUnmounted
  • destroy -> unmounted

Compostion API:

  • setup函数相当于beforeCreate & created, 如果同时定义了setup和beforeCreate & create钩子,setup最先执行
  • setup里面没有onBeforeCreate和onCreated钩子

3. Composition API VS Options API

Composition API带来了什么?

  • 更好的代码组织

    同一块逻辑所涉及的函数、响应式数据可以放在一块,而不是分散的放在data和methods中

  • 更好的逻辑复用

    功能代码可以抽离成单个函数,方便复用,而原先实现一个功能的data和method是分离的

  • 更好的类型推导

    Vue组件定义时的methods和data最后都是挂载到vue实例上的(this.xxx可以访问),不利于类型的推导

简单功能使用Options API,复杂功能使用Composition API

4. 理解ref, toRef和toRefs

ref是什么

  1. 生成值类型的响应式数据
  2. 可用于模板和reactive
  3. 通过**.value**修改值

toRef

将reactive的对象的一个属性创建ref,并保持引用关系

export default {
    setup(){
        const state = reactive({
            age: 20,
            name: "test"
        })
        const nameRef = toRef(state, 'name');
        return {
            state,
            nameRef
        }
    }
}

toRefs

将一个reactive的对象变成普通对象,这个普通对象的每个属性都是ref

export default {
    setup(){
        const state = reactive({
            age: 20,
            name: "test"
        })
        const stateAsRefs = toRefs(state);  // stateAsRefs是一个普通对象
        // const {age:ageRef, name:nameRef} = stateAsRefs;
        // return {ageRef,nameRef}
        return stateAsRefs  // 注意这里实际是返回{age,name},只是这所有的属性都是ref
    }
}

实际上作用就是可以把reactive的对象解构,在模板中不用写reactiveObj.xxx,因为reactiveObj如果解构的话,就没有响应式了。

为何需要ref?(为什么不全用reactive)

  • 值类型的数据会丢失响应式
  • 在setup, computed,合成函数(useXXX)中都有可能返回值类型,不能限制用户的使用

为什么ref类型需要.value

ref作用于值类型,因为我们无法对基础数据类型值做拦截,只能将基础数据类型转换成了对应的包装类实例,比如

// let str = "abc"; 基础数据类型无法做拦截// 转化成包装类, 再拦截let str = new String('abc');str.valueOf(); // 'abc';

5. Vue3升级的功能

  • creatApp

    -2021-06-27-185605.jpg

  • emits属性: 声明事件

    47ea23b67e90992b57a40a73be3c7cc4.png

  • 生命周期

  • 多事件

    9b158031bff69e296a0eb93d01364e32.png

  • Fragment

  • 移除.sync

  • 异步组件写法

  • 移除filter

  • Teleport

  • Suspense

    f9f2fb6a9a18b086b6ac2989326ea39d.png

  • Composition API

6. Composition API如何实现代码复用

// useMousePosition.js
import { reactive, ref, onMounted, onUnmounted } from 'vue'

function useMousePosition() {
    const x = ref(0)
    const y = ref(0)

    function update(e) {
        x.value = e.pageX
        y.value = e.pageY
    }

    onMounted(() => {
        console.log('useMousePosition mounted')
        window.addEventListener('mousemove', update)
    })

    onUnmounted(() => {
        console.log('useMousePosition unMounted')
        window.removeEventListener('mousemove', update)
    })

    return {
        x,
        y
    }
}
export default useMousePosition

// 分割===========================================
// 组件中
export default {
    name: 'MousePosition',
    setup() {
        const { x, y } = useMousePosition()
        return {
            x,
            y
        }
    }
}

7. Vue3的响应式实现

// 依赖收集
const targetMap = new WeakMap(); // 存放多个对象的响应式数据

let activeEffect = null; // 表示当前运行的 effect

// 注册effect
function effect(eff) {
  activeEffect = eff;
  activeEffect(); // 调用eff的时候,会触发proxy.get的逻辑, 进而在track中将eff收集到deps中
  activeEffect = null;
}

// 响应式依赖收集
function track(target, key) {
  if (activeEffect) { // 防止多次重复的依赖收集
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()));
    }
    let deps = depsMap.get(key);
    if (!deps) {
      depsMap.set(key, (deps = new Set()));
    }
    deps.add(activeEffect);
  }
}

// 触发effect更新
function trigger(target, key) {
  let depsMap = targetMap.get(target);
  if (depsMap) {
    let deps = depsMap.get(key);
    if (deps) {
      deps.forEach((effect) => effect());
    }
  }
}

// Proxy的handler
const reactiveHandler = {
  get(target, key, receiver) {
    track(target, key); // 只在effect注册的时候执行一次track收集依赖
    return Reflect.get(target, key, receiver);
    // ######!!如果要实现深度监听,return reactive(Reflect.get(target, key, receiver))!!!!!!#######
  },
  set(target, key, value, receiver) {
    const oldVal = target[key];
    console.log(`oldValue:${oldVal}, value:${value}`);
    const res = Reflect.set(target, key, value, receiver); // 先赋值,再触发effect计算
    if (oldVal !== value) {
      trigger(target, key);
    }
    return Reflect.set(target, key, value, receiver);
  },
  deleteProperty(target, key) {
    return Reflect.deleteProperty(target, key);
  },
};

function reactive(obj) {
  return new Proxy(obj, reactiveHandler);
}

function ref(raw) {
  const r = {
    get value() {
      track(r, "value");
      return raw;
    },
    set value(newVal) {
      raw = newVal;
      trigger(r, "value");
    },
  };
  return r;
}

function computed(getter) {
  const r = ref();
  effect(() => {
    r.value = getter();
  });
  return r;
}

实例代码:

let obj = reactive({ a: 10, b: 20 });let timesA = ref(0);let sum = 0;effect(() => {    timesA.value = obj.a * 10;});sum = computed(()=>{    return timesA.value + obj.b;})console.log(timesA); // 100obj.a = 100;  // obj.a更新console.log(obj.a); // 多次使用obj.a的getter(之前在effect里面用过),只会收集一次依赖console.log(timesA.value); // 1000console.log(sum.value); // 期望 1020

对比Vue2响应式的实现Object.defineProperty的优势

  • 深度监听,性能更好(在getter触发时才会深度递归,Vue2的defineReactive是运行的第一步就递归监听)
  • 原生就支持属性新增和删除(proxyhandler.deleteProperty(target, key))
  • 可监听数组变化

Proxy实现响应式的局限(或者说Proxy的局限)

  • 无法兼容所有浏览器,无法polyfill

8. watch和watchEffect的区别

  • 两者都可以监听data属性变化
  • watch需要明确监听哪个属性
  • watchEffect会根据其中的属性,自动监听其变化
export default {    setup(){        const numberRef = ref(0);        const state = reactive({            name: 'Sam',            age: 26        })        // 监听ref         watch(numberRef, (newVal, oldVal)=>{            console.log(newVal);        },{            immediate: true        })        // 监听reactive, 第一个参数是一个函数,返回需要监听的属性        watch(()=>state.age,(newVal)=>{            console.log(newValu);        })                // watchEffect,初始化时一定会执行一次,因为要收集依赖        watchEffect(()=>{            console.log('修改了:',state.name);            console.log('修改了:',state.age);        })                watchEffect(()=>{            console.log('只监听age:', state.age)        })                // 如果延迟修改state.age,则上述两个watchEffect都会执行                // ...        return {            numberRef,            state        }    }}

watchEffect高阶使用

/* onInvalidate作为一个参数传递,目的是消除副作用比如下面这个例子, id.value在短时间之内触发了多次修改,导致多次异步请求,但是无法保证响应回来的顺序的正确性因此要取消之前的请求*/watchEffect(onInvalidate => {  const token = performAsyncOperation(id.value)  onInvalidate(() => {    // id has changed or watcher is stopped.    // invalidate previously pending async operation    token.cancel()  })})
/*watchEffect的副作用函数是一个异步函数的情况需要在最前面注册清楚函数(异步函数都会隐式地返回一个 Promise, 必须要在Promise被resolve之前注册)*/const data = ref(null)watchEffect(async onInvalidate => {   onInvalidate(() => { /* ... */ }) // 我们在Promise解析之前注册清除函数  data.value = await fetchData(props.id)})

9. setup中如何获取组件实例

  • getCurrentInstance()获取当前实例
import { onMounted, getCurrentInstance } from 'vue'
export default{
    data() {
        return {
            x: 1,
            y: 2
        }
    },
    setup() {
        console.log('this1', this)

        onMounted(() => {
            console.log('this in onMounted', this)
            console.log('x', instance.data.x)  // 注意setup函数是beforeCreate和created的合集,实例还没初始化的时候,没有data.x
        })

        const instance = getCurrentInstance()
        console.log('instance', instance)
},
}

10. Vue3为何比Vue2快

  • Proxy响应式

  • PatchFlag

    • 编译模板的时候,动态结点会打上标记(render函数的返回值里面,createVnode的时候)
    • 标记分为不同类型,比如TEXT, PROPS, CLASS
    • diff算法的时候,可以区分静态结点,以及不同类型的动态结点
  • hoistStatic

    • 静态结点的定义提升到父作用域,缓存起来
    • 多个相邻的静态结点,会被合并起来
    • 典型的拿空间换时间策略
  • cacheHandler

    • 缓存事件
  • SSR优化

    • 静态结点直接输出,绕过vdom
  • tree-shaking

    • 编译时根据不同情况,引入不同的API

11. Vite相关

Vite为何启动快

  • 开发环境使用ES Module,无需打包
  • 生产环境使用rollup打包,并不会快很多

ES Module相关特性

  • <script type='module'>支持import写法,无需打包编译

  • 支持远程引入import {xxx} from 'http://.........mjs'

  • 支持动态引入(异步引入)

    <script type="module">
        btn.onClick = async () => {
            const add = await import("./src/add.js");
            console.log(add);  // Module对象, 有一个default指向add函数
            add.default(1,2);
            
            const {multi} = await import('./src/math.js');
            multi(1,2); // 结构的不用default
        }
    </script>
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
JavaScript
1
https://gitee.com/samuelsue/frontend_interview.git
git@gitee.com:samuelsue/frontend_interview.git
samuelsue
frontend_interview
前端面试
master

搜索帮助

344bd9b3 5694891 D2dac590 5694891