Vue学习笔记(一)
2021-06-04 Vue
对MVVM的理解 #
MVVM是Model-View-ViewModel的简写。本质上是MVC的改进版。MVVM就是将其中的View的状态和行为抽象化,将试图UI和业务逻辑分开。这些事情ViewModel已经帮我们做到了,可以quchuModel的数据的同时帮忙处理View中由于需要展示内容而设计的业务逻辑。
MVVM优点 #
- 低耦合 视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
- 可重用 可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
- 独立开发 开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计
- 可测试 界面素来是比较难于测试的,测试可以针对ViewModel来写。
Vue2 与Vue3响应式数据 #
vue2响应式数据 #
- 对象类型值变化时,是内部通过
defineReactive
方法,使用Object.defineProperty
将属性进行劫持(只会劫持已经存在的属性) - 数组类型值变化时,则是通过重写数组刚发来实现。
多层对象是通过递归来实现劫持。
Vue3响应式数据 #
Vue3是通过proxy
来实现的.
Vue中检测数组变化 #
- 数组考虑性能的原因没有用
defineProperty
对数组的每一项进行拦截,而是重写数组的(push
,shift
,pop
,splice
,unshift
,sort
,reverse
)方法。 - 数组中如果是对象类型也会进行递归劫持
- 数组的索引和长度变化时候无法被监控
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted) // 新增的数据需要进行观测
// notify change
ob.dep.notify()
return result
})
})
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted) // 新增的数据需要进行观测
// notify change
ob.dep.notify()
return result
})
})
Vue中的依赖收集过程 #
- 每个属性都拥有自己的
dep
属性,存放它所依赖的watcher,当属性变化后会通知自己的watcher去进行更新 - 默认在初始化时会调用render函数,此时会触发属性依赖收集
dep.depend
- 当属性发生变化时会触发
watcher
更新dep.notify()
Vue中的模板编译原理 #
template是怎样转换成render函数的
- 将template模板转换成
ast
语法树-parserHtml
- 对静态语法做静态标记-
markUp
diff
来做优化的静态节点 - 重新生成代码 -
codeGen
src/compiler/index.js:11
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options) // 1.解析ast语法树
if (options.optimize !== false) {
optimize(ast, options) // 2.对ast树进行标记,标记静态节点
}
const code = generate(ast, options) // 3.生成代码
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options) // 1.解析ast语法树
if (options.optimize !== false) {
optimize(ast, options) // 2.对ast树进行标记,标记静态节点
}
const code = generate(ast, options) // 3.生成代码
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
Vue生命周期方法 #
beforeCreate
在实例初始化之后,数据观测和event/watcher事件配置之前被调用created
实例已经创建完成之后被调用,在这,实例已经完成 数据观测、属性和方法的运算,watch/event事件回调 这里没有$elbeforeMount
在关在开始之前被调用,相关的render函数首次被调用mounted
el被创建的 vm.$el 替换,并关在到实例上去之后调用该钩子beforeUpdate
数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前。updated
由于数据更改导致的虚拟DOM重新渲染和补丁,在这之后会调用改钩子。beforeDestroy
实例销毁之前调用。此时,实例仍然完全可用destroyed
实例销毁后调用,调用后,Vue实例的所有东西都会解除绑定,所有事件监听器会被移除,所有的子实例也会被销毁。该生命周期在服务器渲染期间不被调用。keep-alive
(activated
和deactivaed
)
Vue的生命周期钩子实现原理 #
- Vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法
- 内部会对钩子进行处理 将钩子函数维护成数组的形式
src/core/instance/init.js:38 初始化合并 src/core/util/options.js:388 合并选项
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
const res = childVal // 儿子有
? parentVal
? parentVal.concat(childVal) // 父亲也有,那就是合并
: Array.isArray(childVal) // 儿子是数组
? childVal
: [childVal] // 不是数组包装成数组
: parentVal
return res
? dedupeHooks(res)
: res
}
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
const res = childVal // 儿子有
? parentVal
? parentVal.concat(childVal) // 父亲也有,那就是合并
: Array.isArray(childVal) // 儿子是数组
? childVal
: [childVal] // 不是数组包装成数组
: parentVal
return res
? dedupeHooks(res)
: res
}
Vue.mixin 的使用场景和原理 #
- Vue.mixin的作用就是抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用
mergeOptions
方法进行合并,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据有冲突,会采用“就近原则”以组件的数据为准 - mixin 中有很多缺陷“命名冲突问题”、“依赖问题”、“数据来源问题”
src/core/global-api/mixin.js
Vue.mixin = function (options) {
this.options = mergeOptions(this.options, options);
return this
}
// =====
const strats = {};
lifeCycleHooks.forEach(hook => {
strats[hook] = mergeHook
})
// =====
function mergeOptions(parent, child) {
const options = {};//合并之后的结果
for (let key in parent) {
mergeField(key);
}
for (let key in child) {
if (parent.hasOwnProperty(key)) {
continue;
}
mergeField(key)
}
function mergeField(key) {
let parentValue = parent[key];
let childValue = child[key];
// 策略模式
if (strats[key]) {
options[key] = strats[key](parentValue, childValue);
} else {
if (isObject(parentValue) && isObject(childValue)) {
options[key] = { ...parentValue, ...childValue }
} else {
// 父亲中有 儿子中没有 就用父亲的
options[key] = child[key] || parent[key]
}
}
}
return options;
}
Vue.mixin = function (options) {
this.options = mergeOptions(this.options, options);
return this
}
// =====
const strats = {};
lifeCycleHooks.forEach(hook => {
strats[hook] = mergeHook
})
// =====
function mergeOptions(parent, child) {
const options = {};//合并之后的结果
for (let key in parent) {
mergeField(key);
}
for (let key in child) {
if (parent.hasOwnProperty(key)) {
continue;
}
mergeField(key)
}
function mergeField(key) {
let parentValue = parent[key];
let childValue = child[key];
// 策略模式
if (strats[key]) {
options[key] = strats[key](parentValue, childValue);
} else {
if (isObject(parentValue) && isObject(childValue)) {
options[key] = { ...parentValue, ...childValue }
} else {
// 父亲中有 儿子中没有 就用父亲的
options[key] = child[key] || parent[key]
}
}
}
return options;
}
Vue组件data是函数的原因 #
- 每次使用组件时都会组件进行实例化操作,并且调用data函数返回一个对象作为组件的数据源。这样可以保证多个组件间数据互不影响。
function Vue() {}
function Sub() { // 会将data存起来
this.data = this.constructor.options.data;
}
Vue.extend = function(options) {
Sub.options = options;
return Sub;
}
let Child = Vue.extend({
data: { name: 'html' }
});
// 两个组件就是两个实例, 希望数据互不干扰
let child1 = new Child();
let child2 = new Child();
console.log(child1.data.name);
child1.data.name = 'js';
console.log(child2.data.name);
function Vue() {}
function Sub() { // 会将data存起来
this.data = this.constructor.options.data;
}
Vue.extend = function(options) {
Sub.options = options;
return Sub;
}
let Child = Vue.extend({
data: { name: 'html' }
});
// 两个组件就是两个实例, 希望数据互不干扰
let child1 = new Child();
let child2 = new Child();
console.log(child1.data.name);
child1.data.name = 'js';
console.log(child2.data.name);
nextTick的使用场景和原理 #
nextTick
中的回调是在下载DOM更新循环结束之后执行的延迟回调- 可以用于获取更新后的DOM
- Vue中数据更新时异步的,使用
nextTick
方法可以保证用户定义的逻辑在更新之后执行。
let waiting = false;
const callback = [];
function flushCallback() {
waiting = false
const copyCallback = callback.slice(0);
callback.length = 0;
copyCallback.forEach(cb => cb())
}
let timerFn = null;
if (Promise) {
timerFn = () => Promise.resolve().then(flushCallback);
} else if (MutationObserver) {
let textNode = document.createTextNode(1);
let observe = new MutationObserver(flushCallback);
observe.observe(textNode, {
characterData: true
})
timerFn = () => {
textNode.textContent = 12;
}
} else if (setImmediate) {
timerFn = () => {
setImmediate(flushCallback);
}
} else {
timerFn = () => {
setTimeout(flushCallback, 0);
}
}
export function nextTick(cb, ctx) {
callback.push(cb);
if (!waiting) {
timerFn();
waiting = true;
}
}
let waiting = false;
const callback = [];
function flushCallback() {
waiting = false
const copyCallback = callback.slice(0);
callback.length = 0;
copyCallback.forEach(cb => cb())
}
let timerFn = null;
if (Promise) {
timerFn = () => Promise.resolve().then(flushCallback);
} else if (MutationObserver) {
let textNode = document.createTextNode(1);
let observe = new MutationObserver(flushCallback);
observe.observe(textNode, {
characterData: true
})
timerFn = () => {
textNode.textContent = 12;
}
} else if (setImmediate) {
timerFn = () => {
setImmediate(flushCallback);
}
} else {
timerFn = () => {
setTimeout(flushCallback, 0);
}
}
export function nextTick(cb, ctx) {
callback.push(cb);
if (!waiting) {
timerFn();
waiting = true;
}
}
computed
和watch
区别 #
computed
和watch
都是Watcher来实现的computed
属性是具备缓存,依赖的值不发生变化,对其取值时计算属性方法不会重新执行- watch则是监控值的变化,当值发生变化时调用对应的回调函数
Vue.prototype.$watch = function (exprOrFn, cb, options = {}) {
options.user = true; // 标记为用户watcher
const watcher = new Watcher(this, exprOrFn, cb, options);
if (options.immediate) {
cb.call(vm, watcher.value)
}
}
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.user = !!options.user;
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn;
} else {
this.getter = function () {
let path = exprOrFn.split('.');
let obj = vm;
for (let i = 0; i < path.length; i++) {
obj = obj[path[i]]
}
return obj;
}
}
this.vm = vm;
this.dirty = options.lazy;
this.lazy = !!options.lazy;
this.exprOrFn = exprOrFn;
this.cb = cb;
this.deps = [];
this.depsId = new Set();
this.options = options;
// 默认应该 让 exprOrFn 执行
this.id = id++;
// 是否是lazy -> computed
this.value = this.lazy ? undefined : this.get(); // 默认初始化 要取值
}
get() { // 稍后用户更新时 可以重新调用getter方法
// defineProperty.get 每个属性都可以收集自己的watcher
// 结论: 一个属性可以对应多个watcher,同时一个watcher可以对应多个属性
pushTarget(this);
const value = this.getter.call(this.vm); // 会调用render方法 会进行取值
popTarget(); // Dep.target = null; 如果有值 说明在模板中有使用
return value;
}
run() {
let value = this.get();
let oldValue = this.value;
this.value = value;
if (this.user) {
this.cb.call(this.vm, value, oldValue)
}
}
addDep(dep) {
let id = dep.id;
if (!this.depsId.has(id)) {
this.depsId.add(id);
this.deps.push(dep);
dep.addSub(this);
}
}
update() {
if (this.lazy) { // 如果是计算的属性,把dirty设置为脏的。 在下次取值时从新计算
this.dirty = true;
} else {
// 每次跟新时 多次调用update 先将watcher缓存下来 ,等下一起更新
queueWatcher(this);
// this.get()
}
}
evaluate() {
this.dirty = false; // 表示已经取过值了
this.value = this.get();
}
depend() {
let i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
}
}
Vue.prototype.$watch = function (exprOrFn, cb, options = {}) {
options.user = true; // 标记为用户watcher
const watcher = new Watcher(this, exprOrFn, cb, options);
if (options.immediate) {
cb.call(vm, watcher.value)
}
}
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.user = !!options.user;
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn;
} else {
this.getter = function () {
let path = exprOrFn.split('.');
let obj = vm;
for (let i = 0; i < path.length; i++) {
obj = obj[path[i]]
}
return obj;
}
}
this.vm = vm;
this.dirty = options.lazy;
this.lazy = !!options.lazy;
this.exprOrFn = exprOrFn;
this.cb = cb;
this.deps = [];
this.depsId = new Set();
this.options = options;
// 默认应该 让 exprOrFn 执行
this.id = id++;
// 是否是lazy -> computed
this.value = this.lazy ? undefined : this.get(); // 默认初始化 要取值
}
get() { // 稍后用户更新时 可以重新调用getter方法
// defineProperty.get 每个属性都可以收集自己的watcher
// 结论: 一个属性可以对应多个watcher,同时一个watcher可以对应多个属性
pushTarget(this);
const value = this.getter.call(this.vm); // 会调用render方法 会进行取值
popTarget(); // Dep.target = null; 如果有值 说明在模板中有使用
return value;
}
run() {
let value = this.get();
let oldValue = this.value;
this.value = value;
if (this.user) {
this.cb.call(this.vm, value, oldValue)
}
}
addDep(dep) {
let id = dep.id;
if (!this.depsId.has(id)) {
this.depsId.add(id);
this.deps.push(dep);
dep.addSub(this);
}
}
update() {
if (this.lazy) { // 如果是计算的属性,把dirty设置为脏的。 在下次取值时从新计算
this.dirty = true;
} else {
// 每次跟新时 多次调用update 先将watcher缓存下来 ,等下一起更新
queueWatcher(this);
// this.get()
}
}
evaluate() {
this.dirty = false; // 表示已经取过值了
this.value = this.get();
}
depend() {
let i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
}
}
Vue.set方法实现 #
- 我们给对象和数组本身都增加了
dep
属性 - 当给对象新增不存在的属性则触发对象依赖
watcher
去更新 - 当修改数组索引时 调用组数本身的splice方法去更新数组
export function set(target, key, val) {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val)
return val;
}
// target 为对象, key在targer或者target.prototype上
// 必须同时不能在Object.prototype上
if (key in targer && !(key in Object.prototype)) {
target[key] = val;
return val;
}
// 以上都不成立,那么久开始给target创建一个全新的属性
// 获取Observe实例
const ob = target.__ob__;
if (!ob) {
target[key] = val;
return val;
}
defineReactive(ob.val, key, val);
ob.dep.notify();
return val;
}
export function set(target, key, val) {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val)
return val;
}
// target 为对象, key在targer或者target.prototype上
// 必须同时不能在Object.prototype上
if (key in targer && !(key in Object.prototype)) {
target[key] = val;
return val;
}
// 以上都不成立,那么久开始给target创建一个全新的属性
// 获取Observe实例
const ob = target.__ob__;
if (!ob) {
target[key] = val;
return val;
}
defineReactive(ob.val, key, val);
ob.dep.notify();
return val;
}
版权属于: vincent
转载时须注明出处及本声明