目录
一个vue实例
生命周期钩子函数
set/get 访问器属性的实现
v-model属性与{{text}}在模板中是如何被解析的?
观察者模式
源码
一个典型的vue实例:
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div>
...
let vm = new Vue({
el: '#app',
data:()=>({
text:'hi'
}),
created () {
console.log(this.text)
}
})
当Vue选项对象中有render渲染函数时,Vue构造函数将直接使用渲染函数渲染DOM树,否则Vue构造函数将尝试通过template模板编译生成渲染函数,然后再渲染DOM树,如果template也没有,则会通过el属性获取挂载元素的outerHTML来作为模板,并编译生成渲染函数。
简言之,优先级render>template>el。
运行效果:
这里有几个问题值得思考:
created是vue实例的生命周期钩子函数之一。
如上所示,所有生命周期钩子函数依次有:beforeCrate->created->beforeMount->mounted->beforeDestroy->destroyed。
这些钩子函数给了开发者在vue生命不同周期阶段执行自己代码的机会。大多数情况下业务逻辑都是在放在created函数内,若干事件监听的移除、资源销毁等放在destroyed函数内。
在上面的示例中,为什么在created中可以用this.text访问data对象中的text属性呢?
在vue实例中,vm.$data指代data,通过this.text访问与通过this.$data.text访问有没有区别?
vue源码略过复杂,有开发者依照其原理,写了一个基本的声明式渲染双向绑定的例子,可以在源码/simple-vue-project/public/index-vue-compile.html文件中查看。
通过学习这个文件,可以了解vue是如何实现的。
在vue实例初始化时,会对data做一些分析,将data的属性依次循环在vm实例上做一个访问器属性代理,主要涉及的代码类似于:
observe(data, this);
// 循环设置每一个属性
function observe (obj, vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm, key, obj[key]);
})
}
// data对象中的属性,被解析并定义在了vm上
// 此处obj = vm
function defineReactive (obj, key, val) {
// 此时监听器是属于单个属性的,一个属性一个监听器
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function () {
// 添加订阅者 watcher 到主题对象 Dep
if (Dep.target) dep.addSub(Dep.target);
return val
},
set: function (newVal) {
if (newVal === val) return
val = newVal;
// 作为发布者发出通知
// 当用户在input处输入内容后,触发data变量的set更新
// 进而触发Watcher.update调用
dep.notify();
}
});
}
在当template或created中访问this时,指代的是vm实例对象。在上文代码中,defineReactive函数给每一个data中的属性,例如text,在vm上通过Object.defineProperty注册了set/get一对访问器属性。
当访问this.text时,执行的是get;当调用this.text = 'xx'时,执行的是set。
由于第三个参数val是一个引用,在上下文中实际指代obj[key],所以val = newVal这行代码才是起作用的。
这就是第4个问题,created函数中,为什么可以通过this.text访问data对象中text属性的答案。
vm.$data.text与this.text指向的是同一块内存区域。但是直接修改vm.$data.text,并没有触发set中的诸如dep.notify之类的视图更新操作,所以两者还是有区别的。
Vue选项对象中的data,只能在实例化前指定;如果运行时想添加被监察变化的属性,可以使用this.$set方法。
var dom = nodeToFragment(document.getElementById(id), this);
// 编译完成后,将 dom 返回到 app 中
document.getElementById(id).appendChild(dom);
...
function nodeToFragment (node, vm) {
var flag = document.createDocumentFragment();
var child;
while (child = node.firstChild) {
// 转化是为了调用 compile,解析template内容
compile(child, vm);
flag.appendChild(child); // 将子节点劫持到文档片段中
}
return flag
}
...
function compile (node, vm) {
var reg = /\{\{(.*)\}\}/;
// 节点类型为元素
if (node.nodeType === 1) {
var attr = node.attributes;
// 解析属性
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue; // 获取 v-model 绑定的属性名
node.addEventListener('input', function (e) {
// 给相应的 data 属性赋值,进而触发该属性的 set 方法
//
vm[name] = e.target.value;
});
// 单就本示例而言,这行代码与下面的Watcher做的工作是重复的
// node.value = vm[name]; // 将 data 的值赋给该 node
node.removeAttribute('v-model');
}
};
// 传递input、text类型,是为了区别更新的属性名称
new Watcher(vm, node, name, 'input');
}
// 节点类型为 text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1; // 获取匹配到的字符串
name = name.trim();
new Watcher(vm, node, name, 'text');
}
}
}
使用DocumentFragment API,是为了解析html。核心逻辑都在compile函数内。
当nodeType为1时,看看没有v-mode属性,如果有,把它删除;在删除之前,对node添加一个input事件监听,当输入文本有变化时,调用vm上的set。在这里是vm.text。
当nodeType为3时,这是一个模板变量,在这里就是{{text}}。这也是一个node对象。通过正则匹配出变量名称,并注册变量的监听器,当变量变化时更新这个node的nodeValue。
这就是双向绑定,是第1、2、3问题的答案。
Watcher是什么?
可以看作是一个观察者模式对象,每个属性都有一个自己的Watcher。
function Watcher (vm, node, name, nodeType) {
// 只有在编译阶段,Dep.target才不为null
// this等于Watcher实例
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.nodeType = nodeType;
// update间接调用了vm的访问器get,在那里注册了data变量变化的监听
this.update();
// target设置为null,只有编译时添加监听一次
Dep.target = null;
}
Watcher.prototype = {
// 数据变化时,仍会调用
update: function () {
this.get();
if (this.nodeType == 'text') {
// nodeValue是{{xxx}}节点文本值
this.node.nodeValue = this.value;
}
if (this.nodeType == 'input') {
// value指input的value
// 这里仅处理示例中的有限指令
this.node.value = this.value;
}
},
// 获取 data 中的属性值
get: function () {
this.value = this.vm[this.name]; // 触发相应属性的 get
}
}
Watcher的update方法,在变量发生变化时会被调用。而data变量更新的根源,还在于对原生html dom元素的事件监听。
https://git.code.tencent.com/shiqiaomarong/vue-go-rapiddev-example/tags/v20200114