本次串讲的主要目的在于给咱们移动端的同窗揭秘下目前前端开发的现状,和一些典型框架或者说是库的产生背景、以及设计思想和解决了什么样的问题。以 Vue.js 为例。这次讲解围绕如下几个方面展开:javascript
最先期的 Web 开发是洪荒时代,开发者可能写着相似如下的代码。检查用户的输入合法性,而后提交用户的表单字段到达服务器。服务器再校验一遍用户的合法性php
<html> <head> <meta charset="UTF-8"> <meta name="description" content="洪荒时代开发Web网页"> <title>洪荒时代</title> <meta> </head> <body> <form action="http://sdg.com/login" method="POST" onsubmit="return validate();"> <label for="username">用户名</label> <input type="text" name="username" id="username" placeholder="请输入用户名"> <label for="password">密码</label> <input type="password" name="password" id="password" placeholder="请输入密码"> <input type="submit"> </form> </body> <script> /* * 判断字符串是否为空 */ function isNotEmptyStr($str) { if($str == "" || $str == undefined || $str == null || $str == "null") { return false; } return true; } function validate () { var username = document.getElementById("username").value; var password = document.getElementById("password").value; if (!isNotEmptyStr(username)) { alert("请输入用户名"); return false; } if (!isNotEmptyStr(password)) { alert("请输入密码"); return false; } } </script> </html>
$username = addslashes($_REQUEST['username']); $password = md5($_REQUEST['password']); //数据表 $table = "user"; //3.获得链接对象 $PdoMySQL = new PdoMySQL(); if ($action == "login") { $salt = "CRO"; $identidier = md5($salt.md5($username.$salt)); $token = md5(uniqid(rand(),true)); $time = time()+60*60*24*7; $currentime = time(); $allrow = $PdoMySQL->find($table,"username='{$username}' and password='{$password}'"); $PdoMySQL->update(["time"=>$time,"identifier"=>$identidier],$table,"username='{$username}' and password='{$password}'"); $autoRows = $PdoMySQL->find($table,"username='".$username."' and identifier='".$userid."'"); if(count($autoRows) == 1){ if($currentime < $autoRows[0]["time"]){ setcookie('auth',base64_encode($autoRows[0]["id"])); // 跳转到主页 }else{ // 给出用户信息失败的提示 alert } } }
再到后来 Javascript 技术的发展愈来愈完善,网页开发有了更复杂的 JS 动画、CSS的特性也愈来愈强,让洪荒时代的 web 开发步入到“火药文明时代”。一些大型应用的场景,页面的数据状态很是多,传统的页面开发方式有了一些问题。css
后来诞生了 ajax 技术。经过 ajax 提升一个较好的体验( 是一种在无需从新加载整个网页的状况下,可以更新部分网页的技术。Ajax 在浏览器与 Web 服务器之间使用异步数据传输(HTTP 请求),这样就可以使网页从服务器请求少许的信息,而不是整个页面)。有了 ajax 赋能前端开发采用了先后端分离的方案,服务端、前端各司其职。先后端开发者经过接口通讯,前端开发者专心作提升用户体验的前端事情,好比写酷炫的动画。传统的服务端渲染的路子走不通了。在此背景下催生了 REST api 。前端开发人员高兴坏了,开发者有了能力去开发大型应用。html
再到后来旧版本、性能低、不主动拥抱变化的浏览器逐渐淘汰,体验很差,用户天然不肯意去用,那么就要淘汰。移动智能设备的诞生让传统的 PC 页面开始在移动端进行尝试,发现效果还能够。当用户也愈来愈挑剔、用户体验的要求也愈来愈高。那么传统的开发方式也不能知足如今的需求了。用户多了,业务复杂了,那么 MVC 也知足不了如今开发者的要求,因而 MVVM 诞生了。固然前端也在搞工程化。前端
应用越复杂,现有情况就是数据状态分散在 model 和 view 中。假如Jquery时代常常将数据隐藏在form表单中只不过是隐藏的。好比 <input class="hidden" id="userId" name="userId">
点击按钮更新用户信息的时候常常须要将隐藏的数据也提交掉。在此背景下诞生了最先一批的框架,表明有 Backbone、Ember。vue
以上3点是表象层的东西,大多数开发者或者团队都会注意。除了这三点,还有一些东西是须要在工程层面须要注意的方面。java
- 可拓展性:软件不是一次性产品,须要不断的迭代更新 - 容易理解:代码易读、规范 - 可测试性:代码可以方便的编写单元测试和集成测试 - 可复用性:可复用,不须要一次次编写轮子
因而,软件设计领域有了几个通用设计原则帮助咱们实现这些目标:单一功能原则、聚合复用原则、接口隔离原则、依赖倒置原则...node
基于这些设计目标和理念又有了设计模式:MVC、MVVM 就属于这个范畴。react
对于 Vue.js 来讲不仅是技术的革新也是开发方式的革新。前端框架和移动端框架的差别:前端框架更像是革命性的革新,连开发方式都是天翻地覆的变化。前端里面 MVVM 的思想每一个库基本都有实现;移动端的话比较少,几个大厂才有实现方式,可是使用起来感受并非很美好。 举个例子:iOS 端的 ReactiveCocoa 使用起来高学习门槛、易出错、调试困难、风格不统一等被诟病。后来美团自研了 EasyReact。它的诞生是为了解决 iOS 工程实现 MVVM 架构但没有对应的框架支撑,而致使的风格不统1、可维护性差、开发效率低等多种问题。而 MVVM 中最重要的一个功能就是绑定,EasyReact 就是为了让绑定和响应式的代码变得 Easy 起来。android
Vue (读音 /vjuː/,相似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不一样的是,Vue 被设计为能够自底向上逐层应用。Vue 的核心库只关注视图层,不只易于上手,还便于与第三方库或既有项目整合。另外一方面,当与现代化的工具链以及各类支持类库结合使用时,Vue 也彻底可以为复杂的单页应用提供驱动。
在我看来 Vue.js 的核心思想就是「数据驱动、组件化开发、虚拟Dom」。固然结合它的脚手架让你开发一个复杂且良好的大型应用变得很容易。下面看一个 Demo 来讲明下 Vue.js 的强大威力。
<html> <head> <title>Vue</title> <style> div{ margin: 50px; } input { border: 1px solid cyan; height: 30px; line-height: 30px; } p { font-size: 30px; } </style> </head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <body> <div id="el"> <input type="text" v-model="username"> <p>{{username}}</p> <ul> <li v-for="(el,index) in hobby" :id="index">{{el.msg}}</li> </ul> </div> </body> <script> var vm = new Vue({ el: '#el', data () { return { username: '@杭城小刘', hobby: [ {msg: '电影'}, {msg: '美食'}, {msg: '旅游'}, {msg: '乒乓球'}, {msg: '编程'} ] } } }) </script> </html>
在上面的代码中就声明了一个 MVVM 框架的 Web 应用,怎么体现?你能够在打开 Chrome 的调试界面,快捷键为 Command + Option + i
,你能够在 console 中输入如下指令,能够看到界面会自动更新
vm.$data.username = '刘斌鹏' vm.username = '刘斌鹏' vm._data.username = '刘斌鹏' vm.$data.hobby.push({msg: '探索本质'}) vm.$data.hobby.pop() vm.$data.hobby.shift()
为何呢?底层实现原理是经过 new Vue({})
声明了一个 MVVM 对象,绑定的 View 经过 el 获取到,数据就是原生的 Javascript 对象,这个 ViewModel 将 View 和 Model 绑定在一块儿, View 和 Model 不直接联系,可是 v-model="username"
是个什么鬼? v-model
是 Vue.js 中的一个指令,底层实现就是 Vue.js 将该 input 的值和 Model 中的 username 进行了绑定,代码以下
<input v-bind:value="username" v-on:input="sth=$event.target.value">
咱们经过 ViewModel 操纵的是 Model 当 Model 中的数据改变,假如经过 vm.$data.username
就会触发属性的 getter,若是经过 vm.$data.username = '刘斌鹏'
访问的就是属性的 setter,Vue 观察到属性变化会自动操做 View 的响应式变化。
npm npm实际上是Node.js的包管理工具(package manager)。开发时,会用到不少别人写的JavaScript代码。若是咱们要使用别人写的某个包,每次都根据名称搜索一下官方网站,下载代码,解压,再使用,很是繁琐。因而一个集中管理的工具应运而生:你们都把本身开发的模块打包后放到 npm 官网上,若是要使用,直接经过npm安装就能够直接用,不用管代码存在哪,应该从哪下载。更重要的是,若是咱们要使用模块A,而模块A又依赖于模块B,模块B又依赖于模块X和模块Y,npm能够根据依赖关系,把全部依赖的包都下载下来并管理起来。不然,靠咱们本身手动管理,确定既麻烦又容易出错。
AMD、CommonJS、CMD 等规范
CommonJS 规范 因为为了编写大型应用程序,代码不可能编写在一个文件里,因此代码(函数、变量)分散在多个文件里面,每一个应用程序都有相应的解决方案,在 Node 中就是“模块”。模块的好处也是不言而喻的,当你编写好某个功能拓展的时候能够很方便的集成到其余的模块中去引用。那么 Node 如何实现模块?因为 Javascript 是函数式编程语言,因此能够利用闭包实现。将咱们的代码用闭包实现起来就能够实现将“变量”只在当前代码内有效,外部没法访问,实现了模块的隔离。因此咱们能够将须要暴露出去的东西暴露给外部,这样子就能够组织大型应用程序的开发
模拟 CommonJS 的实现
// 准备module对象: var module = { id: 'hello', exports: {} }; var load = function (module) { // 读取的hello.js代码: function greet(name) { console.log('Hello, ' + name + '!'); } module.exports = greet; // hello.js代码结束 return module.exports; }; var exported = load(module); // 保存module: save(module, exported);
上述代码就能够实现将所须要的东西实现模块。CommonJS 规范使用步骤:1. 编写代码逻辑,经过 module.export = 变量;
暴露给外部;2. 调用者经过 let 变量名 = require('模块名')
来导入所须要的模块,用一个变量去承接,而后访问属性和方法 2.因为 CommonJS 中的规范针对于 Node 很适合,由于代码文件是放在服务端磁盘,因此是同步的,读取速度很快,代码同步执行没问题。可是要在浏览器端使用这套规范显然是行不通的。为何?看看下面代码有什么问题?
let Hello = require('./Hello'); Hello.sayHi()
用户访问页面后卡死了?由于浏览器的环境下代码资源都须要经过网络获取,因此会比较慢,若是是同步用户访问的话基本上不会去第二次访问你的网站了。在此背景下产生了针对浏览器环境下的模块问题的 AMD 规范(Asynchronous Module Definition),想想若是是你的话如何设计?采用异步加载的方式,模块的加载不影响后续代码的执行,若是遇到的代码是依赖于模块,那么这些代码都会被放到一个回调函数中,等模块加载完毕才会去执行回调函数里面的内容。AMD 也采用 require()
语句,不一样于 CommonJS 它要求2个参数。
reuqire([module], callback)
说明:第一个参数是一个数组,里面是要加载模块;第二个参数 callback 是加载成功的回调函数。好比
require(['./Hello'], () => { Hello.sayHi() })
Webpack 查看之前的文章 Webpack、webpack-dev-server
ES6 几个概念:ES、JS、CoffeeScript、TypeScript ES(ECMAScript):标准 JS:浏览器对其的实现 CoffeeScript:能够编译为 Javascript,抛弃 JS 中一些很差的设计 TypeScript 是现今对 JavaScript 的改进中,惟一彻底兼容并做为它的超集存在的解决方案
Flexbox 传统布局解决方案好比盒模型在实现一些效果的时候不是很方便,因此 W3C 在2009年提出了 Flex 布局系统。 Flex参考资料
html、CSS MDN
- 看着 [Vue官方文档](https://cn.vuejs.org/v2/guide/) 边看边写,由于在你 coding 的时候是拿着键盘写代码的,也须要感受,因此平时多敲代码,边思考 - 对于没有接触过 ES6 和 Webpack 的童鞋来讲,不建议直接用官方的脚手架 **vue-cli** 构件项目。因此先花点时间去学习下 ES6 的威力和 Webpack 解决了什么样的问题和它的简单用法 - 了解下 npm 的概念和解决了什么样的问题 - 一些 CSS 的知识 - 等适应了 Vue-cli 和工程构建方式以及代码组织方式后能够看看 Vue-Router、Vuex - Vue-Router、Vuex 应用到工程项目中去,作一个 TodoList 项目 - 项目结束复盘、review 下 - [项目 Vue 小结](./2.17.md)
- [Vue 代码风格指南](https://cn.vuejs.org/v2/style-guide/#避免-v-if-和-v-for-用在一块儿-必要) - ES6 吃透(万变不离其宗,不要一昧追求新技术,掌握本质核心) - 封装高阶组件(slot 等技术点) - 设计优秀良好的组件(好比用 TS 书写代码类型更为安全) - 封装公司或者业务线或者产品为核心点的组件库 - 关注代码实现原理 - 关注前端的技术社区:[segmentfault](https://segmentfault.com)... - 思考 Vue 框架设计的思想。类比其余框架甚至是大前端如何实现或者有没有相似的问题 - 尝试找到应用的性能症结所在,分析问题,给出解决方案并优化 - 参加行业的大会。VueConf、ReactConf
看看下面的代码
var Book = {}; var name = ''; Object.defineProperty(Book, 'name', { set: function (value) { name = value; console.log('本书名称叫作:' + value); }, get: function () { return '<' + name + '>'; } }); Book.name = 'Vue.js 权威指北' console.log(`我买了本书叫作${Book.name}`);
发现打印出来的东西和 Vue console 中输出基本一直,因此猜测 Vue 的实现也是依赖 Object.defineProperty
目前主流的框架基本都实现了单向数据绑定,在我看来双向数据绑定无非就是在单项数据绑定的基础上实现了给可输入元素(input、textarea)添加了 change(input)事件来动态修改 Model 和 View,因此咱们的注意力不须要注意双向仍是单向数据绑定。Vue 支持单双向数据绑定。
实现数据绑定的作法大体有以下几种方式:
Object.defineProperty()
拦截属性的 setter 和 getter。在数据变更的时候发布消息给订阅者、触发相应的监听回调。思路整理:
看几个属性:Object.defineProperty 中的 writable 和 configurable 和 enumerable 的理解 configurable 若是为 false 则不能够修改, 不能够删除。writable 若是设置为 false 则不能够采用数据运算符进行赋值 作个实验看看特殊状况。若是 writable 为 true 的时候, configurable 为 false 结果如何?
var o = {}; // 建立一个新对象 Object.defineProperty(o, "a", { value : "original", writable : false, // 这个地方为 false enumerable : true, configurable : true }); o.a = 'LBP'; console.log(o.a) // "original" 此时候, 是更改不了 a 的. var o = {}; // 建立一个新对象 Object.defineProperty(o, "a", { value : "original", writable : true, enumerable : true, configurable : false //这里为false }); o.a = "LBP"; console.log(o.a) //LBP.此时候, a 进行了改变 delete o.a // 返回 false
结论:onfigurable 控制是否能够删除; writable 控制是否能够修改(赋值); enumerable 控制是否能够枚举
var data = {name: '杭城小刘'}; observe(data); data.name = 'LBP'; function observe(data) { if (!data || typeof data !== 'object') { return; } // 取出全部属性遍历 Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); }); }; function defineReactive(data, key, val) { observe(val); // 监听子属性 Object.defineProperty(data, key, { enumerable: true, // 可枚举 configurable: false, //不能再delete get: function() { return val; }, set: function(newVal) { console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal); val = newVal; } }); }
这样咱们已经能够监听每一个数据的变化了,那么监听到变化以后就是怎么通知订阅者了,因此接下来咱们须要实现一个消息订阅器,很简单,维护一个数组,用来收集订阅者,数据变更触发 notify,再调用订阅者的 update 方法,代码改善以后是这样:
// ... function defineReactive(data, key, val) { var dep = new Dep(); observe(val); // 监听子属性 Object.defineProperty(data, key, { // ... set: function(newVal) { if (val === newVal) return; console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal); val = newVal; dep.notify(); // 通知全部订阅者 } }); } function Dep() { this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach(function(sub) { sub.update(); }); } };
那么问题来了,谁是订阅者?怎么往订阅器添加订阅者? 没错,上面的思路整理中咱们已经明确订阅者应该是 Watcher, 并且 var dep = new Dep()
是在 defineReactive
方法内部定义的,因此想经过 dep
添加订阅者,就必需要在闭包内操做,因此咱们能够在 getter
里面动手脚:
// Observer.js // ... Object.defineProperty(data, key, { get: function() { // 因为须要在闭包内添加watcher,因此经过Dep定义一个全局target属性,暂存watcher, 添加完移除 Dep.target && dep.addDep(Dep.target); return val; } // ... }); // Watcher.js Watcher.prototype = { get: function(key) { Dep.target = this; this.value = data[key]; // 这里会触发属性的getter,从而添加订阅者 Dep.target = null; } }
这里已经实现了一个 Observer 了,已经具有了监听数据和数据变化通知订阅者的功能
compile 主要作的事情是解析模板指令,将模板中的变量替换成数据,而后初始化渲染页面视图,并将每一个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变更,收到通知,更新视图,如图所示:
由于遍历解析的过程有屡次操做dom节点,为提升性能和效率,会先将根节点 el
转换成文档碎片 fragment
进行解析编译操做,解析完成,再将 fragment
添加回原来的真实dom节点中
function Compile(el) { this.$el = this.isElementNode(el) ? el : document.querySelector(el); if (this.$el) { this.$fragment = this.node2Fragment(this.$el); this.init(); this.$el.appendChild(this.$fragment); } } Compile.prototype = { init: function() { this.compileElement(this.$fragment); }, node2Fragment: function(el) { var fragment = document.createDocumentFragment(), child; // 将原生节点拷贝到fragment while (child = el.firstChild) { fragment.appendChild(child); } return fragment; } };
compileElement 方法将遍历全部节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定,详看代码及注释说明:
Compile.prototype = { // ... compileElement: function(el) { var childNodes = el.childNodes, me = this; [].slice.call(childNodes).forEach(function(node) { var text = node.textContent; var reg = /\{\{(.*)\}\}/; // 表达式文本 // 按元素节点方式编译 if (me.isElementNode(node)) { me.compile(node); } else if (me.isTextNode(node) && reg.test(text)) { me.compileText(node, RegExp.$1); } // 遍历编译子节点 if (node.childNodes && node.childNodes.length) { me.compileElement(node); } }); }, compile: function(node) { var nodeAttrs = node.attributes, me = this; [].slice.call(nodeAttrs).forEach(function(attr) { // 规定:指令以 v-xxx 命名 // 如 <span v-text="content"></span> 中指令为 v-text var attrName = attr.name; // v-text if (me.isDirective(attrName)) { var exp = attr.value; // content var dir = attrName.substring(2); // text if (me.isEventDirective(dir)) { // 事件指令, 如 v-on:click compileUtil.eventHandler(node, me.$vm, exp, dir); } else { // 普通指令 compileUtil[dir] && compileUtil[dir](node, me.$vm, exp); } } }); } }; // 指令处理集合 var compileUtil = { text: function(node, vm, exp) { this.bind(node, vm, exp, 'text'); }, // ... bind: function(node, vm, exp, dir) { var updaterFn = updater[dir + 'Updater']; // 第一次初始化视图 updaterFn && updaterFn(node, vm[exp]); // 实例化订阅者,此操做会在对应的属性消息订阅器中添加了该订阅者watcher new Watcher(vm, exp, function(value, oldValue) { // 一旦属性值有变化,会收到通知执行此更新函数,更新视图 updaterFn && updaterFn(node, value, oldValue); }); } }; // 更新函数 var updater = { textUpdater: function(node, value) { node.textContent = typeof value == 'undefined' ? '' : value; } // ... };
这里经过递归遍历保证了每一个节点及子节点都会解析编译到,包括了{{}}表达式声明的文本节点。指令的声明规定是经过特定前缀的节点属性来标记,如 <span v-text="content" other-attr
中 v-text
即是指令,而 other-attr
不是指令,只是普通的属性。 监听数据、绑定更新函数的处理是在compileUtil.bind()
这个方法中,经过 new Watcher()
添加回调来接收数据变化的通知
Watcher 订阅者做为 Observer 和 Compile 之间通讯的桥梁,主要作的事情是: 一、在自身实例化时往属性订阅器(dep)里面添加本身 二、自身必须有一个 update() 方法 三、待属性变更 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调,则功成身退。
function Watcher(vm, exp, cb) { this.cb = cb; this.vm = vm; this.exp = exp; // 此处为了触发属性的getter,从而在dep添加本身,结合Observer更易理解 this.value = this.get(); } Watcher.prototype = { update: function() { this.run(); // 属性值变化收到通知 }, run: function() { var value = this.get(); // 取到最新值 var oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调,更新视图 } }, get: function() { Dep.target = this; // 将当前订阅者指向本身 var value = this.vm[exp]; // 触发getter,添加本身到属性订阅器中 Dep.target = null; // 添加完毕,重置 return value; } }; // 这里再次列出Observer和Dep,方便理解 Object.defineProperty(data, key, { get: function() { // 因为须要在闭包内添加watcher,因此能够在Dep定义一个全局target属性,暂存watcher, 添加完移除 Dep.target && dep.addDep(Dep.target); return val; } // ... }); Dep.prototype = { notify: function() { this.subs.forEach(function(sub) { sub.update(); // 调用订阅者的update方法,通知变化 }); } };
实例化 Watcher
的时候,调用 get()
方法,经过 Dep.target = watcherInstance
标记订阅者是当前watcher实例,强行触发属性定义的 getter
方法,getter
方法执行的时候,就会在属性的订阅器 dep
添加当前 watcher 实例,从而在属性值有变化的时候,watcherInstance 就能收到更新通知。
MVVM 做为数据绑定的入口,整合 Observer、Compile、Watcher 三者,经过 Observer 来监听本身的 Model 数据变化,经过Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通讯桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 Model 变动的双向绑定效果。
一个简单的 MVVM 构造器是这样子:
function MVVM(options) { this.$options = options; var data = this._data = this.$options.data; observe(data, this); this.$compile = new Compile(options.el || document.body, this) }
可是这里有个问题,从代码中可看出监听的数据对象是 options.data,每次须要更新视图,则必须经过 var vm = new MVVM({data:{name: '杭城小刘'}}); vm._data.name = 'LBP';
这样的方式来改变数据。
显然不符合咱们一开始的指望,咱们所指望的调用方式应该是这样的: var vm = new MVVM({data: {name: '杭城小刘'}}); vm.name = 'LBP';
因此这里须要给 MVVM 实例添加一个属性代理的方法,使访问 vm 的属性代理为访问 vm._data 的属性,改造后的代码以下:
function MVVM(options) { this.$options = options; var data = this._data = this.$options.data, me = this; // 属性代理,实现 vm.xxx -> vm._data.xxx Object.keys(data).forEach(function(key) { me._proxy(key); }); observe(data, this); this.$compile = new Compile(options.el || document.body, this) } MVVM.prototype = { _proxy: function(key) { var me = this; Object.defineProperty(me, key, { configurable: false, enumerable: true, get: function proxyGetter() { return me._data[key]; }, set: function proxySetter(newVal) { me._data[key] = newVal; } }); } };
这里主要仍是利用了 Object.defineProperty()
这个方法来劫持了 vm 实例对象的属性的读写权,使读写 vm 实例的属性转成读写了 vm._data
的属性值,达到鱼目混珠的效果
{{data}}
的形式将数据 Model 中的某个属性绑定到 Dom 节点上v-bind:class="hasError"
将某个 Model 的属性绑定到对应的属性上。这样 Vue 在识别到 v-bind 指令的时候会自动将属性跟 Model 绑定起来,这样就能够经过 ViewModel 操做 Model 来动态的更新 View 层。v-model
实现双向绑定。能够实现 View 到 Model 的双向绑定。View 变更了 Model 会跟着变, Model 变了 View 会自动更新。先看看如下代码,针对同一个字符串反转的功能,2个库如何实现
<div id="app"> <p>{{ message }}</p> <button v-on:click="reverseMessage">Reverse Message</button> </div> new Vue({ el: '#app', data: { message: 'Hello Vue.js! }, methods: { reverseMessage: function () { this.message = this.message.split('').reverse().join(''); } } });
class App extends React.Component { constructor(props) { super(props); this.state = { message: 'Hello React.js!' }; } reverseMessage() { this.setState({ message: this.state.message.split('').reverse().join('') }); } render() { return ( <div> <p>{this.state.message}</p> <button onClick={() => this.reverseMessage()}> Reverse Message </button> </div> ) } } ReactDOM.render(App, document.getElementById('app'));
类似之处:
差异:
React 严格上只针对 MVC 的 view 层,Vue 则是 MVVM 模式
数据绑定: Vue 实现了数据的双向绑定,React 数据流动是单向的 单向数据流是指数据的流向只能由父组件经过props将数据传递给子组件,不能由子组件向父组件传递数据,要想实现数据的双向绑定,只能由子组件接收父组件props传过来的方法去改变父组件的数据,而不是直接将子组件的数据传递给父组件。 单向数据量组件props是父级往下传递,你不能向上去修改父组件的数据,而且也不能在自身组件中修改props的值。React不算mvvm,虽然能够实现双向绑定,在React中实现双向绑定经过state属性,但若是将state绑定到视图中时,直接修改state属性是不可的,须要经过调用setState去触发更新视图,反过来视图若是要更新也须要监听视图变化 而后调用setState去同步state状态。标准MVVM应该属于给视图绑定数据后,操做数据便是更新视图
virtual DOM 不同,Vue 会跟踪每个组件的依赖关系,不须要从新渲染整个组件树.而对于 React 而言,每当应用的状态被改变时,所有组件都会从新渲染,因此 React 中会须要 shouldComponentUpdate 这个生命周期函数方法来进行控制
组件写法不同, React推荐的作法是 JSX + inline style, 也就是把 HTML 和 CSS 全都写进 JavaScript 中,即 'all in js'; Vue 推荐的作法是 webpack+vue-loader 的单文件组件格式,即 html,css,JS 写在同一个文件
代码书写方式 使用 Vue 你能够很方便的将现有的工程迁移或者接入 Vue,由于工程现有的 HTML 就是 Vue 中的视图模版,你只须要作一些 Webpack 配置化的东西,代码改动成本低,后期不用 Vue 了你更换框架的成本也比较低。 可是使用 React 你若是须要对现有工程接入的话成本很高,你甚至是重写代码,代码组织方式,工程处理方式基本也改变了。开发者可能须要适应一段时间,门槛稍高。
运行时性能 在 React 中当某个组件的状态发生变化的时候,它会以该组件为根,将全部的子组件树进行更新。对于若是知道不须要更新的组件可能须要使用 PureComponent
或者手动实现 shouldComponentUpdate
方法。Vue 中不须要额外注意这些事情,默认实现的。使得开发者专心作业务开发。 Vue.js使用基于依赖追踪的观察而且使用异步队列更新。轻量,高性能
开发方式 在 React 中组件的渲染功能都依赖于 JSX(Javascript的一种语法糖,尽管这种方式对于 Javascript 来讲很爽,可是对于已有业务进行重构是很麻烦的,为何?你须要将你页面的东西拆分为组件,可是在 React 中组件的输出是靠 render
函数,render 函数内部不能直接写 HTML,而是须要 JSX 语法糖。 Vue.js 在这方面就比较友好,对于已经有的项目能够低成本的接入,由于已有的 HTML 代码就是模版代码,而后将业务写入到 Script 标签,操做 ViewModel。虽然 Vue.js 的组件也支持 JSX 的方法来写代码,为的就是让 React 开发者很快上手。
import React, { Component } from 'react'; import { Image, ScrollView, Text } from 'react-native'; class AwkwardScrollingImageWithText extends Component { render() { return ( <ScrollView> <Image source={{uri: 'https://i.chzbgr.com/full/7345954048/h7E2C65F9/'}} style={{width: 320, height:180}} /> <Text> 在iOS上,React Native的ScrollView组件封装的是原生的UIScrollView。 在Android上,封装的则是原生的ScrollView。 在iOS上,React Native的Image组件封装的是原生的UIImageView。 在Android上,封装的则是原生的ImageView。 React Native封装了这些基础的原生组件,使你在获得媲美原生应用性能的同时,还能受益于React优雅的架构设计。 </Text> </ScrollView> ); } }
组件做用域内的 CSS React 中的 css 是经过 css-in-JS 来实现的,和传统书写 CSS 是有区别的,不是无缝对接的, Vue 中的 css 编写和传统的开发是一致的,你能够在 .vue 文件中对标签添加 scoped 属性来告诉 css-loader 这些 css 规则只在该模块内有效。
<style scoped> @media (min-width: 250px) { .list-container:hover { background: orange; } } </style>
这个属性的做用就是会自动添加一个属性,为组件内的 css 指定做用域,编译成 .list-container[data-v-21e5b78]:hover
向上拓展 React 和 Vue 都提供路由、全局状态管理的解决方案,区别在于 Vue 是官方维护的,React 则是社区维护的。(Vuex、Redux、Vue-Router) 都有脚手架,Vue-cli 容许你自定义一些设备而 React 不支持。
向下拓展 React 学习曲线比较陡峭、也能够说对现有的工程改造门槛较高,须要大范围改写,相比 Vue 则较为友善点,侵入性低。能够像 jQuery 同样引入一个核心的 min.js 文件就能够改造接入现有工程。
原生渲染 React 有 React Native 一个较为成熟的方案,Vue 则有阿里的 Weex。差异在于你写了 React Native 应用则不能在浏览器运行,而 Weex 能够在浏览器中和移动设备上运行。所谓多端运行的能力,
开发缺点 Vue 中不能检测到属性的添加、删除,因此能够用相似 React 中的 set 方法。
quick demo
虽然都是采用 Vue.js 开发,可是存在 Weex 与平台的差别:上下文、DOM、样式、事件(Weex 不支持事件冒泡和捕获)、样式(Weex支持单个类选择器、而且只支持 CSS 规则的子集)、Vue 网页端的一些配置、钩子、在 Weex 中不支持
html 标签 目前 Weex 支持了基本容器(div)、文本(text)、图片(image)、视频(video)等组件,可是须要注意是组件而不是标签,虽然写起来跟标签同样很像,可是写其余的标签必须和这些组合起来使用。类比 Native 的视图层级
Weex 中不存在 Dom Weex 解析 Vue 获得的不是 dom,而是原生布局树
支持有限的事件 由于在移动端中全部有些网页端的事件是不支持的,请查看支持的事件列表
没有 BOM,但能够调用原生 Api DOM?BOM? javascript组成:ECMAScript 基本语法;BOM(Borwser Object Model:浏览器对象模型,使用对象模拟了浏览器的各个部份内容);DOM(Document Object Model:文档对象模型:浏览器加载显示网页的时候浏览器会为每一个标签都建立一个对应的对象描述该标签的全部信息)
在 Weex 中可以调用原生设备的 api,使用方法是经过注册、调用模块来实现的,其中一些模块是 Weex 内置的,好比 clipboard、navigator、storage 等。为了保持框架的通用性,Weex 内置的原生模块颇有限,不过 Weex 提供了横向拓展的能力,能够拓展原生模块。具体参考 Androi 拓展、iOS 拓展
样式差别 Weex 中的样式是由原生渲染器解析的,出于性能和功能复杂角度的考虑,Weex 对于 css 特性作了一些取舍。(Weex 中只支持单个类名选择器,不支持关系选择器、也不知支持属性选择器;组件级别的做用域,为了保持 Web 和 Native 的一致性,须要使用 style scoped 的写法;支持基本的盒模型和 flexbox 的写法,box-sizing 默认为 border-box,margin,padding,border 属性不支持合并简写;不支持 display:none;能够用 display: 0; 代替,display < 0.01 的时候能够点击穿透;样式属性不支持简写、提升解析效率;css 不支持 3D 变化)
align-items: baseline;align-content:space-around;align-self:wrap_revserse;
等display:none;
因此 v-show 条件渲染写法也是不支持的,能够用 v-if
代替,或者 display:0;
模拟。因为移动端的渲染特色是当 opacity < 0.01 的时候 view 是能够点击穿透,因此 Weex 中当元素 display < 0.01 的时候元素看不见,可是占位空间还在,但用户没法与之交互,一样点击时会发生穿透的效果。调试方式 若是说 React Native 的调试方式解放了原生开发调试、那么 Weex 就是赋予了 web 模式调试原生应用的能力。