仿vue2原理做一个简单vue2
1. 起步
1.1 响应式原理
我们都知道响应式是vue最独特的特性,是非入侵的响应式系统.数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。
下面是vue响应式原理的简易图示:
index.png
在vue官网,深入响应式原理中, 它是这么描述的:
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
1.2原理分析
Vue类负责初始化,将data中的属性注入到Vue实例中,并调用Observer类和Complier类,对数据进行劫持和解析,Observer类负责数据劫持, 通过Object,definePrototype,实现每一个data转换为getter和setter.Compiler负责解析指令和编译模板,初始化视图,收集依赖, 更新视图.Dep类负责收集依赖.添加观察者模式.通知data对应的所有观察者watcher来更新视图.Watcher类负责数据更新后,使关联视图重新渲染(更新DOM).
2. 开始
2.1 Vue类
class Vue {
constructor(options) {
// 限定Options类型为对象, 如果类型不为对象, 就抛出一个错误
if (!(options instanceof Object)) throw new TypeError('The parameter type should be an Object');
// 保存options中的数据
this.$options = options || {};
this.$data = options.data || {};
this.$el = options.el;
// 将vue实例中的data属性转换为 getter和setter, 并注入到vue实例中, 方便调用vm.msg
this._proxyData(this.$data)
//调用Observer类, 进行数据监听
new Observer(this.$data)
// 如果el元素有值, 调用Compiler类, 解析指令和插值表达式
if (this.$el) new Compiler(this.$el, this)
}
_proxyData(data) {
// 遍历data属性的key, 利用Object,definePrototype 进行数据劫持
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
set(newVal) {
if (newVal !== data[key]) data[key] = newVal;
},
get() {
return data[key]
}
})
})
}
}
2.2 Observer类
class Observer {
constructor(data) {
this.observe(data)
}
observe(data) {
// 如果设置的数据类型为对象就设置为响应式数据
if (data && typeof data === 'object') {
Object.keys(data).forEach(key => {
//调用设置响应式数据的方法
this.definePReactive(data, key, data[key])
})
};
}
// 设置属性为响应式数据
definePReactive(obj, key, value) {
// 利递归使深层属性转换为 响应式数据
this.observe(value)
const that = this; // 保存内部this, 方便内部调用
// 负责收集依赖并发送通知
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 订阅数据变化时, 往Dep中添加观察者, 收集依赖;
Dep.target && dep.addSub(Dep.target);
return value
},
set(newVal) {
that.observe(newVal)
if (newVal !== value) {
//如果新设置的值也为对象, 也转换为响应式数据
value = newVal;
}
// 发送通知;
dep.notify()
}
})
}
}
2.3 Compiler类
class Compiler { // 解析指令, 编译模板
static compileUtil = {// 解析指令的对象
getVal(key, vm) {// 获取指令上的数据值
//利用reduce 获取实例对象深层的属性值
return key.split('.').reduce((data, current) => {
return data[current]
}, vm.$data)
},
// 改变实例上的数据
setVal(key, vm, inputVal) {// key 属性值, vm: vue实例对象, inputVal: 输入框的值
let total = 'vm';// 用于拼接 出 vm['person']['name'] = inputVal
if (key.split('.').length === 1) vm[key] = inputVal;
else {
// 对字符串进行拼接
key.split('.').forEach(k => total += `['${k}']`)
total += '=' + `${'inputVal'}`
eval(total) // 利用eval强行将字符串解析为函数执行
}
},
text(node, key, vm) { // 编译v-text指令的方法
let value;// 保存获取的数据值
if (/\{\{(.+?)\}\}/.test(key)) {
// 全局匹配{{}}里面的变量, 利用...运算符展开 匹配的内容
// 利用正则解析出{{xxx.xx}}中的变量, 并取出相应的变量值
value = key.replace(/\{\{(.+?)\}\}/, (...args) => {
// 创建watcher对象, 当数据改变时, 更新视图
new Watcher(vm, args[1], newVal => { // 接受callback执行时第一个参数
this.updater.textUpdater(node, newVal)
})
return this.getVal(args[1], vm);
})
} else {
// 获取key 对应的数据
value = this.getVal(key, vm);
}
// 更新视图
this.updater.textUpdater(node, value)
},
model(node, key, vm) {// 解析v-model 指令
const value = this.getVal(key, vm);
/// 数据 => 视图
new Watcher(vm, key, newVal => {
this.updater.modelUpdater(node, newVal)
})
// 视图 => 数据 => 视图 双向数绑定
node.addEventListener('input', e => {
this.setVal(key, vm, e.target.value)
})
this.updater.modelUpdater(node, value)
},
html(node, key, vm) {// 解析HTML指令
const value = this.getVal(key, vm);
new Watcher(vm, key, newVal => {
this.updater.htmlUpdater(node, newVal)
})
this.updater.htmlUpdater(node, value)
},
on(node, key, vm, eventName) {// 解析v-on:click指令
// 获取实例对象中的methods中的方法
const fn = vm.$options.methods && vm.$options.methods[key];
// 绑定事件
node.addEventListener(eventName, function (ev) {
fn.call(vm, ev)// 改变fn函数内部的this,并传递事件对象event
}, false)
},
bind(node, key, vm, AttrName) {// 解析 v-bind 指令
node[AttrName] = vm.$data[key]
},
updater: {// 保存所有更新页面视图的方法的对象
textUpdater(node, value) {
node.textContent = value
},
htmlUpdater(node, value) {
node.innerHTML = value
},
modelUpdater(node, value) {
node.value = value
}
}
}
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 获取文档碎片, 减少页面的回流和重绘
const fragment = this.nodeFragment(this.el);
// 编译文档碎片
this.compile(fragment)
// 追加到根元素
this.el.appendChild(fragment)
}
nodeFragment(el) {
// 创建文档碎片
const fragment = document.createDocumentFragment();
// 如果当前第一个子节点有值, 追加到文档碎片
while (el.firstChild) {
fragment.appendChild(el.firstChild)
}
return fragment
}
compile(fragment) {
// 获取子节点
const childNodes = fragment.childNodes;
// 遍历所的子节点
[...childNodes].forEach(child => {
// 如果为元素节点
if (this.isElementNode(child)) {
this.compileElement(child)
} else {
// 解析文本节点
this.compileText(child)
}
// 如果子节点还有子节点元素就递归遍历该子节点
if (child.childNodes && child.childNodes.length) {
this.compile(child)
}
})
}
compileElement(node) { // 编译元素节点
const {compileUtil} = Compiler;
// 获取元素节点的所有自定义属性
const attributes = node.attributes;
//利用展开运算符将attributes类数组对象转换为数组并遍历
[...attributes].forEach(attr => {
//将v-mode=msg 中的 v-model 和 msg 解构出来
const {name, value} = attr;
//判断属性是否为 v-开头
if (this.isDirective(name)) {
// 解构出v-text 中的 text
const [, directive] = name.split('-');
// 解构出 v-on:click 中的 on 和 click
const [dirname, eventName] = directive.split(':');
// 利用策略组合模式 调用相应的解析方法并 更新数据 数据驱动视图
compileUtil[dirname](node, value, this.vm, eventName)
// 删除有指令的标签上的属性
node.removeAttribute('v-' + directive)
}
})
}
compileText(node) { // 编译文本节点
const text = node.textContent;
// 把Compiler类中的compileUtil对象解构出来
const { compileUtil} = Compiler;
let reg = /\{\{(.+?)\}\}/;// 匹配 {{xx.xx}}的正则
if (reg.test(text)) { // 如果是 {{}}的文本节点
compileUtil['text'](node, text, this.vm)
}
}
isDirective(attrName) { // 是否为v-开头的指令
return attrName.startsWith('v-')
}
isElementNode(node) { // 是否为 元素节点
return node.nodeType === 1;
}
}
2.4 Dep类
class Dep {
constructor() {
// 保存所有的观察者列表
this.subs = []
}
addSub(sub) { // 收集观察者
this.subs.push(sub)
}
notify() { // 通知观察者就更新视图
this.subs.forEach(w => w.update())
}
}
2.5 Watcher类
class Watcher {
constructor(vm, key, callback = value => {}) {
this.vm = vm;
// data中的属性名
this.key = key;
//回调函数负责更新视图
this.callback = callback;
//先把旧值保存起来;
this.oldValue = this.getOldValue()
}
getOldValue() {
// 把Watcher对象挂载到 Dep类的静态属性 target中
Dep.target = this;
const oldVal = Compiler.compileUtil.getVal(this.key, this.vm);
// 清空watcher对象,避免重复设置
Dep.target = null;
return oldVal
}
// 当数据发生改变时更新视图
update() {
const newVal = Compiler.compileUtil.getVal(this.key, this.vm);
if (newVal !== this.oldValue) {
//当数据发生改变调用callback并传递新值
this.callback(newVal)
}
}
}
3.html文檔碎片
- 直译过来就是文档碎片,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的 Document。最大的区别是因为 DocumentFragment 不是真实DOM树的一部分,它的变化不会触发 DOM 树的(重绘)(DOM對各個節點的大小位置的各種計算) ,且不会导致性能等问题。
- 它主要用来解决dom元素的插入问题,比如需要插入多个dom节点时,可以创建一个DocumentFragment,把节点依次添加到DocumentFragment上,添加完毕后再把DocumentFragment添加到页面document上,这样只会产生一次重绘。而如果直接把dom节点依次添加到页面document上就会引发多次重绘
- 实际上现代浏览器对js引发重绘的操作也会进行节流控制的,比如短时间内触发了多次repaint的话,浏览器会将他们合并成一次repaint来处理,不过保险起见直接使用DocumentFragment即可。
- 可以通过
document.createDocumentFragment创建一个文档碎片。
var oFrag=document.createDocumentFragment();
for (var i=0; i<1000; i++) {
var op=document.createElement("div");
var oText=document.createTextNode(‘i’);
op.appendChild(oText);
oFrag.appendChild(op);
}
document.body.appendChild(oFrag);
- html原生的
<template>标签其实就是一个documentfragment,使用template标签时,实际插入到document上面的是,template的子节点,它本身不会被添加到dom上面。 - react框架也有类似template的文档碎片概念:fragments,从而可以一个组件返回多个元素。
- vue也使用了template标签,不过他不允许一个组件模板上有多个父元素节点(必须有一个唯一父节点)。
4.js語法
in
var arr = [1,2,3]
var a = 1
consoel.log(a in arr) //true
a = '1'
consoel.log(a in arr) //true
5.reduce
JS数组reduce()方法详解及高级技巧
reduce()方法可以搞定的东西,for循环,或者forEach方法有时候也可以搞定,那为啥要用reduce()?这个问题,之前我也想过,要说原因还真找不到,唯一能找到的是:通往成功的道路有很多,但是总有一条路是最捷径的,亦或许reduce()逼格更高...
1、语法
arr.reduce(callback,[initialValue])
reduce 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素,接受四个参数:初始值(或者上一次回调函数的返回值),当前元素值,当前索引,调用 reduce 的数组。
callback (执行数组中每个值的函数,包含四个参数)
1、previousValue (上一次调用回调返回的值,或者是提供的初始值(initialValue))
2、currentValue (数组中当前被处理的元素)
3、index (当前元素在数组中的索引)
4、array (调用 reduce 的数组)
initialValue (作为第一次调用 callback 的第一个参数。)
2、实例解析 initialValue 参数
先看第一个例子:
var arr = [1, 2, 3, 4];
var sum = arr.reduce(function(prev, cur, index, arr) {
console.log(prev, cur, index);
return prev + cur;
})
console.log(arr, sum);
打印结果: 1 2 1 3 3 2 6 4 3 [1, 2, 3, 4] 10
这里可以看出,上面的例子index是从1开始的,第一次的prev的值是数组的第一个值。数组长度是4,但是reduce函数循环3次。
再看第二个例子:
var arr = [1, 2, 3, 4];
var sum = arr.reduce(function(prev, cur, index, arr) {
console.log(prev, cur, index);
return prev + cur;
},0) //注意这里设置了初始值
console.log(arr, sum);
打印结果: 0 1 0 1 2 1 3 3 2 6 4 3 [1, 2, 3, 4] 10
这个例子index是从0开始的,第一次的prev的值是我们设置的初始值0,数组长度是4,reduce函数循环4次。
结论:如果没有提供initialValue,reduce 会从索引1的地方开始执行 callback 方法,跳过第一个索引。如果提供initialValue,从索引0开始。
注意:如果这个数组为空,运用reduce是什么情况?
var arr = [];
var sum = arr.reduce(function(prev, cur, index, arr) {
console.log(prev, cur, index);
return prev + cur;
})
//报错,"TypeError: Reduce of empty array with no initial value"
但是要是我们设置了初始值就不会报错,如下:
var arr = [];
var sum = arr.reduce(function(prev, cur, index, arr) {
console.log(prev, cur, index);
return prev + cur;
},0)
console.log(arr, sum); // [] 0
所以一般来说我们提供初始值通常更安全
3、reduce的简单用法
当然最简单的就是我们常用的数组求和,求乘积了。
var arr = [1, 2, 3, 4];
var sum = arr.reduce((x,y)=>x+y)
var mul = arr.reduce((x,y)=>x*y)
console.log( sum ); //求和,10
console.log( mul ); //求乘积,24
4、reduce的高级用法
(1)计算数组中每个元素出现的次数
let names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];
let nameNum = names.reduce((pre,cur)=>{
if(cur in pre){
pre[cur]++
}else{
pre[cur] = 1
}
return pre
},{})
console.log(nameNum); //{Alice: 2, Bob: 1, Tiff: 1, Bruce: 1}
(2)数组去重
let arr = [1,2,3,4,4,1]
let newArr = arr.reduce((pre,cur)=>{
if(!pre.includes(cur)){
return pre.concat(cur)
}else{
return pre
}
},[])
console.log(newArr);// [1, 2, 3, 4]
(3)将二维数组转化为一维
let arr = [[0, 1], [2, 3], [4, 5]]
let newArr = arr.reduce((pre,cur)=>{
return pre.concat(cur)
},[])
console.log(newArr); // [0, 1, 2, 3, 4, 5]
(3)将多维数组转化为一维
let arr = [[0, 1], [2, 3], [4,[5,6,7]]]
const newArr = function(arr){
return arr.reduce((pre,cur)=>pre.concat(Array.isArray(cur)?newArr(cur):cur),[])
}
console.log(newArr(arr)); //[0, 1, 2, 3, 4, 5, 6, 7]
(4)、对象里的属性求和
var result = [
{
subject: 'math',
score: 10
},
{
subject: 'chinese',
score: 20
},
{
subject: 'english',
score: 30
}
];
var sum = result.reduce(function(prev, cur) {
return cur.score + prev;
}, 0);
console.log(sum) //60
6.||和??
let res = val1 ?? val2; //若val1为null 或者 undefined 则 取后面的值; let res1 = val1 || val2; //若val1 为 null undefinde 0 false 空字符串 则取后面的值
双问号更适合在不知道变量是否定义或者是否赋值时的场景来使用