本次串讲的主要目的在于给我们移动端的同学揭秘下目前前端开发的现状,和一些典型框架或者说是库的产生背景、以及设计思想和解决了什么样的问题。以 Vue.js 为例。此次讲解围绕以下几个方面展开:
MV* 框架模式
历史
最早期的 Web 开发是洪荒时代,开发者可能写着类似以下的代码。检查用户的输入合法性,然后提交用户的表单字段到达服务器。服务器再校验一遍用户的合法性
<html>
<head>
<meta charset="UTF-8">
<meta name="description" content="洪荒时代开发Web网页">
<title>洪荒时代</title>
<meta>
</head>
<body>
<form action="http://sdg.com/login" method="POST" οnsubmit="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 开发步入到“火药文明时代”。一些大型应用的场景,页面的数据状态非常多,传统的页面开发方式有了一些问题。
- 比如页面一个报错如果是服务端渲染,那么 error 信息直接显示到页面上。对于用户而言这些 error 信息很懵逼,体验很不好
- error 信息里面有你的服务端信息,比如什么语言,什么框架,什么版本,什么引擎、什么服务器,这些东西对于不怀好心的 Eve 就可以利用现有漏洞去攻击服务器
- 开发维护方式很不友好。假如你的页面有报错信息,你甚至需要前端开发者和服务端开发者一起去排查问题。开发方式就是前端开发者写模版代码,写好之后将代码交给服务端开发者,服务端开发者根据业务,去操作数据库执行 SQL ,再通过类似于 JSP、PHP 这种传统的技术渲染页面。开发效率极低。
后来诞生了 ajax 技术。通过 ajax 提高一个较好的体验( 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。Ajax 在浏览器与 Web 服务器之间使用异步数据传输(HTTP 请求),这样就可使网页从服务器请求少量的信息,而不是整个页面)。有了 ajax 赋能前端开发采用了前后端分离的方案,服务端、前端各司其职。前后端开发者通过接口通信,前端开发者专心做提高用户体验的前端事情,比如写酷炫的动画。传统的服务端渲染的路子走不通了。在此背景下催生了 REST api 。前端开发人员高兴坏了,开发者有了能力去开发大型应用。
再到后来旧版本、性能低、不主动拥抱变化的浏览器逐渐淘汰,体验不好,用户自然不愿意去用,那么就要淘汰。移动智能设备的诞生让传统的 PC 页面开始在移动端进行尝试,发现效果还可以。当用户也越来越挑剔、用户体验的要求也越来越高。那么传统的开发方式也不能满足现在的需求了。用户多了,业务复杂了,那么 MVC 也满足不了现在开发者的要求,于是 MVVM 诞生了。当然前端也在搞工程化。
应用越复杂,现有状况就是数据状态分散在 model 和 view 中。假如Jquery时代经常将数据隐藏在form表单中只不过是隐藏的。比如 <input class="hidden" id="userId" name="userId">
点击按钮更新用户信息的时候经常需要将隐藏的数据也提交掉。在此背景下诞生了最早一批的框架,代表有 Backbone、Ember。
MV* 说明(MVC、MVP、MVVM…)
- 先不讲 MVC 是什么,先谈谈软件设计的一些原则和理念。
- 可靠性:应用的功能可以正常使用
- 健壮性:在用户非正常使用的时候,应用也可以正常反应,不要奔溃
- 效率性:启动时间、响应时间、效率等在用户可以容忍范围之内
以上3点是表象层的东西,大多数开发者或者团队都会注意。除了这三点,还有一些东西是需要在工程层面需要注意的方面。
- 可拓展性:软件不是一次性产品,需要不断的迭代更新
- 容易理解:代码易读、规范
- 可测试性:代码能够方便的编写单元测试和集成测试
- 可复用性:可复用,不需要一次次编写轮子
于是,软件设计领域有了几个通用设计原则帮助我们实现这些目标:单一功能原则、聚合复用原则、接口隔离原则、依赖倒置原则…
基于这些设计目标和理念又有了设计模式:MVC、MVVM 就属于这个范畴。
- MV*
- MVC:Model(模型) + View(视图) + Controller(控制器),主要目的在于分层,各司其职。 View 通过 Controller 来和 Model 联系。Controller 用来管理 View 和 Model。View 将事件传递给 Controller,Controller 完成业务逻辑后要求 Model 改变,Model 将新的数据发送到 View,用户得到反馈。
- MVP:从 MVC 演变而来,都通过 Presenter/Controller 负责逻辑处理,View 负责界面展示,Model 负责数据。在 MVP 中主要逻辑在 Presenter 中。View 与 Model 不发生联系,都通过 Presenter 传递。View 层非常薄,不部署任何业务逻辑,没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。
- MVVM:将 MVP 中的 中,Presenter 变成了 ViewModel,View 的变动会自动同步到 ViewModel,ViewModel 的变化也会同步到 View 上,这种同步的实现是对 ViewModel 中的属性实现了 Observer,当对属性存取会触发 setter 和 getter,都会触发对应的操作。
Vue.js
对于 Vue.js 来说不只是技术的革新也是开发方式的革新。前端框架和移动端框架的差异:前端框架更像是革命性的革新,连开发方式都是天翻地覆的变化。前端里面 MVVM 的思想每个库基本都有实现;移动端的话比较少,几个大厂才有实现方式,但是使用起来感觉并不是很美好。
举个例子:iOS 端的 ReactiveCocoa 使用起来高学习门槛、易出错、调试困难、风格不统一等被诟病。后来美团自研了 EasyReact。它的诞生是为了解决 iOS 工程实现 MVVM 架构但没有对应的框架支撑,而导致的风格不统一、可维护性差、开发效率低等多种问题。而 MVVM 中最重要的一个功能就是绑定,EasyReact 就是为了让绑定和响应式的代码变得 Easy 起来。
什么是 Vue.js
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
MVVM 实现原理
几种实现双向绑定的实现原理。
看看下面的代码
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 支持单双向数据绑定。
实现数据绑定的做法大致有如下几种方式:
- 发布者-订阅者模式:Backbone.js。不去讨论
- 脏值检查:Angular.js。基本通过 DOM 事件、比如用户输入、按钮点击、XHR 响应事件、浏览器 Location 变更事件、Timer、apply 等
- 数据劫持:Vue.js。通过数据劫持结合发布者-订阅者模式实现。
Object.defineProperty()
拦截属性的 setter 和 getter。在数据变动的时候发布消息给订阅者、触发相应的监听回调。
思路整理:
- 实现一个属性监听器 Observer,能够对数据对象的所有属性进行监听,如果有变动则将最新的值通知给订阅者
- 实现一个指令解析 Compiler,对每个元素节点进行扫描和解析,根据指令模版替换数据,以及绑定相应的更新函数
- 实现一个 Wacther,作为连接 Observer 和 Compiler 的桥梁,能够订阅并观察到每个属性的变化通知,执行指令绑定的相应回调,从而更新视图
- MVVM 入口函数,整合Observer、Compiler、Wacther
看几个属性: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 控制是否可以枚举
- 实现 Observer
可以利用 Obeject.defineProperty() 来监听属性变动,将需要 Observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter
给这个对象的某个值赋值就会触发setter,那么就能监听到了数据变化。
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
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
Watcher 订阅者作为 Observer 和 Compile 之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个 update() 方法
3、待属性变动 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
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
的属性值,达到鱼目混珠的效果
- 什么是单向绑定和双向绑定?
单向绑定:将 Model 绑定到 View 上。当我们通过接口或者事件操作 Model 的改变的时候那么 View 的改变会自动触发,View 自动刷新改变。 - 双向绑定:将 Model 绑定到 View 上,通过也将 View 绑定到 Model 上。这样 View 的改变会触发 Model 的改变,Model 的改变也会自动触发 View 的自动更新。
- Vue 中如何实现单项数据绑定?
- 通过插值表达式。通过
{ {data} }
的形式将数据 Model 中的某个属性绑定到 Dom 节点上 - 通过 v-bind 指令。通过
v-bind:class="hasError"
将某个 Model 的属性绑定到对应的属性上。这样 Vue 在识别到 v-bind 指令的时候会自动将属性跟 Model 绑定起来,这样就可以通过 ViewModel 操作 Model 来动态的更新 View 层。
- 通过插值表达式。通过
- Vue 中实现双向绑定
Vue 中通过v-model
实现双向绑定。可以实现 View 到 Model 的双向绑定。View 变动了 Model 会跟着变, Model 变了 View 会自动更新。
Vue 与 React 的对比
先看看以下代码,针对同一个字符串反转的功能,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'));
相似之处:
- 都有非常多的 star,开发者非常拥护
- 都使用 Virtua DOM
- 提供了响应式和组件化的视图组件
- 将注意力放在核心实现,将其他功能比如路由、全局状态管理交给相关的库
差别:
-
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 方法。
有 Vue 基础如何快速上手 Weex
-
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 变化)- 单位
Weex 中所有的 css 属性值单位为 px,也可以省略不写 - Flexbox 支持不完全
align-items: baseline;align-content:space-around;align-self:wrap_revserse;
等 - 显隐性
在 Weex 中的 iOS 和 Android 端不支持display:none;
所以 v-show 条件渲染写法也是不支持的,可以用v-if
代替,或者display:0;
模拟。由于移动端的渲染特点是当 opacity < 0.01 的时候 view 是可以点击穿透,所以 Weex 中当元素 display < 0.01 的时候元素看不见,但是占位空间还在,但用户无法与之交互,同样点击时会发生穿透的效果。 - css3
相比 React Native 不能用 css3,Weex 的 css3 的支持程度算比较高,但是有一些 css3 的属性还是不支持的。transform 支持 2D;font-family 支持 ttf 和 woff 字体格式的自定义的字体;liner-gradient 只支持双色渐变
- 单位
-
调试方式
如果说 React Native 的调试方式解放了原生开发调试、那么 Weex 就是赋予了 web 模式调试原生应用的能力。
-