前言
DEMO地址,先下载DEMO,打开index.html体验一下吧
- 阅读的时候最好是先把DEMO过一遍,然后带着问题来看这篇文章,不然可能会一脸懵逼
- 我的DEMO源码简化了非常非常多的代码,所以功能很基础,为了方便把流程先理清楚
- 不要死抓住细节不放,有些问题放一放,等把握全局之后就可以理解了
- 可以下载DEMO源码,打开里面index.html先体验下
灵魂图
先上一张灵魂图,可能看完图就要跑了。。。为了讲清楚画的比较乱
总体介绍
从上图中可以大致看出,分为两条线🧵
第一条线:
new Vue -> reactive[new Proxy -> return proxy] -> baseHandler(set, get) -> effect文件中的依赖收集(track,trigger)-> trigger触发任务调度 -> 完了
第一条线遗留几个问题:
- 依赖是什么时候生成的?
- 依赖(dom和data之间的关系)是如何形成的?
先保留这两个问题,继续看第二条线
第二条线
从根路径app开始解析子节点 -> 解析出当前node,node里面的指令{{name}} -> 实例化effect(传入回调函数:回调里面去proxy上面获取name) -> 调用effect
第二条线的重点
调用effect主要做两件事:
- 把当前effect push进activeReactiveEffectStack
- 执行回调,回调函数里面从proxy获取name
- 【从proxy获取name】这一步会触发proxy的get
- get里面从activeReactiveEffectStack获取最后一个依赖,进行依赖收集
源码解析
reactive
import { mutableHandlers } from './baseHandler'
// 这个map存储key: target, value:proxy
// 作用:
// 1.避免重复proxy
const rawToReactive = new WeakMap()
// 这个map存储key:proxy, value:target
// 作用:
// 1.避免proxy对象再次被proxy
const reactiveToRaw = new WeakMap()
export const targetMap = new WeakMap()
export function reactive(target){
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers
)
}
// 创建响应式对象
function createReactiveObject(target, toProxy, toRaw, handlers){
// 如果当前对象已被proxy,那么直接返回
let observed = toProxy.get(target)
if (observed !== void 0) {
return observed
}
// 检测被proxy的对象,即这里的target,自身是否是个proxy,如果是的话,直接返回
if (toRaw.has(target)) {
return target
}
// 当前的target既没有被proxy,也不是个proxy对象,那么对它proxy
observed = new Proxy(target, handlers)
// 实例化之后把它维护到两个map
toProxy.set(target, observed)
toRaw.set(observed, target)
// 把当前的target维护到targetMap,targetMap的作用 -> 【继续往下看,先不管】
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}
// toRaw函数,传入proxy对象,获取target
//
export function toRaw(observed) {
return reactiveToRaw.get(observed) || observed
}
// Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/
reactive的作用
可以看出reactive的作用主要是:
- 创建proxy实例并返回
- 维护一个targetMap队列
reactive的遗留问题
- targetMap是干嘛用的?
- handler来自于baseHandler
ok,这两个疑问🤔️先保留,继续看baseHandler的源码
baseHandler
import { toRaw } from './reactive'
import { track, trigger } from './effect'
// 为了便于理解,这里只做了get和set的proxy
// 其他的代码都是一般的代理,不讲,讲一下track和trigger
// 从vue 2.0的源码其实可以知道:
// 1.get的时候会做依赖收集:即这里的track
// 2.set的时候会做更新广播:即这里的trigger
export const mutableHandlers = {
get(target, key, receiver){
const res = Reflect.get(target, key, receiver)
track(target, 'get', key)
return res
},
set(target, key, value, receiver){
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 这里检测key是否是target的自有属性
const hadKey = target.hasOwnProperty(key)
// 在reactive维护了一个reactiveToRaw队列,存储了[proxy]:[target]这样的队列,这里检测下是否是使用createReactiveObject新建的proxy
if (target === toRaw(receiver)) {
// 判断是否值改变,才触发更新
if (hadKey && value !== oldValue) {
trigger(target, 'set', key)
}
}
return result
}
}
// Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/
baseHandler的作用
- get获取值,其次依赖收集
- set设置值,其次触发任务调度
baseHandler的遗留问题
- 什么是依赖?
- 收集的依赖中,dom和data的关系是怎样的
- 如何做任务调度?
effect
重点来了,effect就是vue3里面用于依赖管理的,主要是管理三个东西:
- 依赖收集
- 依赖实例化
- 依赖存储
import { targetMap } from './reactive'
const activeReactiveEffectStack = []
// 下面这两个api是初始化effect,就不过于纠结了
export function effect(fn, options){
const effect = createReactiveEffect(fn, options)
return effect
}
function createReactiveEffect(fn, options){
const effect = function(){
if (!activeReactiveEffectStack.includes(effect)) {
try {
activeReactiveEffectStack.push(effect)
return fn()
} finally {
activeReactiveEffectStack.pop()
}
}
}
effect.scheduler = options.scheduler
return effect
}
// 作用:
// 1.收集依赖
export function track(target, type, key){
const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
// proxy初始化的时候,这个depsMap为new Map
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
// 如果是第一次这个dep是没有的,因为depsMap是new Map
let dep = depsMap.get(key)
if (dep === void 0) {
// 这里把依赖放进去。依赖是个Set
depsMap.set(key, (dep = new Set()))
}
// 这里的effect就是依赖。
// 依赖是啥?可以理解为依赖保存了data <-> dom的关系
dep.add(effect)
// effect.deps.push(dep)
}
// 作用:
// 1.触发了数据更新,这时候得更新dom了
export function trigger(target, type, key){
const depsMap = targetMap.get(target)
const effects = new Set()
const run = effect => {
scheduleRun(effect, target, type, key)
}
// 解析出依赖中要更新的effect
addRunners(effects, depsMap.get(key))
// 任务调度执行
effects.forEach(run)
}
function addRunners(effects, effectsToAdd){
effectsToAdd.forEach(effect => {
effects.add(effect)
})
}
// 任务调度,就理解为data更新之后,调用effect.scheduler去更新dom
function scheduleRun(effect, target, type, key){
if (effect.scheduler !== void 0) {
effect.scheduler(effect)
} else {
effect()
}
}
// Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/
effect重点
- 依赖收集用到两个东西:activeReactiveEffectStack,targetMap
- 触发依赖也用到两个:targetMap,scheduler的queueJob
可以看出:1.targetMap是用来存储依赖的
继续看下scheduler的queueJob任务调度
scheduler
import { callWithErrorHandling } from './errorHandling'
const queue = []
const p = Promise.resolve()
let isFlushing = false
export function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job)
if (!isFlushing) {
nextTick(flushJobs)
}
}
}
export function nextTick(fn) {
return fn ? p.then(fn) : p
}
function flushJobs(seenJobs) {
isFlushing = true
let job
while ((job = queue.shift())) {
job()
}
isFlushing = false
}
// Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/
scheduler queueJob
queueJob主要是利用了Promise来进行一个微任务队列的依赖更新:其实执行effect实例函数
第一条线内容结束语
到这里第一线的内容就完了,遗留一个问题:
- 在get的时候从activeReactiveEffectStack的最后一个取依赖
这说明啥?
说明在调用了effect -> 把effect push进activeReactiveEffectStack 之后,需要调用proxy[name]来触发get
明白了这一点之后,继续来看第二条线,第二条线的compile内容属于自研,跟源码差距比较大
compile
import { effect } from './effect'
import { queueJob } from './scheduler'
export function compile(el, vm){
let fragment = document.createDocumentFragment();
let node;
while(node = el.firstChild){
compileNode(vm, node)
fragment.append(node)
}
return fragment
}
const reg = /\{\{(.*)\}\}/;
function compileNode(vm, node){
let { nodeType, nodeValue, nodeName } = node;
node.update = (type, bindName) => {
return effect(() => {
node[type] = vm[bindName]
}, { scheduler: queueJob })
}
let bindName;
switch(nodeType){
case 1:
if(nodeName == 'INPUT'){
let { attributes } = node;
for(let attr of attributes){
if(attr.name === 'v-model'){
bindName = attr.value;
}
}
if(bindName){
node.addEventListener('input', e => {
vm[bindName] = e.target.value;
})
}
node.update('value', bindName)()
}
break;
case 3:
let isModal = reg.test(nodeValue)
if(isModal){
bindName = RegExp.$1 && RegExp.$1.trim();
node.update('nodeValue', bindName)()
}
break;
}
}
// Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/
compile的重点
重点在于:当解析出node和bingName之后,其实这时候可以调用proxy[name]来直接获取值了
-> 可是这里创建了个effect来建立key和当前node之间的关系
- effect的回调是用来调用node[’nodeValue’] = proxy[’name’]来触发get收集依赖的
- 当set name使name改变之后,查询当前key下面的effect队列,调用各个effect的回调更新dom
写在最后
因为时间比较紧,所以写得很仓促,自己也感觉文章写的比较乱,单独看文章的话可能会看不懂。需要先把DEMO过一遍,然后带着问题来看这篇文章
作者:Houdini
链接:https://juejin.im/post/6844903969156923405
看完两件小事
如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:
- 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
- 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程