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;

以上就是最基础的使用;
但是我们一起来看一下,下面几个问题会输出什么?

  1. 直接访问 data.value => 访问了 value
  2. 改变 data.value => 修改了 value
  3. 直接输出 data => 空对象:
  4. 给 initData 添加一个新值 => 输出新值结果:2
  5. 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 语法糖?

  1. 更少的样板内容,更简洁的代码。
  2. 能够使用纯 TypeScript 声明 props 和自定义事件。
  3. 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
  4. 更好的 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 源码解析:代码生成器

Vue 3.0 源码逐行解析:响应式模块

Vue3 源码解析:ref 与 computed 原理揭秘

webpack核心模块tapable源码解析

webpack核心模块tapable源码解析

Vue3 源码逐行解析