前言
多年前刚转前端的时候,对频繁的拼接页面元素深恶痛绝,当时是通过封装字符串模版来处理页面的。之后又陆续发现,数据变化后需要频繁的修改dom节点来操作页面,便不得不自己写很多更新的代码,直到出现了vue和react、就转向了框架开发。
手写js原生的年代
如果每一次页面元素都需要手动拼接,工作量会很巨大,因此当时便是通过封装字符串模板来进行数据 + html渲染的。
如下代码:
var htmlTemplate = '<div clssName="demo">' +
'<div className="count">${count}</div>' +
'</div>';
function createHtml(data) {
return htmlTemplate.replace(/\$\{[a-z]+\}/g, function(str) {
// 塞入data数据,产出最终渲染的html成果
var key = str.substring(2, str.length - 1);
return data[key];
})
}
通过上面的方法可以实现封装好一个个的html模版,在实际拼接页面的时候只需要通过数据进行自定义的数据代码块进行替换就好了。
数据渲染的问题解决了,现在我们又遇到了另一个棘手的问题
<div>
<button onclick="increment">计数</button>
<div clssName="demo">
<!-- 这边对应到之前的 ${count} -->
<div className="count">123</div>
</div>
</div>
<script>
var data = {
count: 0
}
function increment() {
// 更新数据
data.count += 1;
// 更新页面
document.querySelector(".count").innerText = data.count;
}
function deCount() {
// 更新数据
data.count -= 1;
// 更新页面
document.querySelector(".count").innerText = data.count;
}
function addResult(addNumber) {
// 更新数据
data.count += addNumber;
// 更新页面
document.querySelector(".count").innerText = data.count;
}
</script>
看上述代码,你会发现页面中有一个按钮,点击会增加count,那么只需要调用increment同时更新数据和页面就行了,但是现实业务场景极其复杂,这边就列举了另外两种情况,如果在页面交互逻辑过程中出现了减少count或者其他增量逻辑,那么就需要多次调用页面渲染了,当然把减少跟增量封装成函数,在函数里加入更新页面逻辑也行,但这样要写很多额外的代码,coding效率并不是很高。
直到出现了defineProperty
// 为了提高开发效率,减少coding,当时我们的选择是自行使用defineProperty来监听数据变化,这样就可以统一处理页面了
function createDataObserver(data) {
// 是为了解决栈溢出,需要用另一个对象来缓存数据
var _data = Object.assign({}, data);
Object.defineProperty(data, 'count' , {
get() {
return _data['count'];
},
set(value) {
_data['count'] = value;
// 这样不管是哪里修改到count,只要这里就可以统一监听去更新页面了
document.querySelector(".count").innerText = value;
}
})
}
好了,上面大概就是当年还没出框架的做法,直到梦最开始的地方,vue和react相继出现(其实还有angular…但不得不说用起来成本比较大、因此只使用了最初的angular1.0版本)
言归正传、让我们开始vue3源码解析吧
和vue1、vue2最大的不同点在于vue3使用proxy:
- Object.defineProperty只能劫持对象的属性,而Proxy是直接代理对象。Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性, 如果属性值也是对象,则需要深度遍历。而 Proxy 直接代理对象,不需要遍历操作 。
- Object.defineProperty对新增属性需要手动进行Observe。 由于Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持。 也正是因为这个原因,使用vue给 data 中的数组或对象新增属性时,需要使用 vm.$set 才能保证新增的属性也是响应式的。
vue3核心代码
先看一下vue3代码库的包结构
数据监听及代理和相关effect调用在包reactivity中,关于运行时的核心代码、dom解析在runtime-core、runtime-dom里面。compiler-前缀是在遍历template及渲染最终页面dom元素过程中存放相关代码的包。
在创建组件实例化的时候,会去对data进行处理,相关函数在runtime-core中的componentOptions文件,调用applyOptions函数时会通过reactive创建和代理data:
// runtime-core -> src -> componentOptions.ts
if (dataOptions) {
if (__DEV__ && !isFunction(dataOptions)) {
warn(
`The data option must be a function. ` +
`Plain object usage is no longer supported.`,
)
}
const data = dataOptions.call(publicThis, publicThis)
if (__DEV__ && isPromise(data)) {
warn(
`data() returned a Promise - note data() cannot be async; If you ` +
`intend to perform data fetching before component renders, use ` +
`async setup() + <Suspense>.`,
)
}
if (!isObject(data)) {
__DEV__ && warn(`data() should return an object.`)
} else {
instance.data = reactive(data)
if (__DEV__) {
for (const key in data) {
checkDuplicateProperties!(OptionTypes.DATA, key)
// expose data on ctx during dev
if (!isReservedPrefix(key[0])) {
Object.defineProperty(ctx, key, {
configurable: true,
enumerable: true,
get: () => data[key],
set: NOOP,
})
}
}
}
}
}
// reactivity -> src -> reactive.ts
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap,
)
}
可以看到vue3最新版本已经是利用Proxy进行数据代理了:
ref的底层实现也是复用了reactive