深入 Vue 源码
约 964 字大约 3 分钟
Vue源码响应式虚拟 DOM
2024-08-13
Vue 3 响应式原理
核心概念
Vue 3 的响应式系统基于 ES6 Proxy 实现。
// 简单响应式实现
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key);
return Reflect.get(target, key);
},
set(target, key, value) {
Reflect.set(target, key, value);
trigger(target, key);
return true;
},
});
}effect 与 track
// effect 栈
const effectStack = [];
// 创建 effect
function effect(fn, options = {}) {
const effect_ = function(...args) {
try {
effectStack.push(effect_);
fn(...args);
} finally {
effectStack.pop();
}
};
effect_.options = options;
effect_.fn = fn;
effect_();
return effect_;
}
// 依赖收集
function track(target, key) {
const effect_ = effectStack[effectStack.length - 1];
if (effect_) {
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(effect_);
}
}
// 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
effects?.forEach(effect_ => {
if (effect_.options.scheduler) {
effect_.options.scheduler(effect_);
} else {
effect_();
}
});
}ref 与 computed
// ref 实现
class RefImpl {
constructor(value) {
this._value = value;
this.dep = new Set();
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal) {
this._value = newVal;
triggerRefValue(this);
}
}
function ref(value) {
return new RefImpl(value);
}
// computed 实现
class ComputedRefImpl {
constructor(getter) {
this._getter = getter;
this._value = undefined;
this.dep = new Set();
}
get value() {
if (!this._dirty) {
return this._value;
}
this._dirty = false;
this._value = this._getter();
return this._value;
}
}
function computed(getter) {
return new ComputedRefImpl(getter);
}Vue Router 原理
路由模式
// Hash 模式
class HashHistory {
constructor(router) {
this.router = router;
this.current = window.location.hash.slice(1) || '/';
}
setupListener() {
window.addEventListener('hashchange', () => {
this.current = window.location.hash.slice(1);
this.router.updateRoute();
});
}
push(location) {
window.location.hash = location;
}
}
// History 模式
class History {
constructor(router) {
this.router = router;
this.current = window.location.pathname;
}
setupListener() {
window.addEventListener('popstate', () => {
this.current = window.location.pathname;
this.router.updateRoute();
});
}
push(location) {
window.history.pushState(null, '', location);
this.current = location;
this.router.updateRoute();
}
}路由匹配
class VueRouter {
constructor(options) {
this.routes = options.routes;
this.matcher = createMatcher(this.routes);
}
match(location) {
return this.matcher.match(location);
}
init(app) {
const history = this.mode === 'hash' ? new HashHistory(this) : new History(this);
this.history = history;
history.setupListener();
history.transitionTo(window.location.pathname);
}
}
function createMatcher(routes) {
const { match, addRoutes } = createRouteMap(routes);
return {
match,
addRoutes,
};
}虚拟 DOM
核心概念
虚拟 DOM 是用 JS 对象描述真实 DOM 的轻量副本。
// 创建 vnode
function h(tag, props, children) {
return {
tag,
props,
children,
};
}
// 简化实现
const vnode = {
tag: 'div',
props: { class: 'container', onClick: () => {} },
children: [
{ tag: 'span', props: {}, children: 'Hello' },
],
};patch 过程
function patch(oldVnode, newVnode) {
// 不同标签,直接替换
if (oldVnode.tag !== newVnode.tag) {
return replaceVnode(oldVnode, newVnode);
}
// 相同标签,比较 props
patchProps(oldVnode.props, newVnode.props);
// 比较 children
patchChildren(oldVnode.children, newVnode.children);
}
function patchChildren(oldChildren, newChildren) {
// 双端对比算法优化
// 1. 从两端向中间比较
// 2. 复用已有节点
// 3. 移动而非销毁重建
}key 的作用
<!-- 有 key 时,Vue 可以精确追踪节点 -->
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
<!-- 无 key 时,可能导致状态错乱 -->模板编译
编译流程
模板字符串 → 解析器 (Parser) → AST → 优化器 (Optimizer) → 代码生成器 (Codegen) → render 函数Parser 实现
function parse(template) {
const stack = [];
const result = [];
while (template.length) {
// 处理开始标签
if (template.startsWith('<')) {
const tag = template.match(/^<(\w+)/)[1];
const props = parseProps(template);
stack.push({ tag, props, children: [] });
template = template.replace(/^<[^>]+>/, '');
}
// 处理文本
const text = template.match(/^[^<]+/)?.[0];
if (text) {
if (stack.length) {
stack[stack.length - 1].children.push({ type: 'text', text });
}
template = template.slice(text.length);
}
// 处理结束标签
if (template.startsWith('</')) {
const node = stack.pop();
if (stack.length) {
stack[stack.length - 1].children.push(node);
} else {
result.push(node);
}
template = template.replace(/^<\/[^>]+>/, '');
}
}
return result[0];
}Codegen 示例
function generate(ast) {
return `with(this) { return ${genNode(ast)} }`;
}
function genNode(node) {
switch (node.type) {
case 'element':
return `h('${node.tag}', ${genProps(node.props)}, [${node.children.map(genNode).join(', ')}])`;
case 'text':
return `\`${node.text}\``;
case 'interpolation':
return `${node.expression}`;
}
}Vuex 原理
核心结构
class Store {
constructor(options) {
this.state = reactive(options.state || {});
this._mutations = {};
this._actions = {};
this._getters = {};
// 注册 mutations
Object.keys(options.mutations).forEach(key => {
this._mutations[key] = (payload) => {
options.mutations[key](this.state, payload);
};
});
// 注册 actions
Object.keys(options.actions).forEach(key => {
this._actions[key] = (payload) => {
options.actions[key]({
commit: this.commit.bind(this),
state: this.state,
}, payload);
};
});
}
commit(type, payload) {
this._mutations[type](payload);
}
dispatch(type, payload) {
return this._actions[type](payload);
}
}手写简易 Vuex
// 实现 install 方法
let Vue;
function install(_Vue) {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
if (this.$options.store) {
Vue.prototype.$store = this.$options.store;
}
},
});
}
// 创建 store
function createStore(options) {
return new Store(options);
}
export default { install, createStore };Diff 算法
双端 Diff
Vue 2 和 Vue 3 都使用快速 Diff 算法。
旧: [A, B, C, D]
新: [A, B, E, C]
1. A vs A → 匹配
2. B vs B → 匹配
3. C vs E → 不匹配
4. D vs C → 不匹配
5. 在 C 前插入 E
6. 删除 D动态节点复用
// 使用 key 标记动态节点
<div v-for="item in list" :key="item.id">
{{ item.content }}
</div>
// 无 key 时,Vue 可能错误复用节点导致状态问题