3.0源码:https://github.com/vuejs/vue-next/
根据作者尤大在直播中讲过大致可以把整个vue分为三个部分来看,reactivity(vue响应式核心),compiler(将template转换成render方法),runtime(运行时的与reactivity进行响应式处理,包含自定义标签的生命周期)。
git下来之后build一下,会在package/vue/dist下创建一个vue.global.js的文件,写个单页试试。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
<script src="./dist/vue.global.js"></script>
<script>
const { createApp, reactive, watchEffect, computed } = Vue
const App = {
template:`
state.count:{{state.count}}
state.count1:{{state.count1}}
count2:{{count2}}
state.count:
`,
setup(){
const state = reactive({
count:0,
})
let count2 = computed(()=>state.count1+1)
watchEffect(()=>{
state.count1 = state.count+1
})
function click(){
state.count += 1
}
return {
state,
click,
count2
}
}
}
createApp(App).mount('#app')
</script>
</html>
核心:通过函数reactive()给对象新增一个Proxy对象监听内部的属性来实现数据监听。
首先从reactive方法开始
let toProxy = new WeakMap() //正向缓存
let toRaw = new WeakMap() //反向缓存
function reactive(target){ //构建Proxy监听对象
let observed = toProxy.has(target) //判断是否已经监听
if(observed){
return observed
}
if(toRaw.has(target)){
return target
}
observed = new Proxy(target,handle)
//handle监听方法
toProxy.set(target,observed) //缓存存储
toRaw.set(observed,target)
return observed //返回被被监听的对象,读取,修改observed时触发Proxy中的get
,set方法
}
handle:
const handle = { //Proxy监听跟设置属性
get(target, key){ //收集依赖
const res = Reflect.get(target, key)
track(target,key) //属性被读取时关联effect中的方法
return typeof res==='object'?reactive(res):res //深度监听
},
set(target, key, value){ //通知更新方法
const res = Reflect.set(target, key, value)
trigger(target,key) //属性被修改时执行effect中的方法
return res
}
}
为了实现computed和effect,必须对方法进行标记,对象修改的时候,通知执行computed和eatchEffect方法,所以需要设置一个存储方法队列的数组,并且对对象的每个属性变化都要执行一次,为了保证在数据被监听时的方法,创建一个WeakMap对象来关联target,key和对应的effect,在修改target的key。
let effectStack = []
let targetMap = new WeakMap()
function track(target,key){ //收集依赖
const effect = effectStack[effectStack.length-1] //获取当前属性被读取时执行函数
if(effect){
let depMap = targetMap.get(target) // 以下为创建关联时的判断,将方法存到targetMap中
if(depMap===undefined){
depMap = new Map()
targetMap.set(target,depMap)
}
let dep = depMap.get(key)
if(dep===undefined){
dep = new Set()
depMap.set(key, dep)
}
if(!dep.has(effect)){
dep.add(effect)
effect.deps.push(dep)
}
}
}
function trigger(target,key,info){ //触发更新
const depMap = targetMap.get(target)
if(depMap===undefined){
return
}
const effects = new Set() //保证唯一性
if(key){
let deps = depMap.get(key)
deps.forEach(effect=>{
effect()
})
}
}
然后实现effect和computed
function effect(fn,options={}){
let e = createReavtiveEffect(fn,options)
if(!options.lazy){ // 判断是否为computed,不是的话直接执行
e()
}
return e
}
function createReavtiveEffect(fn,options){
const effect = function effect(...args){ //存储之后返回这个函数被执行的函数
return run(effect,fn,args)
}
effect.deps = []
effect.lazy = options.lazy
return effect
}
function run(effect, fn, args){
if(effectStack.indexOf(effect)===-1){ //虽然已经有new Set去重了,但还是做二次判断
try{
effectStack.push(effect) //将方法存入effectStack中
return fn(...args)
}finally{
effectStack.pop() //读取完之后清除effectStack,
}
}
}
function computed(fn){ //computed就是一个后触发的effect事件,加上lazy判断
const runner = effect(fn,{lazy:true})
return { //直接读取读取不到只能获取到返回的对象
get value(){
return runner()
}
}
}
computed这里有个问题,返回的是个对象,源码的处理方式设置对象再返回value值
调用试试
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>demo</title>
</head>
<body>
<div id="app"></div>
<button id="btn" onclick="countAdd()">add</button>
<script src="./vue3.js"></script>
<script>
const root = document.getElementById('app')
const btn = document.getElementById('btn')
const obj = reactive({
count:0,
})
let obj1 = computed(()=>obj.count*2)
effect(()=>{
root.innerHTML = `
count:
${obj.count}-${obj1.value}
`
})
function countAdd(){
obj.count+=1
}
</script>
</body>
</html>