Open WangShuXian6 opened 5 years ago
https://www.zhihu.com/question/267797409
无框架的年代
我们需要开发这样的一个功能,点击+号按钮,上面的数字就加1
<div class="wrap"> <div class="text">0</div> <button class='plus'> + </button> </div>
//document.querySelector,其实就是DOM的api,选择一个元素
const wrap = document.querySelector(".wrap"); const plus = wrap.querySelector(".plus"); const text = wrap.querySelector(".text");
let number = 0; //dom api,给元素添加一个监听器 plus.addEventListener( "click", function() { number++; text.innerHTML = number; }, false );
>上古时期的开发们大约就是这样去书写代码,因为功能非常的简单,所以代码量也很少,思路很清晰。
>随着页面的元素越来越多,你这个「点击加1按钮」的功能可能被复用,
>那么你现在用一个最快的办法对其进行复用:复制HTML->复制JS代码->粘贴HTML和JS代码
>这种方法是很挫的.....
>于是为了简单化我们的流程,我们想出了一个聪明的办法去复用
***
#### 1.1 看似聪明的办法
```ts
class Add {
render() {
return `
<div class="text">0</div>
<button class='plus'>
+
</button>`;
}
}
const wrap = document.querySelector(".wrap");
const add = new Add();
wrap.innerHTML = add.render();
const plus = wrap.querySelector(".plus");
const text = wrap.querySelector(".text");
let number = 0;
plus.addEventListener(
"click",
function() {
number++;
text.innerHTML = number;
},
false
);
代码稍微的一改,我们用一个class Add代替了HTML,当调用这个类的render函数的时候,就会返回一段字符串,这段字符串就是我们想要的组件。
由此,我们的组件复用方法变成了:复制JS代码->粘贴JS代码
这么一来啊,我们就简化了我们组件复用的流程。但是我们可以看到,我们手动操作DOM的次数太多了,而且逻辑非常的重复,因此,我们把这部分逻辑抽象一下,使得我们组件复用更加简单。
1.2 更加简单的组件复用
class Add { createWrapper(string) { //document.createElement, DOM api,根据标签名字创建DOM元素 const wrapper = document.createElement("div"); wrapper.innerHTML = string; return wrapper; }
render() { const domString = `
<button class='plus'>
+
</button>`;
//生成DOM元素
const wrapper = this.createWrapper(domString);
//DOM中查找元素
const plus = wrapper.querySelector(".plus");
const text = wrapper.querySelector(".text");
let number = 0;
plus.addEventListener(
"click",
function() {
number++;
text.innerHTML = number;
},
false
);
return wrapper;
} }
const wrap = document.querySelector(".wrap"); const add = new Add(); wrap.appendChild(add.render());
>现在我们的这个组件复用就非常的简单了,
>我们只需要将class Add 通过export的方法导出以后,
>你可以在任何地方使用几行代码,就可以使用他,
>我们复用我们的组件就变成了:复制三行JS代码->粘贴三行JS代码
```ts
const wrap = document.querySelector(".wrap");
const add = new Add();
wrap.appendChild(add.render());
什么是数据驱动呢?简单的来说,就是「数据是什么,我们就展示什么」。 回顾我们之前的组件内部
//生成DOM元素 const wrapper = this.createWrapper(domString); //DOM中查找元素 const plus = wrapper.querySelector(".plus"); const text = wrapper.querySelector(".text");
let number = 0; plus.addEventListener( "click", function() { number++; text.innerHTML = number; }, false );
>我们在其中大量的使用了,querySelector这种api,使得我们需要各种手动的去操作DOM元素,
>如果我们的组件变成
```html
<div class="wrap">
<div class="text">0</div>
<div class="text_2">0</div>
<div class="text_3">0</div>
<button class='plus'>
+
</button>
</div>
然后我需要点一下按钮,三个标签都得变化, 那我们的代码很可能就需要对每一个text进行选择:
//生成DOM元素 const wrapper = this.createWrapper(domString); //DOM中查找元素 const plus = wrapper.querySelector(".plus"); const text = wrapper.querySelector(".text"); const text_2 = wrapper.querySelector(".text_2"); const text_3 = wrapper.querySelector(".text_3");
let number = 0; plus.addEventListener( "click", function() { number++; text.innerHTML = number; text_2.innerHTML = number; text_3.innerHTML = number; }, false );
>这种做法,显然是不符合我们的预期的,
>因为这非常影响我们的开发效率,
>而我们也仅仅是简单的增添了一些新的标签以及展示,我们都非得增加一大堆的代码。
***
#### 2.1 引入组件的状态
>「数据是什么,我们就展示什么」,这是我们最终的想要达到的成果,
>方法很多,但是最省代码的方式和最简单的方式就是:当我们数据发生改变的同时,我们根据数据的变化,重新渲染整个组件,那就简单多了。
>话非常的绕,我们的代码这么去写:
```ts
class Add {
constructor() {
//构造函数,设置state
this.state = {
number: 0
};
}
//重写setState方法,使得每次调用setState,就会重新渲染,更新组件
setState(NewState) {
const oldElement = this.wrapper;
this.state = { ...NewState }; //ES6语法,展开操作符
this.wrapper = this.render(); //重新渲染
//拿到新的组件,更新原来的组件
if (this.update) this.update(oldElement, this.wrapper);
}
createWrapper(string) {
const wrapper = document.createElement("div");
wrapper.innerHTML = string;
return wrapper;
}
onClick() {
//事件调用
let NewState = this.state.number + 1;
this.setState({
number: NewState
});
}
render() {
const domString = `
<div class="text">${this.state.number}</div>
<div class="text_2">${this.state.number}</div>
<div class="text_3">${this.state.number}</div>
<button class='plus'>
+
</button>`;
this.wrapper = this.createWrapper(domString);
const plus = this.wrapper.querySelector(".plus");
plus.addEventListener("click", this.onClick.bind(this), false);
return this.wrapper;
}
}
const wrap = document.querySelector(".wrap");
const add = new Add();
wrap.appendChild(add.render());
add.update = (old, next) => {
//给组件定义一个方法
wrap.insertBefore(next, old); //插入新的组件
wrap.removeChild(old); //去掉旧的组件
};
代码稍微有些长了,但是其实很好理解。我们最终达到的目的就是「当我们数据发生改变的同时,我们根据数据的变化,重新渲染整个组件」
我们现在已经自己开发出一个非常容易去复用的组件了, 但是当我们要开发另外一个组件的时候或许就要重新写一套这样的「复用」逻辑。 在我们的思想里,所有的组件都应该拥有「自动更新的能力」, 因此我们将组件根据状态,自动更新的能力抽取出来,那我们以后就非常方便了。
class ButtonComponent { constructor() {} createWrapper(string) { //document.createElement, DOM api,根据标签名字创建DOM元素 const wrapper = document.createElement("div"); wrapper.innerHTML = string; return wrapper; }
setState(newState) { const oldElement = this.wrapper; this.state = { ...newState }; this.wrapper = this.renderElement(); //渲染出元素 if (this.update) this.update(oldElement, this.wrapper); }
renderElement() { this.wrapper = this.createWrapper(this.render()); const plus = this.wrapper.querySelector(".plus"); if (this.onClick) { plus.addEventListener("click", this.onClick.bind(this), false); } return this.wrapper; }
render() {} //玩家需要重写的函数 }
>好了,逻辑抽象完成。
>还记得我们的React有一个ReactDOM.render方法吗?
>我们改一下名字直接叫做:
```ts
const renderToDOM = (component, DOMElement) => {
DOMElement.appendChild(component.renderElement());
component.update = (old, next) => {
//给组件定义一个方法
DOMElement.insertBefore(next, old); //插入新的组件
DOMElement.removeChild(old); //去掉旧的组件
};
};
最后呢,我们刚刚的组件,可以写成这样~
class Add extends ButtonComponent { constructor() { super(); this.state = { number: 0 }; } onClick() { //事件调用 let NewState = this.state.number + 1; this.setState({ number: NewState }); } render() { return ` <div class="text">${this.state.number}</div> <div class="text_2">${this.state.number}</div> <div class="text_3">${this.state.number}</div> <button class='plus'> + </button>`; } }
renderToDOM(new Add(), document.getElementById("wrap"));
>自此,我们的组件化就完成了。
>你可以看到,我们的组件其实外貌上已经非常像React,并且已经拥有了一个良好的组件复用能力。
***
#### 最后:我们为什么需要React?
>通过上述的一点一点分析,实现我们可以得知,实际上React的出现,已经完完全全的颠覆了传统的开发模式。
>我们无须再去:
>使用DOM API,一个一个的元素进行更新,操作
>组件复用非常的简单,几乎每次复用之前写的组件,就是1-2行代码就搞定
>组件内部状态,自己管理自己,跟外界完全无关
>实际上,所有的框架出现都是为了解决「开发者困难」的问题。试想一下,一个10万行的项目,整天都是一个一个的元素进行更新,操作,那请10个程序员都维护不过来。有了这些框架的帮助,我们就能够更好的开发。
>当我们站在这个角度去看前端框架的时候,我们就会发现,其实三大框架也不过就是:react牌锤子,vue牌锤子,ng牌锤子,他们都是锤子,以后就算有框架出来,也还是锤子....
只关心事情的样子。
通常用于排版和布局 typography and layout
经常只使用
props.children
[react]示例:
h1
、section
、div
、span
、Icon
(可能包含className
和可访问性属性)
关心事情的样子。
仅通过
props
接收数据和回调。除了 UI 状态之外,很少有自己的状态
没有生命周期方法
示例:头像、信息、列表 Avatar, Info, List
关注事物与展示组件的外观如何相同。
未连接到 redux,或拥有自己的 UI 状态以外的状态
可以在没有状态或连接到 redux 的情况下进行测试
可以有生命周期方法
通常与容器位于同一文件中
示例:
UserPagePresenter
、UserListPresenter
[react]
关心事情是如何运作的。
不使用样式
经常连接到 redux 或者有自己的状态
向展示组件或其他容器组件提供数据和行为。
示例:用户页面、用户列表 UserPage, UserList
例如,如果多个组件需要进行相同的数据获取和更新逻辑,可以将这部分逻辑封装在自定义钩子中。
import { useState, useEffect } from 'react';
function useUserData(userId) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`).then(response => response.json()).then(setUser);
}, [userId]);
return user;
}
function UserProfile({ userId }) {
const user = useUserData(userId);
return <div>{user ? user.name : 'Loading...'}</div>;
}
Components / HTML 组件开发