阅读本文能够帮助你什么?
- 在学习vue源码的时候发现组件化过程很绕?
- 在响应式过程中
Observer
、Dep
、Watcher
三大对象傻傻分不清? - 搞不清楚对象、数组依赖收集、派发更新的流程?
dep
、watcher
互调造成混乱? - 想直接上代码学源码十分枯燥、乏味,而且缺乏大体流程概念?
- 像我一样,有段时间没看vue源码好像有点遗忘?但是想快速回顾却无从下手?
本文主要讲解组件化、响应式的核心流程。或许上述的这些问题,在这里你都能找到答案
一、组件化流程
1. 整个new Vue
阶段做了什么?
- 执行init操作。包括且不限制
initLifecycle
、initState
等 - 执行mount。进行元素挂载
- compiler步骤在runtime-only版本中没有。
- compiler步骤对template属性进行编译,生成render函数。
- 一般在项目中是在
.vue
文件开发,通过vue-loader处理生成render函数。
- 执行render。生成vnode
- render例子,如下
<div id="app">{{ message }}</div> 复制代码
- 对应手写的render函数
render (h) { return h('div', { attrs: { id: 'app' }, }, this.message) } 复制代码
- patch。新旧vnode经过diff后,渲染到真实dom上
2. 普通dom元素如何渲染到页面?
- 执行
$mount
。- 实际执行
mountComponent
- 这里会实例化一个Watcher
- Watcher中会执行
get
方法,触发updateComponent
- 实际执行
- 执行
updateComponent
。执行vm._update(vm._render(), hydrating)
- 执行
vm.render()
。- render其实调用
createElment
(h
函数) - 根据tag的不同,生成组件、原生VNode并返回
- render其实调用
- 执行
vm.update()
。createElm()
到createChildren()
递归调用 - 将VNode转化为真实的dom,并且最终渲染到页面
3. 组件如何渲染到页面?
-
这里以如下代码案例讲解更加清晰~没错,就是这么熟悉!就是一个初始化的Vue项目
// mian.js import Vue from 'vue' import App from './App.vue' new Vue({ render: h => h(App), }).$mount('#app') 复制代码
// App.vue <div id="app"> <p>{{ msg }}</p> </div> <script> export default { name: 'App', data () { return { msg: 'hello world' } } } </script> 复制代码
-
主要讲解组件跟普通元素的不同之处,主要有2点:
- 如何生成VNode——创建组件VNode
createComponent
- (注意:这一步对应上图render流程的紫色块的展开!!!)
- 区分普通元素VNode
- 普通VNode:tag是html的保留标签,如
tag: 'div'
- 组件VNode:tag是以
vue-component
开头,如tag: 'vue-component-1-App'
- 普通VNode:tag是html的保留标签,如
-
如何patch——组件new Vue到patch流程
createComponent
- (注意:这一步对应上图patch流程的紫色块的展开!!!)
- $vnode:占位符vnode。最终渲染vnode挂载的地方。所有的组件通过递归调用createComponent直至不再存在组件VNode,最终都会转化成普通的dom。
{ tag: 'vue-component-1-App', componentInstance: {组件实例}, componentOptions: {Ctor, ..., } } 复制代码
- _vnode:渲染vnode。
{ tag: 'div', { "attrs": { "id": "app" } }, // 对应占位符vnode: $vnode parent: { tag: 'vue-component-1-App', componentInstance: {组件实例}, componentOptions: {Ctor, ..., } }, children: [ // 对应p标签 { tag: 'p', // 对应p标签内的文本节点{{ msg }} children: [{ text: 'hello world' }] }, { // 如果还有组件VNode其实也是一样的 tag: 'vue-component-2-xxx' } ] } 复制代码
- 如何生成VNode——创建组件VNode
4. Vue组件化简化流程
- 相信你看完细粒度的Vue组件化过程可能已经晕头转向了,这里会用一个简化版的流程图进行回顾,加深理解
二、响应式流程
- 案例代码
// 案例
export default {
name: 'App',
data () {
return {
msg: 'hello world',
arr = [1, 2, 3]
}
}
}
复制代码
1. 依赖收集
-
这里会从Observer、Dep、Watcher三个对象进行讲解,分object、array两种依赖收集方式
- 三个核心对象:
Observer
(蓝)、Dep
(绿)、Watcher
(紫)
- 依赖收集准备阶段——Observer、Dep的实例化
- 注意对象、根数组的不同处理方式。这里以核心代码+图讲解
// 以下是initData调用的方法讲解,排列遵循调用顺序 function observe (value, asRootData) { if (!isObject(value)) return // 非对象则不处理 // 实例化Observer对象 var ob; ob = new Observer(value); return ob } function Observer (value) { this.value = value; // 保存当前的data this.dep = new Dep(); // 实例化dep,数组进行依赖收集的dep(对应案例中的arr) def(value, '__ob__', this); if (Array.isArray(value)) { if (hasProto) { // 这里会改写数组原型。__proto__指向重写数组方法的对象 protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); } } // 遍历数组元素,执行对每一项调用observe,也就是说数组中有对象会转成响应式对象 Observer.prototype.observeArray = function observeArray (items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } } // 遍历对象的全部属性,调用defineReactive Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); // 如案例代码,这里的 keys = ['msg', 'arr'] for (var i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]); } } function defineReactive (obj, key, val) { // 产生一个闭包dep var dep = new Dep(); // 如果val是object类型,递归调用observe,案例代码中的arr会走这个逻辑 var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { get: function reactiveGetter () { // 求value的值 var value = getter ? getter.call(obj) : val; if (Dep.target) { // Dep.target就是当前的Watcher // 这里是闭包dep dep.depend(); if (childOb) { // 案例代码中arr会走到这个逻辑 childOb.dep.depend(); // 这里是Observer里的dep,数组arr在此依赖收集 if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { // 下文派发更新里进行讲解 } }); } 复制代码
- 依赖收集触发阶段——Wather实例化、访问数据、触发依赖收集
- 三个核心对象:
2. 派发更新
-
派发更新区分对象属性、数组方法进行讲解
-
这里可以先想一下,以下操作会发生什么?
this.msg = 'new val'
this.arr.push(4)
-
是的,毫无疑问都会先触发他们之中的
get
,那再触发什么呢?我们接下来看- 对象属性修改触发set,派发更新。
this.msg = 'new val'
... Object.defineProperty (obj, key, { get () {...}, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val; // 判断新值相比旧值是否已经改变 if (newVal === value || (newVal !== newVal && value !== value)) { return } // 如果新值是引用类型,则将其转化为响应式 childOb = !shallow && observe(newVal); // 这里通知dep的所有watcher进行更新 dep.notify(); } } ... 复制代码
- 数组调用方法。
this.arr.push(4)
// 数组方法改写是在 Observer 方法中 function Observer () { if (hasProto) { // 用案例讲解,也就是this.arr.__proto__ = arrayMethods protoAugment(value, arrayMethods); } } // 以下是数组方法重写的实现 var arrayProto = Array.prototype; // 保存真实数组的原型 var arrayMethods = Object.create(arrayProto); // 以真数组为原型创建对象 // 可以看成:arrayMethods.__proto__ = Array.prototype var methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; // 一个装饰器模型,重写7个数组方法 methodsToPatch.forEach(function (method) { // 保存原生的数组方法 var original = arrayProto[method]; // 劫持arrayMethods对象中的数组方法 def(arrayMethods, method, function mutator () { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; var result = original.apply(this, args); var ob = this.__ob__; // 当我门调用this.arr.push(),这里就能到数组对象的ob实例 var inserted; switch (method) { case 'push': case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); break } if (inserted) { ob.observeArray(inserted); } // 由于数组对象在new Observer中实例化了一个dep,这里就能在ob实例中拿到dep属性 ob.dep.notify(); return result }); }) 复制代码
- 对象属性修改触发set,派发更新。
-
整个new Vue阶段、到依赖收集、派发更新的全部流程就到这里结束了。可以纵观流程图看出,Vue应用就是一个个Vue组件组成的,虽然整个组件化、响应式流程很多,但核心的路径一旦走通,你就会恍然大悟。
-
总体来说,Vue的源码其实是比较好上手的。整体代码流程非常的清晰,想要深入某一块逻辑可以结合流程图去仔细去研究,相信你一定会有所收获。
写这篇文章也算是自己的一个知识沉淀吧,毕竟很早之前就学习过Vue的源码了,但是也一直没做笔记。现在回顾一下,发现很多都有点忘了,但是缺乏一个快速记忆、回顾的笔记。如果要直接硬磕源码重新记忆,还是比较费时费力的~作为知识分享,希望可以帮助到想学习源码,想要进阶的你,大家彼此共勉,一同进步!
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END