简介
vue 介绍
我们都知道目前使用vue已经是前端开发的主流框架之一,经过大家的经常使用越来越多的的人也渐渐的想要深入学习vue的原理,这篇文章将由我带大家学习vue的源码,vue有很多属性比如data,props,computed,watch等,那么这些属性是怎么实现的呢,接下来我会慢慢的带大家实现一下vue.js的实现原理。vue 本质上就是个函数,我们知道vue是MVVM框架这里就不介绍这个框架了,这里大致说一下它的实现原理 通过Object.defineProperty对数据进行劫持/代理,再使用数据的时候就会触发数据的 get 方法,修改数据的时候触发 set 方法,从而对数据进行劫持,再根据观察者模式(发布订阅者模式改造版,只不过订阅者也可以向发布者发送信息,所以两者都称为发布者)。这里我们需要知道Object.defineProperty的功能和发布-订阅设计模式的设计思维。
注意:开发的时候的使用脚手架
vue-cli帮我们快速生成一个vue项目,我们可以在项目中使用以.vue后缀名结尾的文件进行页面和逻辑的编写,这是因为vue的脚手架搭配了webpack,webpack中使用vue-loader可以帮我们把.vue文件编译为浏览器识别的html+css+js文件。所以vue的本质还是Vue的实现。
vue 在单页面文件中使用
vue 在 html 的引入以及使用。vue 本身是一个构造函数,采用 new 的方式创建实例,然后进行传入配置项,初始化 vue 实例的状态。接着我们可以在页面中使用vue语法。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>vue源码分析</title>
<script src="./myVue.js"></script>
</head>
<body>
<div
id="app"
style="color: red;background-color: aquamarine;font-size: 14px;"
>
<span style="color: yellow;">{{a}}</span>
<div class="text">2332{{b}}</div>
<div class="app"></div>
</div>
<script>
// 对vue实例传入配置
let vm = new Vue({
el: "#app",
data() {
return {
a: 99999,
c: {
test: 12,
str: "qwe",
},
hobby: [
{
label: "数组",
}
],
};
},
});
vm.c.test = 1234;
vm.hobby[0].ttt = 123;
vm.hobby.push(4444);
console.log("vm==", vm.hobby);
</script>
</body>
</html>
如下图我们可以看到vue的工作流程
一 初始化
vue 有多个配置项,比如:data,props,watch,computed 等配置,在初始化 vue 项目的时候,需要对这些配置项进行初始化,还有就是 vue 本身的方法和属性初始化,所以也是初始化 vue 的状态。初始化emit,$on 等方法,还有_c,_s,_v 等render 函数组成函数。 createElementVNode 和 createTextVNode会在生成虚拟 DOM 函数中介绍。
如下我们开始初始化vue的属性和方法
function Vue() {
this._init(options);
}
initMixin(Vue);
initLifeCycle(Vue)
initMixin方法拓展vue原型方法,初始化vue原型方法
function initMixin(Vue) {
//_init方法获取配置传入vue的配置()
Vue.prototype._init = function (options) {
//vue默认以$开头的为自己的属性
const vm = this;
vm.$options = options;
//初始化状态
initState(vm);
if (options.el) {
vm.$mount(options.el);
}
};
Vue.prototype._c=function(){
//详细代码可以去拉取仓库查看
}
//初始化$mount方法,用于挂载模板。el为传递的模板的css选择器
Vue.prototype.$mount = initMount
}
initLifeCycle方法是初始化生命周期,其中_update方法就是vue执行更新操作的时候调用的函数,他会将render函数执行的结果也就是虚拟DOM保存起来,在下一次更新的时候采用diff算法
//初始化生命周期
function initLifeCycle(Vue) {
//执行更新操作,传入的是虚拟DOM,与旧的虚拟DOM相比,更新变化的部分
Vue.prototype._update = function (vnode) {
this.$el = patch(this.$el, vnode); //patch既有初始化的功能又有更新功能
};
//渲染虚拟DOM
//ast语法树渲染太好性能,所以转成render函数之后,每次更新只执行render函数即可,无需再执行ast转化过程
Vue.prototype._render = function () {
const vm = this;
//修改render函数的this指向vm
return vm.$options.render.call(this);
};
}
initState用来初始化传入vue配置项,即new Vue(option),初始化option配置项
function initState(vm) {
const ops = vm.$options;
// 初始化prop属性
if (ops.props) {
initProps();
}
// 初始化data属性
if (ops.data) {
initData(vm);
}
// 初始化computed属性
if (ops.computed) {
initComputed(vm);
}
// 初始化watch属性
if (ops.watch) {
initWatch(vm);
}
}
初始化 data 响应式数据
初始化 vue 的 data 的数据时,因为 data 可能为对象或者函数,所以需要先获取真正的数据,data 中的对象(object)和基础类型(number,string 等)使用(Object.defineProperty)数据进行劫持,然后将数据绑定在 vm 的实例属性_data 上,即 vm._data 等于配置项的 data,至此 data 配置项的数据和 vm 实例实现绑定。 observe 函数主要用于对数据进行劫持之前会先判断 vue 数据是否已经为响应式,然后再使用 Observe 来设置数据,如果是数组则采用重写数组的方法来将数组劫持,如果是对象则采用 Object.defineProperty 对数据进行劫持。
function initData(vm) {
let data = vm.$options.data;
data = typeof data === "function" ? data.call(this) : data; //data可能为函数或者对象
vm._data = data; //在vm上绑定data,用于绑定data配置的数据
observe(data); //对数据进行劫持
for (let key in data) {
proxy(vm, "_data", key); //将数据代理到vm实例上,使用this来访问变量如this.name,并且只代理data数据的第一层
}
}
//vue2的响应式原理,对数据的劫持
function observe(data) {
if (typeof data !== "object" || data === null) {
return; //data不是对象则不劫持
}
if (data.__ob__ instanceof Observe) {
return data.__ob__;
}
// 对数据进行劫持操作
return new Observe(data);
}
数据劫持
使用数据的劫持,首先在数据初始化的时候会给 data 绑定一个 ob 的属性,该属性指向 vm。有两个作用:1 是可以判断数据是否已经被劫持,2 是对数组的方法进行劫持的时候,数组的元素可能为对象,因此也需要回调观察者的方法进行递归观察
注意:数组本身的引用已经被劫持了,所以调用数组的时候会触发 setter,但是对数组内部的元素却没有劫持,因为元素的调用并不会触发 setter 或者 getter。因此数组元素变化是采用重写数组的方法实现的。 dep属性是用来进行依赖收集的,下面会有依赖收集的介绍
Observe表示一个劫持数组的操作类,并且在劫持数据的时候会递归调用,所以每个被劫持的数据身上都会有一个收集器this.dep 用来收集页面上使用该数据的地方(也就是vue组件)
//观察对象是否被劫持
class Observe {
constructor(data) {
//挂载this到data的属性上,有两个作用:1是可以判断数据是否已经被劫持,2是对数组的方法进行劫持的时候,
// 数组的元素可能为对象,因此也需要回调观察者的方法
//数组只是重写方法,没有进行依赖收集
//需要对数组的对象元素进行劫持,所以对数组和对象增加一个dep依赖收集
this.dep = new Dep();//依赖收集
Object.defineProperty(data, "__ob__", {
value: this,//挂载this到vm.data上
enumerable: false, //不可枚举,不可遍历
});
if (Array.isArray(data)) {
// reDefineArray重写数组方法
data.__proto__ = reDefineArray();
this.observeArray(data);
} else {
this.walk(data);
}
}
walk(data) {
//循环对象属性来劫持对象的引用类型属性
Object.keys(data).forEach((key) => defineReactive(data, key, data[key]));
}
//监测数组的变化
observeArray(data) {
data.forEach((item) => observe(item));
}
}
对对象数据进行劫持
采用的就是Object.defineProperty对数据进行劫持,当数据进行get和set操作的时候就会触发该函数,并且我们会递归遍历对象,是对象上的所有引用类型都被劫持。这也是vue性能不好的地方之一。
//对对象数据进行劫持
function defineReactive(target, key, value) {
//这里会递归调用,也会对每个属性绑定dep依赖收集器
observe(value);
Object.defineProperty(target, key, {
get() {
//取值的时候
return value;
},
set(newValue) {
if (newValue === value) return;
value = newValue;
},
});
}
重写数组的七个方法来对数组改变数组的七个方法进行劫持
大家可能会有疑问,为什么数组不能采用Object.defineProperty对数据进行劫持,因为Object.defineProperty只对对象的属性进行劫持,对数组的元素不能劫持。为什么重写数组方法会对数组数据进行劫持?并且还会更新页面呢?
我们看一下源码
function reDefineArray() {
let prototype = Array.prototype;
let newArrayProto = Object.create(prototype); //复制原型对象
let methods = ["push", "pop", "shift", "unshift", "sort", "splice"];
methods.forEach((item) => {
newArrayProto[item] = function (...args) {
const result = prototype[item].call(this, ...args); //内部调用原来的方法,函数的劫持,切片原理
let inserted;
let ob = this.__ob__; //获取observe实例对象,调用数组方法
switch (item) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
default:
break;
}
if (inserted) {
ob.observeArray(inserted);
}
ob.dep.notify(); //数组变化了,通知对应的watcher实现更新逻辑
return result;
};
});
return newArrayProto;
}
从上面的代码我们可以看到对改变数组的七个方法进行重写,并不是对其方法逻辑进行重写,本质还是调用数组原型上的方法,而是在调用这几个方法的添加一些操作,首先inserted表示有数组的七个操作方法,当使用这七个方法的时候就会调用observeArray方法遍历数组元素,然后对引用类型进行劫持,这样就是为什么如果数组元素是引用类型还会具有响应式,如:this.arr数组的第一个元素是引用类型时this.arr[0].name="change"或者this.arr[0]=123也会引起页面的更新。但是如果数组元素是基本数据类型时数组元素不具有响应式,也就是this.arr[0]=123数组元素改变不会更新页面。ob.dep会通知页面更新,这个属性会在下面的观察者模式中介绍,这里的主要作用就是当数组调用这七个方法时会更新页面。
proxy 代理函数
proxy 代理方法:代理_data/data 的数据,让 vm 可以直接访问即 this 来访问,如 this.a,并且只访问 data 对象的第一层属性
//对data上的数据依次进行代理
function proxy(vm, target, key) {
Object.defineProperty(vm, key, {
get() {
return vm[target][key];
},
set(newValue) {
vm[target][key] = newValue;
},
});
}
二 生成虚拟DOM
模板解析,即获取模板的内容(html 的字符串)生成虚拟 DOM,当数据变化后比较新旧的虚拟 DOM 的差异,进而更新差异的部分,将差异的部分转变为真实 DOM,即将 template 语法转变成 render 函数,在init 方法中初始化$mount 方法
1 初始化$mount 方法
initMount方法主要使用来挂载传入的DOM元素组成的字符串
function initMount(el) {
const vm = this;
el = document.querySelector(el);
const ops = vm.$options;
//先查看是否有render配置项,如果没有render函数,则使用template
if (!ops.render) {
let template;
//查看是否配置项中是否有template配置项,没有的话查找el选择器对应的DOM元素
if (!ops.template && el) {
template = el.outerHTML; //获取DOM字符串,可以打印出来看看,这是一个包含空格与换行的字符串
} else {
if (el) {
template = ops.template;
}
}
if (template) {
//编译模板方法,之后生成render函数
const render = complieToFunction(template);
ops.render = render;
}
}
//最终可以使用render函数
mountComponent(vm, el);
};
页面渲染函数,将会在下述的观察者watcher中执行
// 组件的挂载方法
function mountComponent(vm, el) {
//这里的el是querySelector处理的
vm.$el = el;
//根据生成的虚拟dom创建真实的dom
const updateComponent = () => {
vm._update(vm._render());
};
//初次渲染和后续更新都选择updateComponent方法
const watcher = new Watcher(vm, updateComponent, true);
}
2 生成 render 函数
生成 render 的过程可分为三步。1.将模板字符解析为 ast 抽象语法树,2.遍历 ast 抽象语法树生成 render 函数。
complieToFunction 函数的作用就是将获取的模板字符串(包含模板语法,如:<div style={background-color='red'}>{{a}}</div>等)转变成一个树形结构的对象,注意这里的对象不是虚拟DOM,它只是对vue模板语法的描述。如下:
{
tag:'div',
type: ELEMENT_TYPE,
children: [
{
text: "{{a}}",//文本的值
type: "text",//文本节点
parent:{...}
}
],
attrs:[
{
name:"background-color",
value:"red"
}
]
parent: null,
}
然后再返回对应的render函数。
// 解析模板函数complie
function complieToFunction(template) {
//将template转化为ast语法树,也就是虚拟DOM
let ast = parseHTML(template);
//将虚拟DOM挂载属性和解析模板语法后生成模板字符串,
// 模板解析的内容有,包括双花括号语法{{name}},v-bind指令等
let code = codeGen(ast);
code = `with(this){return ${code}}`; //with函数就是将vm实例传入进去,然后再进行解析的时候,将{{}}中的变量填入data中的值
//模板解析引擎的实现原理 with+new Function
let render = new Function(code); //将解析好的模板字符串进行函数化,也就是render函数
return render;
}
模板解析-compile
将包含 vue 语法的模板字符串解析生成 ast 抽象语法树,简单来说就是一个对象,其中包含对DOM元素的描述信息,因为还包含vue的模板语法所以还不是虚拟DOM,因为vue中的模板解析所用到的正则表达式和内容比较多,这里我只实现了模板解析只对标签的style属性,vue的模板语法{{}}进行解析。
点击查看代码
//解析DOM字符串模板,生成虚拟DOM
function parseHTML(html) {
//html标签的第一个字符为<
//将template转化成ast语法树
//通过正则表达式来匹配
const ELEMENT_TYPE = "element";
const TEXT_TYPE = "text";
const stack = []; //元素栈
let currentParent; //指向栈中的最后一个元素
let root; //根元素
const ncname = `[a-zA-Z][\\-\\.0-9_a-zA_Z]*`; //匹配标签名
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //带命名空间的标签
const startTagOpen = new RegExp(`^<${qnameCapture}`); //匹配一个标签名<xxxx开始标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); //匹配的是</xxx>最终匹配的结束标签
//匹配css属性的正则表达式
const attribute =
/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
const startTagClose = /^\s*(\/?)>/; //匹配自闭合标签如<br />
//vue3采用的不是正则,采用模板解析
//创建元素抽象语法树模型
function createASTElement(tag, attrs) {
return {
tag,
type: ELEMENT_TYPE,
children: [],
attrs,
parent: null,
};
}
//解析开始标签
function parseStartTag() {
const startMatch = html.match(startTagOpen);
if (startMatch) {
const match = {
tagName: startMatch[1], //标签名
attrs: [], //标签内部的键值对属性对象
};
//删除匹配的标签信息
advance(startMatch[0].length);
//如果不是开始标签就一直匹配下去
let attr;
let end;
while (
!(end = html.match(startTagClose)) &&
(attr = html.match(attribute))
) {
end = html.match(startTagClose);
advance(attr[0].length);
match.attrs.push({
name: attr[1],
value: attr[3] || attr[4] || attr[5] || true,
});
}
if (end) {
advance(end[0].length);
}
return match;
}
return false;
}
//边解析字符串,边去掉解析过的字符串,直至字符串解析完
function advance(n) {
html = html.substring(n);
}
//一个标签的开始
function start(tag, attrs) {
let node = createASTElement(tag, attrs);
if (!root) {
root = node;
}
if (currentParent) {
node.parent = currentParent;
currentParent.children.push(node);
}
stack.push(node);
currentParent = node;
}
//一个标签里面的文本内容
function handlerText(text) {
// text
text = text.replace(/\s/g, ""); //空格转为空字符
text &&
currentParent.children.push({
type: TEXT_TYPE,
text,
parent: currentParent,
});
}
//一个标签的结尾
function end(tag) {
let node = stack.pop();
// if(node!==tag)校验标签
currentParent = stack[stack.length - 1];
}
while (html) {
let textEnd = html.indexOf("<"); //如果indexOf中的索引是0,则说明是个标签
if (textEnd === 0) {
const startTagMatch = parseStartTag();
if (startTagMatch) {
start(startTagMatch.tagName, startTagMatch.attrs);
continue;
}
let endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
end(endTagMatch[1]);
continue;
}
}
if (textEnd > 0) {
let text = html.substring(0, textEnd);
if (text) {
handlerText(text);
advance(text.length);
}
}
}
return root;
}
生成 render 函数
其中 render 函数会包含_c,_s,_v(_c,_s,_v等函数是定义在在vue原型上的方法,用来转换语法) 等函数,这些函数在 vue 状态初始化的时候有介绍,就是将 ast 抽象语法树解析成虚拟 DOM,可以前去查看。主要作用就是生成虚拟 DOM 的时候会将元素节点和文本解析成对应的 DOM 节点,也会解析模板字符中变量,也就是将 data 中的变量填入 DOM 元素中,也就是 MVVM 结构中的 model->viewModel->view 的过程。 如下图

点击查看代码详情
//属性
function genChildren(children) {
return children.map((child) => gen(child)).join(",");
}
//模板语法解析
function codeGen(ast) {
let children = genChildren(ast.children);
let code = `_c('${ast.tag}',${
//_c函数是将html标签在render函数执行的时候转为虚拟DOM的标签
ast.attrs.length > 0 ? genProps(ast.attrs) : "null"
}${ast.children.length ? `,${children}` : ""}
)`;
return code;
}
function gen(node) {
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; //匹配模板中的{{}}的变量名称
if (node.type === "element") {
return codeGen(node);
} else {
//
let text = node.text;
if (!defaultTagRE.test(text)) {
return `_v(${JSON.stringify(text)})`; //_v函数是将标签的文本内容在render函数执行的时候转为虚拟DOM的标签文本内容
} else {
let tokens = [];
defaultTagRE.lastIndex = 0;
let lastIndex = 0;
while ((match = defaultTagRE.exec(text))) {
let index = match.index;
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)));
}
tokens.push(`_s(${match[1].trim()})`); //匹配模板中的{{}}的变量名称,放到_s函数下,并在render函数执行的过程中将变量转为真正的值
lastIndex = index + match[0].length;
if (lastIndex < text.length) {
tokens.push(text.slice(lastIndex));
}
return `_v(${tokens.join("+")})`;
}
}
}
}
//遍历标签属性,生成render函数
function genProps(attrs) {
let str = "";
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
if (attr.name === "style") {
let obj = {};
attr.value.split(";").forEach((item) => {
let [key, value] = item.split(":");
if (key && value) {
obj[key] = value;
}
});
attr.value = obj;
}
str += `${attr.name}:${JSON.stringify(attr.value)},`;
}
return `{${str.slice(0, -1)}}`;
}
3 生成虚拟 DOM
render 函数执行后就会产生虚拟 DOM,每次数据依赖变化就会触发render函数执行,执行的虚拟DOM会保存在内存中,当遇到更新操作的时候会采用diff算法进行新旧的虚拟DOM对比,更新与旧虚拟DOM不一样的地方。
//ast做的是语法层面的转化,他描述的vue模板语法本身
//我们虚拟dom是描述的是dom元素,可以增加自定义属性,比如vm即为模板的根节点
function vnode(vm, tag, key, data, children, text) {
return {
vm,
tag,
key,
data,
children,
text,
};
}
将html标签DOM描述转为虚拟DOM的方法
function createElementVNode(vm, tag, data, ...children) {
if (!data) {
data = {};
}
let key = data.key;
if (key) {
delete data.key;
}
return vnode(vm, tag, key, data, children);
}
将虚拟的文本节点描述转为虚拟DOM
function createTextVNode(vm, text) {
return vnode(vm, undefined, undefined, undefined, undefined, text);
}
渲染页面
patch方法
patch方法用来根据虚拟DOM生成真实DOM的方法
点击查看代码
//patch算法
function patch(oldVNode, vnode) {
const isRealElement = oldVNode.nodeType;
// 如果是初次渲染不用进行diff比较
if (isRealElement) {
let ele = oldVNode;
let parentEle = ele.parentNode;
let newEle = creatEle(vnode);
// 获取到id为app元素并且将虚拟DOM生成的真实DOM替换为app的兄弟节点,再删除app节点
document.querySelector("#app").parentNode.insertBefore(newEle, ele.nextSibling);
document.querySelector("#app").parentNode.removeChild(ele);
return newEle;
} else {
//diff算法
}
}
//创建真实dom
function creatEle(vnode) {
let { tag, data, children, text } = vnode;
if (typeof tag === "string") {
vnode.el = document.createElement(tag);
patchProps(vnode.el, data);
children.forEach((child) => {
vnode.el.appendChild(creatEle(child));
});
} else {
vnode.el = document.createTextNode(text);
}
return vnode.el;
}
//给真实DOM添加属性
function patchProps(el, props) {
for (let key in props) {
if (key === "style") {
for (let styleName in props.style) {
el.style[styleName] = props.style[styleName];
}
} else {
el.setAttribute(key, props[key]);
}
}
}
依赖收集
dep收集器
```js
let depId=0
class Dep { constructor() { this.id = depId++; //用于标识dep收集器 this.subs = []; //存放属性对应的Watcher } depend() { //这里不希望放重复的watcher,而且刚才只是一个单向的关系dep->watcher //watcher记录dep Dep.target.addDep(this); //让watcher记住dep } //收集watcher addSub(watcher) { this.subs.push(watcher); } //数据更新的时候触发set方法,从而触发Dep的notify方法,通知各个依赖的页面Watcher,然后更新视图 notify() { this.subs.forEach((watcher) => { watcher.update(); }); } } ```
页面Watcher
watcher的作用是当页面的数据是data中的数据时就让data中的数据的dep依赖收集器属性收集页面渲染的watcher
let watcherId = 0;
//不同的组件拥有不同的Watcher,所以用id来区分
class Watcher {
// isRender用于标识是否是一个渲染Watcher
constructor(vm, fn, options, callback) {
this.id = watcherId++;//标识watcherDOM
this.renderWatcher = options; //是一个渲染Watcher
// fn就是updateComponent更新页面DOM的操作,里面将render方法生成虚拟DOM生成真实DOM
//getter意味着调用这个函数可以发生取值操作
if (typeof fn === "string") {
this.getter = function () {
return vm[fn];
};
} else {
this.getter = fn;
}
this.callback = callback; //watch的回调函数
this.deps = []; //后续实现计算属性要用到
this.depsId = new Set();//Set数据结构会去重
this.lazy = options.lazy;//后续实现计算属性要用到
this.dirty = this.lazy;//后续实现计算属性要用到
this.value = this.lazy ? undefined : this.get();//后续实现计算属性要用到
this.user = options.user; //获取user
this.vm = vm;
}
//收集dep,当页面上的数据改变的时候,通知数据进行改变
addDep(dep) {
//一个组件有多个属性
let id = dep.id;
if (!this.depsId.has(id)) {
this.deps.push(dep);
this.depsId.add(id);
dep.addSub(this); //Watcher已经记住dep了,而且已经去重了,让dep来收集watcher
}
}
//1.当我们创建渲染watcher的时候我们会把watcher放到Dep.target上
//调用_render()会取值,走到get上
get() {
pushTarget(this); //静态属性只有一份
const value = this.getter.call(this.vm); //会去vm上取值
popTarget(this); //渲染完毕后清空
return value;
}
// 设计计算属性的用到该方法,将数据去脏
evaluate() {
this.value = this.get(); //获取用户函数的返回值,并且还要标识为脏
this.dirty = false;
}
depend() {
for (let i = 0; i < this.deps.length; i++) {
this.deps[i].depend(); //让计算属性watcher也收集渲染watcher
}
}
update() {
// this.get(); //重新更新渲染
//因为每次修改数据都会触发更新,所以我们直接让多个数据修改完之后再进行一次更新
if (this.lazy) {
//如果计算属性的依赖发生了变化,就标识计算属性是脏值
this.dirty = true;
} else {
queueWatcher(this); //把当前的Watcher暂存起来
}
}
run() {
let oldValue = this.value;
let newValue = this.get();
if (this.user) {
this.callback.call(this.vm, newValue, oldValue);
}
}
}
观察者模式
vue所实现的是每当有数据改变的时候依赖数据的页面也会随之更新,这个时候采用的是Object.defineProperty对数据进行劫持,并且每个数据劫持的时候,都会将依赖该数据的页面渲染的watcher进行收集起来放在Dep容器里面,可以在Observe类中看见dep收集器属性,也就是说当数据改变的时候会触发set方法,这时候就会遍历收集器Dep通知的Watcher。同时Watcher也会收集对应的Dep也就是data数据,当页面上的视图数据改变的时候可以通知dep收集器去更新对应的数据,也就是大名鼎鼎的双向绑定原理。
至此我们可以在html文件中添加vue的模板语法{{}},将数据渲染到页面上,并且数据变化的时候可以更新页面。
最后:本文主要对Vue的设计原理作了基本的介绍,本文页面展示的代码不全。详细代码可以去 我的vue源码 查看,虽然目前还没有完全实现vue,后续我会继续实现更新。