理解vue

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值