实现一个Mini-Vue可以加深对vue的理解 ,包括渲染系统模块、可响应式系统模块、 应用程序入口模块(没有实现编译系统模块) 。
vue核心模块
vue有三个核心模块 ,编译系统,渲染系统, 响应式系统。
编译系统
- 将 temp性能优化的方法late 模板解析算法的五个特性成抽象语法树(AST)。算法的时间复杂度取决于
- 对 AST 做优化处理。
- 根据 AST 生成 render 函数。
渲染系统
- reder函数返回vnode
- vnode之间算法是什么会形成树结构vdom
- 根据vdom生成真实dom,渲染到浏览器
响应式系统
- 将新旧vnode利用diff算法进行对比
- 渲染系统根据vnode重新生成dom,渲染到浏览器
渲染系统
通过h函数生成vnode
h函数包括3个参数,元素, 属性, 子元素。生html简单网页代码成的vnode是一个javascript对象
function h(tag, props, children) {
// vnode --> javascript对象
return {
tag,
props,
children
}
}
// 1.通过h函数创建vnode
const vnode = h("div", {class: 'lin'}, [
h("span", null, '我是靓仔'),
h("button", {onClick: function() {}}, 'change')
])
文档: H函数
通过mount函HTML数, 将vnode挂载到div#app上
mount(vnode, document.querySelector("#app"))
const h = (tag, props, children) => {
// vnode --> javascript对象
return {
tag,
props,
children
}
}
const mount = (vnode, container) => {
// 1. 创建出真实的元素, 并且在vnode上保存el
const el = vnode.el = document.createElement(vnode.tag)
// 2. 处理props
if (vnode.props) {
for (let key in vnode.props) {
const value = vnode.props[key]
if (key.startsWith("on")) { // 是否是事件
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value)
}
}
}
// 3. 处理子元素 字符串,数组
if (vnode.children) {
if (typeof vnode.children === "string") {
el.textContent = vnode.children
} else {
// 数组 多个子元素 递归
vnode.children.forEach(element => {
mount(element, el)
})
}
}
// 4.将el挂载到container上
container.appendChild(el)
}
patch对比新旧节点
当节点发生变化时, 对比新旧节点并进行更新。以新的VNode为基准,改造旧的oldVNode使之成为跟新的VNode一样(打补丁javascript是干什么的)
setTimeout(() => {
const vnode2 = h("div", {class: 'jin', onClick: function() {console.log("我是靓仔")}}, [
h("button", {class: "zhang"}, '按钮')
])
patch(vnode, vnode2)
}, 2000)
const patch = (n1, n2) => {
// n1旧 n2新
// 如果父元素不一样, 直接替换
if (n1.tag !== n2.tag) {
// 获取父元素
const n1Elparent = n1.el.parentElement
// 移除旧节点
n1Elparent.removeChild(n1.el)
// 重新挂载新节点
mount(n2, n1Elparent)
} else {
// 引用, 修改一个另一个也会改变, n1.el 在n1挂载(mount)时赋值
const el = n2.el = n1.el
// 对比props
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 把新的props添加到el上
for (let key in newProps) {
const oldValue = oldProps[key]
const newValue = newProps[key]
if (newValue !== oldValue) {
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), newValue)
} else {
el.setAttribute(key, newValue)
}
}
}
// 删除旧的props 移除监听器, 属性
for (let key in oldProps) {
if (key.startsWith("on")) {
el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key])
} else {
if (!key in newProps) {
el.removeAttribute(key)
}
}
}
// 对比children
// children 可能是字符串, 数组, 对象(插槽), 字符串跟数组比较常见
// n1 [v1, v2, v3, v4, v5]
// n2 [v1, v7, v8]
const oldChildren = n1.children || []
const newChidlren = n2.children || []
if (typeof newChidlren === 'string') { // 如果newChidlren是字符串
if (typeof oldChildren === "string") {
if (newChidlren !== oldChildren) {
// textContent属性表示一个节点及其后代的文本内容
el.textContent = newChidlren
}
} else {
// innerHTML 返回 HTML textContent 通常具有更好的性能,因为文本不会被解析为HTML 使用 textContent 可以防止 XSS 攻击。
el.innerHtml = newChidlren
}
} else { // 如果newChidlren是数组
const oldLength = oldChildren.length
const newLength = newChidlren.length
const minLength = Math.min(oldLength, newLength)
// 先对比相同长度的部分
for (let i = 0; i < minLength; i++) {
patch(oldChildren[i], newChidlren[i])
}
// 如果新的比较长, 则mount新增的节点
if (newLength > oldLength) {
newChidlren.slice(minLength).forEach(item => {
mount(item, el)
})
}
// 如果旧的比较长, 则移除多余节点
if (newLength < oldLength) {
oldChildren.slice(minLength).forEach(item => {
el.removeChild(item.el)
})
}
}
}
}
响应式系统
当数据发生改变的时候,所有使用到数据的地方也应该发生改变
let obj = {
name: 'lin'
}
const change = () => {
console.log('输出为:', obj.name)
}
change()
obj.name = 'jin'
// 当obj发生变化时,有使用到到obj的地方也会发生相应的改变
change()
定义依赖收集类
class Depend {
constructor() {
// Set对象允许你存储任何类型的唯一值,不会出现重复
this.reactiveFns = new Set()
}
addDepend(reactiveFn) {
this.reactiveFns.add(reactiveFn)
}
notify() {
this.reactiveFns.forEach(item => {
item()
})
}
}
let obj = {
name: 'lin'
}
const change = () => {
console.log('输出为:', obj.name)
}
const dep = new Depend()
dep.addDepend(change)
obj.name = 'jin'
dep.notify()
自动监听对象的变化
每次对象发生改变我们都要重新调用页面性能优化一次notify
方法, 我们可以使算法导论用 proxy来监听对象的变化。
响应式函数
并不是每个函数都需要变成浏览器怎么打开网站响应式函数,我们javascript面试题可以定义一个函数来接收需要转化成响应式的函数。
let activeReactiveFn = null
function watchFn(fn) {
activeReactiveFn = fn
fn() // 调用一次触发get(看下面代码)
activeReactiveFn = null
}
监听对象的变化
vue2跟vue3实现方式不同:
- vue2使用 Object.defineProperty() 劫持对象监听数据的变化
- 不能监听数组的变化
- 必须遍历对象的每个属性
- 必须深层遍历嵌套的对象
- vue3使用 proxy 监听对象浏览器历史记录设置的变算法的五个特性化
- 针对对象:针对整个对象,而不是对象的某个属性,所以也就不需要对 keys 进行遍历。
- 支持数组:Prox算法工程师y 不需要对数组的方法进行重载,省去了众多 hack,页面性能优化减少代码量等于减少了维护成本,而且标准的就是最好的。
- Proxy 的第二个参数可javascript面试题以有 13 种拦截方法,这比起 Object浏览器的历史.defineProperty() 要更加丰富
- Proxy 作为新标准受到浏览器厂商的性能优化的方法重点关注和性能优化,相比之下 Object.defineProperty() 是一个已有的老方法。
const reactive = (obj) => {
let depend = new Depend()
// 返回一个proxy对象, 操作代理对象, 如果代理对象发生变化, 原对象也会发生变化
return new Proxy(obj, {
get: (target, key) => {
// 收集依赖
depend.addDepend()
return Reflect.get(target, key)
},
set: (target, key, value) => {
Reflect.set(target, key, value)
// 当值发生改变时 触发
depend.notify()
}
})
}
// 修改 Depend类中的addDepend方法
// addDepend() {
// if (activeReactiveFn) {
// this.reactiveFns.push(activeReactiveFn)
// }
// }
let obj = {
name: 'lin'
}
let proxyObj = reactive(obj)
const foo = () => {
console.log(proxyObj.name)
}
watchFn(foo)
proxyObj.name = 'jin'
文档:
- Reflect
正确收集依赖
每当我们改变代理对象(vue2对象)的时候,比如我们新增一个age
属性,即使chan算法的有穷性是指ge
函数里面没有使用到age
, 我们也会触发change
函数。 所以我们要正确收集依赖,怎样正确收集依赖呢。
- 不同算法导论的对象单独存储
- 同一个对象不安卓性能优化同属性也要单独存储
- 存储对象我们可以使用 WeakMap
WeakMap
对象是一组算法分析的目的是键/值对的集合,其中的键是弱引用(原对象销毁的时候可以被垃圾回收javascript:void(0))的。其键必须是对象
,而值可以是任意的。
- 存储对象不同属性可以使用 Map
Map
对象保存键浏览器的历史记录在哪值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值) 都可以作为一javascriptdownload个键或一个值。
const targetMap = new WeakMap()
const getDepend = (target, key) => {
// 根据target对象获取Map
let desMap = targetMap.get(target)
if (!desMap) {
desMap = new Map()
targetMap.set(target, desMap)
}
// 根据key获取 depend类
let depend = desMap.get(key)
if (!depend) {
depend = new Depend()
desMap.set(key, depend)
}
return depend
}
const reactive = (obj) => {
return new Proxy(obj, {
get: (target, key) => {
// 收集依赖
const depend = getDepend(target, key)
depend.addDepend()
return Reflect.get(target, key)
},
set: (target, key, value) => {
const depend = getDepend(target, key)
Reflect.set(target, key, value)
// 当值发生改变时 触发
depend.notify()
}
})
}
应用程序入口模块
新建一个html文件,把浏览器怎么打开网站创建浏览器的历史记录在哪的函数所在的js文件都引进来
<script>
// 创建根组件
const App = {
// 需要进行响应式的数据
data: reactive({
counter: 0
}),
render() {
// h函数渲染节点
return h("div", {class: 'lin'}, [
h("div", {class: 'text'}, `${this.data.counter}`),
h("button", {onClick: () => {
this.data.counter++
console.log(this.data.counter)
}}, '+')
])
}
}
// 挂载根组件
const app = createApp(App)
app.mount("#app")
</script>
新建一个js文件保存createApp
函数,这个函数返回一个对象,对象里面有一个mount
方法
const createApp = (rootComponent) => {
return {
mount(selector) {
const container = document.querySelector(selector)
// 响应式函数
watchEffect(function() {
const vNode = rootComponent.render()
// 把节点挂载到 #div
mount(vNode, container)
})
}
}
}
这样汽车性能优化有一点问题,每次点击+
按钮都js性能优化会新增节点
第一次挂载(mount) –> 值改变 –> patch
const createApp = (rootComponent) => {
return {
mount(selector) {
const container = document.querySelector(selector)
// isMounted 是否已经挂载
let isMounted = false
let oldVNode = null
watchEffect(function() {
if (!isMounted) {
oldVNode = rootComponent.render()
// 第一次挂载
mount(oldVNode, container)
isMounted = true
} else {
const newVNode = rootComponent.render()
// 对比新旧节点
patch(oldVNode, newVNode)
oldVNode = newVNode
}
})
}
}
}
写的很菜,等我变秃了再回来改javascript:void(0)进改进!!!
参考文档
王红元 《深入vue3 + typescript》