前端面试笔记

164 阅读9分钟

HTML部分

一、HTML语义化

概念题,需要回答:「是什么、怎么做、解决了什么问题、优点是、缺点是、怎么解决缺点」

答:

  1. 是什么: 用正确的标签来写代码
  2. 怎么做: 比如标题用 h1~h6,段落用 p, 文章用 article ,主要内容用 main ,边栏用 aside, 导航用 nav 等等(找到中文对应)
  3. 解决啥: 明确了HTML的书写规范
  4. 优点是: 一、更适合搜索引擎检索,二、适合阅读,便于维护
  5. 没有缺点

二、使用过哪些HTML标签

记忆题,不要提自己不熟悉的标签,不然很可能会变成下一道题。

  • 文章相关:header main footer article
  • 多媒体: video canvas audio
  • 表单相关: type=email type=tel 更多的可以去MDN了解相关使用教程

CSS部分

一、盒模型

盒模型:内容,内边距,边框,外边距组成

  • content-box:width只包含 内容
  • border-box: width包含 内容+内边距+边框

二、如何实现垂直居中

具体看如何实现垂直居中

三、BFC

  • 是什么: 块级格式化上下文
  • 怎么做: BFC的触发条件:
    1. 浮动元素(元素的float不是none)
    2. 绝对定位元素(元素的position为absolute或fixed)
    3. 行内块 inline block 元素
    4. overflow 值不为 visible 的块元素
    5. 弹性元素(display为 flex 或 inline-flex元素的直接子元素)
  • 解决啥:
    1. 清除浮动
    2. 防止 margin 合并

CSS选择器的优先级

  1. 选择器越具体,优先级越高
  2. 相同优先级,出现在后面的,覆盖前面的
  3. 属性后面加!important的优先级最高(尽量不用)

如何清除浮动

  • 方法一: 给父元素加上.clearfix
.clearfix::after{
    content: "";
    display: block; //或者table
    clear: both;
}
.clearfix {
    zoom: 1 ; // 兼容IE
}
  • 方法二: 给父元素添加: overflow:hidden;

JS部分

JS的数据类型有哪些?

  • 基本类型:string, boolen, number, symbol, undefined, null, bigint
  • 引用类型: object 类型判断:
  1. typeof: 最常用的类型判断方法(缺点:无法判断 数组 和 null 的数据类型)
typeof [] // object
typeof {} // object
typeof null // object
  1. instanceof: 用于判断引用类型的继承关系( 不支持基本类型的判断)
Function instanceof Object // true
Date instanceof Function // true
'1' instanceof String // false
1 instanceof Number // false
  1. Object.prototype.toString: 类型判断的最佳实践
Object.prototype.toString.call()                    // object Undefined
Object.prototype.toString.call([])                  // object Array
Object.prototype.toString.call({})                  // object Object
Object.prototype.toString.call(()=>{})              // object Function
Object.prototype.toString.call(async function(){})  // object AsyncFunction
Object.prototype.toString.call('1')                 // object String
Object.prototype.toString.call(1)                   // object Number
Object.prototype.toString.call(null)                // object Null
Object.prototype.toString.call(undefined)           // object Undefined

0.1 + 0.2 !== 0.3

  • 进制转换:js在做数字运算的时候,0.1和0.2会转化成二进制后无限循环,但是JS采用的是IEEE 754二进制浮点数运算,会导致精度丢失

  • 解决方法

    1. 转化成整数运算
    function add(a,b){
        const maxLen = Math.max(
            a.toString().split(".")[1].length,
            b.toString().split(".")[1].length
        }
        const base = 10 ** maxLen
        const bigA = BigInt(base * a)
        const bigB = BigInt(base * b)
        const bigRes = (bigA + bigB) / BigInt(base)
        return Number(bigRes)
    }
    
    1. 转化成字符串运算
    let addStrings = function (num1, num2){
        let i = num1.length - 1
        let j = num2.length - 1
        const res = []
        let carry = 0
        while(i >= 0 || j >=0){
            const n1 = i >= 0 ? Number(num1[i] : 0;
            const n2 = j >= 0 ? Number(num2[j] : 0;
            const sum = n1 + n2 + carry
            res.unshift(sum % 10)
            carry = Math.floor(sum/10)
            i--
            j--
        }
        if(carry){res.unshift(carry)}
        return res.join("")
    }
    function isEqual(a, b, sum){
        const [intStr1, deciStr1] = a.toString().split(".")
        const [intStr2, deciStr2] = b.toString().split(".")
        const inteSum = addStrings(intStr1, intStr2)
        const deciSum = addStrings(deciStr1, deciStr2)
        return inteSum + "." + decisum === string(sum)
    }
    

JS的new做了什么?

  1. 创建临时对象/新对象
  2. 绑定原型
  3. 指定this = 临时对象
  4. 执行构造函数
  5. 返回临时对象 可以通过方方的创建士兵模型了解更多

JS的立即执行函数是什么?

声明一个匿名函数并立刻执行他,这种做法就是立即执行函数。

(function(){alert('我是匿名函数')} ())  // 用括号把整个表达式包起来
(function(){alert('我是匿名函数')}) ()  // 用括号把函数包起来
!function(){alert('我是匿名函数')}()    // 求反,我们不在意值是多少,只想通过语法检查。
+function(){alert('我是匿名函数')}()
-function(){alert('我是匿名函数')}()
~function(){alert('我是匿名函数')}()
void function(){alert('我是匿名函数')}()
new function(){alert('我是匿名函数')}()
var x = function(){return '我是匿名函数'}()

在 ES6 之前,只能通过它来「创建局部作用域」。

普通函数和箭头函数

this 指向:

  • 普通函数: function (){}
    1. 谁调用函数或者方法,this 指向谁
  • 箭头函数: () => {}
    1. this 在函数定义的时候就确定下来的
    2. this 指向父级作用域的执行上下文
    3. 无法使用 apply, call 和 bind 来改变 this 的指向

区别

  • 普通函数
    1. 语法格式: function(){}
    2. new 和 原型: 有
    3. arguments: 有
    4. this 指向: 动态
    5. call, apply, bind: 修改 this
  • 箭头函数
    1. 语法格式: () => {}
    2. new 和 原型: 没有
    3. arguments: 没有,可以调用外围
    4. this 指向: 一般是全局对象,被普通函数包含则指向上一层
    5. call, apply, bind: 不可以修改this

什么是原型链

原型链涉及的概念挺多的,举例说明

先说一下原型,假设我们有一个普通对象x={},这个x会有一个隐藏属性叫做_proto_,这个属性会指向Object.prototype

x._proto_ === Object.prototype //原型

接下来说说什么是原型链,假设我们有个数组对象a=[],这个a也有一个隐藏属性,叫做_proto_,这个属性会指向Array.prototype

  • a的原型是 Array.prototype
  • a的原型的原型是Object.prototype 于是就通过隐藏属性形成了一个链条
a ===> Array.prototype ===> Object.prototype

解决啥: 在没有Class的情况下实现继承,以上述例子为例

  1. a 是 Array 的实例, a 拥有 Array.prototype 里的属性
  2. Array 继承了 Object
  3. a 是 Object 的间接实例,a 拥有 Object.prototype 里的属性 这样一来, a 既拥有 Array.prototype 里的属性,又拥有 Object.prototype 里的属性

优点: 简单 缺点: 跟Class相比,不支持私有属性

var let const 的区别

  • var 不存在块级作用域, let 和 const 有块级作用域
  • var 存在变量提升, let 和 const 不存在变量提升
  • var 可以重复声明, let 和 const 不行
  • var 和 let 在声明时可以不给初始值, const 声明后必须给初始值
  • let 可以重新赋值, const 不能,但不意味着值不能改动,只是变量指向的内存地址不能改动

闭包

是什么: 闭包是JS的一种语法特性,闭包=函数+自由变量 怎么做:

let count
function add(){
    count += 1  // 访问了外部变量的函数
}
  • 把上面的代码放在[非全局环境]里就是闭包(闭包是 count + add 组成的整体。)
  • 如何实现一个【闭包的应用】:创建一个立即执行函数,并return
const add2 = function (){
    var count 
    return function add (){
        count += 1
    }
}()

解决啥:

  1. 避免污染全局
  2. 提供对局部变量的间接访问(因为只能 count += 1,不能 count -= 1)
  3. 维持变量,使其不被垃圾回收

缺点: 闭包使用不当可能造成内存泄漏

JS如何实现类

方法一:使用原型

function Dog(name){
    this.name = name 
    this.legsNumber = 4
}
Dog.prototype.kind = '狗'
Dog.prototype.say = function(){
    console.log(`我是${this.name}, 我有${this.legsNumber}条腿。`)
}
const dog1 = new Dog('小狗')  // Dog 函数就是一个类
dog1.say()

方法二:使用class

class Dog {
    constructor(name) {
        this.name = name 
        this.legsNumber = 4
    }
    say(){
        console.log(`我是${this.name}, 我有${this.legsNumber}条腿。`)
    }
}
const dog1 = new Dog("小狗")
dog1.say()

JS如何实现继承

方法一:使用原型链

function Animal(legsNumber){
    this.legsNumber = legsNumber
}
Animal.prototype.kind = '动物'

function Dog(name){
    this.name = name
    Animal.call(this, 4)
}
Dog.prototype._proto_ = Animal.prototype

Dog.prototype.kind = '狗'
Dog.prototype.say = function(){
    console.log(`我是${this.name}, 我有${this.legsNumber}条腿。`)
}
const dog1 = new Dog('小狗')  // Dog 函数就是一个类
console.dir(dog1)

方法二:使用class

class Animal{
    constructor(legsNumber){
        this.legsNumber = legsNumber
    }
    run(){
        console.log(`${this.legsNumber}条腿跑起来`)
    }
}
class Dog extends Animal{
    constructor(name){
        super(4)
        this.name = name 
    }
    say(){
        console.log (`我是${this.name},我有${this.legsNumber}条腿。`)
    }
}
const dog1 = new Dog("小狗")
dog1.say()

手写节流throttle、防抖 debounce

  • 节流(类似技能冷却)
const throttle = (fn, time) => {
    let cd = false
    return (...args) => { //...args 把传入的参数收集起来形成数组
        if(cd) {return}
        fn.call(undefined, ...args)
        cd = true
        setTimeout(()=>{
            cd = false
        },time
    }
}
  • 防抖(类似回城)
const debounce = (fn, time) => {
    let TP = null
    return(...args) => {
        if (TP !== null){
            clearTimeout(TP) //打断回程
        }
        // 重新回城
        TP = setTimeout(()=>{
            fn.call(undefined, ...args)
            TP = null
        },time)
    }
}

手写AJAX

const ajax = (method, url, data, success, fail) => {
    var request = new XMLHttpRequest()
    request.open(method, url);
    request.onreadystatechange = function(){
        if(request.readyState === 4){
            if(request.status >= 200 && request.status < 300 || request.status === 304){
            success(request)
            }else{
                fail(request)
            }
        }
    };
    request.send();
}

手写深拷贝

方法一:用JSON:

const list = [{name: '拷贝'}]
const listCopy = JSON.parse(JSON.stringify(list))
listCopy[0].name = '深拷贝'

这个方法有如下缺点:

  • 不支持Date、正则、undefined、函数等数据
  • 不支持引用

方法二:递归

要点: 递归,判断类型,检查环,不拷贝原型上的属性

const deepClone = (a, cache) => {
    if(!cache){
        cache = new Map() //缓存不能全局,最好临时创建并递归传递
    }
    if(a instanceof Object){
        if(cache.get(a)){return cache.get(a)}
        let result
        if(a instanceof Function){
            if(a.prototype){ //有prototype就是普通函数
                result = function(){return a.apply(this.arguments)}
            } else {
                result = (...args) => {return a.call(undefined, ...args)}
            }
        }else if(a instanceof Array) {
            result = []
        } else if (a instanceof Date) {
            result = new Date(a - 0)
        } else if (a instanceof RegExp) {
            result = new RegExp(a.source, a.flags)
        } else {
            result = {}
        }
        cache.set(a, result)
        for(let key in a){
            if(a.hasOwnProperty(key)){
                result[key] = deepClone(a[key], cache)
            }
        }
        return result
    } else {
        return a
    }
}

手写数组去重

  1. hash:利用对象的属性不能相同的特点
function unique(array){
    let Array = []
    let hash = {}
    for(let i = 0; i < array.length; i++){
        if(!hash[array[i]]){
            Array.push(array[i])
            hash[array[i]] = true
        }
    }
    return Array
}
  1. Set
function unique(array){
    return Array.from(new Set(array))
}
  1. Map
function unique(array){
    let map = new Map()
    for(let i = 0; i < array.length; i++){
        let number = a[i]
        if(number === undefined){continue}
        if(map.has(number)){
            continue
        }
        map.set(number, true)
    }
    return [...map.keys()]
}

DOM部分

简述DOM事件模型

  • 每个事件都会经历从上到下的捕获阶段,再经历从下到上的冒泡阶段
  • 怎么选择捕获(冒泡):添加事件监听的时候addEventLisner('click',fn,true/false)true:捕获,false或者不传:冒泡
  • 可以使用event.stopPropagation()来阻止捕获或者冒泡

手写事件委托

简版:

ul.addEventListener('click', function(e){
    if(e.target.tagName.toLowerCase() === 'li'){
        fn()
    }
})

BUG:当用户点击的是li里面的span,就没法触发fn

进阶版

点击 span 后,递归遍历 span 的祖先元素看其中有没有 ul 里面的 li。

function delegate(element, eventType, selector, fn){
    element addEventListener(eventType, e => {
        let el = e.target
        while(!el.matches(selector)){
            if(element === el){
                el = null
                break
            }
            el = el.parentNode
        }
        el && fn.call(el, e, el)
    })
    return element
}
delete(ul, 'click', 'li', f1)

HTTP部分

GET和POST的区别

  1. 幂等性
    • 由于GET是读,POST是写,所以GET是幂等的,POST不是幂等的
    • 由于GET是读,POST是写,所以用浏览器打开网页会发送GET请求,POST打开网页需要用form标签
    • 由于GET是读,POST是写,所以GET打开的页面刷新是没区别的,POST打开的页面刷新需要确认
    • 由于GET是读,POST是写,所以GET结果会被缓存,POST结果不会被缓存
    • 由于GET是读,POST是写,所以GET打开的页面可以被书签收藏,POST打开不行
  2. 请求参数
    • 通常,GET请求参数放在url里,POST请求的参数放在body里
    • GET比POST更不安全,因为参数直接暴露在URL上
    • GET请求参数在url里有长度限制,POST在body里没有长度限制
  3. TCP packet
    • GET产生一个TCP数据包,POST产生两个或以上TCP数据包

HTTP缓存有哪些方案

缓存内容协商
HTTP 1.1Cache-Control: max-age=3600 Etag: ABCIf-None-Match: ABC 响应状态码:304 或 200
HTTP 1.0Expires: Wed, 21 Oct 2015 02:30:00 GMT Last-Modified: Wed, 21 Oct 2015 01:00:00 GMTIf-Modified-Since: Wed, 21 Oct 2015 01:00:00 GMT 响应状态码:304 或 200
  • Cache-Control: 在文件的响应头写上 max-age=3600,就会自动缓存1个小时,1小时内再次访问同样的URL就直接不发请求
  • Etag: 服务器给每个文件的特征值
  • If-None-Match: 通过这个特征值向服务器询问是否需要更新文件
  • 304: 不需要更新
  • 200: 需要更新,并附带新的文件

HTTP和HTTPS 的区别

HTTPS = HTTP + SSL/TLS(安全层)

区别

  1. HTTP是明文传输的,不安全; HTTPS是加密传输的,非常安全
  2. HTTP使用的80端口,HTTPS使用443端口
  3. HTTP较快, HTTPS较慢
  4. HTTPS的证书一般需要购买, HTTP不需要证书

HTTP/1.1 和 HTTP/2 的区别

  1. HTTP/2使用了二进制传输,而且将head 和 body 分成帧来传输;HTTP/1.1是字符串传输
  2. HTTP/2 支持多路复用,HTTP/1.1 不支持。多路复用:一个TCP连接从单车道变成几百个双向通行的车道
  3. HTTP/2 可以压缩head, 但是 HTTP/1.1 不行
  4. HTTP/2 支持服务器推送, HTTP/1.1 不支持

TCP三次握手和四次挥手是什么

TCP:传输内容协议

建立TCP连接时 server 与 client 会经历三次握手

  1. 浏览器向服务器发送TCP数据: SYN(seq = x)
  2. 服务器向浏览器发送TCP数据: ACK(seq = x + 1)SYN(y)
  3. 浏览器向服务器发送TCP数据: ACK(seq = y + 1)

关闭TCP连接时 server 与 client 会经历四次挥手

  1. 浏览器向服务器发送TCP数据: FIN(seq = x)
  2. 服务器向浏览器发送TCP数据: ACK(seq = x + 1)
  3. 服务器向浏览器发送TCP数据: FIN(seq = y)
  4. 浏览器向服务器发送TCP数据: ACK(seq = y + 1)
  • 2、3中间服务器可能还有数据要发送,不能提前发送FIN,所以2、3不能合并
  • 可能有很多数据交流,所以有序号x

同源策略和跨域

什么是同源策略?

  • 如果两个URL的协议、端口和域名都完全一致的话,那么这两个URL是同源
  • 只要在浏览器里打开页面,就默认遵守同源策略
  • 优点: 保证用户的隐私安全和数据安全
  • 缺点: 很多时候,前端需要访问另一个域名的后端接口,会被浏览器阻止获取响应

怎么跨域

  1. JSONP
    • 甲站点利用 script 标签可以跨域的特性,向乙站点发送get请求
    • 乙站点后端改造 JS 文件的内容,将数据传进回调函数
    • 甲站点通过回调函数拿到乙站点的数据
  2. CORS
    • 对于简单请求,乙站点在响应头里添加Access-Control-Allow-Origin:http//甲站点即可
    • 对于复杂请求,如PATCH,乙站点需要:
      1. 响应OPTIONS请求,在响应中添加如下的响应头
      Access-Control-Allow-Origin: https://甲站点
      Access-Control-Allow-Methods: POST, GET, OPTIONS, PATCH
      Access-Control-Allow-Headers: Content-Type
      
      1. 响应POST请求,在响应中添加Access-Control-Allow-Origin头。
    • 如果需要附带身份信息,JS中需要在AJAX里设置xhr.withCredentials = true
  3. Nginx代理/Node.js代理
    • 前端 → 后端 → 另一个域名的后端

Session、Cookie、LocalStorage、SessionStorage的区别

  • Cookie VS LocalStorage
    1. 主要区别是 Cookie 会被发送到服务器,而 LocalStorage 不会
    2. Cookie 一般最大 4K, LocalStorage 可以有 5Mb 甚至 10Mb
  • LocalStorage VS SessionStorage
    1. LocalStorage 一般不会自动过期(除非用户手动清除)
    2. SessionStorage 在会话结束时过期(如关闭浏览器之后,具体由浏览器自行决定)
  • Cookie VS Session
    1. Cookie 存在浏览器的文件里,Session 存在服务器的文件里
    2. Session 是基于 Cookie 实现的, 具体做法就是把 SessionID 存在 Cookie 里

TypeScript部分

TS 和 JS 的区别是什么?有什么优势?

  1. 语法层面: TypeScript = JavaScript + Type (TS 是 JS 的超集)
  2. 执行环境层面: 浏览器、Node.js 可以直接执行JS,但不能执行TS
  3. 编译层面: TS有编译阶段,JS没有编译阶段
  4. 编写层面: TS更难写一点,但是类型更安全
  5. 文档层面: TS的代码写出来就是文档,IDE可以完美提示。 JS的提示主要靠TS

any、unknown、never的区别是什么?

any VS unknown

二者都是顶级类型(top type), 任何类型的值都可以赋值给顶级类型变量:

let foo: any = 123; // 不报错
let bar: unknown = 123; // 不报错

但是 unknown 比 any 的类型检查更加严格,any 什么检查都不做, unknown 要求先收窄类型:

const value: unknown = "Hello World";
const someString: string = value;
// 报错: Type 'unknown' is not assignable to type 'string'.(2322)
const value: unknown = "Hello World";
const someString: string = value as string;//不报错

如果改成any,基本在哪都不报错。所以能用 unknown 就优先用 unknown, 类型更安全一点

never

never 是底类型,表示不应该出现的类型

interface A{
    type: 'a'
}
interface B{
    type: 'b'
}
type All = A | B
function handleValue(val: All){
    switch(val.type){
        case 'a':
            // 这里 val 被收窄为 A
            break
        case 'b':
            // val 在这里是 B
            break
    default:
        // val 在这里是 never
        const exhaustiveCheck: never = val
        break
    }
}

type 和 interface 的区别

  1. 组合方式: interface 使用 extends 来实现继承, type 使用 & 来实现联合类型
  2. 扩展范围: interface 可以重复声明用来扩展, type 一个类型只能声明一次
  3. 范围不同: type 适用于基本类型, interface 一般不行
  4. 命名方式: interface 会创建新的类型名, type 只是创建类型别名,并没有新创建类型

TS工具类型Partial、 Required、 Readonly、 Exclude、 Extract、 Omit、 ReturnType 的作用和实现

  1. Partial: 部分类型(部分实现,不需要把所有类型都写上)
  2. Required: 必填类型(跟部分类型相反,需要全部写上)
  3. Readonly: 只读类型(只读,里面的内容不可更改)
  4. Exclude: 排除类型 (排除不要的)
  5. Extract: 提取类型 (提取需要的)
  6. Pick/Omit: 排除key类型(Pick选择需要的,Omit选择不要的)(接对象类型)
  7. ReturnType: 返回值类型

Vue2 部分

Vue2 的生命周期钩子有哪些?数据请求放在哪个钩子?

  • 常用的
    1. beforecreate,created
    2. beforemount,mounted
    3. beforeupdate,updated
    4. beforedestroy,destroyed
  • 不常用的
    1. activated:被 keep-alive 缓存的组件激活时调用。
    2. deactivated: 被 keep-alive 缓存的组件失活时调用。
    3. errorCaptured: 捕获一个来自后代组件的错误时被调用,返回 false 以阻止该错误继续向上传播。
  • 请求一般放在 mounted 里面,因为放在其他地方都不合适

Vue2 组件间的通信方式

  1. 父子组件: 使用[ props 和事件]进行通信
  2. 爷孙组件:
    • 使用两次父子组件间通信来实现
    • 使用 [ provide + inject ]来通信
  3. 任意组件: 使用eventBus = new Vue()来通信
    • 主要 API 是 eventBus.$oneventBus.$emit
    • 缺点是事件多了后就很乱,难以维护
  4. 任意组件: 使用Vuex通信

Vue 中的 key 有什么作用

  1. 虚拟 DOM 中 key 的作用:
    • key 是虚拟 DOM 的标识,当数据发生变化时, Vue 会根据【新数据】生产【新虚拟 DOM 】,随后vue进行新旧的差异比较
  2. 对比规则:新旧之间如果有相同的key
    • 如果内容没变,就复用原来的
    • 如果内容变了,就生成新的真实 DOM ,替换页面旧的真实 DOM

Vuex

  1. Vuex 是一个转为Vue.js应用程序开发的状态管理模式+库
  2. 核心概念的名字和作用:
    • store 是个大容器,包含以下所有内容
    • State 用来读取状态,带有一个 mapState 辅助函数
    • Getter 用来读取派生状态,带有一个 mapGetter 辅助函数
    • Mutation 用于同步提交状态变更,附有一个 mapMutation 辅助函数
    • Action 用于异步变更状态,但它提交的是 mutation,而不是直接变更状态
    • Module 用来给store划分模块,方便维护代码 常见追问: 为什么 Mutation 和 Action 要分开 答:为了让代码更容易维护(但是Pinia把这两个合并了。可用减少概念)

VueRouter

  1. VueRouter 是 Vue.js 的官方路由,使构建单页面应用变得简单
  2. 说出核心概念的名字和作用:
    • router-link:点击跳转,
    • router-view:容纳路由视图,
    • 嵌套路由:路由可以加上children属性,路由里面还可以有路由,子路由就渲染在router-view里,
    • Hash模式和History模式,
    • 导航守卫:每一个路由都可以设置个钩子,进入离开解析时做什么,
    • 懒加载: import()
  3. Hash模式和History模式的区别:
    • 一个用的是Hash,一个用的 History API
    • 一个需要后端配合,一个不需要后端nginx配合
  4. 导航守卫如何实现登录控制
router.beforeEach((to, from, naxt) => {
    if(to.path === '/login') return next()
    if(to是受控页面 && 没有登录) return next('/login')
    next()
})

Vue2 是如何实现双向绑定的?

  1. 一般是通过v-model/ .sync实现的,v-model是v-bind:valuev-on:input的语法糖
    • v-bind:value 实现了 data => UI 的单向绑定
    • v-on:input 实现了 UI => data 的单向绑定
    • 这两个加起来就是双向绑定了
  2. 这两个单向绑定是如何实现的
    • 前者通过 Object.defineProperty API 给 data 创建 getter 和 setter, 用于监听 data 的改变,data 改变就会安排改变 UI
    • 后者通过 template compiler 给 DOM 添加事件监听, DOM input 的值改变就回去修改 data

Vue3 部分

Vue3 为什么使用 Proxy?

  1. 弥补 Object.defineProperty 的两个不足
    • 动态创建的 data 属性需要用 Vue.set 来赋值, Vue3 用了 Proxy 就不需要了
    • 基于性能考虑, Vue2 篡改了数组的 7 个 API, Vue3 用了Proxy 就不需要了
  2. defineProperty 需要提前递归地遍历 data 做到响应式,而Proxy 可以在真正用到深层数据的时候再做响应式

Composition API

  1. Composition API 比 mixins、高阶组件、 extends、 Renderless Components 等更好,原因有三
    • 模板中的数据来源不清晰
    • 命名空间冲突
    • 性能
  2. 更适合 TypeScript

Vue3 对比 Vue2 做了哪些改动

  1. createApp() 代替了 new Vue()
  2. v-model 代替了以前的 v-model 和 .sync
  3. 根元素可以有不止一个元素了
  4. 新增 Teleport 传送门
  5. destroyed 被改名为 unmounted
  6. ref 属性支持函数了