知识要点
- vue工作机制
- vue响应式的原理
- 依赖收集与追踪
- 编译compile
vue工作机制
初始化
在new Vue()
之后。Vue会调用进行初始化,会初始化生命周期、事件、props、methods、data、computed与watch等。其中最重要的是通过Object.defineProperty
设置setter
与getter
,用来实现响应式以及依赖收集。
初始化后调用$mount
挂载组件。
new Vue()
init初始化后,$mount挂载到你准备渲染的DOM节点中,然后compile()
会进行编译,生成渲染更新函数render function
更改的都是虚拟DOM节点,更新之前会做diff算法的比较,计算出最小的DOM更新。然后执行到patch()
进行更新打补丁(用js的时间换DOM操作的时间)减少页面渲染的次数和数量,最后更新到真实的DOM。
Watcher
观察者,当数据变化时进行更新
编译
编译模式分为3个阶段
-
parse
- 使用正则解析template中的vue的指令(v-xxx)变量等等,形成语法书AST
-
optimize
- 标记一些静态节点,用做后面的性能优化,在diff的时候直接略过
-
generate
- 把第一部分生成的AST转化成渲染函数render function
响应式原理 defineProperty
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>defineProperty</title>
</head>
<body>
<div id="app">
<p id="name"></p>
</div>
<script>
var obj = {};
Object.defineProperty(obj, 'name', {
get: function () {
return document.querySelector('#name').innerHTML;
},
set: function (val) {
document.querySelector('#name').innerHTML = val;
}
})
obj.name = "htf";
</script>
</body>
</html>
自己实现一个响应式
- html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>HVue</title>
</head>
<body>
</body>
<script src="./HVue.js"></script>
<script>
const app = new HVue({
data: {
test: "I am test",
foo: {
bar: "bar"
}
}
});
app.$data.test = "hello, HTF";
app.$data.foo.bar = "oh my bar";
</script>
</html>
- js
class HVue {
constructor(options) {
this.$options = options;
// 数据响应化
this.$data = options.data;
this.observe(this.$data);
}
observe(value) {
if(!value || typeof value !== 'object') return;
// 遍历该对象
Object.keys(value).forEach(key => {
this.defineReactive(value, key, value[key])
})
}
// 数据响应化
defineReactive(obj, key, val) {
this.observe(val); // 递归遍历,对象中的嵌套属性
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
if(newVal === val) return;
val = newVal
console.log(`${key}属性更新了:${val}`);
}
})
}
};
ps: vue数据响应的原理:
vue利用defineProperty进行数据劫持,简单描述下,vue利用的了Object.defineProperty()属性,把data里面的数据的每一个属性都定义了一个getter和setter的方法,让我们有机会去监听这些属性的变化,当这些属性发生变化的时候,可以通知那些需要更新的地方去更新
依赖收集与追踪
理解下面例子
new Vue({
template:
`<div>
<span>{{ name1 }}</span>
<span>{{ name2 }}</span>
<span>{{ name1 }}</span>
</div>`
data: {
name1: 'name1',
name2: 'name2',
name3: 'name3'
},
created() {
this.name1 = "HTF",
this.name3 = "HZH"
}
})
name1被修改,视图更新,且需要更新两处
name2被修改,视图更新
name3没用到,不需要更新
如何实现呢,需要扫描视图收集依赖,知道视图中到底哪些地方对数据有依赖,这样当数据变化的时候就能知道需
要更新哪个,不需要更新哪个。
demo
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>HVue</title>
</head>
<body>
</body>
<script src="./HVue.js"></script>
<script>
const app = new HVue({
data: {
test: "I am test",
foo: {
bar: "bar"
}
}
});
app.$data.test = "hello, HTF";
app.$data.foo.bar = "oh my bar";
</script>
</html>
HVue.js
class HVue {
constructor(options) {
this.$options = options;
// 数据响应化
this.$data = options.data;
this.observe(this.$data);
// 模拟一下watcher创建
new Watcher();
this.$data.test;
new Watcher();
this.$data.foo.bar;
}
observe(value) {
if(!value || typeof value !== 'object') return;
// 遍历该对象
Object.keys(value).forEach(key => {
this.defineReactive(value, key, value[key])
})
}
// 数据响应化
defineReactive(obj, key, val) {
this.observe(val); // 递归遍历,对象中的嵌套属性
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
// 判断这个依赖项是否存在,存在就加到dep中
Dep.target && dep.addDep(Dep.target)
return val
},
set(newVal) {
if(newVal === val) return;
val = newVal
console.log(`${key}属性更新了:${val}`);
// 通知需要更新的依赖项
dep.notify();
}
})
}
};
// Dep: 用来管理Watcher
class Dep {
constructor() {
// 这里存放若干依赖(watcher)
this.deps = [];
}
addDep(dep) {
this.deps.push(dep)
}
notify() {
this.deps.forEach(dep => dep.update())
}
}
// Watcher
class Watcher {
constructor() {
// 将当前Watcher实例指定到Dep静态属性target
Dep.target = this;
}
update() {
console.log("属性更新了")
}
}
编译compile
核心逻辑获取dom,遍历dom,获取{{}}格式的变量,以及每个dom的属性,截取k-和@开头的设置响应式
所有代码逻辑
index2.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>HVue</title>
</head>
<body>
<div id="app">
<p>{{name}}</p>
<p k-text="name"></p>
<p>{{age}}</p>
<p>{{doubleAge}}</p>
<input type="text" k-model="name">
<button @click="changeName">呵呵</button>
<div k-html="html"></div>
</div>
</body>
<script src="./HVue.js"></script>
<script src="./compile.js"></script>
<script>
const Htf = new HVue({
el: "#app",
data: {
name: "I am test",
age: "12",
html: "<button>这是一个按钮</button>"
},
created () {
console.log("开始了");
setTimeout(() => {
this.name = "我是测试"
}, 1500);
},
methods: {
changeName() {
this.name = "哈喽, htf";
this.age = 1
}
}
})
</script>
</html>
HVue.js
class HVue {
constructor(options) {
this.$options = options;
// 数据响应化
this.$data = options.data;
this.observe(this.$data);
// 模拟一下watcher创建
// new Watcher();
// this.$data.test;
// new Watcher();
// this.$data.foo.bar;
// 初始化编译器
new Compile(options.el, this);
// created的执行
if (options.created) {
options.created.call(this);
}
}
observe(value) {
if(!value || typeof value !== 'object') return;
// 遍历该对象
Object.keys(value).forEach(key => {
this.defineReactive(value, key, value[key]);
// 代理data中的属性到vue实例上
this.proxyData(key);
})
}
// 数据响应化
defineReactive(obj, key, val) {
this.observe(val); // 递归遍历,对象中的嵌套属性
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
// 判断这个依赖项是否存在,存在就加到dep中
Dep.target && dep.addDep(Dep.target)
return val
},
set(newVal) {
if(newVal === val) return;
val = newVal
console.log(`${key}属性更新了:${val}`);
// 通知需要更新的依赖项
dep.notify();
}
})
}
proxyData(key) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
})
}
};
// Dep: 用来管理Watcher
class Dep {
constructor() {
// 这里存放若干依赖(watcher)
this.deps = [];
}
addDep(dep) {
this.deps.push(dep)
}
notify() {
this.deps.forEach(dep => dep.update())
}
}
// Watcher
class Watcher {
constructor(vm, key, cd) {
this.vm = vm;
this.key = key;
this.cd = cd;
// 将当前Watcher实例指定到Dep静态属性target
Dep.target = this;
this.vm[this.key]; // 触发getter,添加依赖
Dep.target = null;
}
update() {
console.log("属性更新了")
this.cd.call(this.vm, this.vm[this.key])
}
}
compile.js
// 用法 new Compile(el, vm)
class Compile{
constructor(el, vm) {
// 要遍历的宿主节点
this.$el = document.querySelector(el);
this.$vm = vm;
// 编译
if(this.$el) {
// 转换内部内容为片段fragment
this.$fragment = this.node2Fragment(this.$el);
// console.log(this.$fragment)
// 执行编译
this.compile(this.$fragment);
// 将编译完的结果追加到$el
this.$el.appendChild(this.$fragment);
}
};
// 将宿主元素中的代码片段拿出来遍历,这样子做比较高效
node2Fragment(el) {
const frag = document.createDocumentFragment();
// 将el中所有子元素搬至frag中
let child;
while(child = el.firstChild ) {
frag.appendChild(child);
}
return frag;
};
compile(el) {
console.log(el.childNodes)
const childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
// 类型判断
if(this.isElement(node)) {
// 元素
// console.log(`编译元素${node.nodeName}`);
// 查找k-, @, :
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach(attr => {
const attrName = attr.name; // 属性名字
const exp = attr.value; // 属性值
if(this.isDirective(attrName)) {
// k-text
const dir = attrName.substring(2);
// 执行指令
this[dir] && this[dir](node, this.$vm, exp);
}
if(this.isEvent(attrName)) {
const dir = attrName.substring(1); // @click
this.eventHandler(node, this.$vm, exp, dir);
}
})
}else if(this.isInterpolation(node)) {
// 文本
// console.log(`编译文本${node.textContent}`);
this.compileText(node);
}
// 递归子节点
if(node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
};
compileText(node) {
// 其下的正则进行匹配分组所可以用.$1获取到插值表达式里面的值
// console.log(RegExp.$1)
this.updated(node, this.$vm, RegExp.$1, 'text' )
}
// 更新函数
updated (node, vm, exp, dir) {
const updaterFn = this[dir+'Updater'];
// 初始化
updaterFn && updaterFn(node, vm[exp]);
// 依赖收集
new Watcher(vm, exp, function(value) {
updaterFn && updaterFn(node, value);
})
}
text(node, vm, exp) {
this.updated(node, vm, exp, 'text' )
}
// 双向绑定的处理
model(node, vm, exp) {
// 指定input的value属性
this.updated(node, vm, exp, "model");
// 视图对于模型的响应
node.addEventListener("input", e => {
vm[exp] = e.target.value;
})
}
// k-html 的绑定
html(node, vm, exp) {
this.updated(node, vm, exp, "html");
}
// 事件处理器
eventHandler(node, vm, exp, dir) {
const fn = vm.$options.methods && vm.$options.methods[exp];
if(dir && fn) {
node.addEventListener(dir, fn.bind(vm));
}
}
textUpdater(node, value) {
node.textContent = value;
}
modelUpdater(node, value) {
node.value = value;
}
htmlUpdater(node, value) {
node.innerHTML = value;
}
isEvent(attr) {
return attr.indexOf('@') == 0;
}
isDirective(attr) {
return attr.indexOf('k-') == 0;
}
isElement(node) {
return node.nodeType === 1;
};
// 差值文本
isInterpolation(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
vue编译过程是怎样的?
什么是编译,首先vue写的模板这些东西浏览器是根本就不识别的,通过编辑可以进行依赖收集,通过依赖收集就可以吧data中的这些数据模型和视图之间产生了依赖关系,如果以后模型发生变化的时候,我们就可以通知这些依赖的地方,让他们进行更新,这就是执行编译的目的,这样就可以做到模型驱动视图的变化。
双向绑定的原理是什么?
我们在做双向数据绑定的时候通常会使用v-model的指令,我们在编译的过程中可以解析出这个v-model,然后我在做操作的时候有两件事情,第一件事是在v-model所属的事件源加了一个事件监听,如果这个input发生变化的时候,我就可以把最新的值设置到vue的实例上,因为vue的实例上已经实现了数据的响应化,他的响应化的setter函数会触发界面中所有模型依赖的更新,会通知所有那些依赖做更新操作,所以界面中跟这些数据相关的所有部分就更新了。
网友评论