响应式学习----从VUE1 到 VUE2

来源:互联网 发布:jquery1.11.3.js 下载 编辑:程序博客网 时间:2024/05/22 08:56

老大让搞个分享,实在想不出有啥能分享的地方,积累不够啊,之前研究过vue 0.1 的源码(对你没看错就是0.1不是1.0),所以想再看看vue2就乱说一通吧。这是原版PPT

大概讲一下以下五方面内容
1. 三大框架
2. 订阅发布模式
3. VUE1双向数据绑定
4. VUE2 virtual DOM
5.其他

1. 三大框架

最近比较喜欢一张非常喜感的图。
刀耕火种时代的前端

其实最初的前端很简单,就跟这位大爷的说法差不多,直接拿起键盘,搞个编辑器然后打开浏览器就可以了,我相信包括我在内的很多人也就是这么单纯地喜欢上前端的,记得宇豪也是说喜欢前端就是因为CSS。

起初的前端,标准混乱,浏览器性能差,随着ajax和node出现带来的两次飞跃,前端能做的事情越来越多,越来越多的需求也放到了前端,由后端主导的静态页面发展到了前后端分离的动态页面,前端逻辑变得复杂,也难以管理。

我们需要更好的管理视图层逻辑,需要更好性能、可维护性、开发效率,不需要关注视图层,只需要处理业务逻辑、管理状态即可。

与此同时,程序员的最大优点懒癌发作,所以,10年angular开源,13年react开源,14年vue发布。

现如今可能比较流行的还是这三大框架,我们简单介绍一下

angular,接触不多,数据绑定采用脏检查,简单理解,angular检测到几种类型的变化(DOM等),会对所有脏数据进行检查,其实性能也还不错,绝不像谣言中的定时检查一样那么差,包括谷歌也做了很多优化(团队实力毋庸置疑,开源贡献也很多,当然可能版本升级不兼容是最遭吐槽的地方了),也一直往更快、更小、更方便的方向优化,据说今年马上就出5了。angular也是早期web非常流行的框架,包括现在也还有不少使用它作为企业级解决方案,如网易等,不过现在的用户可能主要集中在angular1、2。

react和vue其实相似的地方很多
1. 都提供了响应式和组件化的视图组件
2. 都专注于核心库开发,其他功能作为插件
3. virtual DOM

不同之处:
1. jsx VS template
2. 数据绑定

react,按我理解的话,数据绑定是单向的
setState =》 传入新状态 =》 创建新virtual DOM =》 DOM diff =》 最小代价render
如果需要用户交互改变状态,那么需要手动绑定事件回调,再进行状态变动

vue双向数据邦迪是基于数据劫持 + 订阅发布模式,这也是本文的重点内容,后面会有比较详细的介绍。

每一个订阅发布都形成了一个链条,元编程实时追踪状态变化,并且根据依赖关系直接变更相应watcher,不用比较盲目脏检查
vue2 加入virtual DOM,同时开始支持ssr(服务端渲染)

2. 订阅发布模式

{    const interFace = function (){        const dependence = {}        return {            publish(type){            // 发布                if(event[type]){                    event[type].forEach(function(element) {                        element()                    })                }            },            watch(type, callback){   // 订阅                if(event[type]){                    event[type].push(callback)                } else {                    event[type] = [callback]                }            }        }    }    const {dispatch, addEvent} = interFace()    watch("我会随时播报vue的数据变化", function(){        console.log("vue的数据变化了,我们要同步DOM更新")        })                                   // 订阅消息    publish("我会随时播报vue的数据变化")         // 发布消息   }

订阅发布模式,又称观察者模式,实质上是一种多对多关系的解耦,发布者会维护一个订阅列表(关系列表)记录订阅者的订阅行为,当有需要的时候,取出对应的订阅者并“通知”他们;订阅者会做出订阅行为,并等待发布者发布消息。

vue其实也是出于解耦“订阅者”和“发布者”两者关系考虑来使用者套模式,并且在发布者(劫持的变化)和订阅者(响应变化进行DOM变化)之间增加了dep对象来管理依赖,实现了更加明确的单一职责,使得代码组织变得更加清晰,可维护性更高,更方便进行后期优化和维护甚至重构。

下面我们结合前面的设计模式来实现一个最简单的vue

<div id="app" style="text-align:center;">    <input id="input" type="text" v-model="name"><br> 姓名:    <span id="text">{{name}}</span></div><script>    const Vue = function (options) {        let {node, data} = options,            name = data.name,        // 编译正则匹配取到 对象两个node节点        input = document.querySelector("#input"),        text = document.querySelector("#text")        Object.defineProperty(data, "name", {            enumerable: true,            configurable: true,            set(newVal) {                input.value = newVal                text.innerHTML = newVal            },            get(){                return name            }        })        input.addEventListener("input", function () {            data.name = this.value        })        // 编译后 利用setter初始化模板中的值   实际上是使用getter初始化        data.name = name    }    new Vue({        el: "#app",        data: {            name: "allen"        }    })</script>

在这个例子中基本干掉了编译的代码,实际的生命周期中是先进行数据劫持,再进行编译和依赖收集。

首先看一下Object.defineProperty(obj, key, descriptor)这个函数,它的descriptor有几种选项,其中包括get和set,定义了当前obj键值key的取和改两种行为,简单说,如果你进行obj.key 触发get方法,其返回值就是get return的值;进行obj.key = 1 赋值会触发set方法,并且第一参数为1。

所以所谓数据劫持就是劫持数据的变化,改变原本正常的行为,在原本的数据变化中加入你的代码,如果你的数据发生变动可以第一时间“被动监听到”。

set函数中监听到了数据变化,那么我们的预期就是会进行相应的DOM变动,这里简单就直接进行了innerHTML等赋值操作改变DOM。

get函数中主要是进行初始化的逻辑,是依赖收集的重要部分,demo中使用setter进行简单替代,注意构造函数最后一行。

当然这么简单的代码确实是可以运行的,也实现了双向数据绑定,但是强耦合赋予的是效率低下和可维护性差;并且随着需要绑定的数据数量和种类增加,甚至更进一步各种复杂指令类型对应的复杂操作,我们的代码会变得越来越臃肿和不可维护。

所以我们需要一种更规范的初始化流程和数据流动管理,抽象公共行为,加入中间对象解耦逻辑,形成职责单一、低耦合、高内聚的代码块。

3. VUE1双向数据绑定

我们来看一个简单的VUE场景

<div id="main">  <h1>count: {{times}}</h1></div><script src="vue.js"></script><script>  var vm = new Vue({    el: '#main',    data: function () {      return {        times: 1      };    },    created: function () {      var me = this;      setInterval(function () {        me.times++;      }, 1000);    }  });</script>

主要实现了一个定时器功能,js操作状态(times)不断变化,视图层随着状态变化而变化。

vue双向数据绑定图

上图是简单的一个内部原理图,表示了times被赋值之后的变化,以及dep的依赖收集。

初始化,先进行数据劫持,然后模板编译,创建watcher也就是订阅者,紧接着dep进行依赖(发布订阅)关系确定,连接起observer和watcher,然后就等待数据变动。

初始化完毕后,times变化,数据劫持到变化,触发nodify(通知,即发布)行为,通知dep然后找到dep中,这个数据的相应依赖,然后updata watcher,watcher最终会进行相应DOM操作。

下面看一下我实现的简单vue的observer函数,其中有比较重要的数据劫持、初始化依赖收集、发布行为(nodify函数)

function Observer(options, keyword) {    this.data = options    Object.keys(options).forEach((key) => {        if (typeof options[key] === 'object' && options[key] instanceof Object) {            new Observer(options[key], key)        } else {            this.defineSetget(key, options[key])        }    })   }Observer.prototype.defineReactive = function (key, val) {    const dep = new Dependence(key, val)    // 闭包一个dep实例    Object.defineProperty(this.data, key, {        enumerable: true,        configurable: true,        get: function () {            if (Dependence.target) {                 dep.addDep()         // 添加watcher进 dep实例            }            return val              // 返回闭包数据        },        set: function (newVal) {            if (val === newVal) return            val = newVal            // 修改闭包数据            console.log(`你改变了${key}`)            if (typeof val === 'object' && val instanceof Object) {                new Observer(val)            }            dep.notify(newVal)      // 通知,即发布数据变动        }    })}

遍历对象,对对象的每个属性数据劫持

在每个defineProperty函数外层作用域创建一个闭包dep,dep和observer是根据作用域链利用闭包进行通信,setter在数据变动时通知watcher,初始化时我们会触发一次getter使其自动收集依赖,完成订阅,进入闭包dep实例的依赖列表中。

再看一下简单的watcher函数,其中有

function Watcher(type, path, DOM, tokens, vm) {    this.type = type    this.keyPath = path    this.node = DOM    this.tokens = tokens    // Directive的功能    if (type === "model") {        DOM.addEventListener("input", e => this.set(vm, path, e.target.value))    }    Dependence.target = this    // get到值 完成依赖收集 然后进行node初始化赋值    this.tokens.forEach((elem) => {        if (!elem.html) {            this.update(this.get(vm, path))         }    })}// 触发相应发布者的getterWatcher.prototype.get = function (vm, path) {}// 触发相应发布者的getterWatcher.prototype.set = function (vm, path, value) {}// 通知Directive更新DOMWatcher.prototype.update = function (value) {}

其中事件监听是简单概括了diective实例的功能,之后根据传入参数进行依赖收集,先确定当前计算的target也就是当前的watcher实例,触发相应observer的getter函数,获得当前初始化值并直接进行updata,进而更新视图。

4. VUE2 virtual DOM

vue2数据响应式过程

vue2一个很大的改动就是加了virtual DOM,对数据绑定影响不太大,主要接管了DOM更新这边的事情。

当watcher被通知发生变动时,watcher会通知组件的render函数进行re-render操作,创建一个新的virtual DOM,再跟旧virtual DOM进行diff,得到一个最优的DOM更新路径(操作次数少,动作小)

实质上可以看做加了一个中间层对DOM渲染进行优化,只需要当做黑盒把状态注入,virtual DOM自动计算最小代价渲染。

看一段snabbdom的代码,snabbdom是一个非常轻量的virtual DOM库

var snabbdom = require("snabbdom");var patch = snabbdom.init([ // 初始化补丁功能与选定的模块  require("snabbdom/modules/class").default, // 使切换class变得容易  require("snabbdom/modules/props").default, // 用于设置DOM元素的属性(注意区分props,attrs具体看snabbdom文档)  require("snabbdom/modules/style").default, // 处理元素的style,支持动画  require("snabbdom/modules/eventlisteners").default, // 事件监听器]);var h = require("snabbdom/h").default; // 用于创建vnode,VUE中render(createElement)的原形var container = document.getElementById("container");var vnode = h("div#container.two.classes", {on: {click: someFn}}, [  h("span", {style: {fontWeight: "bold"}}, "This is bold"),  " and this is just normal text",  h("a", {props: {href: "/foo"}}, "I\"ll take you places!")]);// 第一次打补丁,用于渲染到页面,内部会建立关联关系,减少了创建oldvnode过程patch(container, vnode);//创建新节点var newVnode = h("div#container.two.classes", {on: {click: anotherEventHandler}}, [  h("span", {style: {fontWeight: "normal", fontStyle: "italic"}}, "This is now italic type"),  " and this is still just normal text",  h("a", {props: {href: "/bar"}}, "I\"ll take you places!")]);//第二次比较,上一次vnode比较,打补丁到页面//VUE的patch在nextTick中,开启异步队列,删除了不必要的patchpatch(vnode, newVnode);// Snabbdom efficiently updates the old view to the new state

注释已经比较详细了,就不详细讲了,再放一个virtual DOM的构造函数

export default class VNode {  tag: string | void;  data: VNodeData | void;  children: ?Array<VNode>;  text: string | void;  elm: Node | void;  ns: string | void;  context: Component | void; // rendered in this component's scope  functionalContext: Component | void; // only for functional component root nodes  key: string | number | void;  componentOptions: VNodeComponentOptions | void;  componentInstance: Component | void; // component instance  parent: VNode | void; // component placeholder node  raw: boolean; // contains raw HTML? (server only)  isStatic: boolean; // hoisted static node  isRootInsert: boolean; // necessary for enter transition check  isComment: boolean; // empty comment placeholder?  isCloned: boolean; // is a cloned node?  isOnce: boolean; // is a v-once node?  constructor (    tag?: string,    data?: VNodeData,    children?: ?Array<VNode>,    text?: string,    elm?: Node,    context?: Component,    componentOptions?: VNodeComponentOptions  ) {    /*当前节点的标签名*/    this.tag = tag    /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/    this.data = data    /*当前节点的子节点,是一个数组*/    this.children = children    /*当前节点的文本*/    this.text = text    /*当前虚拟节点对应的真实dom节点*/    this.elm = elm    /*当前节点的名字空间*/    this.ns = undefined    /*编译作用域*/    this.context = context    /*函数化组件作用域*/    this.functionalContext = undefined    /*节点的key属性,被当作节点的标志,用以优化*/    this.key = data && data.key    /*组件的option选项*/    this.componentOptions = componentOptions    /*当前节点对应的组件的实例*/    this.componentInstance = undefined    /*当前节点的父节点*/    this.parent = undefined    /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/    this.raw = false    /*静态节点标志*/    this.isStatic = false    /*是否作为跟节点插入*/    this.isRootInsert = true    /*是否为注释节点*/    this.isComment = false    /*是否为克隆节点*/    this.isCloned = false    /*是否有v-once指令*/    this.isOnce = false  }  // DEPRECATED: alias for componentInstance for backwards compat.  /* istanbul ignore next */  get child (): Component | void {    return this.componentInstance  }}

再放一个简单的解析virtual DOM的函数

HTMLParser(html, {  start: function( tag, attrs, unary ) { //标签开始    results += "<" + tag;    for ( var i = 0; i < attrs.length; i++ )      results += " " + attrs[i].name + "="" + attrs[i].escaped + """;    results += (unary ? "/" : "") + ">";  },  end: function( tag ) { //标签结束    results += "</" + tag + ">";  },  chars: function( text ) { //文本    results += text;  },  comment: function( text ) { //注释    results += "<!--" + text + "-->";  }});return results;

5. 其他

劫持

一个vue的视图系统主体是一个个的vm实例,组件vm层层嵌套,最终组成整个视图,而在vm上vue将vm中的数据proxy控制到vm上,可能说的有点抽象,实际效果就是

const vm = new Vue({    data: {        name: allen    }})

按照逻辑来看访问name属性应该是这样的

vm.data.name

但实际上你只需要这样

vm.name

这是因为构造函数内部有一段这样的逻辑

_proxy(options.data);/*构造函数中*//*代理*/function _proxy (data) {    const that = this;    // 指向vue实例    Object.keys(data).forEach(key => {        Object.defineProperty(that, key, {            configurable: true,            enumerable: true,            get: function proxyGetter () {                return that._data[key];            },            set: function proxySetter (val) {                that._data[key] = val;            }        })    });}

其实就是做了一个劫持,当访问vm.name属性时,返回vm.data.name

数组的特殊处理

JavaScript似乎从很多角度都能被看做一门奇怪的语言,从设计上说,“+”既可以为相加有可以为连接(包括动态类型,这类问题其实导致了一些JavaScript引擎解析上的性能问题)、易错的with语句,从实现来看,早期一直有很多bug……

而数组呢,其实也比较奇怪,在es6之前没有标准明确定义数组空位的问题,而length又是可读写的同时创建数组限制十分宽松,所以各大浏览器也是有很多不同的实现,es6之后对数组空位有了明确的规定。

在vue中,因为数组的宽松规定,在给我们带来一定便利的同时也带来很多问题:
vue中数组操作的注意事项

vue不仅定义了新方法,也重写了vue的一些变异方法(会改变被这些方法调用的原始数组的方法)

// vue version-0.1['push',  'pop',  'shift',  'splice',  'unshift',  'sort',  'reverse'].forEach(function(method){  // cache original method  var original = Array.prototype[method]  // define wrapped method  _.define(arrayAugmentations, method,     function(){      var args = slice.call(arguments)      var result = original.apply(this, args)      var ob = this.$observer      var inserted, removed, index      switch(method){        case 'push':          inserted = args          index = this.length - args.length          break        case 'unshift':          inserted = args          index = 0          break        case 'pop':          removed = [result]          index = this.length          break        case 'shift':          removed = [result]          index = 0          break        case 'splice':          inserted = args.slice(2)          removed = result          index = args[0]          break      }      // link/unlink added/removed elements      if (inserted) {        ob.link(inserted, index)      }      if (removed) {        ob.unlink(removed)      }      //updata indices      if (method !== 'push' && method !== 'pop') {        ob.updataIndecies()      }      // emit length change      if (inserted || removed) {        ob.notify('set', 'length', this.length)      }      // empty path, value is the Array itself      ob.notify('mutate', '', this, {        method: method,        args: args,        result: result,        index: index,        inserted: inserted || [],        removed: removed || []      })      return result    })})

上面是vue 0.1的部分源码,主要是对数组的增、删、改、排序等方法进行重写,将其变为适应vue响应式变化的方法,如删除则通知相应watcher变动,增加则要重新关联依赖等。

运行时编译 or 模板编译

如果用过比较新版的vue-cli的读者可能遇到过如下场景

Runtime + Compiler: recommended for most users Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specificHTML) are ONLY allowed in .vue files - render functions are required elsewhere   

这两者的区别还得从vue内部的编译过程说起,大概分为以下两步:
1. 将template编译为render函数(可选)
2. 创建vue实例,调用render函数

在vue-loader解析过程中,template会被解析成一个对象,也就是说你打包好的代码已经完成了这一步,template在你的代码中是以对象形式呈现的,这跟react有异曲同工之妙(猜测是因为他们都使用virtual DOM,都需要render函数),这个过程称为模板编译,所以如果你不用template,那么模板编译其实就基本没有用武之地了。

而运行时编译则是用来创建 Vue 实例,渲染并处理 virtual DOM 等行为的代码。基本上就是除去编译器的其他一切。同时也是比较建议使用运行时,因为运行时构建相比完整版缩减了 30% 的体积。

其实在vue1中整个编译过程是不作区分的,而在vue2中因为有了两个编译环境—客户端和服务端,而在进行ssr(server side render)时,因为是node环境,所以需要document.dagment等DOM接口支持的编译就不在适用了,所以做出了以上区分。

6. 总结

源码看得比较少,理解的也比较浅显,下一步还是要多多深入,尽可能从整体架构以及设计模式设计思想上来看vue,当然也要学习尤大的编码习惯、风格以及代码组织等。

另外再讲点其他的吧,随着敲代码越多,理解以及感想也就越来越多:

  1. 其实很多比较吊的东西都是程序员懒癌发作的结果,可能每天写着重复的代码,做着重复的工作很崩溃,每次构建这么复杂,然后有了增量构建,每次打开网页这么慢,然后有了各种ssr优化手段。然后这些的方案啊,框架啊就诞生了。每六周换一个框架不是梦,真的是现实。

  2. 底层实现也越来越吊,就像我们维护优化自己的网站等产品一样,Chrome等浏览器厂商也在维护优化自己的产品,其成果比较喜人包括js速度越来越快, 各种跨平台跨端,同时前端的权限也越来越大,比如现在的chorme 61 beta,支持 javascript模块(script标签 type=”module”),桌面支付请求API ,web share API (与app共享内容),webUSB(外设通信)等,另外除了浏览器还有各种工具、编译器(编译为js),总之前端应用场景越来越多,能做的事情越来越多。

  3. 后端的逻辑越来越多的放到前端处理,甚至包括机器学习算法部分,可能主要还是考虑到分担服务端计算压力,提高即时用户体验,比如美登科技,他们也是试验了一套前后端机器学习的整体架构,前端会包括打点、个性化推荐、个性化配置学习等。

  4. 未来展望。这么发展下去还真的很难说未来如何,不过我个人还是很相信前端的前途的,有一种比较著名的说法(记得是阮一峰老师说的)是,未来只有两种工程师,端工程师和云工程师

    就我理解而言,还是两大方向,数据化可视化(平台建设),数据挖掘(这里是自己理解比较广义的)

好了说一千道一万也没用,我们还是努力coding,多总结,多交流,也可以多参与开源。

番外:
上面提到了重写js原有方法,其实一般来说建议尽量避免这么做的~
当然我个人认为坑爹就要坑到底,所以如果真重置了标准方法,那么就多补一刀吧

重写方法后有一个不大不小的漏洞,比如Chrome的控制台再打印这个方法后就不会打印出[native code]字样了(以下注释为当前Chrome console实测结果)

Array.prototype.push = Array.prototype.pop    //ƒ pop() { [native code] }const a = [1,2,3]a.push(4)a    //(2) [1, 2]
Array.prototype.pop.toString = () => 'function push() { [native code] }'Array.prototype.push   // ƒ push() { [native code] }
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 京东第三方店铺显示关闭怎么办 派派怎么提现朋友不够怎么办 派派邀请30个好友才能提现怎么办 派派更换手机号后提现时怎么办 京东白条扫码支付被骗怎么办 实体店买的商品一天后降价怎么办 东西没收到确确认收货了怎么办 工行手机银行转农行卡号错了怎么办 发了后才知道顺丰快递到不了怎么办 三鹰之森吉卜力美术馆没票了怎么办 网贷平台借款如果还找你要钱怎么办 网贷要钱威胁成精神病怎么办 手机清除数据后忘了帐号密码怎么办 拼多多改了标题排名降了怎么办 16g的苹果手机内存不够怎么办 魅族手机没电关机充不进电怎么办 淘宝上买电器售后得不到处理怎么办 苏宁易购物流漏送货已签收怎么办 大件包裹快递快递员不送上楼怎么办 滴滴车主提现忘记登录密码怎么办 荣耀6玩游戏不卡但是闪退怎么办 qq扫码允许别人电脑登录怎么办 药店被举卖假药药检局没查到怎么办 苹果手机连接汽车点了不信任怎么办 装修的化妆品柜台与图纸不合怎么办 买手机邮到了是假手机怎么办 京东白条分期还款第一期逾期怎么办 快递电话留错了在派件怎么办 如果在派件途中客户电话有误怎么办 在派件途中客户电话有误怎么办 成立售电公司后供电公司怎么办 新买的洗衣机外壳坏了怎么办 京东过了7天退货怎么办 同款衣服比京东便宜怎么办 国美不让休班还不给加班钱怎么办 在国美电器买贵商品怎么办 给民俗差评老板骂你怎么办 华为p10后置摄像头调黑了怎么办 美图m6手机相机拍照模糊该怎么办 美图t8用久了卡怎么办 美图m4手机开不开机怎么办