问题: .vue 中的 HTML 是真实的 html 吗? 不是
中间做了什么事情, 让 假的 html 标签节点 被渲染成了 真实的 html 标签节点?
- 编译时: compiler
- 运行时: runtime
什么是运行时?
https://cn.vuejs.org/api/options-rendering.html#render
render: 用于编程式地创建组件虚拟 DOM 树的函数。
作用: 代替 template 模板来完成 DOM 的渲染
- 以下是 运行时 的代码框架
运行时可以利用 render 把 vnode 渲染成真实 dom 节点
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.22/vue.global.js"></script>
</head>
<body>
<div id="app">
<!--期望目标: <div class="test">hello render</div> -->
</div>
</body>
<script>
const {render,h}=Vue
//生成 vnode
const vnode=h('div',{
class:'test'
},'hello render')
//拿到承载的容器
const container= document.querySelector('#app')
//渲染
render(vnode,container)
</script>
</html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.22/vue.global.js"></script>
</head>
<body>
<div id="app">
</div>
</body>
<script>
//根据如下数据渲染出 div
const vnode={
type:'div',
props:{
class:'test'
},
children:'hello render'
}
//创建一个 render 函数
function render(vnode){
const ele=document.createElement(vnode.type)
ele.className=vnode.props.class
ele.innerText=vnode.children
document.body.appendChild(ele)
}
render(vnode)
</script>
</html>
什么是编译时?
如果只靠 运行时 , 那么是没法通过HTML 标签结构的方式,来进行渲染解析的,就需要借助 编译时
- 主要作用:把 template 中的 html 编译成 render 函数。如何再利用 运行时 通过 render 挂载对应的 dom
- 编译时可以把 html 的节点, 编译成 render 函数
<script>
const { compile, createApp } = Vue;
//创建一个 html 结构
const html = `<div class="test">hello compiler </div>`;
//利用 compile 函数,生成 render 函数
const renderFn = compile(html);
console.log(renderFn);
//创建实例
const app = createApp({
//利用 render 函数进行渲染
render: renderFn,
});
//挂载
app.mount("#app");
</script>
Vue 是一个 运行时+编译时 的框架
- vue 通过 compiler 解析 html 模板, 生成 render 函数, 然后通过 runtime 解析 render, 从而挂载真实 dom
为什么这样设计呢?
我们就需要知道 dom 渲染是如何进行的
- 对于 dom 渲染而言, 可以被分为两部分:
- 初次渲染,可以叫做 挂载
- 更新渲染, 可以叫 打补丁
初次渲染
//当 div 的 innerHTML 为空时,
<div id="app"></div>
//渲染如下节点:
<ul>
<li>1</li>
<li>2</li>
</ul>
这样就是初始渲染
更新渲染
li -2 上升到了第一位, 那这样浏览器如何更新这次渲染呢?
两种方式:
- 删除原有的所有节点, 重新渲染新的节点
- 好处在于不需要任何比对,需要执行 6 次( 删除 3 次, 重新渲染 3 次) dom 处理即可 【会涉及到更多的 dom 操作】
- 删除原位置的 li-2, 在新位置插入 li-2
- 对比 旧节点 和 新节点 之间的差异
- 根据差异, 删除一个旧节点, 增加一个新节点
【会涉及到 js 计算+ 少量的 dom 操作】
<script>
//相同数量的 DOM 操作和 JS 计算操作, 哪一个更快
const length = 10000;
//增加一万个 dom 节点, 查看耗时 3.992923...ms
console.time("element");
for (let i = 0; i < length; i++) {
const div = document.createElement("div");
document.body.appendChild(div);
}
console.timeEnd("element");
0.
//增加一万个 js 对象, 查看耗时 0.402099...ms
console.time("js");
const divList = [];
for (let i = 0; i < length; i++) {
const ele = {
type: "div"
};
divList.push(ele);
}
console.timeEnd("js");
</script>
- dom 操作要比 js 的操作耗时多的多, 即 dom 操作比 js 更加耗费性能
为什么 vue 要设计成一个运行时+编译时 的框架呢? 答案:
查看文档 : https://cn.vuejs.org/guide/extras/rendering-mechanism.html
- 1.针对于纯运行时而言:因为不存在编译器,所以我们只能够提供一个复杂的 JS 对象。
- 2.针对于纯编译时而言:因为缺少运行时,所以它只能把分析差异的操作,放到编译时进行,同样因为省略了运行时,所以速度可能会更快。但是这种方式这将损失灵活性。比如 svelte,它就是一个纯编译时的框架,但是它的实际运行速度可能达不到理论上的速度。
- 3.运行时+编译时:比如 vue 或 react 都是通过这种方式来进行构建的,使其可以在保持灵活性的基础上,尽量的进行性能的优化,从而达到一种平衡。
什么是副作用?
副作用会有多个吗? 会的

vue3 对 ts 支持友好, 并不是因为 vue3 是 ts 编写的
搭建框架雏形
为 vue 开启 sourceMap 用于 debugger
- package.json 中 build 命令加-s
"build": "node scripts/build.js -s ",
导入 ts, prettier
npm install -g typescript@4.7.4
npm install --save-dev prettier
模块打包器: rollup
- rollup 是一个模块打包器,和 webpack 一样可以将 JavaScript 打包为指定的模块。
- 但是不同的是,对于 webpack 而言,它在打包的时候会产生许多冗余的代码,这样的一种情况在我们开发大型项目的时候没有什么影响,但是如果我们是开发一个库的时候,那么这些冗余的代码就会大大增加库体积,这就不好美好了。
- 所以说我们需要一个小而美的模块打包器,这就是 rollup:
Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。
-
安装插件:
npm i -D @rollup/plugin-commonjs@22.0.1 @rollup/plugin-node-resolve@13.3.0 @rollup/plugin-typescript@8.3.4npm i --save-dev tslib@2.4.0 typescript@4.7.4
配置路径映射
- tsconfig.json
// 设置快捷导入
"baseUrl": ".",
"paths": {
"@vue/*": ["packages/*/src"]
}
响应系统
响应系统的核心设计原则
- 会影响视图变化的数据称为响应数据,当响应式数据发生变化时, 视图理应发生变化
- 提出问题:
- 那么 Vue 中这样的响应性数据是如何进行实现的呢?
- Vue2 和 Vue3 之间响应性的设计有什么变化吗? 为什么会 产生这种变化呢?
1.JS 的程序性
- JS 的程序性是一套固定的, 不会发生变化的执行流程,
- 在这样的一个程序性之下, 我们不可以拿到想要的 50, 想要程序变聪明就需要具备响应性
<script>
//定义一个商品对象
let product = {
price: 10,
quantity: 2,
};
//总价格
let total = product.price * product.quantity;
console.log("总价格:", total);//20
//修改商品的数量
product.quantity = 5;
console.log("总价格:", total);//20? 为啥不是 50
</script>
2.初步响应性
- 存在问题: 必须主动在数量发生变化之后, 重新主动执行 effect 才可以得到我们想要的结果, 那么这样太麻烦了
<script>
//定义一个商品对象
let product = {
price: 10,
quantity: 2,
};
//总价格
let total = 0;
//计算总价格
let effect = () => {
total = product.price * product.quantity;
};
effect();
console.log("总价格:", total); //20
//修改商品的数量
product.quantity = 5;
effect();
console.log("总价格:", total); //20
</script>
3.使用 Vue2 的 Object.defineProperty
- 会直接在一个对象定义一个新属性, 或者修改一个对象的现有属性, 并返回此对象
<script>
//定义一个商品对象
let quantity = 2;
let product = {
price: 10,
quantity: quantity,
};
//总价格
let total = 0;
//计算总价格
let effect = () => {
total = product.price * product.quantity;
};
//第一次打印
effect();
console.log("总价格:", total); //20
Object.defineProperty(product, "quantity", {
set(newVal) {
console.log("setter");
quantity=newVal
effect()
},
get() {
console.log("getter");
return quantity
},
});
</script>
存在一个致命的缺陷
由于 JavaScript 的限制,Vue2 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。
vue2 有以下响应性的限制:
- 当为 对象 新增一个没有在 data 中声明的属性时,新增的属性 不是响应性的
- 当为 数组 通过下标的形式新增一个元素时,新增的元素不是响应性的
想要搞明白这个原因,那就需要明白官网所说的由于 JavaScript 的限制指的是什么意思。
我们知道:
- 1.vue2 是以 Object.defineProperty 作为核心 API 实现的响应性
- 2.Object.defineProperty 只可以监听 指定对象 的指定属性的 getter 和 setter
- 3.被监听了 getter 和 setter 的属性, 就被叫做 该属性具备了响应性
那么这就意味着:我们 必须要知道指定对象中存在该属性,才可以为该属性指定响应性。
但是 由于 JavaScript 的限制,我们没有办法监听到 指定对象新增了一个属性,所以新增的属性就没有办法通过 Object.defineProperty 来监听 getter 和 setter,所以新增的属性将失去响应性
(那么如果想要增加具备响应性的新属性,那么可以通过 Vue.set 方法实现)
Vue3 的响应性核心 API: proxy
Proxy 和 Object.defineProperty 存在一个非常大的区别, 那就是:
proxy
- 1.proxy 将代理一个对象(被代理对象),得到一个新的对象(代理对象),同时拥有被代理对象中所有的属性。
- 2.当想要修改对象的指定属性时,我们应该使用代理对象进行修改
- 3.代理对象的任何一个属性都可以触发 handler 的 getter 和 setter
Object.defineProperty
- 1.Object.defineProperty 为 指定对象的指定属性 设置属性描述符
- 2.当想要修改对象的指定属性时, 可以使用原对象进行修改
- 3.通过属性描述符, 只有 被监听 的指定属性, 才可以触发 getter 和 setter
所以当 Vue3 通过 Proxy 实现响应性核心 API 之后, vue3 将 不会 再存在新增属性时失去响应性的问题
<script>
//定义一个商品对象
let product = {
price: 10,
quantity: 2,
};
//product: 被代理对象
//proxyProduct: 代理对象 (只有代理对象才会触发 setter, getter)
const proxyProduct = new Proxy(product, {
set(target ,key, newVal,receiver) {
// console.log(target ,key, newVal,receiver);
// console.log("setter");
target[key]=newVal
effect()
return true
},
get(target ,key, receiver) {
// console.log(target ,key, receiver,);
// console.log("getter");
return target[key]
},
});
//总价格
let total = 0;
//计算总价格
let effect = () => {
total = proxyProduct.price * proxyProduct.quantity;
};
effect();
console.log("总价格:", total); //20
</script>
proxy 的最佳拍档:Reflect---拦截 js 对象操作
- "伴生对象" : Reflect
Reflect 属性, 多数时候会与 proxy 配合进行使用在 MDN Proxy 的例子中, Reflect 也有对此出现。
const odj = {
name: "张",
};
console.log(obj.name)//张三
console.log(Reflect.get(obj,'name'))//张三
由以上代码可以发现,两次打印的结果是相同的。这其实也就说明了
Reflect.get(obj,'name') 本质上 和 obj.name 的作用相同。
那为啥还需要 Reflect 呢? 其还有第三个参数 receiver, 官网介绍:如果 target 对象中指定了 getter,receiver 则为 getter 调用时的 this 值。(Reflect 可以修改 this 指向)
<script>
const p1 = {
lastName: "张",
firstName: "三",
//通过 get 标识符标记, 可以让方法的调用像属性的调用一样
get fullName() {
return this.lastName + this.firstName;
},
};
const p2 = {
lastName: "李",
firstName: "四",
get fullName() {
return this.lastName + this.firstName; // ↓---触发 2 次
},
};
/**
* 第三个参数 receiver 在对象指定了 getter 时表示为 this
* 利用 p2 作为第三个参数 receiver, 以此来修改 fullName 的打印结果。
* 即:此时触发的 fullName 不是 p1 的而是 p2 的
* */
console.log(Reflect.get(p1, "name", p2)); //李四
const proxy = new Proxy(p1, {
//target:被代理对象 receiver:代理对象
get(target, key, receiver) {
console.log("getter 行为被触发");
// return target[key];//1 次
return Reflect.get(target, key, receiver); //触发 3 次
},
});
console.log(proxy.fullName); //张三 ---↑触发 1 次
//思考:使用 return target[key]; getter 行为应该被触发几次? 3 次, 实际触发一次
</script>
总结
当我们期望监听代理对象的 getter 和 setter 时, 不应该使用 target[key], 因为它在某些时刻(比如 fullName) 下是不可靠的。应该使用 Reflect, 借助它的 get 和 set 方法, 使用 receiver ( proxy 实例 ) 作为 this, 已达到期望的结果(触发三次 getter)
初见 reactivity 模块
阅读源码????
构建 reactive 函数, 获取 proxy 实例
- 整个 reactive 函数,本质上是返回了一个 proxy 实例
1.创建 packages/reactivity/src/reactive.ts 模块
import { mutableHandlers } from './baseHandlers'
/**
* 响应性 Map 缓存对象
* key:target
* val:proxy
*/
export const reactiveMap = new WeakMap<object, any>()
/**
* 为复杂数据类型, 创建响应式对象
* @param target 被代理的对象
* @returns 代理对象
*/
export function reactive(target: object) {
return createReactiveObject(target, mutableHandlers, reactiveMap)
}
/**
* 创建响应式对象
* @param target 被代理对象
* @param baseHandlers handler
* @param proxyMap
*/
function createReactiveObject(
target: object,
baseHandlers: ProxyHandler<any>,
proxyMap: WeakMap<object, any>
) {
//如果该实例已经被代理, 则直接读取即可
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
//未被代理则生成 proxy 实例
const proxy = new Proxy(target, baseHandlers)
//缓存代理对象
proxyMap.set(target, proxy)
return proxy
}
2.创建 packages/reactivity/src/baseHandlers.ts
/**
* 响应性的 handler
*/
export const mutableHandlers: ProxyHandler<object> = {}
3.创建 packages/reactivity/src/index.ts ,作为 reactivity 的入口模块
export { reactive } from './reactive'
4.创建 packages/vue/src/index.ts ,导入 reactivity 模块
export { reactive } from '@vue/reactivity'
5.执行 build 打包, 生成 vue.js
6.创建测试用例
WeakMap 和 Map 有什么区别?
核心共同点:都是{key,value}的结构对象
对于 WeakMap 而言,
- key 必须是对象
- key 是弱引用的
弱引用:不会影响垃圾回收机制。即:WeakMap 的 key 不再存在任何引用时, 会被直接回收
强引用:会影响垃圾回收机制。存在强应用的对象永远不会被回收
创建 createGettter 和 createSetter
packages/reactivity/src/baseHandlers.ts
/**
* 响应性的 handler
*/
import { track, trigger } from './effect'
const get = createGetter()
function createGetter() {
return function get(target: object, key: string | symbol, receiver: object) {
const res = Reflect.get(target, key, receiver)
track(target, key)
return res
}
}
const set = createSetter()
function createSetter() {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key, value)
return result
}
}
export const mutableHandlers: ProxyHandler<object> = {
get,
set
}
设置热更新
"dev": "rollup -c -w",
构建 effect 函数, 生成 ReactiveEffect
export function effect<T = any>(fn: () => T) {
const _effect = new ReactiveEffect(fn)
_effect.run()
}
export let activeEffect: ReactiveEffect | undefined
export class ReactiveEffect<T = any> {
constructor(public fn: () => T) {}
run() {
//当前被激活的 effect 实例
activeEffect = this
return this.fn() //完成第一次 getter 行为的触发
}
}
- 成功渲染了数据到 html 中
<body>
<div id="app"></div>
</body>
<script>
const { reactive,effect } = Vue
const obj=reactive({
name:'张三'
})
effect(()=>{
document.querySelector('#app').innerText=obj.name
})
</script>
track 和 trigger
由 baseHandlers.ts 中代码可知: 当触发 getter 行为时, 其实我们会触发 track 方法, 进行依赖收集, 当触发 setter 行为时, 会触发 trigger 方法, 来触发依赖
- 响应性指的是: 当响应性数据触发 setter 时执行 fn 函数,
- 想要达到这样的目的, 就必须在getter 时能够收集当前的 fn 函数, 以便 setter 的时候可以执行对应的 fn 函数
但是对于收集而言, 如果仅仅是把 fn 存起来还是不够的, 我们还需要知道, 当前的这个 fn 是哪个响应式数据对象的哪个属性对应的, 只有这样, 我们才可以在 该属性触发 setter 的时候, 准确的执行响应性
如何进行依赖收集
WeakMap :
key: 响应性对象
value: Map 对象
key: 响应性对象的指定属性
value:指定对象的指定属性的执行函数
//effect.ts
type KeyToDepMap = Map<any, ReactiveEffect>
/**
* 收集所有依赖的 WeakMap 实例
* key: 响应性对象
* value: Map 对象
* key: 响应性对象的指定属性
* value:指定对象的指定属性的执行函数
*/
const targetMap = new WeakMap<any, KeyToDepMap>()
/**
* 收集依赖
* @param target WeakMap 的 key
* @param key key 代理对象的 key, 当依赖被触发时,需要根据该 key 获取
*/
export function track(target: object, key: unknown) {
if (!activeEffect) return
//尝试从 targetMap 中, 根据 target 获取 map
let depsMap = targetMap.get(target)
//如果获取到的 map 不存在,则生成新的 map 对象, 并把该对象赋值给对应的 value
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
//为指定 map, 指定 key, 设置回调函数
depsMap.set(key, activeEffect)
console.log(targetMap)
}
trigger 触发依赖
/**
* 触发依赖
* @param target WeakMap 的 key
* @param key key 代理对象的 key, 当依赖被触发时,需要根据该 key 获取
*/
export function trigger(target: object, key: unknown, newValue: unknown) {
//依据 target 获取存储的 map 实例
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
//依据 key, 从 depsMap 中取出 value, 该 value 是一个 ReactiveEffect 类型的数据
const effect = depsMap.get(key) as ReactiveEffect
if (!effect) {
return
}
//执行 effect 中保存的 fn 函数
effect.fn()
}
- 这样就可以触发 setter 时, 执行保存的 fn 函数了
构建 Dep 模块, 处理一对多的依赖关系
<div id="app">
<p id="p1"></p>
<p id="p2"></p>
</div>
effect(()=>{
document.querySelector('#p1').innerText=obj.name
})
effect(()=>{
document.querySelector('#p2').innerText=obj.name
})
name 属性对应两个 DOM 的变化, 但是 p1 的更新渲染是无效的
我们期望: 一个 key 可以对应多个有效的 effect 函数的话, 应该可以构建一个 Set 类型的对象, 作为 Map 的 value
reactive('张三')不能接收简单数据类型
- 对于我们 reactive 的 effect 而言, 最终生成一个 proxy 实例, 其中第一个参数必须是一个对象(被代理对象), 如果是简单数据类型, 是没法代理的
对 reactive 的属性进行解构, 不会具备响应性
<script>
const { reactive,effect } = Vue
const obj=reactive({
name:'张三'
})
let {name}=obj
console.log('name',name);
effect(()=>{
document.querySelector('#app').innerText=name
})
setTimeout(()=>{
obj.name='李四'
console.log('obj',obj);
},2000)
</script>
reactive 总结
对于 reactive 的响应性函数而言,
- 1.是通过 proxy 的 setter 和 getter 来实现的数据监听
- 2.需要配合 effect 函数进行使用
- 3.基于 WeakMap 完成的依赖收集和处理
- 4.可以存在一对多的依赖关系
同时我们也了解了 reactive 函数的不足:
- 1.reactive 只能对复杂数据类型进行使用
- 2.reactive 的响应性数据,不可以进行解构
因为 reactive 的不足,所以 vue3 又为我们提供了 ref 函数构建响应性,那么:
- 1.ref 函数的内容是如何进行实现的呢?
- 2.ref 可以构建简单数据类型的响应性吗?
- 3.为什么 ref 类型的数据,必须要通过.value 访问值呢?
ref 的响应性
如果 ref 传递过来的 value 是一个对象, 那么当前 ref 的响应性本质上是通过 reactive 实现的; 如果不是对象, 则原样将 value 返回
源码阅读

构建 ref 简单数据类型响应性
详情看代码实现
class RefImpl<T> {
private _value: T
private _rawValue: T
...
constructor(
value: T,
public readonly __v_isShallow: boolean
) {
...
//原始数据
this._rawValue = value
}
...
/**
* newValue 新数据
* this._rawValue 为旧数据(原始数据)
* 对比两个数据是否发生改变
*/
set value(newValue) {
if (hasChanged(newValue, this._rawValue)) {
//更新原始数据
this._rawValue = newValue
//更新 .value 的值
this._value = toReactive(newValue)
//触发依赖
triggerRefValue(this)
}
}
}
//为 ref 的 value 进行触发依赖工作
export function triggerRefValue(ref) {
if (ref.dep) {
triggerEffects(ref.dep)
}
}
- 2.在 packages/shared/src/index.ts 中,新增 hasChanged 方法
/**
* 对比两个数据是否发生改变, 如果发生改变则返回 true
*/
export const hasChanged = (value: any, oldValue: any): boolean =>
!Object.is(value, oldValue)
ref 总结
-
简单数据类型, 不具备数据监听的概念, 即本身并不是响应性的。只是因为 vue 通过了 set value()的语法,把 函数调用变成了属性调用的形式,让我们通过主动调用该函数, 来完成了一个“类似于”响应性的结果。
-
1.ref 函数的内容是如何进行实现的呢?
答:ref 函数本质上是生成了一个 RefImpl 类型的实例对象,通过 get 和 set 标记处理了 value 函数 -
2.ref 可以构建简单数据类型的响应性吗?
答:是的。ref 可以构建简单数据类型的响应性 -
3.为什么 ref 类型的数据,必须要通过.value 访问值呢?
答:1.因为 ref 需要处理简单数据类型的响应性, 但是对于简单数据类型而言,它无法通过 proxy 建立代理。
2.所以 vue 通过 get value()和 set value()定义了两个属性函数,通过主动触发这两个函数(属性调用)的形式来进行依赖收集和触发依赖
3.所以我们必须通过.value 来保证响应性
watch 和 computed 【复杂!!!】
计算属性 computed
- 会基于其响应式依赖被缓存,并且在依赖的响应式数据发生变化时 重新计算
<script>
const { reactive,effect,computed } = Vue
const obj=reactive({
name:'张三'
})
const computedObj= computed(()=>{
return '姓名:'+obj.name
})
effect(()=>{
document.querySelector('#app').innerText=computedObj.value
})
setTimeout(()=>{
obj.name='李四'
},2000)
</script>
在以上测试实例中,程序主要执行了 5 个步骤:
- 1.使用 reactive 创建响应性数据
- 2.通过 computed 创建计算属性 computedObj,并且触发了 obj 的 getter
- 3.通过 effect 方法创建 fn 函数
- 4.在 fn 函数中, 触发了 computed 的 getter
- 5.延迟触发了 obj 的 setter
computed 的代码在 packages/reactivity/src/computed.ts 中, 我们可以在这里为 computed 函数增加断点.
-
1.代码进入 computed 函数
-
2.执行 const onlyGetter=isFunction(getterOrOptions)方法:
- getterOrOptions 为传入的第一个参数, 因为我们传入的为函数, 所以 onlyGetter=true
-
3.执行: getter=getterOrOptions, 即: getter 为我们传入的函数
-
4.执行: setter=NOOP, NOOP 为()=>{}
-
5.执行: new ComputedRefImpl,创建 ComputedRefImpl 实例。 那么这里的 ComputedRefImpl 是什么呢?
-
6.进入 ComputedRefImpl
-
a.在构造函数中,可以看到: 创建了 ReactiveEffect 实例,并且传入了两个参数:
//1.getter:触发 computed 函数时,传入的第一个参数 //2.匿名函数: 当 this._dirty 为 false 时,会触发 triggerRefValue,我们知道 triggerRefValue 会 **依次触发依赖** ()=>{ //_dirty 表示"脏"的意思, 这里可以理解为: 依赖的响应性数据发生了变化, 计算属性需要重新计算了 if(!this._dirty){ this._dirty=true triggerRefValue(this) } } -
b.而对于 ReactiveEffect 而言,
- 1.它位于 packages/reactivity/src/effect.ts 文件中
- 2.提供了一个 run 方法和一个 stop 方法:
run 方法: 触发 fn, 即传入的第一个参数
stop 方法: 语义上为停止的意思 - 3.生成的实例, 我们一般叫做 effect
-
c.执行 this.effect.computed=this, 即effect 实例 被挂载了一个新的属性 computed 为当前的 ComputedRefImpl 的实例.
-
d.ReactiveEffect构造函数执行完成
-
-
7.在 computed 中返回了 ComputedRefImpl 实例
由以上代码可知, 当我们在执行 computed 函数时:
- 1.定义变量 getter 为我们传入的回调函数
- 2.生成了 ComputedRefImpl 实例, 作为 computed 函数的返回值
- 3.ComputedRefImpl 内部, 利用了 ReactiveEffect 函数, 并且传入了 第二个参数
computed 的 getter
当 computed 代码执行完成之后, 我们在 effect 中触发了 computed 的 getter:
computedObj.value
根据我们之前在学习 ref 的时候可知,.value 属性的调用本质上是一个get value 的函数调用,而 computedObj 作为 computed 的返回值,本质上是 ComputedRefImpl 的实例, 所以此时会触发 ComputedRefImpl 下的 get value 函数。
-
1.进入 ComputedRefImpl 下的 get value 函数
-
2.执行 trackRefValue(self),该方法我们是有过了解的,知道它的作用是: 收集依赖,它接收个 ref 作为参数,该 ref 本质上就是 ComputedRefImpl 的实例:
-
3.执行 self._dirty=false,我们知道 _dirty 是 脏 的意思,如果 _dirty = true 则会触发执行依赖。在当前(标记为 false 之前),self._dirty=true
-
4.所以接下来执行 self.effect.run()!,执行了 run 方法,我们知道 run 方法内部其实会触发 fn 函数,即: computed 接收的第一个参数
-
5.接下来把 self._value = self.effect.run()!, 此时 self._value 的值为 computed 第一个参数(fn 函数)的返回值,即为: 计算属性计算之后的值
-
6.最后执行 return self._value,返回计算的值
由以上代码可知:
- 1.ComputedRefImpl 实例本身就没有 代理监听 , 它本质上是一个 get value 和 set value 的触发
- 2.在每一次 get value 被触发时, 都会主动触发一次 依赖收集
- 3.根据 _dirty 和 _cacheable 的状态判断, 是否需要触发 run 函数
- 4.computed 的返回值, 其实是 run 函数执行之后的返回值
ReactiveEffect 的 scheduler
初步分析完成了 computed 的源码执行逻辑, 还存在一些问题。
我们知道对于计算属性而言, 当它依赖的响应式数据发生变化时, 它将重新计算。换句话说:当响应性数据触发 setter 时, 计算属性需要触发依赖.
在上面的代码中,我们知道,当《每一次 getvalue 被触发时,都会主动触发一次依赖收集》,但是触发依赖的地方在哪呢?
根据以上代码可知: 在 ComputedRefImpl 的构造函数中,我们创建了 ReactiveEffect 实例,并且传递了第二个参数,该参数为一个回调函数,在这个回调函数中: 我们会根据脏的状态来执行 triggerRefValue,即触发依赖,重新计算。
那么这个 ReactiveEffect 第二个参数是什么呢?它会在什么时候被触发,以触发依赖呢?
我们来看一下:
- 1.进入 packages/reactivity/src/effect.ts 中
- 2.查看 ReactiveEffect 的构造函数,可以第二个参数为 scheduler
- 3.scheduler 表示 调度器 的意思, 我们查看 packages/reactivity/src/effect.ts 中 triggerEffect 方法, 可以发现这里进行了调度器的判定:
function triggerEffect(...){ ... if(effect.scheduler){ effect.scheduler() } }
跟踪代码
我们知道延迟两秒之后,会触发 obj.name
即 reactive 的 setter 行为.
所以我们可以在
packaaes/reactivity/src/baseHandlers.ts 中为 set 增加一个断点:
- 1.进入 reactive 的 setter (注意: 这里是延迟两秒之后 setter 行为)
- 2.跳过之前的相同逻辑之后,可知,最后会触发:trigger(target,TriggerOpTypes.SET,key,
value,oldValue)方法 - 3.进入 trigger 方法:
- 4.同样跳过之前相同逻辑,可知,最后会触发:triggerEffects(deps[0],eventInfo)方法
- 5.进入 triggerEffects 方法:
- 6.
构建 ComputedRefImpl,读取计算属性
packages/reactivity/src/computed.ts
- 构建 ComputedRefImpl 类, 创建出 computed 方法, 并且能够读取值
computed 的响应性: 初见调度器, 处理脏的状态
如果想要实现响应性, 必须具备两个条件:
- 1.收集依赖: 已在 get value 中进行
- 2.触发依赖:
调度器
- 调度器 scheduler 是一个相对比较复杂的概念,它在 computed 和 watch 中都有涉及, 但是在当前的 computed 实现中, 它的作用还算比较清晰。
- 所以根据我们秉承的 没有使用就当做不存在 的理念,我们只需要搞清楚,它在当前的作用即可。
- 此时的 scheduler 就相当于一个回调函数.
- 在 triggerEffect 只要 effect 存在 scheduler,则就会执行该函数。
_dirty 脏
它只是一个变量, 我们只需要知道: 它为 false 时, 表示需要触发依赖。为 true 时表示需要重新执行 run 方法, 获取数据 即可
computed 的缓存性
computed 区别于 function 最大的地方就是: computed 具备缓存,当多次触发计算实行时,那么计算属性只会计算一次
死循环 的问题
<script>
const { reactive,effect,computed } = Vue
const obj=reactive({
name:'张三'
})
const computedObj= computed(()=>{
console.log('计算属性执行')
return '姓名:'+obj.name
})
effect(()=>{
document.querySelector('#app').innerText=computedObj.value
document.querySelector('#app').innerText=computedObj.value
})
setTimeout(()=>{
obj.name='李四'
},2000)
</script>
运行到浏览器,出现了 死循环 的问题
这个死循环是在 延迟两秒后 出现的,而延迟两秒之后是 obj.name 的调用, 即: reactive 的 getter 行为被触发, 也就是 trigger 方法触发时:
1.为 packages/reactivity/src/effect.ts 中的 trigger 方法增加断点, 延迟两秒之后,进入断点:
2.此时执行的代码是 obj.name='李四', 所以在 target 为{name:'李四'}
3.但是要注意, 此时 targetMap 中, 已经在 收集过 effect 了, 此时的 dep 中包含一个计算属性的 effect:
4.进入到 triggerEffects(dep)方法
死循环 的问题---解决方法
- packages/reactivity/src/effect.ts
/**
* 触发 dep 中保存的依赖
* @param dep
*/
export function triggerEffects(dep: Dep) {
//把 dep 构建为一个数组
const effects = isArray(dep) ? dep : [...dep]
//不再依次触发依赖
// for (const effect of effects) {
// triggerEffect(effect)
// }
//而是先触发所有的计算属性依赖, 再触发所有的非计算属性依赖
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect)
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect)
}
}
}
总结---计算属性 computed
1.计算属性的实例,本质上是一个 ComputedRefImpl 的实例
2.ComputedRefImpl 中通过 dirty 变量来控制 run 的执行和 triggerRefValue 的触发
3.想要访问计算属性的值,必须通过.value,因为它内部和 ref 一样是通过 get value 来进行实现的
4.每次.value 时都会触发 trackRefValue 即:收集依赖
5.在依赖触发时,需要谨记,先触发 computed 的 effect,再触发非 computed 的 effect
响应性的数据监听器 watch
vue 官方文档
watch 可以监听响应式数据的变化, 从而触发指定的函数
- 源码阅读
<script>
const { reactive,watch} =Vue
const obj=reactive({
name:'张三'
})
watch(obj,(value,oldValue)=>{
document.querySelector('#app').innerHTML=obj.name
})
setTimeout(()=>{
obj.name='李四'
},2000)
</script>
-
同时因为接下来 同步任务已经执行完成, 所以 异步的微任务 马上就要开始执行, 即接下来我们将会进入 flushJobs 中
-
watch 整体分为了四大块:
- watch 函数本身
- reactive 的 setter
- flushJobs
- job
-
整个 watch 还是比较复杂的,主要是因为 vue 在内部进行了很多的 兼容性处理,使代码的复杂度上升了好几个台阶,我们自己去实现的时候会简单很多的。
深入 scheduler 调度系统实现机制
整个调度系统其实包含两部分实现:
- 1.lazy: 懒执行
//packages/reactivity/src/effect.ts
export interface ReactiveEffectOptions {
lazy?: boolean
scheduler?: EffectScheduler
}
export function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions) {
const _effect = new ReactiveEffect(fn)
if (!options || !options.lazy) {
_effect.run()
}
}
- 2.scheduler: 调度器 [稍微复杂一些]
- 控制执行顺序
<script>
const { reactive,effect } = Vue
const obj=reactive({
count:1
})
effect(()=>{
console.log(obj.count)
},{
scheduler(){
setTimeout(()=>{
console.log(obj.count)
})
}
})
obj.count=2
console.log('代码运行结束');
</script>
//1
//代码结束..
//2
- 控制执行规则
- packages/runtime-core/src/scheduler.ts
let isFlushPending = false
const resolvedPromise = Promise.resolve() as Promise<any>
let currentFlushPromise: Promise<void> | null = null
const pendingPreFlushCbs: Function[] = []
export function queuePreFlushCb(cb: Function) {
queueCb(cb, pendingPreFlushCbs)
}
function queueCb(cb: Function, pendingQueue: Function[]) {
pendingQueue.push(cb)
queueFlush()
}
function queueFlush() {
if (!isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
function flushJobs() {
isFlushPending = false
flushPreFlushCbs()
}
export function flushPreFlushCbs() {
if (pendingPreFlushCbs.length) {
//拷贝去重,类似深拷贝
let activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
pendingPreFlushCbs.length = 0
for (let i = 0; i < activePreFlushCbs.length; i++) {
activePreFlushCbs[i]()
}
}
}
<script>
const { reactive,effect,queuePreFlushCb } = Vue
const obj=reactive({
count:1
})
effect(()=>{
console.log(obj.count)
},{
scheduler(){
queuePreFlushCb(()=>console.log(obj.count));
}
})
obj.count=2
obj.count=3
</script>
watch 数据监听器
packages/runtime-core/src/apiWatch.ts
watch 数据监听器的依赖收集
- 解决问题---"响应式数据的改变并不会引起 watch 的触发"
/**
* 本质上是拿 value, 出发一次 getter 行为
* @param value
*/
export function traverse(value: unknown) {
if (!isObject(value)) {
return value
}
for (const key in value as object) {
traverse((value as object)[key])
}
return value
}
runtime 运行时
- 运行时: 即把 VNode 渲染到页面中。
运行时核心设计原则
HTML DOM 节点树与虚拟 DOM 树
- 虚拟 DOM 是一种编程概念,意为将目标所需的 UI 通过数据结构“虚拟”地表示出来,保存在内存中,然后将真实的 DOM 与之保持同步。这个概念是由 React 率先开拓, 随后在许多不同的框架中都有不同的实现, 当然也包括 Vue
- 在运行时 runtime, 渲染器renderer会遍历整个虚拟 DOM 树, 并据此构建真实的 DOM 树, 这个过程可以叫做 挂载 mount
- 当这个 VNode 对象发生变化时, 那么我们会对比 旧的 VNode 和新的 VNode 之间的区别, 找出它们之间的区别, 并应用这其中的变化到真实的 DOM 上. 这个过程被称为更新 patch
挂载和更新
- 挂载案例
<body>
<div id="app"></div>
</body>
<script>
// <div>hello render</div>
const vnode = {
type: 'div',
children: 'hello render'
}
function render(oldVNode, newVNode, container) {
if (!oldVNode) {
mount(newVNode, container)
}
}
//挂载函数
function mount(vnode, container) {
//根据 type 生成 element
const ele = document.createElement(vnode.type)
//把 children 赋值给 ele 的 innerText
ele.innerText = vnode.children
//把 ele 作为子节点插入 body 中
container.appendChild(ele)
}
render(null, vnode, document.querySelector('#app'))
</script>
- 更新案例
- 在 patch 函数中, 我们先 删除了旧的 VNode, 然后创建了一个新的 vnode,
<script>
// <div>hello render</div>
const vnode = {
type: 'div',
children: 'hello render'
}
// <div>patch render</div>
const vnode2 = {
type: 'div',
children: 'patch render'
}
function render(oldVNode, newVNode, container) {
if (!oldVNode) {
mount(newVNode, container)
} else {
patch(oldVNode, newVNode, container)
}
}
function mount(vnode, container) {
const ele = document.createElement(vnode.type)
ele.innerText = vnode.children
container.appendChild(ele)
}
//将之前内容清空
function unmount(container) {
container.innerHTML = ''
}
function patch(oldVNode, newVNode, container) {
unmount(container)
const ele = document.createElement(newVNode.type)
ele.innerText = newVNode.children
container.appendChild(ele)
}
render(null, vnode, document.querySelector('#app'))
setTimeout(() => {
render(vnode, vnode2, document.querySelector('#app'))
}, 2000);
</script>
总结
- 挂载: 运行时渲染器调用渲染函数, 遍历返回的虚拟 DOM 树, 并基于它创建实际的 DOM 节点.
- 更新: 当一个依赖发生变化后, 副作用会重新运行, 这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树, 将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。
h 函数与 render 函数
h 函数
- h 函数本质上就是一个 用来生成 VNode 的函数
- type: string | Component : 既可以是一个字符串, 也可以是一个 vue 组件定义
- props?:object | null: 要传递的 prop
- children?: Children | Slot | Slots: 子节点
<script>
const { render, h } = Vue
//生成 vnode
const vnode = h('div', {
class: 'test'
}, 'hello render')
//拿到承载的容器
const container = document.querySelector('#app')
//渲染函数
render(vnode, container)
</script>
- 打印 vnode 后, 精简的 vnode
render 函数
- 通过 render 函数, 我们可以: 使用编程式地方式,创建虚拟 DOM 树对应的真实 DOM 树,到指定位置。
运行时核心设计原则---解释
1.runtime-core 与 runtime-dom 的关系, 为什么要这么设计?
在 vue 源码中, 关于运行时的包主要有两个:
- 1.packages/runtime-core: 运行时的核心代码
- 2.packages/runtime-dom : 运行时关于浏览器渲染的代码
2.渲染时, 挂载和更新的逻辑处理
构建 h 函数生成 vnode
源码阅读
- 按位或赋值
- 测试实例:
<body>
<div id="app">
</div>
</body>
<script>
const { h } = Vue
const vnode = h('div', {
class: 'test',
}, 'hello world')
console.log(vnode);
</script>
- 在 packages/runtime-core/src/h.ts 为 208 行 const l = arguments.length 增加 debugger
构建 h 函数,处理 ELEMENT+TEXT_CHILDREN 场景
shapeFlag 为 9
创建 packages/shared/src/shapeFlags.ts 写入所有的对应类型
export const enum ShapeFlags {
/**
* type=Element
*/
ELEMENT = 1,
/**
* 函数组件
*/
FUNCTIONAL_COMPONENT = 1 << 1,
/**
* 有状态(响应数据)组件
*/
STATEFUL_COMPONENT = 1 << 2,
/**
* children=Text
*/
TEXT_CHILDREN = 1 << 3,
/**
* children=Array
*/
ARRAY_CHILDREN = 1 << 4,
/**
* children=slot
*/
SLOTS__CHILDREN = 1 << 5,
/**
* 组件: 有状态(响应数据)组件 | 函数组件
*/
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
- 创建 packages/runtime-core/src/h.ts
- 创建 packages/runtime-core/src/vnode.ts
处理 ELEMENT+ARRAY_CHILDREN 场景
shapeFlag 为 17

源码阅读: h 函数, 组件的本质与对应的 VNode
- 在 vue 中, 组件本质上是 一个对象或一个函数(少见)
- 可以直接利用 h 函数+render 函数渲染出一个基本的组件:
- 创建 h-component.html
<script>
const { h, render } = Vue
const component = {
render() {
// const vnode1=h('div','这是一个 component')
// console.log(vnode1);
// return vnode1
//直接利用当前打印的 vnode , 绕过 h 的渲染
return {
"__v_isVNode": true,
"type": 'div',
"children": '这是一个 component',
"shapeFlag": 9
}
}
}
// const vnode2=h(component)
// console.log(vnode2);
const vnode2 = {
"__v_isVNode": true,
"shapeFlag": 4,
"type": component
}
render(vnode2, document.querySelector('#app'))
</script>
实现: 处理组件的 VNode
- packages/runtime-core/src/vnode.ts 加上 isObject(type)的判定
export function createVNode(type, props, children): VNode {
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: 0
return createBaseVNode(type, props, children, shapeFlag)
}
源码阅读: h 函数, 跟踪 Text、Comment、Fragment 场景
<script>
//Fragment : 片段 vue3 一个模板中, 可以有多个根节点,就是利用这片段完成的
const { h, render, Text,Comment,Fragment } = Vue
// const vnodeText = h(Text, '这是一个 Text')
// console.log(vnodeText)
// const vnodeComment=h(Comment,'这是一个 comment')
// console.log(vnodeComment);
// render(vnodeComment,document.querySelector('#app'))
const vnodeFragment=h(Fragment)
console.log(vnodeFragment);
render(vnodeFragment,document.querySelector('#app'))
</script>
实现: Text、Comment、Fragment
- packages/runtime-core/src/vnode.ts
export const Fragment = Symbol('Fragment')
export const Text = Symbol('Text')
export const Comment = Symbol('Comment')
源码阅读: 对 class 和 style 绑定
完成虚拟节点下的 class 和 style 的增强
packages/shared/src/normalizeProp.ts
import { isArray, isObject, isString } from '@vue/shared'
export function normalizeClass(value: unknown): string {
let res = ''
if (isString(value)) {
res = value
} else if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
const normalized = normalizeClass(value[i])
if (normalized) {
res += normalized + ' '
}
}
} else if (isObject(value)) {
for (const name in value as object) {
if ((value as object)[name]) {
res += name + ' '
}
}
}
return res.trim()
}
- 案例
<script>
const {h} =Vue
const vnode=h('div',{
class:[
{
'red':true,
},
{
'pink':true,
},
{
'blue':false
}
]
},'增强 class')
console.log(vnode)
</script>






















