文章内容输出来源:拉勾教育大前端高薪训练营
文章内容包括:模块作业、学习笔记
简答题
1、请简述 Vue 首次渲染的过程。
Vue 首次渲染的过程:
(1)Vue 初始化:初始化 vue 的实例成员、静态成员;
(2)new Vue():初始化结束之后,调用 vue 构造函数。构造函数中调用了 this._init() ,这个方法相当于vue的入口,最终调用 vm.$mount() ;
(3)调用入口文件的 vm.$mount():这个方法主要是将模板编译成 render 函数。先判断是否传递了 render 选项,如果没有传递 render ,就把模板编译成 render 函数。这个过程是通过 compileToFunctions() 生成 render() 渲染函数 (new Function)。最后将 options.render = render;
(4)调用runtime版本中的vm.$mount():在这个方法中会重新获取 el
(5)调用 lifecycle.js 中的 mountComponent(this, el):方法中先判断是否有render选项,如果没有但是传入了模板,并且当前是开发环境的话会发送警告;触发 beforeMount ;然后定义 updateComponent ,这个方法中会调用 vm._render() 渲染虚拟dom ,调用 vm._update() 将虚拟dom转换成真实dom;然后创建 Watcher 实例,创建过程中传入了 updateComponent 会在 Watcher 内部调用 ,再调用 get() 方法;然后触发 mounted ,最终返回 vm ;
(6)watcher.get():创建完 Watcher 会调用一次 get ,get内部调用 updateComponent() ;调用 vm._render() 创建 VNode :调用实例化时用户传入的 render 或者编译 template 生成的 render ,返回 VNode ;调用 vm._update() : 内部调用了 vm.__patch__(vm.$el, vnode)
去挂载真实dom 并记录 vm.$el 。
2、请简述 Vue 响应式原理。
Vue 响应式原理其实是在 vm._init() 中完成的,调用顺序 initState() --> initData() --> observe() 。observe() 就是响应式的入口函数。
(1)observe(value):这个方法接收一个参数 value ,就是需要处理成响应式的对象;判断 value 是否为对象,如果不是直接返回;判断 value 对象是否有 __ob__
属性,如果有直接返回;如果没有,创建 observer 对象;返回 observer 对象;
(2)Observer:给 value 对象定义不可枚举的 __ob__
属性,记录当前的 observer 对象;数组的响应式处理,覆盖原生的 push/splice/unshift 等方法,它们会改变原数组,当这些方法被调用时会发送通知;对象的响应式处理,调用 walk 方法,遍历对象的每个属性,调用 defineReactive ;
(3)defineReactive:为每一个属性创建 dep 对象,如果当前属性的值是对象,再调用 observe ;定义 getter ,收集依赖,返回属性的值;定义 setter ,保存新值,如果新值是对象,调用 observe,派发更新(发送通知),调用 dep.notify() ;
(4)依赖收集:在 Watcher 对象的 get 方法中调用 pushTarget 记录 Dep.target 属性;访问 data 中的成员时收集依赖, defineReactive 的 getter 中收集依赖;把属性对应的 watcher 对象添加到 dep 的 subs 数组中;给 childOb 收集依赖,目的是子对象添加和删除成员时发送通知;
(5)Watcher:dep.notify 在调用 watcher 对象的 update() 方法时,调用 queueWatcher() ,判断 watcher 是否被处理,如果没有的话添加到 queue 队列中,并调用 flushSchedulerQueue() : 触发 beforeUpdate 钩子,调用 watcher.run() , run() --> get() --> getter() --> updateComponent ,清空上一次的依赖,触发 actived 钩子,触发 updated 钩子。
3、请简述虚拟 DOM 中 Key 的作用和好处。
Key 的作用:
主要用来在虚拟 DOM 的 diff 算法中,在新旧节点的对比时辨别 vnode ,使用 key 时,Vue 会基于 key 的变化重新排列元素顺序,尽可能的复用页面元素,只找出必须更新的DOM,最终可以减少DOM操作。常见的列子是结合 v-for 来进行列表渲染,或者用于强制替换元素/组件。
设置 Key 的好处:
(1)数据更新时,可以尽可能的减少DOM操作;
(2)列表渲染时,可以提高列表渲染的效率,提高页面的性能;
4、请简述 Vue 中模板编译的过程。
模板编译的过程:
(1)compileToFunctions(template, ...):模板编译的入口函数,先从缓存中加载编译好的 render 函数,如果缓冲中没有,则调用 compile(template, options) 开始编译;
(2)compile(template, options):先合并选项 options ,再调用 baseCompile(template.trim(), finalOptions) ;compile 的核心是合并 options ,真正处理模板是在 baseCompile 中完成的;
(3)baseCompile(template.trim(), finalOptions):先调用 parse() 把 template 转换成 AST tree ;然后调用 optimize() 优化 AST ,先标记 AST tree 中的静态子树,检测到静态子树,设置为静态,不需要在每次重新渲染的时候重新生成节点,patch 阶段跳过静态子树;调用 generate() 将 AST tree 生成js代码;
(4)compileToFunctions(template, ...):继续把上一步中生成的字符串形式的js代码转换为函数,调用 createFunction() 通过 new Function(code)
将字符串转换成函数; render 和 staticRenderFns 初始化完毕,挂载到 Vue 实例的 options 对应的属性中。
学习笔记
Vue.js 源码剖析-响应式原理
准备工作
Vue 源码获取
- 项目地址: https://github.com/vuejs/vue
- Fork 一份到自己的仓库,克隆到本地,可以自己写注释提交到 github
为什么分析 Vue 2.6
- 到目前为止 Vue 3.0 的正式版还没有发布
- 新版本发布后,现有项目不会升级到 3.0 ,2.x 还有很长的一段过渡期
- 3.0 项目地址: https://github.com/vuejs/vue-...
了解 Flow
- 官网: https://flow.org/
- JavaScript 的静态类型检查器
- Flow 的静态类型检查错误是通过静态类型推断实现的
- 文件开头通过
// @flow
或者/* @flow */
声明
调式设置
打包
打包工具 Rollup
- Vue.js 源码的打包工具使用的是 Rollup , 比 webpack 轻量
- webpack 把所有文件当做模块,Rollup 只处理 js 文件更适合在 Vue.js 这样的库中使用
- Rollup 打包不会生成冗余的代码
设置 sourcemap
- package.json 文件中的 dev 脚本中添加参数 --sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
执行 dev
- npm run dev 执行打包,用的是 rollup , -w 参数是监听文件的变化,文件变化自动重新打包
Vue的不同构建版本
- npm run build 重新打包所有文件
术语
- 完整版:同时包含编译器和运行时的版本
- 编译器:用来将模板字符串编译称为 JS 渲染函数的代码,体积大、效率低
- 运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码,体积小、效率高、基本上就是除去编译器的代码
- UMD:UMD 版本是通用的模板版本,支持多种模块方式。vue.js 默认文件就是运行时 + 编译器的 UMD 版本
- CommonJS(cjs): CommonJS 版本用来配合老的打包工具比如 Browserify 或 webpack 1
ES Module: 从 2.6 开始 Vue 会提供两个 ES Modules 构建文件,为现代打包工具提供的版本
- ESM 格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行 tree-shaking 并将用不到的代码排除出最终的包
- ES6 模块与 CommonJS 模块的差异,参考 https://es6.ruanyifeng.com/?s...
寻找入口文件
- 查看 dist/vue.js 的构建过程
- 执行 npm run dev
script/config.js 的执行过程
- 作用:生成 rollup 构建的配置文件
- 使用环境变量 TARGET = web-full-dev
从入口开始
- srcplatformswebentry-runtime-with-compiler.js
阅读源码记录
- el 不能是 body 或者 html 标签
- 如果没有 render ,把 template 转换成 render 函数
- 如果有 render 方法,直接调用 mount 挂载 DOM
Vue初始化的过程
四个导出Vue的模块
srcplatformswebentry-runtime-with-compiler.js
- web 平台相关的入口
- 重写了平台相关的 $mount() 方法
- 注册了 Vue.compile() 方法,传递一个 HTML 字符串返回 render 函数
srcplatformswebruntimeindex.js
- web平台相关
- 注册和平台相关的全局指令: v-model v-show
- 注册和平台相关的全局组件: v-transition v-transition-group
- 全局方法:
__patch__
: 把虚拟dom转换成真实DOM;$mount
: 挂载方法
srccoreindex.js
- 与平台无关
- 设置了 vue的静态方法,initGlobalAPI(Vue)
srccoreinstanceindex.js
- 与平台无关
- 定义了狗仔函数,调用了 this._init(options) 方法
- 给 Vue 中混入了常用的实例成员
数据响应式原理
通过查看源码解决下面的问题
- vm.msg = { count: 0 } ,重新给属性赋值,是否是相应式的?
- vm.arr[0] = 4 , 给数组元素赋值,视图是否会更新?
- vm.arr.length = 0 , 修改数组的 length ,视图是否会更新
- vm.arr.push(4) , 视图是否会更新
相应式处理的入口
src/core/instance/init.js
- initState(vm) vm 状态的初始化
- 初始化了 _data , _props , methods 等等
- src/core/instance/state.js
Watcher 类
- Watcher 分为三种,Computed Watcher 、用户 Watcher (侦听器)、渲染 Watcher
- 渲染 Watcher 的创建时机: /src/core/instance/lifecycle.js
set 方法源码
- Vue.set() global-api/index.js
- vm.$set() instance/index.js
delete 方法源码
- Vue.delete() global-api/index.js
- vm.$delete() instance/index.js
- 删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制,但是应该很少会使用它。
- 注意:目标对象不能是一个 Vue 实例或 Vue 实例的跟数据对象
vm.$watch
vm.$watch(expOrFn, callback, [options])
- 观察 Vue 实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。表达式只接受监督的键路径。对于更复杂的表达式,用一个函数取代。
- expOrFn: 要监视的 $data 中的属性,可以是表达式或函数
callback: 数据变化后执行的函数
- 函数:回调函数
- 对象: 具有 handler 属性(字符串或者函数),如果该属性为字符串则 methods 中相应的定义
options: 可选的选项
- deep: 布尔类型,深度监听
- immediate: 布尔类型,是否立即执行一次回调函数
三种类型的 Watcher 对象
- 没有静态方法,因为 $watch 方法中要使用 Vue 的实例
- Watcher 分三种:计算属性 Watcher、用户 Watcher(侦听器)、渲染 Watcher
- 创建顺序:计算属性 Watcher、用户 Watcher(侦听器)、渲染 Watcher
- vm.$watch() : src/core/instance/state.js
异步更新队列-nextTick()
- Vue 更新 DOM 是异步执行的,批量的
- 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
vm.$nextTick(function () { /*操作DOM*/ })
或Vue.nextTick(function () {})
- 源码位置: src/core/instance/render.js
源码
- 手动调用 vm.$nextTick()
- 在 Watcher 的 queueWatcher 中执行 nextTick()
- src/core/util/next-tick.js
- 核心是 timerFunc 函数的处理,Promise -> MutationObserver -> setImmediate -> setTimeout
Vue.js 源码剖析-虚拟 DOM
什么是虚拟DOM
- 虚拟 DOM (Virtual DOM) 是使用 JavaScript 对象描述真实 DOM
- Vue.js 中的虚拟 DOM 借鉴 Snabbdom ,并添加了 Vue.js 的特性。例如:指令和组件机制
为什么要使用虚拟 DOM
- 避免直接操作 DOM,提高开发效率
- 作为一个中间件可以跨平台
虚拟 DOM 不一定可以提高性能
- 首次渲染的时候会增加开销
- 复杂视图情况下提升渲染性能
h 函数
vm.$createElement(tag, data, children, normalizeChildren)
- tag: 标签名或者组件对象
- data: 描述 tag ,可以设置 DOM 的属性或者标签的属性
- children: tag 中的文本内容或者子节点
设置 key 的情况
设置 Key 的好处:
(1)数据更新时,可以尽可能的减少DOM操作;
(2)列表渲染时,可以提高列表渲染的效率,提高页面的性能;
Vue.js 源码剖析-模板编译和组件化
模板编译的作用
- Vue 2.x 使用 VNode 描述视图以及各种交互,用户自己编写 VNode 比较复杂
- 用户只需要编写类似 HTML 的代码 - Vue.js 模板,通过编译器将模板转换为返回 VNode 的 render 函数
- .vue 文件会被 webpack 在构建的过程中转换成 render 函数
编译生成的函数的位置
- _c() srccoreinstancerender.js
- _m()/_v()/_s() srccoreinstancerender-helpersindex.js
抽象语法树
什么是抽象语法树
- 抽象语法树简称 AST (Abstract Syntax ZTree)
- 使用对象的形式描述树形的代码结构
- 此处的抽象语法树是用来描述树形结构的 HTML 字符串
为什么要使用抽象语法树
- 模板字符串转换成 AST 后,可以通过 AST 对模板做优化处理
- 标记模板中的静态内容,在 patch 的时候直接跳过静态内容
- 在 patch 的过程中静态内容不需要对比和重新渲染
组件化
- 一个 Vue 组件就是一个拥有预定义选项的一个 Vue 实例
- 一个组件可以组成页面上一个功能完备的区域,组件可以包含脚本、样式、模板
首次渲染过程
- Vue 构造函数
- this._init()
- this.$mount()
- mountComponent()
- new Watcher() 渲染 Watcher
- updateComponent()
- vm._render() -> createElement()
- vm._update()