面试相关
面试相关知识点整理

关于前端面试题

特性相关

Vue和React的区别

  • vue使用类似html的template模板语法,react使用jsx
  • 组件通信方式不同
    • vue组件中子组件传值给父组件,可以通过emit事件
    • react中,则需要父组件传入一个方法,由子组件调用
  • 状态管理
    • vue使用vuex/pinia进行状态管理
    • react使用的是redux
  • 生命周期
    • vue组件有生命周期钩子
    • react类组件可以使用生命周期,函数式组件一般使用hooks代替生命周期
  • diff算法的实现不同
    • react使用基于层级的diff算法
      • 首先比较两棵树的根节点
      • 核心思想是比较和替换节点
    • vue使用的是双端比较diff算法

生命周期

生命周期有哪些?

  • beforeCreate 组件实例被创建之前

  • create 组件实例完全创建

  • beforeMount 组件挂载之前

  • mounted 组件挂载到实例上后

  • beforeUpdate 组件数据发生变化,更新之前

  • updated 组件数据更新之后

  • beforeDestroy 组件实例销毁之前

  • destroyed 组件实例销毁之后

  • activated keep-alive缓存的组件激活时

  • deactivated keep-alive缓存的组件停用时

  • errorCaptured 捕获一个来自子孙组件的错误时被调用

进入组件之后会执行哪些生命周期

  • beforeCreate 组件实例被创建之前
    • data不存在,dom未渲染
  • create 组件实例完全创建
    • data存在,dom未渲染
  • beforeMount 组件挂载之前
    • data存在,dom未渲染
  • mounted 组件挂载到实例上后
    • data数据存在,dom也已经被渲染出来了

父组件引入子组件,生命周期执行顺序是?

  1. 父组件-beforeCreate
  2. 父组件-created
  3. 父组件-beforeMount
  4. 子组件-beforeCreate
  5. 子组件-created
  6. 子组件-beforeMount
  7. 子组件-mounted
  8. 父组件-mounted

总结:父组件先准备数据,子组件接着渲染dom后,父组件渲染dom

created中如何获取dom

获取时机:

  1. 由于js执行代码遵循先同步后异步的机制,因此只要在异步回调中就能获取到dom
  2. 在setTimeout等宏任务中获取
  3. $nextTick

获取方式:

  1. dom方法
  2. ref

发送请求在created还是mounted?

答:

  • 这个问题具体要看项目和业务情况了,
  • 因为组件的加载顺序是:父组件引入了子组件,那么先执行父组件的前3个生命周期,再执行子组件的前4个生命周期
  • 如果需要优先加载子组件的数据,那么父组件的请求需要放在mounted中
  • 如果组件没有依赖关系,那么请求放在哪个生命周期都是可以的。

为什么发送请求不在beforeCreate里?beforeCreate和created有什么区别?

答:

  1. 在beforeCreate里,是获取不到methods里的方法的。
  2. beforeCreate阶段,获取不到$el和$data
  3. created中能获取到$date,也能访问methods里的方法

第2次,或者第N次进入组件会执行哪些生命周期

答:

  • 如果加入了keep-alive,只执行activated
  • 如果未加入,执行前4个生命周期

加入keep-alive会执行哪些生命周期?

  • activated
  • deactivated

生命周期的使用场景有哪些?

答:

  • created
    • 单个组件进行数据请求
  • mounted
    • 获取dom
    • 控制父子组件请求顺序
  • activated
    • 组件入口携带不同的路由参数进入时,处理参数
  • destroyed
    • 清理监听器和定时器
    • 记录页面视频上次播放的时间

组件

keep-alive

答:

  • 用途:缓存当前组件,提升性能,减少发送请求的数量
  • 如果用户请求的页面组件和当前组件相同,不会发起新的请求
  • 被keep-alive包裹的组件,再次进入时,仅会触发activated生命周期,不再执行前四个生命周期

组件传值有几种方式

  • 父传子
    • 方法1:父组件引用子组件,子组件使用props接收参数,缺点是父组件无法传值给孙子辈组件
      • 不允许子组件直接修改prop传入的数据
      • 需要通过事件或者回调的方式通知父组件,由父组件进行修改
    • 方法2:子组件使用$parent直接获取父组件数据,孙子辈可以使用this.$parent.$parent获取,子组件可以直接修改父组件的数据
    • provide/inject进行依赖注入,父组件直接向子代组件注入数据
      • 传入为基本数据类型时,inject接收值为副本,子组件修改数据不会影响组件/其它后代组件中的数据
      • 传入为引用类型时,会影响
  • 子传父
    • 通过事件或者回调方式
    • 通过this.$children[index].xxx,直接获取子组件实例中的数据
      • 当存在条件渲染时,使用索引访问会变得很不可靠
    • 绑定ref,通过this.$refs.子组件ref名称
  • 兄弟互传
    • 通过bus
    1
    2
    3
    4
    5
    import bus from '@/utils/bus.js'
    bus.$emit('change','new-value')
    bus.$on('change',val=>{
    console.log(val)
    })

子组件如何直接修改父组件的值

  • this.$parent.xxx知己修改

子组件如何找到父组件

  • this.$parent

如何找到根组件

  • this.$root

provide/inject

  • 祖先组件通过provide注入数据
  • 子孙组件通过inject接收数据

slot/插槽

  • 匿名插槽
  • 具名插槽
    1
    2
    <slot name="header"></slot>
    <template #header></template>
  • 作用域插槽(可以传值)
    1
    2
    <slot name="header" :arr='["a","b","c"]' :brr='123'></slot>
    <template #header='{arr,brr}'></template>

如何封装组件

组件尽量复杂,涉及到slot/组件通信…

举例来说,我封装过一个2d可视化地图组件,
这个组件主要作用是实时追踪卫星轨迹,对不同种类的卫星进行个性化定制展示,
原本是作为独立项目进行开发的,到了后期,业务要求将这个功能嵌入到其它多个系统中,所以就需要封装成组件
这个组件一共有2种模式,一种是提供精准追踪轨迹的方式,卫星会根据传入组件内的轨迹参数严格运行
一种是智能推演方式,以传入组件的轨迹点位为锚点,推演各个锚点间的轨迹
另外,像地图样式、卫星图例样式都是可以根据传入组件的参数进行个性化定制
组件内提供一些事件回调:轨迹追踪开始、追踪结束、卫星发生碰撞、敌我卫星超过危险距离等事件
另外,关于卫星图例中,像图表标题、图例、表头、表尾、工具栏等位置,都是提供插槽定制功能的,可以进行自定义操作。

props和data优先级谁高

答:props的优先级高

初始化状态优先级:

  1. props
  2. methods
  3. data
  4. computed
  5. watch

为什么data是一个函数

  • 重复创建实例时避免数据污染
  • 保证对象独立

vuex

Vuex有哪些属性

  • state 全局共享属性
    • $store.state.str
    • mapState
  • getters 针对state数据进行二次计算
    • $store.getters.xxx
    • mapGetters
    • getters数据无法修改
  • mutations
    • 存放同步方法的
    • 在mutations中修改state
  • actions
    • 存放一步方法的,提交mutations
  • modules
    • 把vuex再次进行模块封装

vuex使用state值

  • this.$store.state.xxx
    • 可以直接修改vuex的state数据的
  • mapState

vuex的getters值修改

vuex是单项数据流还是双向数据流

答:单向数据流
可以通过mutation进行修改

vuex的mutations和actions区别

1
2
3
4
5
6
7
8
9
10
mutations:{
add(state){
state.number += 2
}
},
actions:{
addNumber({commit,state}){
commit('add')
}
}
  • mutations无返回值,功能局限在状态变更上
  • actions返回Promise对象,能够进行一步操作
    • actions用于提交mutation,不是直接变更状态

vuex持久化存储

vuex本身无法实现持久化存储

  • 方式1:localStorage
  • 方式2:vuex-persistedstate

路由

路由的模式和区别

  • hash
    • url在根路径后跟随#
    • 当请求的页面不存在时,不会向后台请求资源,自动跳转根路径
    • 原理:基于url的hash值来实现路由,hash值变化,浏览器不会向服务器发送请求,而是触发hashChange事件,前端路由库监听该事件,根据hash值的变化动态渲染对应组件
    • 无需服务器配置,无论hash值如何变化,服务器返回同一个页面
    • 不利于SEO优化
    • 用于需要兼容浏览器获不需要服务器配置的项目
    • 打包后前台可以自测看到页面内容
  • history
    • 请求页面不存在时,依旧发送请求
    • 一般会跳转404页面
    • 原理:基于html5的history api
    • 需要服务器配置和浏览器兼容
    • 用于对url美观性和seo要求较高的项目
    • 打包后前台自测无法正常访问路由

子路由

动态路由

答: 常用于详情页

1
2
3
4
5
6
7
8
9
10
11
{
path:'/list',
name:"List",
children:[
{
path:"/list:id",
name:"Detail",
component:()=>import("../views/Detail.vue")
}
]
}

路由传值

  • 显式传值
1
2
3
4
5
6
7
// http://localhost:8000/about?a=1
this.$router.push({
path:'/about',
query:{
a:1
}
})
  • 隐式传值
1
2
3
4
5
6
7
// http://localhost:8000/about?a=1
this.$router.push({
path:'/about',
params:{
a:1
}
})

导航故障

  • 当前页跳转当前页的时候,会报导航故障错误
  • 产生原因:在history模式下,使用html5的history api实现路由跳转,如果前端路由没有处理【同页面跳转】的情况,就可能导致导航故障
  • 解决方法:重写VueRouter.prototype.push方法:
1
2
3
4
5
import VueRouter from 'vue-router'
const routerPush = VueRouter.prototype.push
VueRouter.prototype.push = function(location){
return routerPush.call(this, location).catch(error=>error)
}

$router和$route的区别

  • $router
    • 包含当前路由
    • 包含整个路由的属性和方法
  • $route
    • 仅包含当前路由对象

导航守卫

  • 全局守卫
    • beforeEach 路由进入之前
    • afterEach 路由进入之后
  • 路由独享守卫
    • beforeEnter 路由进入之前
    1
    2
    3
    4
    5
    6
    7
    8
    {
    path:'/about',
    name:'about',
    beforeEnter:function(to,from,next){
    if(true) next()
    else next('/login')
    }
    }
  • 组件内守卫
    • beforeRouteEnter 路由进入之前
    • beforeRouteUpdate 路由更新之前
    • beforeRouteLeave 路由离开之前

使用场景:一些只有在用户登录之后才能开发的页面,或者是需要对用户身份进行划分的组件

介绍一下SPA以及SPA有什么缺点

SPA:单页面应用

缺点:

  • SEO优化不好
  • 性能不是特别好

router跳转和a标签跳转区别

  • a标签跳转回重新从服务器端请求资源,耗流量、耗性能
  • spa单页路由模式,router跳转只是加载中间的html代码,不会请求资源

api

$set

$set(target,key,value)

1
2
3
// [1,2,3]变为[1,5,3]
// this.arr[1] = 5
this.$set(this.arr, '1', 5)

$nextTick

  • 传入的回调函数会被异步执行
  • 常用于执行组件渲染后的操作
  • 是微任务,因为使用Promise.resolve实现
1
2
3
4
5
6
7
class Vue{
$nextTick(callback){
return Promise.resolve().then(()=>{
callback()
})
}
}

$refs

获取dom

$el

获取当前组件的根节点

$data

获取当前组件data数据

$children

获取当前组件的所有子组件(数组格式)

$parent

  • 找到当前组件的父组件,如果找不到就返回自身

$root

获取根组件

data定义数据

1
2
3
4
5
6
data(){
this.num = 222
return {
str:'111'
}
}
  • 使用this.xxx定义的数据,没有getter和setter劫持处理,单独修改时数据不会刷新

computed计算属性

computed计算的值可以被修改吗(包括v-model双向绑定)?

  • 可以,通过get和set
1
2
3
4
5
6
7
8
changeStr:{
get(){
return this.sre.slice(-2)
},
set(val){
this.sre = val
}
}

watch

  • watch的初始化监听
    • 加上配置项:immediate:true
  • 深度监听
    • deep:true

methods、computed、watch区别

  • computed有缓存机制
    • 当参与计算的值被改变时,计算属性会监听到并进行返回
  • methods没有缓存机制
  • watch 监听数据/路由的变化
    • 监听对象发生改变时,会执行

指令

如何自定义指令

vue单项绑定

v-if和v-show的区别

  • v-if 添加/删除dom节点
    • 一般用于显隐切换不频繁的渲染节点
  • v-show 使用display:none对元素进行显隐控制
    • 一般用于显隐切换频繁的渲染节点

v-if和v-for优先级

答:

  • vue2中:v-for优先级高,这是vue的源码决定的
  • vue3中:v-if优先级高

原理

$nextTick原理

v-model双向绑定原理

scoped原理

给元素节点新增自定义属性:

1
2
3
4
5
6
<div data-v-xxxx class="hello-box">Hello</div>>
<style>
.hello-box[data-v-xxxx]{
background:red
}
</style>

样式穿透

  • scss/stylus中的样式穿透:
    • 方式1:父元素 /deep/ 子元素
    • 方式2:父元素 >>> 子元素

render函数

  • render函数支持用js语言来构建dom
  • vue是虚拟dom,拿到template模版也会转译成vnode函数
  • 使用render构建dom,就直接省去了转译的过程
  • 使用render时,使用h函数构建虚拟dom

axios二次封装

vue3相关

vue2和vue3区别

  1. 双向绑定机制不同
    • vue2:Object.defineProperty()
      • 使用循环对data返回的对象进行遍历,定义get和set
      • 后添加的属性是劫持不到的
      • 有些情况下需要使用$set进行数据刷新
    • vue3:new Proxy()
      • 使用new Proxy对响应式数据进行包裹
      • 既不需要循环遍历,后添加的属性也可以被劫持到
      • 不需要$set刷新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// vue2 双向绑定原理
let data = {
a:1,
b:2
}
let vue = {}
for(let key in data){
Object.defineProperty(vue, key, {
get(){
console.log('数据被获取')
return data[key]
},
set(value){
console.log('数据被设置')
data[key] = value
}
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vue3 双向绑定原理
let data = {
a:1,
b:2
}
const vue = new Proxy(data, {
get(target, propKey, receiver){
console.log('数据被获取')
return Relect.get(targte, propKey, receiver)
},
set(target, propKey, value, receiver){
console.log('数据被设置')
return Reflect.set(target, propKey, value, receiver)
}
})
  1. vue2是选项式API,vue3可以向下兼容,也可以是组合式api或者是setup语法糖的形式
  2. v-if和v-for的优先级不同了
  3. ref和$children也不同
  4. vue3使用Tree-shaking
  • 打包时会对没有用到的api进行剔除,这样bundle的体积更小
  • 创建app实例由new Vue()变为createApp
  1. suspense允许程序在等待异步组件时渲染一些后备内容
  • 一步组件使用defineAsyncComponent进行加载

vue3使用setup怎么组织代码

答:可以使用hooks对复用逻辑进行封装

也就是组合式函数,用来封装和服用逻辑的函数:

1
2
3
4
5
6
import {ref} from 'vue'
function useSameLogic(){
let xxx = ref(123)
return xxx
}
export default useSameLogic

vue3使用setup写如何获取类似vue2中的this

1
let {proxy} = getCurrentInstance()

vue3常用api有哪些

createApp

  • 创建一个应用实例
  • 相当于Vue2的new Vue()
  • 使用场景:写插件/封装全局组件时会使用

provide/inject

  • 依赖注入,跨辈传值
  • 父组件传值到后代组件,跨越多个层级
  • 不好维护和查询数据来源

directive

  • 自定义指令
  • 使用场景:后台管理系统中的按钮权限控制,根据用户角色决定按钮操作权限

mixin

  • 全局混入
  • 可以添加生命周期,在小程序的分享功能会用到
  • 不好维护和查询数据来源

app.config.globalProperties

  • 获取vue这个全局对象的属性和方法
  • 自己封装插件的时候需要把方法添加到对象中

nextTick

  • 等待下一次Dom更新刷新的工具方法
  • 返回一个Promise,回调函数参数异步执行

computed

  • 计算属性
  • 可以缓存返回结果

reactive、ref

  • 来定义数据和vue2的data类似

watch

  • 监听(vue3不需要深度监听)

markRaw()

  • 静态数据,不被new Proxy代理

defineProps()

  • setup形式的组件,父组件传递的值,子组件需要使用defineProps接收

defineEmits()

  • setup形式的组件,自定义事件需要使用defineEmits

slot

  • 分为匿名、具名、作用域
  • 后台管理项目,主内容部分根据菜单的选择进行切换,这时可以使用slot插槽

介绍一下vue3常见的响应式数据类型

  • ref 定义基本类型
1
2
const abc = ref(10)
console.log(abc.value)
  • reactive 定义引用类型
1
2
3
4
5
const data = reactive({
name:123,
age:23
})
console.log(data.name)
  • toRef 对reactive对象进行单个值解构处理
1
const name = toRef(data,'name')
  • toRefs 对reactive对象进行全部解构处理
1
const {name, age} = toRefs(data)

介绍一下teleport组件及其使用场景

使用场景:希望指定组件渲染到根组件下,和app元素同级的位置(如弹窗)

1
2
3
<teleport to="body">
<div>Hello</div>
</teleport>

介绍watch和watchEffect的区别

  • watch
    • 除非配置immediate,否则只有监听值发生变化才会执行
    • 需要传递监听对象
    • 能获取到更改前后的值
    • 监听明确数据
  • watchEffect
    • 会立即执行
    • 只需要传递回调函数
    • 无法获取更改前的值
    • 监听能访问到的所有响应式属性

介绍vue2和vue3中watch的区别

  • vue3
    • watch简化了语法,与setup函数结合更加紧密
    • watch作为函数调用
    • 能够监听对象的子属性,能通过数组组合监听
  • vue2
    • watch作为一个模块,在其中定义相应的监听事件

vue3生命周期

  • setup
    • 相当于vue2中的beforeCreate、created
  • onBeforeMount
  • onMounted
  • onBeforeUpdate
  • onUpdated
  • onBeforeUnmount
  • onUnmounted
  • onActivated
  • onDeactivated
  • onErrorCaptured

项目开发相关

什么是渐进式框架

vuejs只是一个核心库,可以通过补充插件的方式将应用规模化,
比如通过补充vue-router实现路由跳转,
补充vuex实现状态集中管理等。

vue如何设置代理

答:vue.config.js,仅在开发环境下有效

1
2
3
4
5
module.exports = {
devServer:{
proxy:'http://localhost:4000'
}
}

打包路径和路由模式(vue项目打包完之后出现空白页,如何解决)

答:

  • 关于路径:
    打包之后,资源默认根路径为绝对路径,需要修改为相对路径:
1
2
3
module.exports = {
publicPath:'./'
}
  • 关于路由模式

    • 需要修改为hash模式,由前端控制路由跳转
  • 关于模式和环境变量

    • 开发环境 .env.development
    • 生产环境 .env.production

什么是MVVM

  • Model
    • 数据
  • View
    • 界面中展示的内容
  • View-Model
    • 视图和数据的交互,也就是vue源码实现的功能

vue源码

模板解析

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="app">
<h1>{{str}}</h1>
{{str}}
</div>

<script type="text/javascript">
new Vue({
el:'#app',
data:{
str:'hello'
}
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Vue{
constructor(options){
this.$data = options.data
this.$el = document.querySelector(options.el)
this.compile(this.$el)
}
compile(node){
node.childNodes.forEach((item,index)=>{
// 元素节点
if(item.nodeType == 1){
this.compile(item)
}
// 文本节点
if(item.nodeType == 3){
let reg = /\{\{(.*?)\}]|/g
let text = item.textContent
item.textContent = text.replace(reg,(match, vmKey)=>{
vmKey = vmKey.trim()
// 返回data上的数据
return this.$data[vmKey]
})
}
})
}
}

生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="app">
<h1>{{str}}</h1>
{{str}}
</div>

<script type="text/javascript">
new Vue({
el:'#app',
data:{
str:'hello'
},
beforeCreate(){},
created(){},
beforeMount(){},
mounted(){},
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Vue{
constructor(options){
if(typeof options.beforeCreate == 'function'){
options.beforeCreate.bind(this)()
}
this.$data = options.data
if(typeof options.created == 'function'){
options.created.bind(this)()
}
if(typeof options.beforeMount == 'function'){
options.beforeMount.bind(this)()
}
this.$el = document.querySelector(options.el)
this.compile(this.$el)
if(typeof options.mounted == 'function'){
options.mounted.bind(this)()
}
}
}

添加事件

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id="app">
<button @click="handleClick">btn</button>
</div>

<script type="text/javascript">
new Vue({
el:'#app',
data:{},
methods:{
handleClick(){}
}
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Vue{
constructor(options){
this.options = options
this.$data = options.data
this.$el = document.querySelector(options.el)
this.compile(this.$el)
}
compile(node){
if(item.nodeType == 1){
if(item.hasAttribute('@click')){
const event = item.getAttribute('@click').trim()
item.addEventListener('click',()=>{
this.options.method[event]()
})
}
}
}
}

data劫持

  • vue大对象属性来自于data中
  • data中属性与vue大对象属性保持双向(劫持)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="app">
<button @click="handleClick">{{str}}</button>
</div>

<script type="text/javascript">
new Vue({
el:'#app',
data:{
str:"hello"
},
methods:{
handleClick(){
console.log(this.str)
}
}
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Vue{
constructor(options){
this.options = options
this.$data = options.data
this.proxyData()
this.$el = document.querySelector(options.el)
this.compile(this.$el)
}
proxyData(){
for(let key in this.$data){
Object.defineProperties(this,key,{
get(){
return this.$data[key]
},
set(value){
this.$data[key] = value
}
})
}
}
}

更新视图

  • 通过一个监听表来存储对数据进行订阅的视图元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 监听对象类型
class Watch{
constructor(vm, key, node, attr){
// 对象
this.vm = vm
// 属性名称
this.key = key
// 节点
this.node = node
// 字符串
this.attr = attr
}
update(){
this.node[this.attr] = this.vm[this.key]
}
}

class Vue{

observer(){
for(let key in this.$data){
let value = this.$data[key]
let thiat = this
Object.defineProperty(this.$data,key,{
get(){
return value
},
set(value){
value = val
// 更新每个订阅器
if(that.$watchEvent[key]){
that.$watchEvent[key].forEach(item=>{
item.update()
})
}
}
})
}
}
}

v-model双向绑定原理

  • 通过Object.defineProperty劫持数据发生的改变
  • set中数据被改变,触发对数据进行监听的节点的update方法更新节点内容

在上面对vue模版进行解析的方法里,继续判断是否存在v-model属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Vue{
compile(){
if(item.hasAttribute('v-model')){
let vmKey = item.getAttribute('v-model')
if(this.hasOwnProperty(vmKey)){
item.value = this[vmKey]
}
item.addEventListener('input',(event)=>{
this[vmKey] = item.value
})
}
}
}

diff算法

  • 功能:提升性能
  • 虚拟dom:把dom数据化
    • 通过h函数传入到vnode生成的数据结构

snabbdom

  • 虚拟节点操作库
  • 通过数据(虚拟dom)来操作dom
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** 
创建一个虚拟节点
{
children: undefined,
data:{},
elm:h1,
key: undefined,
sel:"h1",
text:"你好h1"
}
对应的真实节点:
<h1>你好h1</h1>
**/
const vnode = h('h1',{},'你好h1')

节点替换规则:

  • 没有用key进行标识的节点,每次都要删除后重新创建
  • 即便是用key进行过标识,节点类型也需要相同
  • 不能越级比较

手写生成虚拟dom的h函数

h函数(生成vnode对象):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* h('h1',{},'你好h1')
* @param sel 标签类型
* @param data 数据
* @param params
*/
export default function(sel, data, params){
if( typeof params == 'string' ){
return vnode(sel, data, undefined,params,undefined)
}else if(Array.isArray(params)){
// 存在子元素
let children = []
for(let item of params){
children.push(item)
}
return vnode(sle,data,children,undefined,undefined)
}
}

手写patch

调用形式:

1
patch(oldVNode, newVNode)
  • patch执行之前,必须确保2个节点都是虚拟节点
    • 如果旧节点不是虚拟节点,需要重新创建虚拟节点
    • 判断新旧虚拟节点是同一个节点
      • 如果不是,则暴力替换
      • 判断新节点内容是文本还是子节点(不是同一个节点)
        • 是文本,直接替换为文本内容
        • 判断旧节点内容是文本还是子节点(新节点内容是子节点)
          • 是文本,直接替换为新节点的子节点
          • 非文本,需要使用diff核心算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
export default function(oldVnode, newVnode){
if(oldVnode.sel == undefined){
// oldVnode不是虚拟节点时
oldVnode = vnode(
oldVode.tagName.toLowerCase(),
{},[],undefined,oldVnode
)
}
if(oldVnode.sel === newVnode.sel){
// 新旧虚拟节点是同一个节点
if(newVnode.children === undefined){
// 新节点没有children
if(newVnode.text !== oldVnode.text){
// 文本不同时,仅替换文本
oldVnode.elm.innerText = newVnode.text
}
}else{
// 新节点有children
if(oldVnode.children !== undefined){
// 旧节点有children
}else{
// 旧节点没有children
oldVnode.elm.innerHTML = ''
for(let child of newVnode.children){
let childDom = createElement(child)
oldVnode.elm.appendChild(childDom)
}
}
}
}else{
// 不是同一个节点,暴力替换,删除旧节点,创建插入新节点
// 创建新节点
let newVnodeElm = createElement(newVnode)
// 插入新节点
oldVnode.elm.parentNode.insert(newVnode)
// 删除旧节点
oldVnode.elm.parentNode.removeChild()

}
}

diff算法

  • diff算法是个深度优先算法

当patch中,需要对新旧元素的子节点进行比较替换时,就需要使用到核心diff算法,
diff算法首先在新旧节点数组头尾处分别设置指针,
然后按照下面顺序进行比较:

  • 旧节点头指针和新节点头指针元素比较
    • 如果匹配,新旧头指针后移
  • 新旧尾指针比较
    • 如果匹配,新旧尾指针前移
  • 旧头指针和新尾指针比较
    • 如果匹配,旧头指针后移,新尾指针前移
  • 旧尾指针和新头指针比较
    • 如果匹配,旧尾指针前移,新头指针后移
  • 以上都不满足,进行查找
  • 创建或删除

掘金回答

原贴
首先,我们拿到新旧节点的数组,然后初始化四个指针,分别指向新旧节点的开始位置和结束位置, 进行两两对比,
若是 新的开始节点和旧开始节点相同,则都向后面移动,
若是结尾节点相匹配,则都前移指针。
若是新开始节点和旧结尾节点匹配上了,则会将旧的结束节点移动到旧的开始节点前。
若是旧开始节点和新的结束节点相匹配,则会将旧开始节点移动到旧结束节点的后面。
若是上述节点都没配有匹配上,则会进行一个兜底逻辑的判断,判断开始节点是否在旧节点中,
若是存在则复用,若是不存在则创建。
最终跳出循环,进行裁剪或者新增,
若是旧的开始节点小于旧的结束节点,则会删除之间的节点,
反之则是新增新的开始节点到新的结束节点。