VUE

Vue
API
extent 创建 基础构造器Vue 的子类 Ctor, 并让 Ctor.super 指向其父构造器
nextTick
-> 有回调 -> 将回调存入队列 -> 尝试使用 Promise 以微任务管理回调队列 -> (若不支持Promise)依次尝试使用 setImmediate/MessageChannel/setTimeout 以宏任务管理回调队列
-> 无回调
-> 支持 Promise
-> 不支持 Promise
set
delete
directive
filter
component
use
mixin
compile
observable

组件间传值
父子关系
被动接受 prop、emit
主动获取 refs 访问子组件 attrs、inheritAttrs 访问来自父组件的注册属性(类似prop)
兄弟关系
? VueX
? Vue-bus

生命周期
beforeCreate
观测数据, 即通过代理完成 data() 数据与 vm._data 的绑定
注册事件
created
rander 函数 > template 选项 > el 选项
template 选项会被编译为 rander 函数
beforeMount // 每次 vm.$mount(el) 时都会触发

mounted // 模板编译完成
beforeUpdate  // 仅在data中的数据变化并渲染到视图时才会触发, 仅改变data不触发
updated // 
beforeDestroy // 卸载 watcher、子组件、事件监听器
destroyed
activated
deactivated
errorCaptured

虚拟DOM树 vNode
vNode 的常用属性
tag: 标签
data: 属性(VNodeData)
children: 子节点
text: 文本节点
elm: DOM节点的引用
context: 编译的作用域
parent: 父节点
key: 标识字段(来自data.key)
当页面创建时生成一颗虚拟DOM树, 页面改变时再创建一颗
使用 diff 算法记录差异点, 同层级比较(不比较父子辈的节点)
replace 非同一类型节点(key相同、标签相同、有data时还会比较data.attrs.type), 直接替换
update 节点的属性或属性值改变, 仅更新节点
text 节点文本变化, 仅更新文本
children 增/删/移 子节点, 遍历子节点找到变化节点, 修改该节点与其之后的子节点(类似数组操作)
使用 vNode.data.key 可以提高 diff 算法的查询效率(不默认加key的原因是vue尽可能采用复用策略, 即减少替换, 优先修改, 不触发完整生命周期和过渡效果)
? 举例说明
更新到真实的DOM中

vm.$createElement
- tag (String | Object | Function) 节点类型
- data (Object) 节点的属性
- children (String | Array) 可以为文本虚拟节点, 也可以为子虚拟节点数组
- normalizationType (子节点的规范方式, 不常用)

路由 Vue-router
导航HOOK
全局
router.beforeEach
router.afterEach
单项
option.beforeEnter
组件内
beforeRouteEnter
beforeRouteUpdate
beforeRouteLeave
跨页传参
params: 类似post, 不显示在url中
query: 类似get, 显示在url中
实现模式
hash: url中带#, 会触发原生的hashchange事件, 不刷新页面
history: 需与服务端的url定义一致
懒加载
动态加载

用户触发nextTick -> nextTick 将传入的回调推入内部执行队列cb, 检测到pending为true, 不再修改 textNode
(宏task执行完毕, 开始执行微任务)
MO检测到 textNode 的修改, 执行MO的回调, 遍历执行 内部执行队列cb
内部执行队列cb 的第一个函数 flushBatcherQueue 将遍历待更新的watcher并执行他们的update方法, 将新数据设置到DOM上
执行之后的cb, 这时打印出的 dom 将是真实数据

变化观测
依赖收集(怎么收集, 收集什么, 收集到哪)
在初始化阶段
非数组类型使用 defineProperty (后续版本计划改为proxy实现) 定义响应式数据的 get 和 set 方法在 getter 中收集 watcher 到 dep (局部变量, 每个key都有自己的dep), 在 setter 中触发依赖
defineProperty 仅能监听到对象属性值的改变, 无法监听到对象属性的增加和删除, 使用 delete 来解决
数组类型元素的增减使用 defineProperty 的 getter 收集依赖到 Observer 下, 通过 拦截器(对原先函数的封装) 覆盖原型方法, 在拦截器中触发依赖
对数组类型元素值改变的监听是通过递归遍历数组的每一项进行观测
缺陷:这种方式检测不到对象属性的添加, 需要使用 delete

Observer 观测类
Object.keys 遍历对象的属性进行观测(数据劫持), 被观测的数据会带上 ob, 指向 Observer 的实例

监听器 Watcher(expOrFn, cb, option) 的实现原理
先将自身(watcher实例)设为 Dep.target, 通过读取被观测对象的值触发其getter, 将自身添加到被观测对象的 dep 中
当数据发生变化时, 触发被观测对象的setter方法, 调用订阅了该对象的watcher实例的update方法,
如果设置了 option.immediate, 则会立即调用一次cb
如果设置了 deep, 则会对 expOrFn 进行递归, 观测后代属性 (数组类型不需要deep, 其在被观测的过程中就已经有了递归这一步)
update 执行回调函数cb, (是否重新渲染虚拟DOM待验证)

模板编译(目的是生成渲染函数, 执行时生成新的vNode)
AST(解析器) -> 静态节点标记(优化器) -> 生成渲染函数(代码生成器) -> 生成虚拟DOM
html解析器通过正则循环截取模板字符串, 不断触发钩子函数将解析出的节点入栈, 以形成树形关系 (这个时候会记录节点的类型)
标记静态节点可以减少运算, 从根节点开始递归判断是否属于静态根节点, 如是则为其AST节点加上 static 或者 staticRoot
代码生成器是对AST的递归生成字符串, 并拼接在一起返回给调用者

虚拟DOM (JS创建的树形结构)
为什么要用虚拟DOM
直接访问DOM的代价比较昂贵, 会触发回流, 并且通常会附带重新生成没有修改的DOM, 造成性能上的浪费
虚拟DOM的方案为通过状态生成虚拟节点树并进行渲染, 渲染前通过diff算法与上一次变化生成的节点树作比对, 只渲染不同的部分
虚拟DOM的工作主要为两部分
通过 模板编译 生成 真实DOM 对应的虚拟DOM
比对虚拟节点(patch), 更新视图
vNode 虚拟节点, 是用js对DOM元素的描述, 渲染时会先创建所有的 vNode, 并据此创建 真实DOM元素, 然后插入到页面中
观测到状态变化时, 会通知到组件, 组件内部使用虚拟DOM渲染视图, 即组件中任意状态变化都会导致整个组件重新渲染(这么做的原因是性能和精确度上的折中), 但并不是所有DOM节点都需要更新, 因此需要比对新的vNode与之前的缓存vNode
patch:vNode -> 真实DOM
创建 createElement
添加 appendChild
删除 removeChild
修改 textContent (innerText、innerHTML 应该也可以)
修改子节点 (过长不做描述, 后续版本应该是有diff优化, 不需要这一步, 待验证)

事件相关的实例方法 off 在 vm._events 对象中注销回调事件 emit 从 vm._events 对象中取出回调事件列表, 并依次触发

生命周期相关的实例方法 nextTick 将回调延迟到下次DOM更新周期后执行, 数据(状态)的变化会通知watcher并触发渲染, 这个触发是异步的, 使用微任务队列, nextTick是将回调插入到队列中 children中删除, 调用watcher.teardown), 解绑指令和事件监听器(调用$off), 触发 beforeDestroy 和 destroyed

mixin 混入
会修改Vue的options属性, 对后续生成的Vue实例产生影响, 可以注入一些自定义行为, 比如监听生命周期钩子

callHook 的作用是遍历并调用 options 属性中设置的生命周期钩子数组中的函数

callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

Vue指令
指令的作用是在传入表达式的值改变时, 将其产生的连带影响响应式地应用到DOM上
指令分为内置指令和自定义指令, 某些内置指令因为实现的原理不同(在模板编译-代码生成阶段实现, 如v-if, v-for、v-once等), 难以用自定义指令(在虚拟DOM渲染至真实DOM阶段, 触发钩子函数)实现
v-on 指令的作用是绑定事件监听器
作用在普通元素上时, 可监听原生DOM事件, 事件被注册到浏览器事件中
作用在组件上时可监听子组件触发($emit)的自定义事件, 事件被注册到Vue的事件系统中, 即 vm._events
在模板编译阶段会生成vNode, v-on注册的事件监听会被添加到vNode.data.on上面
之后在虚拟DOM的修补过程中, 会比对新旧vNode来确定为对应的真实DOM注册(addEventListener)/注销DOM事件(removeEventListener)
自定义指令
虚拟DOM在渲染更新DOM元素时会触发生命周期钩子, 指令的逻辑处理是监听了create、update、destroy钩子, 通过对比新旧vNode来选择触发指令生命周期钩子
生命周期
bind: 比对新旧vNode时, 发现新vNode中有而旧vNode中没有的指令时触发
update: 比对新旧vNode时, 发现新旧vNode中都有的指令时触发
inserted: 绑定元素被添加到父节点时触发
componentUpdate: 当组件所在的vNode及其子vNode都完成更新时触发
unbind: 比对新旧vNode时, 发现旧vNode中有而新vNode中没有的指令

Vue 最佳实践
1、为 v-for 的每一项添加key
2、当 v-if 和 v-else 的元素类型相同时, 也需要添加 key
以上两项的原因相同, 当重新渲染时会生成一份新的虚拟DOM与之前生成的进行diff比对(判断tag和key是否一致), 找出需要渲染的部分, Vue会尽可能复用之前的节点, 因此元素类型(tag)相同时, 会采用修补而不是替换的方式, 添加key可以避免这个问题
对于key的取值, 因为Vue中的diff算法是同层级比较, 所以只需要保证其在父组件下唯一即可
举例:假设有一个复选框列表, 勾选第一项后, 通过unshift往数组头部插入新项, 此时我们期望他选中的是我的第二项, 实际上却是第一项
3、避免 v-for 和 v-if 同时使用
v-for 比 v-if 优先级更高, 因此重新渲染时会遍历列表所有项, 更好的做法是 使用computed 来定义 v-for 中符合 v-if 条件的集合, 这样做的好处有几点:
1、使用 computed 属性, 因为其缓存特性, 即使数据短时间内发生多次变化, 也只执行一次渲染, 效率更高
2、需要渲染的列表可视为 v-for 的子集, 计算量更小
3、可以解耦渲染层的逻辑, 可维护性更好(比如渲染的条件变更时)

VueX
Vuex是什么
专为vue应用开发的状态管理模式(集中存贮管理应用所有组件的状态, 并以相应的规则保证状态以一种可预测的方式发生变化)
Vuex的核心概念
state: 存储数据, 存储状态;在根实例中注册了store 后, 用 this.$store.state 来访问;对应vue里面的data;存放数据方式为响应式, vue组件从store中读取数据, 如数据发生变化, 组件也会对应的更新。
getter: 可以认为是 store 的计算属性, 它的返回值会根据它的依赖被缓存起来, 且只有当它的依赖值发生了改变才会被重新计算。
mutation: 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。
action:包含任意异步操作, 通过提交 mutation 间接更变状态。
module:将 store 分割成模块, 每个模块都具有state、mutation、action、getter、甚至是嵌套子模块。
为什么用Vuex

.Vue 文件中的样式作用域 scoped
会为组件内的元素添加一个特殊属性(随机), scoped的CSS选择器, 会在末尾带上这个属性选择器, 形成了一个不影响其他组件(父子作用域不继承)的封闭作用域, 穿透到子级作用域的方法是使用 /deep/

next-tick = update过程 + DOM操作

疑问
数据修改的时候, 如何在微任务队列中插入重新渲染DOM的事件并重新生成虚拟DOM

SSR
预编译
wik压力测试

Vue 3.0
使用 proxy 代替 Object.defineProperty
proxy 解决了 数组、对象增删元素的监听问题
proxy 支持 map 等格式
proxy 支持懒收集, 并不是在初始化时就进行递归收集
虚拟DOM Compile
composition mixin
runtime
静态标记, 在模板编译阶段就进行了标记, diff更加高效, 只对比动态节点

手写 响应式原理
const baseHandler = {
get(target, key) {
const res = target[key]
// 依赖收集
track(target, key)
return typeof res === 'object' ? reactive(res) : res
},
set(target, key, val) {
const info = {oldValue: target[key], newValue: val}
target[key] = val
// 通知变化
trigger(target, key, info)
}
}

function reactive() {
const observed = new Proxy(target, baseHandler)
return observed
}
// 特殊的 effect
function computed() {

}
// 收集到的依赖函数, 类似watch
function effect(fn, option={}) {

}

let effectStack = []
let targetMap = new WeakMap()
// 依赖收集, 收集到一个 map 中
function track(target, key) {
const effect = effectStack[effectStack.length-1]
if(effect) {
let depMap = targetMap.get(target)
if(depMap === undefined) {
depMap = new Map()
targetMap.set(key, depMap)
}
let dep = depMap.get(key)
if(dep === undefined) {
dep = new Set()
depMap.set(key, dep)
}
// 容错
if(!dep.has(effect)) {
// 新增
dep.
}
}
}
// 依赖触发
function trigger() {
// 找到依赖
const depMap = targetMap.get(target)
if(depMap === undefined) return
// 区分 effect computed, 先执行 effect , 因为可能会被 computed 依赖
const effects = new Set()
const computedRunners = new Set()
if(key) {
let deps = depMap.get(key)
deps.forEach(() => {

})

}
}

手写 render

// 极简数据观测
var depMap = {}
function defineRetive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
if(window.target) depMap[key] ? depMap[key].push(window.target) : depMap[key] = [window.target]
return val
},
set(newVal) {
depMap[key].forEach(v => v(newVal, val))
val = newVal
}
})
}

var o = {name: 1}
defineRetive(o, 'name', o['name'])

function watch(obj, key, fn) {
window.target = fn
obj[key]; // 触发get完成依赖收集
window.target = null
}

watch(o, 'name', (a,b)=>{console.log(a,b)})
o.name = 2

// 极简 订阅/发布
function eventBus() {
let _event = {}
this.on = (eName, fn) => {
_event[eName] ? _event[eName].push(fn) : _event[eName] = [fn]
}
this.off = (eName, fn) => {
fn ? _event[eName].splice(_event[eName].indexOf(fn), 1) : _event[eName] = []
}
this.emit = (eName) => {
_event[eName].forEach(f => f())
}
// 思考题,如何实现一个 once ?
this.once = (eName, fn) => {
let wFn = () => {
fn()
this.off(eName, wFn)
}
this.on(eName, wFn)
}
}
var e = new eventBus()
function addCb() { console.log('add2') }
e.on('add', () => { console.log('add1') })
e.on('add', addCb)
e.emit('add')
e.off('add', addCb)
e.emit('add')
e.emit('add')
e.once('add3', () => { console.log('add3') })
e.emit('add3')
e.emit('add3')

patch
新增节点 newVnode 存在而 oldVnode 不存在, 使用 document.createElement 和 nodeOps.appendChild(parent, elm) / nodeOps.insertBefore(parent, elm, ref)
删除节点 newVnode 不存在而 oldVnode 存在, 使用 nodeOps.removeChild(parent, elm)
更新节点 newVnode 与 oldVnode 相同时, 进行更细致的比较 (patchVnode)
比较方法主要是tag与key是否相同, 细节上还会有是否存在data属性、是否都是注释节点、是否相同的输入类型
也就是说, 并不是两个节点完全相同, 仅判断节点能否复用
patchVnode
如有text属性, 则为文本节点, 使用textContent修改文本
如有children属性, 更新子节点, 主要是新旧列表的循环比对
新增子节点
更新子节点
移动子节点
删除子节点
优化策略(双指针, 执行条件是 (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) )
新前/旧前 是相同节点, 仅更新节点
新后/旧后 是相同节点, 仅更新节点
新后/旧前 是相同节点, 更新节点, 并将该DON移动到旧列表中未处理节点的最后
新前/旧后 是相同节点, 更新节点, 并将该DON移动到旧列表中未处理节点的最前
如果优化策略未能命中, 则进行循环对比
如果旧节点未设置key, 则采用节点的索引作为key
遍历新列表, 通过key在旧列表中查找对应节点
在新节点有key的情况下, 可以直接通过旧节点的对应表找到旧节点, 若没有就只能循环比对是否相同节点

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务