阅读Vue3源码,进行简单手写实现
如果想要阅读源码,请阅读我的下一篇文章 vue3源码(梳理阅读)
了解虚拟DOM的优势
先对真实的元素节点进行抽象,抽象成为虚拟节点(VNode)
因为对真实DOM进行操作很不方便,而Vnode就是js对象,操作起来很方便.
VNode方便实现跨平台,将其渲染成为你想要的任意节点(Android,ios...)
template (通过compiler编译)->render函数进行渲染(调用h函数)->虚拟节点->真实dom(解析)->浏览器展示
源码包含三大核心
手写h函数,进行挂载,并进行patch(diff算法)
// 挂载到#app上
<div id="app"></div>
<script src="./renderer.js"></script>
<script>
// 1.通过h函数来创建一个Vnode
const vnode1 = h("div",{class:'hao'},[
h("h2",null,"Fhup"),
h("button",{onClick:()=>{console.log("你点击了按钮!!");}},"点击")
])
// vdom->很多的vnode组成vdom
console.log(vnode1);
// 2.通过mount函数将vnode1进行挂载
mount(vnode1,document.querySelector("#app"))
setTimeout(() => {
// 3.创建新的vnode
const vnode2 = h("div",{class:'fhup'},[
h("h2",null,"美美美"),
h("button",{onClick:()=>{console.log("美美美!!");}},"点击")
])
// 4.两个虚拟dom做对比(diff算法),只更新变化的
patch(vnode1,vnode2)
}, 2000);
</script>
// h函数的实现
const h=(tag,props,children)=>{
// VNode->js对象=>{}
// 返回vnode
return{
tag,
props,
children
}
}
const mount = (vnode,container)=>{
// 将vnode转为真实的dom
// 1.创建真实的dom,并且在vnode保留el
const el = vnode.el = document.createElement(vnode.tag)//vnode.el js会动态添加el属性
// 2.处理props
if(vnode.props){
for(const 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)
}
}
}
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)
}
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中进行保存
// 注意:n2之前没有el
const el = n2.el= n1.el
// 2.处理props
const oldprops=n1.props || {}
const newprops=n2.props || {}
if(newprops){
for(const key in newprops){
const newValue=newprops[key]
const oldValue=oldprops[key]
//相同不用管,不相同添加上
if(newValue!=oldValue){
if(key.startsWith("on")){//对监听事件的判断
el.addEventListener(key.slice(2).toLowerCase(),newValue)
}else{
el.setAttribute(key,newValue)
}
}
}
}
// 3.删除旧的props
if(oldprops){
for(const key in oldprops){
if(!(key in newprops)){
if(key.startsWith("on")){//对监听事件的判断
el.removeEventListener(key.slice(2).toLowerCase(),oldprops[key])
}else{
el.removeAttribute(key)
}
}
}
}
//4.处理children
const oldChildren = n1.children || []
const newChildren = n2.children || []
if(typeof newChildren === "string"){//情况一:对children是字符串的处理
// 边界判断(edge case)
if(typeof oldChildren === "string"){
if(oldChildren !== newChildren){
el.innerHTML = newChildren
}
}else{
el.innerHTML = newChildren
}
}else{//情况二:不是String,是数组
if(typeof oldChildren === "string"){
el.innerHTML=""
newChildren.forEach(item=>{
mount(item,el)
})
}else{
//二个children都是数组的话
// oldChildren:[v1,v2,v3,v8,v9]
// newChildren:[v1,v5,v6]
// 1.前面有相同节点的元素进行patch操作
const minlength=Math.min(oldChildren.length,newChildren.length)
for(let i=0;i<minlength;i++){
patch(oldChildren[i],newChildren[i])
}
if(oldChildren.length>newChildren.length){
oldChildren.slice(newChildren.length).forEach(item=>{
el.removeChild(item.el)
})
}else{
newChildren.slice(oldChildren.length).forEach(item=>{
mount(item,el)
})
}
}
}
}
}
手写reactivity函数,实现响应式.
// 实现思路: 将所有包含info的函数进行收集,值变化一次,收集到的函数执行一次
// 简单理解为发布订阅者模式
// 定义依赖类dep
class Dep {
constructor(){// 构造器
this.subscribes=new Set()// 默认创建set集合
}
// 收集依赖函数
depend(){
if(activeEffect){
this.subscribes.add(activeEffect)
}
}
// 值变化进行通知,执行函数
notify(){
this.subscribes.forEach(effect=>{
effect()
})
}
}
// 是否进行响应式,进行判断
let activeEffect = null
function watchEffect(effect){
activeEffect = effect
effect()// 默认执行一次
activeEffect = null
}
// Map :key是一个字符串
// WeakMap :key是一个对象,弱引用
const targetMap = new WeakMap()
function getDep(target,key) {//({counter:100},counter)
// 1.根据target取出对应的map对象
let depMap=targetMap.get(target)
if(!depMap){
depMap=new Map()
targetMap.set(target,depMap)
}
// 2.取出具体的dep对象
let dep=depMap.get(key)
if(!dep){
dep = new Dep()
depMap.set(key,dep)
}
return dep
}
// vue2对raw进行数据劫持
// function reactive(raw){
// // 对raw里面所有的key进行劫持
// 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
// }
// vue3对raw进行数据劫持
// 选择Proxy,因为Proxy劫持的是整个对象
// vue2对象新增时,需要再次调用defineProperty
function reactive(raw){
// 返回代理对象
return new Proxy(raw,{// 所有的修改,代理到raw对象中
//之后执行set()和get()
get(target, key){//target就是raw对象
const dep=getDep(target,key)
dep.depend()
return target[key]// 不会出现引用问题
},
set(target, key, newValue){
const dep=getDep(target,key)
target[key]=newValue
dep.notify()
},
})
}
const info = reactive({counter: 100 , name: "Fhup"})
const foo= reactive({height: 1.88})
watchEffect(function(){
console.log("effect1", info.counter * 2, info.name);
})
watchEffect(function(){
console.log("effect2", info.counter * info.counter, info.counter);
})
watchEffect(function(){
console.log("effect3", info.counter + 10, info.name);
})
watchEffect(function(){
console.log("effect4", foo.height);
})
// info.counter++
// info.name="Fhup"
foo.height=2
将上面二个手写的函数进行合并,实现mini-vue
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="renderer.js"></script>
<script src="reactivity.js"></script>
<script src="createapp.js"></script>
<script>
// 1.创建根组件
const App = {
data: reactive({
counter: 0
}),
render() {
return h("div", null, [
h("h2", null, `当前计数: ${this.data.counter}`),
h("button", {
onClick: () => {
this.data.counter++
console.log(this.data.counter);
}
}, "+1")
])
}
}
// 2.挂载根组件
const app = createApp(App);
app.mount("#app");
</script>
</body>
</html>
// createapp.js
function createApp(rootComponent) {
return {
mount(selector) {
const container = document.querySelector(selector);
let isMounted = false;
let oldVNode = null;
watchEffect(function() {
if (!isMounted) {// 默认进行挂载
oldVNode = rootComponent.render();
mount(oldVNode, container);
isMounted = true;
} else {// 数据改变进行patch(diff算法)
const newVNode = rootComponent.render();
patch(oldVNode, newVNode);
oldVNode = newVNode;
}
})
}
}
}