一、框架设计概览
声明式与命令式(Declarative vs Imperative)
声明式和命令式是两种编程范式。vue/react
是声明式的,jquery那样直接操作dom是命令式
Alright here’s a metaphor.
Declarative Programming is like asking your friend to draw a landscape. You don’t care how they draw it, that’s up to them.
Imperative Programming is like your friend listening to Bob Ross tell them how to paint a landscape. While good ole Bob Ross isn’t exactly commanding, he is giving them step by step directions to get the desired result.
声明式就像你告诉你朋友画一幅画,你不用去管他怎么画的细节
命令式就像按照你的命令,你朋友一步步把画画出来
换言之
- 命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。
- 声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。
声明式与命令式的差异: 命令式更加注重过程,声明式注重结果。命令式理论上可以做到极致优化,但用户承受巨大的心智负担。声明式则牺牲一定的性能,换取代码可维护性,减轻用户的心智负担。
性能与可维护性的权衡
结论: 声明式代码的性能不优于命令式代码的性能
如果把直接修改的性能消耗定义为A,把找出差异的性能消耗定义为B,那么:
- 命令式代码的更新性能消耗 = A
- 声明式代码的更新性能消耗 = A + B
Q: Vue为什么要采用声明式的设计方案?
A:消耗一定的性能来换取更优的代码可维护性。
设计者要做到的宗旨:
在保持可维护性的同时让性能损失最小化。
虚拟DOM的性能如何
明确虚拟DOM要解决的问题:使找出差异的性能消耗最小化
运行时和编译时
- 纯运行时: 没有编译过程,没法分析内容差异进行优化
- 运行时+编译时(VUE):HTML模板 --> 编译器 --> 数据对象(虚拟DOM)--> 渲染器 --> 渲染DOM
- 纯编译时(Svelte): HTML模板 --> 编译器 --> DOM命令式代码,性能可能更好,但不够灵活
提高用户的开发体验
作者举了2个例子
- 提供友好的警告信息,比如vue中的
warn
函数。它会收集当前发生错误的组件栈信息,并向用户提供有用的信息。
warn("xxxx")
2. 自定义formatter函数,实现在开发环境中输出自定义的数据格式
控制框架代码体积
框架应该输出怎么样的构建产物
特性开关
错误处理
良好Typescript支持
Vue3的设计思路
声明式的描述 UI
Vue 2种描述UI的方式:
- 模板
- 对象
Q: 二者有何不同?
A: 使用JavaScript对象描述UI更加灵活
初识渲染器
Q:什么是虚拟dom?
A: 虚拟DOM就是用JavaScript对象描述真实DOM结构Q:什么是渲染器?
A:把虚拟DOM渲染为真实DOM
组件的本质
虚拟dom除了能描述真实dom之外,还能描述组件。
Q: 组件的本质是什么?
A: 组件就是一组dom元素的封装
模板工作原理
Q: 什么是编译器
二、响应式系统
响应式系统的作用与实现
响应式数据与副作用函数
副作用函数指的是会产生副作用的函数。
function effect() {
document.body.innerText = 'hello vue3'
}
响应式数据是指:
- 能够捕获该数据的变更。
- 该数据变更后能自动调用更新依赖于它的副作用函数
响应式数据的基本实现
如何才能让 obj 变成响应式数据呢?
- 当副作用函数 effect 执行时,会触发字段 obj.text 的读取操作;
- 当修改 obj.text 的值时,会触发字段 obj.text 的设置操作。
所以基本思路就是拦截一个对象的读取和设置操作,当读取字段obj.text 时,我们可以把副作用函数 effect 存储到一个“桶”里。接着,当设置 obj.text 时,再把副作用函数 effect 从“桶”里取出并执行即可。
在ES2015 之前,只能通过Object.defineProperty
函数实现,这也是 Vue.js 2 所采用的方式。在 ES2015+ 中,我们可以使用代理对象Proxy
来实现,这也是 Vue.js 3 所采用的方式。
简单例子:
// 存储副作用函数的桶
const bucket = new Set()
// 原始数据
const data = { text: "hello world" }
// 对原始数据的代理
const data = new Proxy(data, {
// 拦截读取操作
get(target, key)=>{
// 将副作用函数effect添加到桶中
bucket.add(effect)
// 返回属性值
return target[key]
},
set(target, key, newVal) {
// 设置值
target[key] = newVal;
// 把副作用从桶里提出并执行
bucket.forEach(fn => fn())
// 返回true代表操作成功
return true;
}
})
// 副作用函数
function effect() {
document.body.innerText = data.text
}
// 执行副作用函数触发读取
effect();
// 1s后修改响应式数据
setTimeout(() =>{
obj.text = 'hello vue3'
}, 1000)
当然存在很多缺陷,比如effect函数一种硬编码的方式调用。
设计一个完善的响应系统
一、框架设计概览
声明式与命令式(Declarative vs Imperative)
声明式和命令式是两种编程范式。vue/react
是声明式的,jquery那样直接操作dom是命令式
Alright here’s a metaphor.
Declarative Programming is like asking your friend to draw a landscape. You don’t care how they draw it, that’s up to them.
Imperative Programming is like your friend listening to Bob Ross tell them how to paint a landscape. While good ole Bob Ross isn’t exactly commanding, he is giving them step by step directions to get the desired result.
声明式就像你告诉你朋友画一幅画,你不用去管他怎么画的细节
命令式就像按照你的命令,你朋友一步步把画画出来
换言之
- 命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。
- 声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。
声明式与命令式的差异: 命令式更加注重过程,声明式注重结果。命令式理论上可以做到极致优化,但用户承受巨大的心智负担。声明式则牺牲一定的性能,换取代码可维护性,减轻用户的心智负担。
性能与可维护性的权衡
结论: 声明式代码的性能不优于命令式代码的性能
如果把直接修改的性能消耗定义为A,把找出差异的性能消耗定义为B,那么:
- 命令式代码的更新性能消耗 = A
- 声明式代码的更新性能消耗 = A + B
Q: Vue为什么要采用声明式的设计方案?
A:消耗一定的性能来换取更优的代码可维护性。
设计者要做到的宗旨:
在保持可维护性的同时让性能损失最小化。
虚拟DOM的性能如何
明确虚拟DOM要解决的问题:使找出差异的性能消耗最小化
运行时和编译时
- 纯运行时: 没有编译过程,没法分析内容差异进行优化
- 运行时+编译时(VUE):HTML模板 --> 编译器 --> 数据对象(虚拟DOM)--> 渲染器 --> 渲染DOM
- 纯编译时(Svelte): HTML模板 --> 编译器 --> DOM命令式代码,性能可能更好,但不够灵活
提高用户的开发体验
作者举了2个例子
- 提供友好的警告信息,比如vue中的
warn
函数。它会收集当前发生错误的组件栈信息,并向用户提供有用的信息。
warn("xxxx")
2. 自定义formatter函数,实现在开发环境中输出自定义的数据格式
控制框架代码体积
框架应该输出怎么样的构建产物
特性开关
错误处理
良好Typescript支持
Vue3的设计思路
声明式的描述 UI
Vue 2种描述UI的方式:
- 模板
- 对象
Q: 二者有何不同?
A: 使用JavaScript对象描述UI更加灵活
初识渲染器
Q:什么是虚拟dom?
A: 虚拟DOM就是用JavaScript对象描述真实DOM结构Q:什么是渲染器?
A:把虚拟DOM渲染为真实DOM
组件的本质
虚拟dom除了能描述真实dom之外,还能描述组件。
Q: 组件的本质是什么?
A: 组件就是一组dom元素的封装
模板工作原理
Q: 什么是编译器
二、响应式系统
响应式系统的作用与实现
响应式数据与副作用函数
副作用函数指的是会产生副作用的函数。
function effect() {
document.body.innerText = 'hello vue3'
}
响应式数据是指:
- 能够捕获该数据的变更。
- 该数据变更后能自动调用更新依赖于它的副作用函数
响应式数据的基本实现
如何才能让 obj 变成响应式数据呢?
- 当副作用函数 effect 执行时,会触发字段 obj.text 的读取操作;
- 当修改 obj.text 的值时,会触发字段 obj.text 的设置操作。
所以基本思路就是拦截一个对象的读取和设置操作,当读取字段obj.text 时,我们可以把副作用函数 effect 存储到一个“桶”里。接着,当设置 obj.text 时,再把副作用函数 effect 从“桶”里取出并执行即可。
在ES2015 之前,只能通过Object.defineProperty
函数实现,这也是 Vue.js 2 所采用的方式。在 ES2015+ 中,我们可以使用代理对象Proxy
来实现,这也是 Vue.js 3 所采用的方式。
简单例子:
// 存储副作用函数的桶
const bucket = new Set()
// 原始数据
const data = { text: "hello world" }
// 对原始数据的代理
const data = new Proxy(data, {
// 拦截读取操作
get(target, key)=>{
// 将副作用函数effect添加到桶中
bucket.add(effect)
// 返回属性值
return target[key]
},
set(target, key, newVal) {
// 设置值
target[key] = newVal;
// 把副作用从桶里提出并执行
bucket.forEach(fn => fn())
// 返回true代表操作成功
return true;
}
})
// 副作用函数
function effect() {
document.body.innerText = data.text
}
// 执行副作用函数触发读取
effect();
// 1s后修改响应式数据
setTimeout(() =>{
obj.text = 'hello vue3'
}, 1000)
当然存在很多缺陷,比如effect函数一种硬编码的方式调用。
设计一个完善的响应系统
原始值的响应式方案(Ref)
引入ref概念
背景:Proxy无法代理原始值,要将原始值变成响应式数据,需要对其做一层包裹
这个函数主要解决2个问题:
- 需要创建包裹对象,让Proxy代理
统一value的命名规范,不让用户随意命名
function ref(val) { // 在ref函数内部创建包裹对象 const wrapper = { value: val } // 将包裹对象变成响应式数据 return reactive(wrapper); }
接下来就是区分变量是原始包裹对线或是一个非原始值的响应式数据:
// 实现上2个变量没区别,但需要区分是不是一个ref对象
const refVal1 = ref(1)
const refVal2 = reactive({value: 1})
通过定义一个不可枚举且不可写的属性__v_isRef
属性。为true
则是ref对象。
响应式丢失问题
展开运算符...
返回一个普通对象2导致响应式丢失。
return {
...obj
}
toRef
与toRefs
就是解决上面问题的方案:
const obj = reactive({foo:1, bar: 222})
const newObj = {
foo: toRef(obj, 'foo')
bar: toRef(obj, 'bar')
}
// 或
const newObj = {...toRefs(obj)}
挂载与更新
挂载子节点与属性元素
el.setAttribute(key, vnode.props[key])
HTML Attribute与DOM Properties
HTML Attribute 指的是定义在HTML标签上的属性。
DOM Properties 指DOM对象包含很多属性。
区别:
- HTML Attribute 与 DOM Properties可能不同名,即非一一对应
- HTML Attribute的作用是设置与之对应的DOM 的Properties的初始值,一旦值改变,getAttribute函数获取还是初始值。(重点)
正确地设置元素属性
优先设置元素的DOM Properties,但当值为空字符串时,手动校正为true。有些DOM Properties是只读的,只能用setAttribute
函数来设置。
class的处理
vue中对class进行了增强,值可以是字符串,数组,对象。
因此需要序列化class的值归一化为统一的字符串形式。
设置class有三种方法: setAttribute、el.className或e.classList。
其中el.className性能最好。
卸载操作
不使用innerHTML=''的原因:
- 无法触发组件,自定义指令生命周期钩子
- 无法解除绑定挂载事件函数
通过vnode.el来获取真实DOM元素,来调用removeChild函数移除。
const el = vnode.el = createElement(vode.type);
四、组件化
组件实现原理
渲染组件
用户:一个有状态组件就是一个选项对象
渲染器:一个组件是一个特殊类型的虚拟DOM节点
const vnode = {
type: Fragment,
...
}
一个组件必须包含一个渲染函数,即render函数,并且渲染函数的返回值应该是虚拟DOM。
组件状态与自更新
props与组件的被动更新
在vue3,没有定义在props选项中的props数据将存储到attrs对象中。
props本质上是父组件数据,当props发生变化时,会触发父组件重新渲染。
mountComponent
组件初始化挂载
pathComponent
组件更新
setup函数作用与实现
setup函数主要作用是配合组合式API,为用户提供一个地方,用于创建组合逻辑、创建响应式数据、创建通用函数、注册生命周期钩子能力等能力。
返回2种情况
- 返回一个函数(render函数)
- 返回一个对象(暴露响应式数据)
组件事件与emit实现
插槽工作原理与实现
组件模板中的插槽内容内容会被编译为插槽函数,而插槽函数返回的就是具体的插槽内容。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。