【Vue2源码学习分析】

# 文件结构

源码目录

# 调试环境搭建

  • 安装依赖: npm i
  • 安装rollup: npm i -g rollup
  • 修改dev脚本,添加sourcemap,package.json
 "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web- full-dev",
    
    
  • 运行开发命令: npm run dev
  • 引入前面创建的vue.js,samples/commits/index.html
<script src="../../dist/vue.js"></script>
    
    

术语解释:

  • runtime:仅包含运行时,不包含编译器
  • common:cjs规范,用于webpack1
  • esm:ES模块,用于webpack2+
  • umd: universal module definition,兼容cjs和amd,用于浏览器

# 入口

dev脚本中 -c scripts/config.js 指明配置文件所在

参数 TARGET:web-full-dev 指明输出文件配置项

// Runtime+compiler development build (Browser){ 'web-full-dev': { entry: resolve('web/entry-runtime-with-compiler.js'), // 入口 dest: resolve('dist/vue.js'),// 目标文件 format: 'umd', // 输出规范 env: 'development', alias: { he: './entity-decoder' }, banner, },}
    
    

# 初始化流程

  1. 入口 platforms/web/entry-runtime-with-compiler.js

扩展默认$mount方法:处理template或el选项

  1. platforms/web/runtime/index.js
  • 安装web平台特有指令和组件
  • 定义__patch__:补丁函数,执行patching算法进行更新
  • 定义$mount:挂载vue实例到指定宿主元素(获得dom并替换宿主元素)
  1. core/index.js

初始化全局api 具体如下:

 Vue.set = setVue.delete = delVue.nextTick = nextTickinitUse(Vue) // 实现Vue.use函数initMixin(Vue) // 实现Vue.mixin函数initExtend(Vue) // 实现Vue.extend函数initAssetRegisters(Vue) // 注册实现Vue.component/directive/filter
    
    
  1. core/instance/index.js

Vue构造函数定义

定义Vue实例API

 function Vue (options) { // 构造函数仅执行了_init this._init(options)}initMixin(Vue) // 实现init函数stateMixin(Vue) // 状态相关api $data,$props,$set,$delete,$watch eventsMixin(Vue)// 事件相关api $on,$once,$off,$emit lifecycleMixin(Vue) // 生命周期api _update,$forceUpdate,$destroy renderMixin(Vue)// 渲染api _render,$nextTick
    
    
  1. core/instance/init.js

创建组件实例,初始化其数据、属性、事件等

initLifecycle(vm) // $parent,$root,$children,$refs initEvents(vm) // 处理父组件传递的事件和回调initRender(vm) // $slots,$scopedSlots,_c,$createElement callHook(vm, 'beforeCreate') initInjections(vm) // 获取注入数据initState(vm) // 初始化props,methods,data,computed,watch initProvide(vm) // 提供数据注入callHook(vm, 'created')
    
    
  1. $mount

mountComponent

执行挂载,获取vdom并转换为dom

new Watcher()

创建组件渲染watcher

updateComponent()

执行初始化或更新

update()

初始化或更新,将传入vdom转换为dom,初始化时执行的是dom创建操作

render() src\core\instance\render.js

渲染组件,获取vdom

整体流程

new Vue() => _init() => $mount() => mountComponent() => updateComponent()/new Watcher() => render() => _update()

# 数据响应式

Vue一大特点是数据响应式,数据的变化会作用于UI而不用进行DOM操作。原理上来讲,是利用了JS语 言特性Object.defineProperty(),通过定义对象属性setter方法拦截对象属性变更,从而将数值的变化 转换为UI的变化

具体实现是在Vue初始化时,会调用initState,它会初始化data,props等,这里着重关注data初始 化,

整体流程

  1. initState (vm: Component) src\core\instance\state.js

初始化数据,包括propsmethodsdatacomputedwatch

  1. initData核心代码是将data数据响应化
function initData (vm: Component) { // 执行数据响应化 observe(data, true /* asRootData */)}
    
    
  1. core/observer/index.js

observe方法返回一个Observer实例

  1. core/observer/index.js
  • Observer对象根据数据类型执行对应的响应化操作
  • defineReactive定义对象属性的getter/setter,getter负责添加依赖,setter负责通知更新
  1. core/observer/dep.js

Dep负责管理一组Watcher,包括watcher实例的增删及通知更新

  1. Watcher
  • Watcher解析一个表达式并收集依赖,当数值变化时触发回调函数,常用于$watch API和指令中。
  • 每个组件也会有对应的Watcher,数值变化会触发其update函数导致重新渲染
 export default class Watcher { constructor () {} get () {} addDep (dep: Dep) {} update () {}}
    
    

相关API: $watcher

测试代码examples\test\02-1-reactive.html

# 数组响应化

数组数据变化的侦测跟对象不同,我们操作数组通常使用push、pop、splice等方法,此时没有办法得 知数据变化。所以vue中采取的策略是拦截这些方法并通知dep。

1. src\core\observer\array.js

为数组原型中的7个可以改变内容的方法定义拦截器

2. Observer中覆盖数组原型

 if (Array.isArray(value)) { // 替换数组原型 protoAugment(value, arrayMethods) // value.__proto__ = arrayMethods  this.observeArray(value)}
    
    

测试代码examples\test\02-2-reactive-arr.html

相关API: Vue.set()/delete()

 data: { arr: []}
    
    
 arr.length = 0 arr[index] = xxxVue.set() Vue.del()
    
    

# 异步更新队列

Vue高效的秘诀是一套批量、异步的更新策略

# 概念解释

  • 事件循环Event Loop:浏览器为了协调事件处理、脚本执行、网络请求和渲染等任务而制定的工 作机制。
  • 宏任务Task:代表一个个离散的、独立的工作单元。浏览器完成一个宏任务,在下一个宏任务执行 开始前,会对⻚面进行重新渲染。主要包括创建文档对象、解析HTML、执行主线JS代码以及各种 事件如⻚面加载、输入、网络事件和定时器等。
  • 微任务:微任务是更小的任务,是在当前宏任务执行结束后立即执行的任务。如果存在微任务,浏 览器会清空微任务之后再重新渲染。微任务的例子有 Promise 回调函数、DOM变化等。

# vue中的具体实现

  • 异步:只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变 更。

  • 批量:如果同一个 watcher 被多次触发,只会被推入到队列中一次。去重对于避免不必要的计算 和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列执行实际工作

  • 异步策略:Vue 在内部对异步队列尝试使用原生的 Promise.then 、 MutationObserver

    或 setImmediate ,如果执行环境都不支持,则会采用 setTimeout 代替。

1. update() core\observer\watcher.js

dep.notify()之后watcher执行更新,执行入队操作

2. queueWatcher(watcher) core\observer\scheduler.js

执行watcher入队操作

3. nextTick(flushSchedulerQueue) core\util\next-tick.js

nextTick按照特定异步策略执行队列操作

watcher中update执行三次,但run仅执行一次,且数值变化对dom的影响也不是立竿⻅影的。

请大家研究一下相关API: vm.$nextTick(cb)

# 虚拟DOM

# 概念

虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用 的各种状态变化会作用于虚拟DOM,最终映射到DOM上。

# 体验虚拟DOM

vue中虚拟dom基于snabbdom实现,安装snabbdom并体验

< !DOCTYPE html > <html lang = "en" > <head > </head><body><div id="app"></div > <!--安装并引入snabbdom--><script src = "../../node_modules/snabbdom/dist/snabbdom.js" > </script><script> / / 之前编写的响应式函数 function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get() { return val }, set(newVal) { val = newVal // 通知更新 update() } }) } // 导入patch的工厂init,h是产生vnode的工厂 const { init, h } = snabbdom // 获取patch函数 const patch = init([]) // 上次vnode,由patch()返回 let vnode; // 更新函数,将数据操作转换为dom操作,返回新vnode function update() { if (!vnode) { // 初始化,没有上次vnode,传入宿主元素和vnode vnode = patch(app, render()) } else { // 更新,传入新旧vnode对比并做更新 vnode = patch(vnode, render()) } } // 渲染函数,返回vnode描述dom结构 function render() { return h('div', obj.foo) } // 赋一个日期作为初始值 obj.foo = new Date().toLocaleTimeString() // 定时改变数据,更新函数会重新执行 setInterval(() => { obj.foo = new Date().toLocaleTimeString() }, 1000); < /script></body > </html>
    
    

# 优点

  • 虚拟DOM轻量、快速:当它们发生变化时通过新旧虚拟DOM比对可以得到最小DOM操作量,配 合异步更新策略减少刷新频率,从而提升性能
patch(vnode, h('div', obj.foo))
    
    
  • 跨平台:将虚拟dom更新转换为不同运行时特殊操作实现跨平台
 <script src="../../node_modules/snabbdom/dist/snabbdom-style.js"></script> <script>// 增加style模块const patch = init([snabbdom_style.default])function render() {// 添加节点样式描述return h('div', { style: { color: 'red' } }, obj.foo)}</script>
    
    
  • 兼容性:还可以加入兼容性代码增强操作的兼容性

# 必要性

vue 1.0中有细粒度的数据变化侦测,它是不需要虚拟DOM的,但是细粒度造成了大量开销,这对于大 型项目来说是不可接受的。因此,vue 2.0选择了中等粒度的解决方案,每一个组件一个watcher实例, 这样状态变化时只能通知到组件,再通过引入虚拟DOM去进行比对和渲染。

# 整体流程

1. mountComponent() core/instance/lifecycle.js

渲染、更新组件

 // 定义更新函数const updateComponent = () => {// 实际调用是在lifeCycleMixin中定义的_update和renderMixin中定义的_rendervm._update(vm._render(), hydrating) }
    
    

2. _render core/instance/render.js

生成虚拟dom

3. _update core\instance\lifecycle.js

update负责更新dom,转换vnode为dom

4. patch() platforms/web/runtime/index.js

__patch__是在平台特有代码中指定的

Vue.prototype.__patch__ = inBrowser ? patch : noop
    
    

测试代码,examples\test\04-vdom.html

# patch获取

patch是createPatchFunction的返回值,传递nodeOps和modules是web平台特别实现

 export const patch: Function = createPatchFunction({ nodeOps, modules })
    
    

1. platforms\web\runtime\node-ops.js

定义各种原生dom基础操作方法

2. platforms\web\runtime\modules\index.js

modules 定义了属性更新实现

watcher.run() => componentUpdate() => render() => update() => patch()
    
    

# patch实现

1. patch core\vdom\patch.js

首先进行树级别比较,可能有三种情况:增删改

  • new VNode不存在就删;
  • old VNode 不存在就增;
  • 都存在就执行diff执行更新

  1. patchVnode

比较两个VNode,包括三种类型操作:属性更新、文本更新、子节点更新

具体规则如下:

  • 新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren
  • 如果新节点有子节点而老节点没有子节点,先清空老节点的文本内容,然后为其新增子节点。
  • 当新节点没有子节点而老节点有子节点的时候,则移除该节点的所有子节点。
  • 当新老节点都无子节点的时候,只是文本的替换。

测试,04-vdom.html

 // patchVnode过程分解// 1.div#demo // 2.h1// 3.text// 4.p// 5.textupdateChildren updateChildren 文本相同跳过 updateChildren setTextContent
    
    

3. updateChildren

updateChildren主要作用是用一种较高效的方式比对新旧两个VNode的children得出最小操作补丁。执 行一个双循环是传统方式,vue中针对web场景特点做了特别的算法优化,我们看图说话:

在新老两组VNode节点的左右头尾两侧都有一个变量标记,在遍历过程中这几个变量都会向中间靠拢。 当oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束循环

下面是遍历规则:

  • 首先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode两两交叉比较,共有4种比较 方法。
  • 当 oldStartVnode和newStartVnode 或者 oldEndVnode和newEndVnode 满足sameVnode,直接将该 VNode节点进行patchVnode即可,不需再遍历就完成了一次循环。如下图

如果oldStartVnode与newEndVnode满足sameVnode。说明oldStartVnode已经跑到了oldEndVnode 后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。

如果oldEndVnode与newStartVnode满足sameVnode,说明oldEndVnode跑到了oldStartVnode的前 面,进行patchVnode的同时要将oldEndVnode对应DOM移动到oldStartVnode对应DOM的前面

如果以上情况均不符合,则在old VNode中找与newStartVnode相同的节点,若存在执行 patchVnode,同时将elmToMove移动到oldStartIdx对应的DOM的前面。

当然也有可能newStartVnode在old VNode节点中找不到一致的sameVnode,这个时候会调用 createElm创建一个新的DOM节点。

至此循环结束,但是我们还需要处理剩下的节点。

当结束时oldStartIdx > oldEndIdx,这个时候旧的VNode节点已经遍历完了,但是新的节点还没有。说 明了新的VNode节点实际上比老的VNode节点多,需要将剩下的VNode对应的DOM插入到真实DOM 中,此时调用addVnodes(批量调用createElm接口)

但是,当结束时newStartIdx > newEndIdx时,说明新的VNode节点已经遍历完了,但是老的节点还有 剩余,需要从文档中删 的节点删除。

# 模板编译

模板编译的主要目标是将模板**(template)转换为渲染函数(render)**

template => render()
    
    

# 模板编译必要性

Vue 2.0需要用到VNode描述视图以及各种交互,手写显然不切实际,因此用户只需编写类似HTML代码 的Vue模板,通过编译器将模板转换为可返回VNode的render函数。

# 体验模板编译

带编译器的版本中,可以使用template或el的方式声明模板,06-1-compiler.html

 (function anonymous(){with(this){return _c('div',{attrs:{"id":"demo"}},[_c('h1',[_v("Vue模板编 译")]),_v(" "),_c('p',[_v(_s(foo))]),_v(" "),_c('comp')],1)}})
    
    

输出结果大致如下:

 (function anonymous() {with(this){return _c('div',{attrs:{"id":"demo"}},[_c('h1',[_v("Vue模板编译")]), _v(" "),_c('p',[_v(_s(foo))]), _v(" "),_c('comp')],1)}})
    
    
  • 元素节点使用createElement创建,别名_c _
  • _本文节点使用createTextVNode创建,别名_v
  • 表达式先使用toString格式化,别名_s
  • 其他渲染helpers:src\core\instance\render-helpers\index.js

# 整体流程

1. compileToFunctions

若指定template或el选项,则会执行编译,platforms\web\entry-runtime-with-compiler.js

2. 编译过程

编译分为三步:解析、优化和生成,src\compiler\index.js

测试代码06-1-compiler.html

# 模板编译过程

实现模板编译共有三个阶段:解析、优化和生成

  1. 解析 - parse

解析器将模板解析为抽象语法树,基于AST可以做优化或者代码生成工作。 调试查看得到的AST,**/src/compiler/parser/index.js**,结构如下:

解析器内部分了HTML解析器、文本解析器和过滤器解析器,最主要是HTML解析器

  1. 优化 - optimize

优化器的作用是在AST中找出静态子树并打上标记。静态子树是在AST中永远不变的节点,如纯文本节 点。

标记静态子树的好处:

  • 每次重新渲染,不需要为静态子树创建新节点
  • 虚拟DOM中patch时,可以跳过静态子树

测试代码,06-2-compiler-optimize.html 代码实现,src/compiler/optimizer.js - optimize

  1. 代码生成 - generate
  • 将AST转换成渲染函数中的内容,即代码字符串
  • generate方法生成渲染函数代码,src/compiler/codegen/index.js

生成的code⻓这样

 `_c('div',{attrs:{"id":"demo"}},[ _c('h1',[_v("Vue.js测试")]), _c('p',[_v(_s(foo))])])`
    
    

# 典型指令的实现:v-if、v-for

  1. 解析v-if:parser/index.js

processIf用于处理v-if解析

  1. 代码生成,codegen/index.js

genIfConditions等用于生成条件语句相关代码

生成结果:

 "with(this){return _c('div',{attrs:{"id":"demo"}},[(foo) ? _c('h1',[_v(_s(foo))]) : _c('h1',[_v("no title")]),_v(" "),_c('abc')],1)}"
    
    
  1. 解析v-for:parser/index.js

processFor用于处理v-for指令

解析结果:v-for="item in items" for:'items' alias:'item'

  1. 代码生成,src\compiler\codegen\index.js

genFor用于生成相应代码

生成结果

 "with(this){return _c('div',{attrs:{"id":"demo"}},[_m(0),_v(" "),(foo)?_c('p', [_v(_s(foo))]):_e(),_v(" "),_l((arr),function(s){return _c('b',{key:s},[_v(_s(s))])}),_v(" "),_c('comp')],2)}"
    
    

v-if,v-for这些指令只能在编译器阶段处理,如果我们要在render函数处理条件或循环只能使用if 和for

 Vue.component('comp', {props: ['foo'],render(h) { // 渲染内容跟foo的值挂钩,只能用if语句if (this.foo=='foo') { return h('div', 'foo')}return h('div', 'bar') }})
    
    
 (function anonymous(){with(this){return _c('div',{attrs:{"id":"demo"}},[_m(0),_v(" "),(foo)?_c('p', [_v(_s(foo))]):_e(),_v(" "),_c('comp')],1)}})
    
    

# 组件化机制

# 组件声明:Vue.component()

initAssetRegisters(Vue) src/core/global-api/assets.js

组件注册使用extend方法将配置转换为构造函数并添加到components选项

# 组件实例创建及挂载

观察生成的渲染函数

 "with(this){return _c('div',{attrs:{"id":"demo"}},[ _c('h1',[_v("虚拟DOM")]),_v(" "), _c('p',[_v(_s(foo))]),_v(" "),_c('comp') // 对于组件的处理并无特殊之处],1)}"
    
    

# 整体流程

首先创建的是根实例,首次_render()时,会得到整棵树的VNode结构,其中自定义组件相关的主要有: createComponent() - src/core/vdom/create-component.js

1. 组件vnode创建

createComponent() - src/core/vdom/patch.js

创建组件实例并挂载,vnode转换为dom

2. 整体流程:

new Vue() => $mount() => vm._render() => createElement() => createComponent() => vm._update() => patch() => createElm => createComponent()

# 创建组件VNode

1. _createElement - src\core\vdom\create-element.js

_createElement实际执行VNode创建的函数,由于传入tag是非保留标签,因此判定为自定义组件通过 createComponent去创建

2. createComponent - src/core/vdom/create-component.js

创建组件VNode,保存了上一步处理得到的组件构造函数,props,事件等

# 创建组件实例

根组件执行更新函数时,会递归创建子元素和子组件,入口createElm

1. createEle() core/vdom/patch.js line751

首次执行_update()时,patch()会通过createEle()创建根元素,子元素创建研究从这里开始

2. createComponent core/vdom/patch.js line144

自定义组件创建

// 组件实例创建、挂载if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */ )}if (isDef(vnode.componentInstance)) { // 元素引用指定vnode.elm,元素属性创建等 initComponent(vnode, insertedVnodeQueue) // 插入到父元素 insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true}
    
    

# 总结

Vue源码学习使我们能够深入理解原理,解答很多开发中的疑惑,规避很多潜在的错误,写出更好的代 码。学习大神的代码,能够学习编程思想,设计模式,训练基本功,提升内力。

  • 28
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值