My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
My Little World

KOA框架剥洋葱原理

发表于 2020-06-24

收集

new 一个koa实例后,调用use接口会将传入的中间件函数push到middleware属性数组中做收集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
application.js

use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}

根据koa框架版本不同,需要将使用generator函数写的中间件转化成基于promise的函数形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
const convert = require('koa-convert');

function convert (mw) {
if (typeof mw !== 'function') {
throw new TypeError('middleware must be a function')
}
//再次确认是否是generator函数
if (mw.constructor.name !== 'GeneratorFunction') {
// 假设它是一个基于promise的中间件
// 就直接返回
return mw
}
//如果是generator函数就转换成promise形式
const converted = function (ctx, next) {
return co.call(ctx, mw.call(ctx, createGenerator(next)))
}
converted._name = mw._name || mw.name
return converted
}

const co = require('co')

function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)

// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);

onFulfilled();

/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/

function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}

/**
* @param {Error} err
* @return {Promise}
* @api private
*/

function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}

/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/

function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}

整合

调用listen函数开启服务时,整合所有中间件为一个大函数,大函数中进行剥洋葱流程
在收到请求时调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
application.js

listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}

callback() {
const fn = compose(this.middleware); //整合中间件

if (!this.listenerCount('error')) this.on('error', this.onerror);

const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn); //调用中间件
};

return handleRequest;
}

handleRequest(ctx, fnMiddleware) {
//通过传递过来的ctx,获取到原生的可写流
const res = ctx.res;
//设置默认状态码
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
//调用中间件大函数 同时准备catch处理中间件级错误
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

整合中间件 ,返回大函数,大函数中有剥洋葱模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
//调用时fnMiddleware(ctx).then(handleResponse).catch(onerror);
//next 是undefined
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {//try-catch 用于保证错误在promise的情况能够正常捕获
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}

剥洋葱流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let index = -1
return dispatch(0) //从第一个中间件开始执行
function dispatch (i) {
//防止next 在一个中间件中被调用多次
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i] //拿到中间件
if (i === middleware.length) fn = next //如果中间件函数全部调用完,fn 赋值为next,next为undefined
if (!fn) return Promise.resolve() //fn为next=>undefined时成立,即执行到最后返回一个Promise.resolve() 可以继续写then
try {//try-catch 用于保证错误在promise的情况能够正常捕获
//执行中间件函数,传入ctx,next参数,next即dispatch.bind(null, i + 1),
//递归调用下一个中间件函数
//从这一步可以实现await next()时,先执行下一个中间件函数
//中间件有调用next,就利用await暂停当前中间件执行,开始执行下一个中间件
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}

利用await暂停当前中间件执行,调用next开始执行下一个中间件

如果下一个中间件没有调next,且不是最后一个中间件,
即不会再调用dispatch执行下下一个中间件,则后续中间件都不会被执行到

如果下一个中间件是最后一个中间件,且在其中调用了next,则不会走到这个流程,
在上面的流程中就return

如果下一个中间件是最后一个中间件,则执行完return Promise.resolve
上一个中间件await 拿到结果后继续执行await后面的代码
执行完await后面代码后,上上一个中间件await 拿到结果,继续执行当前中间件await后代码
依次类推,直到第一个中间件await后代码执行完毕,整个中间件流程,
即剥洋葱流程先内层后外层执行流程完毕
返回promise 接着then处理响应或者中间有错误捕获错误
fnMiddleware(ctx).then(handleResponse).catch(onerror)

My Little World

vuex 源码学习

发表于 2020-04-27
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
let moduleA = {
namespaced: true,
state(){
return{
counta: 0, //分别在模块b和全局下被使用,直接返回对象会通过引用被共享,防止模块间数据污染,使用函数,每次生成新对象,原理同vue的data
}
},
getters:{
getCount(...args){ //this.$store.getters['b/aa/getCount'] 作为b子模块时
console.log(args)
return 'this is a getter~~~~~'
}
},
mutations: { // this.$store.commit('b/aa/increment') 仅触发模块b下面的aa子模块
increment (state) { //this.$store.commit('a/increment') 仅触发全局的模块a 对应的子模块
state.counta++
},
incrementaaa (state) { //在被引用时,套嵌层如果都没有namespaced: true,可以直接用this.$store.commit('incrementaaa')调用进行更改
state.counta++
}
}
}

let moduleB = {
namespaced: true,
state: {
countb: 0,//this.$store.state.b.countb
},
getters:{
getCount(...args){ //this.$store.getters['b/getCount'] 因为设置了namespaced所以可以重名
console.log(args)
return 'this is a getter~~~~~'
}
},
mutations: {
increment (state) { //如果不加namespaced,执行this.$store.commit('increment'),会同时执行
state.countb++
}
},
modules: {//如果moduleB不加namespaced,aa可以访问数据但不能调用this.$store.commit('b/aa/increment')进行更改
aa: moduleA,//this.$store.state.b.aa.counta
}
}
// vuex相关代码
const store = new Vuex.Store({
state: {
count: 0, //this.$store.state.count
},
getters:{
getCount(...args){ //this.$store.getters.getCount
console.log(args)
return 'this is a getter'
}
},
mutations: {
increment (state) { //this.$store.commit('increment') 子模块有相同函数时,都会触发执行
state.count++
}
},
modules: {
a: moduleA, //this.$store.state.a.counta
b: moduleB //this.$store.state.b.countb
}
})

以上面store结构为例,分析vuex源码

new Store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
var Store = function Store (options) {
var this$1 = this;
if ( options === void 0 ) options = {};
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];
var strict = options.strict; if ( strict === void 0 ) strict = false;

// 一些内部参数
this._committing = false;
this._actions = Object.create(null);
this._actionSubscribers = [];
this._mutations = Object.create(null);
this._wrappedGetters = Object.create(null);
this._modules = new ModuleCollection(options);
this._modulesNamespaceMap = Object.create(null);
this._subscribers = [];
this._watcherVM = new Vue();
this._makeLocalGettersCache = Object.create(null);

//绑定 commit 和 dispatch 的执行对象始终指向自己,防止外界重新绑定
var store = this;
var ref = this;
var dispatch = ref.dispatch;
var commit = ref.commit;
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
};
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
};

// 严格模式
//使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。
this.strict = strict;
//拿到处理后的state
var state = this._modules.root.state;

// 初始化全局module
// 同时递归注册所有子模块
// 收集this._wrappedGetters中的所有模块的getters
installModule(this, state, [], this._modules.root);

// 初始化store vm, 用于数据响应
// 同时注册 _wrappedGetters 作为计算属性)
resetStoreVM(this, state);

// 使用插件处理
plugins.forEach(function (plugin) { return plugin(this$1); });
//options.devtools为某个特定的 Vuex 实例打开或关闭 devtools。
//对于传入 false 的实例来说 Vuex store 不会订阅到 devtools 插件。可用于一个页面中有多个 store 的情况
var useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools;
if (useDevtools) {
devtoolPlugin(this);
}
};

install

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function install (_Vue) {
if (Vue && _Vue === Vue) {
//Vue.use(Vuex) should be called only once
return
}
Vue = _Vue;
applyMixin(Vue);
}
function applyMixin (Vue) {
var version = Number(Vue.version.split('.')[0]);

if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit }); //使用beforeCreate将$store绑定到每个VueComponent 上
} else {
//小于版本2的用_init初始化
var _init = Vue.prototype._init;
Vue.prototype._init = function (options) {
if ( options === void 0 ) options = {};

options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit;
_init.call(this, options);
};
}

function vuexInit () {
var options = this.$options;
// store injection
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store;
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store; //当前组件$store指向父组件$store,递归指向,从而保证唯一性
}
}
}

ModuleCollection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var ModuleCollection = function ModuleCollection (rawRootModule) {
// 根据new Store 传入的参数,注册根模块
this.register([], rawRootModule, false);
};
// new store 时传入的参数 [], rawRootModule, false
ModuleCollection.prototype.register = function register (path, rawModule, runtime) {
var this$1 = this;
if ( runtime === void 0 ) runtime = true;
var newModule = new Module(rawModule, runtime);
//{runtime:false,_children:{},_rawModule:rawModule,state:rawModule上面的state,__proto__:一系列方法}
if (path.length === 0) {
this.root = newModule; //初始化根模块
} else { //初始化子模块
var parent = this.get(path.slice(0, -1)); //拿到父模块
parent.addChild(path[path.length - 1], newModule); //给父模块_children属性增加子模块
}

// 注册嵌套模块
if (rawModule.modules) {
forEachValue(rawModule.modules, function (rawChildModule, key) {
this$1.register(path.concat(key), rawChildModule, runtime); //递归注册
});
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var Module = function Module (rawModule, runtime) {
this.runtime = runtime;
// Store some children item
this._children = Object.create(null);
// Store the origin module object which passed by programmer
this._rawModule = rawModule;
var rawState = rawModule.state;

// Store the origin module's state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {};
};

Module.prototype.addChild = function addChild (key, module) {
this._children[key] = module;
};
Module.prototype.getChild = function getChild (key) {
return this._children[key]
};

所以这一步结束后

1
this._modules = new ModuleCollection(options);

this._modules为如下对象

1
2
3
4
5
6
7
8
9
10
ModuleCollection {
root: Module {
runtime: false,
_children: {a: Module, b: Module}
_rawModule: {state: {…}, mutations: {…}, modules: {…}}
state: {count: 0}
__proto__: Object
}
__proto__: Object
}

ModuleCollection对象上的其他方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ModuleCollection.prototype.get = function get (path) {
return path.reduce(function (module, key) {
return module.getChild(key)
}, this.root)
};

ModuleCollection.prototype.getNamespace = function getNamespace (path) {
var module = this.root;
//对设置了namespaced的模块进行拼接
return path.reduce(function (namespace, key) {
module = module.getChild(key); //从_children取出子模块
return namespace + (module.namespaced ? key + '/' : '')
}, '')
};

ModuleCollection.prototype.update = function update$1 (rawRootModule) {
update([], this.root, rawRootModule);
};
ModuleCollection.prototype.unregister = function unregister (path) {
var parent = this.get(path.slice(0, -1));
var key = path[path.length - 1];
if (!parent.getChild(key).runtime) { return }

parent.removeChild(key);
};

installModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//初始化调用时传参 (this, state, [], this._modules.root)
function installModule (store, rootState, path, module, hot) {
//根模块时,参数分别为store本身,根模块state, [], 根模块,undefined
//递归子模块时,参数为 store本身,根模块state,子模块在modules对象中的路径key值
var isRoot = !path.length; //空数组即根模块
//ModuleCollection.prototype.getNamespace 根模块返回空字符串''
var namespace = store._modules.getNamespace(path);

//如果模块设置了命名空间 在store._modulesNamespaceMap属性中注册
if (module.namespaced) {
if (store._modulesNamespaceMap[namespace] && "development" !== 'production') {
console.error(("[vuex] duplicate namespace " + namespace + " for the namespaced module " + (path.join('/'))));
}
store._modulesNamespaceMap[namespace] = module;
}

//对子模块state进行数据劫持处理
if (!isRoot && !hot) {
var parentState = getNestedState(rootState, path.slice(0, -1));
var moduleName = path[path.length - 1];
store._withCommit(function () {
{
if (moduleName in parentState) {
console.warn(
("[vuex] state field \"" + moduleName + "\" was overridden by a module with the same name at \"" + (path.join('.')) + "\"")
);
}
}
Vue.set(parentState, moduleName, module.state); //将子模块state按照模块名添加到父模块state中
});
}
//根据有无namespace
//截取state,getter的get,
//封装触发函数dispactch和commit,有namespace拼接后再触发
var local = module.context = makeLocalContext(store, namespace, path);

module.forEachMutation(function (mutation, key) {
var namespacedType = namespace + key;
registerMutation(store, namespacedType, mutation, local);
});

module.forEachAction(function (action, key) {
var type = action.root ? key : namespace + key;
var handler = action.handler || action;
registerAction(store, type, handler, local);
});

module.forEachGetter(function (getter, key) {
var namespacedType = namespace + key;
registerGetter(store, namespacedType, getter, local);
});

module.forEachChild(function (child, key) {
installModule(store, rootState, path.concat(key), child, hot);
});
}

forEachValue

公共方法

1
2
3
function forEachValue (obj, fn) {
Object.keys(obj).forEach(function (key) { return fn(obj[key], key); });
}

注册 Mutation

1
2
3
4
5
6
7
8
9
10
11
12
Module.prototype.forEachMutation = function forEachMutation (fn) {
if (this._rawModule.mutations) {
forEachValue(this._rawModule.mutations, fn);
}
};
//参数:store本身,经过namespace拼接后的类型标记,具体对应的mutation,当前模块上封装好的state,getters,commit,dispatch
function registerMutation (store, type, handler, local) {
var entry = store._mutations[type] || (store._mutations[type] = []);
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload);
});
}

小结:将当前模块的mutations上的各个mutations结合namespace存储到store._mutations属性上
可见,如果子模块没有namespace,同名的mutation会跟根模块的mutation存储在相同的type下面
所以当触发commit的时候会一起执行,
如果子模块设置了namespace,这时存储的type会包含该模块对应的名称,
即使同名的mutation也被存放在不同的type中,实现了隔离

注册 Action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Module.prototype.forEachAction = function forEachAction (fn) {
if (this._rawModule.actions) {
forEachValue(this._rawModule.actions, fn);
}
};

function registerAction (store, type, handler, local) {
var entry = store._actions[type] || (store._actions[type] = []);
entry.push(function wrappedActionHandler (payload) {
var res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload);
if (!isPromise(res)) {
res = Promise.resolve(res);
}
if (store._devtoolHook) {
return res.catch(function (err) {
store._devtoolHook.emit('vuex:error', err);
throw err
})
} else {
return res
}
});
}

小结:将当前模块的actions上的各个action结合namespace存储到store._actions属性上

注册 Getter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Module.prototype.forEachGetter = function forEachGetter (fn) {
if (this._rawModule.getters) {
forEachValue(this._rawModule.getters, fn);
}
};

function registerGetter (store, type, rawGetter, local) {
if (store._wrappedGetters[type]) {
{
console.error(("[vuex] duplicate getter key: " + type));
}
return
}
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // 当前模块state
local.getters, // 当前模块 getters
store.state, // 根模块 state
store.getters // 根模块 getters
)
};
}

小结:将当前模块的getters上的各个getter结合namespace存储到store._wrappedGetters属性上

注册 Module

递归调用installModule注册子模块

1
2
3
4
5
6
7
Module.prototype.forEachChild = function forEachChild (fn) {
forEachValue(this._children, fn);
};

module.forEachChild(function (child, key) {
installModule(store, rootState, path.concat(key), child, hot);
});

makeLocalContext

优化 dispatch, commit, getters and state
如果没有设置namespace, 就使用根模块的名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
  function makeLocalContext (store, namespace, path) {
var noNamespace = namespace === '';

var local = {
dispatch: noNamespace ? store.dispatch : function (_type, _payload, _options) {
var args = unifyObjectStyle(_type, _payload, _options);
var payload = args.payload;
var options = args.options;
var type = args.type;

if (!options || !options.root) {
type = namespace + type;//有namespace拼接后再触发
if (!store._actions[type]) {
console.error(("[vuex] unknown local action type: " + (args.type) + ", global type: " + type));
return
}
}

return store.dispatch(type, payload)
},

commit: noNamespace ? store.commit : function (_type, _payload, _options) {
var args = unifyObjectStyle(_type, _payload, _options);
var payload = args.payload;
var options = args.options;
var type = args.type;

if (!options || !options.root) {
type = namespace + type; //有namespace拼接后再触发
if (!store._mutations[type]) {
console.error(("[vuex] unknown local mutation type: " + (args.type) + ", global type: " + type));
return
}
}

store.commit(type, payload, options);
}
};

// getters and state object must be gotten lazily
// because they will be changed by vm update
Object.defineProperties(local, {
getters: {
get: noNamespace
? function () { return store.getters; }
: function () { return makeLocalGetters(store, namespace); }
},
state: {
get: function () { return getNestedState(store.state, path); }
}
});

return local
}

function makeLocalGetters (store, namespace) {
if (!store._makeLocalGettersCache[namespace]) {
var gettersProxy = {};
var splitPos = namespace.length;
Object.keys(store.getters).forEach(function (type) {
// skip if the target getter is not match this namespace
if (type.slice(0, splitPos) !== namespace) { return }

// extract local getter type
var localType = type.slice(splitPos);

// Add a port to the getters proxy.
// Define as getter property because
// we do not want to evaluate the getters in this time.
Object.defineProperty(gettersProxy, localType, {
get: function () { return store.getters[type]; },
enumerable: true
});
});
store._makeLocalGettersCache[namespace] = gettersProxy;
}

return store._makeLocalGettersCache[namespace]
}
function getNestedState (state, path) {
return path.reduce(function (state, key) { return state[key]; }, state)
}

小结

该步骤完成后

store对象长这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
commit: ƒ boundCommit(type, payload, options)
dispatch: ƒ boundDispatch(type, payload)
strict: false
_actionSubscribers: []
_actions: {} //存放所有模块的action
_committing: false
_makeLocalGettersCache: {}
_modules: ModuleCollection {
root: Module
context: //新增根据namespace处理后的触发函数
commit: ƒ boundCommit(type, payload, options)
dispatch: ƒ boundDispatch(type, payload)
getters: (...)
state: (...)
get getters: ƒ ()
get state: ƒ ()
__proto__: Object
runtime: false
state: //将子模块state集中到根模块state
a: {counta: 2329}
b:
aa: {counta: 2329}
countb: 0
__proto__: Object
count: 0
__proto__: Object
_children: //对子模块递归过程挂载context属性
a: Module {runtime: false, _children: {…}, _rawModule: {…}, state: {…}, context: {…}}
b: Module {runtime: false, _children: {…}, _rawModule: {…}, state: {…}, context: {…}}
_rawModule: {state: {…}, mutations: {…}, modules: {…}}
namespaced: (...)
__proto__: Object
__proto__: Object
}
_modulesNamespaceMap: {b/: Module} //存放设置了namespace的模块
_mutations: {increment: Array(1), incrementaaa: Array(1), b/increment: Array(1), b/incrementaaa: Array(1)} //存放所有模块的mutation
_subscribers: []
_watcherVM: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
_wrappedGetters: {} //存放所有模块的getters
state: (...) //对局部变量做了处理,还没有挂到store上

resetStoreVM

利用vue的数据处理逻辑,解决getters和state之间订阅发布关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function partial (fn, arg) {
return function () {
return fn(arg)
}
}
function resetStoreVM (store, state, hot) {
var oldVm = store._vm;
//绑定存储公共的getters
store.getters = {};
// 重置内部getters缓存到_makeLocalGettersCache
store._makeLocalGettersCache = Object.create(null);
var wrappedGetters = store._wrappedGetters;
var computed = {};
forEachValue(wrappedGetters, function (fn, key) {
//如果直接使用,闭包里面会包含oldVm
computed[key] = partial(fn, store);
Object.defineProperty(store.getters, key, {
get: function () { return store._vm[key]; },//直接调用vue的compute属性
enumerable: true // for local getters
});
});

//使用vue实例存放state和getters
var silent = Vue.config.silent;
/* Vue.config.silent暂时设置为true的目的是在new一个Vue实例的过程中不会报出一切警告 */
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed: computed
});
Vue.config.silent = silent;

/* 使能严格模式,保证修改store只能通过mutation */
if (store.strict) {
enableStrictMode(store);
}

if (oldVm) {
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(function () {
oldVm._data.$$state = null;
});
}
Vue.nextTick(function () { return oldVm.$destroy(); });
}
}

store上的其他方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
Store.prototype.commit = function commit (_type, _payload, _options) {
var this$1 = this;

// check object-style commit
var ref = unifyObjectStyle(_type, _payload, _options);
var type = ref.type;
var payload = ref.payload;
var options = ref.options;

var mutation = { type: type, payload: payload };
var entry = this._mutations[type];
if (!entry) {
{
console.error(("[vuex] unknown mutation type: " + type));
}
return
}
this._withCommit(function () {
entry.forEach(function commitIterator (handler) {
handler(payload);
});
});

this._subscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.forEach(function (sub) { return sub(mutation, this$1.state); });

if (
options && options.silent
) {
console.warn(
"[vuex] mutation type: " + type + ". Silent option has been removed. " +
'Use the filter functionality in the vue-devtools'
);
}
};

Store.prototype.dispatch = function dispatch (_type, _payload) {
var this$1 = this;

// check object-style dispatch
var ref = unifyObjectStyle(_type, _payload);
var type = ref.type;
var payload = ref.payload;

var action = { type: type, payload: payload };
var entry = this._actions[type];
if (!entry) {
{
console.error(("[vuex] unknown action type: " + type));
}
return
}

try {
this._actionSubscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.filter(function (sub) { return sub.before; })
.forEach(function (sub) { return sub.before(action, this$1.state); });
} catch (e) {
{
console.warn("[vuex] error in before action subscribers: ");
console.error(e);
}
}

var result = entry.length > 1
? Promise.all(entry.map(function (handler) { return handler(payload); }))
: entry[0](payload);

return result.then(function (res) {
try {
this$1._actionSubscribers
.filter(function (sub) { return sub.after; })
.forEach(function (sub) { return sub.after(action, this$1.state); });
} catch (e) {
{
console.warn("[vuex] error in after action subscribers: ");
console.error(e);
}
}
return res
})
};

Store.prototype.subscribe = function subscribe (fn) {
return genericSubscribe(fn, this._subscribers)
};

Store.prototype.subscribeAction = function subscribeAction (fn) {
var subs = typeof fn === 'function' ? { before: fn } : fn;
return genericSubscribe(subs, this._actionSubscribers)
};

Store.prototype.watch = function watch (getter, cb, options) {
var this$1 = this;

{
assert(typeof getter === 'function', "store.watch only accepts a function.");
}
return this._watcherVM.$watch(function () { return getter(this$1.state, this$1.getters); }, cb, options)
};

Store.prototype.replaceState = function replaceState (state) {
var this$1 = this;

this._withCommit(function () {
this$1._vm._data.$$state = state;
});
};

Store.prototype.registerModule = function registerModule (path, rawModule, options) {
if ( options === void 0 ) options = {};

if (typeof path === 'string') { path = [path]; }

{
assert(Array.isArray(path), "module path must be a string or an Array.");
assert(path.length > 0, 'cannot register the root module by using registerModule.');
}

this._modules.register(path, rawModule);
installModule(this, this.state, path, this._modules.get(path), options.preserveState);
// reset store to update getters...
resetStoreVM(this, this.state);
};

Store.prototype.unregisterModule = function unregisterModule (path) {
var this$1 = this;

if (typeof path === 'string') { path = [path]; }

{
assert(Array.isArray(path), "module path must be a string or an Array.");
}

this._modules.unregister(path);
this._withCommit(function () {
var parentState = getNestedState(this$1.state, path.slice(0, -1));
Vue.delete(parentState, path[path.length - 1]);
});
resetStore(this);
};

Store.prototype.hotUpdate = function hotUpdate (newOptions) {
this._modules.update(newOptions);
resetStore(this, true);
};

Store.prototype._withCommit = function _withCommit (fn) {
var committing = this._committing;
this._committing = true;
fn();
this._committing = committing;
};

function genericSubscribe (fn, subs) {
if (subs.indexOf(fn) < 0) {
subs.push(fn);
}
return function () {
var i = subs.indexOf(fn);
if (i > -1) {
subs.splice(i, 1);
}
}
}

function resetStore (store, hot) {
store._actions = Object.create(null);
store._mutations = Object.create(null);
store._wrappedGetters = Object.create(null);
store._modulesNamespaceMap = Object.create(null);
var state = store.state;
// init all modules
installModule(store, state, [], store._modules.root, true);
// reset vm
resetStoreVM(store, state, hot);
}
My Little World

vue 源码学习四【数据双向绑定】

发表于 2020-04-11

vue双向绑定原理,依赖收集是
在created声明周期之前,
render生成虚拟dom的时候

1
2
3
4
5
6
7
8
9
Vue.prototype._init = function (options) {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
callHook(vm, 'beforeCreate');//运行订阅了beforecreate钩子的相关方法
initState(vm);//处理数据
callHook(vm, 'created');

转换

转换成内置函数mergedInstanceDataFn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
function mergeOptions (
parent,
child,
vm
) {
{
checkComponents(child);
}

if (typeof child === 'function') {
child = child.options;
}

// ... normalizeProps, normalizeInject, normalizeDirectives

if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm);
}
if (child.mixins) {
for (var i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
}

var options = {};
var key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
//在上面的循环中会遍历到配置的options参数里的data属性,则会调用到strats.data
function mergeField (key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options
}
//
strats.data = function (
parentVal,
childVal,
vm
) {
if (!vm) {
//...vm就是VUE实例,所以不会走这里
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
};

function mergeDataOrFn (
parentVal,
childVal,
vm
) {
if (!vm) {
...
} else {
return function mergedInstanceDataFn () {
// instance merge
var instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal;
var defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal;
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}

这一步结束后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vm.$options.data = function mergedInstanceDataFn () {
//声明时传递进来的data属性值,是函数则执行拿到对象
var instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal;
//内部属性没有data属性,所以 defaultData 为undefined
var defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal;
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
function mergeData (to, from) {
if (!from) { return to }
...//省略若干逻辑
}

数据劫持

开始进行真正初始化—数据劫持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
}

function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
//得到的data不是object对象
if (!isPlainObject(data)) {
data = {};
}
// proxy data on instance
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
{if (methods && hasOwn(methods, key)) {...与method同名警告}}
if (props && hasOwn(props, key)) {
...与prop同名警告
} else if (!isReserved(key)) { //不是以$或者_开头的key
proxy(vm, "_data", key); //将key值存一份到vm的'_data'属性上
}
}
// observe data
observe(data, true /* asRootData */);
}

function getData (data, vm) {
// #7573 disable dep collection when invoking(调用) data getters
pushTarget();
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, "data()");
return {}
} finally {
popTarget();
}
}
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}

给data创建观察者实例,挂载‘_ob_’属性,指向一个Observer对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}

Observer对象有三个属性,‘ob’属性相同
{
value:data,
dep : new Dep();
vmCount :0
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
//遍历所有属性并将它们转换为getter / setter。
//仅当值类型为Object时才应调用此方法。
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i]);
}
};

真正进行劫持的方法defineReactive$$1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();

var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}

// cater(迎合) for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}

var childOb = !shallow && observe(val);//对值进行劫持处理 开始递归处理
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) { //new watch的时候,Dep.target指向Watcher对象,再运行render函数,访问具体变量就会调用这里
dep.depend();//对key的依赖进行依赖收集
if (childOb) {//如果值是一个对象的情况下
childOb.dep.depend();//对对象值的依赖进行依赖收集 实现深度监听
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
//相同值,不更新
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
//有setter方法直接用setter方法更新,没有直接赋值
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
//对新值进行劫持处理 更新childOb
//如果新值是对象对新赋的值在更新时进行依赖收集
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}

每个被劫持过的数据的标识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var Dep = function Dep () {
this.id = uid++;
this.subs = [];
};
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
};

Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this); //调用Watcher.addDep方法,this指dep
}
};

Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if (!config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update(); //执行watcher的update函数
}
};
Dep.target = null;
var targetStack = [];

订阅发布

mountComponent函数会new Watcher一个对象,执行get方法【详见源码分析二】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
var _Set;
/* istanbul ignore if */ // $flow-disable-line
if (typeof Set !== 'undefined' && isNative(Set)) {
// use native Set when available.
_Set = Set;
} else {
// a non-standard Set polyfill that only works with primitive keys.
_Set = /*@__PURE__*/(function () {
function Set () {
this.set = Object.create(null);
}
Set.prototype.has = function has (key) {
return this.set[key] === true
};
Set.prototype.add = function add (key) {
this.set[key] = true;
};
Set.prototype.clear = function clear () {
this.set = Object.create(null);
};
return Set;
}());
}
var Watcher = function Watcher (...args){
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
}
Watcher.prototype.get = function get () {
pushTarget(this); //将当前Watcher挂到第三方Dep.target上
var value;
var vm = this.vm;
try {
//this.getter会执行render,触发数据的get方法,进行相互订阅
value = this.getter.call(vm, vm);
} catch (e) {
...
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
function pushTarget (target) {
targetStack.push(target);
Dep.target = target;
}
//watcher和dep互相存id
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this); //将watcher加入到dep的sub属性中
}
}
};
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
Watcher.prototype.run = function run () {
if (this.active) {
var value = this.get(); //重新调用render函数更新数据
if (
value !== this.value ||
//对于需要深度监听的数据类型,值没有变,也许值内容发生了变化
isObject(value) ||
this.deep
) {
var oldValue = this.value;
this.value = value;
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};

vue3.0数据响应式原理

vue3.0的改进

1.对源码进行优化
使用monorepo和TS管理和开发源码,提升自身代码的可维护性
2.性能优化
减少源码体积
移除冷门的feature(如filter,inline-template)
引入tree-shaking,减小打包体积

数据劫持优化
原来缺点
无法判断对象属性删除新增
深度层次结点要递归遍历,也不知道会不会用到,都要劫持,不友好

解决
使用proxy API 可以检测到删除还是新增
在用到时才劫持,避免无脑递归

3.编译优化
判断更新的颗粒度变小,从原来的组件级,利用block tree的区块细化到动态节点级
引入 compositionAPI 优化逻辑关注点相关的代码,可以避免mixin的时候的命名冲突

My Little World

sso单点登录实现

发表于 2020-03-25

背景

多项目想要实现统一登录,即登录一个系统后,在打开其他相关系统页面后,是已经登录的状态,不再进行登录

原理

多个系统使用统一的顶级域名,利用cookie的domain属性,将登录后cookie保存在浏览器中,只要是该cookie的domain所包含的域名的站点,都可以拿到这个cookie

实现

选择原来多个站点中其中一个站点的登录页,作为公共跳转页
在登录之后将token塞到浏览器中
这里设置统一token在cookie中的key值为_a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
根据运行环境设置domain,sit,dev均为测试环境
function getDomain(name){
let testENVs = ['localhost','sit','dev']
let testENV = ''
testENVs.forEach(env=>{
if(location.hostname.includes(env)){ //根据域名判断环境
testENV = env
}
})
let temp = testENV?testENV +'.xxxx.com':'xxxx.com'
//本地开发不加domain,默认localhost,否则本地没办法进行开发
let domain = testENV === 'localhost' ? '': (name === '_a'? temp:location.hostname)//个别cookie字段仅用于本站点,所以domain设置为站点域名
return domain
}
setCookie=(name,value) => {
const Days = 0.5
const exp = new Date()
exp.setTime(exp.getTime() + Days*24*60*60*1000)
let str = name + '=' + escape(value) + ';expires=' + exp.toGMTString()
let domain = getDomain(name)
if(domain){
str +=';domain=' + domain
}
document.cookie = str
},

之所以要根据不同运行环境设置不同domain,是因为要在不同测试环境也要实现单点登录
比如测试环境在sit环境的a.sit.xxxx.com和b.sit.xxxx.com
a.sit.xxxx.com登录页作为公共登录页
在a.sit.xxxx.com登录后,token的cookie会被设置成
key是_a,domain是sit.xxxx.com(浏览器会自动在前面加一个点),这样打开b.sit.xxxx.com的页面,浏览器中还会存在_a这个cookie,b.sit.xxxx.com就可以直接拿这个cookie向服务器发请求了

以此类推,在dev环境,这两个站点域名会变成
a.dev.xxxx.com和b.dev.xxxx.com
_a的domain会被设置成dev.xxxx.com
这时如果去b.dev.xxxx.com,_a是会有的
但去a.sit.xxxx.com,是不会有的,因为_a此时仅限于dev.xxxx.com域名及其子域名

等到了生产环境,二者域名变为
a.xxxx.com和b.xxxx.com
_a的domain会被设置成xxxx.com
此时就会出现一个问题,假如我先登录了线上环境
有了domain为xxxx.com的_a
再去测试环境登录,此时会加进去domain为sit/dev.xxx.com的_a
两个_a如何区分哪个才是当前环境的_a?

遇到的问题一 :测试环境_a加不进去,出现一闪而过
开始以为是浏览器机制问题,后来才发现,是代码逻辑问题
其实cookie是有写进浏览器的,跟cookie本身处理机制无关,可以写进去,问题出在了取cookie _a再给后台发请求的时候,以前是保存token的只有一个特殊key名,直接取那个key名就行,现在有两个_a,逻辑还是按照第一个来取,结果取错了,后台给发过来401,前端代码处理http状态码的逻辑又是401清cookie跳登录,所以domain为. sit/dev. xxxx. com的cookie就又消失了😅

遇到问题二 :两个_a如何区分哪个才是当前环境的_a?
因为后台实现分层,与前端直接对接的后台,无法访问后台的用户cookie表,能拿到cookie表的后台站在大局角度看问题,这个逻辑又觉得不是有必要加的,所以不能要求后台拿到报文中cookie后去表里面做判断
所以由前端通过读取document.cookie,整理拿到两个_a后,依次用每个_a去给后台发请求,如果成功了,说明当前_a是正确的,就可以用这个_a去发真正的请求,同时记录下这个_a,避免之后的重复探测

小结

1.碰到问题一时,很无厘头,但分析出原因后,让人感觉很无语,正确的说是,让人很没面子,遇到问题应该善于分析,戒骄戒躁
2.问题二,有时候,你认为很恶心的做法,恰巧可能会被采纳

后记

发版到线上后,发现部分同事登陆成功后又跳回登录页面,中间发起的请求报401

经过查看同事浏览器的cookie发现,存在设置了httponly的_a,即我们所有系统统一使用的token标识,
cookie设置了httponly意味着js没有权限进行获取更改甚至不能删除,所以新登录的token是没有写进去的,拿到的token也就是错的
解决办法就是让后台同事强制写一个没有httponly的_a进去,让后台覆盖后台,
说明浏览器优先级是先处理set-cookie的cookie逻辑,再看是否有httponly,决定js是否有权限处理cookie

相关链接
Cookie写不进去问题深入调查 https Secure Cookie
js创建cookie时获取一级域名设置domain解决跨域问题
js与cookie的domain和path之间的关系
前端单点登录(SSO)实现方法(二级域名与主域名)

My Little World

使用vuepress构建文档系统

发表于 2020-03-24

背景

面向普通用户的应用系统,包含很多产品的使用说明,属于静态页面
一方面占据整体项目的体积,打包时间用时长,打包结果体积大
另一方面,不断新增的不同产品的使用说明,对于前端开发人员来说,
相对开发真正的产品功能来说优先级较低,且占用开发时间

所以希望将产品使用说明与当前项目隔离,建立静态网站,存放使用说明
再由原系统进行跳转
将新增使用说明的任务交由相关产品负责人等非开发人员进行新增

问题

1.对于非开发人员来说,选择何种文档格式,可以既方便编写,又可以轻松转成html
2.原有系统用vue编写的批量文件如何迁移到新的静态网站

知识

vuepress

vuepress是一款使用vue驱动的静态网站生成器
用户可以使用markdown格式文件编辑文本,然后转义成html
另外页面中功能还可以使用vue进行嵌入

批量迁移

原系统vue编写的批量页面含有vue语法,不能直接迁移
所以需要将生成好的html爬下来转成md文件,应用到新系统
‘html-to-md’的npm 包可以帮助将html转换成md

解决

vuepress搭建

因为要与原系统主题风格样式类似,故需要在原来主题代码上进行修改
安装好vuepress包之后
将包里面的默认主题@vuepress/theme-default复制粘贴到doc/.vuepress/theme文件夹下面
修改里面的代码就可以直接应用了

配置config.js文件

在.vuepress文件夹下新增config.js

1
2
3
4
5
6
7
8
title:标签页名称
head:在页面头部引入的文件
base:当不在域名根目录下部署时,配置部署路径
themeConfig:{ 主题配置
logo:标题栏左上角logo
nav:右上角搜索后面的按钮
sidebar:左侧侧边栏
}

enhanceApp.js

在.vuepress文件夹下新增enhanceApp.js
该文件,可以添加对项目的额外处理
比如当前项目右上角三个按钮根据权限显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
export default ({
Vue, // VUE构造函数对象
options, // vue实例选项
router, // 路由对象
siteData//配置的数据
}) => {
async function getAuthorization(){
if(location.hostname.includes('localhost')){
return true
}
let cookie_str = document.cookie
let cookie_parts = cookie_str.split(';')
let auths = []
cookie_parts.forEach(c=>{
if (c){
let kv = c.split('=')
let k = kv[0].trim()
let v = kv[1].trim()
if (k == '_a'){
auths.push(v)
}
}
})
if(auths.length<1){
return false
}
for (var c of auths){
try {
let response = await axios.get(location.origin + '/api/v1/edit-user', {headers: {'X-Auth-Token': c}})
if(response.status < 400){
return true
}
} catch (err) {
console.log(err)
return false
}
}
}
getAuthorization().then(flag=>{
let divs = document.getElementsByClassName('nav-item')
if(flag){
divs[1].style.display = 'none'
divs[2].style.display = 'none'
}
})
}

更改页面呈现样式

增加悬浮锚点定位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
page.vue
<template>
<main class="page">
<div v-show="$page.headers && isShowAncher && $page.frontmatter.ancherShow" class='ancherSection'>
<template v-for="(item,i) in headers">
<div :key="i">
<a @click="gotoAncher(item.slug)">{{item.title}}</a>
</div>
</template>
</div>
<slot name="top" />

<Content class="theme-default-content" />
<PageEdit />

<PageNav v-bind="{ sidebarItems }" />

<slot name="bottom" />
</main>
</template>

export default {
components: { PageEdit, PageNav},
props: ['sidebarItems'],
data () {
return {
isShowAncher: false, //滚动过程控制悬浮锚点
}
},
computed:{
headers:function(){//计算锚点内容
let ancherLevel = this.$page.frontmatter.ancherLevel
let sidebarDepth = this.$page.frontmatter.sidebarDepth?this.$page.frontmatter.sidebarDepth:this.$themeConfig.sidebarDepth
let levelStr = ancherLevel?ancherLevel:(sidebarDepth>1?'h2,h3':'h2')
if(ancherLevel && !(/^(h2|h3|h2\,\s*\h3)$/ig.test(ancherLevel.trim()))){
return []
}
if(levelStr.includes(',')){
return this.$page.headers && groupHeaders(this.$page.headers).length>0?this.$page.headers:[]
}else{
let level = +levelStr[1]
return this.$page.headers.filter(h => h.level === level)
}
}
},
updated(){
this.setPictureZoom()
},
mounted () {
let handleScroll= ()=>{
let scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop // 滚动条偏移量
this.isShowAncher = !!scrollTop
}
window.addEventListener('scroll',handleScroll,false)
this.setPictureZoom()
},
methods:{
//锚点定位
gotoAncher(ancher){
let el = document.getElementById(ancher)
let anchersHeight = document.getElementsByClassName('ancherSection')[0].clientHeight
window.scrollTo({
top: el.offsetTop-anchersHeight,
behavior: "smooth"
})
},
//添加图片放大功能
setPictureZoom () {
let img = document.querySelectorAll('p > img')
for (let i = 0; i < img.length; i++) {
if (img[i]) {
let a = document.createElement("a");
let parent = img[i].parentNode
parent.appendChild(a)
let src = img[i].getAttribute('src')
a.setAttribute('data-fancybox', '')
a.setAttribute('href', src)
a.appendChild(img[i])
}
}
}
}
}
</script>

<style lang="stylus">
@require '../styles/wrapper.styl'
.page
padding-bottom 2rem
display block
.ancherSection
position: fixed
background-color: white
box-shadow: rgb(234, 229, 229) 0px 1px 2px;
top: 64px
min-height: 42px
width: calc(100% - 320px);
display flex
align-items center
justify-content left
flex-wrap: wrap;
padding: 10px 0px 0px;
z-index:2
div
margin-left 20px
margin-bottom: 10px;
a
color black
cursor pointer

</style>

更换图标

在sidebarGroup.vue文件同级别新增arrow.svg图片,使用相对路径可插入

1
<img src='./arrow.svg' v-if="collapsable" :class="open ? 'down' : ''" style='width:20px' />

批量迁移

目录改造

原有系统页面图片按照产品名命名文件夹,故先将整体文件结构进行改造
原来目录结构

1
2
3
4
5
6
7
8
9
10
11
--content
--a
--a1.png
--a2.png
......省略若干
--a13.png
--b
--b1.png
--b2.png
......省略若干
--b13.png

改造成

1
2
3
4
5
6
7
8
9
10
11
12
13
--content
--a
--images
--a1.png
--a2.png
......省略若干
--a13.png
--b
--images
--b1.png
--b2.png
......省略若干
--b13.png

就是在当前父文件夹下新增images文件夹,然后把图片移进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var fs = require('fs');//引用文件系统模块
function readFileList(path, filesList) {
var files = fs.readdirSync(path);
files.forEach(function (itm) {
var obj = {};//定义一个对象存放文件的路径和名字
obj.path = path;//路径
obj.filename = itm//名字
filesList.push(obj);
})
}
var getFiles = {
//获取文件夹下的所有文件
getFileList: function (path) {
var filesList = [];
readFileList(path, filesList);
return filesList;
}
}
let files = getFiles.getFileList("./content/") //拿到a,b这一级别文件
for(let i=0;i<files.length;i++){
let filepath = files[i].path+files[i].filename+'/' //拼接a,b路径
let images = getFiles.getFileList(filepath) //拿到a,b下一级的文件,即所有图片
let imgpath = filepath+'images'
fs.mkdirSync(imgpath);//新建文件夹
for(let j = 0;j<images.length;j++){ //将图片移到新文件夹中
let oldpath = images[j].path+images[j].filename
let newpath = imgpath+'/'+images[j].filename
fs.rename(oldpath,newpath,function(err){
console.log(err)
})
}
}

转义文件

因为原项目为单页面文件,无法进行爬虫批量获取
最终实现也还是手动copy页面中dom,一个页面一个页面的copy
然后处理

安装转义包

1
npm install html-to-md

开始使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const html2md=require('html-to-md')
const fs = require('fs');
//在页面中找到使用说明的dom,copy之后存放到temp.txt中
//当做字符串被读取
var contentText = fs.readFileSync('temp.txt','utf-8');
let res = html2md(contentText)
//匹配md的‘[]()’书写形式
let reg1=/\[([\S ]*?)]\s?()\( *<?([^\s'"]*?(?:\([\S]*?\)[\S]*?)?)>?\s*(?:()(['"])(.*?)\5)? *\)/g
//匹配md的‘![]()’书写形式
let reg2 = /!\[([^\]]*?)][ \t]*()\([ \t]?<?([\S]+?(?:\([\S]*?\)[\S]*?)?)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g
//在每篇开头注入转义规则front matter
res = '---\nsidebarDepth: 2 \nancherShow: true \nancherLevel: h3 \n--- \n' + res.replace(reg1,function(group,text){ //html-to-md包转义结果将标题转移成没有连接但有连接名的书写形式[xxx](),所以这里将其替换成 md写法
return text?'## '+text:group
}).replace(reg2,function(...res){ //将图片引用路径做统一处理
let arr = res[3].slice(res[3].lastIndexOf('/')+1).split('.')
return '![](./images/'+arr[0]+'.'+arr[2]+')'
})
//将处理后的文件写入相应的文件夹中的manual.md文件中,即生成md文件
fs.writeFile("./content/xxxx/manual.md", res, function(err) {
if(err) {
return console.log(err);
}
console.log("The file was saved!");
});

所以最终文件夹目录为

1
2
3
4
5
6
7
--content
--a
--images
--manual.md
--b
--images
--manual.md

现在将content目录下文件全都粘贴到docs文件夹下面,与.vuepres平级

1
2
3
4
5
6
7
8
--docs
--.vuepress
--a
--images
--manual.md
--b
--images
--manual.md

就可以在.vuepress文件夹下面的config.js中配置sidebar了

1
2
3
4
5
6
7
8
9
 sidebar: [
{
title: '产品a',
collapsable: true,
children:[
['/a/manual.md', 'a操作手册'],
]
}
}

小结

1.如何将公共开源的项目更契合的应用到公司级项目,对原项目的破坏是避免不了的,比如替换主题颜色,替换相关图标,以及为了新增一些原来项目中没有的功能,而去更改原有处理逻辑,所谓二次开发,实质上,可能应该叫破坏性开发,不遵循原来的使用规则,在原来基础上,造自己的规则

2.对于大批量重复性工作的处理,要学会使用工具,
比如局部大量替换,借助vscode的局部锁定(先选取范围再点击下图1,锁定按钮)
再比如规则类似的替换,又要善于利用vscode的正则匹配去替换,如下图2
vscode
如果属于大规模处理,还要学会自己造工具
比如上述批量处理目录结构,改造md文件
人之所以为人,学会使用工具才能算是进入了文明社会

My Little World

一个发版问题

发表于 2020-02-19

问题

用户正在使用某个页面期间,后台发布新的版本,用户再切换其他页面,
浏览器会报一个资源找不到的error,导致页面崩溃

原理

最终发布的版本会经过打包处理,每次打包的产物,文件名会不同
新发布的版本是最新打包的结果,上个版本发布的是上次发版前的结果
后台发布到服务器的项目新版本不再包含上个版本发布的文件
即服务器仅有新版本的文件
如果触发切换页面,此时浏览器向服务器发起请求上个版本的文件
服务器仅有最新版本文件,不再包含上个版本文件
故返回404,浏览器崩溃

背景

使用vue框架编写项目,使用vue-router进行路由切换

解决

利用路由

在路由生命钩子函数中添加onerror的处理,
让当前页面重新load一下,把资源拉回来,再让用户自己去点击要切换的页面

1
2
3
4
5
6
7
8
router.onError((error) => {
const pattern = /Loading chunk (\d)+ failed/g
const isChunkLoadFailed = error.message.match(pattern)
if (isChunkLoadFailed) {
$Message('系统更新,页面将进行刷新操作!')
window.location.reload()
}
})

轮询

使用webpack打包的时候生成一个hash.json的文件,然后前端轮询,
发现hash改变,就弹窗提示用户进行更新

借助CDN缓存策略

发版代码放到cdn上,用户未进行任何刷新操作则仍使用老代码,
用户进行了手动刷新,则拿到最新版本,进入新页面

My Little World

vueRouter 源码

发表于 2020-02-19
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<body>
<div id = 'app' class='hello'>
<p>
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar/user">Go to Bar</router-link>
</p>
<router-view></router-view>
</div>
<script type="text/javascript" src="vue.js"></script>
<script type="text/javascript" src="vue-router.js"></script>
<script>
const Foo = { template: '<div>foo</div>',mounted(){}}//路由foo组件
const Bar = {
template: '<div>bar<div>{{id}}</div><a @click="aa">go to foo</a></div>',//路由bar组件
data(){
return {
id:'',
}
},
methods:{
aa(){
console.log(this.$router)
this.$router.push({name:'foo',params:{name:'jck'}})
}
},
mounted(){
console.log(this.$route,this.$router)
this.id = this.$route.params.id
}
}
const routes = [
{ path: '/foo/:name',name:'foo', component: Foo },
{ path: '/bar/:id?',name:'bar', component: Bar }
]
const router = new VueRouter({routes})
let app = new Vue({
el:'#app',
data:{},
method:{},
router,
})
</script>
</body>

执行vue-Router.js文件

引入vue-router之前会先引入vue.js文件,执行会生成vue构造函数,
因为vue-router的执行过程会用到vue构造函数这个对象上的方法
当代码引入vue-router.js时,里面相应的代码会执行一遍,
此时vue构造函数已经生成
所以vue-router.js运行后,
不仅会生成vue-router的构造对象
还会调用vue构造函数上的use方法将与router相关的信息挂到vue构造函数对象上
即做一些注册挂载的初始化工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
(function (global, factory) {
global.VueRouter = factory());
}(this, function () { 'use strict';
//vue-router的构造对象
var VueRouter = function VueRouter (options) {
if ( options === void 0 ) options = {};
//options即new vueRouter时传进来的参数对象,这里只有一个属性即路由配置对象routes
this.app = null;
this.apps = [];
this.options = options;
this.beforeHooks = [];
this.resolveHooks = [];
this.afterHooks = [];
//该函数会根据路由配合生成路由表,返回两个函数,一个match函数用来匹配路由,另一个addRoutes用来动态增加路由,详见下文具体分析
this.matcher = createMatcher(options.routes || [], this);
var mode = options.mode || 'hash';//路由对象模式,没有配置的话默认初始化为'hash'
//fallback参数用于配置当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式
//默认值为 true 官文:https://router.vuejs.org/zh/api/#fallback
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false;
if (this.fallback) { //history模式下在不支持pushstate方法且配置允许回退hash的情况下回退hash模式
mode = 'hash';
}
if (!inBrowser) { //没有在浏览器中,那应该是在nodejs中,模式重置为abstract
mode = 'abstract';
}
this.mode = mode;//模式确定

switch (mode) { //初始化history对象,三种类型的history构造函数继承自同一父类构造函数History
case 'history':
this.history = new HTML5History(this, options.base);
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback);
break
case 'abstract':
this.history = new AbstractHistory(this, options.base);
break
default:
{
assert(false, ("invalid mode: " + mode));
}
}
};
VueRouter.install = install;
VueRouter.version = '3.1.5';

function install (Vue) { } //挂到VueRouter上,被vue.use函数调用

if (inBrowser && window.Vue) { //只在浏览器环境自动挂载,说明在node.js中需要自己调用Vue.use手动挂载
window.Vue.use(VueRouter);
}

return VueRouter;

}));

vue.use 与 VueRouter.install

vue的use方法会调用插件的install方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//在运行initGlobalAPI(Vue)函数时调用initUse挂载到VUE上
Vue.use = function (plugin) {
var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));//this是指Vue哦
if (installedPlugins.indexOf(plugin) > -1) { //避免重复挂载
return this
}
//这个方法会从arguments中取出从第一个数以后的所有参数,这里返回空数组===>[]
var args = toArray(arguments, 1);
args.unshift(this);//this是vue,将vue传给组件进行初始化 ==>[vue]
if (typeof plugin.install === 'function') { //组件定义了plugin.install方法,执行组件plugin.install方法
plugin.install.apply(plugin, args);
} else if (typeof plugin === 'function') { //没定义定义plugin.install方法且组件本身是函数,则调用自身
plugin.apply(null, args);
}
installedPlugins.push(plugin);//将插件推入vue对象上的installedPlugins集合中
return this
};

VueRouter.install方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function install (Vue) { //挂到VueRouter上,被vue.use函数调用
if (install.installed && _Vue === Vue) { return }//避免重复挂载
install.installed = true;

_Vue = Vue;

var isDef = function (v) { return v !== undefined; };

var registerInstance = function (vm, callVal) {
var i = vm.$options._parentVnode;
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal);//如果定义了registerRouteInstance就执行
}
};
//调用vue.mixin方法 将beforeCreate 和 destroyed,两个生命周期函数存到到对应的钩子函数的集合中
//在执行new vue时会调用callHook执行 beforeCreate相关的钩子函数
//在vue中执行destroyed钩子函数时,同样也会调用这里定义好的生命周期函数
Vue.mixin({
beforeCreate: function beforeCreate () { //这里this指向vue
if (isDef(this.$options.router)) {
this._routerRoot = this;
this._router = this.$options.router;
//调用vueRouter对象上的init方法,主要功能是初始化当前路由,设置监听 详见下文
this._router.init(this);
Vue.util.defineReactive(this, '_route', this._router.history.current);
//defineReactive会对数据进行劫持,进行依赖收集,方便之后页面切换进行监听
} else {//如果没有在new vue时传进vuerouter对象
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
}
registerInstance(this, this);
},
destroyed: function destroyed () {
registerInstance(this);
}
});
//访问this.$router,返回new vueRouter生成的对象
Object.defineProperty(Vue.prototype, '$router', {
get: function get () { return this._routerRoot._router }
});
//访问this.$route,当前页面路由对象 this._router.history.current
Object.defineProperty(Vue.prototype, '$route', {
get: function get () { return this._routerRoot._route }
});
//挂载RouterView,RouterLink两个组件
Vue.component('RouterView', View);
Vue.component('RouterLink', Link);
//使用vue生命钩子函数合并策略定义路由生命周期钩子函数合并策略 其实就是mergeHook函数
var strats = Vue.config.optionMergeStrategies;
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
}

小结

vueRouter在执行阶段会做以下两件事
1.生成vue-router的构造对象
2.挂载vuerouter相关信息到vue对象

挂载到vue对象时会做以下几件事
1.注入两个生命周期函数:beforeCreate和 destroyed
2.指定vue的$router和$route分别指向传进去的vueRouter对象的本身和history.current属性
3.挂载RouterView,RouterLink两个组件
4.指定路由生命周期函数合并策略使用vue中生命周期函数的合并策略

beforeCreate会在new vue时被调用,主要做以下几件事
1.在vue对象上指定与router相关的属性
2.调用vuerouter的init方法,初始化当前路由,设置监听
3.对当前路由进行数据劫持,进行监听

在new vueRouter时,会做以下几件事
1.根据路由配置数组生成路由表,匹配函数以及动态增加路由的函数
2.确定路由模式
3。根据路由模式创建history属性

一个疑惑

当跳转的路由带有query或者params时,将其中的值插入到页面中为什么不会引起XSS攻击?
因为拿到的值类型都是原始js类型,vue在替换值时只是当做字符串替换,不会进行innerHTML

使用js跳转路由

比如this.$router.push({name:foo,query:{id:1}})方法跳转路由
目的页面拿到的query/params数据,就是调用push函数传入的数据
当我们调用push函数时,就是在调用这段代码

1
2
3
4
5
6
7
8
9
10
VueRouter.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise(function (resolve, reject) {//进行异步跳转
this$1.history.push(location, resolve, reject);
})
} else {//进行同步跳转
this.history.push(location, onComplete, onAbort);
}
};

location就是我们传入的参数,没有传入 onComplete, onAbort,所以会进入if分支
可见无论进入哪个分支都会调用history对象的push方法
由于在new VueRouter对象时,在构造函数中history是根据传入的选项生成的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var mode = options.mode || 'hash';
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false;
if (this.fallback) {
mode = 'hash';
}
if (!inBrowser) {
mode = 'abstract';
}
this.mode = mode;

switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base);
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback);
break
case 'abstract':
this.history = new AbstractHistory(this, options.base);
break
default:
{
assert(false, ("invalid mode: " + mode));
}
}

所以push方法的实现可能为以下三种情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
HTML5History.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;

var ref = this;
var fromRoute = ref.current;
this.transitionTo(location, function (route) {
pushState(cleanPath(this$1.base + route.fullPath));
handleScroll(this$1.router, route, fromRoute, false);
onComplete && onComplete(route);
}, onAbort);
};

HashHistory.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;
var ref = this;
var fromRoute = ref.current;
this.transitionTo(
location,
function (route) {
pushHash(route.fullPath);//更改url
handleScroll(this$1.router, route, fromRoute, false);
onComplete && onComplete(route);
},
onAbort
);
};
AbstractHistory.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;
this.transitionTo(
location,
function (route) {
this$1.stack = this$1.stack.slice(0, this$1.index + 1).concat(route);
this$1.index++;
onComplete && onComplete(route);
},
onAbort
);
};

可见三种模式下都会调用共同的父类History上的transitionTo方法
接下来以HashHistory为基础分析接下来的流程

在HashHistory push调用transitionTo时,
传入三个参数:
location 即我们调用push时传入的参数
onAbort 调用push时没有传入,故该值为undefined
第三个参数是一个匿名函数

1
2
3
4
5
function (route) {
pushHash(route.fullPath);//更改url
handleScroll(this$1.router, route, fromRoute, false);//处理页面滚动相关功能
onComplete && onComplete(route);//调用push时没有传入onComplete,故onComplete=>undefined,不会被执行
},

可见匿名函数内将来要执行pushHash函数

pushHash函数更改浏览器url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function pushHash (path) {
if (supportsPushState) { 使用history.pushState更改URl
pushState(getUrl(path));
} else {//如果不支持history.pushState则直接更改hash值
window.location.hash = path;
}
}
function getUrl (path) {
var href = window.location.href;
var i = href.indexOf('#');
var base = i >= 0 ? href.slice(0, i) : href;
return (base + "#" + path)
}
function pushState (url, replace) {
saveScrollPosition();
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
var history = window.history;
try {
if (replace) { //在使用replace函数跳转路由会走该分支,由replaceState函数调用,replace传入为true
// 保留现有的历史记录状态,因为用户可能会覆盖它
var stateCopy = extend({}, history.state);
stateCopy.key = getStateKey();
history.replaceState(stateCopy, '', url); //使用window的history上的方法
} else { //没有传入replace,故进入该分支
history.pushState({ key: setStateKey(genStateKey()) }, '', url);
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url);
}
}

所以pushHash函数最终的目的是更改浏览器中的url
handleScroll函数处理页面滚动状态,这里暂不做深入
onComplete 值为undefined,故不执行

transitionTo

1
2
3
4
5
6
7
8
9
History.prototype.transitionTo = function transitionTo (
location,
onComplete,
onAbort
) {
var this$1 = this;
var route = this.router.match(location, this.current);//得到目的路由的route对象
this.confirmTransition(...暂时省略));
};

transitionTo函数中第一步就是根据根据当前路由信息和传入的目的路由信息生成目的路由对象

1
var route = this.router.match(location, this.current);

location 是我们调用push函数传入的参数,
this.current即当前页面路由信息对象

1
2
3
4
5
6
7
8
9
VueRouter.prototype.match = function match (
raw,
current,
redirectedFrom
) {
return this.matcher.match(raw, current, redirectedFrom)
};

this.matcher = createMatcher(options.routes || [], this); //new vuerouter时在构造函数中生成

createMatcher

createMatcher函数即创造匹配器的函数,主要做三件事
1.生成路由表
2.生成匹配器,用于将当然路由对象和目的路由信息对象合成目的路由对象
3.生成动态添加路由的函数,因这里可以直接使用路由表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
function createMatcher (
routes,
router
) {
var ref = createRouteMap(routes);//根据路由配置生成路由表
var pathList = ref.pathList;
var pathMap = ref.pathMap;
var nameMap = ref.nameMap;

function addRoutes (routes) { //动态增加路由
createRouteMap(routes, pathList, pathMap, nameMap);
}
//根据当前路由对象和目的路由信息以及是否重定向等条件合成目的路由对象返回
function match (
raw,
currentRoute,
redirectedFrom
) {
//统一路由参数,合并params给到目的路由
var location = normalizeLocation(raw, currentRoute, false, router);
var name = location.name;
if (name) {//走这条分支
var record = nameMap[name];
{
warn(record, ("Route with name '" + name + "' does not exist"));
}
if (!record) { return _createRoute(null, location) }
var paramNames = record.regex.keys //没有在路由path中配置动态参数,keys数组为空
.filter(function (key) { return !key.optional; })
.map(function (key) { return key.name; });

if (typeof location.params !== 'object') {
location.params = {}; //给location 挂上params
}

if (currentRoute && typeof currentRoute.params === 'object') {
for (var key in currentRoute.params) {//params中有相同key值时,从当前路由将值给到目的路由
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key];
}
}
}
//如果path有动态参数,使用params将path补全,//以name发起的跳转,挂上了path
location.path = fillParams(record.path, location.params, ("named route \"" + name + "\""));
return _createRoute(record, location, redirectedFrom) 可知返回一route对象
} else if (location.path) { //详见link跳转分析
location.params = {};
for (var i = 0; i < pathList.length; i++) {
var path = pathList[i];
var record$1 = pathMap[path];
if (matchRoute(record$1.regex, location.path, location.params)) {
console.log(location)
return _createRoute(record$1, location, redirectedFrom)
}
}
}
// no match
return _createRoute(null, location)
}
function redirect () {...对重定向一系列的处理,会返回createRoute()}
function alias () {...对路由配置中的别名进行处理,会返回createRoute()}
function _createRoute ( //会生成fullpath
record,
location,
redirectedFrom
) {
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
return {
match: match,
addRoutes: addRoutes
}
}

路由表生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
function createRouteMap ( //生成路由表
routes,
oldPathList,
oldPathMap,
oldNameMap
) {
// the path list is used to control path matching priority
var pathList = oldPathList || [];
// $flow-disable-line
var pathMap = oldPathMap || Object.create(null);
// $flow-disable-line
var nameMap = oldNameMap || Object.create(null);

routes.forEach(function (route) {
addRouteRecord(pathList, pathMap, nameMap, route);
});

// ensure wildcard(通配符) routes are always at the end
for (var i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0]);
l--;
i--;
}
}

{
// warn if routes do not include leading slashes(斜线)
var found = pathList
// check for missing leading slash
.filter(function (path) { return path && path.charAt(0) !== '*' && path.charAt(0) !== '/'; });

if (found.length > 0) {
var pathNames = found.map(function (path) { return ("- " + path); }).join('\n');
warn(false, ("Non-nested routes must include a leading slash character. Fix the following routes: \n" + pathNames));
}
}

return {
pathList: pathList,//数组,存放path值的集合
pathMap: pathMap,//对象,有配置path的路由record集合
nameMap: nameMap//对象,有配置name的路由record的集合
}
}
//根据配置的路由将其转换成内部使用的路由对象record,并根据配置情况放到不同路由集合(前三个参数)中,
function addRouteRecord (
pathList,
pathMap,
nameMap,
route,//具体配置的route
parent,//用于处理配置了children,被递归调用时传入,
matchAs
) {
var path = route.path;
var name = route.name;

var pathToRegexpOptions =
route.pathToRegexpOptions || {};
var normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict);

if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive;
}

var record = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
instances: {},
name: name,
parent: parent,
matchAs: matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props:
route.props == null
? {}
: route.components
? route.props
: { default: route.props }
};

if (route.children) {...}

if (!pathMap[record.path]) {
pathList.push(record.path);
pathMap[record.path] = record;
}

if (route.alias !== undefined) {...}

if (name) {
if (!nameMap[name]) {
nameMap[name] = record;
} else if ( !matchAs) {
warn(
false,
"Duplicate named routes definition: " +
"{ name: \"" + name + "\", path: \"" + (record.path) + "\" }"
);
}
}
}

合并params

在match函数中首先会整理参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function normalizeLocation (
raw,
current,
append,
router
) {
//link标签点击传入的raw是to标签指令的值, 为字符串
var next = typeof raw === 'string' ? { path: raw } : raw;
// named target
if (next._normalized) {//已经处理过
return next
} else if (next.name) { //当前调用push使用name标记路由,只会走这个分支
next = extend({}, raw);
var params = next.params;
if (params && typeof params === 'object') {
next.params = extend({}, params);
}
return next
}

// relative params
if (!next.path && next.params && current) {
next = extend({}, next);
next._normalized = true;
var params$1 = extend(extend({}, current.params), next.params);
if (current.name) {
next.name = current.name;
next.params = params$1;
} else if (current.matched.length) {
var rawPath = current.matched[current.matched.length - 1].path;
next.path = fillParams(rawPath, params$1, ("path " + (current.path)));
} else {
warn(false, "relative params navigation requires a current route.");
}
return next
}

var parsedPath = parsePath(next.path || '');
var basePath = (current && current.path) || '/';
var path = parsedPath.path
? resolvePath(parsedPath.path, basePath, append || next.append)
: basePath;

var query = resolveQuery(
parsedPath.query,
next.query,
router && router.options.parseQuery
);

var hash = next.hash || parsedPath.hash;
if (hash && hash.charAt(0) !== '#') {
hash = "#" + hash;
}
return {
_normalized: true,
path: path,
query: query,
hash: hash
}
}

创建路由route对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
function createRoute (
record,
location,
redirectedFrom,
router
) {
var stringifyQuery = router && router.options.stringifyQuery;

var query = location.query || {};
try {
query = clone(query);
} catch (e) {}

var route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query: query,//调用push函数传递进来的query,赋值时只能赋值js数据类型,拿到的也是js数据类型
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),//生成fullPath
matched: record ? formatMatch(record) : []
};
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery);
}
return Object.freeze(route) //禁止改动route对象
}
function getFullPath ( //根据query生成fullPath
ref,
_stringifyQuery
) {
var path = ref.path;
var query = ref.query; if ( query === void 0 ) query = {};
var hash = ref.hash; if ( hash === void 0 ) hash = '';

var stringify = _stringifyQuery || stringifyQuery;
return (path || '/') + stringify(query) + hash
}
//组装url上参数的部分
function stringifyQuery (obj) {
var res = obj ? Object.keys(obj).map(function (key) {
var val = obj[key];

if (val === undefined) {
return ''
}

if (val === null) {
return encode(key)
}

if (Array.isArray(val)) {
var result = [];
val.forEach(function (val2) {
if (val2 === undefined) {
return
}
if (val2 === null) {
result.push(encode(key));
} else {
result.push(encode(key) + '=' + encode(val2));
}
});
return result.join('&')
}

return encode(key) + '=' + encode(val)
}).filter(function (x) { return x.length > 0; }).join('&') : null;
return res ? ("?" + res) : ''
}

所以在调用完match之后会得到route,接着会调用confirmTransition

confirmTransition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
在transitionTo调用时
this.confirmTransition(
route,
function () {
this$1.updateRoute(route);//更新router对象
onComplete && onComplete(route);//调用transitionTo时传入的第二个参数函数,里面有pushHash来更改url
this$1.ensureURL();

// fire ready cbs once
if (!this$1.ready) {
this$1.ready = true;
this$1.readyCbs.forEach(function (cb) {
cb(route);
});
}
},
function (err) {
if (onAbort) {
onAbort(err);
}
if (err && !this$1.ready) {
this$1.ready = true;
this$1.readyErrorCbs.forEach(function (cb) {
cb(err);
});
}
}
);

History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) {
var this$1 = this;

var current = this.current;
var abort = function (err) {
// after merging https://github.com/vuejs/vue-router/pull/2771 we
// When the user navigates through history through back/forward buttons
// we do not want to throw the error. We only throw it if directly calling
// push/replace. That's why it's not included in isError
if (!isExtendedError(NavigationDuplicated, err) && isError(err)) {
if (this$1.errorCbs.length) {
this$1.errorCbs.forEach(function (cb) {
cb(err);
});
} else {
warn(false, 'uncaught error during route navigation:');
console.error(err);
}
}
onAbort && onAbort(err);
};
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
route.matched.length === current.matched.length
) {
this.ensureURL();
return abort(new NavigationDuplicated(route))
}

var ref = resolveQueue(
this.current.matched,
route.matched
);
var updated = ref.updated;
var deactivated = ref.deactivated;
var activated = ref.activated;

var queue = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(function (m) { return m.beforeEnter; }),
// async components
resolveAsyncComponents(activated)
);

this.pending = route;
var iterator = function (hook, next) {
if (this$1.pending !== route) {
return abort()
}
try {
hook(route, current, function (to) {
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this$1.ensureURL(true);
abort(to);
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort();
if (typeof to === 'object' && to.replace) {
this$1.replace(to);
} else {
this$1.push(to);
}
} else {
// confirm transition and pass on the value
next(to);
}
});
} catch (e) {
abort(e);
}
};

runQueue(queue, iterator, function () {
var postEnterCbs = [];
var isValid = function () { return this$1.current === route; };
// wait until async components are resolved before
// extracting in-component enter guards
var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
var queue = enterGuards.concat(this$1.router.resolveHooks);
runQueue(queue, iterator, function () {
if (this$1.pending !== route) {
return abort()
}
this$1.pending = null;
onComplete(route);//所有需要运行的钩子函数运行完执行更改url,更新current,处理页面滚动
if (this$1.router.app) {
this$1.router.app.$nextTick(function () {
postEnterCbs.forEach(function (cb) {
cb();
});
});
}
});
});
};
function runQueue (queue, fn, cb) {
var step = function (index) {
if (index >= queue.length) {
cb();
} else {
if (queue[index]) {
fn(queue[index], function () {
step(index + 1);
});
} else {
step(index + 1);
}
}
};
step(0);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
History.prototype.updateRoute = function updateRoute (route) {
var prev = this.current;
this.current = route;
this.cb && this.cb(route); //通知app更改_route属性值,即更改$route值
this.router.afterHooks.forEach(function (hook) {
hook && hook(route, prev);
});
};
this.cb在install的时候被定义如果获取
History.prototype.listen = function listen (cb) {
this.cb = cb;
};
//在new vue时被定义
VueRouter.prototype.init = function init (app ) {
history.listen(function (route) {
this$1.apps.forEach(function (app) {
app._route = route;
});
});
}
//install的时候
function install (Vue) {
Object.defineProperty(Vue.prototype, '$route', {
get: function get () { return this._routerRoot._route }
});
}

link标签跳转路由

点击link标签时,根据绑定函数即可知道,同样会调用router.push

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var router = this.$router;
var current = this.$route;
var ref = router.resolve(
this.to,
current,
this.append
);

var location = ref.location;
var route = ref.route;
var href = ref.href;

var handler = function (e) {
if (guardEvent(e)) {
if (this$1.replace) {
router.replace(location, noop);
} else {
router.push(location, noop);//push,直接走同步流程
}
}
};

var on = { click: guardEvent };
if (Array.isArray(this.event)) {
this.event.forEach(function (e) {
on[e] = handler;
});
} else {
on[this.event] = handler;
}

其中,经过resolve处理过的to标签属性的值,会生成统一规范的后续需要使用的location,route

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
VueRouter.prototype.resolve = function resolve (
to,
current,
append
) {
current = current || this.history.current;
var location = normalizeLocation(
to,
current,
append,
this
);
var route = this.match(location, current);
var fullPath = route.redirectedFrom || route.fullPath;
var base = this.history.base;
var href = createHref(base, fullPath, this.mode);
return {
location: location,
route: route,
href: href,
// for backwards compat
normalizedTo: location,
resolved: route
}
};

location 会在normalizeLocation中以第三个return的位置返回如下结构

1
2
3
4
5
6
{
_normalized: true,
path: path,
query: query,
hash: hash
}

这样相当于提前normalizeLocation调用,所以在match再调用的时候直接返回自身
在接下来的match流程中,会进入以path为依据的流程

1
2
3
4
5
6
7
8
9
10
else if (location.path) {
location.params = {};
for (var i = 0; i < pathList.length; i++) {
var path = pathList[i];
var record$1 = pathMap[path];//根据path拿到record对象
if (matchRoute(record$1.regex, location.path, location.params)) {
return _createRoute(record$1, location, redirectedFrom)
}
}
}

根据标签to赋予的path值,和生成路由表时拿到的path配置值,根据正则表达式解析params对象,
也说明了,为什么跳转路包含动态参数的path时,要跟params配对使用

matchRoute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function matchRoute (
regex,
path,
params
) {
var m = path.match(regex);
if (!m) {//当前路由不符合route定义时格式
return false
} else if (!params) {
return true
}
//匹配路由组装params
for (var i = 1, len = m.length; i < len; ++i) {
var key = regex.keys[i - 1];
var val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i];//始终为子字符串
if (key) {
// Fix #1994: using * with props: true generates a param named 0
params[key.name || 'pathMatch'] = val;
}
}

return true
}

直接更改url跳转

在浏览器直接更改含有动态路由的的url,回车跳转,会根据事先绑定的监听事件进行解析处理
在new vue时初始化完当前路由,以执行回调的方式绑定监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  VueRouter.prototype.init = function init (app /* Vue component instance */) {
var this$1 = this;

this.app = app;

var history = this.history;

if (history instanceof HTML5History) { //h5history在new h5history时,在构造函数中设置popstate事件监听
history.transitionTo(history.getCurrentLocation());
} else if (history instanceof HashHistory) {
var setupHashListener = function () {
history.setupListeners();//设置监听
};
history.transitionTo( //无论成功与否都绑定
history.getCurrentLocation(),
setupHashListener,
setupHashListener
);
}

history.listen(function (route) { //route改变时调用,this.cb
this$1.apps.forEach(function (app) {
app._route = route;
});
});
};

设置监听具体内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
HashHistory.prototype.setupListeners = function setupListeners () {
var this$1 = this;

var router = this.router;
var expectScroll = router.options.scrollBehavior;
var supportsScroll = supportsPushState && expectScroll;

if (supportsScroll) {
setupScroll();
}

window.addEventListener(
supportsPushState ? 'popstate' : 'hashchange',
function () {
var current = this$1.current;
if (!ensureSlash()) {
return
}
//直接调用transitionTo进行路由更新
this$1.transitionTo(getHash(), function (route) { //成功回调不用pushHash更新url
if (supportsScroll) {
handleScroll(this$1.router, route, current, true);
}
if (!supportsPushState) {
replaceHash(route.fullPath);
}
});
}
);
};

可见通过监听事件触发的路由改变会直接调用transitionTo函数进行跳转

两个收获

1.normalizeLocation函数发挥的作用,根据第一参数location对象不同的属性状态进行不同的处理,用于在上游的不同情境(不同方式导致跳转)使用中统一调用
2.在transitionTo函数中调用comfirmTransition,为什么不直接在transitionTo函数中书写confirmTransition函数内容呢,为什么要单独摘出来呢?同样,comfirmTransition函数不止会在transition中调用,
还会在AbstractHistory的go函数中被直接调用,HashHistory和HTML5History的go方法会直接使用window.history.go(n);nodejs全局对象是global,不是windows,所以需要特殊处理

vuerouter

My Little World

vuex 一些api使用规则

发表于 2020-02-05

官网地址
基本思想:
通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性
以一个全局单例模式管理抽取出来的组件共享状态
思路图

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Vue.use(Vuex) //会调用install,在beforeMount声明周期进行初始化工作

const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})

const app = new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
components: { Counter },
template: `
<div class="app">
<counter></counter>
</div>
`
})

const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}

state

具体变量声明对象,存放每个具体的公共变量

声明

1
2
3
4
5
const store = new Vuex.Store({
state: {
count: 0
},
)

在vue中使用—访问

1
this.$store.state.count

辅助函数mapstate

getter

相当与vue的计算属性
访问声明的属性时,执行对应的函数,拿到的值为函数返回的值
如果函数返回另一个函数,则可以调用返回的函数去实现相应的功能
适用于多个组件共享一个同样处理逻辑的函数

getter 的返回值会根据它的依赖被缓存起来
且只有当它的依赖值发生了改变才会被重新计算
Getter 接受 state 作为其第一个参数

声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
},
// 受其他 getter 作为第二个参数,调用其他getter方法
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
})

在vue中使用—访问

1
2
3
this.$store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
this.$store.getters.doneTodosCount // -> 1
this.$store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

映射辅助函数mapGetters

Mutation

存储同步改变state保存值的方法

声明

1
2
3
4
5
6
7
8
9
10
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state, payload) { //payload即调用时传递进来的参数
state.count += payload.amount
}
}
})

在vue中使用—调用

1
2
3
this.$store.commit('increment', {
amount: 10
})

注意事项
1.提前在 store 中初始化好所有所需属性。
2.当需要在对象上添加新属性时,应该使用 Vue.set或者以新对象替换老对象
映射辅助函数mapMutations

Action

存储异步改变state保存值的方法
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。但他不是 store 实例本身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
},
incrementAsync ({ commit }) { //结构仅拿到contex里面的commit
setTimeout(() => {
commit('increment')
}, 1000)
},
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
},
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
},
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
})

在VUE中使用—调用

1
2
3
4
5
6
7
8
9
10
11
this.$store.dispatch('incrementAsync', { //作为第二个参数传递action
amount: 10
})

this.$store.dispatch('actionA').then(() => {
// ...
})

this.$store.dispatch('actionB').then(() => {
// ...
})

Modules

声明多个store配置对象,每个作为一个模块
每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块
套嵌条件下,state 会处理成树状结构进行访问,但要进行更改操作时,
如果有子模块没有进行命名空间设置,但又包含跟全局状态相同处理名称的 mutation 或 action
则在调用时,都会执行

默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
let moduleA = {
namespaced: true,
state(){
return{
counta: 0, //分别在模块b和全局下被使用,直接返回对象会通过引用被共享,防止模块间数据污染,使用函数,每次生成新对象,原理同vue的data
}
},
mutations: { // this.$store.commit('b/aa/increment') 仅触发模块b下面的aa子模块
increment (state) { //this.$store.commit('a/increment') 仅触发全局的模块a 对应的子模块
state.counta++
},
incrementaaa (state) { //在被引用时,套嵌层如果都没有namespaced: true,可以直接用this.$store.commit('incrementaaa')调用进行更改
state.counta++
}
}
}

let moduleB = {
namespaced: true,
state: {
countb: 0,//this.$store.state.b.countb
},
mutations: {
increment (state) { //如果不加namespaced,执行this.$store.commit('increment'),会同时执行
state.countb++
}
},
modules: {//如果moduleB不加namespaced,aa可以访问数据但不能调用this.$store.commit('b/aa/increment')进行更改
aa: moduleA,//this.$store.state.b.aa.counta
}
}
// vuex相关代码
const store = new Vuex.Store({
state: {
count: 0, //this.$store.state.count
},
mutations: {
increment (state) { //this.$store.commit('increment') 子模块有相同函数时,都会触发执行
state.count++
}
},
modules: {
a: moduleA, //this.$store.state.a.counta
b: moduleB //this.$store.state.b.countb
}
})

小结
namespaced: true相当于子模块的隔离层,
如果设置了,外界不能直接通过mutations里面的函数名进行commit调用,
即当调用相同函数名时,可以防止一起被调用
如果没有设置,则当调用相同类型commit时,会一起被调用

1.对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。
2.带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名
3.对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const moduleA = {

namespaced: true,

state: { count: 0 },
mutations: {
increment (state) {
// 这里的 `state` 对象是模块的局部状态
state.count++
}
},

getters: {
doubleCount (state) {
return state.count * 2
}
},

actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}

更新补充

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
let moduleA = {
namespaced: true,
state(){
return{
counta: 0, //分别在模块b和全局下被使用,直接返回对象会通过引用被共享,防止模块间数据污染,使用函数,每次生成新对象,原理同vue的data
}
},
getters:{
getCount(...args){ //this.$store.getters['b/aa/getCount'] 作为b子模块时
console.log(args)
return 'this is a getter~~~~~'
}
},
mutations: { // this.$store.commit('b/aa/increment') 仅触发模块b下面的aa子模块
increment (state) { //this.$store.commit('a/increment') 仅触发全局的模块a 对应的子模块
state.counta++
},
incrementaaa (state) { //在被引用时,套嵌层如果都没有namespaced: true,可以直接用this.$store.commit('incrementaaa')调用进行更改
state.counta++
}
}
}

let moduleB = {
namespaced: true,
state: {
countb: 0,//this.$store.state.b.countb
},
getters:{
getCount(...args){ //this.$store.getters['b/getCount'] 因为设置了namespaced所以可以重名
console.log(args)
return 'this is a getter~~~~~'
}
},
mutations: {
increment (state) { //如果不加namespaced,执行this.$store.commit('increment'),会同时执行
state.countb++
}
},
modules: {//如果moduleB不加namespaced,aa可以访问数据但不能调用this.$store.commit('b/aa/increment')进行更改
aa: moduleA,//this.$store.state.b.aa.counta
}
}
// vuex相关代码
const store = new Vuex.Store({
state: {
count: 0, //this.$store.state.count
},
getters:{
getCount(...args){ //this.$store.getters.getCount
console.log(args)
return 'this is a getter'
}
},
mutations: {
increment (state) { //this.$store.commit('increment') 子模块有相同函数时,都会触发执行
state.count++
}
},
modules: {
a: moduleA, //this.$store.state.a.counta
b: moduleB //this.$store.state.b.countb
}
})

My Little World

vue 源码学习二【编译器】

发表于 2020-02-05

Vue声明周期
分析官网的流程图可知
new Vue时,进行一系列initxxx初始化工作后会根据选项el和template配置情况去做编译
即
情况1:挂载了el和template情况下直接去将template编译成渲染函数
情况2:挂载了el而没有挂载template情况下,将以el外部的html为template进行编译
情况3:没有挂载el的情况下,需要手动调用$mount函数挂载,这时再去判断有没有挂载template,在进行依据template有无继续接下来的编译工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Vue.prototype._init = function (options) {
var vm = this;
vm._uid = uid$3++;
var startTag, endTag;
vm._isVue = true; // 防止被监听的标识
//合并内置选项与传进来的选项
if (options && options._isComponent) {
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
/* istanbul ignore else */
{
initProxy(vm);
}
//进行一系列初始化
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm);
initState(vm);//对数据进行劫持
initProvide(vm);
callHook(vm, 'created');

if (vm.$options.el) { //判断有没有配置el,有则调用vm.$mount进入情况1和2,没有则进入情况3,等待手动调用vm.$mount
vm.$mount(vm.$options.el);//生成渲染函数后,给option挂上render,staticRenderFns属性
}
};
}

$mount挂载函数的巧妙设计

一开始定义的$mount仅用于运行时,功能是将编译好的渲染函数运行实现挂载组件

1
2
3
4
5
6
7
Vue.prototype.$mount = function ( 
el,
hydrating
) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};

之后会将运行时定义存放到另一个变量
方便定义之后包含编译步骤的挂载函数,
同时编译运行方案还会调用这个函数
这样在打包时,就可以方便的去掉进行编译过程的代码

1
var mount = Vue.prototype.$mount;

在定义含有编译过程的挂载函数,将template编译成渲染函数
再调用运行时$mount(这时依托在mount变量上),进行挂载组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && query(el);//query检查挂载点dom是否存在
//不要挂载到<body>元素或者<html>元素,
//因为挂载点的本意是组件挂载的占位,它将会被组件自身的模板替换掉,而<body>元素和<html>元素是不能被替换掉的。
if (el === document.body || el === document.documentElement) {
warn(
"Do not mount Vue to <html> or <body> - mount to normal elements instead."
);
return this
}

var options = this.$options;
// 解析 template/el 转换成render函数
if (!options.render) {
var template = options.template;
if (template) { //配置了template
if (typeof template === 'string') {
//如果第一个字符是 #,那么会把该字符串作为 css 选择符
//去选中对应的元素,并把该元素的 innerHTML 作为模板
if (template.charAt(0) === '#') {
template = idToTemplate(template);
if (!template) {
warn(("Template element not found or is empty: " + (options.template)),this);
}
}
} else if (template.nodeType) {
//如果第一个字符不是 #,且template的类型是元素节点,
//那就用 template 自身的字符串值作为模板
template = template.innerHTML;
} else {{ warn('invalid template option:' + template, this);}
//否则报错不存在配置的template
return this
}
} else if (el) { //如果template选项不存在,那么使用el元素的outerHTML作为模板内容
template = getOuterHTML(el);
}
if (template) {
//编译生成静态结点渲染函数和动态结点渲染函数
var ref = compileToFunctions(template, {...参数下文有说明}, this);
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
//生成渲染函数后,给option挂上render,staticRenderFns渲染函数属性
options.render = render;
options.staticRenderFns = staticRenderFns;
}
}
return mount.call(this, el, hydrating) //调用运行时挂载函数
};

compileToFunctions

compileToFunctions 作用就两个
1.调用compile编译生成渲染函数字符串
2.将渲染函数字符串生成渲染函数返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function createCompileToFunctionFn (compile) {
var cache = Object.create(null);
return function compileToFunctions (template, options,vm) {
options = extend({}, options);
var warn$$1 = options.warn || warn;
delete options.warn;
{ //检测 new Function() 是否可用,
//在某些极端情况下(如CSP限制)给一个有用的提示
try {new Function('return 1');} catch (e) {
if (e.toString().match(/unsafe-eval|CSP/)) {
warn$$1(...警告内容);
}
}
}
// 有缓存直接返回缓存,不用再进行编译一次,
// cache在闭包父域定义,相当于全局,
// 所以每次编译的结果都可以缓存起来
//options.delimiters这个选项是配置纯文本插入分隔符
//这里用来拼接做缓存key值
var key = options.delimiters
? String(options.delimiters) + template
: template;
if (cache[key]) {
return cache[key]
}

//调用 baseCompile编译模板并返回其生成的值
var compiled = compile(template, options);

//将字符串代码转换成函数
var res = {};
var fnGenErrors = [];
res.render = createFunction(compiled.render, fnGenErrors);
res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
return createFunction(code, fnGenErrors)
});
return (cache[key] = res)//缓存字符串模板的编译结果,防止重复编译,提升性能
}
}

try-catch语句如果有错误发生且错误的内容中
包含如 ‘unsafe-eval’ 或者 ‘CSP’ 这些字样的信息时就会给出一个警告

CSP全称Content Security Policy ,可以直接翻译为内容安全策略
就是为了页面内容安全而制定的一系列防护策略
通过CSP所约束的的规责指定可信的内容来源
(这里的内容可以指脚本、图片、iframe、fton、style等等可能的远程的资源)。
通过CSP协定,让WEB处于一个安全的运行环境中

如果你的策略比较严格,那么 new Function() 将会受到影响,从而不能够使用
但是将模板字符串编译成渲染函数又依赖 new Function(),所以解决方案有两个:
1、放宽你的CSP策略
2、预编译

一些变量参数说明

因为之后分析会用到,事先了解一下

变量之间关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var modules$1 = [
klass$1,
style$1,
model$1
];

var klass$1 = {
staticKeys: ['staticClass'],
transformNode: transformNode,
genData: genData
};

var style$1 = {
staticKeys: ['staticStyle'],
transformNode: transformNode$1,
genData: genData$1
};

var model$1 = {
preTransformNode: preTransformNode
};

baseOptions

baseOptions包含编译器在运作的时候所需的基本配置选项。

1
2
3
4
5
6
7
8
9
10
11
12
var baseOptions = {
expectHTML: true,
modules: modules$1,
directives: directives$1,
isPreTag: isPreTag,
isUnaryTag: isUnaryTag,
mustUseProp: mustUseProp,
canBeLeftOpenTag: canBeLeftOpenTag,
isReservedTag: isReservedTag,
getTagNamespace: getTagNamespace,
staticKeys: genStaticKeys(modules$1)
};

第三个属性:directives 值是三个属性 (model、text、html) 的对象,且属性的值都是函数。
第四个属性:isPreTag 它是一个函数,其作用是通过给定的标签名字检查标签是否是 ‘pre’ 标签。
第五个属性:isUnaryTag 是一个通过makeMap生成的函数,该函数的作用是检测给定的标签是否是一元标签。
第六个属性:mustUseProp 它是一个函数,其作用是用来检测一个属性在标签中是否要使用props进行绑定。
第七个属性:canBeLeftOpenTag 一个使用makeMap生成的函数,它的作用是检测非一元标签,但却可以自己补全并闭合的标签。比如 div 标签是一个双标签,你需要这样使用<div> text </div>,但是你依然可以省略闭合标签,直接这样写:<div> text ,且浏览器会自动补全。但是有些标签你不可以这样用,它们是严格的双标签。
第八个属性:isReservedTag 它是一个函数,其作用是检查给定的标签是否是保留的标签。
第九个属性:getTagNamespace 它也是一个函数,其作用是获取元素(标签)的命名空间。
第十个属性:staticKeys 它的值是通过以 modules 为参数调用 genStaticKeys 函数的返回值得到的。 其作用是根据编译器选项的 modules 选项生成一个静态键字符串。

compileToFunctions传入的参数

实际调用时

1
2
3
4
5
6
7
var ref = compileToFunctions(template, {
outputSourceRange: "development" !== 'production',
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,//更改纯文本插入符
comments: options.comments//设置为true时,保留且渲染模板HTML注释,false时舍弃注释
}, this);

参数说明
在我们innerHTML获取内容时,换行符和制表符分别被转换成了&#10和&#9。
在IE中,不仅仅是 a 标签的 href 属性值,任何属性值都存在这个问题
这就会影响Vue的编译器在对模板进行编译后的结果,为了避免这些问题Vue需要知道什么时候要做兼容工作,
如果 shouldDecodeNewlines 为 true,意味着 Vue 在编译模板的时候,要对属性值中的换行符或制表符做兼容处理。
而shouldDecodeNewlinesForHref为true 意味着Vue在编译模板的时候,要对a标签的 href 属性值中的换行符或制表符做兼容处理。
delimiters和comments都是 Vue 提供的选项,Vue实例的$options属性
delimiters代表纯文本插入分隔符
comments代表编译时是否保留注释,会在parse阶段进行判断

compiler编译器生成

compiler的爷爷createCompilerCreator,
会生成compiler的爸爸createCompiler
createCompiler会生成compiler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
function createCompilerCreator (baseCompile) {
return function createCompiler (baseOptions) {
function compile (
template,
options
) {
//所有的配置选项最终都会挂载在这个finalOptions 对象上
var finalOptions = Object.create(baseOptions);
var errors = [];
var tips = [];
var warn = function (msg, range, tip) { (tip ? tips : errors).push(msg) };
//options为调用 compileToFunctions 函数时传递的选项参数。
//baseOptions理解为编译器的默认选项或者基本选项,options 是用来提供定制能力的扩展选项
if (options) {
// merge custom modules
//如果 options.modules 存在,就在 finalOptions 对象上添加 modules 属性,
//其值为 baseOptions.modules 和 options.modules 这两个数组合并后的新数组
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules);
}
// merge custom directives
//对于directives 采用原型链的原理实现扩展属性对基本属性的覆盖
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
);
}
// copy other options
for (var key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key];
}
}
}

finalOptions.warn = warn;

var compiled = baseCompile(template.trim(), finalOptions);//调用 baseCompile并返回其生成的值
{
detectErrors(compiled.ast, warn);
}
compiled.errors = errors;
compiled.tips = tips;
return compiled
}

return {
compile: compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}

传入实际编译器生成createCompiler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var createCompiler = createCompilerCreator(function baseCompile (
template,
options
) {
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
optimize(ast, options);
}
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
});
var ref$1 = createCompiler(baseOptions);
var compile = ref$1.compile;
var compileToFunctions = ref$1.compileToFunctions;

可见compileToFunctions也由createCompiler函数生成

createCompiler函数 由 createCompilerCreator函数在传入函数 baseCompile 条件下生成

在createCompiler函数中会声明compile函数,在compile中会调用 baseCompile并返回其生成的值

然后对 createCompileToFunctionFn 函数传入compile函数,返回一个函数即compileToFunctions

在compileToFunctions 函数中会调用compile函数并处理其返回值
最终生成render和staticRenderFns属性

所以可见在compileToFunctions还是在调用baseCompile来生成render和staticRenderFns属性,
现在摘出来主要逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//base函数
function baseCompile (
template,
options
) {
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
optimize(ast, options);
}
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}

// compileToFunctions调用compile
// 实际调用 baseCompile得到其生成的值
var compiled = compile(template, options);

// 调用后处理
//将字符串转成函数
var res = {};
var fnGenErrors = [];
res.render = createFunction(compiled.render, fnGenErrors);
res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
return createFunction(code, fnGenErrors)
});

baseCompile 的主要核心代码。

1
var ast =parse(template.trim(), options);

parse 会用正则等方式解析 template 模板中的指令、class、style等数据,形成AST。

1
optimize(ast, options);

optimize 的主要作用是标记 static 静态节点,
这是 Vue 在编译过程中的一处优化,后面当 update 更新界面时
会有一个 patch 的过程, diff 算法会直接跳过静态节点
从而减少了比较的过程,优化了 patch 的性能

1
var code =generate(ast, options);

生成目标平台所需的代码,将 AST 转化成 render function 字符串的过程
得到结果是 render 的字符串以及 staticRenderFns 字符串。

编译器的基本知识

编译器的技术分为词法分析、语法分析和语义分析三个部分
通常编译器的第一项工作叫做词法分析
就像阅读文章一样,文章是由一个个的中文单词组成的
程序处理也一样,只不过这里不叫单词,而是叫做“词法记号”,英文叫 Token

1
<div id="app" v-if="ret">{{ message }}</div>

其实,我们可以手写程序制定一些规则来区分每个不同的 Token,
这些规则用“正则文法”表达,符合正则文法的表达式称为“正则表达式”
通过他们来完成具体的词法分析工作

编译器下一个阶段的工作是语法分析
词法分析是识别一个个的单词,而语法分析就是在词法分析的基础上识别出程序的语法结构
这个结构是一个树状结构,是计算机容易理解和执行的
程序也要定义良好的语法结构,它的语法分析过程,就是构造这么一棵树
一个程序就是一棵树,这棵树叫做抽象语法树(Abstract Syntax Tree,AST)
树的每个节点(子树)是一个语法单元,这个单元的构成规则就叫“语法”。

而我们这里要讲的 parser 就是在编译器对源代码处理的第一步
parser 把某种特定格式的文本(字符串)转换成某种数据结构的程序(对象)
并且这个数据结构是编译器能够理解的
因为编译器的后续步骤
比如上面提到的 句法分析,类型检查/推导,代码优化,代码生成 等等都依赖于该数据结构

注:parse & parser 这两个单词,不要混淆,parse 是动词,代表“解析”的过程,parser 是名词,代表“解析器”。
Vue 的编译器也不例外, 在词法分析阶段 Vue 会把字符串模板解析成一个个的令牌(token)
该令牌将用于句法分析阶段,在句法分析阶段会根据令牌生成一棵 AST
最后再根据该 AST生成最终的渲染函数,这样就完成了代码的生成

parse

进入baseCompile第一步就是调用parse,将HTML转成AST

标签配对原理

在parse函数和parseHTML函数中都会声明一个stack数组用来存放开始标签
parse函数中的stack在传给parseHTML函数中的回调函数中被调用
parseHTML函数中的stack在解析开始结束标签时被调用
其实两个stack装的结点相同
只是parse函数中stack存放的虚拟节点对象是对parseHTML函数中存放的对象做了进一步处理的结果
配对原理就是
每遇到一个开始标签就push进stack中
每遇到一个结尾标签就在stack中找到与该结尾标签tag相同的最近一次push进去的开始标签的位置
如果该位置不是在stack最后一位,说明之前解析到的开始标签中存在未闭合的标签,
则先将与结束标签相同的开始标签之后的包括开始标签调用parseHTML传入的回调进行闭合,
然后通过更改stack长度,直接将开始标签及以后的标签全部pop出stack
parseHTML传入的回调会处理parse函数中声明的stack数组

变量声明

parse函数中会声明一系列变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
warn$2 = options.warn || baseWarn; //打印警告信息
//一个编译器选项,其作用是通过给定的标签名字判断该标签是否是 pre 标签
platformIsPreTag = options.isPreTag || no;
//一个编译器选项,其作用是用来检测一个属性在标签中是否要使用元素对象原生的 prop 进行绑定
platformMustUseProp = options.mustUseProp || no;
//一个编译器选项,其作用是用来获取元素(标签)的命名空间
platformGetTagNamespace = options.getTagNamespace || no;

transforms = pluckModuleFunction(options.modules, 'transformNode');
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');

delimiters = options.delimiters;//在创建 Vue 实例对象时所传递的 delimiters 选项
/**存储开始标签,与解析到的闭合标签对比,进行一元标签处理和双标签判断处理**/
var stack = [];
//options.preserveWhitespace 选项用来告诉编译器在编译 html 字符串时是否放弃标签之间的空格
//如果为 true 则代表放弃。
var preserveWhitespace = options.preserveWhitespace !== false;
var whitespaceOption = options.whitespace;
var currentParent;/**维护元素描述对象之间的父子关系,当前节点父元素**/
var inVPre = false;//标识当前解析的标签是否在拥有 v-pre 的标签之内
var inPre = false;//标识当前正在解析的标签是否在 <pre></pre> 标签之内
var root;/**存储最终生成的AST**/

parseHTML

parse 函数中会执行parseHTML函数,然后返回root,即AST
parseHTML 函数的作用就是用来做词法分析的
parse函数的作用则是在词法分析的基础上传入回调,做句法分析从而生成一棵AST
被调用时传入参数如下:

1
2
3
4
5
6
7
8
9
parseHTML(template, {
warn: warn$2,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,//对应配置项comments,true为保留模板中注释语句
outputSourceRange: options.outputSourceRange,

start

解析完一段模板的开始标签后的回调
例如:<div id=’jk’ class=’menu’ v-if=’isShow’><span>123</span></div>
解析完<div id=’jk’ class=’menu’ v-if=’isShow’>的回调函数

1
2
3
4
start: function start (tag, attrs, unary, start$1, end) {
// check namespace.
// inherit parent ns if there is one
var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);

ns 变量,代表标签的命名空间
platformGetTagNamespace 函数只会获取 svg 和 math 这两个标签的命名空间
但这两个标签的所有子标签都会继承它们两个的命名空间

1
2
3
4
5
// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs);
}

这里通过isIE来判断宿主环境是不是IE浏览器,并且前元素的命名空间为svg
如果是通过guardIESVGBug处理当前元素的属性数组attrs,并使用处理后的结果重新赋值给attrs变量
该问题是svg标签中渲染多余的属性,如下svg标签:
<svg xmlns:feature=”http://www.openplans.org/topp"></svg>
被渲染为:
<svg xmlns:NS1=”” NS1:xmlns:feature=”http://www.openplans.org/topp""></svg>
标签中多了 ‘xmlns:NS1=”” NS1:’ 这段字符串,解决办法也很简单,将整个多余的字符串去掉即可
而 guardIESVGBug 函数就是用来修改NS1:xmlns:feature属性并移除xmlns:NS1=”” 属性的

接着start函数会创建虚拟结点,返回一个描述该标签的对象结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var element = createASTElement(tag, attrs, currentParent); //下文有代码
if (ns) {
element.ns = ns;
}

{
if (options.outputSourceRange) { //处理虚拟节点占用原模板源码范围
element.start = start$1;
element.end = end;
element.rawAttrsMap = element.attrsList.reduce(function (cumulated, attr) {
cumulated[attr.name] = attr;
return cumulated
}, {});
}
//检查每个属性名是否符合规则
attrs.forEach(function (attr) {
if (invalidAttributeRE.test(attr.name)) {
warn$2('属性名不能包含空格引号<, >, / 或者 =',
{start: attr.start + attr.name.indexOf("["),end: attr.start + attr.name.length});
}
});
}
//是否是不允许的style或者script标签,而且不是在服务端渲染的情况下 isForbiddenTag下文有
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true;
warn$2(...警告内容,{ start: element.start });
}
for (var i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element;
}

preTransforms 是通过pluckModuleFunction 函数从options.modules 选项中筛选出名字为preTransformNode 函数所组成的数组
实际上 preTransforms 数组中只有一个 preTransformNode 函数该函数只用来处理 input 标签

inVpre 定义在parse函数中,一开始为false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (!inVPre) {
//解析是否有配置v-pre指令,有的话删除
//v-pre指令可以跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。
//跳过大量没有指令的节点会加快编译。
processPre(element);
if (element.pre) {
inVPre = true;
}
}

if (platformIsPreTag(element.tag)) {
inPre = true;
}
if (inVPre) {
processRawAttrs(element);
} else if (!element.processed) { // 处理结构指令v-for,v-if,v-once
//processFor会将解析的结果挂到element上
//{for:in后面的数组表达式,alias:in前的表达式}
//解析结果只是对表达式进行解析,in/of前后配置的啥,解析出来
processFor(element);
processIf(element);//处理v-if,v-else,v-else-if的表达式
processOnce(element);//处理v-once指令
}

所有process 系列函数的作用都会先拿到解析的配置的值,
然后将v-
属性从attrsList列表里面移除,只剩下纯html支持的属性
将针对v-*属性的解析结果挂到虚拟节点对象上
使这个对象能更加详细地描述一个元素

1
2
3
4
5
6
if (!root) {
root = element;//root节点没有挂载东西时,挂上,即找到了根节点
{
checkRootConstraints(root);
}
}

在编写 Vue 模板的时候会受到两种约束
首先模板必须有且仅有一个被渲染的根元素
第二不能使用 slot 标签和 template 标签作为模板的根元素
checkRootConstraints 函数内部首先通过判断el.tag === ‘slot’ || el.tag === ‘template’
来判断根元素是否是slot 标签或 template 标签
如果是则打印警告信息
接着会判断当前元素是否使用了 v-for 指令
因为v-for 指令会渲染多个节点所以根元素是不允许使用 v-for 指令的

1
2
3
4
5
6
7
8
9
  //这里的stack是parse函数中定义的stack
if (!unary) {//非一元标签
currentParent = element;//确定当前节点即接下来标签的父节点
stack.push(element);//存入栈中,到时供结束标签判断,前一个标签是一元标签还是最近的开始标签
} else {
//如果是一元的话,解析结束,调用结束标签函数,该函数在parseHTML函数调用前,parse函数中定义
closeElement(element);//会建立父子结点关系
}
},

小结

在解析完开始标签后,回调函数会做以下几件事
1.针对ie浏览器对svg和math标签属性做一个兼容性处理
2.生成虚拟dom对象
3.标识开始标签在原始代码所在的位置
4.检查属性命名是否规则
5.检查是不是不允许的script和style标签
6.处理配置的v-for,v-if,v-once属性
7.如果根结点还没有被挂载,则挂载到根结点root上,然后检查是否允许挂到根结点(template,slot,v-for)
8.如果是一元标签,解析结束;如果不是,更新当前父元素结点为该虚拟dom,并存入stack,用于配对结束标签

end

解析完一个结束标签时调用

1
2
3
4
5
6
7
8
9
10
11
12
13
end: function end (tag, start, end$1) {
var element = stack[stack.length - 1];
//每当遇到一个非一元标签的结束标签时,
//都会回退 currentParent 变量的值为之前的值
//这样我们就修正了当前正在解析的元素的父级元素
//在解析兄弟节点时是必须要这样做的
stack.length -= 1;
currentParent = stack[stack.length - 1];
if (options.outputSourceRange) {
element.end = end$1;
}
closeElement(element);
},

chars

解析到一段非标签内容后调用
比如

1
<div></div>

拿到’hello‘这一段后调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
chars: function chars (text, start, end) { 
if (!currentParent) { //一些边界情况处理
{//没有根元素,只有文本。
if (text === template) {
warnOnce('Component template requires a root element, rather than just text.',{ start: start }
);
} else if ((text = text.trim())) { //文本在根元素之外
warnOnce(("text \"" + text + "\" outside root element will be ignored."),{ start: start }
);
}
}
return
}
//来解决 IE 浏览器中渲染 <textarea> 标签的 placeholder 属性时存在的 bug 的
if (isIE && currentParent.tag === 'textarea' && currentParent.attrsMap.placeholder === text
) {return}

var children = currentParent.children;
//对空格空行进行删除,编译器只会保留那些 不存在于开始标签之后的空格。
if (inPre || text.trim()) {
//isTextTag判断是不是style或者script标签
//decodeHTMLCached 函数对文本进行解码。
text = isTextTag(currentParent) ? text : decodeHTMLCached(text);
} else if (!children.length) {
text = '';
} else if (whitespaceOption) {
if (whitespaceOption === 'condense') {
//代码压缩模式下,如果包含换行符,删除这样的空白结点,否则压缩成空格
text = lineBreakRE.test(text) ? '' : ' ';
} else {
text = ' ';
}
} else {
//preserveWhitespace 是一个布尔值代表着是否保留空格,只有它为真的情况下才会保留空格。
//但即使preserveWhitespace 常量的值为真,如果当前节点的父节点没有子元素则也不会保留空格,
text = preserveWhitespace ? ' ' : '';
}
if (text) {
if (!inPre && whitespaceOption === 'condense') {
//将连续的空格压缩为单个空格
text = text.replace(whitespaceRE$1, ' ');
}
var res;
var child;
//模板中存在的文本节点包含了 Vue 语法中的字面量表达式,
//而 parseText 函数的作用就是用来解析这段包含了字面量表达式的文本的
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
//含有表达式或者变量的情况
//res返回动态变量的描述
//例{{a}},返回:{expression: "_s(a)",tokens: [{@binding: "a"}]
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text: text
};
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
//纯常量字符串的情况
child = {
type: 3,
text: text
};
}
if (child) {
if (options.outputSourceRange) {
child.start = start;
child.end = end;
}
children.push(child);
}
}
},

小结

主要作用就是处理换行符,空格符,
然后按照常量字符串和动态字符串分别创建虚拟dom,
因为动态字符串需要进行一下解析
对虚拟dom挂载在源码中起止位置信息
将虚拟dom推入父节点children属性

comments

针对解析到的注释进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  comment: function comment (text, start, end) {
//只会处理被标签包裹的注释,因为根节点不允许有兄弟结点
if (currentParent) {
//普通文本节点与注释节点的元素描述对象的类型是一样的都是 3 ,
//不同的是注释节点的元素描述对象拥有 isComment 属性,并且该属性的值为 true,
//目的就是用来与普通文本节点作区分的。
var child = {
type: 3,
text: text,
isComment: true
};
if (options.outputSourceRange) {
child.start = start;
child.end = end;
}
currentParent.children.push(child);
}
}
});

真正的parseHTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//解析HTML
function parseHTML (html, options) {
var stack = [];//在 while 循环中处理 html 字符流的时候每当遇到一个非单标签,都会将该开始标签 push 到该数组。它的作用模板中 DOM 结构规范性的检测
var expectHTML = options.expectHTML;
var isUnaryTag$$1 = options.isUnaryTag || no; //用来检测一个标签是否是一元标签
var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;//用来检测一个标签是否是可以省略闭合标签的非一元标签
var index = 0; //标识着当前字符流的读入位置
//last 存储剩余还未编译的 html 字符串
//lastTag 始终存储着位于 stack 栈顶的元素
var last, lastTag;
while (html) {
last = html; //HTML为待解析的HTML片段,last 保留最原始的片段,advance会在解析过程中切割html
// 确保没有在script/style纯文本标签里面
//var isPlainTextElement = makeMap('script,style,textarea', true);
if (!lastTag || !isPlainTextElement(lastTag)) {
var textEnd = html.indexOf('<');
if (textEnd === 0) { //如果以<开头
// comment = /^<!\--/;如果是以注释开头的
if (comment.test(html)) {
var commentEnd = html.indexOf('-->'); //拿到注释结尾的位置
if (commentEnd >= 0) { //如果有完整注释
//根据new vue时传递进来的comments选项值判断是否保留模板中注释
if (options.shouldKeepComment) {
//options.comment在parse函数调用parseHTML时传递进来,即回调
//该函数会将html.substring拿到注释的内容包装成 type:3的节点,
//然后push进其父节点的children属性
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3);
}
advance(commentEnd + 3);//裁剪html模板,位移index的值
continue
}
}

// 不处理条件注释,直接略过
if (conditionalComment.test(html)) {
var conditionalEnd = html.indexOf(']>');

if (conditionalEnd >= 0) {
advance(conditionalEnd + 2);
continue
}
}

// var doctype = /^<!DOCTYPE [^>]+>/i;
//不处理<!DOCTYPE>标签
var doctypeMatch = html.match(doctype);
if (doctypeMatch) {
advance(doctypeMatch[0].length);
continue
}

//var endTag = new RegExp(("^<\\/" + qnameCapture + "[^>]*>"));
//qnameCapture详见parseStartTag函数
var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[1], curIndex, index);//处理stack中开标签/一元标签
continue
}

//如果是正常编写的标签(由<>包裹)返回一个描述开始标签的对象
var startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1);
}
continue
}
}


//处理html非尖括号开头情况,即解析到文本内容的时候
var text = (void 0), rest = (void 0), next = (void 0);
if (textEnd >= 0) { //此时html非尖括号开头,但剩下的里面还含有尖括号
rest = html.slice(textEnd);//截断尖括号以前的内容,拿到剩下的内容
while ( 第一个尖括号不是开始标签也不是结束或者评论标签
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// 处理文本里的尖括号,直到找到是标签的尖括号的位置,即文本结束的位置
next = rest.indexOf('<', 1);//从第二个字符开始找剩下html里面的第一个尖括号
if (next < 0) { break } //如果没有,直接结束
textEnd += next;//如果有的话,重制text结束位置为该尖括号位置
rest = html.slice(textEnd);//截取html,在判断尖括号是否是标签或者注释
}//while循环的作用是找出非标签内容
text = html.substring(0, textEnd);//最终得到非标签内容
}

if (textEnd < 0) { //html里面没尖括号了
text = html;
}

if (text) {
advance(text.length);
}

if (options.chars && text) {
options.chars(text, index - text.length, index);//调用回调,处理文本结点
}
} else {
//处理textarea内容和结束标签
var endTagLength = 0;
var stackedTag = lastTag.toLowerCase();
var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
//直接通过end标签拿到标签之间的文本内容,从避免分析中间的尖括号内容,直接保留
var rest$1 = html.replace(reStackedTag, function (all, text, endTag) {
endTagLength = endTag.length;
if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
text = text
.replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
}
if (shouldIgnoreFirstNewline(stackedTag, text)) {
text = text.slice(1);
}
if (options.chars) {
options.chars(text);
}
return ''
});
index += html.length - rest$1.length;
html = rest$1;
parseEndTag(stackedTag, index - endTagLength, index);
}

if (html === last) {
options.chars && options.chars(html);
if (!stack.length && options.warn) {
options.warn(("Mal-formatted tag at end of template: \"" + html + "\""), { start: index + html.length });
}
break
}
}

// Clean up any remaining tags
parseEndTag();

function advance (n) {
index += n;
html = html.substring(n);
}

parseStartTag

解析开始标签
使用unicode为基础的正则startTagOpen匹配html tag
unicode会用于比较标签名,组件名以及属性路径
var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;
var ncname = “[a-zA-Z_][\-\.0-9_a-zA-Z” + (unicodeRegExp.source) + “]*”;
var qnameCapture = “((?:” + ncname + “\:)?” + ncname + “)”;
var startTagOpen = new RegExp((“^<” + qnameCapture)); 标签开头正则
【正则表达式的source属性是其表达式的字符串形式】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function parseStartTag () {
var start = html.match(startTagOpen);
if (start) {
var match = {
tagName: start[1],
attrs: [],//用来存储将来被匹配到的属性
start: index //初始值为 index,是当前字符流读入位置在整个 html 字符串中的相对位置
};
advance(start[0].length);
var end, attr;
//startTagClose = /^\s*(\/?)>/;标签结束正则,只可以匹配>或者/>,或者之前有n个空格
//attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
//dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
//dynamicArgAttribute捕获标签上配置的属性push到节点的attr属性里面
//得到的结果是['key=val','key','=','val',]
//while条件是没有匹配到开始标签的结束部分,并且匹配到了开始标签中的属性,这个时候循环体将被执行,直到遇到开始标签的结束部分为止
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
attr.start = index;
advance(attr[0].length);
attr.end = index;
match.attrs.push(attr);
}
<!-- 即使匹配到了开始标签的开始部分以及属性部分但是却没有匹配到开始标签的结束部分,这说明这根本就不是一个开始标签。所以只有当变量end存在,即匹配到了开始标签的结束部分时,才能说明这是一个完整的开始标签 -->
if (end) {
match.unarySlash = end[1];//是否是单标签,双标签该值为空字符串,单标签该值为'/'
advance(end[0].length);//位移一个标签结束符'>'或者'/>'加前面n个空格的距离
match.end = index;
return match//一个html双标签的开头标签或者单标签解析完毕
}
<!-- 只有当变量end存在时,即能够确定确实解析到了一个开始标签的时候parseStartTag函数才会有返回值,并且返回值是match对象,其他情况下parseStartTag全部返回undefined -->
}
}

主要作用是
1.正则拿到开始标签名
2.while循环拿到属性的key-value对象集合,对象中包含该属性在源码中所占的起止位置
3.标识是否为一元标签,返回解析结果

handleStartTag

处理开始标签的解析结果,对上一步结果做进一步处理

1
2
3
4
5
function handleStartTag (match) {
var tagName = match.tagName;
var unarySlash = match.unarySlash;
//baseOptions中默认设置为true
if (expectHTML) {

如果最近一次遇到的开始标签是 p 标签
并且当前正在解析的开始标签必须不能是段落式内容(Phrasing content)模型
每一个 html 元素都拥有一个或多个内容模型(content model)
其中p 标签本身的内容模型是流式内容(Flow content)
并且 p 标签的特性是只允许包含段落式内容(Phrasing content)
所以条件成立的情况如下:<p><h1></h1></p>
在解析上面这段 html 字符串的时候,首先遇到p标签的开始标签,此时lastTag被设置为 p
紧接着会遇到 h1 标签的开始标签,由于 h2 标签的内容模型属于非段落式内容(Phrasing content)模型
所以会立即调用 parseEndTag(lastTag) 函数闭合 p 标签,此时由于强行插入了</p> 标签
所以解析后的字符串将变为如下内容:<p></p><h2></h2></p>
继续解析该字符串,会遇到 <h2></h2>标签并正常解析之
最后解析器会遇到一个单独的p 标签的结束标签,即:</p>
这个时候就回到了我们前面讲过的,当解析器遇到 p 标签或者 br 标签的结束标签时会补全他们
最终<p><h2></h2></p>
这段 html 字符串将被解析为:<p></p><h2></h2><p></p>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
    if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag);//处理结束标签
}
//当前正在解析的标签是一个可以省略结束标签的标签,并且与上一次解析到的开始标签相同
//例<p>max<p>kaixin
//当解析到一个p标签的开始标签并且下一次遇到的标签也是p标签的开始标签时,会立即关闭第二个p标签。
//即调用:parseEndTag(tagName) 函数,然后由于第一个p标签缺少闭合标签所以会Vue会给你一个警告。
if (canBeLeftOpenTag$$1(tagName) && lastTag === tagName) {
parseEndTag(tagName);
}
}
var unary = isUnaryTag$$1(tagName) || !!unarySlash;//是否是一元标签

var l = match.attrs.length;
var attrs = new Array(l);//整理属性
for (var i = 0; i < l; i++) {
var args = match.attrs[i];
var value = args[3] || args[4] || args[5] || '';
//对于a标签是否插入换行编码
var shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
? options.shouldDecodeNewlinesForHref
: options.shouldDecodeNewlines;
attrs[i] = {
name: args[1],
//decodeAttr 用是对属性值中所包含的 html 实体进行解码,将其转换为实体对应的字符。
value: decodeAttr(value, shouldDecodeNewlines)
};
if (options.outputSourceRange) {
attrs[i].start = args.start + args[0].match(/^\s*/).length;
attrs[i].end = args.end;
}
}
//如果开始标签是非一元标签,则将该开始标签的信息入栈,
//即push到stack数组中,并将lastTag的值设置为该标签名。
if (!unary) {//这里的stack是parseHTML中定义的stack
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end });
lastTag = tagName;//lastTag 所存储的标签名字始终保存着 stack 栈顶的元素
}

if (options.start) { 调用配置的开头标签处理函数,是parse函数中传输parseHTML函数的参数
options.start(tagName, attrs, unary, match.start, match.end);
}
}

作用如下:
1.处理p标签闭合情况,以及a标签属性值是否插入换行的处理
2.进一步处理属性值结合,将正则得到的数组转成key-value对象
3.将节点push到stack中,进行闭合标签配对,设置lastTag为最近一次的开始标签
4.调用开始标签回调start

createASTElement

创建虚拟节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createASTElement (
tag,
attrs,
parent
) {
return {
type: 1,
tag: tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent: parent,
children: []
}
}

parseEndTag

处理结束标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
  function parseEndTag (tagName, start, end) {
var pos, lowerCasedTagName;
//当 start 和 end 不存在时,将这两个变量的值设置为当前字符流的读入位置,即index
//在handleStartTag 函数中调用时会被这样这样调用
if (start == null) { start = index; }
if (end == null) { end = index; }
//找到最近的相同类型的开始标签的位置
if (tagName) {
lowerCasedTagName = tagName.toLowerCase();
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {//调用 parseEndTag() 函数时不传递任何参数,就处理stack栈中剩余未处理的标签,
pos = 0;
}
//删除最近开始标签 以后的开始标签(可能是一元标签)
if (pos >= 0) {//检测是否缺少闭合标签
// Close all the open elements, up the stack
for (var i = stack.length - 1; i >= pos; i--) {
if (i > pos || !tagName && options.warn) {
//如果发现 stack 数组中存在索引大于 pos 的元素,那么一定有元素是缺少闭合标签的
options.warn(("tag <" + (stack[i].tag) + "> has no matching end tag."),{ start: stack[i].start, end: stack[i].end });
}
if (options.end) {
options.end(stack[i].tag, start, end);//回调parse函数调用时传递进来的回调函数
}
}
//从stack中删除开标签
stack.length = pos;
lastTag = pos && stack[pos - 1].tag;//将lastTag重制为stack栈顶元素
} else if (lowerCasedTagName === 'br') {
//为了与浏览器的行为相同,专门处理br与p的结束标签,即:</br> 和</p>。
//</br> 标签被正常解析为 <br> 标签,而</p>标签被正常解析为 <p></p>
if (options.start) {
options.start(tagName, [], true, start, end);
}
} else if (lowerCasedTagName === 'p') {
if (options.start) {
options.start(tagName, [], false, start, end);
}
if (options.end) {
options.end(tagName, start, end);
}
}
}
}

主要作用是
1.利用stack进行配对,处理stack和lastTag
2.特殊处理br和p标签
3.调用end回调函数处理父子关系

closeElement

1
2
3
4
5
6
function closeElement(element) {
trimEndingWhitespace(element);
if (!inVPre && !element.processed) {
//processElement会继续处理element上的属性key,is,inline-template,ref,v-bind,v-on等等,会挂载plain属性供generate创建render函数时使用
element = processElement(element, options);
}

使用 v-if 或 v-else-if 或 v-else 创建多个根结点时
v-else-if 或 v-else创建的虚拟节点放到v-if节点的ifconditions属性下面【待验证】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
  if (!stack.length && element !== root) {
// allow root elements with v-if, v-else-if and v-else
if (root.if && (element.elseif || element.else)) {
{
checkRootConstraints(element);
}
//给root.ifconditions属性push if,elseif,else等条件模板下的虚拟dom对象
addIfCondition(root, {
exp: element.elseif,
block: element
});
} else {
warnOnce(
"Component template should contain exactly one root element. " +
"If you are using v-if on multiple elements, " +
"use v-else-if to chain them instead.",
{ start: element.start }
);
}
}
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent);
} else {
if (element.slotScope) {
var name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
}
//这样就建立了元素描述对象间的父子级关系。
currentParent.children.push(element);
element.parent = currentParent;
}
}
element.children = element.children.filter(function (c) { return !(c).slotScope; });
trimEndingWhitespace(element);
if (element.pre) {
inVPre = false;
}
if (platformIsPreTag(element.tag)) {
inPre = false;
}
for (var i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options);
}
}

主要作用是
1.处理条件渲染节点关系
2.建立父子结点关系

isForbiddenTag

1
2
3
4
5
6
7
8
9
function isForbiddenTag(el) {
return (
el.tag === 'style' ||
(el.tag === 'script' && (
!el.attrsMap.type ||
el.attrsMap.type === 'text/javascript'
))
)
}

optimize

通过两个递归函数给结点挂载static和staticRoot两个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
function optimize (root, options) {
if (!root) { return }
isStaticKey = genStaticKeysCached(options.staticKeys || '');
isPlatformReservedTag = options.isReservedTag || no;
// first pass: mark all non-static nodes.
markStatic$1(root);
// second pass: mark static roots.
markStaticRoots(root, false);
}
//判断结点是否为静态结点
function isStatic (node) {
if (node.type === 2) { // expression
return false
}
if (node.type === 3) { // text
return true
}
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}
function markStatic$1 (node) {
node.static = isStatic(node);
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
for (var i = 0, l = node.children.length; i < l; i++) {
var child = node.children[i];
markStatic$1(child);
if (!child.static) {//子节点一但为动态结点,则父节点为动态结点
node.static = false;
}
}
if (node.ifConditions) { //处理条件渲染节点是否为静态结点
for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
var block = node.ifConditions[i$1].block;
markStatic$1(block);
if (!block.static) {
node.static = false;
}
}
}
}
}

function markStaticRoots (node, isInFor) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor;
}
//对只含有一个纯文本结点的node添加staticRoot = false
//否则staticRoot = true
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true;
return
} else {
node.staticRoot = false;
}
if (node.children) {
for (var i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for);
}
}
if (node.ifConditions) {
for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
markStaticRoots(node.ifConditions[i$1].block, isInFor);
}
}
}
}

主要处理思想就是
子节点一旦为动态结点,则父节点为动态结点, static = false
只含有一个纯文本结点的node,没有其他子节点,staticRoot = false

generate

1
2
3
4
5
6
7
8
9
10
11
function generate (
ast,
options
) {
var state = new CodegenState(options);
var code = ast ? genElement(ast, state) : '_c("div")';
return {
render: ("with(this){return " + code + "}"),
staticRenderFns: state.staticRenderFns
}
}

CodegenState

1
2
3
4
5
6
7
8
9
10
11
12
var CodegenState = function CodegenState (options) {
this.options = options;
this.warn = options.warn || baseWarn;
this.transforms = pluckModuleFunction(options.modules, 'transformCode');
this.dataGenFns = pluckModuleFunction(options.modules, 'genData');
this.directives = extend(extend({}, baseDirectives), options.directives);
var isReservedTag = options.isReservedTag || no;
this.maybeComponent = function (el) { return !!el.component || !isReservedTag(el.tag); };
this.onceId = 0;
this.staticRenderFns = [];
this.pre = false;
};

genElement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function genElement (el, state) {
if (el.parent) {
el.pre = el.pre || el.parent.pre;
}

if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
var code;
if (el.component) {
code = genComponent(elp.comonent, el, state);
} else {//普通标签情况
var data;
//在processElement函数中
// el.plain = !el.key &&!el.scopedSlots && !el.attrsList.length
// v-pre 可以跳过编译
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData$2(el, state);//拼接el标签上配置的内容
}

var children = el.inlineTemplate ? null : genChildren(el, state, true);
code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
}
for (var i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code);
}
return code
}
}

genData$2

genData$2 生成el标签上配置的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
function genData$2 (el, state) {
var data = '{';
//优先处理指令,因为指令可能会影响其他属性
//很重要这个步骤很重要,会给input等存在交互的标签因为v-model绑定响应事件
//genDirectives下文有详解
var dirs = genDirectives(el, state);
if (dirs) { data += dirs + ','; }

// key
if (el.key) {
data += "key:" + (el.key) + ",";
}
// ref
if (el.ref) {
data += "ref:" + (el.ref) + ",";
}
if (el.refInFor) {
data += "refInFor:true,";
}
// pre
if (el.pre) {
data += "pre:true,";
}
// record original tag name for components using "is" attribute
if (el.component) {
data += "tag:\"" + (el.tag) + "\",";
}
// module data generation functions
for (var i = 0; i < state.dataGenFns.length; i++) {
data += state.dataGenFns[i](el);
}
// attributes
if (el.attrs) {
data += "attrs:" + (genProps(el.attrs)) + ",";
}
// DOM props
if (el.props) {
data += "domProps:" + (genProps(el.props)) + ",";
}
// event handlers
if (el.events) {
data += (genHandlers(el.events, false)) + ",";
}
if (el.nativeEvents) {
data += (genHandlers(el.nativeEvents, true)) + ",";
}
// slot target
// only for non-scoped slots
if (el.slotTarget && !el.slotScope) {
data += "slot:" + (el.slotTarget) + ",";
}
// scoped slots
if (el.scopedSlots) {
data += (genScopedSlots(el, el.scopedSlots, state)) + ",";
}
// component v-model
if (el.model) {
data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
}
// inline-template
if (el.inlineTemplate) {
var inlineTemplate = genInlineTemplate(el, state);
if (inlineTemplate) {
data += inlineTemplate + ",";
}
}
data = data.replace(/,$/, '') + '}';
// v-bind dynamic argument wrap
// v-bind with dynamic arguments must be applied using the same v-bind object
// merge helper so that class/style/mustUseProp attrs are handled correctly.
if (el.dynamicAttrs) {
data = "_b(" + data + ",\"" + (el.tag) + "\"," + (genProps(el.dynamicAttrs)) + ")";
}
// v-bind data wrap
if (el.wrapData) {
data = el.wrapData(data);
}
// v-on data wrap
if (el.wrapListeners) {
data = el.wrapListeners(data);
}
return data
}

genDirectives

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function genDirectives (el, state) {
var dirs = el.directives;
if (!dirs) { return }
var res = 'directives:[';
var hasRuntime = false;
var i, l, dir, needRuntime;
for (i = 0, l = dirs.length; i < l; i++) {
dir = dirs[i];
needRuntime = true;
var gen = state.directives[dir.name];
if (gen) {
// 处理ast里面的编译时指令
// 如果还需要运行时副本,则返回true
needRuntime = !!gen(el, dir, state.warn);
}
if (needRuntime) {
hasRuntime = true;
res += "{name:\"" + (dir.name) + "\",rawName:\"" + (dir.rawName) + "\"" + (dir.value ? (",value:(" + (dir.value) + "),expression:" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (",arg:" + (dir.isDynamicArg ? dir.arg : ("\"" + (dir.arg) + "\""))) : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},";
}
}
if (hasRuntime) {
return res.slice(0, -1) + ']'
}
}
el.directives

el.directives属性在closeElement—>processElement—>processAttrs
函数调用流程中被挂载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function processAttrs (el) {
var list = el.attrsList;
var i, l, name, rawName, value, modifiers, syncGen, isDynamic;
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name;
value = list[i].value;
if (dirRE.test(name)) { // var dirRE = /^v-|^@|^:|^#/;
el.hasBindings = true;
if (bindRE.test(name)) { ...// v-bind
} else if (onRE.test(name)) {... // v-on
} else { // normal directives
name = name.replace(dirRE, '');
var argMatch = name.match(argRE);
var arg = argMatch && argMatch[1];
isDynamic = false;
if (arg) {
name = name.slice(0, -(arg.length + 1));
if (dynamicArgRE.test(arg)) {
arg = arg.slice(1, -1);
isDynamic = true;
}
}
addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i]);
if (name === 'model') {
checkForAliasModel(el, value);
}
}
} else {....}
}
}

function addDirective (el,name,rawName,value,arg,isDynamicArg,modifiers,range) {
(el.directives || (el.directives = [])).push(rangeSetItem({
name: name,
rawName: rawName,
value: value,
arg: arg,
isDynamicArg: isDynamicArg,
modifiers: modifiers
}, range));
el.plain = false;
}

v-model

假如现在标签为<input v-model=’a’ >
那么现在的el.directives=[{name: “model”,rawName: “v-model”,value: “a”,isDynamicArg: false,start: 106,end: 117,modifiers:undefined}]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var gen = state.directives[dir.name];
--->
state = new CodegenState()
--->
var CodegenState = function CodegenState (options) {
this.directives = extend(extend({}, baseDirectives), options.directives);
}
--->
options (finalOptions = Object.create(baseOptions);--->baseCompile()--->generate())
--->
var baseOptions = {
directives: directives$1,
}
--->
directives$1 = {
model: model,
text: text,
html: html
};
--->>>>>>>>>>
state.directives = directives$1
state.directives[el.directives[0].name] ===>model

针对不同标签处理双向绑定,添加相应事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function model (el,dir,_warn) {
warn$1 = _warn;
var value = dir.value;
var modifiers = dir.modifiers;
var tag = el.tag;
var type = el.attrsMap.type;
{if (tag === 'input' && type === 'file') {warn$1(....);}}

if (el.component) {
genComponentModel(el, value, modifiers);
return false
} else if (tag === 'select') {
genSelect(el, value, modifiers);
} else if (tag === 'input' && type === 'checkbox') {
genCheckboxModel(el, value, modifiers);
} else if (tag === 'input' && type === 'radio') {
genRadioModel(el, value, modifiers);
} else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers);
} else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers);
// component v-model doesn't need extra runtime
return false
} else {
warn$1(...);
}
return true
}

针对input 的v-model绑定input事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function genDefaultModel (
el,
value,
modifiers
) {
var type = el.attrsMap.type;

var ref = modifiers || {};
var lazy = ref.lazy;
var number = ref.number;
var trim = ref.trim;
var needCompositionGuard = !lazy && type !== 'range';
var event = lazy
? 'change'
: type === 'range'
? RANGE_TOKEN
: 'input'; //默认input事件触发双向绑定

var valueExpression = '$event.target.value';
if (trim) {
valueExpression = "$event.target.value.trim()";
}
if (number) {
valueExpression = "_n(" + valueExpression + ")";
}
//input事件响应函数字符串形式
var code = genAssignmentCode(value, valueExpression);
if (needCompositionGuard) {
code = "if($event.target.composing)return;" + code;
}

addProp(el, 'value', ("(" + value + ")"));
addHandler(el, event, code, null, true); //addHandler函数给el绑定events属性
if (trim || number) {
addHandler(el, 'blur', '$forceUpdate()');
}
}

$event.target.composing是vue自己添加的属性,用于控制在输入中日韩这类语言时
确定一个字或词输入完成后再更新绑定值,而不是每次输入一个字母就更新一次绑定值

genChildren

1
var children = el.inlineTemplate ? null : genChildren(el, state, true);

genElement函数中组建完本身标签内容后,会继续组建子标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function genChildren (
el,
state,
checkSkip,
altGenElement,
altGenNode
) {
var children = el.children;
if (children.length) {
var el$1 = children[0];
// optimize single v-for
if (children.length === 1 &&
el$1.for &&
el$1.tag !== 'template' &&
el$1.tag !== 'slot'
) {
var normalizationType = checkSkip
? state.maybeComponent(el$1) ? ",1" : ",0"
: "";
return ("" + ((altGenElement || genElement)(el$1, state)) + normalizationType)
}
var normalizationType$1 = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0;
var gen = altGenNode || genNode;
return ("[" + (children.map(function (c) { return gen(c, state); }).join(',')) + "]" + (normalizationType$1 ? ("," + normalizationType$1) : ''))
}
}
function genNode (node, state) {
if (node.type === 1) {
return genElement(node, state)
} else if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}

genChildren函数会循环父标签的的children属性生成每个子标签的渲染函数字符串,
以数组字符串形式返回给父标签的_c函数,用作参数

小结

假如html内容为:

1
2
3
4
<div id = 'app' class='hello'>
<p >{{a+'...'+arr[0]}}</p>
<input class="kk" key='kkk' ref='hellodiv' v-model='a' @input='search'/>
</div>

那么generate函数返回的render值如下

1
"with(this){return _c('div',{staticClass:"hello",attrs:{"id":"app"}},[_c('p',[_v(_s(a+'...'+arr[0]))]),_v(" "),_c('input',{directives:[{name:"model",rawName:"v-model",value:(a),expression:"a"}],key:"kkk",ref:"hellodiv",staticClass:"kk",domProps:{"value":(a)},on:{"input":[function($event){if($event.target.composing)return;a=$event.target.value},search]}})])}"

可见,generate函数只是将parse和optimize处理得到的标签描述对象ast整理成函数参数
交由不同类型的可以生成vnode对象的处理函数,整个函数,由字符串拼接形成
整个函数字符串会在compileToFunctions函数中调用createFunction转化为真正的函数对象

1
2
3
4
5
6
7
8
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err: err, code: code });
return noop
}
}

渲染函数字符串中的函数缩写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function installRenderHelpers (target) {
target._o = markOnce;
target._n = toNumber;
target._s = toString;
target._l = renderList;//v-for
target._t = renderSlot;
target._q = looseEqual;
target._i = looseIndexOf;
target._m = renderStatic;
target._f = resolveFilter;
target._k = checkKeyCodes;
target._b = bindObjectProps;
target._v = createTextVNode;
target._e = createEmptyVNode;
target._u = resolveScopedSlots;
target._g = bindObjectListeners;
target._d = bindDynamicKeys;
target._p = prependModifier;
}
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
});
installRenderHelpers(FunctionalRenderContext.prototype);
--->
render:("with(this){return " + code + "}")

_c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
function createElement (context,tag,data,children,normalizationType,alwaysNormalize) {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children;
children = data;
data = undefined;
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE;
}
return _createElement(context, tag, data, children, normalizationType)
}
function _createElement (context,tag,data,children,normalizationType) {
//这里省略一系列边界判断
var vnode, ns;
if (typeof tag === 'string') {
var Ctor;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
if (config.isReservedTag(tag)) { //标签名是一般的html标签
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
);
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { //标签名是组件标签名
vnode = createComponent(Ctor, data, context, children, tag);
} else {
//未知或未列出的命名空间元素在运行时检查,
//因为当其父项标准化子元素时可能会为其分配一个命名空间
vnode = new VNode(
tag, data, children,
undefined, undefined, context
);
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children);
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) { applyNS(vnode, ns); }
if (isDef(data)) { registerDeepBindings(data); }
return vnode
} else {
return createEmptyVNode()
}
}
var VNode = function VNode (
tag,
data,
children,
text,
elm,
context,
componentOptions,
asyncFactory
) {
this.tag = tag;
this.data = data;
this.children = children;//children本身是vnode对象集合,所以不需递归处理父子关系
this.text = text;
this.elm = elm;//挂载真正的dom对象
this.ns = undefined;
this.context = context;
this.fnContext = undefined;
this.fnOptions = undefined;
this.fnScopeId = undefined;
this.key = data && data.key;
this.componentOptions = componentOptions;
this.componentInstance = undefined;
this.parent = undefined;
this.raw = false;
this.isStatic = false;
this.isRootInsert = true;
this.isComment = false;
this.isCloned = false;
this.isOnce = false;
this.asyncFactory = asyncFactory;
this.asyncMeta = undefined;
this.isAsyncPlaceholder = false;
};

注意createElement函数只是将一个结点转成vnode对象
上面generate得到的render字符串的逻辑是生成一个根结点,
里面传递的子节点数组也是调用createElement函数生成的一个个vnode
所以这里调用render生成vnode函数时,没有递归逻辑,
而是每个结点自己去调createElement生成自己的vnode

_s

1
2
3
4
5
6
7
function toString (val) {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2)
: String(val)
}

_v

1
2
3
function createTextVNode (val) {
return new VNode(undefined, undefined, undefined, String(val))
}

_l

循环组建结点的创建函数字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function renderList (
val,
render
) {
var ret, i, l, keys, key;
if (Array.isArray(val) || typeof val === 'string') {
ret = new Array(val.length);
for (i = 0, l = val.length; i < l; i++) {
ret[i] = render(val[i], i);
}
} else if (typeof val === 'number') {
ret = new Array(val);
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i);
}
} else if (isObject(val)) {
if (hasSymbol && val[Symbol.iterator]) {
ret = [];
var iterator = val[Symbol.iterator]();//获取对象生成器
var result = iterator.next();
while (!result.done) {
ret.push(render(result.value, ret.length));
result = iterator.next();
}
} else {
keys = Object.keys(val);
ret = new Array(keys.length);
for (i = 0, l = keys.length; i < l; i++) {
key = keys[i];
ret[i] = render(val[key], key, i);
}
}
}
if (!isDef(ret)) {
ret = [];
}
(ret)._isVList = true;
return ret
}

mountComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function mountComponent (vm,el,hydrating) {
vm.$el = el;
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
{...}
callHook(vm, 'beforeMount');

var updateComponent;
if (config.performance && mark) {...
} else { //定义updateComponent
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
}
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true);
hydrating = false;
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
}

watch

watcher 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var Watcher = function Watcher (vm,expOrFn,cb,options,isRenderWatcher) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
//...省略不重要代码
this.expression = expOrFn.toString(); //updateComponent字符串化
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn; //指向updateComponent
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
warn(....);
}
}
this.value = this.lazy
? undefined
: this.get();//里面会调用this.getter
};
Watcher.prototype.get = function get () {
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);//就是在调用updateComponent
} catch (e) {...
} finally {...
}
return value //最终返回value
};

可知在new Watcher过程中构造函数会将updateComponent挂到getter属性上,
在通过get属性进行调用
再来看一下updateComponent怎么定义

1
2
3
updateComponent = function () {
vm._update(vm._render(), hydrating);
};

updateComponent要调用vue的_update函数
注意这里调用了vm._render(),生成了vnode,当做参数传递了进去
vue的_update函数是在构建vue构造函数时
调用lifecycleMixin函数被挂载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function lifecycleMixin (Vue) {
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;//vm.$el初次渲染时为挂载点所在html,即最原始的,parse处理的html模板
var prevVnode = vm._vnode;//这里初次渲染时为null
var restoreActiveInstance = setActiveInstance(vm);//给全局变量activeInstance赋值为vm
vm._vnode = vnode;//给vue挂上得到的vnode
if (!prevVnode) {//初次渲染走这里
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {//更新时走这里
vm.$el = vm.__patch__(prevVnode, vnode);
}
restoreActiveInstance();//处理完后更新activeInstance为null
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null;
}
if (vm.$el) {
vm.$el.__vue__ = vm;
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el;
}
};

patch

patch函数直接在生成vue构造函数时被挂载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
Vue.prototype.__patch__ = inBrowser ? patch : noop;
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });

function createPatchFunction (backend) {
var i, j;
var cbs = {};

var modules = backend.modules;
var nodeOps = backend.nodeOps;

for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]]);
}
}
}
//省略若干函数声明......
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
return
}
var isInitialPatch = false;
var insertedVnodeQueue = [];
if (isUndef(oldVnode)) {
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
var isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {//更新时,详见更新节点
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {//初次渲染时oldVnode为原始html结点
if (isRealElement) {
//是真实的DOM而且是服务端渲染的结果
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR);
hydrating = true;
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true);
return oldVnode
} else {
warn(....);
}
}
//不是服务端渲染或者合成失败,就创建空的vnode代替老结点
oldVnode = emptyNodeAt(oldVnode);
//oldVnode:{elm:指向原始未编译前的真实html结点}
}

// 替换正存在的结点
var oldElm = oldVnode.elm;//原来结点
var parentElm = nodeOps.parentNode(oldElm);//原节点的父节点

// 创建dom结点
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
);
//创建dom结束后,body里面会有两个挂载点,一个编译前html,一个编译后html
//所以新老结点更新的思路是,在父元素在新增新结点子元素,然后将老结点删除
// 递归更新父占位符节点元素
if (isDef(vnode.parent)) {....}

// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0);//从父元素中删除老结点
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
}

invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm //返回真正的dom结点
}
}

createElm

生成真正的dom结点挂载在各自的vnode的elm属性上,并在生成的过程中建立父子关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
  function createElm (vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {//预渲染时克隆源节点,不再重新生成
vnode = ownerArray[index] = cloneVNode(vnode);
}

vnode.isRootInsert = !nested; // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}

var data = vnode.data;
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
{
if (data && data.pre) {creatingElmInVPre++;}
if (isUnknownElement$$1(vnode, creatingElmInVPre)) { warn(...);}
}

vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);//没有命名空间直接生成真正html标签挂到elm属性上
setScope(vnode);
{
createChildren(vnode, children, insertedVnodeQueue); //构建子节点
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);//处理标签上属性(id,class....)
}
insert(parentElm, vnode.elm, refElm);
}
if (data && data.pre) {
creatingElmInVPre--;
}
} else if (isTrue(vnode.isComment)) {//处理注释结点
vnode.elm = nodeOps.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
} else {//处理纯静态文本结点
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
----->nodeOps.createElement----->
function createElement$1 (tagName, vnode) {
var elm = document.createElement(tagName);
if (tagName !== 'select') {
return elm
}
// false or null will remove the attribute but undefined will not
if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
elm.setAttribute('multiple', 'multiple');
}
return elm
}
---->nodeOps.createTextNode---->
function createTextNode (text) {
return document.createTextNode(text)
}
---->nodeOps.createComment---->
function createComment (text) {
return document.createComment(text)
}

createChildren

构建子节点

1
2
3
4
5
6
7
8
9
10
11
12
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
{
checkDuplicateKeys(children);//检查子节点是否有重复key值
}
for (var i = 0; i < children.length; ++i) { //递归调用createElm创建子节点
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
}
}

insert

构建父子dom结点关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 function insert (parent, elm, ref$$1) {
if (isDef(parent)) {
if (isDef(ref$$1)) {
if (nodeOps.parentNode(ref$$1) === parent) {
nodeOps.insertBefore(parent, elm, ref$$1);
}
} else {
nodeOps.appendChild(parent, elm);
}
}
}
---->nodeOps.insertBefore----->纯dom操作
function insertBefore (parentNode, newNode, referenceNode) {
parentNode.insertBefore(newNode, referenceNode);
}
----->nodeOps.appendChild---->纯dom操作
function appendChild (node, child) {
node.appendChild(child);
}
----->nodeOps.removeChild---->纯dom操作
function removeChild (node, child) {
node.removeChild(child);
}

小结

mountComponent主要做这样几件事
1.执行beforeMount钩子函数
2.定义updateComponent,并在new watcher的时候调用
3.调用updateComponent函数时会调用vm_render函数生成vnode对象,这个过程ast中变量会拿到实际的值
4.updateComponent函数会将vnode传递给vm_update,vm_update在生成vue构造函数时被挂载
5.vm_update会调用vm__patch__,vm__patch__会调用patch函数
6.patch函数会根据传入参数特征处理第一次挂载还是更新操作
7.patch函数会调用createElement递归将vnode对象里面的结点转成真正的dom结点挂载在对应的elm属性上,同时会处理好父子关系以及标签上class,id之类的属性
8.因为处理最外层结点时,即挂载点结点时,获取的父节点与未编译的挂载点是同一个,所以要把未编译的挂载点调用removeVnodes移除,

更新节点

patchVnode

负责具体的结点更新工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function patchVnode (oldVnode,vnode,insertedVnodeQueue,ownerArray,index,removeOnly
) {
if (oldVnode === vnode) {
return
}
//...省略若干边界/特殊条件处理
var i;
var data = vnode.data;
//组件才会有hook属性
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode);
}

var oldCh = oldVnode.children;
var ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {//处理标签上属性更新
for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }
if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); }
}
if (isUndef(vnode.text)) {//如果新结点不是纯文本结点
if (isDef(oldCh) && isDef(ch)) {//如果新老结点都有子节点
if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
} else if (isDef(ch)) { //如果新结点有子节点,老结点没有,直接新增
{
checkDuplicateKeys(ch);
}
if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) { //如果老结点有子节点,新结点没有,直接删除
removeVnodes(oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {//如果是文本结点,清空
nodeOps.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) { //如果新结点也是纯文本结点,则更新内容
nodeOps.setTextContent(elm, vnode.text);
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }
}
}

updateChildren

负责判断子节点是否需要更新,递归调用patchVnode进行更新操作
主要思路是以新结点为标尺,对比老结点,在老结点树上面移动或者新增删除节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
var oldStartIdx = 0;
var newStartIdx = 0;
var oldEndIdx = oldCh.length - 1;
var oldStartVnode = oldCh[0];
var oldEndVnode = oldCh[oldEndIdx];
var newEndIdx = newCh.length - 1;
var newStartVnode = newCh[0];
var newEndVnode = newCh[newEndIdx];
var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
var canMove = !removeOnly;

{
checkDuplicateKeys(newCh);
}

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
//因为老node的开始节点竟然与新node的结束节点一致,说明老node的开始节点需要移动到当前的老node结束节点的后一位
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
//老node的结束节点移动到老node的开始节点之前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
//判断老结点数组中是否有当前这个新结点
if (isUndef(idxInOld)) { // 如果没有在当前老结点前面新增这个新结点
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
} else {
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldCh[idxInOld] = undefined;
//如果有且key相同,对比之后,移动到当前老结点前面,保持新老结点位置相同
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
如果新老结点不是同一个key,则新增新结点
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
if (oldStartIdx > oldEndIdx) {
//此时newStartIdx和newEndIdx之间的vnode是新增的,调用addVnodes,把他们全部插进before的后边,before很多时候是为null的
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
//newStartIdx > newEndIdx,可以认为newCh先遍历完。
//此时oldStartIdx和oldEndIdx之间的vnode在新的子节点里已经不存在了,调用removeVnodes将它们从dom里删除。
removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}
}

function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}

流程图

My Little World

关于css的一些事

发表于 2020-01-27

1.CSS 选择器的优先级
!important
内联样式(1000)
id选择器(100)
类选择器(10)
元素选择器(1)

2.为兼容浏览器,对css基本样式做统一处理的三种方法
【reset】
消除浏览器默认样式,针对具体元素标签重写样式
【normalize】
样式规范确定的情况下,直接重写全部样式
【neat】
前两者结合,针对确定的样式直接重写,不确定的进行清除

3.css预处理语法工具需要具备的特性
可以变量声明和计算
支持语法表达式
可以定义函数进行处理
属性可以继承
可以自行实现autoprefix类似的兼容性补全功能

4.实现动画的6种方式
纯JS(利用setInternal,16ms)
svg(利用标签配置)
canvas(利用jsAPI配置)
css3-transition(过渡动画,不能独立实现动画)
css3-animation(纯css实现动画)
requestAnimationFrame(类似setInterval,但性能消耗低)
前两种兼容性良好,后四种适用于移动端

5.屏幕适配布局方案
想办法让比例不变
A.在html/body标签上加上zoom属性,缺点是不灵活
B.使用rem,相对于文档根元素font-size,
利用mediaQuery对常见几种屏幕宽度设置html的font-size
以font-size大小为1rem

6.响应式设计的两种方案
前端或者后端判断useragent来跳转不同站点得到不同页面
使用mediaQuery媒体查询让页面根据不同设备自动改变页面布局和显示

7.根据浏览器屏幕宽度和分辨率加载不同大小的图片
使用mediaQuery判断加载不同的背景图
使用H5的picture标签
模板进行判断然后渲染不同图片
请求图片时带上相关参数,让图片服务器吐出不同大小图片

table-layout: fixed; 妙用

具有滚动条的table表头和tbody分别用两个table实现时,在每列设置宽度的情况下,有时可能出现表头和tbody对不齐的情况,

这是因为table默认情况下,会按照表格内容进行列宽绘制,tbody和表头整个宽度是一样宽的,而tbody有滚动条宽度,表头没有滚动条,就会导致列对不上了
解决办法:

  1. 给table设置 table-layout: fixed;让table按照设置的列宽进行绘制
    .xxx-table {
    table-layout: fixed;
    }
  2. 不让所有的列都有具体的宽度,让某一列不设置宽度
1…789…26
YooHannah

YooHannah

260 日志
1 分类
23 标签
RSS
© 2025 YooHannah
由 Hexo 强力驱动
主题 - NexT.Pisces