跟着 coderwhy 的vue3视频 一步一步实现的javascript
mini-vue 源码连接html
Vue源码包含三大核心:前端
实现一个简洁版的 Mini-Vue, 包含三个模块:vue
在传统的前端开发中,咱们编写本身的HTML,最终被渲染到浏览器上。java
而目前框架都会引入虚拟DOM来对真实的DOM进行抽象,这样作有不少的好处node
该模块主要包含三个功能:react
h 函数用于返回一个VNode对象ios
mount函数: 用于将VNode挂载在DOM上git
patch函数: 用于俩个VNode进行对比,判断如何处理新的VNodegithub
h函数的做用就是 生成VNode, 而vnode本质上是一个JavaScript对象
实现一个h 函数很简单,直接返回一个VNode对象便可
const h = (tag, props, children) => {
return {
tag,
props,
children
}
}
复制代码
mount 函数的做用就是 挂载VNode, 将vnode挂载DOM元素上并显示在浏览器上
实现思路:
根据 tag , 建立HTML元素,而且存到 vnode的el中 (目前只考虑 标签 ,不考虑组件)
处理 props 属性 (目前只考虑俩种状况)
处理子节点(只考虑俩种状况:字符串和数组)
代码以下:
const h = (tag, props, children) => {
return {
tag,
props,
children
}
}
const mount = (vnode, container) => {
// 1. 建立 html 元素
const el = vnode.el = document.createElement(vnode.tag)
// 2. 处理 props属性
if (vnode.props) {
for (const key in vnode.props) {
if (!vnode.props.hasOwnProperty(key)) {return}
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(item => {
mount(item, el)
})
}
}
container.appendChild(el)
}
复制代码
这样就能实现简单的渲染啦~
<body>
<div id="app"></div>
<script src="renderer.js"></script>
<script> const vnode = h('div', { class: 'wangpf' }, [ h('h2', { class: 'title' }, 'hello,I am wangpf'), h('div', null, '当前计数:100'), h('button', { class: 'btn' }, '+1') ]) mount(vnode, document.querySelector('#app')) </script>
</body>
复制代码
patch 函数做用就是 对比俩个新旧vnode,将不一样的给替换掉,运用到了 diff 。
对 patch 函数的实现,分为俩种状况 (n1为旧的vnode,n2为新的vnode)
代码实现:
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) {
const n1ElParent = n1.el.parentElement
n1ElParent.removeChild(n1.el)
mount(n2, n1ElParent)
} else {
// 1.取出 element对象,而且在 n2中进行保存
const el = n2.el = n1.el
// 2. 处理 props
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 2.1 获取全部的 newProps 添加到 el
for (const key in newProps) {
if (!newProps.hasOwnProperty(key)) {return}
const oldValue = oldProps[key]
const newValue = newProps[key]
if (oldValue !== newValue) {
if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), newValue)
} else {
el.setAttribute(key, newValue)
}
}
}
// 2.2 删除旧的props
for (const key in oldProps) {
if (!newProps.hasOwnProperty(key)) {return}
if (key.startsWith('on')) {
el.removeEventListener(key.slice(2).toLowerCase())
}
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
// 3. 处理 children
const oldChildren = n1.children || []
const newChildren = n2.children || []
if (typeof newChildren === 'string') { // 状况一
if (typeof oldChildren === 'string') {
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.innerHTML = newChildren
}
} else { // 状况二:newChildren是个数组
if (typeof oldChildren === 'string') {
el.innerHTML = ''
newChildren.forEach(item => {
mount(item, el)
})
} else {
// 若是都是数组
// oldChildren : [v1,v2,v3]
// newChildren : [v1,v5,v7,v8,v9]
const commonLength = Math.min(newChildren.length, oldChildren.length)
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i])
}
if (newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren.length).forEach(item => {
mount(item, el)
})
}
if (newChildren.length < oldChildren.length) {
oldChildren.slice(newChildren.length).forEach(item => {
el.removeChild(item.el)
})
}
}
}
}
}
复制代码
<body>
<div id="app"></div>
<script src="renderer.js"></script>
<script> // 1.经过 h 函数建立一个 vnode const vnode = h('div', { class: 'wangpf', id: 'aaa' }, [ h('h2', { class: 'title' }, 'hello,I am wangpf'), h('div', null, '当前计数:100'), h('button', { class: 'btn' }, '+1') ]) // 2. 经过 mount 函数, 将 vnode 挂载在div#app上 mount(vnode, document.querySelector('#app')) // 3. 建立新的 vnode setTimeout(() => { const newVnode = h('div', { class: 'pf', id: 'aaa' }, [ h('h2', { class: 'title' }, 'hello,I am wangpf'), h('div', null, '当前计数:0'), h('button', { class: 'btn222' }, '-1') ]) patch(vnode, newVnode) }, 2000) </script>
</body>
复制代码
当定时器达到2s后, 新的vnode 会替换掉旧的vnode,经过 ptach 函数来diff出不一样的地方进行替换。
大体上这就这样简单的实现一下渲染系统模块,分别有 h函数(返回vnode对象)、mount函数(用于挂载到页面上)、patch函数(对比新旧vnode,更新为最新的)
响应式模块是vue的重中之重,vue2版本是经过 Object.defineProperty
来进行对数据进行依赖收集劫持的 , vue3版本是经过 proxy
来实现的
换为 proxy
缘由在于 defineProperty
这个API虽然兼容性好,可是不能检测到对象和数组的变化,好比对对象的新增属性,咱们须要去手动的给该属性收集依赖(经过**$set**),才能实现响应式。 对于 proxy
来讲, Proxy 是劫持的整个对象,不须要作特殊处理 (我以为这个为何换为 proxy 的根本缘由)
雏形的响应式系统: 发布订阅的思想
// 响应式系统模块
class Dep {
constructor() {
this.subscribers = new Set()
}
addEffect(effect) {
this.subscribers.add(effect)
}
notify() {
this.subscribers.forEach(effect => {
effect()
})
}
}
const info = { counter: 100 }
const doubleCounter = () => {
console.log(info.counter * 2)
}
const multiplyCounter = () => {
console.log(info.counter * info.counter)
}
const dep = new Dep()
dep.addEffect(doubleCounter)
dep.addEffect(multiplyCounter)
setInterval(() => {
info.counter++
dep.notify()
}, 2000)
复制代码
上述代码有不少不足之处,只要数据发生变化就得手动去调用。
咱们但愿数据只要一发生变化,那么就自动的去收集依赖并执行
因此改进了以下:
const dep = new Dep()
const watchEffect = (effect) => {
dep.addEffect(effect)
}
const info = { counter: 100 }
watchEffect(() => {
console.log(info.counter * 2)
})
watchEffect(() => {
console.log(info.counter * info.counter)
})
setInterval(() => {
info.counter++
dep.notify()
}, 2000)
复制代码
我就用 watchEffect 来统一管理它, 只不过须要在 watchEffect 函数中执行逻辑。
但这仍是有些不足,好比不知道是谁的逻辑,并且并非自动收集依赖
所以,再次进行改进,以下:
// 响应式系统模块
class Dep {
constructor() {
this.subscribers = new Set()
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach(effect => {
effect()
})
}
}
const dep = new Dep()
let activeEffect = null
const watchEffect = (effect) => {
activeEffect = effect
dep.depend()
activeEffect = null
}
const info = { counter: 100 }
watchEffect(() => {
console.log(info.counter * 2)
})
watchEffect(() => {
console.log(info.counter * info.counter)
})
setInterval(() => {
info.counter++
dep.notify()
}, 2000)
复制代码
用 depend 来取代替 addEffect , 这样作的目的是 不须要去知道 subscribers 添加的具体是什么
可是呢, 这样作会使得对 info 整个有依赖, 若是我想监听 info 的某一个属性,全部咱们须要有一个数据劫持的方法来实现。
这时候就能够用vue2响应式原理的思想来实现了, 经过 Object.defineProperty
(Vue2响应式原理的核心)
// 响应式系统模块
class Dep {
constructor() {
this.subscribers = new Set()
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach(effect => {
effect()
})
}
}
let activeEffect = null
const watchEffect = (effect) => {
activeEffect = effect
effect() // vue3 中 watchEffect 就会默认执行一次
activeEffect = null
}
const targetMap = new WeakMap()
const getDep = (target, key) => {
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Dep()
depsMap.set(key, dep)
}
return dep
}
// vue2 数据劫持原理
const reactive = (raw) => {
Object.keys(raw).forEach(key => {
const dep = getDep(raw, key)
let value = raw[key];
Object.defineProperty(raw, key, {
get() {
dep.depend()
return value
},
set(newValue) {
if (value !== newValue) {
value = newValue
dep.notify()
}
}
})
})
return raw
}
复制代码
实现效果:
const info = reactive({ counter: 100 })
watchEffect(() => {
console.log(info.counter * 2)
})
watchEffect(() => {
console.log(info.counter * info.counter)
})
info.counter++
// 70 10000
// 67 202
// 70 10201
复制代码
defineProperty
已经说过了,因此咱们可使用proxy
对 reactive 函数进行重构
// vue3 proxy 数据劫持
const reactive = (raw) => {
return new Proxy(raw, {
get(target, p, receiver) {
const dep = getDep(target, p)
dep.depend()
return target[p]
},
set(target, p, newValue, receiver) {
const dep = getDep(target, p)
target[p] = newValue
dep.notify()
}
})
}
复制代码
上述已经实现了 渲染系统模块和响应式系统模块,接下来咱们就差最后一步了,模仿一下vue3 使用 createApp函数 做为入口 以及mount函数将其挂载到页面上
从框架的层面来讲,咱们须要有俩部份内容:
const createApp = (rootComponent) => {
return {
mount(selector) {
let isMounted = false
let preVnode = null
watchEffect(() => {
// 首次须要挂载, 后边就 patch
if (!isMounted) {
preVnode = rootComponent.render()
mount(preVnode, document.querySelector(selector))
isMounted = true
} else {
const newVnode = rootComponent.render()
patch(preVnode, newVnode)
preVnode = newVnode
}
})
}
}
}
复制代码
// 1. 建立根组件
const App = {
data: reactive({
counter: 0
}),
render() {
return h('div', null, [
h('h2', null, `计数:${this.data.counter}`),
h('button', {
onClick: () => {this.data.counter++}
}, '+1')
])
}
}
// 2. 挂载根组件
createApp(App).mount('#app')
复制代码
点击便可完成加一操做!