一、vue2源码目录
- compiler:将template编译成 render 函数。对于在线编译,render 在运行时执行,执行时会生成 vnode
- core:核心
- platform:web平台、weex多平台、mpvue小程序端
- server:服务端渲染
- sfc:单文件处理,将.vue文件的template、script、style拆分
- shared:工具、常量
二、compiler 运行时、编译时
- 运行时:new Vue时产生一个对象在内存中维护,包含数据、方法、生命周期等,它们通过 数据与模板的绑定关系 维持vue应用的使用,被维持的绑定关系就是运行时。
- 编译时:是创建对应关系的代码,编译的对象是template,分为离线编译和在线编译:
- 离线编译:开发、打包的过程,将.vue业务代码编译成js render函数的过程。
- 在线编译:上线的代码在运行时编译,new Vue代码在浏览器里跑的过程。
💡 vue和react源码区别
- react做运行时优化,所以react源码复杂
- vue没有太多运行时优化但也很流畅,因为vue做编译时优化
- 所以vue模板必须按它的规则去写,而react就很灵活
compiler编译步骤:
- 如果有render函数,那么已经是编译完成的,返回
- 判断template:
- 如果是string,判断 "#app" 还是 "<div>...</div>",分别进行处理
- 如果是DOM,获取innerHTML
- 如果是离线编译,还要判断编译环境,执行 complieToFunctions 函数
- 然后将template编译成函数。
parser:
compiler 的一个步骤,对模板进行 AST 分析(先分词,再做词法分析)。
- html解析
- 因为匹配是正则匹配,所以字符串越短,匹配效率越高。所以模板应该尽量小。
- 匹配顺序:
- parseEndTag'</'
- doctype
- ie条件注释
- 注释
- parseStartTag '<'
- filter解析
- 分析 v-for 的 key。
- 优化:
- 判断静态节点,纯dom、文本、没有vue指令的是纯静态节点,且子节点均为静态节点。
三、core 核心
- component:模板编译代码
- global-api:文件接口
- instance:实例,处理初始化、状态、数据、生命周期、事件
- observer:数据订阅
- utils:方法
- vdom:虚拟dom,使用虚拟dom的原因是,原生dom有很多无用的属性,占用太多内存
一、observer
observer 是 core 的核心。
- defineReactive
- 核心是创建vue data的监听,通过 Object.defineProperty 方法,为 vue 对象定义 get 和 set 方法。
- get:第一次执行 get 方法将该数据对应的 watcher 绑定到对应的 dep上。此后均为返回值。
- set:如果 newValue === value,则返回;否则设置新值,并更新对应的 dep。
- 区分哪一个 vue data 对应哪一个 dep、watcher:data 本身对应 watcher,dep时在 defineReactive 中作为闭包保存的。
- 对于数组,因为Array.prototype.__proto__ === Object.prototype,所以理论上也是可以监听的。但是对于新创建的数组下标无法监听,所以针对数组新增了 dependArray 方法。
- 同样的,为对象添加新的属性,新增的属性也是无法被监听的。
- 数组重写:
- 原因:数组的操作是很昂贵的。当我给一个数组 unshift(value) 时,所有原来数组下标的值都要向后移动一位,相当于遍历一遍原始数组。在 vue 中,由于使用了 get 和 set 方法,就会触发多次执行,非常浪费资源。
- 具体实现:判断浏览器是否支持 __proto__ 属性(比如Android UC不支持)。
- 如果不支持(选),就找它的原型对象设置到 __proto__ 属性上。
- 然后重写数组方法,包括 push、pop、shift、unshift、splice、sort、reverse等。因为这些方法会造成数组元素索引改变。
- 对push、unshift、splice、reverse等会出现新增元素的方法,将新增元素设置为 observer 对象,然后手动更新 dep。
- 核心是创建vue data的监听,通过 Object.defineProperty 方法,为 vue 对象定义 get 和 set 方法。
-
observer
-
将一个数据修改为可观察数据,主要是为了解决数组和对象新增下标或属性时,新元素不是可观察数据的问题。
-
它会创建一个属性名为 __ob__ 的不可配置、不可枚举的对象,用于表示当前对象已经是响应式对象了。
-
对于嵌套对象,递归调用。
-
- watcher
- watcher 是观察者模式的观察者,观察者都应该有回调函数,watcher 的作用就是,对 render 函数和 vnode之间做连接。而watcher 的回调就是执行render。
- 创建 watcher 对象时,会使 Dep.target = this。然后在 watcher 构造函数中,使 this.value = this[name],这会触发 Object.defineProperty 的 get。在 get 中,将 watcher 添加到 dep中。
- vue1.0 与 vue2.0的区别
- vue1.x:
- 一个指令对应一个 watcher。
- 因为它精确定位,所以不需要 dom diff,所有的更改都位置都记录下来了,但这样的结果是维护成本高性能差。
- vue2.x:
- 一个组件对应一个 watcher。
- 在render函数中,with(this) 的 this就是这个组件的observer对象。由于 render函数 返回了一串长长的 html 字符串,所以需要对它做dom diff 。
- 于此同时,维护的 watcher 少很多。因此,组件的代码写的少点,会对运行时计算的压力降低。
- vue1.x:
- Dep
- dep是观察者模式中的发布者,可以有多个指令订阅它。
- 一般来说,一个数据对应一个 dep ,一个 dep 可以有多个 watcher。
- scheduler
- 调度器,负责批处理功能。
- 将本次更新全部放到一个执行队列中;对于任意数据,按照字段区分,设置等待;将这个执行队列 通过调用 nextTick 延迟执行。所以 vue 数据变化不是立刻执行的。
- nextTick
- 延迟到下一帧执行,核心是 timerFunc 函数, 本质是一个延迟函数,尝试先匹配 微任务 。
- 判断是否支持 promise、mutationObserver、setImmidate,setTimeout 兜底;然后执行回调。
- 小结:
- core是vue代码的核心,而 observer 是 core 的核心。它利用 Object.defineProperty 实现对数据的操作拦截,然后将数据绑定到一个 由 观察者模式 为单元(watcher)组成的数据维护中心(Dep)。
- 对于数组,新增或修改元素可能造成整个数组的元素重新排列,所以对数组进行了重写。所有新增的元素都调用 observer 方法,使其变为一个可观察的对象。
- 对于任何数据,都进行递归操作,使其任意属性变得可以追踪。
二、components
keep-alive,保存的是 vnode 节点,而不是数据。
由于 vnode 节点比描述状态的数据大一些,所以 keep-alive 能够保存的数据大小有限,所以它存在取舍问题,一般舍弃最老的组件。
对于任意组件,无论是否被添加到 keep-alive 缓存列表中,重新访问时,都会把它设置为列表的结尾。
三、use
用于为vue设置插件,它维护一个插件队列,判断是否已存在,如果未存在,执行插件,并且添加到插件队列中。
四、vue运行流程总结
-
编译模板,无论是在线编译,还是离线编译,均会为每一个组件生成一个对应的 render 函数。并且,会根据节点的状态,为它们优化成静态节点 render 函数。
-
defineReactive 函数利用 Object.defineProperty方法, 使 vue data 重写成可控对象。
-
在此方法内,创建一个用于双向数据绑定的、观察者模式中的发布者 dep。
-
在 get 方法中,判断是否为第一次访问数据,第一次访问数据时需要通知 dep 发布。
-
在 set 方法中,判断 newValue === oldValue,需要更新则通知 dep 发布。
-
-
dep 作为 vue data 实现双向数据绑定功能中的发布者。每一个 dep 对应一个数据,维护所有与该数据有关的 watcher。
-
这些 render 函数在某个vue钩子的生命周期时被执行,此时会创建 watcher。
-
watcher 作为 vue data 实现双向数据绑定功能中的观察者,每一个 watcher 对应一个组件。
-
watcher 最主要的工作是维护 数据 与 dom 之间的渲染关系。
-
新创建的 watcher 会调用 get 方法,将触发 Object.defineProperty 的 get 方法,将 watcher 添加到对应的 dep中。
-
watcher 的 get 方法还会尝试调用渲染函数,所以当创建 watcher 后,组件中的数据就已经被渲染了。如果是在线编译,第一次渲染会删除原生 dom ,此后均为 vnode。
-
-
首次渲染完成。。。
-
vue data 在后续更新时不是立刻更新,而是将数据更新的过程添加到一个调度器中,实现批量更新而不是每次数据变化时更新 dom 。由 nextTick 执行回调批量更新。
五、性能优化总结
编译时优化:
- 模板不宜过长:的在对模板进行 compiler 编译时,会使用正则表达式。由于正则表达式匹配的回溯性,过长的模板会造成编译性能问题。
运行时优化:
- 模板不宜过长:compiler 将模板编译成了render函数,在执行 render 函数时,每一个 render函数对应一个 watcher,模板过长会导致 dom diff 计算量大。
- 批处理:更新 vue data 时,不是立刻更新,而是将数据通过调度器组织起来,放在 nextTick 后,在下一个微任务/宏任务时作为回调函数执行。
todo。。。key
六、vue源码原理
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>vue源码分析</title>
</head>
<body>
<div id="app">
<div id='div' v-text="msg"></div>
<input id="input" type="text" v-model="msg">
</div>
<br>
<button id="button">测试批量更新</button>
</body>
<script src="vue/index.js"></script>
<script src="vue/compiler.js"></script>
<script src="vue/dep.js"></script>
<script src="vue/observer.js"></script>
<script src="vue/schedule.js"></script>
<script src="vue/watcher.js"></script>
<script>
const vue = new Vue({
el: 'app',
data: {
msg: 1,
},
});
document.getElementById('button').onclick = function (){
for (let i = 0; i < 100; i++) {
vue.msg++;
}
};
</script>
</html>
index.js
// 是一个构造函数,返回vue实例
function Vue({ data, el }){
// 数据
this.data = data;
// 双向数据
defineReactive(this, this.data);
// 编译
compiler(this, document.getElementById(el));
}
compiler.js
// 编译模板
// ⚠️源码中用到了大量的正则,这里仅仅做案例,ast分析也忽略了
function compiler(vm, el){
const div = el.querySelector('#div');
const input = el.querySelector('#input');
// 新创建dom,是可以绑定很多与vue有关的值,这里模拟的是 createElement
const _div = document.createElement('div');
const _input = document.createElement('input');
const parentElement = div.parentElement;
parentElement.removeChild(div);
parentElement.removeChild(input);
// 替换成新创建的dom
parentElement.appendChild(_div);
parentElement.appendChild(_input);
// 'msg'
const msg = div.getAttribute('v-text');
// div 的 render,正常的render函数是 width(this){xxx},xxx是ast分析后的结构,并且通过不同的渲染函数渲染
const divRender = function (innerHTML){
_div.innerHTML = innerHTML;
};
const inputRender = function (value){
_input.value = value;
};
// watcher
new Watcher(vm, msg, divRender);
const inputWatcher = new Watcher(vm, msg, inputRender);
_input.oninput = function (){
// input中输入的值
const value = this.value;
// 触发 Object.defineProperty 的 set
inputWatcher.vm[inputWatcher.key] = value;
};
}
dep.js
let depId = 0;
// 观察者模式的发布者
function Dep(){
// 区分是哪个发布者
this.depId = depId++;
// 维护关心某个数据的所有watcher,这里用set可以去重
this.watcherList = new Set();
}
// 当且仅当 创建任意 watcher 时,会指向正在被创建的 watcher
Dep.target = null;
// 添加 watcher
Dep.prototype.add = function (watcher){
if (this.watcherList.has(watcher)) {
return;
}
watcher.depId = this.depId;
this.watcherList.add(watcher);
};
// 发布
Dep.prototype.notify = function (){
for (let o of this.watcherList) {
// 通知到每一个订阅了该数据的 watcher
o.update(this.depId);
}
};
observer.js
function defineReactive(vm, data){
Reflect.ownKeys(data).forEach(key => {
let value = data[key];
// 为每一个key创建新的发布者
const dep = new Dep();
Object.defineProperty(vm, key, {
get(){
// 第一次执行get,只有在创建 watcher 时 === true
if (!!Dep.target) {
// 添加一个 watcher 到 发布者 dep
dep.add(Dep.target);
}
return value;
},
set(v){
// 相同则什么都不做
if (v === value) {
return;
}
// 先设置值
value = v;
// 再更新
dep.notify();
},
});
});
}
schedule.js
// 调度器是为了批量更新和同步调度,这里只写了nextTick和批量更新
const taskSet = new Map();
// 添加任务,然后调用nextTick
function addTask(depId, watcher){
if (!taskSet.has(depId)) {
taskSet.set(depId, new Set());
}
const set = taskSet.get(depId);
set.add(watcher);
nextTick(depId);
}
function nextTick(depId){
// nextTick优先尝试使用微任务,然后才是宏任务,这里做了简化
setTimeout(() => {
const set = taskSet.get(depId);
for (let watcher of set) {
// 执行每一个 watcher 的渲染函数
watcher.render();
}
// 执行完后清空任务
set.clear();
});
}
watcher.js
function Watcher(vm, key, render){
// 表示当前正在被创建的 watcher,源码中是一个 watcher 的栈
Dep.target = this;
this.vm = vm;
this.key = key;
// 渲染函数,由compiler生成
this.render = function (){
render(this.value);
};
this.value = this.get();
Dep.target = null;
// 更新这个 watcher 所在的 dep
this.update(this.depId);
}
// ⚠️源码中,get含义不同,get直接更新了视图
Watcher.prototype.get = function (){
// 会触发 defineReactive 中的 get,从而添加这个 watcher 到 dep
return this.vm[this.key];
};
Watcher.prototype.update = function (depId){
// 先得到新的值
this.value = this.get();
// 再添加到批量更新任务列表
addTask(depId, this);
};
更多推荐
vue-vue2.0源码分析
发布评论