# 🌵 Vue
# 001: Vue 生命周期(钩子函数)
创建阶段: beforeCreate、created、beforeMount、mounted;
更新阶段: beforeUpdate、updated;
销毁阶段: beforeDestory、destroyed;
还有针对来缓存组件状态的 <keep-alive></keep-alive>
提供的 activated 和 deactivated。
其中最常用的是 beforeCreate 和 created、beforeMount 和 mounted,他们的主要区别是:
beforeCreate
:el 和 data 都未初始化,数据和方法都不能被访问。
created
:组件实例创建完成,属性已经绑定,data 已初始化,el 未初始化(说明可在 created 中首次拿到 data 中定义的数据)。
在这里可以拿到 data 数据和更改数据,但不会触发 updated 函数。可以做一些初始数据的获取,在当前阶段无法与 Dom 进行交互,如果非要想,可以通过 vm.$nextTick
来访问 Dom。
beforeMount
:完成 data 和 el 的初始化,template 模板已导入渲染函数编译,但未挂载仍无法获取 DOM。(vm.$mount(el)
会触发这个状态,把 HTML 结构渲染出来,但 vue 实例中的数据没有渲染到 DOM 中,只是利用虚拟 DOM 把坑占住)。
mounted
:el 会被新创建的vm.$el
替换,即有了 DOM 且完成了双向绑定 可访问 DOM 节点,并挂在是渲染 DOM,完成挂载(说明在 mounted 中 dom 树渲染结束,可访问 dom 结构)。
例子:
<!DOCTYPE html>
<html>
<head>
<title>vue-test-demo</title>
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/vue/2.1.3/vue.js"
></script>
</head>
<body>
<div id="app">
<p>{{message}}</p>
</div>
<script type="text/javascript">
var app = new Vue({
el: "#app",
data: {
message: "this is a demo for vue",
},
beforeCreate: function() {
console.group("beforeCreate 创建前状态 :");
console.log("el: ", this.$el);
console.log("data: ", this.$data);
console.log("message: ", this.message);
},
created: function() {
console.group("created 创建完成状态 :");
console.log("el: ", this.$el);
console.log("data: ", this.$data);
console.log("message: ", this.message);
},
beforeMount: function() {
console.group("beforeMount 挂载前状态 :");
console.log("el: ", this.$el);
console.log("data: ", this.$data);
console.log("message: ", this.message);
},
mounted: function() {
console.group("mounted 挂载完成状态 :");
console.log("el: ", this.$el);
console.log("data: ", this.$data);
console.log("message: ", this.message);
},
beforeUpdate: function() {
console.group("beforeUpdate 更新前状态 :");
console.log("el: ", this.$el, this.$el.innerHTML);
console.log("data: ", this.$data);
console.log("message: ", this.message);
},
updated: function() {
console.group("updated 更新完成状态 :");
console.log("el: ", this.$el, this.$el.innerHTML);
console.log("data: ", this.$data);
console.log("message: ", this.message);
},
beforeDestory: function() {
console.group("beforeDestory 摧毁前状态 :");
console.log("el: ", this.$el);
console.log("data: ", this.$data);
console.log("message: ", this.message);
},
destoryed: function() {
console.group("destoryed 摧毁后状态 :");
console.log("el: ", this.$el);
console.log("data: ", this.$data);
console.log("message: ", this.message);
},
});
</script>
</body>
</html>
输出结果如图:
其中也可以看出 updated 和 destoryed 前后状态的变化:
beforeUpdate
:可以监听到 data 的变化,但是 view 层的数据没有变化,因此 view 层没有被重新渲染。updated
:view 层被重新渲染,数据更新。beforeDestory
:实例销毁之前调用,实例仍可访问可用。destoryed
:Vue 实例销毁后调用,调用后实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
参考文章:
详解 vue 生命周期 (opens new window)
# beforeCreate 之前做了什么
new Vue()创建一个实例后,会调用this._init(options)
,并且在外部通过initMixin()、stateMiXin()、eventMixin()、lifeMixin()、renderMixin()
去做初始化事件、初始化生命周期的一些操作。
# 混入 mixin 的执行顺序,以及如果有冲突怎么处理的?
混入 mixin 时,mixin 的生命周期钩子函数执行会优先于组件。
当组件和混入对象含有同名选项时,选项将以恰当的方式“合并”:
钩子函数,created、mounted 等同名钩子函数会合并成一个数组,都将被调用。混入对象钩子函数优先执行(生命周期执行 mixin 先于组件)。
Data 数据,数据对象在内部进行递归合并,并在发生冲突时以组件数据优先;
对象选项:components、methods、directives 等对象,会被合并为同一对象。如果出现了相同键值对,则当前组件中的键具有优先级。
# keep-alive 缓存时的生命周期执行顺序
keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
页面第一次进入,钩子的触发顺序 created-> mounted-> activated,
退出时触发 deactivated,当再次进入(前进或者后退)时,只触发 activated。
# 002: 父子组件挂载顺序是怎么样的?
加载渲染过程:
父 beforeCreate -> 父 created -> 父 beforedMount -> 子 beforeCreate -> 子 created -> 子 beforedMount -> 子 mounted -> 父 mounted
另外关于 updated 和 destroyed 的顺序(从外到内,再从内到外):
- 子更新: 父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
- 父更新: 父 beforeUpdate -> 父 updated
- 销毁: 父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
# 003: Vue 的模版编译过程是怎么样的
普通的 template 或者 el 都是字符串,需要解析成 AST,再将 AST 转化为 render 函数。
Vue 的编译过程就是将 template/el 转化为 render 函数的过程。主要会经历以下阶段:
(1)解析模版,生成 AST 树(一种用 JavaScript 对象的形式来描述整个模板)
会使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
(2)优化
优化过程就是深度遍历 AST 树,按照相关条件对树节点进行标记。这些被标记的节点被称为(staticRenderFns) 静态节点,可以不需响应式的结点)。
这是 Vue 在编译过程中的一处优化,后面当 update 更新界面时,会有一个 patch 的过程, diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能。
(3)编译
将 AST 转化成 render function 字符串的过程,得到结果是 render 的字符串以及 staticRenderFns 字符串。
# 004: 模板编译原理
- 解析 html 并生成 AST 语法树;
// src/compiler/parse.js
// 以下为源码的正则 对正则表达式不清楚的同学可以参考小编之前写的文章(前端进阶高薪必看 - 正则篇);
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; //匹配标签名 形如 abc-123
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //匹配特殊标签 形如 abc:234 前面的abc:可有可无
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配标签开始 形如 <abc-123 捕获里面的标签名
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束 >
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾 如 </abc-123> 捕获里面的标签名
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性 形如 id="app"
// match用法:
// let str="antzone"; console.log(str.match("n")); 返回["n", index: 1, input: "antzone", groups: undefined]
// (2)数组第一个元素是match方法首次匹配到的子字符串,"antzone"虽然有多个"n",但是返回的数组只存储首次匹配到的"n",如果match方法的参数是全局匹配的正则,将会存储所有的匹配到的子字符串。
// (3)index属性值返回首次匹配到子字符串的位置。
// (4)input属性值是原字符串"antzone"。
// https://www.cnblogs.com/ranyonsue/p/10757567.html
let root, currentParent; //代表根节点 和当前父节点
// 栈结构 来表示开始和结束标签
let stack = [];
// 标识元素和文本type
const ELEMENT_TYPE = 1;
const TEXT_TYPE = 3;
// 生成ast方法
function createASTElement(tagName, attrs) {
return {
tag: tagName,
type: ELEMENT_TYPE,
children: [],
attrs,
parent: null,
};
}
// 对开始标签进行处理
function handleStartTag({ tagName, attrs }) {
let element = createASTElement(tagName, attrs);
console.log(4, "将匹配上的match生成ast", element);
if (!root) {
root = element;
}
currentParent = element;
stack.push(element);
console.log(5, "ast推入栈", [...stack]);
}
// 对结束标签进行处理
function handleEndTag(tagName) {
// 栈结构 []
// 比如 <div><span></span></div> 当遇到第一个结束标签</span>时 会匹配到栈顶<span>元素对应的ast 并取出来
let element = stack.pop();
// 当前父元素就是栈顶的上一个元素 在这里就类似div
currentParent = stack[stack.length - 1];
// 建立parent和children关系
if (currentParent) {
element.parent = currentParent;
currentParent.children.push(element);
}
}
// 对文本进行处理
function handleChars(text) {
// 去掉空格
text = text.replace(/\s/g, "");
if (text) {
currentParent.children.push({
type: TEXT_TYPE,
text,
});
}
}
// 解析标签生成ast核心
function parse(html) {
while (html) {
// 查找<
let textEnd = html.indexOf("<");
// 如果<在第一个 那么证明接下来就是一个标签 不管是开始还是结束标签
if (textEnd === 0) {
// 如果开始标签解析有结果
const startTagMatch = parseStartTag();
if (startTagMatch) {
// 把解析好的标签名和属性解析生成ast
handleStartTag(startTagMatch);
continue;
}
// 匹配结束标签</
const endTagMatch = html.match(endTag);
console.log(7, "匹配endMAtch", endTagMatch);
if (endTagMatch) {
advance(endTagMatch[0].length);
handleEndTag(endTagMatch[1]);
continue;
}
}
let text;
// 形如 3333</span></div>
if (textEnd >= 0) {
// 获取文本
text = html.substring(0, textEnd);
}
if (text) {
advance(text.length);
handleChars(text);
}
}
// 匹配开始标签
function parseStartTag() {
const start = html.match(startTagOpen);
console.log(0, html.match(startTagOpen));
// start: ["<div", "div", index: 0, input: "<div id="div1"><span>3333</span></div>", groups: undefined]
if (start) {
const match = {
tagName: start[1],
attrs: [],
};
//匹配到了开始标签 就截取掉,方便后续剩下字符的其他操作
advance(start[0].length);
// 开始匹配属性
// end代表结束符号> 如果不是匹配到了结束标签
// attr 表示匹配的属性
// 不断匹配,直到attr匹配完或者end被匹配上了
let end, attr;
while (
!(end = html.match(startTagClose)) &&
(attr = html.match(attribute))
) {
console.log(1, end, attr);
// null, [" id="div1"", "id", "=", "div1", undefined, undefined, index: 0, input: " id="div1"><span>3333</span></div>", groups: undefined]
advance(attr[0].length);
attr = {
name: attr[1],
value: attr[3] || attr[4] || attr[5], //这里是因为正则捕获支持双引号 单引号 和无引号的属性值
};
match.attrs.push(attr);
console.log(2, match);
}
if (end) {
console.log(
3,
end,
"如果匹配到了startTagClose返回match",
match
);
// 代表一个标签匹配到结束的>了 代表开始标签解析完毕
advance(1);
return match;
}
}
}
//截取html字符串 每次匹配到了就往前继续匹配
function advance(n) {
html = html.substring(n);
}
// 返回生成的ast
return root;
}
// 测试例子
const template =
'<div id="div1" class="test"><span>3333</span><input type="text"/></div>';
console.log(parse(template));
- 根据 AST 生成代码;
// src/compiler/codegen.js
const defaultTagRE = /\{\{((?:.|\\n)+?)\}\}/g; //匹配花括号 {{ }} 捕获花括号里面的内容
function gen(node) {
// 判断节点类型
// 主要包含处理文本核心
// 源码这块包含了复杂的处理 比如 v-once v-for v-if 自定义指令 slot等等 咱们这里只考虑普通文本和变量表达式{{}}的处理
// 如果是元素类型
if (node.type == 1) {
// 递归创建
return generate(node);
} else {
// 如果是文本节点
let text = node.text;
// 不存在花括号变量表达式
console.log(
"1 判断有没有匹配上表达式{{}}",
text,
defaultTagRE.test(text)
);
const hasExp = !defaultTagRE.test(text);
if (!hasExp) {
console.log(
"1-1 没匹配上表达式{{}} 通过_v",
text,
JSON.stringify(text)
);
return `_v(${JSON.stringify(text)})`;
}
// 正则是全局模式 每次需要重置正则的lastIndex属性 不然会引发匹配bug
let lastIndex = (defaultTagRE.lastIndex = 0);
let tokens = [];
let match, index;
// exec的用法:
// var str = "Visit W3School"; var patt = new RegExp("W3School","g"); var result = patt.exec(str))
// console.log(result, patt.lastIndex) => W3School 14
while ((match = defaultTagRE.exec(text))) {
// match : ["{{myData}}", "myData", index: 4, input: "3333{{myData}}", groups: undefined]
console.log("1-2 匹配上表达式{{}}", text, match);
// index代表匹配到的位置
index = match.index;
if (index > lastIndex) {
// 匹配到的{{位置 在tokens里面放入普通文本
// 把 '{{' 之前的文本推入
tokens.push(JSON.stringify(text.slice(lastIndex, index)));
console.log(
"2 把{{之前的文本推入",
JSON.stringify(text.slice(lastIndex, index)),
[...tokens]
);
}
// 放入捕获到的变量内容
tokens.push(`_s(${match[1].trim()})`);
// 匹配指针后移
lastIndex = index + match[0].length;
}
// 如果匹配完了花括号 text里面还有剩余的普通文本 那么继续push
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)));
}
// _v表示创建文本
return `_v(${tokens.join("+")})`;
}
}
// 处理attrs属性
function genProps(attrs) {
let str = "";
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
// 对attrs属性里面的style做特殊处理
if (attr.name === "style") {
let obj = {};
attr.value.split(";").forEach((item) => {
let [key, value] = item.split(":");
obj[key] = value;
});
attr.value = obj;
}
str += `${attr.name}:${JSON.stringify(attr.value)},`;
}
return `{${str.slice(0, -1)}}`;
}
// 生成子节点 调用gen函数进行递归创建
function getChildren(el) {
const children = el.children;
console.log("0 解析每个children", children);
if (children) {
return `${children.map((c) => gen(c)).join(",")}`;
}
}
// 递归创建生成code
function generate(el) {
let children = getChildren(el);
// _c( el.tag, {...el.attrs}, -v(文本内容), ...children)
// _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本
let code = `_c('${el.tag}',${
el.attrs.length ? `${genProps(el.attrs)}` : "undefined"
}${children ? `,${children}` : ""})`;
return code;
}
// ast生成code: _c('div',{id:"div1",class:"test"}, _v("13"), _c('span',undefined,_v("3333{{myData}}")), _c('input',{type:"text"},_v(">")))
console.log("ast生成code:", generate(astNode));
- 根据 code 字符串转成 render 函数
export function compileToFunctions(template) {
let code = generate(ast);
// 使用with语法改变作用域为this 之后调用render函数可以使用call改变this 方便code里面的变量取值 比如 name值就变成了this.name
let renderFn = new Function(`with(this){return ${code}}`);
return renderFn;
}
https://juejin.cn/post/6936024530016010276
# 005: diff 算法
# 006:虚拟 DOM 实现原理,有什么优缺点?
虚拟 DOM 本质是通过 JS 对象抽离出模拟真实 DOM 的一个树形结构,包含了 tag、props、children 三个属性。(对应源码中的 VNode 类,它定义在 src/core/vdom/vnode.js 中)
虚拟 DOM 在 JS 操控 DOM 的过程中进行了一个缓存。
使用真实 DOM 对更新的数据重绘整个视图层是相当消耗性能,通过虚拟 DOM 和 diff 算法得出一些需要修改的最小单位,并对小单位的位图作出对应的更新,这样子可以减少不必要的 DOM 操作,以此来提高性能。
虚拟 DOM 的优点:1)对粗暴的 DOM 操作进行一个缓存,性能要好;2)无需操作 dom,仅需处理好 ViewModel 代码逻辑,框架会根据虚拟 DOM 和数据双向绑定,更新视图; 3)可跨平台,用于服务端渲染和 weex
虚拟 DOM 的缺点:无法进行极致优化
# 007: key 属性的作用
key 的作用是尽可能的复用 DOM 元素。
diff 比对时,一个重要的对比方法 isSameNode,主要根据 vnode 的 tag、key 两个属性判断两个节点是否相同 (isSameNode 函数),适当的 key 配置能够有效提高 DOM 复用率,降低 DOM 节点重建频率来提高运行性能。
同时在 updateChildren 的阶段,key 也就是 children 中节点的唯一标识。通过 key 建立新旧结点的映射关系,以便能够在旧孩子结点中找到可以复用的结点,降低重建开销。
# 008: computed、methods、 watch 区别?
# methods
不具备缓存能力,也非响应式,只能是作为事件处理函数,被动调用。
# computed
1)computed 可以当作一个属性来用;
2)具有缓存能力,默认不执行除非被访问;
3)响应式的,当它依赖的属性变化时,它会主动执行,进行响应式更新。
计算属性具有缓存原因
computed 会拥有自己的 watcher。初始化时传入 lazy 属性表明是 computed 对应的订阅者,lazy 首次不执行,然后也会对应生成一个 dirty 属性,初始值为 true。
它内部有个属性 dirty 开关来决定 computed 的值是需要重新计算还是直接复用之前的值。
dirty 打开时,会调用 watcher.evaluate(重新去求值,后关闭 dirty),否则返回旧值。
computed: { sum() { return this.count + 1 } }
在 sum 第一次进行求值的时候会读取响应式属性 count,收集到这个响应式数据作为依赖。并且计算出一个值来保存在自身的 value 上,把 dirty 设为 false,接下来在模板里再访问 sum 就直接返回这个求好的值 value,并不进行重新的求值。
而 count 发生变化了以后会通知 sum 所对应的 watcher 把自身的 dirty 属性设置成 true,这也就相当于把重新求值的开关打开来了。这个很好理解,只有 count 变化了, sum 才需要重新去求值。
那么下次模板中再访问到 this.sum 的时候,才会真正的去重新调用 sum 函数求值,并且再次把 dirty 设置为 false,等待下次的开启……
计算属性如何收集依赖
当 computed 作为一个属性被使用时会为他收集依赖,其实它依赖于 data 属性变化而变化,因此帮 computed 收集依赖相对于帮所依赖的 data 属性收集依赖,所以这个 watcher 对象里面会维护一个 dep 存储给哪些发布者所订阅了,然后为依赖属性 dep 中放入自己这个 watcher。computed 在组件内普适性更强,因此适用于复杂的数据转换、统计等场景;一个数据受多个数据影响时使用。
# watch
1)初始化时默认会执行一次
2)响应式的,属性一变化就会触发对应的回调函数
3)一般一个数据影响多个数据时使用
4)当需深度监听时,可设置deep:true
,这样便会对对象每一项属性进行监听。但是这样会带来性能问题,优化的话可以使用字符串形式监听。
// 优化前
watch: {
obj: {
handler(val){
console.log('obj has changed to ', val);
},
immediate: true,
deep: true
}
}
// 优化后
watch: {
'obj.a': {
handler(val){
console.log('obj.a has changed to ', val);
},
immediate: true,
}
}
computed 和 watch 的应用场景:
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,可利用 computed 的缓存特性,避免每次获取值时都重新计算;
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
https://blog.csdn.net/weixin_46837985/article/details/106632342
https://www.imooc.com/article/14466
https://segmentfault.com/a/1190000021761594
# 009: filters 与 computed 的区别与用法
# filters
1)不具备缓存和响应功能,显式调用时触发,一般应用在模板渲染上。
2)优点是可链式调用,容易在组件外抽象;
缺点是每次模板渲染时都需要重新执行计算
3)filter 有两种定义方式:
一是在组件内部通过 filters 属性定义;
一是在组件外部通过 Vue.filter 函数定义:
# 010: v-show 与 v-if 区别?
v-if
是真正的条件渲染,根据判断条件来动态的进行增删 DOM 元素。如果初始渲染时条件为假则什么都不做,直至条件为真时才开始渲染。v-show
不管初始条件是什么,元素总会被渲染,通过display: none
隐藏起来。
v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
# 011: Vue.js 双向绑定原理
Vue 双向绑定是指数据(Model)和视图(View)同步更新的一个过程,即数据从 DOM 到 JS 和从 JS 到 DOM 监测并同步更新的过程。
从 DOM 到 JS 的过程通常用 addEventListener 去监测 DOM 的事件行为进而触发 JS 的数据变化。
那如何检测 JS 数据对象属性发生变化 进而同步更新视图呢?
这个主要采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()
来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,然后触发相应的监听回调。
- 第一步,在组件初始化,通过 Observer(数据监听器),遍历 data 里面的每个属性对象,为他们绑定一个专用的 Dep 对象(发布者),并且通过
Object.defineProperty
重写数据的 getter 和 setter,劫持监听属性。
getter 会收集依赖,Setter 会更新依赖,在属性更新后触发 dep.notify 去通知所有依赖。
第二步,会对 props、computed、watch 创建 watcher:
1)调用 initComputed ,将 computed 属性转化为 watcher 实例;
2)调用 initWatcher,将 watcher 配置属性转化为 watcher 实例;
3)调用 mountComponent,将 render 函数绑定 watcher 实例;其中,Watcher 要做的事情:
1)强制执行 getter,把自己添加进属性订阅池里;
2)编写 update 方法,在 dep.notify 里触发 watcher.update 方法后(通过 compile 绑定的更新回调)更新视图。第三步,通过 Compile 遍历节点:
1)替换模版变量,初始化页面;
2)对节点绑定函数事件(v-bind on-click..遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应指令更新函数(vm.$options.methods[exp])进行绑定)
# 怎么实现对数组的监听?
以下情况 vue 会对数组无法监听:
(1)利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
(2)修改数组的长度时,例如:vm.items.length = newLength
(3)push、pop 一些特殊的方法也不能触发 setter
vue 主要通过修改重新原型上的 push,pop,shift,unshift,splice, sort,reverse 七个方法,去监听造成数组变化的方法,触发这个方法的同时去调用挂载好的响应页面方法,达到页面响应式的效果。
// 监听函数
// 记录原始Array未重写之前的API原型方法
const arrayProto = Array.prototype;
// 拷贝一份上面的原型出来
const arrayMethods = Object.create(arrayProto);
// 将要重写的方法
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function(method) {
def(arrayMethods, method, function mutator(...args) {
// 原有的数组方法调用执行
const result = arrayProto[method].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);
// 触发订阅,像页面更新响应就在这里触发
ob.dep.notify();
return result;
});
});
// 改变数组里的元素不能监听到,但是数组内的值是对象类型的,修改它依旧能得到监听响应
// 如改变list[0].val可以得到监听,但是改变list[0]不能,但是依旧没有对数组本身的变化进行监听。
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
// 监听数组元素
observe(items[i])
}
}
# 双向绑定的实现
👉 更详细的戳我 (opens new window)
# 012: keep-alive 组件有什么作用?
<keep-alive>
是 vue 内置的一个组件,一般结合路由和动态组件一起使用,主要作用是缓存组件,使被包含的组件保留状态,避免重新渲染加载。
它的 prop 属性值分别有 include 和 exclude ,分别表示只有匹配名称的组件需要缓存和不会被缓存。优先级 exclude 较高。
对应两个钩子函数:当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated,下一次进入时只会触发 activated。
keep-alive 的中还运用了 LRU(Least Recently Used)算法。
# 013: Vue 组件间通信有哪几种方式?
https://www.cnblogs.com/mengfangui/p/9995470.html
https://juejin.im/post/5d59f2a451882549be53b170#heading-16
prop / $emit
:适合父子间通信ref / $parents / $children
: 适合父子间通信
ref = 'xxx'
被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs
对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。
$parent / $children
分别指向当前实例的直接父组件(如果没有父实例则指向自己)和子组件。
eventBus ($emit / $on)
:适合任何组件间通信
公共事件总线eventBus
的本质是创建一个空的 vue 实例作为桥梁(事件中心),实现任何组件间的通信。
// 注册一个全局的事件总线
Vue.prototype.$eventBus = new Vue();
// 兄弟组件A(A发送aClick事件)
this.$eventBus.$emit("aClick", { msg: "i am a msg from brotherA" });
// 兄弟组件B(B监听aClick事件)
this.$eventBus.$on("aClick", (msg) => {
// 兄弟A发来的事件
this.msgFromA = msg;
});
$attrs / $listeners
适合多级嵌套组件间通信(vue2.4 新增属性)
$attrs / $listeners
可作为解决多级组件嵌套,需要组件通过prop
和$emit
中转情况的优化方案。
$attrs
继承所有的父组件属性(除了通过 prop 传递的属性、class 和 style),通常配合 inheritAttrs 选项一起使用。inheritAttrs:true
时,子组件可通过$attrs
获取到除 props 之外的所有属性;inheritAttrs:false
只继承 class 属性。
父组件 A 把很多数据传给子组件 B,子组件 B 利用$attrs
收集起来,然后可以利用 v-bind="$attrs"
传给 B 的子组件 C(也就是 A 组件的孙组件),这样就不用一个一个属性去传了。
$listeners
与 $attrs
类似,$listeners
传递的是事件,可接收除了带 .native
事件修饰符的所有事件监听器,通过 v-on="$listeners"
将所有事件监听器传入当前组件的内部组件。内部组件可通过 $emit
触发事件。
// Father.vue
<template>
<div class="father">
<Child :name="name" :age="age" :weight="weight" @updateInfo="updateInfo" @click.native='clickHandler'></Child>
</div>
</template>
<script>
import Child from './Child.vue';
export default {
name: 'father',
components: { Child },
data() {
return {
name: 'father',
age: 30,
weight: 50
}
},
methods: {
updateInfo() {
console.log('father A: update info');
},
clickHandler() {
console.log('father A: click handler');
}
}
}
</script>
// Son.vue
<template>
<div class="son">
// son 组件将继承父作用域的属性传至其自身的子组件(即 GrandSon )
// son 组件将接收到父作用域的事件监听器传至自身的子组件(即 GrandSon )
<GrandSon v-bind='$attrs' v-on='$listerners'></GrandSon>
</div>
</template>
<script>
import GrandSon from './GrandSon.vue';
export default {
name: 'son',
components: {GrandSon},
props: ['name'],
inheritAttrs: true,
created() {
//父组件共传来三个值,name、age、weight,由于 name 被 props 接收了,故 $attrs 只包括 age 、weight 属性
//{age: 30, weight: 50}
console.log(this.$attrs);
//父组件监听了两个事件,updateInfo和click,由于click被native修饰了,故$listerners 只有 updateInfo 事件
// {updateInfo: fn}
console.log(this.$listener);
}
}
</script>
// GrandSon.vue
<template>
<div class="grandson">
{{ $attrs }} --- {{ $listeners }}
</div>
</template>
<script>
import GrandSon from './GrandSon.vue';
export default {
name: 'son',
components: {GrandSon},
props: ['name'],
inheritAttrs: true,
mounted() {
//{age: 30, weight: 50}
console.log(this.$attrs);
// {updateInfo: fn}
console.log(this.$listener);
// 可触发 father 组件中的updateInfo函数
// father A: update info
this.$emit('updateInfo');
}
}
</script>
provide / inject
适用于隔代组件通信(vue2.2 新增属性)
在祖先组件中可通过 provide
指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject
来注入 provide
提供的数据或方法。
provide / inject
API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
Vuex
适用于所有组件间通信
Vuex 是一个 Vue.js 的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。
store
基本上就是一个容器,它包含着你的应用中大部分的状态 state
。
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
# 014: nextTick 原理分析
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
简单来说:
DOM 至少会在当前 tick 里面的代码全部执行完毕后再更新。所以不可能做到在修改数据后 DOM 马上更新,要保证在 DOM 更新以后再执行某一块代码,就必须把这块代码放到下一次事件循环里面,比如 setTimeout(fn, 0),这样 DOM 更新后,就会立即执行这块代码。
应用场景:
在 Vue 生命周期的 created()钩子函数进行的 DOM 操作一定要放在 Vue.nextTick()的回调函数中。
原因:是 created()钩子函数执行时 DOM 其实并未进行渲染。在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的 DOM 结构的时候,这个操作应该放在 Vue.nextTick()的回调函数中。
原因:Vue 异步执行 DOM 更新,只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变,如果同一个 watcher 被多次触发,只会被推入到队列中一次。
// 点击按钮显示原本以 v-show = false 隐藏起来的输入框,并获取焦点。
showsou(){
this.showit = true //修改 v-show
document.getElementById("keywords").focus() //在第一个 tick 里,获取不到输入框,自然也获取不到焦点
}
// 需要修改成
showsou(){
this.showit = true
this.$nextTick(function () {
// DOM 更新了
document.getElementById("keywords").focus()
})
}
<div id="app">
<p ref="myWidth" v-if="showMe">{{ message }}</p>
<button @click="getMyWidth">获取p元素宽度</button>
</div>
<script>
getMyWidth() {
this.showMe = true;
//this.message = this.$refs.myWidth.offsetWidth;
//报错 TypeError: this.$refs.myWidth is undefined
this.$nextTick(()=>{
//dom元素更新后执行,此时能拿到p元素的属性
this.message = this.$refs.myWidth.offsetWidth;
})
}
</script>
nextTick 主要使用了宏任务和微任务。根据执行环境分别尝试采用:Promise、MutationObserver、setImmediate,如果以上都不行则采用 setTimeout。即延迟调用优先级如下:Promise > MutationObserver > setImmediate > setTimeout
。
定义了一个异步方法,多次调用 nextTick 会将方法存入队列中,通过这个异步方法清空当前队列。
this.a = 'test
当修改了一个属性以后,会触发以下步骤:
进入 a 的 setter,通知所有订阅 a 属性的 watcher;
watcher 收到通知以后,会将自己放入更新数组 (queueWatcher (opens new window))
接着执行 nextTick(flushSchedulerQueue)
,flushSchedulerQueue 函数主要作用是执行更新数组。
接着进入next-tick.js 对外暴露的 nextTick (opens new window):
把传入的回调函数 cb (flushSchedulerQueue)压入 callbacks 数组;
执行 timerFunc 函数(决定何时调用),延迟调用 flushCallbacks 函数(优先级:
Promise > MutationObserver > setImmediate > setTimeout
)执行 flushCallbacks 函数,即执行 callbacks 数组里面的所有 cb 回调
这里的 callbacks 没有直接在 nextTick 中执行回调函数的原因是保证在同一个 tick 内多次执行 nextTick,不会开启多个异步任务,而是把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。
每次调用 Vue.nextTick 时会执行其中 (3-6) 步骤。
Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
如果 Vue 使用 setTimeout 等宏任务函数,那么势必要等待 UI 渲染完成后的下一个宏任务执行。如果 Vue 使用微任务函数,无需等待 UI 渲染完成才进行 nextTick 的回调函数操作,可以想象在 JS 引擎线程和 GUI 渲染线程之间来回切换,以及等待 GUI 渲染线程的过程中,浏览器势必要消耗性能,这是一个严谨的框架完全需要考虑的事情。
更详细可以参考,子奕大大的文章《你真的理解$nextTick 么》 (opens new window)
# 015: .native 修饰符的作用
对组件绑定 DOM 事件时,可通过.native
修饰。在父实例解析完毕开始挂载时,遇到子元素是组件,然后去解析组件内部并生成 DOM 之后才 addEventListener 绑定。绑定的原生事件,会被存放在组件的外壳节点上 vm.$vnode
。
.native
主要用于监听组件根元素的原生事件,给自定义的组件添加原生事件。
值得注意的是:加了 native 相当于把自定义组件看成了 html 元素可以直接监听原生 click 事件;否则在自定义组件上 绑定的事件 默认是 自定义事件,而如果在自定义组件上没有定义这个事 件,不加 native 不会执行。
例子:
// Button.vue
<template>
<button type="button" v-on:click="clickHandler">
<slot></slot>
</button>
</template>
<script>
export default {
methods: {
clickHandler() {
this.$emit("vclick"); // 触发 vclick 事件
}
}
};
</script>
原生 html 标签绑定事件,通过 addEventListener 绑定 DOM 事件,事件会被保存在这个节点解析成的 vnode 中,就是 vm._vnode
。
// 引用 Button.vue 的组件作为<VButton>
<template>
<div class="a">
// 如果不添加native修饰click,会被认为没有注册自定义元素的自定义事件click对应的处理函数。但会执行vClickHandler
<vButton v-on:click="clickHandler" v-on:vclick="vClickHandler"> 按钮1 </vButton>
// clickHandler 和 vclickHanddler 都会执行
<vButton v-on:click.native="clickHandler" v-on:vclick="vClickHandler">按钮2</vButton>
</div>
</template>
<script>
import vButton from "./Button.vue";
export default {
methods: {
vClickHandler() {
// 自定义事件
cconsole.log("in v-click");
},
clickHandler() {
// dom事件
cconsole.log("in on-click");
}
}
};
</script>
# 016: 父组件可以监听到子组件的生命周期吗?
可以,可通过$emit
实现,但这个方式需要通过手动触发,也可以通过 hook 解决。
// 在父组件中 @hook:钩子
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
# 016: Vue 的单向数据流的理解
父组件可以通过设置子组件的属性(Props)来向子组件传递数据,而父组件想获得子组件的数据,需要向子组件注册事件,在子组件通过$emit
触发这个事件把数据传递出来。即 Props 向下传递数据,事件向上传递数据。
这样子做的意义是为了避免子组件不经意改变了父组件传来的数据,导致不确定数据的流向处理。
# 017: Vue 事件绑定原理说一下
原生事件绑定是通过 addEventListener 绑定给真实元素的,组件事件绑定是通过 Vue 自定义的 $on
实现的。如果要在组件上使用原生事件,需要添加.native
去修饰,这样子相当于在伏组件中把子组件当做是普通 html,然后加上原生事件。
# 018: MVVM 和 MVC 理解
# MVC
- Model(模型):是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据
- View(视图):是应用程序中处理数据显示的部分。通常视图是依据模型数据创建的
- Controller(控制器):是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。
Model 层与 View 层耦合,交互形成一个回路:
流程:用户操作 -> View(负责接收用户的输入操作) -> Controller(业务逻辑处理) -> Model(数据处理)-> View(将结果反馈给 View)。
# MVVM
View 层:视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。
Model 层:数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。
ViewModel 层:做了两件事达到了数据的双向绑定:
一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。
二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。
MVVM 框架实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应 Vue 数据驱动的思想)
这样 View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要,是前后端分离方案实施的重要一环。
用 Vue 的视角来解释 MVVM 的话,View 视图对应的是 Vue 的模板和样式表;而 ViewModel 则是该组件的实例,每个组件的作用域都是独立的,互不影响;而 Vue 的 Model 层,在没有引入第三方状态管理的情况下,就是指组件中 data 属性中的内容。如果系统中引入了全局的 Model 层,比如 vuex,那 Model 层也包含一个和 Vue 组件脱离的对象。
# 019:vue-router 路由模式有几种
vue-router 路由模式主要有两种:hash 和 history。
# 常用的 hash 和 history 路由模式实现原理
# 020: vue3.0 特性
- 监测机制的改变
基于 Proxy 实现数据劫持,消除了 Vue 2 基于 Object.defineProperty 的实现所存在的很多限制:
- 监测对象;检测属性的添加和删除;
- 检测数组索引和长度的变更;支持 Map、Set、WeakMap 和 WeakSet。
- Composition API
Vue 3 推出了 Composition API ,Composition API 是以函数的形式封装公共逻辑,它通过显式的返回一个对象,让开发人员能在组件中直接了解到被引入的字段,从而避免 Mixin 命名冲突。
- 虚拟 DOM 优化和 diff 算法优化
# 021: Vue 的优化
# 代码层面的优化
减少 data 数据,或者对不该动的属性采用
Object.freeze
,否则数据都会增加 getter 和 setter,会收集对应的 watcherv-if 和 v-show 区分使用场景
v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
computed 和 watch 区分使用场景
SPA 页面善用 keep-alive 缓存组件
key 保证唯一
图片资源懒加载
第三方插件的按需引入
防抖、节流
created 开启异步任务处理,提前请求加载数据
# Webpack 层面的优化
# 022: Vue.set 的实现原理
受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除,也无法检测数组长度或者通过索引改变数组元素。
由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的
export function set(target: Array<any> | Object, key: any, val: any): any {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
target.length = Math.max(target.length, key);
// 利用数组的splice变异方法触发响应式(数组方法已被重新,因此会自动触发)
target.splice(key, 1, val);
return val;
}
// key 已经存在,直接修改属性值
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
// 因为响应式数据 我们给对象和数组本身都增加了__ob__属性,代表的是 Observer 实例。
const ob = (target: any).__ob__;
// target 本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val;
return val;
}
// 对属性进行响应式处理
// 将属性定义成响应式的
defineReactive(ob.value, key, val);
ob.dep.notify();
return val;
}
由代码可知,Vue.set 的实现原理:
(1)如果目标是数组,直接使用数组的 splice 方法触发相应式;
(2)如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
# 023:Vue 数据渲染过程
$mount
是渲染的起点。在渲染过程中,提供了三种渲染模式,自定义 Render 函数、template、el 均可以渲染页面。
除了 Render 函数模式,其他两种都需要解析成 AST,再将 AST 转化为 Render 函数。
Render 函数的作用是什么呢?在/src/core/instance/lifecycle.js 中有这么一段代码:
vm._watcher = new Watcher(
vm,
() => {
vm._update(vm._render(), hydrating);
},
noop
);
通过 Watcher 的绑定,每当数据发生变化时,执行_update
的方法,此时会先执行vm._render()
。在这个 vm._render()
中,我们的 Render 函数会执行,而得到 VNode 对象。
Render 函数执行生成了 VNode,而 VNode 只是 Virtual DOM。
过 DOM Diff 之后,来生成真正的 DOM 节点。在/src/core/vdom/patch.js 中的 patch(oldVnode, vnode ,hydrating)方法来完成的。
patch 其中的主要逻辑为:
1)当 VNode 为真实元素或旧的 VNode 和新的 VNode 完全相同时,直接调用 createElm 方法生成真实的 DOM 树。
2)当 VNode 新旧存在差异时,则调用 patchVnode 方法,比较新旧 VNode 节点,根据不同的状态对 DOM 做合理的添加、删除、修改 DOM,最后调用 createElm 生成真实的 DOM 树。(更进一步了解可看 diff 算法过程)
因此过程大概总结如下:
- new Vue,执行初始化, 调用
Vue.prototype._init(option)、initMixin、stateMixin、eventsMixin、lifecycleMixin、renderMixin
- 挂载
$mount
方法,通过自定义 Render 方法、template、el 等生成 Render 函数 - 通过 Observe 监听数据的变化,然后通过
dep.notify
通知对应的 watchers,触发vm._update(vm._render(), hydrating);
- Render 函数执行生成 VNode 对象
- 通过 patch 方法,对比新旧 VNode 对象,通过 DOM Diff 算法,添加、修改、删除真正的 DOM 元素
# 024:组件更新的过程
# 025:为什么 v-for 和 v-if 不能能同时使用
把 v-if 和 v-for 同时用在同一个元素上。当 Vue 解析指令时,v-for 比 v-if 具有更高的优先级,所以 vue 会先循环所有数据在进行判断,影响性能。
# 026:为什么组件的 data 是一个函数
# 027:什么是作用域插槽
# 028:简述 vuex 工作原理
# 029:action 和 mutation 区别
# 030:vue-router 导航守卫类型
- 全局前置守卫:router.beforeEach
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})
当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于等待中。一定要调用next()
方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
- 全局解析守卫: router.beforeResolve
和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
全局后置钩子: router.afterEach
路由独享的守卫: beforeEnter
// 你可以在路由配置上直接定义 beforeEnter 守卫,作用类似用beforeEnter
const router = new VueRouter({
routes: [
{
path: "/foo",
component: Foo,
beforeEnter: (to, from, next) => {
// ...
},
},
],
});
- 组件内的守卫: beforeRouteEnter、beforeRouteUpdate (2.2 新增)、beforeRouteLeave
const Foo = {
template: `...`,
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不能获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
//不过,可以通过传一个回调给 next来访问组件实例。
//在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
next((vm) => {
// 通过 `vm` 访问组件实例
});
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
// 通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
},
};
# 031:完整的导航解析流程
导航被触发。
在失活的组件里调用 beforeRouteLeave 守卫。
调用全局的 beforeEach 守卫。
在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
在路由配置里调用 beforeEnter。
解析异步路由组件。
在被激活的组件里调用 beforeRouteEnter。
用全局的 beforeResolve 守卫 (2.5+)。
导航被确认。
调用全局的 afterEach 钩子。
触发 DOM 更新(从 beforeCreate -> created -> beforeMount -> mounted [-> activated])。
调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
# 032:简单聊聊 new Vue 以后发生的事情
在 vue 的生命周期上第一个就是 new Vue() 创建一个 vue 实例出来,对应到源码在\vue-dev\src\core\instance\index.js:
function Vue(options) {
if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
warn(
"Vue is a constructor and should be called with the `new` keyword"
);
}
this._init(options);
}
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
// vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化data、props、computed、watcher 等等
# 033:模板字符串解析
let template = "我是{{name}},年龄{{age}},性别{{sex}}";
let data = {
name: "chieminchan",
age: 18,
sex: "girl",
};
function render(template, data) {
let computed = template.replace(/\{\{(\w+)\}\}/g, function(match, key) {
console.log(match, key);
return data[key];
});
return computed;
}
render(template, data);