- 本阶段围绕当下国内最主流的前端核心框架 Vue.js 展开,深入框架内部,通过解读源码或者手写实现的方式,剖析 Vue.js 框架的内部实现原理,让你做到知其所以然。同时我们还会介绍 Vue.js 的进阶用法、周边生态以及性能优化,让你轻松应对更加复杂的项目业务需求。
模块二 Vue.js 源码分析(响应式、虚拟 DOM、模板编译和组件化)
- 本模块会带你深入分析 Vue.js 源码,包括:Vue.js 初始化开始、首次渲染的过程、响应式的依赖收集、Watcher 渲染视图、虚拟 DOM 和模板编译等。通过阅读源码了解底层实现,学习 Vue.js 处理问题的方式,最终解决你的面试难题。
任务一:Vue.js 基础回顾
- 课程目标
- Vue.js 的静态成员和实例成员初始化过程
- 首次渲染的过程
- 数据响应式原理
- 准备工作-目录结构
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RUXnn0Mr-1626755610301)(./img/1626400047632.jpg)]
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lUQSKmCa-1626755610302)(./img/1626400344105.jpg)]
- 准备工作-调试
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tU8njL60-1626755610303)(./img/1626400520787.jpg)]
- 准备工作-Vue的不同构建版本
- 完整版包含编译器,编译器有三千多行代码
- vue-cli 创建的项目
- 基于vue-cli创建的项目,也是运行时版本
- vue inspect 可以查看当前生成的项目webpack配置
- vue inspect > output.js >把前面的运行结果输出到后面的文件中去,所以单文件组件在运行的时候,也是不需要编译器的
- 在打包的时候,会把单文件组件转换为对象,转换成对象的过程中,还会把模版转换成render函数
- 问题:既然运行时版不带编译器,那么vue-cli生成的项目打包时,是谁把单文件组件中的template模版编译成render函数的?
- vue-loader
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HYuL8Ahz-1626755610305)(./img/1626401117090.jpg)]
- 寻找入口文件
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IhzGHnNl-1626755610306)(./img/1626402472997.jpg)]
- 从入口开始
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I3mMaTtl-1626755610307)(./img/1626403526107.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zV3VvSby-1626755610308)(./img/1626404140804.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-31aNYNhi-1626755610309)(./img/1626404261175.jpg)]
- Vue初始化的过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bClQMW1Y-1626755610309)(./img/1626405023286.jpg)]
-
Vue初始化-两个问题
-
Vue初始化-静态成员
-
Vue初始化-实例成员
-
Vue初始化-实例成员-init
-
Vue初始化-实例成员-initState
-
调试Vue初始化过程
-
首次渲染过程
-
首次渲染过程-总结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r2ygLpS9-1626755610310)(./img/1626421617827.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9esIPSDz-1626755610311)(./img/1626421558838.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TXc4b1gJ-1626755610311)(./img/1626421451552.jpg)]
- 数据响应式原理-响应式处理入口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-70TU8dVf-1626755610312)(./img/1626484992612.jpg)]
-
数据响应式原理-Observer
-
数据响应式原理-defineReactive
-
数据响应式原理-依赖收集
-
数据响应式原理-依赖收集-调试
-
数据响应式原理-数组
-
数据响应式原理-数组练习
-
数据响应式原理-Watcher上
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WjqtioTI-1626755610313)(./img/1626492470517.jpg)]
-
数据响应式原理-Watcher下
-
数据响应式原理-调试上
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tmCRRTd8-1626755610313)(./img/1626493545750.jpg)]
- 如果数组中有一个是对象,修改这个对象的为数字,会触发更新嘛? [2, 3, {a: 3}] > [2, 3, 4]
-
数据响应式原理-调试下
-
数据响应式原理-总结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K2PmFSYa-1626755610314)(./img/1626501527376.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mEDnAViz-1626755610315)(./img/1626501591811.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hSstCfBQ-1626755610316)(./img/1626501640418.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bLDRmIGo-1626755610316)(./img/1626501683897.jpg)]
-
动态添加一个响应式属性
-
set-源码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sG0ceoZC-1626755610317)(./img/1626502111658.jpg)]
-
set-调试
-
delete
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ES5KhrBJ-1626755610317)(./img/1626503158203.jpg)]
-
delete-源码
-
watch-回顾
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sSD2oVM1-1626755610318)(./img/1626503582159.jpg)]
- 三种类型的 Watcher
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pBDtL6G2-1626755610319)(./img/1626504201779.jpg)]
- watch-源码
- 没有静态方法,因为内部要用到Vue的实例
- nextTick-回顾
- 使用场景:比如一个聊天窗,当用户发了新消息后,消息位于最下面,在用户更新消息的地方,调用 vm.$nextTick() 执行聊天窗下拉。(在本次虚拟dom更新后执行,还没有更新到页面上)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z3TTC5Il-1626755610319)(./img/1626505478550.jpg)]
- nextTick-源码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-12gCJp6B-1626755610320)(./img/1626505737168.jpg)]
- setImmediate 比 setTimeout 性能好,setTimeout及时设置为0,也要等4毫秒
任务二:Vue.js 源码剖析-虚拟 DOM
- 课程回顾
- 虚拟DOM库 - Snabbdom
- Vue.js 响应式原理模拟实现
- Vue.js 源码剖析 - 响应式原理
- 虚拟 DOM 概念回顾
- 什么是虚拟DOM
- 虚拟DOM (Virtual DOM) 是使用JavaScript对象描述真实DOM
- Vue.js 中的虚拟DOM借鉴Snabbdom,并添加了Vue.js的特性。例如:指令和组件机制
- 为什么要使用虚拟DOM
- 避免直接操作DOM,提高开发效率
- 作为一个中间层可以跨平台
- 虚拟DOM不一定可以提高性能
- 首次渲染的时候增加开销
- 复杂视图情况下提升渲染性能
- 代码演示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MXFvehkK-1626755610320)(./img/1626509569572.jpg)]
- VNode 的核心属性: tag data children text elm key
-
整体过程分析
-
createElement-上
-
createElement-下
-
update
-
patch 函数的初始化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kS7aYchc-1626755610321)(./img/1626510981105.jpg)]
-
patch
-
createElm
-
patchVnode
-
updateChildren
-
没有设置key的情况
-
设置key的情况
任务三:Vue.js 源码分析(响应式、虚拟 DOM、模板编译和组件化)
- 模板编译介绍
- Vue2.x使用NNode描述视图以及各种交互,用户自己编写VNode比较复杂
- 用户只需要编写类似HTML的代码 - Vue.js模版,通过编译器将模版转换为返回VNode的render函数
- .vue 文件会被webpack 在构建的过程中转换成render函数
- 编译分为运行时编译和打包构建时编译,运行时编译必须是Vue完整版,打包时编译webpack可以借助vue-loader
-
体验模板编译的结果-上
-
体验模板编译的结果-下
-
Vue Template Explorer
- template-explorer.vuejs.org 可以把模版转换成 render函数。vue2
- 模版中html 标签内部尽量不要有空白内容(空格和换行),因为在 render函数中也会对应的产生空格和换行,占用内存空间,影响性能
- vue-next-template-explorer.netlify.app vue3
- vue3 编译后已经去除了多余的空白
- 模板编译的入口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qruP7POL-1626755610322)(./img/1626531399907.jpg)]
-
模板编译过程-compileToFunctions
-
模板编译过程-compile
-
模板编译过程-baseCompile-AST
- 什么是抽象语法树
- 抽象语法树简称AST(ABstract Syntax Tree)
- 使用对象的形式描述树形的代码结构
- 此处的抽象语法树是用来描述树形结构的HTML字符串
- 为什么要使用抽象语法树
- 模版字符串转换成AST后,可以通过AST对模版做优化处理
- 标记模版中的静态内容,在patch的时候直接跳过静态内容
- 在patch的过程中静态内容不需要对比和重新渲染
- astexplorer.net 查看模版编译后的语法树
-
模板编译过程-baseCompile-parse
-
模板编译过程-baseCompile-optimize
-
模板编译过程-generate-上
-
模板编译过程-generate-下
-
模板编译过程-调试
-
模板编译过程-总结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AKUboAmK-1626755610322)(./img/1626575626048.jpg)]
- 组件化回顾
- 一个Vue组件就是一个拥有预定义选项的一个Vue实例
- 一个组件可以组成页面上一个功能完备的区域,组件可以包含脚本、样式、模版
-
组件注册
-
Vue.extend
-
调试组件注册过程
-
组件的创建过程
-
组件的 patch 过程
-
总结
- 打包过程
- “dev”: “rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev”
- -w 监控 -c 使用配置文件 scripts/config.js
- 通过 scripts/config.js 找到 src/platforms/web/entry-runtime-with-compiler.js’
- 四个导出Vue的文件
- src/instance/index 给原型对象添加成员方法
- Vue 的构造函数,此处不用class 的原因是因为方便后续给 Vue 实例混入实例成员
- 测试环境下,如果不是通过new Vue 的方式调用构造函数,报错
- 调用_init 方法
- initMixin(Vue) 给Vue的prototype添加 _init 方法,在_init中完成实例(vm)的初始化
- stateMixin(Vue) 注册 vm 的 d a t a / data/ data/props/ s e t / set/ set/delete/$watch
- eventMixin(Vue) 初始化事件相关方法, o n , on, on,once, o f f , off, off,emit
- lifecycleMinxins(Vue)初始化生命周期相关混入方法 _update/ f o r c e U p d a t e / forceUpdate/ forceUpdate/destroy
- renderMixin(Vue) $nextTick/_render 安装运行时便利助手 Vue.prototype._b/_d…
- Vue 的构造函数,此处不用class 的原因是因为方便后续给 Vue 实例混入实例成员
- src/core/index
- initGlobalAPI(Vue) 给Vue添加静态成员方法
- Vue.set/delete/nextTick
- Vue.observable 让一个对象变成响应式对象
- Vue.options 初始化 Vue.options 对象,并给其扩展
- extend(Vue.options.components, builtInComponents) 设置keep-alive 组件到Vue.options.component上
- initUse(Vue) 注册 Vue.use() 用来注册插件
- initMixin(Vue) 注册 Vue.minxin() 实现混入
- initExtend(Vue) Vue.extend() 基于传入的options返回一个组件的构造函数
- initAssetRegisters(Vue) 注册 Vue.directive()/Vue.component()/Vue.filter()
- Vue.component() 实现本质上是调用Vue.extend(),只是多加了一个全局注册的步骤,挂载在了this.options[‘components’]上
- initGlobalAPI(Vue) 给Vue添加静态成员方法
- src/platforms/web/runtime/index
- extend(Vue.options.directives, platformDirectives) 加载平台内置的指令和组件
- Vue.prototype.patch 给 Vue 原型对象挂载__patch__ 方法
- Vue.prototype. m o u n t 给 V u e 原 型 对 象 上 添 加 mount 给Vue原型对象上添加 mount给Vue原型对象上添加mount方法
- src/platforms/web/entry-runtime-with-compiler.js
- 重写 Vue.prototype.$mount 方法
- 主要判断是否有render函数
- 如果没有通过compileToFunctions 把template 模版编译成render函数
- Vue.compile = compileToFunctions 导出把template 转换为 render 的函数
- 重写 Vue.prototype.$mount 方法
- Vue 首次渲染的过程
- 先进行Vue的初始化
- 给原型对象添加成员方法
- 给 Vue 添加静态成员方法
- 加载平台内置指令和组件,给原型添加 新旧虚拟DOM对比的方法_patch_,给原型加上 $mount 方法
- 重写 $mount 在其中加入编译器
- 执行 _init 方法
- 合并参数options
- vm 生命周期相关变量初始化
- 调用 vm.$mount 方法进行挂载
- 如果有render函数,直接进行下一步,没有则找template 编译成render 函数
- 通过 _render 函数把 render函数转换为虚拟DOM
- 通过 _update 函数 中调用 patch 对比 DOM(el) 和 newVNode 区别,更新DOM 完成渲染
- 执行Vue构造函数,调用 _init 方法。
- vm._uid 标记uid=0
- vm._isVue = true 标记是 Vue 实例
- 合并实例和构造函数上的 options
- initLifecycle(vm) vm 的生命周期相关变量初始化 c h i l d r e n / children/ children/parent/ r o o t / root/ root/refs
- initEvents(vm) vm 的事件监听初始化,父组件绑定在当前组件上的事件
- initRender(vm) vm 的编译render初始化
- vm._c 对编译生成的 render 进行渲染的方法
- vm.$createElement 手写 render 函数进行渲染的方法
- 给 vm. a t t r s , v m . attrs, vm. attrs,vm.listener 添加监控
- callHook(vm, ‘beforeCreate’) 生命钩子回调
- initInjections(vm) 把 inject 的成员注入到 vm上
- initState(vm) 初始化vm的 _props/methods/_data/computed/watch
- initProvide(vm) 初始化 provide
- callHook(vm, ‘created’) 触发created 生命钩子函数
- vm.
m
o
u
n
t
(
v
m
.
mount(vm.
mount(vm.options.el) 调用$mount挂载
- el = el && query(el) 获取el对象
- 判断是否有 render函数,如果没有rander函数,则判断tepmlate是否存在,存在则把template转换为模版
- mount.call(this, el) 调用 mount 方法,渲染 DOM
- 获取 el 对象
- 执行 mountComponent(this, el) 函数
- callHook(vm, ‘beforeMount’) 执行 beforeMount 函数
- new Watcher 创建观察者
- 执行 get() > 调用 updateComponent
- pushTarget(this) 和 popTarget() 给 Dep.target 赋值和去掉值
- 执行 _render
- render.call(vm._renderProxy, vm.$createElement)
- vm.$createElement
- createElement
- createElement
- vm.$createElement
- render.call(vm._renderProxy, vm.$createElement)