重学 Vue.js
Vue.js
渐进式 MVVM 单向数据流 双向绑定
Vue 官网
Vue.js 技术揭秘
组件通信
- 父子组件通信
- props / $emit + v-on
- v-model
- $attrs / $listeners(inheritAttrs: false)
- 兄弟组件通信
- $parent.$children, 在 $children 中通过组件 name 查询到需要的组件实例
- Vue3: $refs
- 跨层级组件通信
- provide inject
- 任意组件通信
- Vuex
- EventBus
自定义 v-model
1 | <template> |
computed
1 | new Vue({ |
watch
1 | vm.$watch('obj', { |
生命周期 Vue2
beforeCreate
初始化一个空的 Vue 实例,data
methods
等尚未被初始化,无法调用
created
Vue 实例初始化完成,data
methods
都已初始化完成,但还没开始渲染模板
beforeMount
编译模板,调用 render
函数生成 vdom ,还没有开始渲染 DOM
mounted
渲染 DOM 完成,组件创建完成,开始进入运行阶段
beforeUpdate
在数据发生改变后,DOM 被更新之前被调用
在 DOM 更新之前移除手动添加的事件监听器
updated
在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用
尽量不要在 updated
中继续修改数据,否则可能会死循环
onActivated
被 keep-alive
缓存的组件激活时调用
onDeactivated
被 keep-alive
缓存的组件卸载时调用
beforeUnmount
销毁组件实例前调用,在这个阶段,实例仍然是完全正常的
清空解绑全局变量、定时器、注册的全局事件、自定义事件
unmounted
销毁组件实例后调用,所有子组件实例也被销毁
mounted
和 updated
都不会保证所有子组件
都挂载完成,$nextTick
等待所有视图渲染完成
Vue3 Composition API
setup
代替了beforeCreate
和created
- 生命周期换成了函数的形式,如
mounted
->onMounted
1 | import { onUpdated, onMounted } from 'vue' |
编译过程
parse
模板解析为 AST 树optimize
优化 AST 树codegen
AST 树转换成 Render 函数
通过执行 Render 函数生成 Virtual DOM 最终映射为真实 DOM
React 的 JSX,Vue 的 template 其实都是语法糖,它们本质上都是一个函数
render 函数
1 | // JSX: <p id="p1">hello world</p> |
每次数据更新(如 React setState)render 函数都会生成 newVnode,然后前后对比 diff(vnode, newVnode)
,计算出需要修改的 DOM 节点,再做更新
Virtual DOM
实现数据视图分离数据驱动视图
的根本,和 JQuery 有了本质的区别
- DOM 操作是昂贵的, JS 操作对象数据很快
- DOM 属于
渲染引擎
, JS 属于JS引擎
, 两个线程之间通信上的性能损耗 - 操作 DOM 会带来
回流重绘
的情况
- DOM 属于
- 通过 JS 模拟 DOM 并且渲染对应的 DOM
- 判断新旧两个 JS 对象的
最小差异
并且实现局部更新
DOM
Virtual DOM 一定就比原生 DOM 快吗?
不一定。如果可以人肉地精准去局部更新 DOM,那么 Virtual DOM 必然没有直接操作 DOM 快生成 vdom,进行 diff 算法,再修改 DOM
初始渲染
Virtual DOM > 脏检查 >= 依赖收集小量数据更新
依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) > Virtual DOM 无优化大量数据更新
脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无法/无需优化) >> MVVM 无优化
- 函数式的 UI 编程方式
- 将 Virtual DOM 作为一个兼容层,实现跨端
Svelte
与使用虚拟(virtual)DOM 差异对比不同。Svelte 编写的代码在应用程序的状态更改时就能像做外科手术一样更新 DOM。
Diff 算法
Diff 算法是一个非常普遍常用的方法,例如提交 github pr 或者(gitlab mr)时,会对比当前提交代码的改动,这就是 Diff
vdom 是一个多叉树
的结构,完整对比两颗树的差异,传统的 tree diff 时间复杂度是O(n^3)
,算法不可用
优化算法实现时间复杂度O(n)
,只同层对比节点,而不跨层对比
- 从上至下,从左往右遍历对象,也就是树的
深度遍历
tag
不同则直接删掉重建
- 然后再判断子节点
- 旧的列表中是否有节点被移除
- 新的列表中是否有节点的加入
- 节点是否有移动,
key
属性复用节点
- 同时还需判断节点的属性是否有变化
Vue React diff 算法有什么区别?
- React
仅向右移动
比较子节点时,仅向右移动,不向左移动 - Vue2
双端比较
定义四个指针,分别比较- oldStartNode 和 newStartNode
- oldStartNode 和 newEndNode
- oldEndNode 和 newStartNode
- oldEndNode 和 newEndNode
然后指针向中间移动,直到指针汇合
- Vue3
最长递增子序列
尽量减少 DOM 的移动,达到最少的 DOM 操作
nextTick
响应式原理
- Vue2中一个组件只有一个 Watcher,组件内部虚拟dom Diff
- new vue的过程: init $mount compile render vnode patch DOM
- 数据监听器 Observer
- 响应式 利用 Object.defineProperty 给数据添加 getter依赖收集 和 setter派发更新
- Dep 是整个 getter 依赖收集的核心,是对 Watcher 的一种管理
- 指令解析器 Compile
- 解析模板字符串生成 AST
const ast = parse(template, options)
- 优化语法树
optimize(ast, options)
标记静态节点、标记静态根 - 生成代码
const code = generate(ast, options)
- 解析模板字符串生成 AST
- Watcher,作为连接 Observer 和 Compile 的桥梁
1 | function Observer(data) { |
Vue-router
hash
使用 url Hash 修改路由地址onhashchange
1 | location.protocol // 'http:' |
history
使用 H5 history API 修改路由地址
history.pushState
window.onpopstate
页面刷新时,服务端要做处理
abstract
不改变 url,路由地址在内存中,页面刷新会重新回到首页
Vue-router v4 升级之后,mode: 'xxx'
替换为 API 的形式,但功能是一样的
mode: 'hash'
替换为createWebHashHistory()
mode: 'history'
替换为createWebHistory()
mode: 'abstract'
替换为createMemoryHistory()
性能优化
v-if 和 v-show
v-if
组件销毁与重建v-show
组件显示和隐藏(切换 CSSdisplay
)
v-for 使用 key
key
可以优化 diff 算法,遍历数组时 key
不要使用 index
computed 缓存
computed
可以缓存计算结果,依赖的数据不变则缓存不失效
keep-alive
<keep-alive>
可以缓存子组件,只创建一次。通过 activated
和 deactivated
生命周期监听是否处于激活状态
异步组件按需加载
- Vue3
defineAsyncComponent
路由懒加载
SSR 服务端渲染
- Nuxt.js
错误监控
errorCaptured 组件错误
1 | errorCaptured(error, instance, info) { |
会监听所有下级组件的错误。可以返回 false
阻止向上传播,因为可能会有多个上级节点都监听错误
errorHandler 全局错误
1 | const app = createApp(App) |
- Vue 全局错误,所有组件的报错都会汇总到这里来
- 如果
errorCaptured
返回false
则不会到这里 errorHandler
会阻止错误走向window.onerror
window.onerror try…catch 其它错误、异步
1 | window.onerror = function(msg, source, line, column, error) { |