Vue3 核心模块源码解析(上)
Posted 程序员啊楠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue3 核心模块源码解析(上)相关的知识,希望对你有一定的参考价值。
Vue3相比大家也都有所了解,即使暂时没有使用上,但肯定也学习过!Vue3是使用TS进行重写,采用了MonoRepo的管理方式进行管理,本篇文章我们一起来看看 Vue3的使用,与Vue2有什么区别,以及我们该如何优雅的去使用?【中】篇会从源码的角度去学习,【下】篇主要是讲解Vue3的高频面试题,开始正文吧!!!
文章目录
一、Vue2 与 Vue3响应式对比
Vue2 与 Vue3 最显著的差别就是响应式的差别,那么是什么原因导致 Vue3 的双向绑定原理采用了Proxy?我们下面来由浅入深的去了解一下。
1. Vue2 的 Object.defineProperty
基础使用:
const initData = value: 1 ;
const data = ;
Object.keys(initData).forEach(key =>
Object.defineProperty(data, key,
get()
console.log('访问了', key);
,
set(v)
console.log('修改了', key);
data[key] = v;
)
)
data.value;
data.value = 2;
data;
initData.value2 = 2;
data.value2;
以上就是最基础的使用;
但是我们一起来看一下,下面几个问题会输出什么?
- 直接访问 data.value => 访问了 value
- 改变 data.value => 修改了 value
- 直接输出 data => 空对象:
- 给 initData 添加一个新值 => 输出新值结果:2
- data.value2 又会输出什么? => undefined
总结一下 Vue2 响应式弊端:给对象加属性和删除属性,响应式会检测不到。通常我们是使用 Vue.set( ) 来解决,那么面试官问 Vue.set( ) 为什么可以解决?他具体经历了那些步骤你知道吗?
2. Vue.set() 为什么可以解决上述问题?他具体经历了那些步骤你知道吗?
Vue.set(target, key, value)
// target 必须是一个响应式的数据源,在下面步骤会讲到
会经历一下三个步骤
对 target 进行数据校验
① 数据是 undefined、null或其他基本数据类型,会报错;
② 数据是 数组:则会取出当前数组的长度与当前 key 值的位置进行一个对比,取两者最大值,作为新数组的长度 -> max(target.length, key) ,然后使用 splice(key, 1, value); 当使用 splice 的时候,会自动遍历设置响应式。
③ 数据是 对象:key 是否在对象里,如果在则直接替换;如果不在则直接判断 target 是不是响应式对象;然后判断是不是 Vue 实例或者根的数据对象,如果是 throw error。如果不是直接给 target 的 key 赋值,如果 target 是响应式,使用 defineReactive 将新的属性添加到 target,进行依赖收集;
Vue.set( ) 源码
// example :
this.$set(data, a, 1);
function set(target: Array<any> object, key: any, val: any): any
// isUndef 是判断 target 是不是等于 undefined 或者 nul1
// isPrimitive 是判断 target 的数据类型是不是 string、number、symbol、boolean 中的一种
if (process.env.NODE ENV !== 'production' &&(isUndef(target) isPrimitive(target)))
warn(`Cannot set readtive property on undefined, null, or primitive value: $((target: 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
// vue给响应式对象(比如 data 里定义的对象)都加了一个 ob 属性,
// 如果一个对象有这个 ob属性,那么就说明这个对象是响应式对象,修改对象已有属性的时候就会触发页面渲染
// 非 data 里定义的就不是响应式对象。
const ob = (target: any).__ob__
if (target. isVue (ob && ob.vmCount))
process.env.NODE ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data' +
'at runtime - declare it upfront in the data option.!'
return val
// 不是响应式对象
if (!ob)
target[key] = val
return val
// 是响应式对象,进行依赖收集
defineReactive(ob.value, key, val)
// 触发更新视图
ob.dep.notify()
return val
3. 如何实现一个简单的 Vue2 响应式 ?
export function Vue(options)
this.__init(options);
// initMixin
Vue.prototype.__init = function (options)
this.$options = options;
// 假如这里是一个字符串,就需要使用 document.querySelector 去获取
this.$el = options.el;
this.$data = options.data;
this.$methods = options.methods;
// beforeCreate -- initState -- initData
proxy(this, this.$data);
// Object.defineProperty
observer(this.$data);
new Compiler(this);
;
// this.$data.message ---> this.message
function proxy(target, data)
let that = this;
Object.keys(data).forEach((key) =>
Object.defineProperty(target, key,
enumerable: true,
configurable: true,
get()
return data[key];
,
set(newVal)
// 考虑 NaN 的情况
// this 指向已经改变
if (!isSameVal(data[key], newVal))
data[key] = newVal;
,
);
);
function observer(data)
new Observer(data);
class Observer
constructor(data)
this.walk(data);
walk(data)
if (data && typeof data === "object")
Object.keys(data).forEach((key) =>
this.defineReactive(data, key, data[key])
);
//要把 data 里面的数据,收集起来
defineReactive(obj, key, value)
let that = this;
this.walk(value);
let dep = new Dep();
Object.defineProperty(obj, key,
enumerable: true,
configurable: true,
get()
// get时 Dep 收集依赖
// 4. 对于 num 来说,就要执行这一句
// 5. num 中的 dep,就有了这个 watcher
Dep.target && dep.add(Dep.target);
return value;
,
set(newVal)
if (!isSameVal(value, newVal))
//赋值进来的新值,是没有响应式的,所以我要在 walk 一次,添加响应式
value = newVal;
that.walk(newVal);
// 重新 set时,notify 通知更新
// 6.
dep.notify();
,
);
// 视图怎么更新?
// 数据改变,视图才会更新。需要去观察
// 1. new Watcher(vm, 'num', ()=> 更新视图上的 num 显示 )
class Watcher
constructor(vm, key, callback)
this.vm = vm; // VUE 的一个实例
this.key = key;
this.callback = callback;
// 2. 此时 Dep.target 作为一个全局变量理解,放的就是就是 watcher
Dep.target = this;
// 3. 一旦进行了这一句赋值,是不是就触发了这个值的 getter 函数
this.__old = vm[key];
Dep.target = null;
// 8. 执行所有的 callback 函数
update()
let newVal = this.vm[this.key];
if (!isSameVal(newVal, this.__old)) this.callback(newVal);
// 每一个数据都要有一个 Dep 依赖
class Dep
constructor()
this.watchers = new Set();
add(watcher)
if (watcher && watcher.update) this.watchers.add(watcher);
// 7. 让所有的 watcher 执行 update 方法
notify()
this.watchers.forEach((watch) => watch.update());
class Compiler
constructor(vm)
this.vm = vm;
this.el = vm.$el;
this.methods = vm.$methods;
this.compile(vm.$el);
// 这里是递归编译 #app 下面的所有的节点内容
compile(el)
let childNodes = el.childNodes;
// childNodes 为类数组
Array.from(childNodes).forEach((node) =>
// 判断如果是文本节点
if (node.nodeType === 3)
this.compileText(node);
// 判断如果是元素节点
else if (node.nodeType === 1)
this.compileElement(node);
// 判断如果还有子节点,就递归下去
if (node.childNodes && node.childNodes.length) this.compile(node);
);
compileText(node)
// 匹配出来 message
let reg = /\\\\(.+?)\\\\/;
let value = node.textContent;
if (reg.test(value))
let key = RegExp.$1.trim();
// 开始时赋值
node.textContent = value.replace(reg, this.vm[key]);
// 给 message 添加观察者
new Watcher(this.vm, key, (val) =>
// 数据改变时更新
node.textContent = val;
);
compileElement(node)
if (node.attributes.length)
Array.from(node.attributes).forEach((attr) =>
let attrName = attr.name;
if (attrName.startsWith("v-"))
// v- 指定匹配成功,可能是 v-on:click 或者 v-model
// 假设我们这里就处理两个指令,Vue源码对这一块是有特殊处理的
attrName =
attrName.indexOf(":") > -1
? attrName.substr(5)
: attrName.substr(2);
let key = attr.value;
this.update(node, key, attrName, this.vm[key]);
);
update(node, key, attrName, value)
if (attrName === "model")
node.value = value;
new Watcher(this.vm, key, (val) => (node.value = val));
node.addEventListener("input", () =>
this.vm[key] = node.value;
);
else if (attrName === "click")
node.addEventListener(attrName, this.methods[key].bind(this.vm));
function isSameVal(a, b)
return a === b || (Number.isNaN(a) && Number.isNaN(b));
Vue2 的响应式我们简单介绍一下,下来一起来看 Vue3 的 Proxy!
2. Vue3 的 Proxy
Proxy:代理或拦截器,Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写;Proxy的思路和 React 的 HOC 很像,组件外面包裹一层,对外界的访问进行过滤和改写;
const initData = value:1;
const proxy = new Proxy(initData,
get(target, key, receiver)
console.log('访问了', key);
return Reflect.get(target, key, receiver);
,
set(target, key, value, receiver)
console.log('修改了', key);
return Reflect.set(target, key, value, receiver);
)
proxy.value;
proxy.value = 2;
proxy;
proxy.value2 = 2;
proxy.value2;
具体这里就不详细说了,感兴趣的大家可以移步下面链接:
Vue3中的响应式原理,为什么使用Proxy(代理) 与 Reflect(反射)
二、Vue3 新特性
Composition API
composition api : 组合式 api,通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 < script setup> 搭配使用。这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如, < script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。
1. 如何理解 setup ?
通俗一点的讲,setup 可以把他理解为 Vue3 组件模块的入口文件,Vue3 中组件的新特性 ,作为组件统一的入口支持;
未使用 setup 语法糖的写法 (了解即可,实际开发还是使用语法糖写法更加便捷):
setup(props, context)
context.attrs --> this.$attrs
context.slot --> this.$slot
context.emit --> this.$emit
context.expose
使用 setup 语法糖写法
<script setup>
// 变量
const msg = 'Hello!'
// 函数
function log()
console.log(msg)
</script>
<template>
<button @click="log"> msg </button>
</template>
为什么推荐使用 setup 语法糖?
- 更少的样板内容,更简洁的代码。
- 能够使用纯 TypeScript 声明 props 和自定义事件。
- 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
- 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。
setup 是在 beforeCreate 和 created 之前去执行
2. 多根节点
什么是多根节点呢?看下图代码
单文件的多根节点
vue3 中之所以可以有多个节点,是因为引入了Fragment的概念,这是一个抽象的节点,如果发现组件有多个根,就创建一个Fragment节点,把多个根节点作为它的children,将来path的时候,如果发现是一个Fragement节点,则直接遍历children创建或更新。
项目的多根节点——多个应用实例
应用实例并不只限于一个。createApp API 允许你在同一个页面中创建多个共存的 Vue 应用,而且每个应用都拥有自己的用于配置和全局资源的作用域。
如果你正在使用 Vue 来增强服务端渲染 html,并且只想要 Vue 去控制一个大型页面中特殊的一小部分,应避免将一个单独的 Vue 应用实例挂载到整个页面上,而是应该创建多个小的应用实例,将它们分别挂载到所需的元素上去。
3. reactive() 与 shallowReactive()
reactive:通过 proxy 声明一个深层的响应式对象,响应式是深层次的,会影响所有嵌套。 等同于 Vue2 的 Vue.observable()
const person =
name: 'Barry',
age: 18,
contacts:
phone: 1873770
const personReactive = reactive(person);
console.log(personReactive); // proxy
const contacts = personReactive.contacts;
consoleVue3 源码解析:代码生成器