30 秒速记
- 响应式关联数据与工作,模板表达界面意图
- 渲染负责把结果应用到宿主
- 依赖收集期间记录读过的字段
- 数据变化后去重调度,避免无差别全量刷新
- 简化实现逐步补齐新增属性、数组与清理
实现一个小型数据驱动框架,可以把责任分成监听变化、理解模板和更新界面。 模板里的表达式读取数据时建立依赖,数据写入后通知相应更新函数,再把结果反映到节点。这样不用每次改值都手动找页面位置。入门实现先把最小链路跑通,再验证数组、嵌套、条件依赖和卸载,否则很容易出现演示可用、稍复杂场景就漏更新的问题。
版本校准: 本文出现 Object.defineProperty、Dep、Watcher 和双端 Diff 时,主要描述 Vue 2 实现;Vue 3 使用 Proxy、effect 与新的渲染器路径。Vue 2 已于 2023-12-31 结束维护,新项目应以 Vue 3 为基线,旧项目参考 Vue 2 EOL 官方说明 制定迁移与安全策略。
# 理解Vue的设计思想

-
MVVM框架的三要素:数据响应式、模板引擎及其渲染
-
数据响应式:监听数据变化并在视图中更新
- Object.defineProperty()
- Proxy
-
模版引擎:提供描述视图的模版语法
- 插值:
{{}} - 指令:
v-bind,v-on,v-model,v-for,v-if
- 插值:
-
渲染:如何将模板转换为html
模板 => vdom => dom
# 数据响应式原理
数据变更能够响应在视图中,就是数据响应式。vue2中利用
Object.defineProperty()实现变更检 测。

简单实现
// 数据响应式:
// Object.defineProperty()
function defineReactive(obj, key, val) {
// val可能还是对象,此时我们需要递归
observe(val)
// 参数3是描述对象
Object.defineProperty(obj, key, {
get() {
console.log('get', key);
return val
},
set(newVal) {
if (newVal !== val) {
console.log('set', key);
// 防止newVal是对象,提前做一次observe
observe(newVal)
val = newVal
}
}
})
}
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return
}
// 遍历
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}
// 对于新加入属性,需要单独处理他的响应式
function set(obj, key, val) {
defineReactive(obj, key, val)
}
const obj = { foo: 'foo', bar: 'bar', baz: { a: 1 } }
observe(obj)
// defineReactive(obj, 'foo', 'foo')
// obj.foo
// obj.foo = 'fooooooooo'
// obj.bar
// obj.bar = 'barrrrrrrr'
// obj.baz.a = '10'
// obj.baz = {a: 10}
// obj.baz.a = 100
// 新添加一些属性
// obj.dong = 'dong' // no ok
set(obj, 'dong', 'dong')
obj.dong
// 前面的方法对于数组是不支持
// 思路:拦截数组7个变更方法push、pop。。。,扩展他们,使他们在变更数据的同时
// 额外的执行一个通知更新的任务
defineProperty()不支持数组
解决数组数据的响应化
# Vue中的数据响应化
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<h3>KVue</h3>
<p>{{counter}}</p>
<p k-text="counter"></p>
<p k-html="desc"></p>
</div>
<script src="kvue.js"></script>
<script>
const app = new KVue({
el:'#app',
data: {
counter: 1,
desc: '<span style="color:red">kvue可还行</span>'
}
})
setInterval(() => {
app.counter++
}, 1000);
</script>
</body>
</html>
# 原理分析
new Vue()首先执行初始化,对data执行响应化处理,这个过程发生在Observer中- 同时对模板执行编译,找到其中动态绑定的数据,从
data中获取并初始化视图,这个过程发生在Compile中 - 同时定义一个更新函数和
Watcher,将来对应数据变化时Watcher会调用更新函数 - 由于
data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个Watcher - 将来
data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

# 涉及类型介绍
KVue:框架构造函数Observer:执行数据响应化(分辨数据是对象还是数组)Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)Watcher:执行更新函数(更新dom)Dep:管理多个Watcher,批量更新
# 实现Vue
# 框架构造函数:执行初始化
执行初始化,对data执行响应化处理,kvue.js
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return
}
// 响应式
new Observer(obj)
}
function defineReactive(obj, key, val) {}
class KVue {
constructor(options) {
this.$options = options;
this.$data = options.data;
observe(this.$data)
}
}
class Observer {
constructor(value) {
this.value = value this.walk(value);
}
walk(obj) {
Object.keys(obj).forEach(key = >{
defineReactive(obj, key, obj[key])
})
}
}
为
$data做代理
class KVue {
constructor(options) {
// 。。。
proxy(this, '$data')
}
}
function proxy(vm) {
Object.keys(vm.$data).forEach(key = >{
Object.defineProperty(vm, key, {
get() {
return vm.$data[key];
},
set(newVal) {
vm.$data[key] = newVal;
}
});
})
}
# 编译 Compile
编译模板中vue模板特殊语法,初始化视图、更新视图

1. 初始化视图
根据节点类型编译
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
if (this.$el) {
this.compile(this.$el);
}
}
console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
console.log("编译插值文本" + node.textContent);
}
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
});
}
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
编译插值
