vue
个人使用的框架除了jquery,就是vue,刚学vue时感觉比较神奇,针对vue的特点,看了许多博客、视屏。在此记录下自己的理解。
1. 和传统jQuery框架相比,vue实现了数据和视图的分离,比如说将数据展示成表格,jquery需要将数据和dom结构拼接起来,再通过append方法添加到文本结构中去,而vue不用。
2. vue以数据驱动视图,通过修改数据,不用修改DOM操作,框架自动实现页面修改,jquery需要我们清空元素,再创建标签,添加到文本节点中去。
说到vue不得不聊一聊,mvc和mvvm两种框架的却别。mvc框架:用户——>view——>Controller——>Model——>view,mvvm(Model View ViewModel)是mvc的微创新,结合前端场景应用创建的。View(对应dom结构)通过DOM listener 影响model(对应JavaScript对象), model通过data Bindings 影响视图,DOM listener和data Bindings就是viewModel,model和view分离的。
虚拟dom
定义:virtual dom,简单讲就是用js模拟dom,将DOM的变化对比放在JS层来做。
virtual-dom可以看做一棵模拟了DOM树的JavaScript树,其主要是通过vnode,实现一个无状态的组件,当组件状态发生更新时,然后触发virtual-dom数据的变化,然后通过virtual-dom和真实DOM的比对,再对真实dom更新。
原因:提高重绘性能,在网站中dom操作是很昂贵的(dom节点的属性特别多),dom结构的变化会引起重排(reflow)与重绘(repaint),而js的运行效率很高,所以可以提高性能。比方说一个列表原来有a,b,c,d三个元素,我们希望删除b,d,仅删去b,d元素,比删去a,b,c,d元素再添加a,c元素快很多 (从数据库中取到的数据我们并不知道,哪些元素发生改变)。
- 将dom结构用js表示
下面示例给出简单表示如何将dom结构用js表示
// 示例 html片段
<ul id="list">
<li class="item">item1</li>
<li class="item">item2</li>
</ul>
// 我们可以使用js来模拟
{
tag: 'ul',
attrs: {
id: 'list'
},
children: [
{
tag: 'li',
attrs: {className: 'item'},
children: ['item1']
},
{
tag: 'li',
attrs: {className: 'item'},
children: ['item2']
}
]
}
- 虚拟dom和jQuery比较
设计一个需求场景:将数据展示成一个表格。随便修改一个信息,表格也跟着修改
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>v-dom compare jQuery</title>
</head>
<body>
<div id="container"></div>
<button id="btn-change">change</button>
<script src="jquery-2.1.1.min.js"></script>
<script>
// jquery中如何实现
var data = [
{
name: '张三',
age: 20,
city: '成都'
},
{
name: '李四',
age: 24,
city: '武汉'
},
{
name: '王麻子',
age: 30,
city: '武汉'
},
]
function render (data) {
var $container = $('#container');
//清空所有内容
$container.html('');
var $table = $('<table>');
$table.append($('<tr><td>name</td><td>age</td><td>city</td><tr>'));
data.forEach(function (item) {
$table.append($('<tr><td>' + item.name + '</td><td>' + item.age +
'</td><td>' + item.city + '</td><tr>'));
});
$container.append($table);
}
$('#btn-change').click(function () {
data[1].age = 25;
data[2].city = '深圳';
render(data);
});
render(data);
// 最理想的效果是只改变data[1],data[2]所填充的节点
// v-dom就可以实现这种需求
</script>
</html>
- vdom的应用,核心API
snabbdom是实现vdom比较好一个开源库。其实现核心API就是h函数和patch函数
首先,我们从最简单的vnode开始入手,vnode实现的功能非常简单,就是讲输入的数据转化为vnode。
//VNode函数,用于将输入转化成VNode
/**
*
* @param sel 选择器
* @param data 绑定的数据,可以有以下类型:attribute、props、eventlistner、class、dataset、hook
* @param children 子节点数组
* @param text 当前text节点内容
* @param elm 对真实dom element的引用
* @returns {{sel: *, data: *, children: *, text: *, elm: *, key: undefined}}
*/
module.exports = function ( sel, data, children, text, elm ) {
var key = data === undefined ? undefined : data.key;
return {
sel: sel, data: data, children: children,
text: text, elm: elm, key: key
};
};
说完vnode,就到h了,h也是一个包装函数,主要是在vnode上再做一层包装,返回一个node节点,模拟真实的dom节点
var VNode = require ( './vnode' );
var is = require ( './is' );
//添加命名空间(svg才需要)
function addNS ( data, children, sel ) {
data.ns = 'http://www.w3.org/2000/svg';
//如果选择器
if ( sel !== 'foreignObject' && children !== undefined ) {
//递归为子节点添加命名空间
for (var i = 0; i < children.length; ++i) {
addNS ( children[ i ].data, children[ i ].children, children[ i ].sel );
}
}
}
//将VNode渲染为VDOM
/**
*
* @param sel 选择器
* @param b 数据
* @param c 子节点
* @returns {{sel, data, children, text, elm, key}}
*/
module.exports = function h ( sel, b, c ) {
var data = {}, children, text, i;
//如果存在子节点
if ( c !== undefined ) {
//那么h的第二项就是data
data = b;
//如果c是数组,那么存在子element节点
if ( is.array ( c ) ) {
children = c;
}
//否则为子text节点
else if ( is.primitive ( c ) ) {
text = c;
}
}
//如果c不存在,只存在b,那么说明需要渲染的vdom不存在data部分,只存在子节点部分
else if ( b !== undefined ) {
if ( is.array ( b ) ) {
children = b;
}
else if ( is.primitive ( b ) ) {
text = b;
}
else {
data = b;
}
}
if ( is.array ( children ) ) {
for (i = 0; i < children.length; ++i) {
//如果子节点数组中,存在节点是原始类型,说明该节点是text节点,因此我们将它渲染为一个只包含text的VNode
if ( is.primitive ( children[ i ] ) ) children[ i ] = VNode ( undefined, undefined, undefined, children[ i ] );
}
}
//如果是svg,需要为节点添加命名空间
if ( sel[ 0 ] === 's' && sel[ 1 ] === 'v' && sel[ 2 ] === 'g' ) {
addNS ( data, children, sel );
}
return VNode ( sel, data, children, text, undefined );
};
现在我们就可以根据dom文档模拟真实的dom节点了
// 示例 html片段
<ul id="list">
<li class="item">item1</li>
<li class="item">item2</li>
</ul>
// js模拟
var vnode = h('ul#list', {}, [
h('li.item', {}, 'item1'),
h('li.item', {}, 'item2')
]);
patch函数有两种情况,patch函数第一个参数是真实的dom节点,第二参数是虚拟的dom对象,patch(container, vnode),就是在第一次渲染的时候,渲染所有的数据;另外一种就是传入两个虚拟的dom对象,patch(vnode, newVnode),第一个是旧的,第二参数是新的,对比两者,找出区别改动,仅仅渲染改动点。
// 引入snabbdom依赖, 这里需要snabbdom-class,snabbdom-props,snabbdom-style,snabbdom-eventlisteners,snabbdom
var snabbdom = window.snabbdom;
var patch = snabbdom.init([snabbdom_class,snabbdom_props,snabbdom_style,snabbdom_eventlisteners]);
var h = snabbdom.h;
var vnode = h('ul#list', {}, [
h('li.item', {}, 'item1'),
h('li.item', {}, 'item2')
]);
var container = document.getElementById('container');
patch(container, vnode);
// 模拟改变
var btnChange = document.getElementById('btn-change');
btnChange.addEventListener('click', function () {
var newVnode = h('ul#list', {}, [
h('li.item', {}, 'item1'),
h('li.item', {}, 'item222'),
h('li.item', {}, 'item333')
]);
patch(vnode, newVnode);
});
实现之前的场景
// 引入依赖并初始化
var data = [
{
name: '张三',
age: 20,
city: '成都'
},
{
name: '李四',
age: 24,
city: '武汉'
},
{
name: '王麻子',
age: 30,
city: '武汉'
},
]
data.unshift({name: 'name', age: 'age', city: 'city'});
var container = document.getElementById("container");
var vnode;
function render (data) {
var newVnode = h('table', {}, data.map(function (item) {
var tds = [];
var i;
for(i in item) {
if (item.hasOwnProperty(i) {
tds.push(h('td'), {}, item[i] + '')
});
}
return h('tr', {}, tds);
}));
if (vnode) {
patch(vnode, newVnode);
} else {
patch(container, newVnode)
}
vnode = newVnode;
}
$('#btn-change').click(function () {
data[1].age = 25;
data[2].city = '深圳';
render(data);
});
render(data);
// 用表格用法简单举例,介绍h函数,patch函数,h函数:h('<标签名>',{...属性...},[...子元素...]),patch(container, vnode), patch(vnode, newVnode)
- diff算法
linux指令,git等等都有使用,用来对比文本文件的,使用到虚拟dom中用来对比两个虚拟dom的节点,找出其差异。diff算法非常复杂,实现难度大,去翻就简。需要找出DOM必须更新的节点来更新,所以需要使用diff算法。
// patch 函数的简单实现
var data = {
tag: 'ul',
attrs: {
id: 'list'
},
children: [
{
tag: 'li',
attrs: {className: 'item'},
children: ['item1']
},
{
tag: 'li',
attrs: {className: 'item'},
children: ['item2']
}
]
}
// 针对data数据,如何通过vnode获取真正的dom节点的大概思路
function createElement (vnode) {
var tag = vnode.tag;
var attrs = vnode.attrs || {};
var children = vnode.children || {};
if (tag = null) {
return null;
}
// 创建真实的元素
var elem = document.createElement(tag);
// 给dom属性挂载属性
var attrName;
for (attrName in attrs) {
if (attrs.hasOwnProperty(attrName)) {
elem.setAttribute(attrName, attrs[attrName]);
}
}
// 通过递归给元素节点添加子元素
children.forEach(function (childVnode) {
elem.appendChild(createElement(childVnode))
});
}
function updateChildren(vnode, newVnode) {
var children = vnode.children || [];
var newChildren = newVnode.children || [];
// 遍历所有的children
children.forEach(function(child, index) {
var newChild = newChildren[index];
if (newChild == null) {
return;
}
if (child.tag === newChild.tag) {
// 两者一样递归比较子元素
updateChildren(child, newChild);
} else {
// 两者不一样
replaceNode(child, newChild);
}
});
}
Vue响应式
响应式:修改data属性之后,vue立刻监听到;data属性被代理到vue实例上。vue通过Object.defineProperty函数实现的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>v-dom compare jQuery</title>
<script src="https://cdn.bootcss.com/vue/2.5.17-beta.0/vue.min.js"></script>
</head>
<body>
<div id="app">
<p>{{name}}</p>
<p>{{age}}</p>
</div>
<script>
var vm = new Vue({
el: "#app",
data: {
name: 'zhangsan',
age: 20
}
});
// vm.name = 'lisi';
// vm.age = '24';
</script>
</body>
</html>
// 如果在浏览器的调试窗口,通过vm修改age, name页面会立即刷新
Object.defineProperty函数实现监听(从es5中加入的)
var obj = {};
var name = '张三';
Object.defineProperty(obj, "name", {
get: function () {
console.log('get');
return name;
},
set: function (newVal) {
console.log('set');
name = newVal;
}
});
console.log(obj.name); // get 张三
obj.name = '123'; // set
console.log(obj.name); // get 123
// 模拟如何将data属性代理到vue实例上去
var vm = {};
var data = {
price: 100,
name: 'zhangsan'
}
var key;
for (key in data) {
// 创建一个闭包,新建一个函数,保证key的独立的作用域
(function (key) {
Object.defineProperty(vm, key, {
set: function () {
return data[key];
}
get: function (newVal) {
data[key] = newVal;
}
});
})(key)
}
解析模板
- 模板是什么
模板本质是字符串,比较像html,但有很大区别;实际意义是有逻辑,有v-if,v-for;html是静态的,vue模板是动态的;最终要展示成html显示,要做到这些必须转换成js代码。模板转换成js函数(render函数)
<div id="app">
<div>
<input v-model="title">
<button @click="add">submit</button>
</div>
<ul>
<li v-for="item in list">{{item}}</li>
</ul>
</div>
- render 函数
// 以该实例简单的分析
<div id="app">
<p>{{price}}</p>
</div>
<script src="https://cdn.bootcss.com/vue/2.5.17-beta.0/vue.min.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
price: 100
}
});
// 手写render函数
function render () {
with(this) { //this就是vm
return _c(
'div',
{attrs: 'id': 'app'},
[_c('p', _v(_s(price)))]
);
}
}
</script>
// 最终解析为 模板中所有的信息都包含在了render函数中,this即vm,price即this.price即vm.price,即data.price
// click实现,给元素绑定click时间
// v-model双向数据绑定
// v-for 循环数据,封装成一个数组返回
- render函数与vdom
render函数返回的是vnode,_c, _v返回的都是vnode
vm._update(vnode) {
var prevVnode = vm._vnode;
vm._vnode = vnode;
if (!prevVnode) {
vm.$el = vm._patch_(vm.$el, vnode)
} else {
vm.$el = vm._patch_(prevVnode, vnode)
}
}
function updateComponent () {
// vm._render即上面的render函数,返回vnode
vm._update(vm_render());
}
// updateComponent中实现vdom的patch
// 页面首次渲染执行updateComponent
// data中每次修改属性,执行updateComponent
1.解析模板成render函数
2.响应式开始监听
3.首次渲染,显示页面,且绑定依赖
4.data属性变化,触发rerender