//... 代表删了很多非必要逻辑代码
本章小知识:vue3响应大纲:
读值时的跟踪:proxy 的 get 处理函数中 track 函数记录了该 property 和当前副作用。
检测该值何时发生变化:在 proxy 上调用 set 处理函数。
触发函数以便它可以更新最终值:trigger 函数(从target的depsMap中)查找哪些副作用依赖于该 property 和它们的执行函数。
异步从队列中执行副作用函数
本章小知识:各个proxy
createReactiveEffect :Vue3响应副作用函数入口创建reactiveEffect,reactiveEffect就是创建相应式数据依赖收集时绑定的副作用函数
createReactiveObject:创建代理对象
RefImpl:有ref创建的对象代理入口,
ComputedRefImpl:Computed创建伪代理
RuntimeCompiledPublicInstanceProxyHandlers 编译代码render函数中with(ctx) (这里注意ctx,模板数据是从ctx获取的,所以数据应该都绑定在ctx)代理入口,也是访问模板数据第一层代理入口,
PublicInstanceProxyHandlers:主要为了过滤一些特殊访问 第二层代理入口
shallowUnwrapHandlers:proxyRefs,RuntimeCompiledPublicInstanceProxyHandlers代理会走到这层代理,主要为了处理ref,如果是ref返回[value]走RefImpl代理,不然返回原值,如果不是响应对象到这里就截止了,如果是响应式走createGetter 代理。
createGetter :createReactiveObject创建的代理入口,比如reactive API就在这里绑定,如果是reactive生成对象访问属性最后会走这里逻辑,这里2个关键逻辑1.track给对应target的属性绑定reactiveEffect副作用函数(在depsmap的dep里),副作用函数会在改变属性值时触发,副作用函数触发页面重新渲染的。2.如果访问的属性是Object会createReactiveObject再创建成createGetter 可代理的,再返回子子属性又会走一遍这里逻辑(禁止套娃)。
createSetter:createReactiveObject创建的代理入口,比如reactive就在这里绑定,如果是reactive生成对象修改属性会走这里逻辑,这里1个关键逻辑trigger,让对应target的属性的副作用涵数执行来更新页面
本章小知识:vue3代码各个关键阶段(后面会附上代码版本再解析一遍)
//组件
startMeasure(instance, `mount`);
startMeasure(instance, `init`);
startMeasure(instance, `compile`)
effect(有数据更新会重新执行这里逻辑)
startMeasure(instance, `render`);
startMeasure(instance, `patch`);
1.startMeasure(instance, init
);
根据组件初步实例进行组件初始化,
1.1调用函数setupComponent,主要逻辑initProps,initSlots,setupStatefulComponent,
1.2setupStatefulComponent:主要是 组件实例proxy处理和组件setup处理。如果有setup对setup数据进行处理(核心就是通过proxy做各种代理),并挂到组件上下文,处理完就进行compile
2startMeasure(instance, compile
)
2.如果组件没有render根据template进行compile
2.1通过template生成ast
2.2通过ast生成可执行的javascript字符串
2.3通过Function生成可执行render函数这个render函数在组件实例上,编译完成
2.4完成编译后有一段// support for 2.x options,vue2数据处理都在这里处理了 data里的数据生成相应式挂载在组件上下文
3 effect 组件实例副作用函数,作为数据更新入口,每当组件数据有更新都会触发这个入口,入口如何触发可以看下上文知识点大纲
3.1startMeasure(instance, render
);
3.1.1根据组件实例(实例在你操作数据时候数据已经被更新了)render成(这里render会调用2.3生成的render函数)一个新的vnode,由于render要访问ctx,根据上面【proxy小知识】我们知道会依次触发RuntimeCompiledPublicInstanceProxyHandlers =>PublicInstanceProxyHandlers=>shallowUnwrapHandlers:proxyRefs=>createGetter (这里会把对应属性绑上副作用函数,至于何时触发后文再说)
3.2startMeasure(instance, patch
);
3.2.1根据新旧vnode patch更新并渲染。
小总结:
vue3响应原理官网已经写得很通透了,除了没贴源码该有的都有了。Vue3源码怎么看尤大推荐先看响应也就是对应的reactivity,接下来对比reactivity.ts和vueglobal文件来解析下具体逻辑
响应式主要是data与view绑定和触发的一个过程,响应式主要2个过程:数据依赖收集(get时收集)和数据派发(set时派发)
vue2.0官网介绍
vue3.0官网介绍
区别:
官方版
Vue2使用Object.defineProperty不能检测数组和对象的变化
Vue3改良:使用Proxy优化了这个问题 ,还有如果有多层嵌套属性Vue3初始化只会收集第一层属性,只有在模板使用到对应子Object才会去递归收集变响应式数据。
return function get(target, key, receiver) {
//...
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res);
}
return res;
};
根据关键阶段解读相应原理
图一上代码demo
normalVaule:{{normalVaule}}
normalObj:{{normalObj.test}}
name:{{obj.name}};age:{{obj.age}};company:{{obj.gs.phone}}{{obj.gs.address}}
const Counter = {
data(){
//通过data生成的
return {dataName:'what'}
},
setup() {
var normalVaule='normalVaule'
var normalObj={test:21};
var refv=ref('ref vaue')
//创建响应式对象 如果不是通过reactive创建的对象 属性改变是不会触发副作用函数的,原因后面再讲
const obj=reactive({
name:'a',
age:12,
gs:{
phone:'12',
address:'wuc',
email:'@@@'
}
})
function add() {
refv.value='refv change'
normalObj.test=0;
normalVaule='normalVaule change'
obj.age=24;
}
return {
refv,
normalVaule,
normalObj,
obj,
add
}
}
}
demo上了几种数据类型下面我们一一解读下
根据【关键阶段】知道setup是在init中setupComponent调用的
图二setup调用逻辑代码
{
startMeasure(instance, `init`);
}
setupComponent(instance);
{
endMeasure(instance, `init`);
}
function setupComponent(instance, isSSR = false) {
isInSSRComponentSetup = isSSR;
const { props, children } = instance.vnode;
const isStateful = isStatefulComponent(instance);
initProps(instance, props, isStateful, isSSR);
initSlots(instance, children);
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined;
isInSSRComponentSetup = false;
return setupResult;
}
function setupStatefulComponent(instance, isSSR) {
const Component = instance.type;
//...
//让模板渲染里的所有属性走PublicInstanceProxyHandlers统一过滤
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
// 2. call setup()
const { setup } = Component;
if (setup) {
//...
//调用demo中setup函数
const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [shallowReadonly(instance.props) , setupContext]);
//...
handleSetupResult(instance, setupResult, isSSR);
}
else {
finishComponentSetup(instance, isSSR);
}
}
调用setup逻辑链有了,那reactive又做了什么让数据变化可以被监听并触发对应的渲染的函数呢?
图三reactive逻辑链
function reactive(target) {
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
//...
const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
//存储当前target如果有相同target会返回一个proxy 具体可以看createReactiveObject代码这里前置判断逻辑
proxyMap.set(target, proxy);
return proxy;
}
baseHandlers.get = function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
if (key === "__v_isReactive" /* IS_REACTIVE */) {
return !isReadonly;
}
else if (key === "__v_isReadonly" /* IS_READONLY */) {
return isReadonly;
}
else if (key === "__v_raw" /* RAW */ &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap).get(target)) {
return target;
}
const targetIsArray = isArray(target);
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
const res = Reflect.get(target, key, receiver);
if (isSymbol(key)
? builtInSymbols.has(key)
: isNonTrackableKeys(key)) {
return res;
}
if (!isReadonly) {
track(target, "get" /* GET */, key);
}
if (shallow) {
return res;
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key);
return shouldUnwrap ? res.value : res;
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res);
}
return res;
};
}
proxy代理逻辑链也有了我们看到了关键代码track(target, "get" /* GET */, key);,其实 reactive逻辑很简单就是通过proxy把属性的访问代理到createGetter
图四ctx挂载生成响应数据供render模板时使用
//调用demo中setup函数
const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [shallowReadonly(instance.props) , setupContext]);
//...
handleSetupResult(instance, setupResult, isSSR);
function handleSetupResult(instance, setupResult, isSSR) {
instance.setupState = proxyRefs(setupResult);
{ //setupState 绑定到ctx,这样render时才能访问到各个模板数据
exposeSetupStateOnRenderContext(instance);
}
finishComponentSetup(instance, isSSR);
}
function exposeSetupStateOnRenderContext(instance) {
const { ctx, setupState } = instance;
Object.keys(toRaw(setupState)).forEach(key => {
if (key[0] === '$' || key[0] === '_') {
warn(`setup() return property ${JSON.stringify(key)} should not start with "$" or "_" ` +
`which are reserved prefixes for Vue internals.`);
return;
}
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
get: () => setupState[key],
set: NOOP
});
});
}
ctx上已经有代理了,再用render函数渲染就会走代理到相应proxy
图五第二阶段complie
function finishComponentSetup(instance, isSSR) {
const Component = instance.type;
//...
Component.render = compile(Component.template, {
isCustomElement: instance.appContext.config.isCustomElement,
delimiters: Component.delimiters
});
//...
// 生成的根据模板编译生成的render函数会放在组件实例上
instance.render = (Component.render || NOOP);
// for runtime-compiled render functions using `with` blocks, the render
// proxy used needs a different `has` handler which is more performant and
// also only allows a whitelist of globals to fallthrough.
if (instance.render._rc) {
instance.withProxy = new Proxy(instance.ctx, RuntimeCompiledPublicInstanceProxyHandlers);
}
}
// support for 2.x options
{
//...
applyOptions(instance, Component);
}
//...
}
complie核心逻辑没看,我们跳过,因为主要是想说通响应原理。
图六complie得到的渲染函数 注意with的ctx
(function anonymous(
) {
const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createVNode: _createVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode("p", null, " name:" + _toDisplayString(obj.name) + ";age:" + _toDisplayString(obj.age) + ";gs:" + _toDisplayString(obj.gs.phone) + _toDisplayString(obj.gs.address), 1 /* TEXT */),
_createVNode("button", { onClick: add }, "add", 8 /* PROPS */, ["onClick"])
], 64 /* STABLE_FRAGMENT */))
}
}
})
编译模板阶段结束进入第三阶段render
图七effect生成
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// create reactive effect for rendering
instance.update = effect(function componentEffect() {
//....
//数据更新副作用函数入口
})
}
function effect(fn, options = EMPTY_OBJ) {
if (isEffect(fn)) {
fn = fn.raw;
}
const effect = createReactiveEffect(fn, options);
if (!options.lazy) {
effect();
}
return effect;
}
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
if (!effect.active) {
return options.scheduler ? undefined : fn();
}
if (!effectStack.includes(effect)) {
cleanup(effect);
try {
enableTracking();
effectStack.push(effect);
activeEffect = effect;
//通过一系列骚操作把组件副作用函数componentEffect存在这里
return fn();
}
finally {
effectStack.pop();
resetTracking();
activeEffect = effectStack[effectStack.length - 1];
}
}
};
//...
return effect;
}
以上代码显示componentEffect命名函数即该组件副作用函数通过闭包形式绑定到reactiveEffect作用域链上的,接下来看下这个是怎么触发的。
页面第一次初始化会自执行一遍componentEffect 里面包含render和patch2个阶段
图八render
function componentEffect() {
if (!instance.isMounted) {
let vnodeHook;
const { el, props } = initialVNode;
const { bm, m, parent } = instance;
const subTree = (instance.subTree = renderComponentRoot(instance));
}else{
// update
}
function renderComponentRoot(instance) {
const { type: Component, vnode, proxy, withProxy, props, propsOptions: [propsOptions], slots, attrs, emit, render, renderCache, data, setupState, ctx } = instance;
let result;
//调用第二阶段生成的render函数
result = normalizeVNode(render.call(proxyToUse, proxyToUse, renderCache, props, setupState, data, ctx));
return result;
}
图八里调用第二阶段生成的render函数会依次触发4层代理,不知道为什么触发可以再看下图二代码,依次到第四层代理(如果是reactive生成的对象的话)。代理到第四层就会触发track
图九track
function track(target, type, key) {
if (!shouldTrack || activeEffect === undefined) {
return;
}
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
if (activeEffect.options.onTrack) {
activeEffect.options.onTrack({
//还记得图七createReactiveEffect
effect: activeEffect,
target,
type,
key
});
}
}
}
const effect = function reactiveEffect() {
if (!effect.active) {
return options.scheduler ? undefined : fn();
}
if (!effectStack.includes(effect)) {
cleanup(effect);
try {
enableTracking();
effectStack.push(effect);
//这个effect即reactiveEffect副作用函数会被绑定到target的属性上,连接起来了。
activeEffect = effect;
return fn();
}
finally {
effectStack.pop();
resetTracking();
activeEffect = effectStack[effectStack.length - 1];
}
}
};
看图九我们知道 activeEffect即reactiveEffect的引用在图七里reactiveEffect副作用函数会被绑定到target的属性上,而属性更新会触发set
图十set
function createSetter(shallow = false) {
return function set(target, key, value, receiver) {
let oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
trigger(target, "set" /* SET */, key, value, oldValue);
return result;
};
}
trigger是用来触发副作用函数的入口我们先放着不管,因为我们还没有去触发数据更新,到这里第三阶段想讲的都讲完了。
图十一第四阶段patch
const subTree = (instance.subTree = renderComponentRoot(instance));
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
patch是核心的核心我们放到后面章再说,patch主要是根据虚拟节点根据diff算法渲染成可见页面。这样第四个阶段也好了,整个渲染大致逻辑清楚了,其中把响应式关键步骤也说清楚了,接下来看下track怎么触发componentEffect来执行render和patch的。
图十二trigger
function trigger(target, type, key, newValue, oldValue, oldTarget) {
const depsMap = targetMap.get(target);
if (!depsMap) {
// never been tracked
return;
}
const effects = new Set();
const add = (effectsToAdd) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || effect.allowRecurse) {
effects.add(effect);
}
});
}
};
if (type === "clear" /* CLEAR */) {
//...
}
else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(key));
}
//...
}
const run = (effect) => {
if (effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
});
}
if (effect.options.scheduler) {
effect.options.scheduler(effect);
}
else {
effect();
}
};
//多个副作用函数一起push
effects.forEach(run);
}
// scheduler
function queueJob(job) {
//允许递归
if ((!queue.length ||
!queue.includes(job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex)) &&
job !== currentPreFlushParentJob) {
//#2768 google翻译使用二分查找在队列中找到合适的位置,以便队列保持作业 id 的递增顺序,这样可以防止作业被跳过,也可以避免重复打补丁
const pos = findInsertionIndex(job);
if (pos > -1) {
queue.splice(pos, 0, job);
}
else {
//函数放到队列
queue.push(job);
}
queueFlush();
}
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true;
//异步更新逻辑来了
currentFlushPromise = resolvedPromise.then(flushJobs);
}
}
function flushJobs(seen) {
isFlushPending = false;
isFlushing = true;
{
seen = seen || new Map();
}
flushPreFlushCbs(seen);
queue.sort((a, b) => getId(a) - getId(b));
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex];
if (job) {
if (true) {
checkRecursiveUpdates(seen, job);
}
callWithErrorHandling(job, null, 14 /* SCHEDULER */);
}
}
}
}
callWithErrorHandling里job就是reactiveEffect 也就是compoentEffect 入口,也就是render和patch阶段入口。
render会根据生成新的虚拟节点,patch根据新旧节点更新页面,(md,禁止套娃)
这里还看到resolvedPromise.then异步更新dom.
至此demo里obj(reactive创建的对象)响应逻辑就讲解完了,接下来看下ref
var refv=ref('ref vaue')
function add() {
refv.value='refv change'
}
图十三RefImpl
class RefImpl {
constructor(_rawValue, _shallow = false) {
this._rawValue = _rawValue;
this._shallow = _shallow;
this.__v_isRef = true;
this._value = _shallow ? _rawValue : convert(_rawValue);
}
get value() {
track(toRaw(this), "get" /* GET */, 'value');
return this._value;
}
set value(newVal) {
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal;
this._value = this._shallow ? newVal : convert(newVal);
trigger(toRaw(this), "set" /* SET */, 'value', newVal);
}
}
}
由【小知识各个proxy】可知ref跟RefImpl 有关,在proxy第三阶段会判断是ref走RefImpl ,根据图十三可知RefImpl 对象在get时track依赖收集并绑定对应副作用函数,set时触发trigger并调用副作用函数render and patch更新页面,ref完毕.
图十四看下普通对象
//normalObj.test不会更新normalVaule不会
function add() {
normalObj.test=0;
normalVaule='normalVaule change'
}
//normalObj.test会更新normalVaule不会
function add() {
refv.value='refv change'
normalObj.test=0;
normalVaule='normalVaule change'
}
//normalObj.test不会更新normalVaule不会
function add() {
refv.value='refv change'
setTimeout(()=>{ normalObj.test=0;
normalVaule='normalVaule change'})
}
首先非响应式代码是无法触发副作用函数的所以无法更新,这里用 refv.value来触发副作用函数来更新页面,会发现normalObj.test值也更新了但是normalVaule 没有更新,这是为什么呢?,我们知道render是根据ctx上数据来渲染的,ctx上normalObj是来自setup函数返回的引用,normalVaule 是对应的值,normalObj引用的属性会反馈到ctx上所以重新渲染可以生效
//如果改成这样都不会生效了
function add() {
refv.value='refv change'
normalObj={test:0};
normalVaule='normalVaule change'
}
图十五再来看下data里的也就是vue2语法的
data(){
//通过data生成的
return {dataName:'what'}
},
methods: {
add2(){
this.dataName='form add2'
//d等效
// vm.dataName='form add2'
},
}
const vm = app.mount('#counter')
为什么2个是等效的呢
图十六先来看下vm是什么
vm=app.mount = (containerOrSelector) => {
const container = normalizeContainer(containerOrSelector);
const proxy = mount(container, false, container instanceof SVGElement);
return proxy;
};
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
vm就是ctx代理
图十七再来看下这里的this指什么
// support for 2.x options
{
//...
applyOptions(instance, Component);
}
function applyOptions(){
//...
const publicThis = instance.proxy
if (methods) {
for (const key in methods) {
{
Object.defineProperty(ctx, key, {
//this也是ctx代理
value: methodHandler.bind(publicThis),
configurable: true,
enumerable: true,
writable: true
});
}
}
if (!asMixin) {
if (deferredData.length) {
deferredData.forEach(dataFn => resolveData(instance, dataFn, publicThis));
}
if (dataOptions) {
//resolveData最后还是通过reactive来生成data的
resolveData(instance, dataOptions, publicThis);
}
{
const rawData = toRaw(instance.data);
for (const key in rawData) {
checkDuplicateProperties("Data" /* DATA */, key);
// expose data on ctx during dev
if (key[0] !== '$' && key[0] !== '_') {
Object.defineProperty(ctx, key, {
configurable: true,
enumerable: true,
get: () => rawData[key],
set: NOOP
});
}
}
}
}
}
methodHandler.bind(instance.proxy) this也是ctx代理,所以个this是一样会走ctx然后按4步骤走代理阶段。而且图十七还显示在兼容处理vue2代码里data的数据也是通过reativeAPI处理的。
PublicInstanceProxyHandlers:
set({ _: instance }, key, value) {
const { data, setupState, ctx } = instance;
else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
//data里的数据改变走到是这里逻辑 而且从上文图十七可知data就reactive生成的代理变量这里修改属性也会走第四阶段来触发副作用函数来触发页面更新
data[key] = value;
}
}
data[key] = value; data里的数据改变走到是这里逻辑 而且从上文图十七可知data就reactive生成的代理变量这里修改属性也 会走第四阶段来触发副作用函数来触发页面更新
图十八computed demo
const obj=reactive({
name:'a',
age:12,
hi: computed(() => 'hi my name is '+obj.name),
gs:{
phone:'12',
address:'wuc',
email:'@@@'
}
})
function add() {
obj.name='my world';
}
图十九computed做了什么
function computed(getterOrOptions) {
let getter;
let setter;
if (isFunction(getterOrOptions)) {
getter = getterOrOptions;
setter = () => {
console.warn('Write operation failed: computed value is readonly');
}
;
}
else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
return new ComputedRefImpl(getter, setter, isFunction(getterOrOptions) || !getterOrOptions.set);
}
class ComputedRefImpl {
constructor(getter, _setter, isReadonly) {
this._setter = _setter;
this._dirty = true;
this.__v_isRef = true;
this.effect = effect(getter, {
lazy: true,
scheduler: () => {
if (!this._dirty) {
this._dirty = true;
trigger(toRaw(this), "set" /* SET */, 'value');
}
}
});
this["__v_isReadonly" /* IS_READONLY */] = isReadonly;
}
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this);
if (self._dirty) {
self._value = this.effect();
self._dirty = false;
}
track(self, "get" /* GET */, 'value');
return self._value;
}
set value(newValue) {
this._setter(newValue);
}
}
this.effect = effect(getter, )熟悉吧,component.update创建组件更新副作用函数也是用的这个只是之前副作用函数是指向compoentEffect,这里副作用函数是getter也就是demo里面的匿名函数。然后getcomputed里的属性时track这个computed,目的是把生成的副作用函数绑定到这个computed的对应属性上。
这里有同学要问了,改变了obj.name怎么就触发() => 'hi my name is '+obj.name这个副作用函数了呢?
图二十仔细分析下ComputedRefImpl里的get
{{obj.hi}}
hi: computed(() => 'hi my name is '+obj.name),
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this);
if (self._dirty) {
//
self._value = this.effect();
self._dirty = false;
}
track(self, "get" /* GET */, 'value');
return self._value;
}
get 逻辑并不是computed()执行触发的,而是在页面渲染阶段要访问obj.hi时触发的,具体跟ref生成的路径差不多,只是ref走的RefImpl的get方法,computed走到是ComputedRefImpl方法,这时访问get才去调用self._value = this.effect();当前hi的副作用函数又会执行obj.name,根据【proxy各个阶段createGetter 代理】关系我们知道,这里去走一遍obj.name的track,根据响应大纲我们知道track是去绑定对应的副作用函数,这里obj.name去track时我们知道副作用函数是() => 'hi my name is '+obj.name。生成的targetMap大致结构上这样的。
图二十一
//hi在render访问时候就track了 然后才走到.value触发的hiComputedRefImpl 的get
{obj:{hi:[functioon:componentEffect],name:[() => 'hi my name is '+obj.name]}}
然后是track(self, "get" /* GET */, 'value');track hiComputedRefImpl,得到大致结果:
图二十二
targetMap{obj:{hi:[functioon:componentEffect],name:[() => 'hi my name is '+obj.name]},hiComputedRefImpl :{value:[functioon:componentEffect]}}
大家可以思考下如果demo里是
:{{obj.name}}say:{{obj.hi}}
会生成怎么样的targetmaps
这里ComputedRefImpl.value为什么绑定的是componentEffect而不是() => 'hi my name is '+obj.name具体逻辑大家可以看下createReactiveEffect代码
图二十三 仔细分析下createReactiveEffect
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
try {
enableTracking();
effectStack.push(effect);
activeEffect = effect;
return fn();
}
finally {
effectStack.pop();
resetTracking();
activeEffect = effectStack[effectStack.length - 1];
}
//...
return effect;
}
每次执行完副作用函数都会把当前副作用栈-1,在computed内部即图二十这段self._value = this.effect()代码执行的内部函数调用时,不管track多少次都绑定的是自己副作用函数,而当 this.effect()执行完副作用函数,当前effect就会出栈,下个栈是即componentEffect track到ComputedRefImpl。我的理解是不管是computed内部方法,还是其他副作用函数,他们本身是为的render功能服务的,render时代表单副作用函数应该永远在最外层,而其他副作用函数都会都有自己的作用域,并在track绑定在effect()完释放就像computed一样。
然后来看下点击改变按钮后又发生了什么。
按钮会触发obj.name='my world',就trigger了obj name的绑定副作用函数,可以回顾下图十二中trigger和图十九的effect
if (effect.options.scheduler) {
effect.options.scheduler(effect);
}
this.effect = effect(getter, {
lazy: true,
scheduler: () => {
if (!this._dirty) {
this._dirty = true;
trigger(toRaw(this), "set" /* SET */, 'value');
}
}
});
这样就触发了hiComputedRefImpl 的副作用函数即componentEffect更新页面。大概梳理完毕- -!
图二十四watch做了什么 watchEffect(() => console.log(obj.name))
getter = () => {
if (instance && instance.isUnmounted) {
return;
}
if (cleanup) {
cleanup();
}
return callWithAsyncErrorHandling(source, instance, 3 /* WATCH_CALLBACK */, [onInvalidate]);
};
scheduler = () => {
if (!instance || instance.isMounted) {
queuePreFlushCb(job);
}
else {
// with 'pre' option, the first call must happen before
// the component is mounted so it is called synchronously.
job();
}
};
const runner = effect(getter, {
lazy: true,
onTrack,
onTrigger,
scheduler
});
//绑定到组件实例
recordInstanceBoundEffect(runner, instance);
runner();
通过effect生成副作用函数,注意这里作用域不在render里,watchEffect本身就是顶层栈了跟render时componetEffect是一个级别的本身不依赖模板渲染,只要你改变watch里对应属性,watch的副作用函数就会触发,这个跟computed还有点不一样,上面的computed依赖render。runner自执行一遍。
1.runner();执行副作用函数即() => console.log(obj.name)代码
2.访问obj.name通过createGetter 代理走track关联obj的name绑定副作用函数getter。
3.打印日志
4.点击按钮触发name的trigger
5.触发watch的scheduler
- queuePreFlushCb(job);watch的副作用函数会进入pendingQueue等待,(这里跟render时副作用函数也不一样,render页面的是进入queueJob中进入queue等待),
7.异步执行
function flushJobs(){
//先出来watch的副作用
flushPreFlushCbs(seen);
//再执行queue副作用
callWithErrorHandling(job, null, 14 /* SCHEDULER */);
}
watch API和watchEffect API 逻辑基本一致区别看官网
总结
到此整个响应逻辑解析完了
Vue总共4个阶段init,complie,render,patch,响应式体现在各个环节我们再来回顾一遍.
1.首先在init处理setup生成可代理数据。
2.在complie阶段生成可执行的渲染函数里面包含关键对象with(ctx),
3.生成副作用函数个,
4自执行副作用函数,在render时通过with(ctx)走4层代理函数。并track对应目标,把reacteEffect即componentEffect(包含render和patch过程)绑定到对应目标属性上。
5.patch虚拟节点渲染成页面。
6.更新的响应式数据,触发proxy的set,触发trigger函数在执行队列里异步执行副作用函数即componentEffect来更新页面。
7.computed的内访问的属性会在ComputedRefImpl里处理对应代理和track。
8.watch内会在doWatch里出来对应代理和track
ps:第一次写这么长的文章也不知道写的怎么样,有可能过一个月过来看自己也看不懂了(手动滑稽)。如果有问题和不足的地方欢迎指正。
本章主要是贯穿整个流程把响应式梳理了一遍,然后几个关键api在里面起到作用简单说明了下。下一章看下从入口函数触发到页面渲染的过程。