大前端-Vue源码分析

Vue源码解析-响应式原理

以下内容来自 拉勾教育大前端训练营 笔者在学习过程中对笔记进行的一个整理

心得体会

嘿嘿嘿~~~ 首先说说拉勾教育大前端训练营的课程视频吧,课程的质量是真的很好哦,并且已经收到了非常多的好评,在课程规划知识体系上,非常详细,对于每一个知识点都讲的非常透彻,可以说是 手摸手系列了,并且视频是学习完一章才能进入下一章学习,给我的感觉就是在打怪升级一样,学习完解锁下一章节,并且也非常期待下一章会Get到什么的技能,从而不断的强化自己 。

以在Vue框架源码与进阶为例,再看源码之前了,我学习了手写 Vue Router、手写Vue响应式实现、虚拟 DOMDiff 算法,以前只是停留在会用的阶段,到目前已经深入学习了Vue的响应式原理,和如何去手写实现一个Vue Router, Get到了非常多的技能以及 黑魔法

首先回顾 Vue Router的基本使用,以及 Hash模式和 History 模式的区别,然后自己手写一个实现基 History 模式的前端路由,了解路由内部实现的原理;接下来在数据响应式实现原理分析中,自己动手一个简易版本的 Vue;最后掌握虚拟 DOM 的作用,通过一个虚拟 DOMSnabbdom 真正了解什么是虚拟 DOM,以及 Diff算法的实现和key的作用。

除此之外呢,在学习完一个大章节都会进行一次互动直播答疑总结, 嘿嘿嘿~~~
我其实是非常期待每一次的直播的,因为每一次都是干货满满,收获很多,还有好几位助教老师在群里进行答疑,只要我们有不懂得问题,老师都会以最快的速度帮助我们解决,如果是实在解决不了的问题,熊熊老师会亲自git 你的代码,然后运行代码进行问题的定位,找到问题并解决之后也会告诉你是如果解决的,真的真的很贴心 ~ emmm 还有其实我们每章都有一个大作业,班主任老师呢会每天督促大家去完成作业,让大家都紧跟脚步,有问题,班主任老师也会及时记录下来。

再说一点吧,学习群每天都很活跃,每天大家都会遇到非常多的问题,大家只要把问题丢进去,很快就会得到其他同学的解答,包括我自己也是非常开心的帮助其他同学解决问题,在拉勾大前端训练营和大家一起学习,一起进步。

笔记将会对以下三点进行总结

  • Vue.js 的静态成员和实例成员初始过程
  • 首次渲染的过程
  • 数据响应式原理

一. 准备工作

Vue源码的获取

  • 项目地址 Vue源码获取
  • Fork 一份到自己的仓库,克隆到本地,可以自己写注释提交到github

为什么分析Vue2.6

  • 到目前为止 Vue3.0 的正式版本还没有发布
  • 新版本发布后,现有的项目不会升级到3.0, 2.x还有很长的一段过渡期
  • Vue3.0项目地址

源码目录结构

我们获取Vue源码后,重点看src下面的目录结构

|--src
   |--compiler        // 编译相关
   |--core            // Vue 核心库
   |--platforms       // 平台相关代码
   |-- server         // SSR,服务端渲染
   |--sfc             // .vue 文件编译为 js对象
   |--shared          // 公共的代码
  • compiler编译器把模板转换成render函数,render函数会帮我们创建虚拟DOM
  • core components 中定义了keep-alive组件,接下来是global-api, 它定义了Vue的静态方法, assets extend, mixin, use等方法
  • instance 是创建Vue实例的位置,这里定义了Vue的构造函数,以及Vue的初始化,还有Vue生命周期函数
  • observer Vue响应式核心
  • util 公共成员
  • vdom 虚拟DOM
  • platforms平台相关代码web weex
  • server vue2.0支持SSR,服务端渲染
  • sfc.vue文件编译为 js对象
  • shared公共的代码

了解Flow

  • Flow官网
  • JavaScript 的静态资源类型检测器
  • Flow的静态文件类型检查错误是通过静态类型推断实现的
  • 文件开头通过 // @flow或者 、/* @flow */ 声明,如下
/* @flow */
function square(n: number): number {
  return n * n;
}
square("2"); // Error

二.调试设置

如何对Vue源码进行打包和调试

打包

  • 打包工具Rollup
  • Vue.js源码的打包工具使用的是Rollup,相比Webpack更轻量
  • Webpack 把所有文件当做模块,Rollup只处理js文件更适合在Vue.js这样的库中使用
  • Rollup 打包不会生成冗余的代码

安装依赖

npm i

设置SourceMap
package.json 文件中的dev脚本中添加参数 --sourcemap方便我们调试

"dev": "rollup -w -c script/config.js --sourcemap --environment TARGET:web-full-dev"

执行dev

  • npm run dev 执行打包, 用的是rollup, -w参数是监听文件的变化,文件变化自动重新打包
  • 结果:
    在这里插入图片描述

package.json 文件中的dev脚本中添加参数 --sourcemap,方便我们调试,出现错误可以看到具体的位置

"dev": "rollup -w -c script/config.js --sourcemap --environment TARGET:web-full-dev"
  • -w是watch
  • -c 设置配置文件 scripts/config.js
  • --environment 环境变量,用来打包生成不同版本Vue

执行打包命令 npm run dev

  • 打包过程会先找到入口文件,然后会编译到dist 目录vue.js中
  • 此时dist中会生成两个文件vue.js 和vue.js.map

三.Vue 的不同构建版本

  • npm run build 重新打包所有文件
  • 官方文档- 对不同构建版本的解释
  • dist\REMADME.md
    来自拉勾大前端

术语

  • 完整版: 同时包含编译器运行时的版本
  • 编译器: 用来将模板字符串编译为JavaScript 渲染函数的代码,体积大,效率低
  • 运行时: 用来创建Vue实例、渲染并处理虚拟DOM等代码,体积小、效率高,基本就是出去编译器的代码
  • UMD: UMD版本通用的模块版本,支持多种模块方式,vue. js默认文件就是运行时+编译器的UMD版本
  • CommonJS(cjs): CommonJS版本用来配合老的打包工具比如Browserifywebpack 1.
  • ES Module:从2.6开始Vue会提供两个ES Modules (ESM)构建文件,为现代打包工具提供的版本。
  • ESM格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行 tree shaking并将用不到的代码排除出最终的包。
  • ES6模块与CommonJS模块的差异

vue inspect > output.js 输出文件 查看webpack配置

  • 我们在创建Vue-cli项目中使用的Vue版本就是vue.runtime.esm.js运行时的版本
  • 推荐使用运行时的版本

四.寻找入口文件

  • 查看 dist/vue.js 的构建过程

执行构建

npm run dev

"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
// environment TARGET:web-full-dev 设置环境变量TARGET

script/config.js 的执行过程

  • 作用: 生成rolllup 构建的配置文件
  • 使用环境变量 TARGET = web-full-dev
// 判断环境变量是否有TARGET
// 如果有的话使用genConfig() 生成rollup 配置文件
if (process.env.TARGET) {
  module.exports = genConfig(process.env.TARGET)
} else{
  // 否则获取全部配置
  exports.getBuild = genConfig
  exports.getAllBuilds = () => object.keys(builds).map(genConfig)
}

在package.json文件中

"script": {
  "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
  "build": "node scripts/build.js",
}
  • dev打包就是dist文件中的一个版本
  • build是所有的版本
  • TARGET:web-full-dev需要打包的版本

五.从入口开始

  • src/platform/web/entry-runtime-with-compiler.js

通过查看源码解决下面问题

  • 观察以下代码,通过阅读源码,回答在页面上输出的结果
const vm = new Vue({
  el: '#app',
  template: '<h3> Hello template</h3>',
  render (h) {
    return h('h4', 'Hello render')
  }
})

如果传入了render函数 不处理template,直接调用mount方法
阅读源码记录

  • el不能是body或者html标签
  • 如果没有render,把template转换成render函数
  • 如果有render方法,直接调用mount挂载DOM
 // el不能是body 或者html
 if (el === document.body || el === document.documentElement) {
   process.env.NODE_ENV !== 'production' && warn(
     `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
   )
   return this
 }

 const options = this.$options
 // resolve template/el and convert to render function
 // 把template/el 转换成render函数
 if (!options.render) {
   ...
   // 2把template/el转换成render 函数
 }
 // 3调用mount 方法,挂载DOM

调试代码, 调试的方法

const vm = new Vue({
  el: '#app',
  template: '<h3> Hello template</h3>',
  render (h) {
    return h('h4', 'Hello render')
  }
})

六.Vue 初始化过程

四个导出Vue的模块

1.src/platforms/wb/entry runtime with compilrjis

  • web平台相关的入口
  • 重写了平台相关的$mount()方法

2.注册了Vue compile()方法,传递一个HTML字符串返回render函数

  • src/platforms/web/runtime/index.js
  • web平台相关
  • 注册和平台相关的全局指令: v-model. v-show
  • 注册和平台相关的全局组件:v-transtion. v-tansition-group
  • 全局方法:
    • patch _:把虚拟DOM转换成真实DOM

    • $mount:挂载方法

3.src/core/index.js

  • 与平台无关
  • 设置了Vue的静态方法,itiltbalaPl(Vue)

4. src/core/instance/index.js

  • 与平台无关
  • 定义了构造函数,调用了`this.init(options)方法
  • Vue中混入了常用的实例成员

总结

platforms/web/runtime/index.js下的文件主要做了以下事情,在这个文件中所有代码都是和平台相关的,注册了平台相关的一些指令,patch函数以及$mount这个两个方法

  • import Vue from 'core/index' 导入了构造函数
  • core/index.js中,调用了initGlobalAPI(Vue)方法,给Vue的构造函数增加以下静态方法,其他内容都是调用Object.definePropertyVue增加了一些成员,还有服务端渲染SSR,
  • core/global-api初始化了Vue的静态方法
  • instance/index.js 创建了Vue构造函数,设置了Vue实例成员

八.Vue 初始化问题

  • Flow 语法红线 “javascript.validate.enable”: false
  • TS代码高亮 Babel JavaScript 插件

九. Vue初始化-静态成员

十.Vue初始化-实例成员

instance·文件夹

  • index.js定义了Vue的构造函数,并且调用了initMixin(Vue), stateMixin(Vue),eventsMixin(Vue) lifecycleMixin(Vue),renderMixin(Vue)
  • initMixin(Vue) 就是在Vue的原型上挂载了_init()方法
  • stateMixin(Vue) 通过Object.defineProperty(Vue.proptotype, '$data', dataDef)在Vue原型上增加了两个属性
  • eventsMixin(Vue) 分别定义了 $on,$once,$off,$emit事件,使用发布订阅模式
  • lifecycleMixin(Vue) 定义了forceUpdate destory()
  • renderMixin

这几个函数的作用都是给Vue原型混入一些成员和属性,给Vue对象增加相应的实例成员

// 注册vm的_init()方法, 初始化vm
initMixin(Vue)
// 注册vm 的$data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相关方法
//$on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入 render
// $nextTick/_render
renderMixin(Vue)

11.Vue实例-实例成员-init

12.Vue实例-实例成员-initState

初始化vm_props/methods/_data/computed/watch

以下是insrance/state.js initState()的源码

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

在instance/state.js中,首先获取了Vue实例中的$options,然后判断options中是否有props,methods,data以及computedwatch这些属性,如果有的话,通过initProps进行初始化
initProps(vm, opts.props)接收了两个参数,一个是Vue实例,一个是Props属性,我们跳转到initProps函数中,首先给Vue实例定义了一个_Props对象, 并且把它存储到了常量里面

const props = vm._props = {}

紧接着,开始遍历PropsOptions的所有属性,它其实就是initProps方法中的第二个参数,遍历每个属性,然后通过defineReactive注入到Props这个对象上,这个props其实就是vm._props所有的成员都会通过defineReacttive转化为getset,最后在Props对象上存储,

注意

  • 在开发模式中,如果我们直接给这个属性赋值的话,会发出一个警告,
  • 生产环境中直接通过defineReactiveprops中的属性转化成getset
  • 最后判断了props属性是否在Vue实例中存在,不存在通过Proxy这个函数把我们的属性注入到Vue的实例中

Proxy中,通过调用Object.defineProperty(target, key,sharePropertyDefinition)

总结initProps的作用就是把我们的Props成员转化成响应式数据,并且注入到Vue实例里面中

initMethods

initMethods(vm, opts.methods)中,也是接收两个参数,Vue实例和选项中的methods,首先获取了选项中的Props,遍历methods所有属性,然后判断当前的环境是否是开发或者生产
开发环境会判断methods是否是functicon

继续往下判断methods方法的名称是否在Props对象中存在,存在就会发送一个警告,警告在属性在Props中已经存在,因为Props和methods最终都要注入到Vue实例上,不能出现同名

之后判断key是否在Vue中存在,并且调用了isReserved(key),判断我们的key是否以_开头或$开头
最后把methods注入到Vue实例上来,注入的时候会判断是否是function,如果不是返回noop,是的话把函数返回bind(methods[key], vm)

总结 initMethods作用就是把选项的methods注入到vue实例,在注入之前,会先判断我们命名是否在Props中存在,并且判断了命名的规范,不建议_和$开头

initData(vm)

options中有data选项时,会调用initData(vm)
当没有的时候此时会给vm初始化一个_data属性observe(vm._data = {}, true),然后调用observe函数,observe是响应式中的一个函数

initData中获取了optionsdata选项,判断了data选项是否是function,如果是调用getData(data,vm)
接着获取data中的所有属性,同时获取了props,methods中所有的属性

const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods

最后做一个响应式处理

observe(data, true)

目前还没整理完成哦

猜你喜欢

转载自blog.csdn.net/sinat_35349493/article/details/107301783