响应式原理
Vue内部使用了Objec.definedProperty()
来实现数据响应式,通过这个函数可以监听到set
和get
事件。
function defineReactive(obj, key, val) {
observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get value');
return val;
},
set: function reactiveSetter(newVal) {
console.log('change value');
val = newVal
}
})
}
function observe(obj) {
if(!obj || typeof obj !== 'object') {
return;
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
var data = { name: 'kiki' }
observe(data);
let name = data.name;
data.name = 'kkk'
以上代码简单的实现了如何监听数据set
和get
的事件,但是仅仅如此是不够的,因为自定义的函数一开始是不会执行的。只有先执行了依赖收集,才能在属性更新的时候派发更新,所以接下来我们需要先触发依赖收集。
<div>{{name}}</div>
在解析如上模版代码时,遇到{{name}}
就会进行依赖收集。
接下来我们先来实现一个Dep
类,用来解藕属性的依赖收集和派发更新操作。
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => {
sub.update();
})
}
}
Dep.target = null;
以上的代码实现很简单,到哪需要依赖收集的时候调用addSub
,当需要派发更新的时候调用notify
。
接下来我们简单的了解下Vue组件挂载是添加响应式的过程。在组件挂载时,会先对所有需要的属性调用Object.defineProperty()
,然后实例化Watcher
,传入组件更新的回调。在实例化过程中,会对模版中的属性进行求值,触发依赖收集。
因为这一小节主要目的是学习响应式原理的细节,所以接下来的代码会简略的表达触发依赖收集时的操作。
class Watcher {
constructor(obj, key, cb) {
Dep.target = this;
this.cb = cb;
this.obj = obj;
this.key = key;
this.value = obj[key];
Dep.target = null;
}
update() {
this.value = this.obj[this.key];
this.cb(this.value);
}
}
以上就是Watcher
的简单实现,在执行构造函数的时候将Dep.target
指向自身,从而使得收集到了对应的Watcher
,在派发更新的时候取出对应的Watcher
然后执行update
函数。
接下来,需要对defineReactive
函数进行改造,在自定义函数中添加依赖收集和派发更新相关的代码。
function defineReactive(obj, key, val) {
observe(val);
let dp = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get value');
if(Dep.target) {
dp.addSub(Dep.target);
}
return val;
},
set: function reactiveSetter(val) {
console.log('change value');
val = newVal;
dp.notify();
}
})
}
以上所有代码实现了一个简易的数据响应式,核心思路就是手动触发一次属性的getter
来实现依赖的收集。
Object.defineProperty的缺陷
如果通过下标方式修改数组数据或者给对象新增属性并不会触发组件的重新渲染,因为Object.defineProperty
不能拦截到这些操作,更准确的来说,对于数组来说,大部分操作都是拦截不到的,这是Vue内部通过重写函数的方式来解决了这个问题。
对于第一个问题,Vue提供了一个API解决
export function set (target: Array<any> | Object, key: any, val: any): any {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
if(key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
const ob = (target: any).__ob__;
if(!ob) {
target[key] = val;
return val;
}
defineReactive(ob.value, key, val);
ob.dep.notify();
return val
}
对于数组而言,Vue内部充血了以下函数实现派发更新
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
methodsToPatch.forEach(function (method) {
const original = arrayProto[method];
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args);
const ob = this.__ob__;
let inserted;
switch(method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
if(inserted) {
ob.observerArray(inserted);
}
ob.dep.notify();
return result;
})
})
编译过程
首先直接把模版丢在浏览器中肯定是不能运行的,模版只是为了方便开发者进行开发。Vue会通过编译器将模版通过几个阶段最终编译位render
函数,然后通过render
函数生成 Virtual DOM最终映射为真实DOM。
接下来我们就来学习下这个编译的过程,了解这个过程中大概发生了什么,这个过程其中又分为三个阶段,分别为:
- 将模版解析为AST
- 优化AST
- 将AST转换为
render
函数 在第一个阶段中,最主要的事情还是通过各种各样的正则表达式去匹配模版中的内容,然后将内容提取出来各种逻辑操作,接下来会生成一个最基本的AST对象
{
// 类型
type: 1,
// 标签
tag,
// 属性列表
attrsList: attrs,
// 属性映射
attrsMap: makeAttrsMap(attrs),
// 父节点
parent,
// 子节点
children: []
}
然后根据这个最基本的AST对象中的属性,进一步扩展AST。
当然在这一阶段中,还会进行其他的一些判断逻辑。比如说对比前后开闭标签是否一致,判断根组件是否存在一个,判断是否符合HTML5规范等问题。
接下来就是优化AST的阶段。在当前版本下,Vue进行的优化内容其实还是不多的。只是对节点进行了静态内容提取,也就是将永远不会改变的节点提取了出来,实现复用Virtual DOM,跳过对比算法的功能。
最后一个阶段就是通过AST生成render
函数了,其实这一阶段虽然分支有很多,但是最主要的目的就是整个AST,根据不同的条件生成不同的代码罢了。
NextTick原理分析
nextTick
可以让我们在下次DOM更新循环结束之后执行延迟回调,用于获得更新后的DOM。
在Vue2.4之前都是使用的microtasks,但是microtasks的优先级过高,在某些情况下可能会出现比事件冒泡更快的情况,但如果使用macrotasks又可能会出现渲染的性能问题。所以在新版本中,会默认使用microtasks,但在特殊情况下会使用macrotasks,比如v-on。
对于实现macrotasks,会先判断是否使用setImmediate
,不能的话降级为MessageChannel
,以上都不行的话就使用setTimeout
if (typeof setImmediate !== 'undefiend' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
}
else if (
typeof MessageChannel !== 'undefiend'
&& (isNative(MessageChannel))
|| MessageChannel.toString() === '[object MessageChannelConstructor]'
) {
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = flushCallbacks;
macroTimerFunc = () => {
port.postMessage(1);
}
}
else {
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0);
}
}